Slide 1

Slide 1 text

Identifying Design Patterns in the Symfony Framework Istanbul – Turkey – May 3rd 2014

Slide 2

Slide 2 text

Hugo HAMON Head of training at SensioLabs Book author Speaker at Conferences Symfony contributor @hhamon

Slide 3

Slide 3 text

Introduction to Design Patterns

Slide 4

Slide 4 text

In software design, a design pattern is an abstract generic solution to solve a particular common problem.

Slide 5

Slide 5 text

Recommended readings

Slide 6

Slide 6 text

Disclaimer They aren’t the holy grail!

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

Loose Coupling

Slide 9

Slide 9 text

Unit testability

Slide 10

Slide 10 text

Maintenance

Slide 11

Slide 11 text

Three patterns families

Slide 12

Slide 12 text

Creational Patterns Abstract Factory Builder Factory Method Lazy Initialization Prototype Singleton

Slide 13

Slide 13 text

Structural Patterns Adapter Bridge Composite Decorator Facade Flyweight Proxy

Slide 14

Slide 14 text

Behavioral Patterns Chain of Responsability Command Interpreter Iterator Mediator Memento Observer State Strategy Template Method Visitor

Slide 15

Slide 15 text

Design Patterns applied to Symfony

Slide 16

Slide 16 text

Creational Patterns

Slide 17

Slide 17 text

Factory Method

Slide 18

Slide 18 text

Define an interface for creating an object, but let subclasses decide which class to instantiate.

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

Form Component

Slide 21

Slide 21 text

ResolvedFormTypeFactoryInterface + createResolvedFormType(…) Product ResolvedFormType ResolvedFormTypeFactory + createResolvedFormType(…) Resolving form field types inheritance

Slide 22

Slide 22 text

namespace Symfony\Component\Form; class ResolvedFormTypeFactory implements ResolvedFormTypeFactoryInterface { public function createResolvedType( FormTypeInterface $type, array $typeExtensions, ResolvedFormTypeInterface $parent = null ) { return new ResolvedFormType( $type, $typeExtensions, $parent ); } }

Slide 23

Slide 23 text

$f = new ResolvedFormTypeFactory(); $form = $f->createResolvedType(new FormType()); $date = $f->createResolvedType(new DateType(), [], $form); $bday = $f->createResolvedType(new BirthdayType(), [], $date);

Slide 24

Slide 24 text

ResolvedFormTypeFactoryInterface + createResolvedFormType(…) Product ResolvedTypeDataCollectorProxy ResolvedTypeFactoryDataCollectorProxy + createResolvedFormType(…) Collecting Resolved Types States

Slide 25

Slide 25 text

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 ); } }

Slide 26

Slide 26 text

$factory = new ResolvedTypeDataCollectorProxyFactory( new ResolvedFormTypeFactory(), new FormDataCollector(…) ); $form = $f->createResolvedType(new FormType()); $date = $f->createResolvedType(new DateType(), [], $form); $bday = $f->createResolvedType(new BirthdayType(), [], $date);

Slide 27

Slide 27 text

The Form Profiler Debug Panel

Slide 28

Slide 28 text

Lazy Initialization

Slide 29

Slide 29 text

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.

Slide 30

Slide 30 text

Why using it? Optimizing performance Saving memory consumption Opening connections when really needed Getting information on-demand

Slide 31

Slide 31 text

Dependency Injection Component

Slide 32

Slide 32 text

The Service Container The service container allows you to standardize and centralize the way objects are constructed in your application.

Slide 33

Slide 33 text

Lazy Initialization + Factory Method = Service Container

Slide 34

Slide 34 text

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)); } }

Slide 35

Slide 35 text

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; } }

Slide 36

Slide 36 text

// 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);

Slide 37

Slide 37 text

Structural Patterns

Slide 38

Slide 38 text

Adapter

Slide 39

Slide 39 text

The adapter pattern allows the interface of an existing class to be used from another interface.

Slide 40

Slide 40 text

Why using it? Make classes work with others without changing their code.

Slide 41

Slide 41 text

Examples Adapting several database vendors Adapting a new version of a REST API Offering a backward compatibility layer

Slide 42

Slide 42 text

Combining heterogenous systems

Slide 43

Slide 43 text

Adapting one to the other

Slide 44

Slide 44 text

Security Component

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

The old CSRF Management API namespace Symfony\Component\Form\Extension\Csrf\CsrfProvider; interface CsrfProviderInterface { public function generateCsrfToken($intention); public function isCsrfTokenValid($intention, $token); }

Slide 47

Slide 47 text

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); } }

Slide 48

Slide 48 text

The old CSRF Management API (< 2.4) $provider = new DefaultCsrfProvider('SecretCode'); $csrfToken = $provider ->generateCsrfToken('intention') ; $csrfValid = $provider ->isCsrfTokenValid('intention', $token) ;

Slide 49

Slide 49 text

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); }

Slide 50

Slide 50 text

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; } }

Slide 51

Slide 51 text

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'); } }

Slide 52

Slide 52 text

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() ); } }

Slide 53

Slide 53 text

Composite

Slide 54

Slide 54 text

Composite lets clients treat individual objects and compositions of objects uniformly.

Slide 55

Slide 55 text

No content

Slide 56

Slide 56 text

Why using it? Representing trees of objects uniformely

Slide 57

Slide 57 text

Examples Representing a binary tree Representing a multi level navigation bar Parsing an XML/HTML document Designing & validating nested forms …

Slide 58

