Ever since I am using the Symfony Framework (be it version 1 or 2), I tend to describe every other project I've done (including those that were built on top of some third party "framework" like Joomla or WordPress) as a "legacy project". Though this has sometimes felt like treason, I still keep doing it: the quality of applications written using Symfony is usually so much higher in terms of maintainability, security and code cleanliness, that even a project done last year using "only PHP" looks like a mess and seems to be no good software at all. So I feel the strong urge to rebuild everything I have in portfolio (as do many other developers), but "this time, I will do it the right way".

I have come to the conclusion that this is a dream that will never come true. Lack of time and lack of money on the customer's side (who does not really get anything in return, unless he cares about clean code) is the main reason this endeavor will fail. Also: to decouple your legacy application is a lot more difficult than you would think. Usually, everything in the application has access to everything else, by means of global variables, superglobals, functions and static methods and variables. Removing them, can make very distantly related parts of your software break to pieces.

But not all is lost. The Symfony team has chosen to provide developers with lots of decoupled components, that can be used in a stand-alone way in any PHP project (as long as the server's PHP version is high enough). It is not just marketing, to speak of highly decoupled, reusable components, it is real. The perspective has become even nicer with the arrival of Composer, which manages your dependencies in a very thourough and yet easy way. Combining a service container, Symfony's EventDispatcher, HttpFoundation, HttpKernel and Routing component, would allow you to quickly create a web application, which creates an appropriate Response for each Request.

Of course, we don't have to invent everything ourselves: we could use Silex, a microframework built with the aforementioned Symfony components. This article will show you how you can take some major steps in elevating your legacy application and making it run another ten years. Let it be not-so-legacy anymore!

Taking a look at a legacy controller

Browsing through some older project's of mine, I see many front controllers like this:

// index.php
$uri = $_SERVER['REQUEST_URI'];

// the database connection will be created here
// if it fails, it does: die('No database connection');
require 'db.php';

$success = false;

include 'header.php';

if ($uri) {
    $controllerFile = __DIR__ . $uri . '.php';

    if (file_exists($controllerFile)) {
        require $controllerFile;
        $controller = trim($uri, '/');
        if (function_exists($controller)) {
            $success = true;
            $controller();
        }
    }
}

if (!$success) {
    ?><p>Page not found.</p><?php
}

include 'footer.php';

What this does is:

  1. Remove all slashes from the beginning and end of the request URI

  2. See if there is a file whose name matches the URI

  3. Include the file

  4. If a function now exists with the name of the URI, execute it

The controller is expected to output content directly, and possibly set some custom headers or redirect the request itself. The output gets wrapped inside a "header" and a "footer", i.e. everything above <body> and everything after </body>.

For instance, after making a request for "/edit_category", the function edit_category() in edit_category.php will be executed. The code below shows you what you might find in a file like this:

// edit_category.php

function edit_category()
{
    $warning = '';
    $values = array();

    if (isset($_POST['save'])) {
        $values = $_POST;
        if ($values['name'] == '') {
            $warning .= 'Please provide a name';
        }

        if ($warning == '') {

            if (isset($values['id'])) {
                $sql = "UPDATE categories SET name='{$values['name']}' WHERE id='{$values['id']}';";
            }
            else {
                $sql = "INSERT INTO categories SET name='{$values['name']}';";
            }
            // assume that a database connection exists
            $result = mysql_query($sql) or die(mysql_error());

            header('Location: /');
            exit;
        }
    }
    else if (isset($_GET['id'])) {
        $result = mysql_query("SELECT * FROM categories WHERE id={$_GET['id']};");
        if ($result && mysql_num_rows($result)) {
            $values = mysql_fetch_assoc($result);
        }
    }

    if ($warning != '') {
        ?><p class="warning"><?php echo $warning; ?></p><?php
    }

    ?>
    <form action="/edit_category" method="post">
        <?php if (isset($values['id'])) { ?>
            <input type="hidden" name="id" value="<?php echo $values['id']; ?>" />
        <?php } ?>
        <p>
            <label for="name">Name:</label>
            <input type="text" name="name" id="name" value="<?php echo (isset($values['name']) ? $values['name'] : ''); ?>" />
        </p>
        <p>
            <input type="submit" name="save" value="Save" />
        </p>
    </form>
    <?php
}

There we have it! These simples lines allow you to create or edit a category. It uses the deprecated mysql_*() functions. It uses the superglobals $_POST and $_GET. It redirects itself using a call to header() (then, don't forget to call exit(), otherwise the remaining code would be still executed!).

By this time, you will be thinking: "Make it stop, please!" Well, let's make it stop. But one step at a time. I am not going to rewrite this application, since I have many controllers like this.

Introducing Silex in a legacy application

It seems to me like a really good idea to add Silex to this legacy application and let it handle all requests. This will give you:

  • HTTP abstraction: Silex wraps the superglobals in objects which are much more cleaner to handle

  • Routes and controllers: Silex allows you to map URI's to closures

  • A service container: Silex is itself a service container (it extends the very elegant service container Pimple)

  • Many service providers: for Twig (templating), Security (authentication and authorization), Translation, etc.

Let's create a composer.json file in the root of the project with the following contents:

{
    "require": {
        "silex/silex": "1.*"
    }
}

Then (assuming you have installed Composer on your machine), run composer install and the necessary dependencies will be installed in the /vendor directory.

Remember to require the generated /vendor/autoload.php file in your application's front controller, so all the available classes can be found.

Now, wrapping existing legacy controllers is easy, it can be accomplished by replacing the code in the old front controller with the following:

require __DIR__ . '/vendor/autoload.php';

require __DIR__ . '/db.php';

use Silex\Application;
use Symfony\Component\HttpFoundation\Response;

$legacyController = function($controllerName) {
    return function(Application $app) use ($controllerName) {
        require_once __DIR__ . '/' . $controllerName . '.php';

        ob_start();

        // we pass the Silex Application to the controller
        $result = $controllerName($app);

        if ($result instanceof Response) {
            ob_end_clean();

            return $result;
        }

        $body = ob_get_contents();

        ob_end_clean();

        return new Response($body);
    };
};

$app->match('/edit_category', $legacyController('edit_category'));

$app->run();

A few notes: first, we pass $app to the controller. This is an instance of Silex\Application, and thereby of \Pimple, the light-weight service container. When trying to decouple your legacy application, you should add services to the container, for instance:

$app['service_name'] = $app->share(function() {
    return new SomeService;
};

Now when you pass the $app variable to the legacy controller, all the new and shiny services will be available in your legacy code too.

Second: remember that the legacy code uses no output buffering by itself, so we have to wrap the call to the controller using ob_start() and ob_end_clean(). We catch the output of the controller and wrap it inside a Response object.

Third: the legacy controller is also allowed to return a Response object. This way, from inside the controller, you might do something like:

return $app->redirect('/');

Which returns a RedirectResponse. This means you can get rid of calls like header('Location: /').

Using Twig for templates

Once you have worked with Twig, you will want to have it in your legacy application too. As we have seen earlier: in the edit_category() controller, there is no real separation between the controller, the model and the view. But we can take one step towards a better life and allow for the existence of Twig code in the output of the controller. To be able to do this, we should add the TwigBridge as a dependency to composer.json, then run composer update to fetch the TwigBridge and Twig itself.

{
    "require": {
        "silex/silex": "1.*",
        "symfony/twig-bridge": "2.1.*"
    }
}

Using Twig and a base template also allows you to get rid of the ugly

include 'header.php';

You should add Twig as a service by mounting the TwigServiceProvider. Also, since we want to use strings as templates (not just files), the twig.loader service should be extended: the \Twig_Loader_String loader should be added to the loader chain:

require __DIR__ . '/vendor/autoload.php';

require __DIR__ . '/db.php';

use Silex\Application;
use Silex\Provider\TwigServiceProvider;
use Symfony\Component\HttpFoundation\Response;

$app = new Application();

$app->register(new TwigServiceProvider, array('twig.path' => __DIR__ . '/views'));
$app['twig.loader'] = $app->extend('twig.loader', function(\Twig_Loader_Chain $loader, Application $app) {
    $loader->addLoader(new \Twig_Loader_String());

    return $loader;
});

$legacyController = function($controllerName) {
    return function(Application $app) use ($controllerName) {
        require_once __DIR__ . '/' . $controllerName . '.php';

        ob_start();
        $result = $controllerName($app);
        if ($result instanceof Response) {
            ob_end_clean();

            return $result;
        }
        $body = ob_get_contents();
        ob_end_clean();

        $template = <<<EOF
{% extends "base.html.twig" %}

{% block body %}
$body
{% endblock body %}
EOF;

        return $app['twig']->render($template);
    };
};

$app->match('/edit_category', $legacyController('edit_category'));

$app->run();

Finally, we should create a /views/base.html.twig file, containing something like this:

<html>
<head>
    <title>{% block title %}{% endblock title %} - Legacy App</title>
</head>
<body>
{% block body %}
{% endblock body %}
</body>
</html>

And now, any output from the legacy controller will be rendered inside the base template.

In my next post, I will show you how to easily (and very cleanly) decouple model related actions in legacy controllers.

One last word of advice: when you are refactoring your old application, don't try to change too much at once - remember, your client has a web application that runs very well. There is a real danger that after all your refactorings, he doesn't.

PHP Silex Twig legacy code controller
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
Mathieu Imbert

Great article, thank you for sharing. I have this project that is using our legacy framework, which relies heavily on singletons. I want to implement unit testing, but I'm unable to because of all the static dependencies. I don't want to rewrite everything using Symfony or Laravel, but this seems like a perfect alternative: start by wrapping the code inside a silex container, then build from there.

Thanks!

Przemysław Lib

Good info!

Some advice on what to do when even front controller is absent?
Switching whole thing at once may be a bad thing. So writing instead some proxy that can be configured to forward some URLs to original files, and some to this new front controller?

Ivan G

I friend of mine just started to work into a Artificial Intelligence and they unfortunatelly use lot of legacy code (of course you should read spaguetti-code). After hear about his intention to leave the company because that my suggestion was to start introducing silex to let the whole project begin adapting their code smoothly. It has been very nice to see an expert like you giving the same advice by the exactly the same reasons, the adoption costs and the customer feeling of a null return of investment.

Oh! by the way: Very interesting your speak yesterday on Barcelona. Thank you for share your ideas.

Guest

Lets keep the big SQL Injection vulnerability hanging around!

Pierre Lhoste

This is truly an excellent post.

I am in the same situation as you are : legacy PHP apps, not enough separation between logic and page rendering. And total lack of easy routing. I shun symfony because it's too much abstraction, and i'm the functional type, not a huge fan of OOP in general. But as you keep showing by example, Silex + Composer is a whole different business. You take what you need.

From my experience, what most people look for when trying to migrate to a framework (symfony, codeigniter and pals) is first some front facing controller to help routing. Then, some way of isolating the 'HTML' template parts so that the web-designers don't f*** up their code and only touch those files they're allowed to touch ;)

But I do also get Nicolas' point. I remember Rich Hickey saying something in the lines of 'stop using the stupid ORM and begin learning SQL and ODBC', and similarrily, (maybe it was Isaac Schuelter or T.J Hollowaychuck) about javascript, that coding with Jquery will never replace deep knowledge of javascript, or Ryan Dahl stating (with huuuuge nods from a happy Douglas Crockford) that Garbage collectors were too much of an abstraction when coding node and it was the main reason he stuck with C most of the time (this one is extreme, i reckon!).

And I do agree with that : there's a danger inherent with relying too much on any framework, especially because it adds a second layer of 'things will break when you update' (1 : the base language, 2 : the framework).

Anyway, thx for sharing this process, this post is one of the best on this real life problem : wrapping legacy code while making bulk-legacy code refactoring non-compulsory.

Matthias Noback

Hi Pierre,

Thanks for taking the time to write! It's very nice to hear that the new "composer" approach to building web applications inspires developers to this: taking what you need, putting all together and thus write your own beautiful symphony ;)

I am not a proponent of rewriting stuff, just to make it work with a certain framework. It will not help you any further (even more in the opposite direction I think). Yet, some tools make things so much easier, that it really pays to use just a few Symfony Components (or use Zend, for that matter). For instance, I have "wrapped" an existing e-commerce site I've written myself from scratch, quite a few years ago, and made it multi-lingual in 24 hours. This would have been a terrible amount of work, when just writing "plain PHP". But with Silex, Twig, Doctrine DBAL and the Symfony Translator, it got a lot easier. This also allowed me to make some other improvements to the codebase, but otherwise I left it intact. When releasing the upgraded site, very few bugs occurred (and I quickly found them, because they were nicely recorded by Monolog).

This does not mean that I would have liked to have these tools at my disposal from the beginning. I have learned a lot about web development, and now that I know of the fundamental issues and pitfalls, I have come to like Symfony and the likes, because they let me skip the now "boring" parts. I agree with your point about things breaking when updating, and also about not knowing the techniques behind the friendly tools. I see this too often, developers trying to "get things done" with a framework, failing, and needing a lot of time to understand the real problems at hand. Many times, these problems arise from a lack of understanding PHP as a language, or from mis-using MySQL, or even MongoDB (of which I recently was a victim too). The framework itself also needs thorough understanding, to be able to use it at its maximum potential. My naive solution is: developers should get certified. PHP, MySQL, Symfony, etc. Taking exams on these subjects forces you to read through big chunks of the manuals, which greatly enhances awareness of problems, solutions and best practices. Of course, you would not have to pay hundreds of euro's for this, you can just take the time yourself, or even better: be forced to take the time (preferably by some boss ;)).

Thanks again!

Matthias Noback

This is why I like the Symfony way so much; Symfony really is a set of components that "know" everything about the common tasks of PHP developers. So, of course, at the end of the day we are just looking at the URI and query or request parameters, and echo something in response to that. Meanwhile, there are frameworks or components to make this much easier and also (more importantly, since most of these tools I can develop myself) prevent me from making the same mistakes that developers of all time and all places have made before me. So, there is input filtering and validation, output escaping, CSRF protection, you have well thought-through and implemented authentication and authorization of users, a standard way of working with your site's configuration. Of course, things like the Form component, or Twig, you can do without. This is what Silex does, it picks a few things from the Symfony stack, and gets you going with routes and controllers, so you have a flying start.

Also, from a collaboration point of view: it is a good idea to work with an (open source) framework, since other developers will be able to learn it quicker than by reading your own framework-like code, trying to figure out what you were trying to do.

So, thanks for commenting on this. I hope this gives you some of the answers.

Nicolas B.

Hi, I stumbled on your post completely by random. I was looking for news on Symfony and got caught reading your post. This is a rant against php frameworks and a question I'd like to ask. I am getting very very critical of php frameworks. I feel there are to many. And they are constantly reinventing the wheel. They rarely help me to code faster. And they don't help me to code better. They just structure my code differently.

To put it simply, I ask, seriously, what is the real benefit of not using $_POST, $_GET, header() and all ? I feel most frameworks are just name wrappers around standard php functions or fancy chain of responsibility patterns that doesn't do that much except parsing url and routing request to a controller class.