Adding Symfony 2 Routing to your legacy PHP application

Recently I’ve had to rewrite portions of a PHP legacy application that was written in a company’s proprietary legacy framework, in order for it to be test friendly and much more maintainable.

In order to accomplish this, I’ve added a few key Symfony components to their framework. The result was that the application was easier to test, easier to cache, and much much easier to maintain than it was previously.


Let’s take a look.


Their front controller was stripped and completely rewritten to implement Symfony’s HTTP Foundation and Symfony’s Routing.

Let’s take a look at a snippet of the front controller.
New Front Controller


include '../vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RouteCollection;
use LtApp\LtAppRoute;
use LtApp\LtAppKernel;

$routeCollection = new RouteCollection();
// inject the Symfony 
// route collection to our 
// App Route class (shown below)
$ltAppRoute = new LtAppRoute($routeCollection); 
$routes = $ltAppRoute->getRoutes();

// using a dependency injection container
// will be covered in another tutorial
$diContainer = include __DIR__.'/../app/LtAppContainer.php';

// create our Request object
$request = Request::createFromGlobals();

// Retrieving the LtAppKernel 
// service from my DI container.
// If you're not using a DI container, 
// you can just instantiate your version 
// of AppKernel (shown below) in place of this.
// Our response object is then returned 
// to the client.
$response = $diContainer
    ->get('ltAppKernel')
    ->handle($request);

// enable caching for our response
//$response = $diContainer
    ->get('ltAppKernelWithCache')
    ->handle($request);

// phone home
$response->send();

exit;


Now let’s take a look at our App Kernel.

Our App Kernel is the engine that will dynamically instantiate our application’s Routing and Controller objects. It is responsible for asking our matcher if the incoming Request object contains a URL string with a valid route, and if so, resolving the correct controller for that valid route.

Let’s take a look at a snippet of our App Kernel.

App Kernel


namespace LtApp;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;

class LtAppKernel implements HttpKernelInterface
{
    private $matcher;
    private $resolver;
    private $dispatcher;

    // Injecting in our Url Matcher
    // and Controller Resolver.
    // Again this injected from my DI container. 
    // If you aren't using a DI container, 
    // just pass the interface objects as 
    // arguments when you instantiate 
    // your App Kernel from inside
    // your front controller.
    public function __construct
    (UrlMatcherInterface $matcher,
     ControllerResolverInterface $resolver)
    {
        $this->matcher = $matcher;
        $this->resolver = $resolver;
    }
    
    // Our handle method takes an 
    // HTTP Request object, 
    // the Symfony Http Kernel
    // so we have access 
    // to the master request, 
    // and a catch flag 
    // (we'll see why below)
    public function handle(
        Request $request, 
        $type = HttpKernelInterface::MASTER_REQUEST, 
        $catch = true)
    {
        // Next we take our HTTP request object and 
        // see if our Request contains a 
        // routing match (see our routes class 
        // below for a match)
        try {
            $request
            ->attributes
            ->add(
              $this
                ->matcher
                ->match(
                  $request
                    ->getPathInfo()
            ));

            // Our request found a match 
            // so let's use the Controller
            // Resolver to resolve our 
            // controller.
            $controller = $this
                           ->resolver
                           ->getController($request);
            
            // Pass our request arguments 
            // as an argument to our resolved 
            // controller (see controller below).
            // If you have form data, the resolver's
            // 'getArguments' method's functionality
            // will parse that data for you and 
            // then pass it as an array to your 
            // controller.
            $arguments = $this
                          ->resolver
                          ->getArguments(
                             $request, $controller);

            // Invoke the name 
            // of the controller that
            // is resolved from a match
            // in our routing class
            $response = call_user_func_array(
                          $controller, $arguments);

        } catch (ResourceNotFoundException $e) {
            // No such route exception 
            // return a 404 response
            $response = new Response(
                'Not Found', 404);
        } catch (\Exception $e) {
            // Something blew up exception
            // return a 500 response
            $response = new Response(
                'An error occurred', 500);
        }
        
        //**Note:
        // If you need any event listeners for specific
        // actions to take on your response object 
        // after returned from your business logic 
        // / controller and before you return 
        // to the client you can invoke them here.
        
        return $response;
    }
}

Now let’s take a look at the routing class that our App Kernel was
calling.
Routing


namespace LtApp;

use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

class LtAppRoute
{
    private $routes;

    // pass in Symfony's 
    // Route Collection object
    public function __construct(
      RouteCollection $routeCollection)
    {
        $this->routes = $routeCollection;
        // ignore routeConfig
        // this I'm using for
        // something else
        $this->routeConfig(); // decouple later
    }

   
   // We register a route by
   // invoking the add method
   // with specific arguments.
   // The first argument is a 
   // unique name for this route.
   // Next we instantiate a Route
   // object with our route.
   // Our route is /docs/{ID}
   // Our associative array contains 
   // the controller that we
   // want to resolve with this route,
   // DocumentController and the 
   // method on that controller
   // that this particular route 
   // invokes viewDocumentAction.
   // separated by the :: 
   // token.
   // We can also pass in arguments
   // that contain a default value
   // if needed, and they will be
   // available in our method's 
   // parameters.
    private function routeConfig()
    {
     $this->routes
     ->add(
      'document_view', 
      new Route('/docs/{documentId}', 
      array(
      '_controller'=>
        'Lt\\DocumentPackage
           \\Controller
           \\DocumentController::
             viewDocumentAction',
      'action' => 
        'documents', // default value
      'documentId' => 
        '1' // default value
     )));
}


DocumentController (our resolved controller)


class DocumentController
{
  // ...
  // ...
  public function viewDocumentAction(
    Request $request, 
            $action, 
            $documentId)
  {
     // ... Grab services and models and
     // ... perform cool business logic
     $response = new Response();
     
     // return response object
     // back to App Kernel
     return $response;
  }
}


And there you have it. As you can see, this design decouples a lot of functionality from the application, which in this case, makes writing tests actually possible, where before automated testing wasn’t really possible.

And.. If you combine this with a Dependency Injection container, the result is even more flexibility and a code base that is much more maintainable.

Have fun coding! 😀

Leave a Reply

Your email address will not be published. Required fields are marked *