A Drupaler in Symfony Land

To say I've spent a lot of time working on Drupal 8 over the last 21 months would be a bit of an understatement. The Plugin System & the Blocks & Layouts Initiative have consumed much of my professional and personal time over that period, and we've worked on a lot of really awesome and interesting stuff. That being said, the vast vast majority of that work was still really "Drupal" and certain aspects of the underlying architecture that we were building on I didn't have the time to learn in detail. I have relied on the invaluable knowledge and insights of 3 individuals in this regard, namely Larry Garfield (Crell), Sam Boyer (sdboyer) and Sebastian Siemssen (fubhy). However, their guidance could only get me so far, and at some point I needed to go learn this for myself. Starting shortly before NYC Camp, I began working through Fabien Potencier's excellent 12 part series on using various Symfony components. If you've not read through it, it's well worth the effort. The most up to date version can be found here: https://github.com/fabpot/Create-Your-Framework/tree/master/book

In the series, Fabien walks the user through many basics of Symfony's routing system and a few more complex use cases (such as http caching and phpunit tests). I had made my way through the series once before when we initially adopted Symfony, but I was a little newer to object orientation than I'd like to admit, and I also didn't have the tools at my disposal, development wise, that I have now. Coming at the series again with a lot more experience with OO and a few tools (such as Drupal's plugin system) available to me, not to mention a little bit of working knowledge I picked up with regard to Symfony's event system, I felt I could dig through what Symfony was doing and that that would help me to interpret what Drupal's doing. All in all, I think this was a very valuable exercise, and I intend to continue it with regard to various other aspects of D8 that I've had issues with. However, I'll save that for another blog.

We've had a lot of people in the community, myself included, complain about certain aspects of what's going on specifically in the routing system (amongst other areas). I can't say that I'm a fan of the entire stack (yet) but I have dissected a lot of the basic components and am coming around to Symfony's approach. What follows are some observations about this approach, and what I hope will give other developers a place to start w/o having to learn this all for themselves. This will be approached w/o much in the way of Drupal specific context. Drupal is wrapping these same concepts in a few other function calls & abstractions, and in order to keep this simple, I'll be avoiding them. The concepts all still apply.

The Class Autoloader

There's not a lot to discuss here. We have a class autoloader, it happens VERY early in the stack. Drupal does a few things surrounding this, but for the sake of explanation, you could literally include the autoload.php file generated by Composer and bingo, you'd have the ability to load any class in any available PSR-0 namespace by simply saying "new ClassName();". Autoload implementation details can change, and there are many different autoloaders in the greater php world that conform to the PSR-0 spec. For the sake of following what I'm describing I'd encourage you to seriously consider using Composer's provided autoloader.

Composer?!?

So here I am, discussing composer, and I've not actually told you what it is. For those not in "the know" it's a dependency management tool for PHP projects. It's very similar to Drupal's .info file architecture, except it uses a composer.json file in the various packages to specify what dependencies it has. It can offer suggested packages that are not dependencies but that you might want to consider using, authorship, and various other details. Composer is run from the command line, and it reads these json file and then builds dependencies, installs them all, and then generates the basic details required to get a class autoloader up and running. Drupal 8 has a composer.json file in the root directory and there are issues on d.o to get us switched over from Symfony's autoloader class to Composer's. Many php projects outside of Drupal have adopted Composer, here are some composer related links worth reading:

Drupal: Use the Composer autoloader to make everything simpler
Composer Homepage
Packagist Repository of Composer Projects

The Dependency Injection Container

