Slide 1

Slide 1 text

Solving Real World Problems with Design Patterns. Confoo 2017 / Dec. 6th / Vancouver / Canada Hugo Hamon h"ps://www.flickr.com/photos/maelick/15302490013/  

Slide 2

Slide 2 text

Hugo Hamon

Slide 3

Slide 3 text

Hugo Hamon Senior Software Developer 15 years of PHP experience 10 years of Symfony experience Conferences speaker @hhamon on social networks Books (co) author

Slide 4

Slide 4 text

https://speakerdeck.com/hhamon

Slide 5

Slide 5 text

Introduction

Slide 6

Slide 6 text

SOLID Principles

Slide 7

Slide 7 text

Single Responsability Open / Closed Liskov Substitution Interface Segregation Dependency Inversion

Slide 8

Slide 8 text

What are Design Patterns?

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

Creational Abstract Factory Builder Factory Method Prototype Singleton Creational design patterns are responsible for encapsulating the algorithms for producing and assembling objects. Patterns

Slide 11

Slide 11 text

Structural Adapter Bridge Composite Decorator Facade Flyweight Proxy Structural design patterns organize classes in a way to separate their implementations from their interfaces. Patterns

Slide 12

Slide 12 text

Behavioral Chain of Responsibility Command Interpreter Iterator Mediator Memento Observer State Strategy Template Method Visitor Behavioral design patterns organize objects to make them collaborate together while reducing their coupling. Patterns

Slide 13

Slide 13 text

What are the pros & cons of design patterns?

Slide 14

Slide 14 text

  Communication   Code Testability Maintainability Loose Coupling   …   Hard to Teach   Hard to Learn   Hard to Apply   Entry Barrier   …

Slide 15

Slide 15 text

Patterns are not always the holly grail!!!

Slide 16

Slide 16 text

Composite Composite pattern enables to treat individual objects and groups of objects of the same type the same way with a uniform common interface.

Slide 17

Slide 17 text

Tailored for hierarchical data structures Family Tree File Explorer Organization Chart Content Management Book XML Parsing Navigation Bars etc. Web Forms Products Combos

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

Loading Routes Definitions from Configuration Files

Slide 20

Slide 20 text

namespace Routing; use Routing\Exception\RouteNotFoundException; final class UrlMatcher implements UrlMatcherInterface { private $routes; public function __construct(RouteCollection $routes) { $this->routes = $this->sortByPath($routes); } public function match(Request $request): array { if (!isset($this->routes[$path = $request->getPathInfo()])) { throw RouteNotFoundException::forUnsupportedPath($path); } return $this->routes[$path]; } }

Slide 21

Slide 21 text

# config/routes.php use Routing\Route; use Routing\RouteCollection; $routes = new RouteCollection(); $route1 = new Route('/contact', 'App\\Action\\ContactDisplay'); $route1->setMethods(['GET']); $routes->add('get_contact', $route1); $route2 = new Route('/contact', 'App\\Action\\ContactProcess'); $route2->setMethods(['POST']); $routes->add('post_contact', $route2); return $routes;

Slide 22

Slide 22 text

use Routing\UrlMatcher; use Routing\Loader\PhpFileLoader; $loader = new PhpFileLoader(); $routes = $loader->load('config/routes.php'); $router = new UrlMatcher($routes); $infos = $router->match(new Request('GET', '/home'));

Slide 23

Slide 23 text

namespace Routing\Loader; use Routing\Exception\UnsupportedFileException; use Routing\RouteCollection; class PhpFileLoader implements FileLoaderInterface { public function load(string $path): RouteCollection { if (!file_exists($path)) { throw UnsupportedFileException::forUnreadableFile($path); } if ('php' !== pathinfo($path, PATHINFO_EXTENSION)) { throw UnsupportedFileException::forUnsupportedType($path, 'php'); } $routes = require $path; if (!$routes instanceof RouteCollection) { throw UnsupportedFileException::forUnexpectedData($path, $routes); } return $routes; } }

