Listen up Drupal!

Robert Slootjes


  • Who am I?
  • Some History
  • Symfony Goodies
  • Controllers Redefined
  • API Building
  • Bonus
  • Conclusions
Who am I?

PHP Developer at MediaMonks

“MediaMonks is a global creative production partner. We work with the world’s leading agencies, biggest brands and innovative media and technology companies.”

Founded in 2001, 600 Monks, 10 Offices

2006 Multiple custom frameworks

2008 Zend Framework

2012 Composer & Packagist

2013 Silex

2015 Symfony Framework


no drupal?

2016 Drupal

Dad, Gamer, DJ, Beer Snob™


Create scalability

Combine strengths

Get people of their islands

Look beyond drupalisms

Make Drupal look like Symfony Framework ;-)

Some History

Lot's of different backend languages
with their own frameworks

PHP Ruby .NET Node.JS Python

And don't forget about the frontend

Javascript iOS Android Unity Flash

APIs were a mess


Data Status Codes Error handling

Pagination Form Handling

Rest API Spec

A collaboration between all dev department leads

Making it easy for PHP devs

Event Subscriber for Silex

Return anything from a controller

Throw exceptions anywhere

Single file, no tests...

Ported to a Symfony Framework bundle

No longer a single file


Properly tested

Show us some code!

Basic Usage

