Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Applying Design Patterns to Symfony

Applying Design Patterns to Symfony

This talk describes a list of GoF design patterns applied to the Symfony framework.

Hugo Hamon

May 03, 2014
Tweet

More Decks by Hugo Hamon

Other Decks in Technology

Transcript

  1. Hugo HAMON Head of training at SensioLabs Book author Speaker

    at Conferences Symfony contributor @hhamon
  2. In software design, a design pattern is an abstract generic

    solution to solve a particular common problem.
  3. namespace Symfony\Component\Form; class ResolvedFormTypeFactory implements ResolvedFormTypeFactoryInterface { public function createResolvedType(

    FormTypeInterface $type, array $typeExtensions, ResolvedFormTypeInterface $parent = null ) { return new ResolvedFormType( $type, $typeExtensions, $parent ); } }
  4. $f = new ResolvedFormTypeFactory(); $form = $f->createResolvedType(new FormType()); $date =

    $f->createResolvedType(new DateType(), [], $form); $bday = $f->createResolvedType(new BirthdayType(), [], $date);
  5. class ResolvedTypeFactoryDataCollectorProxy implements ResolvedFormTypeFactoryInterface { private $proxiedFactory; private $dataCollector; public

    function __construct( ResolvedFormTypeFactoryInterface $proxiedFactory, FormDataCollectorInterface $dataCollector ) { $this->proxiedFactory = $proxiedFactory; $this->dataCollector = $dataCollector; } public function createResolvedType( FormTypeInterface $type, array $typeExtensions, ResolvedFormTypeInterface $parent = null ) { return new ResolvedTypeDataCollectorProxy( $this->proxiedFactory->createResolvedType($type, $typeExtensions, $parent), $this->dataCollector ); } }
  6. $factory = new ResolvedTypeDataCollectorProxyFactory( new ResolvedFormTypeFactory(), new FormDataCollector(…) ); $form

    = $f->createResolvedType(new FormType()); $date = $f->createResolvedType(new DateType(), [], $form); $bday = $f->createResolvedType(new BirthdayType(), [], $date);
  7. The lazy initialization pattern is the tactic of delaying the

    creation of an object, the calculation of a value, or some other expensive process until the first time it is really needed.
  8. The Service Container The service container allows you to standardize

    and centralize the way objects are constructed in your application.
  9. class Container implements ContainerInterface { private $services = []; private

    $methodMap = []; public function get($id) { // Re-use shared service instance if it exists. if (isset($this->services[$id])) { return $this->services[$id]; } // Lazy instantiate the service $method = $this->methodMap[$id]; return call_user_func(array($this, $method)); } }
  10. class appDevDebugProjectContainer extends Container { protected function getLoggerService() { //

    Instanciate the requested class. $obj = new \Symfony\Bridge\Monolog\Logger('app'); $this->services['logger'] = $obj; // Initialize the instance before usage. $obj->pushHandler($this->get('monolog.handler.chromephp')); $obj->pushHandler($this->get('monolog.handler.firephp')); $obj->pushHandler($this->get('monolog.handler.main')); $obj->pushHandler($this->get('monolog.handler.debug')); return $obj; } }
  11. // Logger is created on demand $logger1 = $container->get('logger'); //

    Logger is simply fetched from array map $logger2 = $container->get('logger'); // Two variables reference same instance var_dump($logger1 === $logger2);
  12. Examples Adapting several database vendors Adapting a new version of

    a REST API Offering a backward compatibility layer
  13. New CSRF token management system since Symfony 2.4 Now done

    by the Security Component instead of the Form Component Keeping a backward compatibility layer with the old API until it’s removed in Symfony 3.0 Adapting the new CSRF API
  14. The old CSRF Management API namespace Symfony\Component\Form\Extension\Csrf\CsrfProvider; interface CsrfProviderInterface {

    public function generateCsrfToken($intention); public function isCsrfTokenValid($intention, $token); }
  15. The old CSRF Management API (< 2.4) namespace Symfony\Component\Form\Extension\Csrf\CsrfProvider; class

    DefaultCsrfProvider implements CsrfProviderInterface { // ... public function generateCsrfToken($intention) { return sha1($this->secret.$intention.$this->getSessionId()); } public function isCsrfTokenValid($intention, $token) { return $token === $this->generateCsrfToken($intention); } }
  16. The old CSRF Management API (< 2.4) $provider = new

    DefaultCsrfProvider('SecretCode'); $csrfToken = $provider ->generateCsrfToken('intention') ; $csrfValid = $provider ->isCsrfTokenValid('intention', $token) ;
  17. The new CSRF Management API (>= 2.4) namespace Symfony\Component\Security\Csrf; interface

    CsrfTokenManagerInterface { public function getToken($tokenId); public function refreshToken($tokenId); public function removeToken($tokenId); public function isTokenValid(CsrfToken $token); }
  18. Combining the old and new APIs for BC class TwigRenderer

    extends FormRenderer implements TwigRendererInterface { private $engine; public function __construct( TwigRendererEngineInterface $engine, $csrfTokenManager = null ) { if ($csrfTokenManager instanceof CsrfProviderInterface) { $csrfTokenManager = new CsrfProviderAdapter($csrfTokenManager); } parent::__construct($engine, $csrfTokenManager); $this->engine = $engine; } }
  19. The CSRF Provider Adapter class CsrfProviderAdapter implements CsrfTokenManagerInterface { private

    $csrfProvider; public function __construct(CsrfProviderInterface $csrfProvider) { $this->csrfProvider = $csrfProvider; } public function refreshToken($tokenId) { throw new BadMethodCallException('Not supported'); } public function removeToken($tokenId) { throw new BadMethodCallException('Not supported'); } }
  20. The CSRF Provider Adapter class CsrfProviderAdapter implements CsrfTokenManagerInterface { public

    function getToken($tokenId) { $token = $this->csrfProvider->generateCsrfToken($tokenId); return new CsrfToken($tokenId, $token); } public function isTokenValid(CsrfToken $token) { return $this->csrfProvider->isCsrfTokenValid( $token->getId(), $token->getValue() ); } }
  21. Examples Representing a binary tree Representing a multi level navigation

    bar Parsing an XML/HTML document Designing & validating nested forms …
  22. Everything is a Form Each element that composes a Symfony

    Form is an instance of the Form class. Each Form instance keeps a reference to its parent Form instance and a collection of its children references.
  23. The (simplified) Form class namespace Symfony\Component\Form; class Form implements FormInterface

    { private $name; public function __construct($name = null) { $this->name = $name; } public function getName() { return $this->name; } }
  24. namespace Symfony\Component\Form; class Form implements FormInterface { private $parent; private

    $children; public function add(FormInterface $child) { $this->children[$child->getName()] = $child; $child->setParent($this); return $this; } }
  25. Building the Form tree $picture = new Form('picture'); $picture->add(new Form('caption'));

    $picture->add(new Form('image')); $form = new Form('product'); $form->add(new Form('name')); $form->add(new Form('description')); $form->add($picture);
  26. Submitting the form data $form->submit(array( 'name' => 'Apple Macbook Air

    11', 'description' => 'The thinest laptop', 'picture' => array( 'caption' => 'The new Macbook Air.', ), ));
  27. Submitting the form data class Form implements FormInterface { public

    function submit(array $data) { $this->data = $data; foreach ($this->children as $child) { if (isset($data[$child->getName()])) { $childData = $data[$child->getName()]; $child->submit($childData); } } } }
  28. Why using it? Extending objects without bloating the code Making

    code reusable and composable Avoiding vertical inheritance
  29. Examples Adding some caching capabilities Adding some logging capabilities Applying

    discount strategies to an order Decorating/wrapping a string content …
  30. Adding an HTTP Caching Layer The default implementation of the

    HttpKernel class doesn’t support caching capabilities. Symfony provides an HttpCache class to decorate an instance of HttpKernel in order to emulate an HTTP reverse proxy cache.
  31. // index.php $dispatcher = new EventDispatcher(); $resolver = new ControllerResolver();

    $store = new Store(__DIR__.'/http_cache'); $httpKernel = new HttpKernel($dispatcher, $resolver); $httpKernel = new HttpCache($httpKernel, $store); $httpKernel ->handle(Request::createFromGlobals()) ->send() ;
  32. class HttpCache implements HttpKernelInterface { public function handle(Request $request) {

    // ... if (!$request->isMethodSafe()) { $response = $this->invalidate($request, $catch); } elseif ($request->headers->has('expect')) { $response = $this->pass($request, $catch); } else { // Get response from the Store or fetch it with http kernel $response = $this->lookup($request, $catch); } // ... return $response; } }
  33. class HttpCache implements HttpKernelInterface { protected $httpKernel; protected function forward(Request

    $request, ...) { // ... $request->server->set('REMOTE_ADDR', '127.0.0.1'); $response = $this->httpKernel->handle($request); // ... $this->processResponseBody($request, $response); return $response; } }
  34. Pros and cons + Easy way to extend an object

    capabilities + No need to change the underlying code - Object construction becomes more complex - Difficulty to test the concrete object type
  35. Examples Reading the content of directory recursively Sorting items in

    a collection Applying filters on items of a collection Lazy fetch data from a datastore …
  36. interface Iterator { public function current(); public function next(); public

    function rewind(); public function valid(); public function key(); }
  37. The Finder $iterator = Finder::create() ->files() ->name('*.php') ->depth(0) ->size('>= 1K')

    ->in(__DIR__); foreach ($iterator as $file) { print $file->getRealpath()."\n"; }
  38. ├── CustomFilterIterator.php ├── DateRangeFilterIterator.php ├── DepthRangeFilterIterator.php ├── ExcludeDirectoryFilterIterator.php ├── FilePathsIterator.php

    ├── FileTypeFilterIterator.php ├── FilecontentFilterIterator.php ├── FilenameFilterIterator.php ├── FilterIterator.php ├── MultiplePcreFilterIterator.php ├── PathFilterIterator.php ├── RecursiveDirectoryIterator.php ├── SizeRangeFilterIterator.php └── SortableIterator.php
  39. Sorting a list of files use Symfony\Component\Finder\Iterator\SortableIterator; use Symfony\Component\Finder\Iterator\RecursiveDirectoryIterator; $dirIterator

    = new \RecursiveIteratorIterator( new RecursiveDirectoryIterator( __DIR__, \RecursiveDirectoryIterator::SKIP_DOTS ) ); $dirIterator->setMaxDepth(0); $iterator = new SortableIterator( $dirIterator, SortableIterator::SORT_BY_NAME );
  40. Finder Adapter class PhpAdapter extends AbstractAdapter { public function searchInDirectory($dir)

    { $iterator = new \RecursiveIteratorIterator(...); if ($this->minDepth > 0 || $this->maxDepth < PHP_INT_MAX) { $iterator = new DepthRangeFilterIterator($iterator, $this->minDepth, $this->maxDepth); } if ($this->mode) { $iterator = new FileTypeFilterIterator($iterator, $this->mode); } if ($this->exclude) { $iterator = new ExcludeDirectoryFilterIterator($iterator, $this->exclude); } // ... Other iterators are added return $iterator; } }
  41. Why using it? Decoupling large pieces of code Hooking a

    third party algorithm to an existing one Easing unit testability of objects Filtering user input data …
  42. Examples Dispatching events when an object’s state changes Hooking new

    responsabilities to a model object Filtering some input data
  43. class OrderService { public function confirmOrder(Order $order) { $order->status =

    'confirmed'; $order->save(); if ($this->logger) { $this->logger->log('New order...'); } $mail = new Email(); $mail->recipient = $order->getCustomer()->getEmail(); $mail->subject = 'Your order!'; $mail->message = 'Thanks for ordering...'; $this->mailer->send($mail); $mail = new Email(); $mail->recipient = '[email protected]'; $mail->subject = 'New order to ship!'; $mail->message = '...'; $this->mailer->send($mail); } }
  44. What’s wrong with this code? Too many responsabilities Not easy

    extensible Difficulty to unit test Bloated code
  45. Event Dispatcher E1   Emits a specific event to notify

    and execute its listeners. L2 L3 L4 L1
  46. Extremly simple and easy to use Decouple objects from each

    other Make code truly extensible Easy to add responsabilities to an object Benefits of the Mediator
  47. The Event Dispatcher Class namespace Symfony\Component\EventDispatcher; class EventDispatcher { function

    dispatch($eventName, Event $event = null); function getListeners($eventName); function hasListeners($eventName); function addListener($eventName, $listener, $priority = 0); function removeListener($eventName, $listener); function addSubscriber(EventSubscriberInterface $subscriber); function removeSubscriber(EventSubscriberInterface $subscriber); }
  48. Connecting listeners to events $listener1 = new CustomerListener($mailer); $listener2 =

    new SalesListener($mailer); $listener3 = new StockListener($stockHandler); $dp = new EventDispatcher(); $dp->addListener('order.paid', [ $listener1, 'onOrderPaid' ]); $dp->addListener('order.paid', [ $listener2, 'onOrderPaid' ]); $dp->addListener('order.paid', [ $listener3, 'onOrderPaid' ], 100); $dp->dispatch('order.paid', new Event());
  49. Implementing listener classes class CustomerListener { private $mailer; public function

    __construct(\Swift_Mailer $mailer) { $this->mailer = $mailer; } public function onOrderPaid(Event $event) { $mail = $this->mailer->createMessage(...); $this->mailer->send($mail); } }
  50. Creating custom event objects use Symfony\Component\EventDispatcher\Event; class OrderEvent extends Event

    { private $order; public function __construct(Order $order) { $this->order = $order; } public function getOrder() { return $this->order; } }
  51. Accessing data from custom events class CustomerListener { // ...

    public function onOrderPaid(OrderEvent $event) { $order = $event->getOrder(); $customer = $order->getCustomer(); $mail = $this->mailer->createMessage(...); $this->mailer->send($mail); } }
  52. Decoupling the code with events class OrderService { private $dispatcher;

    public function __construct(EventDispatcher $dispatcher) { $this->dispatcher = $dispatcher; } public function confirmOrder(Order $order) { $order->status = 'confirmed'; $order->save(); $event = new OrderEvent($order); $this->dispatcher->dispatch('order.paid', $event); } }
  53. Kernel Events Event name Meaning kernel.request Filters the incoming HTTP

    request kernel.controller Initializes the controller before it’s executed kernel.view Generates a template view kernel.response Prepares the HTTP response nefore it’s sent kernel.exception Handles all caught exceptions kernel.terminate Terminates the kernel
  54. Form Events Event name Meaning form.pre_bind Changes submitted data before

    they’re bound to the form form.bind Changes data into the normalized representation form.post_bind Changes the data after they are bound to the form form.pre_set_data Changes the original form data form.post_set_data Changes data after they were mapped to the form
  55. Security Events Event name Meaning security.interactive_login User is successfully authenticated.

    security.switch_user User switched to another user account. security.authentication.success User is authenticated by a provider security.authentication.failure User cannobt be authenticated by a provider
  56. The strategy pattern encapsulates algorithms of the same nature into

    dedicated classes to make them interchangeable.
  57. The Profiler stores the collected data into a storage engine.

    The persistence algorithms must be interchangeable so that we can use any types of data storage (filesystem, database, nosql stores…).
  58. namespace Symfony\Component\HttpKernel\Profiler; interface ProfilerStorageInterface { function find($ip, ...); function read($token);

    function write(Profile $profile); function purge(); } Designing the profiler storage strategy
  59. namespace Symfony\Component\HttpKernel\Profiler; class FileProfilerStorage implements ProfilerStorageInterface { public function read($token)

    { if (!$token || !file_exists($file = $this->getFilename($token))) { return; } return $this->createProfileFromData($token, unserialize(file_get_contents($file))); } public function write(Profile $profile) { $file = $this->getFilename($profile->getToken()); $data = array(...); // ... return false !== file_put_contents($file, serialize($data)); } } The default profiler storage
  60. namespace Symfony\Component\HttpKernel\Profiler; abstract class PdoProfilerStorage implements ProfilerStorageInterface { public function

    read($token) { $db = $this->initDb(); $args = array(':token' => $token); $data = $this->fetch($db, 'SELECT ...', $args); $this->close($db); if (isset($data[0]['data'])) { return $this->createProfileFromData($token, $data[0]); } } } The PDO profiler storage
  61. // Profiled data to be saved $profile = new Profile('abcdef');

    // Filesystem storage $fsStorage = new FileProfilerStorage('/tmp/profiler'); $profiler = new Profiler($fsStorage); $profiler->saveProfile($profile); // Mysql database storage $dsn = 'mysql:host=localhost'; $dbStorage = new MySQLProfilerStorage($dsn); $profiler = new Profiler($dbStorage); $profiler->saveProfile($profile); Using the Profiler
  62. + templateMethod() [final]! # stepOne()! # stepTwo()! # stepThree()! #

    stepOne()! # stepTwo()! # stepThree()! # stepOne()! # stepTwo()! # stepThree()!
  63. abstract class AbstractAuthenticationListener implements ListenerInterface { final public function handle(GetResponseEvent

    $event) { // … try { // … if (null === $returnValue = $this->attemptAuthentication($request)) { return; } // … } catch (AuthenticationException $e) { $response = $this->onFailure($event, $request, $e); } $event->setResponse($response); } abstract protected function attemptAuthentication(Request $request); }
  64. namespace Symfony\Component\Security\Http\Firewall; class SimpleFormAuthenticationListener extends AbstractAuthenticationListener { protected function attemptAuthentication(Request

    $request) { // ... $token = $this->simpleAuthenticator->createToken( $request, trim($request->get('_username')), $request->get('_password') ); return $this->authenticationManager->authenticate($token); } } The Simple Form Authentication Listener
  65. namespace Acme\SsoBundle\Security\Core\Authentication; class SsoAuthenticationListener extends AbstractAuthenticationListener { protected function attemptAuthentication(Request

    $request) { if (!$ssoToken = $request->query->get('ssotoken')) { return; } $token = new SsoToken($ssoToken); return $this->authenticationManager->authenticate($token); } } Custom Authentication Listener