Slide 24

Slide 24 text

Slide 25

Slide 25 text

class XmlFileLoader implements FileLoaderInterface { public function load(string $path): RouteCollection { // ... try { $xml = new \SimpleXMLElement(file_get_contents($path)); } catch (\Exception $e) { throw UnsupportedFileException::forUnreadableFile($path, $e); } $routes = new RouteCollection(); foreach ($xml->route as $route) { $routes->add($route['name'], $this->makeRoute($route)); } return $routes; } }

Slide 26

Slide 26 text

use Routing\UrlMatcher; use Routing\Loader\XmlFileLoader; $loader = new XmlFileLoader(); $routes = $loader->load('config/routes.xml'); $router = new UrlMatcher($routes); $infos = $router->match(new Request('GET', '/home'));

Slide 27

Slide 27 text

What if the file loader must be chosen at runtime?

Slide 28

Slide 28 text

use Routing\Loader\DelegatingLoader; use Routing\Loader\PhpFileLoader; use Routing\Loader\XmlFileLoader; $loader = new DelegatingLoader(); $loader->add(new PhpFileLoader()); $loader->add(new XmlFileLoader()); $routes = $loader->load('config/routes.xml'); $routes = $loader->load('config/routes.php');

Slide 29

Slide 29 text

class DelegatingLoader implements FileLoaderInterface { private $loaders = []; public function add(FileLoaderInterface $loader): void { $this->loaders[] = $loader; } // ... }

Slide 30

Slide 30 text

class DelegatingLoader implements FileLoaderInterface { // ... public function load(string $path): RouteCollection { foreach ($this->loaders as $loader) { if ($routes = $this->tryLoader($path, $loader)) { return $routes; } } throw UnsupportedFileException::forUnsupportedType($path); } private function tryLoader(string $path, FileLoaderInterface $loader): ?RouteCollection { try { return $loader->load($path); } catch (UnsupportedFileException $exception) { return null; } } }

Slide 31

Slide 31 text

Builder Builder pattern separates the construction of a complex object from its representation so that the same construction process can create different representations.

Slide 32

Slide 32 text

$matcher = UrlMatcherBuilder::create() ->load('config/routes.xml') ->add('/home', 'homepage', 'App\\Action\\Homepage') ->add('/about', 'about', 'App\\Action\\About') ->load('/config/routes.php') ->build() ; $infos = $router->match(new Request('GET', '/home'));

Slide 33

Slide 33 text

final class UrlMatcherBuilder implements UrlMatcherBuilderInterface { private $loader; private $routes; public function __construct(FileLoaderInterface $loader) { $this->loader = $loader; $this->routes = new RouteCollection(); } public static function create(): self { $loader = new DelegatingLoader(); $loader->add(new XmlFileLoader()); $loader->add(new PhpFileLoader()); return new self($loader); } }

Slide 34

Slide 34 text

class UrlMatcherBuilder { public function load(string $path): self { $this->routes->merge($this->loader->load($path)); return $this; } public function add(string $path, string $name, callable $callback): self { $this->routes->add(new Route($path, $name, $callback)); return $this; } public function build(): UrlMatcherInterface { return new UrlMatcher($this->routes); } }

Slide 35

Slide 35 text

Decorator Decorator pattern enables to dynamically attach new responsabilities to an object without changing its class.

Slide 36

Slide 36 text

Decorator leverages Object Composition over Class Inheritance.

Slide 37

Slide 37 text

No content

Slide 38

Slide 38 text

$instance = new OtherConcreteDecorator( new SomeConcreteDecorator( new ConcreteDecorator( new DecoratedComponent() ) ) ); $instance->operation();

Slide 39

Slide 39 text

http://stackphp.com/

Slide 40

Slide 40 text

Simple Object Decoration Example

Slide 41

Slide 41 text

// ... use Psr\Log\LoggerInterface; class LoggableUrlMatcher implements UrlMatcherInterface { private $urlMatcher; private $logger; public function __construct( UrlMatcherInterface $urlMatcher, LoggerInterface $logger ) { $this->urlMatcher = $urlMatcher; $this->logger = $logger; } }

Slide 42

Slide 42 text

class LoggableUrlMatcher implements UrlMatcherInterface { // ... public function match(Request $request): array { $this->logger->info(sprintf('Matching path "%s".', $request->getPathInfo())); try { $infos = $this->urlMatcher->match($request); $this->logger->info(sprintf('Matched route "%s".', $infos['_route'])); } catch (RoutingException $e) { $this->logger->info($e->getMessage()); throw $e; } return $infos; } }

Slide 43

Slide 43 text

$loader = new PhpFileLoader(); $routes = $loader->load('/config/routes.php'); $matcher = new LoggableUrlMatcher( new UrlMatcher($routes), new Logger('/tmp/app.log') ); $matcher->match(new Request(...));

Slide 44

Slide 44 text

Making Infinite Object Responsabilities Combinations

Slide 45

Slide 45 text

Coupon Codes Management Consider an e-commerce website offering coupon codes for customers to reduce their orders. Coupons are accepted and will discount an order total amount if their restrictions constraints are validated.

Slide 46

Slide 46 text

Coupons Restrictions ü  Limited lifetime period, ü  Minimum total amount required, ü  Minimum quantity of ordered items required, ü  Valid for a specific geographical area, ü  Customer must own the loyalty membership card, ü  Coupon is valid for Premium / VIP customers only, ü  Valid for the customer’s very first order, ü  Some products are not eligible for discounts, ü  Valid only on some specific products areas (luxury, food…), ü  etc.

Slide 47

Slide 47 text

No content

Slide 48

Slide 48 text

interface CouponInterface { public function getCode(): string; /** * Returns the new total amount after the coupon has been * applied on the given order. * * @param OrderableInterface $order The order to discount * * @return Money The new order total amount * * @throws CouponException When coupon is not applicable */ public function applyDiscount(OrderableInterface $order): Money; }

Slide 49

Slide 49 text

Modeling Coupon Objects Classes

Slide 50

Slide 50 text

class ValueCoupon implements CouponInterface { private $code; private $discount; public function __construct(string $code, Money $discount) { $this->code = $code; $this->discount = $discount; } public function getCode(): string { return $this->code; } public function applyDiscount(OrderableInterface $order): Money { return $order->getTotalAmount()->subtract($this->discount); } } Modelling a Value Coupon

Slide 51

Slide 51 text

class RateCoupon implements CouponInterface { private $code; private $rate; public function __construct(string $code, float $rate) { Assertion::greaterThan($rate, 0); Assertion::lowerThanOrEqual($rate, 1); $this->code = $code; $this->rate = $rate; } } Modelling a Rate Coupon

Slide 52

Slide 52 text

class RateCoupon implements CouponInterface { // ... public function getCode() { return $this->code; } public function applyDiscount(OrderableInterface $order): Money { $amount = $order->getTotalAmount(); return $amount->subtract($amount->multiply($this->rate)); } }

Slide 53

Slide 53 text

abstract class CouponDecorator implements CouponInterface { protected $coupon; public function __construct(CouponInterface $coupon) { $this->coupon = $coupon; } public function getCode(): string { return $this->coupon->getCode(); } protected function createCouponException(string $message, \Throwable $previous = null) { return new CouponException($message, 0, $previous); } } Modelling a Coupon Decorator

Slide 54

Slide 54 text

Modeling Coupon Restrictions as Coupon Decorators

Slide 55

Slide 55 text

class LimitedLifetimeCoupon extends CouponDecorator { private $period; function __construct(CouponInterface $coupon, DateTimePeriod $period) { parent::__construct($coupon); $this->period = $period; } public function applyDiscount(OrderableInterface $order): Money { // ... Add restriction logic here. return $this->coupon->applyDiscount($order); } }

Slide 56

Slide 56 text

class LimitedLifetimeCoupon extends CouponDecorator { // ... public function applyDiscount(OrderableInterface $order): Money { if (!$this->period->isStarted()) { throw $this->createCouponException(sprintf( 'Coupon is usable from %s.', $this->period->getStartDate() )); } if ($this->period->isFinished()) { throw $this->createCouponException(sprintf( 'Coupon was valid until %s.', $this->period->getDueDate() )); } return $this->coupon->applyDiscount($order); } }

Slide 57

Slide 57 text

use SebastianBergmann\Money\Money; use Shop\Discount\LimitedLifetimeCoupon; use Shop\Discount\RateCoupon; use Shop\Discount\ValueCoupon; // Coupon offers a discount value of 20 €. $coupon1 = new LimitedLifetimeCoupon( new ValueCoupon('3s2h7pd65s', Money::fromString('20.00', EUR')), DateTimePeriod::fromString('FROM 2017-12-01 TO 2017-12-31'); ); // Coupon offers 25% off. $coupon2 = new LimitedLifetimeCoupon( new RateCoupon('76cqa6qr19', 0.25), DateTimePeriod::fromString('FROM 2017-12-01 TO 2017-12-31'); );

Slide 58

Slide 58 text

class MinimumPurchaseAmountCoupon extends CouponDecorator { private $minimumAmount; function __construct(CouponInterface $coupon, Money $minAmount) { parent::__construct($coupon); $this->minimumAmount = $minAmount; } public function applyDiscount(OrderableInterface $order): Money { $amount = $order->getTotalAmount(); if ($amount->lessThan($this->minimumAmount)) { throw $this->createCouponException(...); } return $this->coupon->applyDiscount($order); } }

Slide 59

Slide 59 text

use SebastianBergmann\Money\Money; use Shop\Discount\MinimumPurchaseAmountCoupon; use Shop\Discount\RateCoupon; use Shop\Discount\ValueCoupon; // Get 20 € off if total amount is greater than 300 € $coupon1 = new MinimumPurchaseAmountCoupon( new ValueCoupon('3s2h7pd65s', new EUR('20.00')), new EUR('300.00') ); // Get 25% off if total amount is greater than 300 € $coupon2 = new MinimumPurchaseAmountCoupon( new RateCoupon('76cqa6qr19', 0.25), new EUR('300.00') );

Slide 60

Slide 60 text

// ... class CustomerFirstOrderCoupon extends CouponDecorator { public function applyDiscount(OrderableInterface $order) { $customer = $order->getCustomer(); if ($customer->hasPastOrders()) { throw $this->createCouponException( 'Customer already has past orders.’ ); } return $this->coupon->applyDiscount($order); } }

Slide 61

Slide 61 text

use SebastianBergmann\Money\Money; use Shop\Discount\CustomerFirstOrderCoupon; use Shop\Discount\RateCoupon; use Shop\Discount\ValueCoupon; // Get 20 € off on the first order $coupon1 = new CustomerFirstOrderCoupon( new ValueCoupon( '3s2h7pd65s', new EUR('20.00') ) ); // Get 25% off on the first order $coupon2 = new CustomerFirstOrderCoupon( new RateCoupon('76cqa6qr19', 0.25) );

Slide 62

Slide 62 text

// Create the special coupon $coupon = new CustomerFirstOrderCoupon( new MinimumPurchaseAmountCoupon( new LimitedLifetimeCoupon( new ValueCoupon('3s2h7pd65s', new EUR('20.00')), DateTimePeriod::until('+60 days') ), new EUR('170.00') ) ); // Create the order instance $customer = new \Shop\Customer('[email protected]'); $order = new \Shop\Order($customer, new EUR('200.00')); // Apply discount coupon on the order $discountedAmount = $coupon->applyDiscount($order);

Slide 63

Slide 63 text

Easy to setup Infinite combinations Unchanged existing code Loose coupling   SOLID compliant   …   Large objects graphs   Proxy objects   Fluent interfaces   …

Slide 64

Slide 64 text

Abstract Factory Abstract Factory provides an interface for creating families of related or dependent objects without specifying their concrete classes.

Slide 65

Slide 65 text

2016 - Twig 1.x 2015 – Symfony 3.0 2012 – Symfony 2.3

Slide 66

Slide 66 text

Both assessments have similarities -­‐ Symfony  2.3  /  3.0 Twig Pricing €250 (USA, Europe, UAE, Japan, etc.) €200 (Brazil, Tunisia, India, etc.) €149 (no country restriction) Eligibility Conditions Candidate must be at least 18 y.o Candidate must not be Expert Certified Up to 2 sessions max per civil year No active exam registration 1 week blank period between 2 exams Candidate must be at least 18 y.o Candidate must not be Expert Certified No active exam registration 1 month blank period between 2 exams Levels Expert Symfony Developer Advanced Symfony Developer Expert Twig Web Designer

Slide 67

Slide 67 text

First Implementation Attempt…

Slide 68

Slide 68 text

class PlaceOrderCommandHandler { private $symfonyPricer; private $twigPricer; // ... public function handle(PlaceOrderCommand $command): void { $order = new Order(); $order->setQuantity($command->getQuantity()); $order->setUnitPrice($this->getUnitPrice($command)); // ... } }

Slide 69

Slide 69 text

class PlaceOrderCommandHandler { // ... private function getUnitPrice(PlaceOrderCommand $command): Money { $country = $command->getCountry(); switch ($code = $command->getExamSeriesType()) { case 'TWXCE': return $this->twigPricer->getUnitPrice($country); case 'SFXCE': return $this->symfonyPricer->getUnitPrice($country); } throw new UnsupportedAssessmentException($code); } }

Slide 70

Slide 70 text

No content

Slide 71

Slide 71 text

Introducing an Abstract Factory Implementation

Slide 72

Slide 72 text

No content

Slide 73

Slide 73 text

No content

Slide 74

Slide 74 text

Defining all main common interfaces

Slide 75

Slide 75 text

namespace Certification; interface CertificationFactoryInterface { public function createEligibilityChecker() : CertificationEligibilityCheckerInterface; public function createTicketPricer() : CertificationTicketPricerInterface; public function createAuthority() : CertificationAuthorityInterface; }

Slide 76

Slide 76 text

namespace Certification; use SebastianBergmann\Money\Money; interface CertificationTicketPricerInterface { public function getUnitPrice(string $country): Money; public function getTotalPrice(string $country, int $quantity): Money; }

Slide 77

Slide 77 text

namespace Certification; use Certification\Domain\AssessmentResult; use Certification\Exception\CandidateNotCertifiedException; use Certification\Exception\UnsupportedAssessmentException; interface CertificationAuthorityInterface { /** * Returns the candidate's certification level. * * @throws CandidateNotCertifiedException * @throws UnsupportedAssessmentException */ public function getCandidateLevel(AssessmentResult $result): string; }

Slide 78

Slide 78 text

Varying Certifications Tickets Pricing

Slide 79

Slide 79 text

$pricer = new \Certification\Symfony\TicketPricer( Money::fromString('250', 'EUR'), Money::fromString('200', 'EUR') ); $price = $pricer->getUnitPrice('FR'); // €250 $price = $pricer->getUnitPrice('TN'); // €200 $pricer = new \Certification\Twig\TicketPricer( Money::fromString('149', 'EUR') ); $price = $pricer->getUnitPrice('FR'); // €149 $price = $pricer->getUnitPrice('TN'); // €149

Slide 80

Slide 80 text

namespace Certification; use SebastianBergmann\Money\Money; abstract class AbstractTicketPricer implements CertificationTicketPricerInterface { /** * Returns the total price for the given quantity. */ public function getTotalPrice(string $country, int $quantity): Money { if ($quantity < 1) { throw new \InvalidTicketQuantityException($quantity); } return $this->getUnitPrice($country)->multiply($quantity); } }

Slide 81

Slide 81 text

namespace Certification\Twig; use SebastianBergmann\Money\Money; use Certification\AbstractTicketPricer; class TicketPricer extends AbstractTicketPricer { private $price; public function __construct(Money $price) { $this->price = $price; } public function getUnitPrice(string $country): Money { return $this->price; } }

Slide 82

Slide 82 text

namespace Certification\Symfony; use SebastianBergmann\Money\Money; use Certification\AbstractTicketPricer; class TicketPricer extends AbstractTicketPricer { private $regularPrice; private $discountPrice; public function __construct(Money $regularPrice, Money $discountPrice) { $this->regularPrice = $regularPrice; $this->discountPrice = $discountPrice; } public function getUnitPrice(string $country): Money { if (in_array($country, ['FR', 'US', 'DE', 'IT', 'CH', 'BE', /* … */])) { return $this->regularPrice; } return $this->discountPrice; } }

Slide 83

Slide 83 text

Varying Certifications Authorities

Slide 84

Slide 84 text

try { $assessment = AssessmentResult::fromCSV('SL038744,SF3CE,35'); $authority = new \Certification\Symfony\Authority(20, 10); $symfonyLevel = $authority->getCandidateLevel($assessment); $assessment = AssessmentResult::fromCSV('SL038744,TW1CE,17'); $authority = new \Certification\Twig\Authority(15); $twigLevel = $authority->getCandidateLevel($assessment); } catch (CandidateNotCertifiedException $e) { // ... } catch (UnsupportedAssessmentException $e) { // ... } catch (\Exception $e) { // ... }

Slide 85

Slide 85 text

namespace Certification\Twig; use Certification\CertificationAuthorityInterface; use Certification\Domain\AssessmentResult; use Certification\Exception\CandidateNotCertifiedException; use Certification\Exception\UnsupportedAssessmentException; class Authority implements CertificationAuthorityInterface { // ... public function getCandidateLevel(AssessmentResult $result): string { if ('TW1CE' !== $assessmentID = $result->getAssessmentID()) { throw new UnsupportedAssessmentException($assessmentID); } if ($result->getScore() < $this->passingScore) { throw new CandidateNotCertifiedException($result->getCandidateID(), $assessmentID); } return 'expert'; } }

Slide 86

Slide 86 text

namespace Certification\Symfony; use Certification\CertificationAuthorityInterface; use Certification\Entity\AssessmentResult; use Certification\Exception\CandidateNotCertifiedException; use Certification\Exception\UnsupportedAssessmentException; class Authority implements CertificationAuthorityInterface { private $expertLevelPassingScore; private $advancedLevelPassingScore; public function __construct(int $expertScore, int $advancedScore) { $this->expertLevelPassingScore = $expertScore; $this->advancedLevelPassingScore = $advancedScore; } // ... }

Slide 87

Slide 87 text

class Authority implements CertificationAuthorityInterface { public function getCandidateLevel(AssessmentResult $result): string { $assessmentID = $result->getAssessmentID(); if (!in_array($assessmentID, ['SF2CE', 'SF3CE'])) { throw new UnsupportedAssessmentException($assessmentID); } $score = $result->getScore(); if ($score >= $this->expertLevelPassingScore) { return 'expert'; } if ($score >= $this->advancedLevelPassingScore) { return 'advanced'; } throw new CandidateNotCertifiedException($result->getCandidateID(), $assessmentID); } // ... }

Slide 88

Slide 88 text

Implementing Concrete Factories

Slide 89

Slide 89 text

No content

Slide 90

Slide 90 text

No content

Slide 91

Slide 91 text

namespace Certification\Symfony; use SebastianBergmann\Money\Money; use Certification\CertificationAuthorityInterface; use Certification\CertificationFactoryInterface; // ... class CertificationFactory implements CertificationFactoryInterface { // ... public function createTicketPricer(): CertificationTicketPricerInterface { return new TicketPricer( Money::fromString('250', 'EUR'), Money::fromString('200', 'EUR') ); } public function createAuthority(): CertificationAuthorityInterface { return new Authority(20, 10); } }

Slide 92

Slide 92 text

namespace Certification\Twig; use SebastianBergmann\Money\Money; use Certification\CertificationAuthorityInterface; use Certification\CertificationFactoryInterface; // ... class CertificationFactory implements CertificationFactoryInterface { // ... public function createTicketPricer(): CertificationTicketPricerInterface { return new TicketPricer(Money::fromString('149', 'EUR')); } public function createAuthority(): CertificationAuthorityInterface { return new Authority(20); } }

Slide 93

Slide 93 text

$assessmentFamily = 'SFXCE'; // Choose the right factory switch ($assessmentFamily) { case 'TWXCE': $factory = new TwigCertificationFactory(...); break; case 'SFXCE': $factory = new SymfonyCertificationFactory(...); break; default: throw new UnsupportedAssessmentFamilyException($assessmentFamily); } // Request and use the factory services $pricer = $factory->createTicketPricer(); $checker = $factory->createEligibilityChecker(); $authority = $factory->createAuthority();

Slide 94

Slide 94 text

The Giga Factory

Slide 95

Slide 95 text

namespace Certification; use Certification\Exception\UnsupportedAssessmentException; //... class CertificationFactoryFactory implements CertificationFactoryFactoryInterface { private $factories = []; public function getFactory(string $assessment): CertificationFactoryInterface { if (isset($this->factories[$assessment])) { return $this->factories[$assessment]; } if ('TW1CE' === $assessment) { return $this->createTwigCertificationFactory($assessment); } if (in_array($assessment, ['SF2CE', 'SF3CE'], true)) { return $this->createSymfonyCertificationFactory($assessment); } throw new UnsupportedAssessmentException($assessment); } }

Slide 96

Slide 96 text

// ... use Certification\Symfony\CertificationFactory as SymfonyCertificationFactory; use Certification\Twig\CertificationFactory as TwigCertificationFactory; class CertificationFactoryFactory implements CertificationFactoryFactoryInterface { private $factories = []; public function getFactory(string $assessment): CertificationFactoryInterface { if (isset($this->factories[$assessment])) { return $this->factories[$assessment]; } if ('TW1CE' === $assessment) { return $this->createTwigCertificationFactory($assessment); } if (in_array($assessment, ['SF2CE', 'SF3CE'], true)) { return $this->createSymfonyCertificationFactory($assessment); } throw new UnsupportedAssessmentException($assessment); } }

Slide 97

Slide 97 text

class CertificationFactoryFactory implements CertificationFactoryFactoryInterface { // ... private function createTwigCertificationFactory(string $assessment) : CertificationFactoryInterface { $this->factories[$assessment] = $f = new TwigCertificationFactory(...); return $f; } private function createSymfonyCertificationFactory(string $assessment) : CertificationFactoryInterface { $this->factories[$assessment] = $f = new SymfonyCertificationFactory(...); return $f; } }

Slide 98

Slide 98 text

class CertificationFactoryFactory implements CertificationFactoryFactoryInterface { // ... public function createCertificationTicketPricer(string $assessment) : CertificationTicketPricerInterface { return $this->getFactory($assessment)->createTicketPricer(); } public function createCertificationAuthority(string $assessment) : CertificationAuthorityInterface { return $this->getFactory($assessment)->createCertificationAuthority(); } public function createCertificationEligibilityChecker(string $assessment) : CertificationEligibilityCheckerInterface { return $this->getFactory($assessment)->createEligibilityChecker(); } }