class ExampleController {
    public function integerAction() {
        return 42;

    "data": 42

Basic Usage

class ExampleController {
    public function stringAction() {
        return 'foobar';

    "data": "foobar"

Basic Usage

class ExampleController {
    public function arrayAction() {
        return ['foo', 'bar'];

    "data": ["foo","bar"]

Custom Status Code And Headers

use Symfony\Component\HttpFoundation\Response;

class ExampleController {
    public function symfonyResponseAction() {
        return new Response('foobar'
                ['X-My-Header' => 'My Value']


class ExampleController {
    public function exceptionAction() {
        throw new \Exception('Foo');

    "error": {
        "code": "error",
        "message": "Foo"

Symfony Http Exceptions

class ExampleController {
    public function notFoundExceptionAction() {
        throw new NotFoundHttpException('Entity not found');

    "error": {
        "code": "error.http.not_found",
        "message": "Entity not found"

Symfony Form Exceptions

class ExampleController {
    public function formAction() {
        /** @var Symfony\Form $form **/
        throw new FormValidationException($form);

Form Exceptions

    "error": {
        "code": "error.form.validation",
        "message": "Not all fields are filled in correctly.",
        "fields": [{
            "field": "#",
            "code": "error.form.validation.csrf",
            "message": "The CSRF token is invalid."
        }, {
            "field": "task",
            "code": "error.form.validation.not_blank",
            "message": "This value should not be blank."


But what about Drupal?

Symfony Goodies
“Drupal 8 is using Symfony Framework”


Symfony Components !== Symfony Framework

Symfony Framework bundles ≈ Drupal modules

Bundles and modules are not compatible

“Symfony is a set of reusable PHP components...”

The standard foundation on which the best PHP applications are built. Choose any of the 50 stand-alone components available for your own applications.

“... and a PHP framework for web projects”

Speed up the creation and maintenance of your PHP web applications. End repetitive coding tasks and enjoy the power of controlling your code.

So what components does Drupal 8 use?

  • HttpKernel
    • HttpFoundation
    • EventDispatcher
  • Routing
  • DependencyInjection
  • YAML
  • ...


Converting a Request into a Response

use Drupal\Core\DrupalKernel;
use Symfony\Component\HttpFoundation\Request;

$autoloader = require_once __DIR__.'/../vendor/autoload.php';

$kernel = new DrupalKernel('prod', $autoloader);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$kernel->terminate($request, $response);

The HttpKernel Component

Hook into the kernel with EventSubscribers

A powerful method to change behavior

What kernel events are there?


  • Add more information
  • Check something in the request
  • Get something from cache
  • Directly return a response


  • Allows setting/updating the controller callable
  • Initialize services


  • Transform non-response object return value into a response object


  • Add headers
  • Store in cache
  • Inject content
  • Serialize


  • Convert into a response object
  • Change exception


  • Perform "heavy" action after response is sent
  • Useful with PHP FPM

Creating an EventSubscriber

use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class CustomHeaderSubscriber implements EventSubscriberInterface {

  public function onResponse(FilterResponseEvent $event) {
    $event->getResponse()->headers->set('Custom', 'Drupal <3 Symfony');

  public static function getSubscribedEvents() {
    return [KernelEvents::RESPONSE => ['onResponse']];

Setting a priority

public static function getSubscribedEvents() {
    return [
        KernelEvents::RESPONSE => [
            ['onResponse', 100]

Higher priority = earlier execution

Register the service

    class: Drupal\acme\EventSubscriber\CustomHeaderSubscriber:
      - { name: event_subscriber }

Register the service, the new way

      - { name: event_subscriber }

That was easy!

Porting the bundle to Drupal

The library only relies on HttpFoundation

Split the bundle into a package and a bundle

Framework agnostic

Would it work with Drupal?

A custom module with 2 yml files did the trick


name: MediaMonks Rest API
description: Transforms controller results into Rest API spec
package: Custom
type: module
core: 8.x


    autowire: true
      - { name: event_subscriber }

    public: false
      - '/api'

  # a few more services


public function helloWorldAction() {
    return 'Hello World';


    data: "Hello World"
Controllers Redefined

Controllers as Services

No ControllerBase

Inject only services you need


  • Configuration with Dependency Injection
  • See what's available
  • Prevent getting fat controllers


  • Create your own helper functions
  • A controller is usually not reusable
  • Takes more work to configure*

Highly debated in the Symfony community.

There is no right or wrong,
it depends on personal preference

How to do it in Drupal?

use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class AcmeController implements ContainerInjectionInterface {
    private $wordGenerator;

    public function __construct(WordGenerator $wordGenerator) {
        $this->wordGenerator = $wordGenerator;

    public static function create(ContainerInterface $container) {
        return new static($container->get(WordGenerator::class));

Still coupled to Drupal and the Symfony container :(

Without create()

use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;

class ExampleController {
    private $wordGenerator;

    public function __construct(WordGenerator $wordGenerator) {
        $this->wordGenerator = $wordGenerator;

Register your controller

  Drupal\acme\WordGenerator: ~

    autowire: true

Autowiring ftw!

Set up routing

  path: /acme/foobar
    _controller: Drupal\acme\Controller\ExampleController:foobarAction

Undocumented at that moment

Structure of routes

Controller Annotations

Symfony Framework "Extra" Bundle

 * @Route("/{id}")
 * @Method("GET")
 * @ParamConverter("post", class="AcmeBundleBundle:Post")
 * @Template("AcmeBundle:Annot:show.html.twig", vars={"post"})
 * @Cache(smaxage="15")
 * @Security("has_role('ROLE_ADMIN')")
public function showAction(Post $post) { // your code }

Can we do this in Drupal?

No... but

it's based on Event Subscribers

Ported to Drupal

 * @Route("/{article}")
 * @Method("GET")
 * @ParamConverter("article", options={"bundle": "article"})
 * @Template()
 * @Cache(maxage="15")
 * @Security(permission="access content")
public function showAction(Node $article) { // your code }

Register the routing annotations

    path: /api
        type: annotation
        module: 'acme'


/** @Route("/examples") */
class ExampleController extends ControllerBase {
    /** @Route @Method("GET")*/
    public function listAction(Node $article) { // your code }

    /** @Route @Method("POST") */
    public function createAction(Node $article) { // your code }


 * @Security(permission="access content")
 * @Security(role="administrator")
 * @Security(access=true)
 * @Security(csrf=true)
 * @Security(custom="Drupal\acme\Security\Controller::access")
public function editAction(Node $article) { // your code }


 * @Template("acme:example:show")
 * @Template("acme:show")
 * @Template
public function showAction(Node $article) { }


 * @Cache(expires="tomorrow", public=true)
 * @Cache(expires="+2 days")
 * @Cache(smaxage="15", maxage="15", public=true)
public function showAction(Node $article) { }


 * @ParamConverter("article", options={"bundle": "article"})
 * @ParamConverter("article")
 * @ParamConverter
public function showAction(Node $article) { }


 * @Route("/blogs/archive/{start}/{end}")
 * @ParamConverter("start", options={"format": "Y-m-d"})
 * @ParamConverter("end", options={"format": "Y-m-d"})
public function archiveAction(\DateTime $start, \DateTime $end) { }

Create your own

namespace Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\HttpFoundation\Request;

interface ParamConverterInterface
    function apply(Request $request, ParamConverter $configuration);

    function supports(ParamConverter $configuration);

PSR-7 Support did not need porting

namespace Drupal\acme\Controller;

use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response;

class AcmeController
    public function psr7Action(ServerRequestInterface $request) {
        $response = new Response();
        $response->getBody()->write('Hello world!');

        return $response;

How does it work?

Act on certain events

Modify request and response objects

Routing uses "routing.route_dynamic" event

Development Process

  • Require Framework Extra Bundle
  • Create custom module
  • Register the event subscribers
  • Fail
  • Change event subscriber priorities
  • Refactor incompatible event subscribers
  • Remove Framework Extra Bundle

Try it!

API Building



“Fractal provides a presentation and transformation layer for complex data output, the like found in RESTful APIs, and works really well with JSON. Think of this as a view layer for your JSON/YAML/etc.”


namespace Drupal\api\Transformer;

use Drupal\node\Entity\Node;
use League\Fractal\TransformerAbstract;

class ArticleTransformer extends TransformerAbstract {
    public function transform(Node $article) {
        return [
            'id' => (int)$article->id(),
            'title' => $article->get('title')->value,
            'body' => $article->get('body')->value,

Transform a single entity

$fractal = new League\Fractal\Manager;
$transformer = new Drupal\api\Transformer\ArticleTransformer;
$resource = new League\Fractal\Resource\Item($entity, $transformer);

Transform multiple entities

$fractal = new League\Fractal\Manager;
$transformer = new Drupal\api\Transformer\ArticleTransformer;
$resource = new League\Fractal\Resource\Collection($entities, $transformer);



class ArticleTransformer extends TransformerAbstract {
    public function transform(Node $article) {
        // node conversion to array
    public function includeAuthor(Node $article) {
        $author = $article->author;

        return $this->item($author, new AuthorTransformer);

Enabling includes

use League\Fractal;

$fractal = new Fractal\Manager();
if (isset($_GET['include'])) {

Be aware of lazy loading!

Try it!


No one likes forms

Symfony Form eases the pain

Great builder

Highly customizable

Validation included

Can be used in Drupal without issues

A simplified example

class ExampleType extends AbstractType {
  public function buildForm(FormBuilderInterface $builder, array $options) {
      ->add('title', TextType::class, [
        'constraints' => [
          new NotBlank(),
          new Length(['min' => 3, 'max' => 50])]

Example API Implementation in Drupal

private function handleForm(Request $request, FormTypeInterface $form) {
    $form = $this->formFactory->create($form);
    if (!$form->isValid()) {
        throw new FormValidationException($form);

    $node = $this->getNodeStorage()->create(
      array_merge($form->getData(), ['type' => 'article'])

    return new Response(null, Response::HTTP_CREATED);

Rendering with Twig - Controller Side

public function createAction(Request $request) {
    $form = $this->createFormBuilder($node)
        ->add('task', TextType::class)
        ->add('dueDate', DateType::class)
        ->add('save', SubmitType::class, ['label' => 'Create Task'])

    if ($form->isSubmitted() && $form->isValid()) {
        $node = $form->getData();
        return $this->redirect('node_created');
    return ['form' => $form->createView()];

Rendering with Twig - Template side

{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}

Form types for every need

  • Money
  • Url
  • Range
  • Choice
  • Country
  • Language
  • Currency
  • DateTime
  • Create your own!

Validators for every need

  • NotBlank
  • Length
  • Email
  • Url
  • Date
  • Regex
  • Image
  • Create your own!

Try it!

Putting it all together

Bonus Time
More practical use of Event Subscribers!
SEO With Prerender

Decoupled since 2014


Frontend use their own frameworks

Creative freedom

Page transitions

So...what about SEO?

Some Solutions

  • Ignore
  • Assume everyone uses Google
  • Duplicate routing in backend
  • Prerender / PhantomJS

Common Implementation

  • Request
  • Detect bot based on user agent
  • Send to Prerender server

Our Implementation

  • Crawl website
  • Send it to Prerender
  • Store in database
  • Listen for Request
  • Find match in database
  • Inject crawled content

PHP Crawler

  • Using generators
  • Highly customizable
  • Whitelisting
  • Blacklisting
  • Normalizing

Basic example

use MediaMonks\Crawler\Crawler;

$crawler = new Crawler;
foreach($crawler->crawl('') as $page) {
    $page->getUrl(); // League Http Uri Scheme object
    $page->getCrawler(); // Symfony DomCrawler object

Getting page data

$c = $page->getCrawler(); // Symfony DomCrawler object


use MediaMonks\Crawler\Crawler;
use MediaMonks\Crawler\Url\Matcher\PathRegexUrlMatcher;

$crawler = new Crawler;
$crawler->addBlacklistUrlMatcher(new PathRegexUrlMatcher('~^/admin~'));
$crawler->addBlacklistUrlMatcher(new PathRegexUrlMatcher('~^/search~'));
foreach($crawler->crawl('') as $page) {
    $page->getUrl(); // Leaugue Http Uri Scheme object
    $page->getCrawler(); // Symfony DomCrawler object

How to implement it in Drupal?

  • Console command
  • Event subscriber
  • Return response if a match was found

Keeping it up to date?

  • Timed
  • CMS Triggered
  • Priority

Another benefit

Full site search!

Give it a try




Development made easier

Warning: never do this in production!

Disable route cache

class RebuildRouteEventSubscriber implements EventSubscriberInterface {
    /** @param RouteBuilderInterface $routeBuilder */
    public function __construct(RouteBuilderInterface $routeBuilder) {
        $this->routeBuilder = $routeBuilder;
    public static function getSubscribedEvents() {
        return [KernelEvents::REQUEST => [['onRequest', 1024]]];
    public function onRequest() {

Disable container dumping

use Drupal\Core\DrupalKernel;
use Symfony\Component\HttpFoundation\Request;

$autoloader = require_once __DIR__.'/../vendor/autoload.php';

$kernel = new DrupalKernel('dev', $autoloader, FALSE);

bool $allow_dumping
to stop the container from being written to or read from disk. Defaults to TRUE.

Drupal Console

Searching for routes and services

drupal router:debug | grep article
drupal container:debug | grep article
  • Events subscribers are really powerful
  • Symfony knowledge is very useful
  • Help out on a project without knowing Drupal
  • There is more than
  • Tons of packages on
  • We can learn from each others solutions
  • Please use Composer
  • In the end it's all PHP

That's it for now, questions?