HFWTSC

Having Fun With The Symfony Container

Who Am I?

  • Bram Van der Sype
  • Brammm (Twitter, IRC, Github...)
  • Freelance Web Developer
  • Currently at  

What's Sparkcentral?

  • “Customer Engagement Platform”
  • Our core API is built on Symfony2
  • Moving away from full stack

What is DI?

Going from …


class Controller {
    public function __construct()
    {
        $storage = new RedisStorage();
        $this->session = new Session($storage, GLOBAL_PREFIX);
    }
}

… to


class Controller {
    public function __construct(Session $session)
    {
        $this->session = $session;
    }
}

Why use DI?

SOLID

Five Principles

  • Single responsibility
  • Open/Closed
  • Liskov Substitution
  • Interface Segregation
  • Dependency Inversion

Testability

symfony/dependency-injection

app/console generate:bundle

src/Acme
`-- DemoBundle
    |-- AcmeDemoBundle.php
    |-- Controller
    |   `-- DefaultController.php
    |-- DependencyInjection
    |   |-- AcmeDemoExtension.php
    |   `-- Configuration.php
    |-- Resources
    |   |-- config
    |   |   |-- routing.yml
    |   |   `-- services.yml
    |   `-- views
    |       `-- Default
    |           `-- index.html.twig
    `-- Tests
        `-- Controller
            `-- DefaultControllerTest.php

Relevant to DI

