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
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;
}
}
"DUH! Is there anyone that hasn't heard of SOLID?
Five Principles
S ingle responsibility
O pen/Closed
L iskov Substitution
I nterface Segregation
D ependency Inversion
Go over abbreviation
You'll hear this a lot at talks/conferences
When trying to write SOLID code, you end up splitting up your classes. You end up with dependencies. There's where DI comes in
symfony/dependency-injection
Not reinventing the wheel, use existing component
symfony/di is used in full stack symfony, doesn't have to, can be stand alone
code examples assume you're creating a symfony bundle
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
Symfony2 has a lot of generators. Don't use them. Except maybe this one...
Explain about Symfony bundles, what's generated etc
Relevant to DI
src/Acme
`-- DemoBundle
|-- AcmeDemoBundle.php
|-- DependencyInjection
| |-- AcmeDemoExtension.php
| `-- Configuration.php
|-- Resources
|-- config
`-- services.yml
In a lot of instances you don't need the Configuration.php file
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)
);
}
}
zoomen op code, eerst handle() uitleggen, zoomen, dan dependencies toelichten
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
De verschillende elementen overlopen
Vermelden dat je ook xml of php kan gebruiken
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');
}
}
Here's where all the bootstrapping happens for your bundle.
$loader->load() is where a lot the magic happens (and the Kernel)
If you're not gonna use configuration, get rid of the first two lines
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
By default, Symfony2 allows you to configure certain parts in the config.yml files
You can extend this with your own bundles
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;
}
}
Won't go into detail about TreeBuilder/Configuration classes
a LOT of possibilities
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 = ...
}
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);
//
}
}
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;
}
}
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
Thanks!
Please leave feedback on joind.in/14183