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 controller