Slide 58 text

Form Component

Slide 59

Slide 59 text

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.

Slide 60

Slide 60 text

Form (name) Form (description) Form (caption) Form (image) Form (product) Form (picture)

Slide 61

Slide 61 text

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; } }

Slide 62

Slide 62 text

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; } }

Slide 63

Slide 63 text

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);

Slide 64

Slide 64 text

Submitting the form data $form->submit(array( 'name' => 'Apple Macbook Air 11', 'description' => 'The thinest laptop', 'picture' => array( 'caption' => 'The new Macbook Air.', ), ));

Slide 65

Slide 65 text

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); } } } }

Slide 66

Slide 66 text

Decorator

Slide 67

Slide 67 text

Adding responsibilities to objects without subclassing their classes.

Slide 68

Slide 68 text

Why using it? Extending objects without bloating the code Making code reusable and composable Avoiding vertical inheritance

Slide 69

Slide 69 text

Examples Adding some caching capabilities Adding some logging capabilities Applying discount strategies to an order Decorating/wrapping a string content …

Slide 70

Slide 70 text

No content

Slide 71

Slide 71 text

HttpKernel Component

Slide 72

Slide 72 text

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.

Slide 73

Slide 73 text

HttpKernelInterface HttpKernel! BasicRateDiscount! handle($request)! handle(Request)! httpKernel! getAmount()! HttpCache! + handle(Request)!

Slide 74

Slide 74 text

// 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() ;

Slide 75

Slide 75 text

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; } }

Slide 76

Slide 76 text

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; } }

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

Behavioral Patterns

Slide 79

Slide 79 text

Iterator

Slide 80

Slide 80 text

The iterator pattern allows to traverse a container and access its elements.

Slide 81

Slide 81 text

Why using it? Easing iterations over collections of objects Filtering records in a collection …

Slide 82

Slide 82 text

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 …

Slide 83

Slide 83 text

interface Iterator { public function current(); public function next(); public function rewind(); public function valid(); public function key(); }

Slide 84

Slide 84 text

Finder Component

Slide 85

Slide 85 text

The Finder $iterator = Finder::create() ->files() ->name('*.php') ->depth(0) ->size('>= 1K') ->in(__DIR__); foreach ($iterator as $file) { print $file->getRealpath()."\n"; }

Slide 86

Slide 86 text

├── 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

Slide 87

Slide 87 text

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 );

Slide 88

Slide 88 text

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; } }

Slide 89

Slide 89 text

Mediator

Slide 90

Slide 90 text

The mediator pattern defines an object that encapsulates how a set of objects interact.

Slide 91

Slide 91 text

No content

Slide 92

Slide 92 text

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 …

Slide 93

Slide 93 text

Examples Dispatching events when an object’s state changes Hooking new responsabilities to a model object Filtering some input data

Slide 94

Slide 94 text

No content

Slide 95

Slide 95 text

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); } }

Slide 96

Slide 96 text

What’s wrong with this code? Too many responsabilities Not easy extensible Difficulty to unit test Bloated code

Slide 97

Slide 97 text

EventDispatcher Component

Slide 98

Slide 98 text

The event dispatcher manages connections between a subject and its attached observers.

Slide 99

Slide 99 text

Event Dispatcher E1   Emits a specific event to notify and execute its listeners. L2 L3 L4 L1

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

Plugin / hook Filtering data Decoupling code Main usages

Slide 102

Slide 102 text

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); }

Slide 103

Slide 103 text

The Event Class class Event { function isPropagationStopped(); function stopPropagation(); }

Slide 104

Slide 104 text

Dispatching Events $dp = new EventDispatcher(); $dp->dispatch('order.paid', new Event()); $dp->dispatch('order.cancelled', new Event()); $dp->dispatch('order.refunded', new Event());

Slide 105

Slide 105 text

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());

Slide 106

Slide 106 text

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); } }

Slide 107

Slide 107 text

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; } }

Slide 108

Slide 108 text

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); } }

Slide 109

Slide 109 text

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); } }

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

HttpKernel Events Workflow

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

Strategy

Slide 115

Slide 115 text

The strategy pattern encapsulates algorithms of the same nature into dedicated classes to make them interchangeable.

Slide 116

Slide 116 text

HttpKernel Component

Slide 117

Slide 117 text

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…).

Slide 118

Slide 118 text

namespace Symfony\Component\HttpKernel\Profiler; interface ProfilerStorageInterface { function find($ip, ...); function read($token); function write(Profile $profile); function purge(); } Designing the profiler storage strategy

Slide 119

Slide 119 text

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

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

// 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

Slide 122

Slide 122 text

Template Method

Slide 123

Slide 123 text

Let subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.

Slide 124

Slide 124 text

+ templateMethod() [final]! # stepOne()! # stepTwo()! # stepThree()! # stepOne()! # stepTwo()! # stepThree()! # stepOne()! # stepTwo()! # stepThree()!

Slide 125

Slide 125 text

Security Component

Slide 126

Slide 126 text

AbstractAuthenticationListener! SimpleFormAuthenticationListener UsernamePasswordFormAuthenticationListener + handle(GetResponseEvent $event) [final] # attemptAuthentication(Request $request) # attemptAuthentication(Request $request) # attemptAuthentication(Request $request)

Slide 127

Slide 127 text

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); }

Slide 128

Slide 128 text

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

Slide 129

Slide 129 text

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

Slide 130

Slide 130 text

Conclusion…

Slide 131

Slide 131 text

No content