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! 😀

Symfony 2 and AngularJS: Server-side rendering of form data

Now that Javascript frameworks are starting to mature and be used by a lot more companies than a few years prior, the question often comes up: How can we use our current server side form rendering while at the same time populate our client side framework with that initial data?

Well, let’s first talk about the pros and cons and then I’ll give a brief example on how you can easily do this with Symfony 2 / Twig and AngularJS.

There’s basically two schools of thought. The first is the camp that believes the client side framework should do all rendering / populating of form data. This means that when a user loads your page, the client does an AJAX call to populate the initial data after the UI has already loaded. This school of thought definitely has some merit, especially from a business point of view because more times than not this makes the user experience faster. However, there’s also a downside with slow data loads when users load your page with low end systems / mobile devices and are waiting around with a blank application, no content, and a confused look on their face. This could (and many times does) violate the cardinal rule of don’t annoy your customers. User annoyance is a for sure way to lose a sale. There is also the issue of not being able to take full advantage of server side optimizations.

The second is the camp (which… *cough* *cough* I happen to belong to) is letting your initial data load with the server side rendering, and then initializing the client side framework with it. When your page loads, your users aren’t sitting around with a WTF feeling, because if your page is loaded, your data is there. The reason I say this is because we’re at a point with the internet that your average user understands a page load vs a non page load, but they definitely don’t have any concept of well I know the app loaded just fine, I’ve just gotta wait around a few seconds until the JSON response returns. Even when the content shows up, your customer more often than not thinks the application is some how broken, loses trust, and moves on. And this is definitely what you don’t want.

The downside to this approach is unfortunately many popular Javascript frameworks including AngularJS (as of this post) view this as going against best practices. My view and others I work with view server side rendering of initial data and seeding the client side framework with it as the best of both worlds. In fact, Twitter has even changed their opinion when it comes to this as well.

OK, enough with the rambling… Let’s see some code! 😀


NOTE:

This article will assume you already have an understanding of Symfony 2 / Twig, how to override in Twig, and at least a basic understanding of what AngularJS directives are and how they work.

On our page load, we pass our form field data to AngularJS via assigning an AngularJS directive as an attribute and let AngularJS repopulate our fields. We add our init directive to the form by overriding our block’s field widget. We don’t add it via attribute in our Form class before rendering, why is beyond the scope of this article. Adding the directive will give control of our rendered form data to AngularJS and then we can continue to work with it in AngularJS as we normally would.

First create a twig file to override and add


{# Content\ProfileBundle\Resources\views\Form\fields_angularjs_init.html.twig #}

{# override the forms media widget #}
{% block _content_profile_userGallery_media_widget %}
{% spaceless %}
    {% set attr = {'data-ng-model': 'userGallery.media', 'data-initialize-directive': ''} %}
    {{ block('form_widget') }}
{% endspaceless %}
{% endblock _content_profile_userGallery_media_widget %}


Next create an AngularJS directive and add


// Content\ProfileBundle\Resources\public\js\directives.js

// change to the way your app loads directives
profileAppDirectives.directive('initializeDirective', function($parse) {
    return {
        restrict: 'A',
        require: '?ngModel',
        scope: false, // change for your scope (you need to be in the correct scope)
    
        // ...

        // the linking function will assign to your init directive 
        // as an attribute in fields_angularjs_init.html.twig
        link: function(scope, element, attrs) {
            // ...

            $parse(attrs.ngModel).assign(scope, attrs.value);
    
            // ...
        }

        // ...
    }
}

You can also add attrs.value, attrs.checked, etc. and checking conditions for the different form fields you’re dealing with.

That’s it, have fun! 😀