Also often called a "Service Container" or "DIC" we want to talk about this early on. A dependency injection container is an "Inversion of Control" technique which essentially ask you as a developer to document for the system as a whole how classes you're providing are to be instantiated. Specifically it's asking, what other classes instance you might depend on to work. In order to understand this fully (if you don't already), you'll need to do a fair amount of reading, experimenting and failing. That's not to discourage you , because it's definitely a worth while exercise, but being realistic, you'll probably need a couple of failures before you can really begin to appreciate how to use the system appropriately. At its simplest, I'll use the example of a Database Query. In order to query the database through a query builder object, that object will need a database connection object first. Today we provide a bit of this through globals in Drupal, but speaking for a puristic standpoint, that's suboptimal. What we really want to do is define connection objects somewhere, and then inject them into our query builders so that you can have different builders for different database connections. Objectively this makes sense, your milage might vary a bit if you try to implement it ;-)

This same concept applies to many classes. You want to inject dependencies into classes and then use the dependencies that were injected. You don't want to reach out of the scope of the class and into some global user space, and call arbitrary functions. This is of course untrue for any raw php function, but if you were in a class, you certainly wouldn't want to be calling drupal_json_encode() for example. Rather, if you need access to that, you should be injecting the Json class from the Drupal\Component\Utility library and making use of its method for that same purpose. Conceptually, this hopefully makes sense.

It's important to discuss the DIC because once we understand this concept, it logically follows that many of the classes we'll be dealing with regularly exist as services within it. I'd specifically like to draw your attention to the Symfony\Component\HttpKernel\HttpKernel. (If you're looking for this in Drupal, we've subclassed it as Drupal\Core\HttpKernel and it resides at the service id "http_kernel")

The HttpKernel

For the sake of simplicity, I'm going to focus on a very limited aspect of the HttpKernel, specifically the handleRaw() method & the KernelEvents::REQUEST event, but before we go there, it's worth discussing how we got here. My example index.php is dead simple:

<?php 

$container = require_once __DIR__ . '/../src/container.php';

use Symfony\Component\HttpFoundation\Request;

$request = Request::createFromGlobals(); 
$response = $container->get('kernel.cache')->handle($request)->send();

?>

What happens here? well, first we include the Dependency Injection Container. For reference, the DIC in this case is including the autoloader as its first action, so in our first two steps we have an autoloader working, and a container that we can begin working with. We then use Symfony's excellent Request class and get a request object from the current global settings. There's a lot of awesome stuff that happens behind the scenes here and I'm not even going to begin dissecting it. If you're interested though, it's well worth your effort. From there, we take the Request object and pass it directly to the kernel's handle() method. Ignore for a moment that that says we're getting the kernel.cache from the container. That's all true, but sort of irrelevant, just focus on HttpKernel. If you happen to have walked through Fabien's 12 part series I linked to earlier, you'll notice this is a little odd. In his series, we end up doing route matching before we hand off to the kernel, however, WHEN we do route matching it's important to note that we're doing little more than populating some attributes of the request object. That object continues on into HttpKernel, which ultimately resolves the _controller that was appended during matching, or attempts to match if no match has yet been found and then resolve the _controller. Knowing this is half the battle to understanding what's going on here.

Route Matching

How we match the route, while being generally important, doesn't matter for understanding the code flow. WHERE we match it is more important. Delaying matching until we're in the HttpKernel itself allows us one very very big advantage over doing it before hand... multiple routers. In Drupal's case, we're using the Symfony provided Symfony\Component\HttpKernel\EventListener\RouterListener class. This class checks to see if someone else has already populated the request with a _controller. If it hasn't been populated yet, then it attempts to do so with the Matcher that was injected into it (through the DIC). What is perhaps more important here is HOW this class is invoked. Drupal's very very used to using various info hooks, general hooks and alter hooks. To a certain degree Symfony's Event Dispatcher covers the later two of these use cases simultaneously.

Event Dispatching

So, if we have an event dispatcher, we can fire a unique string through that (much like our hooks) and format an Event class to go along with it. Events generally are injected at construction with the various classes you expect anyone listening to your event to need (might include the request object, any available response object, etc) and various getters for getting these instances out. Since it's all OOP, all classes are passed by reference, so this sort of ends up being like a generic hook and an alter hook simultaneously. We can perform tasks based upon the information that was passed to us, and we can also alter the information we have. Getting back to the case of our Routing, we're using a very specific event for this, the KernelEvents::REQUEST event. This is defined as a class constant so that if the string changes, no one has to update stuff (so use the constant in your listeners). For routing, this event fires, it passes along the request object, and the RouterListener looks at that Request object and alters it as necessary or returns if a controller is already set.

So wait, what was the point?

Well... Events have a priority associated with them (the opposite of how we think about weight in Drupal, bigger numbers happen earlier), and if an event is being used in order to perform routing (and it is) then that means we can actually hijack the entire routing mechanism before it even attempts to route, and provide our own facility for it (or fall through if our routing mechanism fails, and let Drupal keep doing what it does). In terms of your average Drupal project, perhaps this isn't super useful, but if you think of it in terms of integrating a completely different application, seamlessly with your Drupal site, this holds a LOT of promise. Especially if it's an application that could make use of existing Drupal classes and methodologies. You are LITERALLY giving yourself an entry point to do whatever you want before Drupal does, and if you return a Response object here, then you're actually preventing Drupal from every doing anything, all while having access to all of Drupal's services, and non-routing-specific architecture.

Most examples I've seen of using this Event in Drupal have implied that it's a replacement for hook_boot, but it's really not. Sure we can use it in terms of needing something at about the same bootstrap level (I want to dsm some message on every page, or whatever the use case it). But this event actually hands off the current request object, and what HttpKernel is looking to get back is a Response object (calling the setResponse() method on the event and passing a Response object through it will succeed at this). It has fallbacks in case it doesn't get a Response object back (and in Drupal's case, it never does from this event).

My specific use case involves Routes as Plugins & a Plugin Manager and a custom listener that resolves the request through that Plugin Manager and ultimately resolves the controller by loading the plugin instance and passing back the Response object that instance generates, but if you want to do something really simple to see how this works, you could just do:

<?php 

/**
 * @file 
 * Contains Drupal\routing_hijack\EventListener\RouterListener. 
 */

namespace Drupal\routing_hijack\EventListener; 

use Symfony\Component\EventDispatcher\EventSubscriberInterface; 
use Symfony\Component\HttpKernel\KernelEvents; 
use Symfony\Component\HttpKernel\Event\GetResponseEvent; 
use Symfony\Component\HttpFoundation\Response; 

class RouterListener implements EventSubscriberInterface { 

  /**
   * {@inheritdoc} 
   */ 
  public static function getSubscribedEvents() { 
    return array(KernelEvents::REQUEST => array('onKernelRequest', 33)); 
  }

  public function onKernelRequest(GetResponseEvent $event) {
    $request = $event->getRequest();
    $path = $request->getPathInfo();
    $path = trim($path, '/');
    list($first) = explode('/', $path);
    if (!in_array($first, array('admin', 'contextual', 'toolbar'))) {
      $response = new Response('this is a test');
      $event->setResponse($response); } 
    } 
  } 

}

?>

I've set the priority for this EventListener to 33. The HttpKernel in Drupal's implementation (and most Symfony implementations as I understand it) is set to 32. That means I get an earlier priority and can response to any request before Drupal even thinks about routing. I've packaged up the code in a drupal.org sandbox for others to play with. You can find it here: https://drupal.org/project/2068457/git-instructions

Happy Drupaling

Eclipse

PS: With all the recent "bad news" around various Drupalers and their status in our community, I thought some time and effort to explain the actual code and what's happening in it might be generally useful to the community. I know working through this and coming to understand it has certainly allayed a lot of my own fears. That's not to say we don't have any problems, but I do feel like Symfony was a very good choice and I'm beginning to feel empowered by it.

PSS: Symfony DX tip. If you're used to grepping for hook implementations, Symfony's event system can work the same way, just grep for the actual class constant that's being used to fire the event. In the case of my examples here grepping for "KernelEvents::REQUEST" should yield some interesting results.

Return to Zero? (not verified)

19 August 2013

and I'm beginning to feel empowered by it.

Given the timelines, this isn't at all inspiring. Or reassuring.

19 August 2013

Given the timelines, this isn't at all inspiring. Or reassuring.

This is not entirely unfair. Still, what I was trying to outline was a lot of the fear around Drupal 8, especially from a development experience perspective, is overstated. The ability to do do what my blog outlined means that you could leverage the D8 stack in a custom application that still runs in the typical drupal space w/o having to start from scratch. And I think that ability potentially forgives a lot of other short-comings (real or imagined).

Eclipse

fubhy (not verified)

19 August 2013

Thanks for this blog post. It's very refreshing to read a positive post about D8 development these days.

29 August 2013

I found this post very inspiring. D7 to D8 is a lot to take in and this post provides a great overview and useful links.

Thanks Kris,

Michael K

29 August 2013

Thanks for putting together this bog post and example of jumping in to the router early on.

All the OOP and knowing what classes do what and where is going to take some getting used to - I feel thats what might scare/worry/concern existing Drupal developers looking towards D8... maybe.

Certainly feels like an overwhelming amount to take in at some point!

Add new comment

The content of this field is kept private and will not be shown publicly.