src/Acme
`-- DemoBundle
    |-- AcmeDemoBundle.php
    |-- DependencyInjection
    |   |-- AcmeDemoExtension.php
    |   `-- Configuration.php
    |-- Resources
        |-- config
            `-- services.yml

Creating services

Creating services


// Acme/DemoBundle/Command/RegisterUserCommandHandler.php
class RegisterUserCommandHandler implements Handler {
    public function __construct(
        UserRepository $userRepo,
        EventDispatcherInterface $dispatcher,
        $defaultPicture)
    {
        // ...
    }

    public function handle(Command $command)
    {
        $user = $this->userRepo->newFromCommand($command);
        $user->setPicture($this->defaultPicture);
        $this->userRepo->persist($user);
        $this->dispatcher->dispatch(
            'user_registered',
            new UserRegisteredEvent($user)
        );
    }
}

Creating services


# Acme/DemoBundle/Resources/config/services.yml
services:
    acme_demo.handler.register_user:
        class: Acme\DemoBundle\Command\RegisterUserCommandHandler
        arguments:
            - "@acme_demo.repo.user" # One of our own services
            - "@event_dispatcher"    # Symfony2 service
            - "images/mrT.jpg"       # Literal string

Creating services


// Acme/DemoBundle/DependencyInjection/AcmeDemoExtension.php
class AcmeDemoExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);

        $loader = new Loader\YamlFileLoader(
            $container,
            new FileLocator(__DIR__.'/../Resources/config')
        );
        $loader->load('services.yml');
    }
}

Configuring services

Parameters


# Acme/DemoBundle/Resources/config/services.yml
parameters:
    default_picture: "images/mrT.jpg"
services:
    acme_demo.handler.register_user:
        class: Acme\DemoBundle\Handler\RegisterUserCommandHandler
        arguments:
            # ...
            - "%default_picture%"

Parameters


# app/config/parameters.yml
parameters:
    database#...

    default_picture: "images/mrT.jpg"

Introducing Config component


#app/config/config.yml
doctrine:
    orm:
        auto_mapping: true

Using Config component


#app/config/config.yml
acme_demo:
    default_picture: "images/mrT.jpg"

Using Config component


// Acme/DemoBundle/DependencyInjection/Configuration.php
class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('acme_demo');

        // Here you should define the parameters that are allowed to
        // configure your bundle. See the documentation linked above for
        // more information on that topic.

        return $treeBuilder;
    }
}

Using Config component


// Acme/DemoBundle/DependencyInjection/Configuration.php
class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('acme_demo');
        $rootNode
           ->children()
                ->scalarNode('default_picture')->end()
            ->end();

        return $treeBuilder;
    }
}

Using Config component


// Acme/DemoBundle/DependencyInjection/AcmeDemoExtension.php
public function load(array $configs, ContainerBuilder $container)
{
    $configuration = new Configuration();
    $config = $this->processConfiguration($configuration, $configs);

    $container->setParameter(
        'default_picture',
        $config['default_picture']
    );

    // $loader = ...
}

Tagging Services

Tagged services


# Acme/DemoBundle/Resources/config/services.yml
services:
    acme_demo.listener.user_registered:
        class: Acme\DemoBundle\EventListener\UserRegisteredEventListener
        tags:
            - { name: kernel.event_listener,
                event: user_registered,
                method: afterRegistered }

The Command Bus


// Acme/DemoBundle/Command/CommandBus.php
class CommandBus {
    public function handle(Command $command)
    {
        $this->getHandler($command)->handle($command);
    }

    private function getHandler(Command $command)
    {
        // return Handler
    }
}

Adding our own tags


# Acme/DemoBundle/Resources/config/services.yml
acme_demo.handler.register_user:
    class: Acme\DemoBundle\Command\RegisterUserCommandHandler
    arguments: # ...
    tags:
        - { name: acme_demo.command_handler,
            handles: "Acme\DemoBundle\Command\RegisterUserCommand" }

The Command Bus


// Acme/DemoBundle/Command/CommandBus.php
class CommandBus {
    public function addHandler($commandClass, Handler $handler)
    {
        $this->handlers[$commandClass] = $handler;
    }

    private function getHandler(Command $command)
    {
        return $this->handlers[get_class($command)];
    }
}

The Compiler Pass


// Acme/DemoBundle/DependencyInjection/CompilerPass/CommandHandlersPass.php
class CommandHandlerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $handlers   = $container->findTaggedServiceIds('acme_demo.command_handler');
        $commandBus = $container->findDefinition('acme_demo.command_bus');

        foreach ($handlers as $serviceId => $tags) {
            foreach ($tags as $tagAttributes) {
                $commandBus->addMethodCall(
                    'addHandler',
                    [
                        $tagAttributes['handles'],
                        new Reference($serviceId)
                    ]
                );
            }
        }
    }
}

Registering the Compiler Pass


// Acme/DemoBundle/AcmeDemoBundle.php
class AcmeDemoBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        parent::build($container);
        $container->addCompilerPass(new CommandHandlerPass());
    }
}

Using it


// Acme/DemoBundle/Controller/UserController.php
class UserController extends Controller
{
    public function registerAction(Request $request)
    {
        // $command = ...
        $this->get('acme_demo.command_bus')->handle($command);
        //
    }
}

Some tricks

Default Symfony2 Controllers


//Acme/DemoBundle/Controller/UserController.php
class UserController extends Controller {
    public function registerAction(Request $request)
    {
        $em   = $this->getDoctrine()->getManager();
        $repo = $em->getRepository('AcmeDemoBundle:User');
        $user = $repo->findUser();

        $command = RegisterUserCommand::fromRequest($request);
        $this->get('command_bus')->handle($command);

        return $this->render(
            'AcmeDemoBundle:Default:index.html.twig',
            ['name' => $user->getName()]
        );
    }
}

Controllers as Services


#Acme/DemoBundle/Resources/config/services.yml
services:
    acme_demo.controller.default:
        class: Acme\DemoBundle\Controller\DefaultController
        arguments:
            - "@acme_demo.command_bus"
            - "@doctrine.orm.default_entity_manager"

#Acme/DemoBundle/Resources/config/routing.yml
acme_demo_default_index:
    path:     /
    defaults: { _controller: AcmeDemoBundle:Default:index }
    -->
    defaults: { _controller: acme_demo.controller.default:indexAction }

Controllers as Services


//Acme/DemoBundle/Controller/DefaultController.php
class DefaultController {
    public function registerAction(Request $request)
    {
        $repo = $this->em->getRepository('AcmeDemoBundle:User');
        $user = $repo->findUser();

        $command = RegisterUserCommand::fromRequest($request);
        $this->commandBus->handle($command);

        return $this->render(
            'AcmeDemoBundle:Default:index.html.twig',
            ['name' => $user->getName()]
        );
    }
}

Aliasing


# Acme/SomeBundle/Resources/config/services.yml
services:
    em: doctrine.orm.default_entity_manager

    acme_demo.controller.default:
        class: Acme\DemoBundle\Controller\DefaultController
        arguments:
        - "@acme_demo.command_bus"
        - "@em"

Doctrine Repositories as Services


# Acme/DemoBundle/Resources/config/services.yml
services:
    acme_demo.repo.user:
        class: Acme\DemoBundle\Repository\UserRepository
        factory_service: em # see what I did here
        factory_method: getRepository
        arguments: ["AcmeDemoBundle:User"]

Doctrine Repositories as Services


#Acme/DemoBundle/Resources/config/services.yml
services:
    acme_demo.controller.default:
        class: Acme\DemoBundle\Controller\DefaultController
        arguments:
        - "@acme_demo.command_bus"
        - "@acme_demo.repo.user"

Doctrine Repositories as Services


//Acme/DemoBundle/Controller/DefaultController.php
class DefaultController {
    public function registerAction(Request $request)
    {
        $user = $this->repo->findUser();

        $command = RegisterUserCommand::fromRequest($request);
        $this->commandBus->handle($command);

        return $this->render(
            'AcmeDemoBundle:Default:index.html.twig',
            ['name' => $user->getName()]
        );
    }
}

Splitting up services.yml


// Acme/DemoBundle/DependencyInjection/AcmeDemoExtension.php
public function load(array $configs, ContainerBuilder $container)
{
    // ...
    $loader = new Loader\YamlFileLoader(
        $container,
        new FileLocator(__DIR__.'/../Resources/config')
    );
    $loader->load('services.yml');
    $loader->load('controllers.yml');
    $loader->load('repositories.yml');
}

Tagging Controller Services

Tagging Controllers


//Acme/DemoBundle/Controller/DefaultController.php
class DefaultController {
    public function indexAction()
    {
        $user = $this->repo->someCostlyLookup();

        return $this->render(
            'AcmeDemoBundle:Default:index.html.twig',
            ['name' => $user->getName()]
        );
    }
}

Tagging Controllers


//Acme/DemoBundle/Controller/DefaultController.php
public function indexAction(Request $request)
{
    $user     = // $this->repo ...
    $response = // $this->render ...

    $response->setLastModified($user->getLastModified());
    $this->redis->setLastModified(
        $request->attributes->get('_route'),
        $user->getLastModified()
    );

    return $response;
}

Tagging Controllers


//Acme/DemoBundle/Controller/DefaultController.php
public function indexAction(Request $request)
{
    $response = new Response();
    $response->setLastModified($this->redis->getLastModified(
        $request->attributes->get('_route'))
    );
    if ($response->isNotModified($request) {
        return $response;
    }

    // ...
}

Tagging controllers


#Acme/DemoBundle/Resources/config/controllers.yml
services:
    acme_demo.controller.default:
    class: Acme\DemoBundle\Controller\DefaultController
    arguments: ["@acme_demo.repo.user"]
    tags:
        - { name: acme_demo.cacheable }

Tagging controllers


#Acme/DemoBundle/Resources/config/services.yml
services:
    acme_demo.listener.cacheable:
        class: Acme\DemoBundle\EventListener\CacheableSubscriber
        arguments: ["@redis"]
        tags:
            - { name: kernel.event_subscriber }

Tagging controllers


// Acme/DemoBundle/DependencyInjection/CompilerPass/CacheablePass.php
public function process(ContainerBuilder $container)
{
    $controllers = $container->findTaggedServiceIds(
        'acme_demo.cacheable'
    );
    $eventSubscriber = $container->getDefinition(
        'acme_demo.listener.cacheable'
    );

    foreach (array_keys($controllers) as $serviceId) {
        $eventSubscriber->addMethodCall('addController', [$serviceId]);
    }
}

Tagging controllers


// Acme/DemoBundle/Listener/CacheableSubscriber.php
class CacheableSubscriber implements EventSubscriberInterface
{
    private $controllers = [];

    public static function getSubscribedEvents()
    {
        return [
            'kernel.request'  => 'onRequest',
            'kernel.response' => 'onResponse',
        ];
    }

    public function addController($controller)
    {
        $this->controllers[] = $controller;
    }

    public function onResponse(FilterResponseEvent $event)
    {
        $this->redis->setLastModified(
            $event->getRequest()->attributes->get('_route'),
            $event->getResponse()->getLastModified()
        );
    }

    public function onRequest(GetResponseEvent $event)
    {
        $controller = $event->getRequest()->attributes->get('_controller');
        $route      = $event->getRequest()->attributes->get('_route');

        list($serviceId, $action) = explode(':', $controller);

        if (! in_array($serviceId, $this->controllers)) {
            return;
        }

        $response = new Response();
        $response->setLastModified($this->redis->getLastModified($route));

        if ($response->isNotModified($event->getRequest())) {
            $event->setResponse($response);
        }
    }
}

Tagging Controllers


//Acme/DemoBundle/Controller/DefaultController.php
class DefaultController {
    public function indexAction(Request $request)
    {
        $user = $this->repo->someCostlyLookup();

        $response = $this->render(
            'AcmeDemoBundle:Default:index.html.twig',
            ['name' => $user->getName()]
        );
        $response->setLastModified($user->getLastModified());
        return $response;
    }
}

Last notes

Alternatives/Honorary mentions

  • pimple/pimple
  • illuminate/container
  • mnapoli/php-di
  • league/container

Recommended Reading

  • A Year With Symfony by Matthias Noback
  • Principles of Package Design by Matthias Noback
  • symfony.com/doc/
  • #symfony on chat.freenode.net

Questions?

Thanks!

Please leave feedback on joind.in/14183