DesignPatternsPHP Documentation

如果无法正常显示,请先停止浏览器的去广告插件。
分享至:
1. DesignPatternsPHP Documentation Release 1.0 Dominik Liebler and contributors Sep 12, 2018
2.
3. Contents 1 Patterns 1.1 Creational . . . . . . . . . . . . 1.1.1 Abstract Factory . . . . 1.1.2 Builder . . . . . . . . . 1.1.3 Factory Method . . . . . 1.1.4 Multiton . . . . . . . . . 1.1.5 Pool . . . . . . . . . . . 1.1.6 Prototype . . . . . . . . 1.1.7 Simple Factory . . . . . 1.1.8 Singleton . . . . . . . . 1.1.9 Static Factory . . . . . . 1.2 Structural . . . . . . . . . . . . . 1.2.1 Adapter / Wrapper . . . 1.2.2 Bridge . . . . . . . . . . 1.2.3 Composite . . . . . . . . 1.2.4 Data Mapper . . . . . . 1.2.5 Decorator . . . . . . . . 1.2.6 Dependency Injection . . 1.2.7 Facade . . . . . . . . . . 1.2.8 Fluent Interface . . . . . 1.2.9 Flyweight . . . . . . . . 1.2.10 Proxy . . . . . . . . . . 1.2.11 Registry . . . . . . . . . 1.3 Behavioral . . . . . . . . . . . . 1.3.1 Chain Of Responsibilities 1.3.2 Command . . . . . . . . 1.3.3 Iterator . . . . . . . . . 1.3.4 Mediator . . . . . . . . 1.3.5 Memento . . . . . . . . 1.3.6 Null Object . . . . . . . 1.3.7 Observer . . . . . . . . 1.3.8 Specification . . . . . . 1.3.9 State . . . . . . . . . . . 1.3.10 Strategy . . . . . . . . . 1.3.11 Template Method . . . . 1.3.12 Visitor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3 3 6 12 16 18 21 24 26 28 31 31 37 40 43 48 51 55 58 61 64 68 70 71 75 79 84 88 93 96 98 103 107 111 115 i
4. 1.4 2 ii More . . . . . . . . . . . . . . . . . 1.4.1 Service Locator . . . . . . . 1.4.2 Repository . . . . . . . . . . 1.4.3 Entity-Attribute-Value (EAV) Contribute . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119 119 123 131 137
5. DesignPatternsPHP Documentation, Release 1.0 This is a collection of known design patterns and some sample code how to implement them in PHP. Every pattern has a small list of examples (most of them from Zend Framework, Symfony2 or Doctrine2 as I’m most familiar with this software). I think the problem with patterns is that often people do know them but don’t know when to apply which. Contents 1
6. DesignPatternsPHP Documentation, Release 1.0 2 Contents
7. CHAPTER 1 Patterns The patterns can be structured in roughly three different categories. Please click on the title of every pattern’s page for a full explanation of the pattern on Wikipedia. 1.1 Creational In software engineering, creational design patterns are design patterns that deal with object creation mechanisms, trying to create objects in a manner suitable to the situation. The basic form of object creation could result in design problems or added complexity to the design. Creational design patterns solve this problem by somehow controlling this object creation. 1.1.1 Abstract Factory Purpose To create series of related or dependent objects without specifying their concrete classes. Usually the created classes all implement the same interface. The client of the abstract factory does not care about how these objects are created, he just knows how they go together. 3
8. DesignPatternsPHP Documentation, Release 1.0 UML Diagram Code You can also find this code on GitHub Parser.php 1 <?php 2 3 namespace DesignPatterns\Creational\AbstractFactory; 4 5 6 7 8 interface Parser { public function parse(string $input): array; } CsvParser.php 4 Chapter 1. Patterns
9. DesignPatternsPHP Documentation, Release 1.0 1 <?php 2 3 namespace DesignPatterns\Creational\AbstractFactory; 4 5 6 7 8 class CsvParser implements Parser { const OPTION_CONTAINS_HEADER = true; const OPTION_CONTAINS_NO_HEADER = false; 9 /** * @var bool */ private $skipHeaderLine; 10 11 12 13 14 public function __construct(bool $skipHeaderLine) { $this->skipHeaderLine = $skipHeaderLine; } 15 16 17 18 19 public function parse(string $input): array { $headerWasParsed = false; $parsedLines = []; 20 21 22 23 24 25 26 27 28 foreach (explode(PHP_EOL, $input) as $line) { if (!$headerWasParsed && $this->skipHeaderLine === self::OPTION_CONTAINS_ ˓ HEADER) { → continue; } 29 $parsedLines[] = str_getcsv($line); 30 } 31 32 return $parsedLines; 33 } 34 35 } JsonParser.php 1 <?php 2 3 namespace DesignPatterns\Creational\AbstractFactory; 4 5 6 7 8 9 10 11 class JsonParser implements Parser { public function parse(string $input): array { return json_decode($input, true); } } ParserFactory.php 1 <?php 2 3 namespace DesignPatterns\Creational\AbstractFactory; 4 (continues on next page) 1.1. Creational 5
10. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 5 6 7 8 9 10 class ParserFactory { public function createCsvParser(bool $skipHeaderLine): CsvParser { return new CsvParser($skipHeaderLine); } 11 public function createJsonParser(): JsonParser { return new JsonParser(); } 12 13 14 15 16 } Test Tests/AbstractFactoryTest.php 1 <?php 2 3 namespace DesignPatterns\Creational\AbstractFactory\Tests; 4 5 6 7 8 use use use use DesignPatterns\Creational\AbstractFactory\CsvParser; DesignPatterns\Creational\AbstractFactory\JsonParser; DesignPatterns\Creational\AbstractFactory\ParserFactory; PHPUnit\Framework\TestCase; 9 10 11 12 13 14 15 class AbstractFactoryTest extends TestCase { public function testCanCreateCsvParser() { $factory = new ParserFactory(); $parser = $factory->createCsvParser(CsvParser::OPTION_CONTAINS_HEADER); 16 $this->assertInstanceOf(CsvParser::class, $parser); 17 } 18 19 public function testCanCreateJsonParser() { $factory = new ParserFactory(); $parser = $factory->createJsonParser(); 20 21 22 23 24 $this->assertInstanceOf(JsonParser::class, $parser); 25 } 26 27 } 1.1.2 Builder Purpose Builder is an interface that build parts of a complex object. Sometimes, if the builder has a better knowledge of what it builds, this interface could be an abstract class with default methods (aka adapter). 6 Chapter 1. Patterns
11. DesignPatternsPHP Documentation, Release 1.0 If you have a complex inheritance tree for objects, it is logical to have a complex inheritance tree for builders too. Note: Builders have often a fluent interface, see the mock builder of PHPUnit for example. Examples • PHPUnit: Mock Builder UML Diagram Code You can also find this code on GitHub Director.php 1 <?php 2 3 namespace DesignPatterns\Creational\Builder; 4 5 use DesignPatterns\Creational\Builder\Parts\Vehicle; 6 7 8 9 10 11 12 13 14 15 16 17 18 /** * Director is part of the builder pattern. It knows the interface of the builder * and builds a complex object with the help of the builder * * You can also inject many builders instead of one to build more complex objects */ class Director { public function build(BuilderInterface $builder): Vehicle { $builder->createVehicle(); $builder->addDoors(); (continues on next page) 1.1. Creational 7
12. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) $builder->addEngine(); $builder->addWheel(); 19 20 21 return $builder->getVehicle(); 22 } 23 24 } BuilderInterface.php 1 <?php 2 3 namespace DesignPatterns\Creational\Builder; 4 5 use DesignPatterns\Creational\Builder\Parts\Vehicle; 6 7 8 9 interface BuilderInterface { public function createVehicle(); 10 public function addWheel(); 11 12 public function addEngine(); 13 14 public function addDoors(); 15 16 public function getVehicle(): Vehicle; 17 18 } TruckBuilder.php 1 <?php 2 3 namespace DesignPatterns\Creational\Builder; 4 5 use DesignPatterns\Creational\Builder\Parts\Vehicle; 6 7 8 9 10 11 12 class TruckBuilder implements BuilderInterface { /** * @var Parts\Truck */ private $truck; 13 public function addDoors() { $this->truck->setPart('rightDoor', new Parts\Door()); $this->truck->setPart('leftDoor', new Parts\Door()); } 14 15 16 17 18 19 public function addEngine() { $this->truck->setPart('truckEngine', new Parts\Engine()); } 20 21 22 23 24 public function addWheel() { $this->truck->setPart('wheel1', new Parts\Wheel()); 25 26 27 (continues on next page) 8 Chapter 1. Patterns
13. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) $this->truck->setPart('wheel2', $this->truck->setPart('wheel3', $this->truck->setPart('wheel4', $this->truck->setPart('wheel5', $this->truck->setPart('wheel6', 28 29 30 31 32 new new new new new Parts\Wheel()); Parts\Wheel()); Parts\Wheel()); Parts\Wheel()); Parts\Wheel()); } 33 34 public function createVehicle() { $this->truck = new Parts\Truck(); } 35 36 37 38 39 public function getVehicle(): Vehicle { return $this->truck; } 40 41 42 43 44 } CarBuilder.php 1 <?php 2 3 namespace DesignPatterns\Creational\Builder; 4 5 use DesignPatterns\Creational\Builder\Parts\Vehicle; 6 7 8 9 10 11 12 class CarBuilder implements BuilderInterface { /** * @var Parts\Car */ private $car; 13 14 15 16 17 18 19 public function addDoors() { $this->car->setPart('rightDoor', new Parts\Door()); $this->car->setPart('leftDoor', new Parts\Door()); $this->car->setPart('trunkLid', new Parts\Door()); } 20 21 22 23 24 public function addEngine() { $this->car->setPart('engine', new Parts\Engine()); } 25 26 27 28 29 30 31 32 public function addWheel() { $this->car->setPart('wheelLF', $this->car->setPart('wheelRF', $this->car->setPart('wheelLR', $this->car->setPart('wheelRR', } new new new new Parts\Wheel()); Parts\Wheel()); Parts\Wheel()); Parts\Wheel()); 33 34 35 36 37 public function createVehicle() { $this->car = new Parts\Car(); } (continues on next page) 1.1. Creational 9
14. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 38 public function getVehicle(): Vehicle { return $this->car; } 39 40 41 42 43 } Parts/Vehicle.php 1 <?php 2 3 namespace DesignPatterns\Creational\Builder\Parts; 4 5 6 7 8 9 10 abstract class Vehicle { /** * @var object[] */ private $data = []; 11 /** * @param string $key * @param object $value */ public function setPart($key, $value) { $this->data[$key] = $value; } 12 13 14 15 16 17 18 19 20 } Parts/Truck.php 1 <?php 2 3 namespace DesignPatterns\Creational\Builder\Parts; 4 5 6 7 class Truck extends Vehicle { } Parts/Car.php 1 <?php 2 3 namespace DesignPatterns\Creational\Builder\Parts; 4 5 6 7 class Car extends Vehicle { } Parts/Engine.php 1 <?php 2 3 namespace DesignPatterns\Creational\Builder\Parts; 4 (continues on next page) 10 Chapter 1. Patterns
15. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 5 6 7 class Engine { } Parts/Wheel.php 1 <?php 2 3 namespace DesignPatterns\Creational\Builder\Parts; 4 5 6 7 class Wheel { } Parts/Door.php 1 <?php 2 3 namespace DesignPatterns\Creational\Builder\Parts; 4 5 6 7 class Door { } Test Tests/DirectorTest.php 1 <?php 2 3 namespace DesignPatterns\Creational\Builder\Tests; 4 5 6 7 8 9 10 use use use use use use DesignPatterns\Creational\Builder\Parts\Car; DesignPatterns\Creational\Builder\Parts\Truck; DesignPatterns\Creational\Builder\TruckBuilder; DesignPatterns\Creational\Builder\CarBuilder; DesignPatterns\Creational\Builder\Director; PHPUnit\Framework\TestCase; 11 12 13 14 15 16 17 class DirectorTest extends TestCase { public function testCanBuildTruck() { $truckBuilder = new TruckBuilder(); $newVehicle = (new Director())->build($truckBuilder); 18 $this->assertInstanceOf(Truck::class, $newVehicle); 19 20 } 21 22 23 24 25 public function testCanBuildCar() { $carBuilder = new CarBuilder(); $newVehicle = (new Director())->build($carBuilder); 26 27 $this->assertInstanceOf(Car::class, $newVehicle); (continues on next page) 1.1. Creational 11
16. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) } 28 29 } 1.1.3 Factory Method Purpose The good point over the SimpleFactory is you can subclass it to implement different ways to create objects. For simple cases, this abstract class could be just an interface. This pattern is a “real” Design Pattern because it achieves the Dependency Inversion principle a.k.a the “D” in SOLID principles. It means the FactoryMethod class depends on abstractions, not concrete classes. This is the real trick compared to SimpleFactory or StaticFactory. 12 Chapter 1. Patterns
17. DesignPatternsPHP Documentation, Release 1.0 UML Diagram Code You can also find this code on GitHub Logger.php 1.1. Creational 13
18. DesignPatternsPHP Documentation, Release 1.0 1 <?php 2 3 namespace DesignPatterns\Creational\FactoryMethod; 4 5 6 7 8 interface Logger { public function log(string $message); } StdoutLogger.php 1 <?php 2 3 namespace DesignPatterns\Creational\FactoryMethod; 4 5 6 7 8 9 10 11 class StdoutLogger implements Logger { public function log(string $message) { echo $message; } } FileLogger.php 1 <?php 2 3 namespace DesignPatterns\Creational\FactoryMethod; 4 5 6 7 8 9 10 class FileLogger implements Logger { /** * @var string */ private $filePath; 11 public function __construct(string $filePath) { $this->filePath = $filePath; } 12 13 14 15 16 public function log(string $message) { file_put_contents($this->filePath, $message . PHP_EOL, FILE_APPEND); } 17 18 19 20 21 } LoggerFactory.php 1 <?php 2 3 namespace DesignPatterns\Creational\FactoryMethod; 4 5 6 7 8 interface LoggerFactory { public function createLogger(): Logger; } 14 Chapter 1. Patterns
19. DesignPatternsPHP Documentation, Release 1.0 StdoutLoggerFactory.php 1 <?php 2 3 namespace DesignPatterns\Creational\FactoryMethod; 4 5 6 7 8 9 10 11 class StdoutLoggerFactory implements LoggerFactory { public function createLogger(): Logger { return new StdoutLogger(); } } FileLoggerFactory.php 1 <?php 2 3 namespace DesignPatterns\Creational\FactoryMethod; 4 5 6 7 8 9 10 class FileLoggerFactory implements LoggerFactory { /** * @var string */ private $filePath; 11 public function __construct(string $filePath) { $this->filePath = $filePath; } 12 13 14 15 16 public function createLogger(): Logger { return new FileLogger($this->filePath); } 17 18 19 20 21 } Test Tests/FactoryMethodTest.php 1 <?php 2 3 namespace DesignPatterns\Creational\FactoryMethod\Tests; 4 5 6 7 8 9 use use use use use DesignPatterns\Creational\FactoryMethod\FileLogger; DesignPatterns\Creational\FactoryMethod\FileLoggerFactory; DesignPatterns\Creational\FactoryMethod\StdoutLogger; DesignPatterns\Creational\FactoryMethod\StdoutLoggerFactory; PHPUnit\Framework\TestCase; 10 11 12 13 14 class FactoryMethodTest extends TestCase { public function testCanCreateStdoutLogging() { (continues on next page) 1.1. Creational 15
20. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) $loggerFactory = new StdoutLoggerFactory(); $logger = $loggerFactory->createLogger(); 15 16 17 $this->assertInstanceOf(StdoutLogger::class, $logger); 18 } 19 20 public function testCanCreateFileLogging() { $loggerFactory = new FileLoggerFactory(sys_get_temp_dir()); $logger = $loggerFactory->createLogger(); 21 22 23 24 25 $this->assertInstanceOf(FileLogger::class, $logger); 26 } 27 28 } 1.1.4 Multiton THIS IS CONSIDERED TO BE AN ANTI-PATTERN! FOR BETTER TESTABILITY AND MAINTAIN- ABILITY USE DEPENDENCY INJECTION! Purpose To have only a list of named instances that are used, like a singleton but with n instances. Examples • 2 DB Connectors, e.g. one for MySQL, the other for SQLite • multiple Loggers (one for debug messages, one for errors) UML Diagram 16 Chapter 1. Patterns
21. DesignPatternsPHP Documentation, Release 1.0 Code You can also find this code on GitHub Multiton.php 1 <?php 2 3 namespace DesignPatterns\Creational\Multiton; 4 5 6 7 8 final class Multiton { const INSTANCE_1 = '1'; const INSTANCE_2 = '2'; 9 /** * @var Multiton[] */ private static $instances = []; 10 11 12 13 14 /** * this is private to prevent from creating arbitrary instances */ private function __construct() { } 15 16 17 18 19 20 21 public static function getInstance(string $instanceName): Multiton { if (!isset(self::$instances[$instanceName])) { self::$instances[$instanceName] = new self(); } 22 23 24 25 26 27 return self::$instances[$instanceName]; 28 } 29 30 /** * prevent instance from being cloned */ private function __clone() { } 31 32 33 34 35 36 37 /** * prevent instance from being unserialized */ private function __wakeup() { } 38 39 40 41 42 43 44 } 1.1. Creational 17
22. DesignPatternsPHP Documentation, Release 1.0 Test 1.1.5 Pool Purpose The object pool pattern is a software creational design pattern that uses a set of initialized objects kept ready to use – a “pool” – rather than allocating and destroying them on demand. A client of the pool will request an object from the pool and perform operations on the returned object. When the client has finished, it returns the object, which is a specific type of factory object, to the pool rather than destroying it. Object pooling can offer a significant performance boost in situations where the cost of initializing a class instance is high, the rate of instantiation of a class is high, and the number of instances in use at any one time is low. The pooled object is obtained in predictable time when creation of the new objects (especially over network) may take variable time. However these benefits are mostly true for objects that are expensive with respect to time, such as database connections, socket connections, threads and large graphic objects like fonts or bitmaps. In certain situations, simple object pooling (that hold no external resources, but only occupy memory) may not be efficient and could decrease performance. 18 Chapter 1. Patterns
23. DesignPatternsPHP Documentation, Release 1.0 UML Diagram Code You can also find this code on GitHub WorkerPool.php 1 <?php 2 (continues on next page) 1.1. Creational 19
24. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 3 namespace DesignPatterns\Creational\Pool; 4 5 6 7 8 9 10 class WorkerPool implements \Countable { /** * @var StringReverseWorker[] */ private $occupiedWorkers = []; 11 /** * @var StringReverseWorker[] */ private $freeWorkers = []; 12 13 14 15 16 public function get(): StringReverseWorker { if (count($this->freeWorkers) == 0) { $worker = new StringReverseWorker(); } else { $worker = array_pop($this->freeWorkers); } 17 18 19 20 21 22 23 24 $this->occupiedWorkers[spl_object_hash($worker)] = $worker; 25 26 return $worker; 27 } 28 29 public function dispose(StringReverseWorker $worker) { $key = spl_object_hash($worker); 30 31 32 33 if (isset($this->occupiedWorkers[$key])) { unset($this->occupiedWorkers[$key]); $this->freeWorkers[$key] = $worker; } 34 35 36 37 } 38 39 public function count(): int { return count($this->occupiedWorkers) + count($this->freeWorkers); } 40 41 42 43 44 } StringReverseWorker.php 1 <?php 2 3 namespace DesignPatterns\Creational\Pool; 4 5 6 7 8 9 10 class StringReverseWorker { /** * @var \DateTime */ private $createdAt; 11 public function __construct() 12 (continues on next page) 20 Chapter 1. Patterns
25. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) { 13 $this->createdAt = new \DateTime(); 14 } 15 16 public function run(string $text) { return strrev($text); } 17 18 19 20 21 } Test Tests/PoolTest.php 1 <?php 2 3 namespace DesignPatterns\Creational\Pool\Tests; 4 5 6 use DesignPatterns\Creational\Pool\WorkerPool; use PHPUnit\Framework\TestCase; 7 8 9 10 11 12 13 14 class PoolTest extends TestCase { public function testCanGetNewInstancesWithGet() { $pool = new WorkerPool(); $worker1 = $pool->get(); $worker2 = $pool->get(); 15 $this->assertCount(2, $pool); $this->assertNotSame($worker1, $worker2); 16 17 } 18 19 public function testCanGetSameInstanceTwiceWhenDisposingItFirst() { $pool = new WorkerPool(); $worker1 = $pool->get(); $pool->dispose($worker1); $worker2 = $pool->get(); 20 21 22 23 24 25 26 $this->assertCount(1, $pool); $this->assertSame($worker1, $worker2); 27 28 } 29 30 } 1.1.6 Prototype Purpose To avoid the cost of creating objects the standard way (new Foo()) and instead create a prototype and clone it. 1.1. Creational 21
26. DesignPatternsPHP Documentation, Release 1.0 Examples • Large amounts of data (e.g. create 1,000,000 rows in a database at once via a ORM). UML Diagram Code You can also find this code on GitHub BookPrototype.php 1 <?php 2 3 namespace DesignPatterns\Creational\Prototype; 4 5 6 7 8 9 10 abstract class BookPrototype { /** * @var string */ protected $title; 11 /** * @var string */ protected $category; 12 13 14 15 16 (continues on next page) 22 Chapter 1. Patterns
27. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) abstract public function __clone(); 17 18 public function getTitle(): string { return $this->title; } 19 20 21 22 23 public function setTitle($title) { $this->title = $title; } 24 25 26 27 28 } BarBookPrototype.php 1 <?php 2 3 namespace DesignPatterns\Creational\Prototype; 4 5 6 7 8 9 10 class BarBookPrototype extends BookPrototype { /** * @var string */ protected $category = 'Bar'; 11 public function __clone() { } 12 13 14 15 } FooBookPrototype.php 1 <?php 2 3 namespace DesignPatterns\Creational\Prototype; 4 5 6 7 8 9 10 class FooBookPrototype extends BookPrototype { /** * @var string */ protected $category = 'Foo'; 11 public function __clone() { } 12 13 14 15 } Test Tests/PrototypeTest.php 1 <?php 2 (continues on next page) 1.1. Creational 23
28. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 3 namespace DesignPatterns\Creational\Prototype\Tests; 4 5 6 7 use DesignPatterns\Creational\Prototype\BarBookPrototype; use DesignPatterns\Creational\Prototype\FooBookPrototype; use PHPUnit\Framework\TestCase; 8 9 10 11 12 13 14 class PrototypeTest extends TestCase { public function testCanGetFooBook() { $fooPrototype = new FooBookPrototype(); $barPrototype = new BarBookPrototype(); 15 for ($i = 0; $i < 10; $i++) { $book = clone $fooPrototype; $book->setTitle('Foo Book No ' . $i); $this->assertInstanceOf(FooBookPrototype::class, $book); } 16 17 18 19 20 21 for ($i = 0; $i < 5; $i++) { $book = clone $barPrototype; $book->setTitle('Bar Book No ' . $i); $this->assertInstanceOf(BarBookPrototype::class, $book); } 22 23 24 25 26 } 27 28 } 1.1.7 Simple Factory Purpose SimpleFactory is a simple factory pattern. It differs from the static factory because it is not static. Therefore, you can have multiple factories, differently param- eterized, you can subclass it and you can mock it. It always should be preferred over a static factory! 24 Chapter 1. Patterns
29. DesignPatternsPHP Documentation, Release 1.0 UML Diagram Code You can also find this code on GitHub SimpleFactory.php 1 <?php 2 3 namespace DesignPatterns\Creational\SimpleFactory; 4 5 6 7 8 9 10 11 class SimpleFactory { public function createBicycle(): Bicycle { return new Bicycle(); } } Bicycle.php 1.1. Creational 25
30. DesignPatternsPHP Documentation, Release 1.0 1 <?php 2 3 namespace DesignPatterns\Creational\SimpleFactory; 4 5 6 7 8 9 10 class Bicycle { public function driveTo(string $destination) { } } Usage 1 2 3 $factory = new SimpleFactory(); $bicycle = $factory->createBicycle(); $bicycle->driveTo('Paris'); Test Tests/SimpleFactoryTest.php 1 <?php 2 3 namespace DesignPatterns\Creational\SimpleFactory\Tests; 4 5 6 7 use DesignPatterns\Creational\SimpleFactory\Bicycle; use DesignPatterns\Creational\SimpleFactory\SimpleFactory; use PHPUnit\Framework\TestCase; 8 9 10 11 12 13 14 15 16 class SimpleFactoryTest extends TestCase { public function testCanCreateBicycle() { $bicycle = (new SimpleFactory())->createBicycle(); $this->assertInstanceOf(Bicycle::class, $bicycle); } } 1.1.8 Singleton THIS IS CONSIDERED TO BE AN ANTI-PATTERN! FOR BETTER TESTABILITY AND MAINTAIN- ABILITY USE DEPENDENCY INJECTION! Purpose To have only one instance of this object in the application that will handle all calls. Examples • DB Connector 26 Chapter 1. Patterns
31. DesignPatternsPHP Documentation, Release 1.0 • Logger (may also be a Multiton if there are many log files for several purposes) • Lock file for the application (there is only one in the filesystem . . . ) UML Diagram Code You can also find this code on GitHub Singleton.php 1 <?php 2 3 namespace DesignPatterns\Creational\Singleton; 4 5 6 7 8 9 10 final class Singleton { /** * @var Singleton */ private static $instance; 11 12 13 14 15 16 17 18 19 /** * gets the instance via lazy initialization (created on first usage) */ public static function getInstance(): Singleton { if (null === static::$instance) { static::$instance = new static(); } 20 return static::$instance; 21 22 } 23 24 25 26 /** * is not allowed to call from outside to prevent from creating multiple ˓ instances, → * to use the singleton, you have to obtain the instance from ˓ Singleton::getInstance() instead → (continues on next page) 1.1. Creational 27
32. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) */ private function __construct() { } 27 28 29 30 31 32 33 34 35 36 37 /** * prevent the instance from being cloned (which would create a second instance ˓ of it) → */ private function __clone() { } 38 /** * prevent from being unserialized (which would create a second instance of it) */ private function __wakeup() { } 39 40 41 42 43 44 45 } Test Tests/SingletonTest.php 1 <?php 2 3 namespace DesignPatterns\Creational\Singleton\Tests; 4 5 6 use DesignPatterns\Creational\Singleton\Singleton; use PHPUnit\Framework\TestCase; 7 8 9 10 11 12 13 class SingletonTest extends TestCase { public function testUniqueness() { $firstCall = Singleton::getInstance(); $secondCall = Singleton::getInstance(); 14 $this->assertInstanceOf(Singleton::class, $firstCall); $this->assertSame($firstCall, $secondCall); 15 16 } 17 18 } 1.1.9 Static Factory Purpose Similar to the AbstractFactory, this pattern is used to create series of related or dependent objects. The difference between this and the abstract factory pattern is that the static factory pattern uses just one static method to create all types of objects it can create. It is usually named factory or build. 28 Chapter 1. Patterns
33. DesignPatternsPHP Documentation, Release 1.0 Examples • Zend Framework: Zend_Cache_Backend or _Frontend use a factory method create cache backends or frontends UML Diagram Code You can also find this code on GitHub StaticFactory.php 1 <?php 2 3 namespace DesignPatterns\Creational\StaticFactory; 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /** * Note1: Remember, static means global state which is evil because it can't be ˓ mocked for tests → * Note2: Cannot be subclassed or mock-upped or have multiple different instances. */ final class StaticFactory { /** * @param string $type * * @return FormatterInterface */ public static function factory(string $type): FormatterInterface { if ($type == 'number') { return new FormatNumber(); (continues on next page) 1.1. Creational 29
34. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) } 20 21 if ($type == 'string') { return new FormatString(); } 22 23 24 25 throw new \InvalidArgumentException('Unknown format given'); 26 } 27 28 } FormatterInterface.php 1 <?php 2 3 namespace DesignPatterns\Creational\StaticFactory; 4 5 6 7 interface FormatterInterface { } FormatString.php 1 <?php 2 3 namespace DesignPatterns\Creational\StaticFactory; 4 5 6 7 class FormatString implements FormatterInterface { } FormatNumber.php 1 <?php 2 3 namespace DesignPatterns\Creational\StaticFactory; 4 5 6 7 class FormatNumber implements FormatterInterface { } Test Tests/StaticFactoryTest.php 1 <?php 2 3 namespace DesignPatterns\Creational\StaticFactory\Tests; 4 5 6 use DesignPatterns\Creational\StaticFactory\StaticFactory; use PHPUnit\Framework\TestCase; 7 8 9 10 11 class StaticFactoryTest extends TestCase { public function testCanCreateNumberFormatter() { (continues on next page) 30 Chapter 1. Patterns
35. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) $this->assertInstanceOf( 'DesignPatterns\Creational\StaticFactory\FormatNumber', StaticFactory::factory('number') ); 12 13 14 15 } 16 17 public function testCanCreateStringFormatter() { $this->assertInstanceOf( 'DesignPatterns\Creational\StaticFactory\FormatString', StaticFactory::factory('string') ); } 18 19 20 21 22 23 24 25 /** * @expectedException \InvalidArgumentException */ public function testException() { StaticFactory::factory('object'); } 26 27 28 29 30 31 32 33 } 1.2 Structural In Software Engineering, Structural Design Patterns are Design Patterns that ease the design by identifying a simple way to realize relationships between entities. 1.2.1 Adapter / Wrapper Purpose To translate one interface for a class into a compatible interface. An adapter allows classes to work together that normally could not because of incompatible interfaces by providing its interface to clients while using the original interface. Examples • DB Client libraries adapter • using multiple different webservices and adapters normalize data so that the outcome is the same for all 1.2. Structural 31
36. DesignPatternsPHP Documentation, Release 1.0 32 Chapter 1. Patterns
37. DesignPatternsPHP Documentation, Release 1.0 UML Diagram 1.2. Structural 33
38. DesignPatternsPHP Documentation, Release 1.0 Code You can also find this code on GitHub BookInterface.php 1 <?php 2 3 namespace DesignPatterns\Structural\Adapter; 4 5 6 7 interface BookInterface { public function turnPage(); 8 public function open(); 9 10 public function getPage(): int; 11 12 } Book.php 1 <?php 2 3 namespace DesignPatterns\Structural\Adapter; 4 5 6 7 8 9 10 class Book implements BookInterface { /** * @var int */ private $page; 11 public function open() { $this->page = 1; } 12 13 14 15 16 public function turnPage() { $this->page++; } 17 18 19 20 21 public function getPage(): int { return $this->page; } 22 23 24 25 26 } EBookAdapter.php 1 <?php 2 3 namespace DesignPatterns\Structural\Adapter; 4 5 6 7 8 /** * This is the adapter here. Notice it implements BookInterface, * therefore you don't have to change the code of the client which is using a Book */ (continues on next page) 34 Chapter 1. Patterns
39. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 9 10 11 12 13 14 class EBookAdapter implements BookInterface { /** * @var EBookInterface */ protected $eBook; 15 16 17 18 19 20 21 22 /** * @param EBookInterface $eBook */ public function __construct(EBookInterface $eBook) { $this->eBook = $eBook; } 23 24 25 26 27 28 29 30 /** * This class makes the proper translation from one interface to another. */ public function open() { $this->eBook->unlock(); } 31 32 33 34 35 public function turnPage() { $this->eBook->pressNext(); } 36 37 38 39 40 41 42 43 44 45 46 47 /** * notice the adapted behavior here: EBookInterface::getPage() will return two ˓ integers, but BookInterface → * supports only a current page getter, so we adapt the behavior here * * @return int */ public function getPage(): int { return $this->eBook->getPage()[0]; } } EBookInterface.php 1 <?php 2 3 namespace DesignPatterns\Structural\Adapter; 4 5 6 7 interface EBookInterface { public function unlock(); 8 9 public function pressNext(); 10 11 12 13 /** * returns current page and total number of pages, like [10, 100] is page 10 of ˓ 100 → * (continues on next page) 1.2. Structural 35
40. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) * @return int[] */ public function getPage(): array; 14 15 16 17 } Kindle.php 1 <?php 2 3 namespace DesignPatterns\Structural\Adapter; 4 5 6 7 8 9 10 11 12 13 14 /** * this is the adapted class. In production code, this could be a class from another ˓ package, some vendor code. → * Notice that it uses another naming scheme and the implementation does something ˓ similar but in another way → */ class Kindle implements EBookInterface { /** * @var int */ private $page = 1; 15 /** * @var int */ private $totalPages = 100; 16 17 18 19 20 public function pressNext() { $this->page++; } 21 22 23 24 25 public function unlock() { } 26 27 28 29 30 31 32 33 34 35 36 37 38 39 /** * returns current page and total number of pages, like [10, 100] is page 10 of ˓ 100 → * * @return int[] */ public function getPage(): array { return [$this->page, $this->totalPages]; } } Test Tests/AdapterTest.php 36 Chapter 1. Patterns
41. DesignPatternsPHP Documentation, Release 1.0 1 <?php 2 3 namespace DesignPatterns\Structural\Adapter\Tests; 4 5 6 7 8 use use use use DesignPatterns\Structural\Adapter\Book; DesignPatterns\Structural\Adapter\EBookAdapter; DesignPatterns\Structural\Adapter\Kindle; PHPUnit\Framework\TestCase; 9 10 11 12 13 14 15 16 class AdapterTest extends TestCase { public function testCanTurnPageOnBook() { $book = new Book(); $book->open(); $book->turnPage(); 17 $this->assertEquals(2, $book->getPage()); 18 } 19 20 public function testCanTurnPageOnKindleLikeInANormalBook() { $kindle = new Kindle(); $book = new EBookAdapter($kindle); 21 22 23 24 25 $book->open(); $book->turnPage(); 26 27 28 $this->assertEquals(2, $book->getPage()); 29 } 30 31 } 1.2.2 Bridge Purpose Decouple an abstraction from its implementation so that the two can vary independently. Examples • Symfony DoctrineBridge 1.2. Structural 37
42. DesignPatternsPHP Documentation, Release 1.0 UML Diagram Code You can also find this code on GitHub FormatterInterface.php 1 <?php 2 3 namespace DesignPatterns\Structural\Bridge; 4 5 6 7 8 interface FormatterInterface { public function format(string $text); } PlainTextFormatter.php 1 <?php 2 3 namespace DesignPatterns\Structural\Bridge; 4 5 6 7 8 9 10 11 class PlainTextFormatter implements FormatterInterface { public function format(string $text) { return $text; } } HtmlFormatter.php 1 <?php 2 3 namespace DesignPatterns\Structural\Bridge; 4 5 6 class HtmlFormatter implements FormatterInterface { (continues on next page) 38 Chapter 1. Patterns
43. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) public function format(string $text) { return sprintf('<p>%s</p>', $text); } 7 8 9 10 11 } Service.php 1 <?php 2 3 namespace DesignPatterns\Structural\Bridge; 4 5 6 7 8 9 10 abstract class Service { /** * @var FormatterInterface */ protected $implementation; 11 /** * @param FormatterInterface $printer */ public function __construct(FormatterInterface $printer) { $this->implementation = $printer; } 12 13 14 15 16 17 18 19 /** * @param FormatterInterface $printer */ public function setImplementation(FormatterInterface $printer) { $this->implementation = $printer; } 20 21 22 23 24 25 26 27 abstract public function get(); 28 29 } HelloWorldService.php 1 <?php 2 3 namespace DesignPatterns\Structural\Bridge; 4 5 6 7 8 9 10 11 class HelloWorldService extends Service { public function get() { return $this->implementation->format('Hello World'); } } Test Tests/BridgeTest.php 1.2. Structural 39
44. DesignPatternsPHP Documentation, Release 1.0 1 <?php 2 3 namespace DesignPatterns\Structural\Bridge\Tests; 4 5 6 7 8 use use use use DesignPatterns\Structural\Bridge\HelloWorldService; DesignPatterns\Structural\Bridge\HtmlFormatter; DesignPatterns\Structural\Bridge\PlainTextFormatter; PHPUnit\Framework\TestCase; 9 10 11 12 13 14 15 class BridgeTest extends TestCase { public function testCanPrintUsingThePlainTextPrinter() { $service = new HelloWorldService(new PlainTextFormatter()); $this->assertEquals('Hello World', $service->get()); 16 // now change the implementation and use the HtmlFormatter instead $service->setImplementation(new HtmlFormatter()); $this->assertEquals('<p>Hello World</p>', $service->get()); 17 18 19 } 20 21 } 1.2.3 Composite Purpose To treat a group of objects the same way as a single instance of the object. Examples • a form class instance handles all its form elements like a single instance of the form, when render() is called, it subsequently runs through all its child elements and calls render() on them • Zend_Config: a tree of configuration options, each one is a Zend_Config object itself 40 Chapter 1. Patterns
45. DesignPatternsPHP Documentation, Release 1.0 UML Diagram Code You can also find this code on GitHub RenderableInterface.php 1 <?php 2 3 namespace DesignPatterns\Structural\Composite; 4 5 6 7 8 interface RenderableInterface { public function render(): string; } Form.php 1 <?php 2 3 namespace DesignPatterns\Structural\Composite; 4 5 6 7 8 9 10 11 12 13 14 /** * The composite node MUST extend the component contract. This is mandatory for ˓ building → * a tree of components. */ class Form implements RenderableInterface { /** * @var RenderableInterface[] */ private $elements; 15 (continues on next page) 1.2. Structural 41
46. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 16 17 18 19 20 21 22 23 24 25 26 /** * runs through all elements and calls render() on them, then returns the ˓ complete representation → * of the form. * * from the outside, one will not see this and the form will act like a single ˓ object instance → * * @return string */ public function render(): string { $formCode = '<form>'; 27 foreach ($this->elements as $element) { $formCode .= $element->render(); } 28 29 30 31 $formCode .= '</form>'; 32 33 return $formCode; 34 } 35 36 /** * @param RenderableInterface $element */ public function addElement(RenderableInterface $element) { $this->elements[] = $element; } 37 38 39 40 41 42 43 44 } InputElement.php 1 <?php 2 3 namespace DesignPatterns\Structural\Composite; 4 5 6 7 8 9 10 11 class InputElement implements RenderableInterface { public function render(): string { return '<input type="text" />'; } } TextElement.php 1 <?php 2 3 namespace DesignPatterns\Structural\Composite; 4 5 6 7 8 9 class TextElement implements RenderableInterface { /** * @var string */ (continues on next page) 42 Chapter 1. Patterns
47. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) private $text; 10 11 public function __construct(string $text) { $this->text = $text; } 12 13 14 15 16 public function render(): string { return $this->text; } 17 18 19 20 21 } Test Tests/CompositeTest.php 1 <?php 2 3 namespace DesignPatterns\Structural\Composite\Tests; 4 5 6 use DesignPatterns\Structural\Composite; use PHPUnit\Framework\TestCase; 7 8 9 10 11 12 13 14 15 16 17 18 class CompositeTest extends TestCase { public function testRender() { $form = new Composite\Form(); $form->addElement(new Composite\TextElement('Email:')); $form->addElement(new Composite\InputElement()); $embed = new Composite\Form(); $embed->addElement(new Composite\TextElement('Password:')); $embed->addElement(new Composite\InputElement()); $form->addElement($embed); 19 20 ˓ → 21 // This is just an example, in a real world scenario it is important to remember that web browsers do not // currently support nested forms 22 23 24 25 26 27 28 $this->assertEquals( '<form>Email:<input type="text" /><form>Password:<input type="text" /></ ˓ form></form>', → $form->render() ); } } 1.2.4 Data Mapper Purpose A Data Mapper, is a Data Access Layer that performs bidirectional transfer of data between a persistent data store (often a relational database) and an in memory data representation (the domain layer). The goal of the pattern is 1.2. Structural 43
48. DesignPatternsPHP Documentation, Release 1.0 to keep the in memory representation and the persistent data store independent of each other and the data mapper itself. The layer is composed of one or more mappers (or Data Access Objects), performing the data transfer. Mapper implementations vary in scope. Generic mappers will handle many different domain entity types, dedicated mappers will handle one or a few. The key point of this pattern is, unlike Active Record pattern, the data model follows Single Responsibility Principle. Examples • DB Object Relational Mapper (ORM) : Doctrine2 uses DAO named as “EntityRepository” UML Diagram Code You can also find this code on GitHub User.php 44 Chapter 1. Patterns
49. DesignPatternsPHP Documentation, Release 1.0 1 <?php 2 3 namespace DesignPatterns\Structural\DataMapper; 4 5 6 7 8 9 10 class User { /** * @var string */ private $username; 11 /** * @var string */ private $email; 12 13 14 15 16 public static function fromState(array $state): User { // validate state before accessing keys! 17 18 19 20 return new self( $state['username'], $state['email'] ); 21 22 23 24 } 25 26 public function __construct(string $username, string $email) { // validate parameters before setting them! 27 28 29 30 $this->username = $username; $this->email = $email; 31 32 } 33 34 /** * @return string */ public function getUsername() { return $this->username; } 35 36 37 38 39 40 41 42 /** * @return string */ public function getEmail() { return $this->email; } 43 44 45 46 47 48 49 50 } UserMapper.php 1 <?php 2 3 namespace DesignPatterns\Structural\DataMapper; 4 (continues on next page) 1.2. Structural 45
50. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 5 6 7 8 9 10 class UserMapper { /** * @var StorageAdapter */ private $adapter; 11 /** * @param StorageAdapter $storage */ public function __construct(StorageAdapter $storage) { $this->adapter = $storage; } 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 /** * finds a user from storage based on ID and returns a User object located * in memory. Normally this kind of logic will be implemented using the ˓ Repository pattern. → * However the important part is in mapRowToUser() below, that will create a ˓ business object from the → * data fetched from storage * * @param int $id * * @return User */ public function findById(int $id): User { $result = $this->adapter->find($id); 33 if ($result === null) { throw new \InvalidArgumentException("User #$id not found"); } 34 35 36 37 return $this->mapRowToUser($result); 38 } 39 40 private function mapRowToUser(array $row): User { return User::fromState($row); } 41 42 43 44 45 } StorageAdapter.php 1 <?php 2 3 namespace DesignPatterns\Structural\DataMapper; 4 5 6 7 8 9 10 class StorageAdapter { /** * @var array */ private $data = []; 11 (continues on next page) 46 Chapter 1. Patterns
51. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) public function __construct(array $data) { $this->data = $data; } 12 13 14 15 16 /** * @param int $id * * @return array|null */ public function find(int $id) { if (isset($this->data[$id])) { return $this->data[$id]; } 17 18 19 20 21 22 23 24 25 26 27 return null; 28 } 29 30 } Test Tests/DataMapperTest.php 1 <?php 2 3 namespace DesignPatterns\Structural\DataMapper\Tests; 4 5 6 7 8 use use use use DesignPatterns\Structural\DataMapper\StorageAdapter; DesignPatterns\Structural\DataMapper\User; DesignPatterns\Structural\DataMapper\UserMapper; PHPUnit\Framework\TestCase; 9 10 11 12 13 14 15 class DataMapperTest extends TestCase { public function testCanMapUserFromStorage() { $storage = new StorageAdapter([1 => ['username' => 'domnikl', 'email' => ˓ 'liebler.dominik@gmail.com']]); → $mapper = new UserMapper($storage); 16 $user = $mapper->findById(1); 17 18 $this->assertInstanceOf(User::class, $user); 19 20 } 21 22 23 24 25 26 27 28 /** * @expectedException \InvalidArgumentException */ public function testWillNotMapInvalidData() { $storage = new StorageAdapter([]); $mapper = new UserMapper($storage); 29 30 $mapper->findById(1); (continues on next page) 1.2. Structural 47
52. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) } 31 32 } 1.2.5 Decorator Purpose To dynamically add new functionality to class instances. Examples • Zend Framework: decorators for Zend_Form_Element instances • Web Service Layer: Decorators JSON and XML for a REST service (in this case, only one of these should be allowed of course) UML Diagram 48 Chapter 1. Patterns
53. DesignPatternsPHP Documentation, Release 1.0 Code You can also find this code on GitHub Booking.php 1 <?php 2 3 namespace DesignPatterns\Structural\Decorator; 4 5 6 7 interface Booking { public function calculatePrice(): int; 8 public function getDescription(): string; 9 10 } BookingDecorator.php 1 <?php 2 3 namespace DesignPatterns\Structural\Decorator; 4 5 6 7 8 9 10 abstract class BookingDecorator implements Booking { /** * @var Booking */ protected $booking; 11 public function __construct(Booking $booking) { $this->booking = $booking; } 12 13 14 15 16 } DoubleRoomBooking.php 1 <?php 2 3 namespace DesignPatterns\Structural\Decorator; 4 5 6 7 8 9 10 class DoubleRoomBooking implements Booking { public function calculatePrice(): int { return 40; } 11 public function getDescription(): string { return 'double room'; } 12 13 14 15 16 } ExtraBed.php 1.2. Structural 49
54. DesignPatternsPHP Documentation, Release 1.0 1 <?php 2 3 namespace DesignPatterns\Structural\Decorator; 4 5 6 7 class ExtraBed extends BookingDecorator { private const PRICE = 30; 8 public function calculatePrice(): int { return $this->booking->calculatePrice() + self::PRICE; } 9 10 11 12 13 public function getDescription(): string { return $this->booking->getDescription() . ' with extra bed'; } 14 15 16 17 18 } WiFi.php 1 <?php 2 3 namespace DesignPatterns\Structural\Decorator; 4 5 6 7 class WiFi extends BookingDecorator { private const PRICE = 2; 8 public function calculatePrice(): int { return $this->booking->calculatePrice() + self::PRICE; } 9 10 11 12 13 public function getDescription(): string { return $this->booking->getDescription() . ' with wifi'; } 14 15 16 17 18 } Test Tests/DecoratorTest.php 1 <?php 2 3 namespace DesignPatterns\Structural\Decorator\Tests; 4 5 6 7 8 use use use use DesignPatterns\Structural\Decorator\DoubleRoomBooking; DesignPatterns\Structural\Decorator\ExtraBed; DesignPatterns\Structural\Decorator\WiFi; PHPUnit\Framework\TestCase; 9 10 11 12 class DecoratorTest extends TestCase { public function testCanCalculatePriceForBasicDoubleRoomBooking() (continues on next page) 50 Chapter 1. Patterns
55. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 13 { $booking = new DoubleRoomBooking(); 14 15 $this->assertEquals(40, $booking->calculatePrice()); $this->assertEquals('double room', $booking->getDescription()); 16 17 18 } 19 20 21 22 23 public function testCanCalculatePriceForDoubleRoomBookingWithWiFi() { $booking = new DoubleRoomBooking(); $booking = new WiFi($booking); 24 $this->assertEquals(42, $booking->calculatePrice()); $this->assertEquals('double room with wifi', $booking->getDescription()); 25 26 27 } 28 29 30 31 32 33 public function testCanCalculatePriceForDoubleRoomBookingWithWiFiAndExtraBed() { $booking = new DoubleRoomBooking(); $booking = new WiFi($booking); $booking = new ExtraBed($booking); 34 35 36 37 38 $this->assertEquals(72, $booking->calculatePrice()); $this->assertEquals('double room with wifi with extra bed', $booking-> ˓ getDescription()); → } } 1.2.6 Dependency Injection Purpose To implement a loosely coupled architecture in order to get better testable, maintainable and extendable code. Usage DatabaseConfiguration gets injected and DatabaseConnection will get all that it needs from $config. Without DI, the configuration would be created directly in DatabaseConnection, which is not very good for testing and extending it. Examples • The Doctrine2 ORM uses dependency injection e.g. for configuration that is injected into a Connection object. For testing purposes, one can easily create a mock object of the configuration and inject that into the Connection object • Symfony and Zend Framework 2 already have containers for DI that create objects via a configuration array and inject them where needed (i.e. in Controllers) 1.2. Structural 51
56. DesignPatternsPHP Documentation, Release 1.0 UML Diagram Code You can also find this code on GitHub DatabaseConfiguration.php 52 Chapter 1. Patterns
57. DesignPatternsPHP Documentation, Release 1.0 1 <?php 2 3 namespace DesignPatterns\Structural\DependencyInjection; 4 5 6 7 8 9 10 class DatabaseConfiguration { /** * @var string */ private $host; 11 /** * @var int */ private $port; 12 13 14 15 16 /** * @var string */ private $username; 17 18 19 20 21 /** * @var string */ private $password; 22 23 24 25 26 27 ˓ → 28 29 30 31 32 33 public function __construct(string $host, int $port, string $username, string $password) { $this->host = $host; $this->port = $port; $this->username = $username; $this->password = $password; } 34 public function getHost(): string { return $this->host; } 35 36 37 38 39 public function getPort(): int { return $this->port; } 40 41 42 43 44 public function getUsername(): string { return $this->username; } 45 46 47 48 49 public function getPassword(): string { return $this->password; } 50 51 52 53 54 } DatabaseConnection.php 1.2. Structural 53
58. DesignPatternsPHP Documentation, Release 1.0 1 <?php 2 3 namespace DesignPatterns\Structural\DependencyInjection; 4 5 6 7 8 9 10 class DatabaseConnection { /** * @var DatabaseConfiguration */ private $configuration; 11 /** * @param DatabaseConfiguration $config */ public function __construct(DatabaseConfiguration $config) { $this->configuration = $config; } 12 13 14 15 16 17 18 19 public { // // // 20 21 22 23 24 function getDsn(): string this is just for the sake of demonstration, not a real DSN notice that only the injected config is used here, so there is a real separation of concerns here 25 return sprintf( '%s:%s@%s:%d', $this->configuration->getUsername(), $this->configuration->getPassword(), $this->configuration->getHost(), $this->configuration->getPort() ); 26 27 28 29 30 31 32 } 33 34 } Test Tests/DependencyInjectionTest.php 1 <?php 2 3 namespace DesignPatterns\Structural\DependencyInjection\Tests; 4 5 6 7 use DesignPatterns\Structural\DependencyInjection\DatabaseConfiguration; use DesignPatterns\Structural\DependencyInjection\DatabaseConnection; use PHPUnit\Framework\TestCase; 8 9 10 11 12 13 14 class DependencyInjectionTest extends TestCase { public function testDependencyInjection() { $config = new DatabaseConfiguration('localhost', 3306, 'domnikl', '1234'); $connection = new DatabaseConnection($config); 15 $this->assertEquals('domnikl:1234@localhost:3306', $connection->getDsn()); 16 } 17 (continues on next page) 54 Chapter 1. Patterns
59. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 18 } 1.2.7 Facade Purpose The primary goal of a Facade Pattern is not to avoid you having to read the manual of a complex API. It’s only a side-effect. The first goal is to reduce coupling and follow the Law of Demeter. A Facade is meant to decouple a client and a sub-system by embedding many (but sometimes just one) interface, and of course to reduce complexity. • A facade does not forbid you the access to the sub-system • You can (you should) have multiple facades for one sub-system That’s why a good facade has no new in it. If there are multiple creations for each method, it is not a Facade, it’s a Builder or a [Abstract|Static|Simple] Factory [Method]. The best facade has no new and a constructor with interface-type-hinted parameters. If you need creation of new instances, use a Factory as argument. 1.2. Structural 55
60. DesignPatternsPHP Documentation, Release 1.0 UML Diagram Code You can also find this code on GitHub Facade.php 1 <?php 2 3 namespace DesignPatterns\Structural\Facade; 4 5 6 7 8 9 10 class Facade { /** * @var OsInterface */ private $os; 11 /** * @var BiosInterface 12 13 (continues on next page) 56 Chapter 1. Patterns
61. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) */ private $bios; 14 15 16 /** * @param BiosInterface $bios $os * @param OsInterface */ public function __construct(BiosInterface $bios, OsInterface $os) { $this->bios = $bios; $this->os = $os; } 17 18 19 20 21 22 23 24 25 26 public function turnOn() { $this->bios->execute(); $this->bios->waitForKeyPress(); $this->bios->launch($this->os); } 27 28 29 30 31 32 33 public function turnOff() { $this->os->halt(); $this->bios->powerDown(); } 34 35 36 37 38 39 } OsInterface.php 1 <?php 2 3 namespace DesignPatterns\Structural\Facade; 4 5 6 7 interface OsInterface { public function halt(); 8 public function getName(): string; 9 10 } BiosInterface.php 1 <?php 2 3 namespace DesignPatterns\Structural\Facade; 4 5 6 7 interface BiosInterface { public function execute(); 8 public function waitForKeyPress(); 9 10 public function launch(OsInterface $os); 11 12 public function powerDown(); 13 14 } 1.2. Structural 57
62. DesignPatternsPHP Documentation, Release 1.0 Test Tests/FacadeTest.php 1 <?php 2 3 namespace DesignPatterns\Structural\Facade\Tests; 4 5 6 7 use DesignPatterns\Structural\Facade\Facade; use DesignPatterns\Structural\Facade\OsInterface; use PHPUnit\Framework\TestCase; 8 9 10 11 12 13 14 class FacadeTest extends TestCase { public function testComputerOn() { /** @var OsInterface|\PHPUnit_Framework_MockObject_MockObject $os */ $os = $this->createMock('DesignPatterns\Structural\Facade\OsInterface'); 15 $os->method('getName') ->will($this->returnValue('Linux')); 16 17 18 $bios = $this->getMockBuilder('DesignPatterns\Structural\Facade\BiosInterface 19 ') ˓ → ->setMethods(['launch', 'execute', 'waitForKeyPress']) ->disableAutoload() ->getMock(); 20 21 22 23 $bios->expects($this->once()) ->method('launch') ->with($os); 24 25 26 27 $facade = new Facade($bios, $os); 28 29 // the facade interface is simple $facade->turnOn(); 30 31 32 // but you can also access the underlying components $this->assertEquals('Linux', $os->getName()); 33 34 } 35 36 } 1.2.8 Fluent Interface Purpose To write code that is easy readable just like sentences in a natural language (like English). Examples • Doctrine2’s QueryBuilder works something like that example class below • PHPUnit uses fluent interfaces to build mock objects • Yii Framework: CDbCommand and CActiveRecord use this pattern, too 58 Chapter 1. Patterns
63. DesignPatternsPHP Documentation, Release 1.0 UML Diagram Code You can also find this code on GitHub Sql.php 1 <?php 2 3 namespace DesignPatterns\Structural\FluentInterface; 4 5 6 7 8 9 10 class Sql { /** * @var array */ private $fields = []; 11 (continues on next page) 1.2. Structural 59
64. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) /** * @var array */ private $from = []; 12 13 14 15 16 /** * @var array */ private $where = []; 17 18 19 20 21 public function select(array $fields): Sql { $this->fields = $fields; 22 23 24 25 return $this; 26 } 27 28 public function from(string $table, string $alias): Sql { $this->from[] = $table.' AS '.$alias; 29 30 31 32 return $this; 33 } 34 35 public function where(string $condition): Sql { $this->where[] = $condition; 36 37 38 39 return $this; 40 } 41 42 public function __toString(): string { return sprintf( 'SELECT %s FROM %s WHERE %s', join(', ', $this->fields), join(', ', $this->from), join(' AND ', $this->where) ); } 43 44 45 46 47 48 49 50 51 52 } Test Tests/FluentInterfaceTest.php 1 <?php 2 3 namespace DesignPatterns\Structural\FluentInterface\Tests; 4 5 6 use DesignPatterns\Structural\FluentInterface\Sql; use PHPUnit\Framework\TestCase; 7 8 9 class FluentInterfaceTest extends TestCase { (continues on next page) 60 Chapter 1. Patterns
65. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) public function testBuildSQL() { $query = (new Sql()) ->select(['foo', 'bar']) ->from('foobar', 'f') ->where('f.bar = ?'); 10 11 12 13 14 15 16 17 ˓ → 18 19 $this->assertEquals('SELECT foo, bar FROM foobar AS f WHERE f.bar = ?', (string) $query); } } 1.2.9 Flyweight Purpose To minimise memory usage, a Flyweight shares as much as possible memory with similar objects. It is needed when a large amount of objects is used that don’t differ much in state. A common practice is to hold state in external data structures and pass them to the flyweight object when needed. UML Diagram Code You can also find this code on GitHub FlyweightInterface.php 1.2. Structural 61
66. DesignPatternsPHP Documentation, Release 1.0 1 <?php 2 3 namespace DesignPatterns\Structural\Flyweight; 4 5 6 7 8 interface FlyweightInterface { public function render(string $extrinsicState): string; } CharacterFlyweight.php 1 <?php 2 3 namespace DesignPatterns\Structural\Flyweight; 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /** * Implements the flyweight interface and adds storage for intrinsic state, if any. * Instances of concrete flyweights are shared by means of a factory. */ class CharacterFlyweight implements FlyweightInterface { /** * Any state stored by the concrete flyweight must be independent of its context. * For flyweights representing characters, this is usually the corresponding ˓ character code. → * * @var string */ private $name; 18 public function __construct(string $name) { $this->name = $name; } 19 20 21 22 23 24 25 26 27 public function render(string $font): string { // Clients supply the context-dependent information that the flyweight needs ˓ to draw itself → // For flyweights representing characters, extrinsic state usually contains ˓ e.g. the font. → 28 return sprintf('Character %s with font %s', $this->name, $font); 29 } 30 31 } FlyweightFactory.php 1 <?php 2 3 namespace DesignPatterns\Structural\Flyweight; 4 5 6 7 8 9 /** * A factory manages shared flyweights. Clients should not instantiate them directly, * but let the factory take care of returning existing objects or creating new ones. */ class FlyweightFactory implements \Countable (continues on next page) 62 Chapter 1. Patterns
67. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 10 { /** * @var CharacterFlyweight[] */ private $pool = []; 11 12 13 14 15 public function get(string $name): CharacterFlyweight { if (!isset($this->pool[$name])) { $this->pool[$name] = new CharacterFlyweight($name); } 16 17 18 19 20 21 return $this->pool[$name]; 22 } 23 24 public function count(): int { return count($this->pool); } 25 26 27 28 29 } Test Tests/FlyweightTest.php 1 <?php 2 3 namespace DesignPatterns\Structural\Flyweight\Tests; 4 5 6 use DesignPatterns\Structural\Flyweight\FlyweightFactory; use PHPUnit\Framework\TestCase; 7 8 9 10 11 12 class FlyweightTest extends TestCase { private $characters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']; private $fonts = ['Arial', 'Times New Roman', 'Verdana', 'Helvetica']; 13 public function testFlyweight() { $factory = new FlyweightFactory(); 14 15 16 17 foreach ($this->characters as $char) { foreach ($this->fonts as $font) { $flyweight = $factory->get($char); $rendered = $flyweight->render($font); 18 19 20 21 22 23 $this->assertEquals(sprintf('Character %s with font %s', $char, $font), $rendered); } } ˓ → 24 25 26 27 28 29 // Flyweight pattern ensures that instances are shared // instead of having hundreds of thousands of individual objects // there must be one instance for every char that has been reused for ˓ displaying in different fonts → (continues on next page) 1.2. Structural 63
68. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) $this->assertCount(count($this->characters), $factory); 30 } 31 32 } 1.2.10 Proxy Purpose To interface to anything that is expensive or impossible to duplicate. Examples • Doctrine2 uses proxies to implement framework magic (e.g. lazy initialization) in them, while the user still works with his own entity classes and will never use nor touch the proxies 64 Chapter 1. Patterns
69. DesignPatternsPHP Documentation, Release 1.0 1.2. Structural 65
70. DesignPatternsPHP Documentation, Release 1.0 UML Diagram 66 Chapter 1. Patterns
71. DesignPatternsPHP Documentation, Release 1.0 Code You can also find this code on GitHub BankAccount.php 1 <?php 2 3 namespace DesignPatterns\Structural\Proxy; 4 5 6 7 interface BankAccount { public function deposit(int $amount); 8 public function getBalance(): int; 9 10 } HeavyBankAccount.php 1 <?php 2 3 namespace DesignPatterns\Structural\Proxy; 4 5 6 7 8 9 10 class HeavyBankAccount implements BankAccount { /** * @var int[] */ private $transactions = []; 11 public function deposit(int $amount) { $this->transactions[] = $amount; } 12 13 14 15 16 public { // // // 17 18 19 20 21 function getBalance(): int this is the heavy part, imagine all the transactions even from years and decades ago must be fetched from a database or web service and the balance must be calculated from it 22 return array_sum($this->transactions); 23 } 24 25 } BankAccountProxy.php 1 <?php 2 3 namespace DesignPatterns\Structural\Proxy; 4 5 6 7 8 9 10 class BankAccountProxy extends HeavyBankAccount implements BankAccount { /** * @var int */ private $balance; 11 (continues on next page) 1.2. Structural 67
72. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) public { // // // 12 13 14 15 16 function getBalance(): int because calculating balance is so expensive, the usage of BankAccount::getBalance() is delayed until it really is needed and will not be calculated again for this instance 17 if ($this->balance === null) { $this->balance = parent::getBalance(); } 18 19 20 21 return $this->balance; 22 } 23 24 } Test 1.2.11 Registry Purpose To implement a central storage for objects often used throughout the application, is typically implemented using an abstract class with only static methods (or using the Singleton pattern). Remember that this introduces global state, which should be avoided at all times! Instead implement it using Dependency Injection! Examples • Zend Framework 1: Zend_Registry holds the application’s logger object, front controller etc. • Yii Framework: CWebApplication holds all the application components, such as CWebUser, CUrlManager, etc. UML Diagram Code You can also find this code on GitHub 68 Chapter 1. Patterns
73. DesignPatternsPHP Documentation, Release 1.0 Registry.php 1 <?php 2 3 namespace DesignPatterns\Structural\Registry; 4 5 6 7 abstract class Registry { const LOGGER = 'logger'; 8 9 10 11 12 13 14 15 /** * this introduces global state in your application which can not be mocked up ˓ for testing → * and is therefor considered an anti-pattern! Use dependency injection instead! * * @var array */ private static $storedValues = []; 16 /** * @var array */ private static $allowedKeys = [ self::LOGGER, ]; 17 18 19 20 21 22 23 /** * @param string $key * @param mixed $value * * @return void */ public static function set(string $key, $value) { if (!in_array($key, self::$allowedKeys)) { throw new \InvalidArgumentException('Invalid key given'); } 24 25 26 27 28 29 30 31 32 33 34 35 self::$storedValues[$key] = $value; 36 } 37 38 /** * @param string $key * * @return mixed */ public static function get(string $key) { if (!in_array($key, self::$allowedKeys) || !isset(self::$storedValues[$key])) 39 40 41 42 43 44 45 46 { ˓ → throw new \InvalidArgumentException('Invalid key given'); 47 } 48 49 return self::$storedValues[$key]; 50 } 51 52 } 1.2. Structural 69
74. DesignPatternsPHP Documentation, Release 1.0 Test Tests/RegistryTest.php 1 <?php 2 3 namespace DesignPatterns\Structural\Registry\Tests; 4 5 6 7 use DesignPatterns\Structural\Registry\Registry; use stdClass; use PHPUnit\Framework\TestCase; 8 9 10 11 12 13 14 class RegistryTest extends TestCase { public function testSetAndGetLogger() { $key = Registry::LOGGER; $logger = new stdClass(); 15 Registry::set($key, $logger); $storedLogger = Registry::get($key); 16 17 18 $this->assertSame($logger, $storedLogger); $this->assertInstanceOf(stdClass::class, $storedLogger); 19 20 } 21 22 /** * @expectedException \InvalidArgumentException */ public function testThrowsExceptionWhenTryingToSetInvalidKey() { Registry::set('foobar', new stdClass()); } 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 /** * notice @runInSeparateProcess here: without it, a previous test might have set ˓ it already and → * testing would not be possible. That's why you should implement Dependency ˓ Injection where an → * injected class may easily be replaced by a mockup * * @runInSeparateProcess * @expectedException \InvalidArgumentException */ public function testThrowsExceptionWhenTryingToGetNotSetKey() { Registry::get(Registry::LOGGER); } } 1.3 Behavioral In software engineering, behavioral design patterns are design patterns that identify common communication pat- terns between objects and realize these patterns. By doing so, these patterns increase flexibility in carrying out this communication. 70 Chapter 1. Patterns
75. DesignPatternsPHP Documentation, Release 1.0 1.3.1 Chain Of Responsibilities Purpose To build a chain of objects to handle a call in sequential order. If one object cannot handle a call, it delegates the call to the next in the chain and so forth. Examples • logging framework, where each chain element decides autonomously what to do with a log message • a Spam filter • Caching: first object is an instance of e.g. a Memcached Interface, if that “misses” it delegates the call to the database interface • Yii Framework: CFilterChain is a chain of controller action filters. the executing point is passed from one filter to the next along the chain, and only if all filters say “yes”, the action can be invoked at last. UML Diagram Code You can also find this code on GitHub Handler.php 1.3. Behavioral 71
76. DesignPatternsPHP Documentation, Release 1.0 1 <?php 2 3 namespace DesignPatterns\Behavioral\ChainOfResponsibilities; 4 5 6 use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; 7 8 9 10 11 12 13 abstract class Handler { /** * @var Handler|null */ private $successor = null; 14 public function __construct(Handler $handler = null) { $this->successor = $handler; } 15 16 17 18 19 /** * This approach by using a template method pattern ensures you that * each subclass will not forget to call the successor * * @param RequestInterface $request * * @return string|null */ final public function handle(RequestInterface $request) { $processed = $this->processing($request); 20 21 22 23 24 25 26 27 28 29 30 31 if ($processed === null) { // the request has not been processed by this handler => see the next if ($this->successor !== null) { $processed = $this->successor->handle($request); } } 32 33 34 35 36 37 38 return $processed; 39 } 40 41 abstract protected function processing(RequestInterface $request); 42 43 } Responsible/FastStorage.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible; 4 5 6 use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler; use Psr\Http\Message\RequestInterface; 7 8 9 10 11 class HttpInMemoryCacheHandler extends Handler { /** * @var array (continues on next page) 72 Chapter 1. Patterns
77. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) */ private $data; 12 13 14 /** * @param array $data * @param Handler|null $successor */ public function __construct(array $data, Handler $successor = null) { parent::__construct($successor); 15 16 17 18 19 20 21 22 $this->data = $data; 23 } 24 25 /** * @param RequestInterface $request * * @return string|null */ protected function processing(RequestInterface $request) { $key = sprintf( '%s?%s', $request->getUri()->getPath(), $request->getUri()->getQuery() ); 26 27 28 29 30 31 32 33 34 35 36 37 38 if ($request->getMethod() == 'GET' && isset($this->data[$key])) { return $this->data[$key]; } 39 40 41 42 return null; 43 } 44 45 } Responsible/SlowStorage.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible; 4 5 6 use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler; use Psr\Http\Message\RequestInterface; 7 8 9 10 11 12 13 14 15 16 17 class SlowDatabaseHandler extends Handler { /** * @param RequestInterface $request * * @return string|null */ protected function processing(RequestInterface $request) { // this is a mockup, in production code you would ask a slow (compared to in- ˓ memory) DB for the results → 18 19 return 'Hello World!'; (continues on next page) 1.3. Behavioral 73
78. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) } 20 21 } Test Tests/ChainTest.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Tests; 4 5 6 use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler; use ˓ DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\HttpInMemoryCacheHandler; → ˓ → 7 8 use DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\SlowDatabaseHandler; use PHPUnit\Framework\TestCase; 9 10 11 12 13 14 15 class ChainTest extends TestCase { /** * @var Handler */ private $chain; 16 protected function setUp() { $this->chain = new HttpInMemoryCacheHandler( ['/foo/bar?index=1' => 'Hello In Memory!'], new SlowDatabaseHandler() ); } 17 18 19 20 21 22 23 24 public function testCanRequestKeyInFastStorage() { $uri = $this->createMock('Psr\Http\Message\UriInterface'); $uri->method('getPath')->willReturn('/foo/bar'); $uri->method('getQuery')->willReturn('index=1'); 25 26 27 28 29 30 $request = $this->createMock('Psr\Http\Message\RequestInterface'); $request->method('getMethod') ->willReturn('GET'); $request->method('getUri')->willReturn($uri); 31 32 33 34 35 $this->assertEquals('Hello In Memory!', $this->chain->handle($request)); 36 } 37 38 public function testCanRequestKeyInSlowStorage() { $uri = $this->createMock('Psr\Http\Message\UriInterface'); $uri->method('getPath')->willReturn('/foo/baz'); $uri->method('getQuery')->willReturn(''); 39 40 41 42 43 44 $request = $this->createMock('Psr\Http\Message\RequestInterface'); $request->method('getMethod') 45 46 (continues on next page) 74 Chapter 1. Patterns
79. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) ->willReturn('GET'); $request->method('getUri')->willReturn($uri); 47 48 49 $this->assertEquals('Hello World!', $this->chain->handle($request)); 50 } 51 52 } 1.3.2 Command Purpose To encapsulate invocation and decoupling. We have an Invoker and a Receiver. This pattern uses a “Command” to delegate the method call against the Receiver and presents the same method “execute”. Therefore, the Invoker just knows to call “execute” to process the Command of the client. The Receiver is decoupled from the Invoker. The second aspect of this pattern is the undo(), which undoes the method execute(). Command can also be aggregated to combine more complex commands with minimum copy-paste and relying on composition over inheritance. Examples • A text editor : all events are Command which can be undone, stacked and saved. • Symfony2: SF2 Commands that can be run from the CLI are built with just the Command pattern in mind • big CLI tools use subcommands to distribute various tasks and pack them in “modules”, each of these can be implemented with the Command pattern (e.g. vagrant) UML Diagram 1.3. Behavioral 75
80. DesignPatternsPHP Documentation, Release 1.0 Code You can also find this code on GitHub CommandInterface.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Command; 4 5 6 7 8 9 10 11 12 interface CommandInterface { /** * this is the most important method in the Command pattern, * The Receiver goes in the constructor. */ public function execute(); } HelloCommand.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Command; 4 5 6 7 8 9 10 11 12 13 14 /** * This concrete command calls "print" on the Receiver, but an external * invoker just knows that it can call "execute" */ class HelloCommand implements CommandInterface { /** * @var Receiver */ private $output; 15 16 17 18 19 20 21 22 23 24 25 /** * Each concrete command is built with different receivers. * There can be one, many or completely no receivers, but there can be other ˓ commands in the parameters → * * @param Receiver $console */ public function __construct(Receiver $console) { $this->output = $console; } 26 27 28 29 30 31 32 33 34 35 /** * execute and output "Hello World". */ public function execute() { // sometimes, there is no receiver and this is the command which does all the ˓ work → $this->output->write('Hello World'); } } 76 Chapter 1. Patterns
81. DesignPatternsPHP Documentation, Release 1.0 Receiver.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Command; 4 5 6 7 8 9 10 11 12 13 /** * Receiver is specific service with its own contract and can be only concrete. */ class Receiver { /** * @var bool */ private $enableDate = false; 14 /** * @var string[] */ private $output = []; 15 16 17 18 19 /** * @param string $str */ public function write(string $str) { if ($this->enableDate) { $str .= ' ['.date('Y-m-d').']'; } 20 21 22 23 24 25 26 27 28 $this->output[] = $str; 29 } 30 31 public function getOutput(): string { return join("\n", $this->output); } 32 33 34 35 36 /** * Enable receiver to display message date */ public function enableDate() { $this->enableDate = true; } 37 38 39 40 41 42 43 44 /** * Disable receiver to display message date */ public function disableDate() { $this->enableDate = false; } 45 46 47 48 49 50 51 52 } Invoker.php 1 <?php (continues on next page) 1.3. Behavioral 77
82. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 2 3 namespace DesignPatterns\Behavioral\Command; 4 5 6 7 8 9 10 11 12 13 14 /** * Invoker is using the command given to it. * Example : an Application in SF2. */ class Invoker { /** * @var CommandInterface */ private $command; 15 /** * in the invoker we find this kind of method for subscribing the command * There can be also a stack, a list, a fixed set ... * * @param CommandInterface $cmd */ public function setCommand(CommandInterface $cmd) { $this->command = $cmd; } 16 17 18 19 20 21 22 23 24 25 26 /** * executes the command; the invoker is the same whatever is the command */ public function run() { $this->command->execute(); } 27 28 29 30 31 32 33 34 } Test Tests/CommandTest.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Command\Tests; 4 5 6 7 8 use use use use DesignPatterns\Behavioral\Command\HelloCommand; DesignPatterns\Behavioral\Command\Invoker; DesignPatterns\Behavioral\Command\Receiver; PHPUnit\Framework\TestCase; 9 10 11 12 13 14 15 class CommandTest extends TestCase { public function testInvocation() { $invoker = new Invoker(); $receiver = new Receiver(); 16 $invoker->setCommand(new HelloCommand($receiver)); 17 (continues on next page) 78 Chapter 1. Patterns
83. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) $invoker->run(); $this->assertEquals('Hello World', $receiver->getOutput()); 18 19 } 20 21 } 1.3.3 Iterator Purpose To make an object iterable and to make it appear like a collection of objects. Examples • to process a file line by line by just running over all lines (which have an object representation) for a file (which of course is an object, too) Note Standard PHP Library (SPL) defines an interface Iterator which is best suited for this! Often you would want to implement the Countable interface too, to allow count($object) on your iterable object 1.3. Behavioral 79
84. DesignPatternsPHP Documentation, Release 1.0 UML Diagram Code You can also find this code on GitHub Book.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Iterator; 4 5 6 7 8 9 10 class Book { /** * @var string */ private $author; 11 /** * @var string */ private $title; 12 13 14 15 (continues on next page) 80 Chapter 1. Patterns
85. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 16 public function __construct(string $title, string $author) { $this->author = $author; $this->title = $title; } 17 18 19 20 21 22 public function getAuthor(): string { return $this->author; } 23 24 25 26 27 public function getTitle(): string { return $this->title; } 28 29 30 31 32 public function getAuthorAndTitle(): string { return $this->getTitle().' by '.$this->getAuthor(); } 33 34 35 36 37 } BookList.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Iterator; 4 5 6 7 8 9 10 class BookList implements \Countable, \Iterator { /** * @var Book[] */ private $books = []; 11 12 13 14 15 /** * @var int */ private $currentIndex = 0; 16 17 18 19 20 public function addBook(Book $book) { $this->books[] = $book; } 21 22 23 24 25 26 27 28 public function removeBook(Book $bookToRemove) { foreach ($this->books as $key => $book) { if ($book->getAuthorAndTitle() === $bookToRemove->getAuthorAndTitle()) { unset($this->books[$key]); } } 29 $this->books = array_values($this->books); 30 31 } 32 (continues on next page) 1.3. Behavioral 81
86. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) public function count(): int { return count($this->books); } 33 34 35 36 37 public function current(): Book { return $this->books[$this->currentIndex]; } 38 39 40 41 42 public function key(): int { return $this->currentIndex; } 43 44 45 46 47 public function next() { $this->currentIndex++; } 48 49 50 51 52 public function rewind() { $this->currentIndex = 0; } 53 54 55 56 57 public function valid(): bool { return isset($this->books[$this->currentIndex]); } 58 59 60 61 62 } Test Tests/IteratorTest.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Iterator\Tests; 4 5 6 7 8 9 use use use use use DesignPatterns\Behavioral\Iterator\Book; DesignPatterns\Behavioral\Iterator\BookList; DesignPatterns\Behavioral\Iterator\BookListIterator; DesignPatterns\Behavioral\Iterator\BookListReverseIterator; PHPUnit\Framework\TestCase; 10 11 12 13 14 15 16 17 18 class IteratorTest extends TestCase { public function testCanIterateOverBookList() { $bookList = new BookList(); $bookList->addBook(new Book('Learning PHP Design Patterns', 'William Sanders ˓ ')); → $bookList->addBook(new Book('Professional Php Design Patterns', 'Aaron Saray ˓ ')); → $bookList->addBook(new Book('Clean Code', 'Robert C. Martin')); (continues on next page) 82 Chapter 1. Patterns
87. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 19 $books = []; 20 21 foreach ($bookList as $book) { $books[] = $book->getAuthorAndTitle(); } 22 23 24 25 $this->assertEquals( [ 'Learning PHP Design Patterns by William Sanders', 'Professional Php Design Patterns by Aaron Saray', 'Clean Code by Robert C. Martin', ], $books ); 26 27 28 29 30 31 32 33 34 } 35 36 37 38 39 public function testCanIterateOverBookListAfterRemovingBook() { $book = new Book('Clean Code', 'Robert C. Martin'); $book2 = new Book('Professional Php Design Patterns', 'Aaron Saray'); 40 $bookList = new BookList(); $bookList->addBook($book); $bookList->addBook($book2); $bookList->removeBook($book); 41 42 43 44 45 $books = []; foreach ($bookList as $book) { $books[] = $book->getAuthorAndTitle(); } 46 47 48 49 50 $this->assertEquals( ['Professional Php Design Patterns by Aaron Saray'], $books ); 51 52 53 54 55 } 56 57 58 59 public function testCanAddBookToList() { $book = new Book('Clean Code', 'Robert C. Martin'); 60 $bookList = new BookList(); $bookList->addBook($book); 61 62 63 $this->assertCount(1, $bookList); 64 65 } 66 67 68 69 public function testCanRemoveBookFromList() { $book = new Book('Clean Code', 'Robert C. Martin'); 70 71 72 73 $bookList = new BookList(); $bookList->addBook($book); $bookList->removeBook($book); 74 75 $this->assertCount(0, $bookList); (continues on next page) 1.3. Behavioral 83
88. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) } 76 77 } 1.3.4 Mediator Purpose This pattern provides an easy way to decouple many components working together. It is a good alternative to Observer IF you have a “central intelligence”, like a controller (but not in the sense of the MVC). All components (called Colleague) are only coupled to the MediatorInterface and it is a good thing because in OOP, one good friend is better than many. This is the key-feature of this pattern. UML Diagram Code You can also find this code on GitHub MediatorInterface.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Mediator; 4 5 6 7 8 /** * MediatorInterface is a contract for the Mediator * This interface is not mandatory but it is better for Liskov substitution principle ˓ concerns. → */ (continues on next page) 84 Chapter 1. Patterns
89. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 9 10 11 12 13 14 15 16 interface MediatorInterface { /** * sends the response. * * @param string $content */ public function sendResponse($content); 17 /** * makes a request */ public function makeRequest(); 18 19 20 21 22 /** * queries the DB */ public function queryDb(); 23 24 25 26 27 } Mediator.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Mediator; 4 5 6 7 8 9 10 11 12 13 14 15 /** * Mediator is the concrete Mediator for this design pattern * * In this example, I have made a "Hello World" with the Mediator Pattern */ class Mediator implements MediatorInterface { /** * @var Subsystem\Server */ private $server; 16 17 18 19 20 /** * @var Subsystem\Database */ private $database; 21 22 23 24 25 /** * @var Subsystem\Client */ private $client; 26 27 28 29 30 31 32 33 34 /** * @param Subsystem\Database $database * @param Subsystem\Client $client * @param Subsystem\Server $server */ public function __construct(Subsystem\Database $database, Subsystem\Client ˓ $client, Subsystem\Server $server) → { $this->database = $database; (continues on next page) 1.3. Behavioral 85
90. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) $this->server = $server; $this->client = $client; 35 36 37 $this->database->setMediator($this); $this->server->setMediator($this); $this->client->setMediator($this); 38 39 40 } 41 42 public function makeRequest() { $this->server->process(); } 43 44 45 46 47 public function queryDb(): string { return $this->database->getData(); } 48 49 50 51 52 /** * @param string $content */ public function sendResponse($content) { $this->client->output($content); } 53 54 55 56 57 58 59 60 } Colleague.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Mediator; 4 5 6 7 8 9 10 11 12 13 14 15 16 /** * Colleague is an abstract colleague who works together but he only knows * the Mediator, not other colleagues */ abstract class Colleague { /** * this ensures no change in subclasses. * * @var MediatorInterface */ protected $mediator; 17 /** * @param MediatorInterface $mediator */ public function setMediator(MediatorInterface $mediator) { $this->mediator = $mediator; } 18 19 20 21 22 23 24 25 } Subsystem/Client.php 86 Chapter 1. Patterns
91. DesignPatternsPHP Documentation, Release 1.0 1 <?php 2 3 namespace DesignPatterns\Behavioral\Mediator\Subsystem; 4 5 use DesignPatterns\Behavioral\Mediator\Colleague; 6 7 8 9 10 11 12 13 14 15 /** * Client is a client that makes requests and gets the response. */ class Client extends Colleague { public function request() { $this->mediator->makeRequest(); } 16 public function output(string $content) { echo $content; } 17 18 19 20 21 } Subsystem/Database.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Mediator\Subsystem; 4 5 use DesignPatterns\Behavioral\Mediator\Colleague; 6 7 8 9 10 11 12 13 class Database extends Colleague { public function getData(): string { return 'World'; } } Subsystem/Server.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Mediator\Subsystem; 4 5 use DesignPatterns\Behavioral\Mediator\Colleague; 6 7 8 9 10 11 12 13 14 class Server extends Colleague { public function process() { $data = $this->mediator->queryDb(); $this->mediator->sendResponse(sprintf("Hello %s", $data)); } } 1.3. Behavioral 87
92. DesignPatternsPHP Documentation, Release 1.0 Test Tests/MediatorTest.php 1 <?php 2 3 namespace DesignPatterns\Tests\Mediator\Tests; 4 5 6 7 8 9 use use use use use DesignPatterns\Behavioral\Mediator\Mediator; DesignPatterns\Behavioral\Mediator\Subsystem\Client; DesignPatterns\Behavioral\Mediator\Subsystem\Database; DesignPatterns\Behavioral\Mediator\Subsystem\Server; PHPUnit\Framework\TestCase; 10 11 12 13 14 15 16 class MediatorTest extends TestCase { public function testOutputHelloWorld() { $client = new Client(); new Mediator(new Database(), $client, new Server()); 17 $this->expectOutputString('Hello World'); $client->request(); 18 19 } 20 21 } 1.3.5 Memento Purpose It provides the ability to restore an object to it’s previous state (undo via rollback) or to gain access to state of the object, without revealing it’s implementation (i.e., the object is not required to have a function to return the current state). The memento pattern is implemented with three objects: the Originator, a Caretaker and a Memento. Memento – an object that contains a concrete unique snapshot of state of any object or resource: string, number, array, an instance of class and so on. The uniqueness in this case does not imply the prohibition existence of similar states in different snapshots. That means the state can be extracted as the independent clone. Any object stored in the Memento should be a full copy of the original object rather than a reference to the original object. The Memento object is a “opaque object” (the object that no one can or should change). Originator – it is an object that contains the actual state of an external object is strictly specified type. Originator is able to create a unique copy of this state and return it wrapped in a Memento. The Originator does not know the history of changes. You can set a concrete state to Originator from the outside, which will be considered as actual. The Originator must make sure that given state corresponds the allowed type of object. Originator may (but not should) have any methods, but they they can’t make changes to the saved object state. Caretaker controls the states history. He may make changes to an object; take a decision to save the state of an external object in the Originator; ask from the Originator snapshot of the current state; or set the Originator state to equivalence with some snapshot from history. Examples • The seed of a pseudorandom number generator 88 Chapter 1. Patterns
93. DesignPatternsPHP Documentation, Release 1.0 • The state in a finite state machine • Control for intermediate states of ORM Model before saving UML Diagram Code You can also find this code on GitHub Memento.php 1.3. Behavioral 89
94. DesignPatternsPHP Documentation, Release 1.0 1 <?php 2 3 namespace DesignPatterns\Behavioral\Memento; 4 5 6 7 8 9 10 class Memento { /** * @var State */ private $state; 11 /** * @param State $stateToSave */ public function __construct(State $stateToSave) { $this->state = $stateToSave; } 12 13 14 15 16 17 18 19 /** * @return State */ public function getState() { return $this->state; } 20 21 22 23 24 25 26 27 } State.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Memento; 4 5 6 7 8 9 10 class State { const STATE_CREATED = 'created'; const STATE_OPENED = 'opened'; const STATE_ASSIGNED = 'assigned'; const STATE_CLOSED = 'closed'; 11 /** * @var string */ private $state; 12 13 14 15 16 /** * @var string[] */ private static $validStates = [ self::STATE_CREATED, self::STATE_OPENED, self::STATE_ASSIGNED, self::STATE_CLOSED, ]; 17 18 19 20 21 22 23 24 25 26 /** 27 (continues on next page) 90 Chapter 1. Patterns
95. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) * @param string $state */ public function __construct(string $state) { self::ensureIsValidState($state); 28 29 30 31 32 33 $this->state = $state; 34 } 35 36 private static function ensureIsValidState(string $state) { if (!in_array($state, self::$validStates)) { throw new \InvalidArgumentException('Invalid state given'); } } 37 38 39 40 41 42 43 public function __toString(): string { return $this->state; } 44 45 46 47 48 } Ticket.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Memento; 4 5 6 7 8 9 10 11 12 13 /** * Ticket is the "Originator" in this implementation */ class Ticket { /** * @var State */ private $currentState; 14 15 16 17 18 public function __construct() { $this->currentState = new State(State::STATE_CREATED); } 19 20 21 22 23 public function open() { $this->currentState = new State(State::STATE_OPENED); } 24 25 26 27 28 public function assign() { $this->currentState = new State(State::STATE_ASSIGNED); } 29 30 31 32 33 public function close() { $this->currentState = new State(State::STATE_CLOSED); } (continues on next page) 1.3. Behavioral 91
96. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 34 public function saveToMemento(): Memento { return new Memento(clone $this->currentState); } 35 36 37 38 39 public function restoreFromMemento(Memento $memento) { $this->currentState = $memento->getState(); } 40 41 42 43 44 public function getState(): State { return $this->currentState; } 45 46 47 48 49 } Test Tests/MementoTest.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Memento\Tests; 4 5 6 7 use DesignPatterns\Behavioral\Memento\State; use DesignPatterns\Behavioral\Memento\Ticket; use PHPUnit\Framework\TestCase; 8 9 10 11 12 13 class MementoTest extends TestCase { public function testOpenTicketAssignAndSetBackToOpen() { $ticket = new Ticket(); 14 // open the ticket $ticket->open(); $openedState = $ticket->getState(); $this->assertEquals(State::STATE_OPENED, (string) $ticket->getState()); 15 16 17 18 19 $memento = $ticket->saveToMemento(); 20 21 // assign the ticket $ticket->assign(); $this->assertEquals(State::STATE_ASSIGNED, (string) $ticket->getState()); 22 23 24 25 26 ˓ → 27 // now restore to the opened state, but verify that the state object has been cloned for the memento $ticket->restoreFromMemento($memento); 28 $this->assertEquals(State::STATE_OPENED, (string) $ticket->getState()); $this->assertNotSame($openedState, $ticket->getState()); 29 30 } 31 32 } 92 Chapter 1. Patterns
97. DesignPatternsPHP Documentation, Release 1.0 1.3.6 Null Object Purpose NullObject is not a GoF design pattern but a schema which appears frequently enough to be considered a pattern. It has the following benefits: • Client code is simplified • Reduces the chance of null pointer exceptions • Fewer conditionals require less test cases Methods that return an object or null should instead return an object or NullObject. NullObjects simplify boilerplate code such as if (!is_null($obj)) { $obj->callSomething(); } to just $obj->callSomething(); by eliminating the conditional check in client code. Examples • Symfony2: null logger of profiler • Symfony2: null output in Symfony/Console • null handler in a Chain of Responsibilities pattern • null command in a Command pattern UML Diagram 1.3. Behavioral 93
98. DesignPatternsPHP Documentation, Release 1.0 Code You can also find this code on GitHub Service.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\NullObject; 4 5 6 7 8 9 10 class Service { /** * @var LoggerInterface */ private $logger; 11 /** * @param LoggerInterface $logger */ public function __construct(LoggerInterface $logger) { $this->logger = $logger; } 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 /** * do something ... */ public function doSomething() { // notice here that you don't have to check if the logger is set with eg. is_ ˓ null(), instead just use it → $this->logger->log('We are in '.__METHOD__); } } LoggerInterface.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\NullObject; 4 5 6 7 8 9 10 11 /** * Key feature: NullLogger must inherit from this interface like any other loggers */ interface LoggerInterface { public function log(string $str); } PrintLogger.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\NullObject; 4 5 6 class PrintLogger implements LoggerInterface { (continues on next page) 94 Chapter 1. Patterns
99. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) public function log(string $str) { echo $str; } 7 8 9 10 11 } NullLogger.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\NullObject; 4 5 6 7 8 9 10 11 class NullLogger implements LoggerInterface { public function log(string $str) { // do nothing } } Test Tests/LoggerTest.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\NullObject\Tests; 4 5 6 7 8 use use use use DesignPatterns\Behavioral\NullObject\NullLogger; DesignPatterns\Behavioral\NullObject\PrintLogger; DesignPatterns\Behavioral\NullObject\Service; PHPUnit\Framework\TestCase; 9 10 11 12 13 14 15 16 17 class LoggerTest extends TestCase { public function testNullObject() { $service = new Service(new NullLogger()); $this->expectOutputString(''); $service->doSomething(); } 18 19 20 21 22 23 24 25 public function testStandardLogger() { $service = new Service(new PrintLogger()); $this->expectOutputString('We are in ˓ DesignPatterns\Behavioral\NullObject\Service::doSomething'); → $service->doSomething(); } } 1.3. Behavioral 95
100. DesignPatternsPHP Documentation, Release 1.0 1.3.7 Observer Purpose To implement a publish/subscribe behaviour to an object, whenever a “Subject” object changes its state, the attached “Observers” will be notified. It is used to shorten the amount of coupled objects and uses loose coupling instead. Examples • a message queue system is observed to show the progress of a job in a GUI Note PHP already defines two interfaces that can help to implement this pattern: SplObserver and SplSubject. UML Diagram Code You can also find this code on GitHub User.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Observer; 4 5 6 7 /** * User implements the observed object (called Subject), it maintains a list of ˓ observers and sends notifications to → * them in case changes are made on the User object (continues on next page) 96 Chapter 1. Patterns
101. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 8 9 10 11 12 13 14 */ class User implements \SplSubject { /** * @var string */ private $email; 15 /** * @var \SplObjectStorage */ private $observers; 16 17 18 19 20 public function __construct() { $this->observers = new \SplObjectStorage(); } 21 22 23 24 25 public function attach(\SplObserver $observer) { $this->observers->attach($observer); } 26 27 28 29 30 public function detach(\SplObserver $observer) { $this->observers->detach($observer); } 31 32 33 34 35 public function changeEmail(string $email) { $this->email = $email; $this->notify(); } 36 37 38 39 40 41 public function notify() { /** @var \SplObserver $observer */ foreach ($this->observers as $observer) { $observer->update($this); } } 42 43 44 45 46 47 48 49 } UserObserver.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Observer; 4 5 6 7 8 9 10 class UserObserver implements \SplObserver { /** * @var User[] */ private $changedUsers = []; 11 12 /** (continues on next page) 1.3. Behavioral 97
102. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) * It is called by the Subject, usually by SplSubject::notify() * * @param \SplSubject $subject */ public function update(\SplSubject $subject) { $this->changedUsers[] = clone $subject; } 13 14 15 16 17 18 19 20 21 /** * @return User[] */ public function getChangedUsers(): array { return $this->changedUsers; } 22 23 24 25 26 27 28 29 } Test Tests/ObserverTest.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Observer\Tests; 4 5 6 7 use DesignPatterns\Behavioral\Observer\User; use DesignPatterns\Behavioral\Observer\UserObserver; use PHPUnit\Framework\TestCase; 8 9 10 11 12 13 class ObserverTest extends TestCase { public function testChangeInUserLeadsToUserObserverBeingNotified() { $observer = new UserObserver(); 14 $user = new User(); $user->attach($observer); 15 16 17 $user->changeEmail('foo@bar.com'); $this->assertCount(1, $observer->getChangedUsers()); 18 19 } 20 21 } 1.3.8 Specification Purpose Builds a clear specification of business rules, where objects can be checked against. The composite specification class has one method called isSatisfiedBy that returns either true or false depending on whether the given object satisfies the specification. 98 Chapter 1. Patterns
103. DesignPatternsPHP Documentation, Release 1.0 Examples • RulerZ UML Diagram Code You can also find this code on GitHub Item.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Specification; 4 5 6 class Item { (continues on next page) 1.3. Behavioral 99
104. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) /** * @var float */ private $price; 7 8 9 10 11 public function __construct(float $price) { $this->price = $price; } 12 13 14 15 16 public function getPrice(): float { return $this->price; } 17 18 19 20 21 } SpecificationInterface.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Specification; 4 5 6 7 8 interface SpecificationInterface { public function isSatisfiedBy(Item $item): bool; } OrSpecification.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Specification; 4 5 6 7 8 9 10 class OrSpecification implements SpecificationInterface { /** * @var SpecificationInterface[] */ private $specifications; 11 /** * @param SpecificationInterface[] ...$specifications */ public function __construct(SpecificationInterface ...$specifications) { $this->specifications = $specifications; } 12 13 14 15 16 17 18 19 /** * if at least one specification is true, return true, else return false */ public function isSatisfiedBy(Item $item): bool { foreach ($this->specifications as $specification) { if ($specification->isSatisfiedBy($item)) { return true; } 20 21 22 23 24 25 26 27 28 (continues on next page) 100 Chapter 1. Patterns
105. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) } return false; 29 30 } 31 32 } PriceSpecification.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Specification; 4 5 6 7 8 9 10 class PriceSpecification implements SpecificationInterface { /** * @var float|null */ private $maxPrice; 11 /** * @var float|null */ private $minPrice; 12 13 14 15 16 /** * @param float $minPrice * @param float $maxPrice */ public function __construct($minPrice, $maxPrice) { $this->minPrice = $minPrice; $this->maxPrice = $maxPrice; } 17 18 19 20 21 22 23 24 25 26 public function isSatisfiedBy(Item $item): bool { if ($this->maxPrice !== null && $item->getPrice() > $this->maxPrice) { return false; } 27 28 29 30 31 32 if ($this->minPrice !== null && $item->getPrice() < $this->minPrice) { return false; } 33 34 35 36 return true; 37 } 38 39 } AndSpecification.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Specification; 4 5 6 7 8 class AndSpecification implements SpecificationInterface { /** * @var SpecificationInterface[] (continues on next page) 1.3. Behavioral 101
106. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) */ private $specifications; 9 10 11 /** * @param SpecificationInterface[] ...$specifications */ public function __construct(SpecificationInterface ...$specifications) { $this->specifications = $specifications; } 12 13 14 15 16 17 18 19 /** * if at least one specification is false, return false, else return true. */ public function isSatisfiedBy(Item $item): bool { foreach ($this->specifications as $specification) { if (!$specification->isSatisfiedBy($item)) { return false; } } 20 21 22 23 24 25 26 27 28 29 30 return true; 31 } 32 33 } NotSpecification.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Specification; 4 5 6 7 8 9 10 class NotSpecification implements SpecificationInterface { /** * @var SpecificationInterface */ private $specification; 11 public function __construct(SpecificationInterface $specification) { $this->specification = $specification; } 12 13 14 15 16 public function isSatisfiedBy(Item $item): bool { return !$this->specification->isSatisfiedBy($item); } 17 18 19 20 21 } Test Tests/SpecificationTest.php 102 Chapter 1. Patterns
107. DesignPatternsPHP Documentation, Release 1.0 1 <?php 2 3 namespace DesignPatterns\Behavioral\Specification\Tests; 4 5 6 7 8 9 10 use use use use use use DesignPatterns\Behavioral\Specification\Item; DesignPatterns\Behavioral\Specification\NotSpecification; DesignPatterns\Behavioral\Specification\OrSpecification; DesignPatterns\Behavioral\Specification\AndSpecification; DesignPatterns\Behavioral\Specification\PriceSpecification; PHPUnit\Framework\TestCase; 11 12 13 14 15 16 17 class SpecificationTest extends TestCase { public function testCanOr() { $spec1 = new PriceSpecification(50, 99); $spec2 = new PriceSpecification(101, 200); 18 $orSpec = new OrSpecification($spec1, $spec2); 19 20 $this->assertFalse($orSpec->isSatisfiedBy(new Item(100))); $this->assertTrue($orSpec->isSatisfiedBy(new Item(51))); $this->assertTrue($orSpec->isSatisfiedBy(new Item(150))); 21 22 23 } 24 25 public function testCanAnd() { $spec1 = new PriceSpecification(50, 100); $spec2 = new PriceSpecification(80, 200); 26 27 28 29 30 $andSpec = new AndSpecification($spec1, $spec2); 31 32 $this->assertFalse($andSpec->isSatisfiedBy(new Item(150))); $this->assertFalse($andSpec->isSatisfiedBy(new Item(1))); $this->assertFalse($andSpec->isSatisfiedBy(new Item(51))); $this->assertTrue($andSpec->isSatisfiedBy(new Item(100))); 33 34 35 36 } 37 38 public function testCanNot() { $spec1 = new PriceSpecification(50, 100); $notSpec = new NotSpecification($spec1); 39 40 41 42 43 $this->assertTrue($notSpec->isSatisfiedBy(new Item(150))); $this->assertFalse($notSpec->isSatisfiedBy(new Item(50))); 44 45 } 46 47 } 1.3.9 State Purpose Encapsulate varying behavior for the same routine based on an object’s state. This can be a cleaner way for an object to change its behavior at runtime without resorting to large monolithic conditional statements. 1.3. Behavioral 103
108. DesignPatternsPHP Documentation, Release 1.0 UML Diagram Code You can also find this code on GitHub OrderContext.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\State; 4 5 6 7 8 9 10 class OrderContext { /** * @var State */ private $state; 11 public static function create(): OrderContext { $order = new self(); $order->state = new StateCreated(); 12 13 14 15 16 return $order; 17 } 18 19 public function setState(State $state) { $this->state = $state; 20 21 22 (continues on next page) 104 Chapter 1. Patterns
109. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) } 23 24 public function proceedToNext() { $this->state->proceedToNext($this); } 25 26 27 28 29 public function toString() { return $this->state->toString(); } 30 31 32 33 34 } State.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\State; 4 5 6 7 interface State { public function proceedToNext(OrderContext $context); 8 public function toString(): string; 9 10 } StateCreated.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\State; 4 5 6 7 8 9 10 class StateCreated implements State { public function proceedToNext(OrderContext $context) { $context->setState(new StateShipped()); } 11 public function toString(): string { return 'created'; } 12 13 14 15 16 } StateShipped.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\State; 4 5 6 7 8 9 10 class StateShipped implements State { public function proceedToNext(OrderContext $context) { $context->setState(new StateDone()); } (continues on next page) 1.3. Behavioral 105
110. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 11 public function toString(): string { return 'shipped'; } 12 13 14 15 16 } StateDone.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\State; 4 5 6 7 8 9 10 class StateDone implements State { public function proceedToNext(OrderContext $context) { // there is nothing more to do } 11 public function toString(): string { return 'done'; } 12 13 14 15 16 } Test Tests/StateTest.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\State\Tests; 4 5 6 use DesignPatterns\Behavioral\State\OrderContext; use PHPUnit\Framework\TestCase; 7 8 9 10 11 12 class StateTest extends TestCase { public function testIsCreatedWithStateCreated() { $orderContext = OrderContext::create(); 13 $this->assertEquals('created', $orderContext->toString()); 14 } 15 16 public function testCanProceedToStateShipped() { $contextOrder = OrderContext::create(); $contextOrder->proceedToNext(); 17 18 19 20 21 $this->assertEquals('shipped', $contextOrder->toString()); 22 } 23 24 public function testCanProceedToStateDone() 25 (continues on next page) 106 Chapter 1. Patterns
111. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) { 26 $contextOrder = OrderContext::create(); $contextOrder->proceedToNext(); $contextOrder->proceedToNext(); 27 28 29 30 $this->assertEquals('done', $contextOrder->toString()); 31 } 32 33 public function testStateDoneIsTheLastPossibleState() { $contextOrder = OrderContext::create(); $contextOrder->proceedToNext(); $contextOrder->proceedToNext(); $contextOrder->proceedToNext(); 34 35 36 37 38 39 40 $this->assertEquals('done', $contextOrder->toString()); 41 } 42 43 } 1.3.10 Strategy Terminology: • Context • Strategy • Concrete Strategy Purpose To separate strategies and to enable fast switching between them. Also this pattern is a good alternative to inheritance (instead of having an abstract class that is extended). Examples • sorting a list of objects, one strategy by date, the other by id • simplify unit testing: e.g. switching between file and in-memory storage 1.3. Behavioral 107
112. DesignPatternsPHP Documentation, Release 1.0 UML Diagram Code You can also find this code on GitHub Context.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Strategy; 4 5 6 7 8 9 10 class Context { /** * @var ComparatorInterface */ private $comparator; 11 public function __construct(ComparatorInterface $comparator) { $this->comparator = $comparator; } 12 13 14 15 16 public function executeStrategy(array $elements) : array { uasort($elements, [$this->comparator, 'compare']); 17 18 19 20 return $elements; 21 } 22 23 } ComparatorInterface.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Strategy; 4 5 6 7 8 interface ComparatorInterface { /** * @param mixed $a (continues on next page) 108 Chapter 1. Patterns
113. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) * @param mixed $b * * @return int */ public function compare($a, $b): int; 9 10 11 12 13 14 } DateComparator.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Strategy; 4 5 6 7 8 9 10 11 12 13 14 15 16 class DateComparator implements ComparatorInterface { /** * @param mixed $a * @param mixed $b * * @return int */ public function compare($a, $b): int { $aDate = new \DateTime($a['date']); $bDate = new \DateTime($b['date']); 17 return $aDate <=> $bDate; 18 } 19 20 } IdComparator.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Strategy; 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class IdComparator implements ComparatorInterface { /** * @param mixed $a * @param mixed $b * * @return int */ public function compare($a, $b): int { return $a['id'] <=> $b['id']; } } Test Tests/StrategyTest.php 1.3. Behavioral 109
114. DesignPatternsPHP Documentation, Release 1.0 1 <?php 2 3 namespace DesignPatterns\Behavioral\Strategy\Tests; 4 5 6 7 8 use use use use DesignPatterns\Behavioral\Strategy\Context; DesignPatterns\Behavioral\Strategy\DateComparator; DesignPatterns\Behavioral\Strategy\IdComparator; PHPUnit\Framework\TestCase; 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class StrategyTest extends TestCase { public function provideIntegers() { return [ [ [['id' => 2], ['id' => 1], ['id' => 3]], ['id' => 1], ], [ [['id' => 3], ['id' => 2], ['id' => 1]], ['id' => 1], ], ]; } 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public function provideDates() { return [ [ [['date' => '2014-03-03'], ['date' => '2015-03-02'], ['date' => '2013- ˓ 03-01']], → ['date' => '2013-03-01'], ], [ [['date' => '2014-02-03'], ['date' => '2013-02-01'], ['date' => '2015- ˓ 02-02']], → ['date' => '2013-02-01'], ], ]; } 39 /** * @dataProvider provideIntegers * * @param array $collection * @param array $expected */ public function testIdComparator($collection, $expected) { $obj = new Context(new IdComparator()); $elements = $obj->executeStrategy($collection); 40 41 42 43 44 45 46 47 48 49 50 $firstElement = array_shift($elements); $this->assertEquals($expected, $firstElement); 51 52 } 53 54 /** 55 (continues on next page) 110 Chapter 1. Patterns
115. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) * @dataProvider provideDates * * @param array $collection * @param array $expected */ public function testDateComparator($collection, $expected) { $obj = new Context(new DateComparator()); $elements = $obj->executeStrategy($collection); 56 57 58 59 60 61 62 63 64 65 $firstElement = array_shift($elements); $this->assertEquals($expected, $firstElement); 66 67 } 68 69 } 1.3.11 Template Method Purpose Template Method is a behavioral design pattern. Perhaps you have encountered it many times already. The idea is to let subclasses of this abstract template “finish” the behavior of an algorithm. A.k.a the “Hollywood principle”: “Don’t call us, we call you.” This class is not called by subclasses but the inverse. How? With abstraction of course. In other words, this is a skeleton of algorithm, well-suited for framework libraries. The user has just to implement one method and the superclass do the job. It is an easy way to decouple concrete classes and reduce copy-paste, that’s why you’ll find it everywhere. 1.3. Behavioral 111
116. DesignPatternsPHP Documentation, Release 1.0 UML Diagram Code You can also find this code on GitHub Journey.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\TemplateMethod; 4 5 6 7 8 abstract class Journey { /** * @var string[] (continues on next page) 112 Chapter 1. Patterns
117. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) */ private $thingsToDo = []; 9 10 11 /** * This is the public service provided by this class and its subclasses. * Notice it is final to "freeze" the global behavior of algorithm. * If you want to override this contract, make an interface with only takeATrip() * and subclass it. */ final public function takeATrip() { $this->thingsToDo[] = $this->buyAFlight(); $this->thingsToDo[] = $this->takePlane(); $this->thingsToDo[] = $this->enjoyVacation(); $buyGift = $this->buyGift(); 12 13 14 15 16 17 18 19 20 21 22 23 24 if ($buyGift !== null) { $this->thingsToDo[] = $buyGift; } 25 26 27 28 $this->thingsToDo[] = $this->takePlane(); 29 } 30 31 /** * This method must be implemented, this is the key-feature of this pattern. */ abstract protected function enjoyVacation(): string; 32 33 34 35 36 /** * This method is also part of the algorithm but it is optional. * You can override it only if you need to * * @return null|string */ protected function buyGift() { return null; } 37 38 39 40 41 42 43 44 45 46 47 private function buyAFlight(): string { return 'Buy a flight ticket'; } 48 49 50 51 52 private function takePlane(): string { return 'Taking the plane'; } 53 54 55 56 57 /** * @return string[] */ public function getThingsToDo(): array { return $this->thingsToDo; } 58 59 60 61 62 63 64 65 } 1.3. Behavioral 113
118. DesignPatternsPHP Documentation, Release 1.0 BeachJourney.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\TemplateMethod; 4 5 6 7 8 9 10 11 class BeachJourney extends Journey { protected function enjoyVacation(): string { return "Swimming and sun-bathing"; } } CityJourney.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\TemplateMethod; 4 5 6 7 8 9 10 class CityJourney extends Journey { protected function enjoyVacation(): string { return "Eat, drink, take photos and sleep"; } 11 protected function buyGift(): string { return "Buy a gift"; } 12 13 14 15 16 } Test Tests/JourneyTest.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\TemplateMethod\Tests; 4 5 6 use DesignPatterns\Behavioral\TemplateMethod; use PHPUnit\Framework\TestCase; 7 8 9 10 11 12 13 class JourneyTest extends TestCase { public function testCanGetOnVacationOnTheBeach() { $beachJourney = new TemplateMethod\BeachJourney(); $beachJourney->takeATrip(); 14 15 16 17 18 $this->assertEquals( ['Buy a flight ticket', 'Taking the plane', 'Swimming and sun-bathing', ˓ 'Taking the plane'], → $beachJourney->getThingsToDo() ); (continues on next page) 114 Chapter 1. Patterns
119. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) } 19 20 public function testCanGetOnAJourneyToACity() { $beachJourney = new TemplateMethod\CityJourney(); $beachJourney->takeATrip(); 21 22 23 24 25 $this->assertEquals( [ 'Buy a flight ticket', 'Taking the plane', 'Eat, drink, take photos and sleep', 'Buy a gift', 'Taking the plane' ], $beachJourney->getThingsToDo() ); 26 27 28 29 30 31 32 33 34 35 } 36 37 } 1.3.12 Visitor Purpose The Visitor Pattern lets you outsource operations on objects to other objects. The main reason to do this is to keep a separation of concerns. But classes have to define a contract to allow visitors (the Role::accept method in the example). The contract is an abstract class but you can have also a clean interface. In that case, each Visitor has to choose itself which method to invoke on the visitor. UML Diagram 1.3. Behavioral 115
120. DesignPatternsPHP Documentation, Release 1.0 Code You can also find this code on GitHub RoleVisitorInterface.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Visitor; 4 5 6 7 8 9 10 11 /** * Note: the visitor must not choose itself which method to * invoke, it is the Visitee that make this decision */ interface RoleVisitorInterface { public function visitUser(User $role); 12 public function visitGroup(Group $role); 13 14 } RoleVisitor.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Visitor; 4 5 6 7 8 9 10 class RoleVisitor implements RoleVisitorInterface { /** * @var Role[] */ private $visited = []; 11 public function visitGroup(Group $role) { $this->visited[] = $role; } 12 13 14 15 16 public function visitUser(User $role) { $this->visited[] = $role; } 17 18 19 20 21 /** * @return Role[] */ public function getVisited(): array { return $this->visited; } 22 23 24 25 26 27 28 29 } Role.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Visitor; (continues on next page) 116 Chapter 1. Patterns
121. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 4 5 6 7 8 interface Role { public function accept(RoleVisitorInterface $visitor); } User.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Visitor; 4 5 6 7 8 9 10 class User implements Role { /** * @var string */ private $name; 11 public function __construct(string $name) { $this->name = $name; } 12 13 14 15 16 public function getName(): string { return sprintf('User %s', $this->name); } 17 18 19 20 21 public function accept(RoleVisitorInterface $visitor) { $visitor->visitUser($this); } 22 23 24 25 26 } Group.php 1 <?php 2 3 namespace DesignPatterns\Behavioral\Visitor; 4 5 6 7 8 9 10 class Group implements Role { /** * @var string */ private $name; 11 12 13 14 15 public function __construct(string $name) { $this->name = $name; } 16 17 18 19 20 public function getName(): string { return sprintf('Group: %s', $this->name); } (continues on next page) 1.3. Behavioral 117
122. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 21 public function accept(RoleVisitorInterface $visitor) { $visitor->visitGroup($this); } 22 23 24 25 26 } Test Tests/VisitorTest.php 1 <?php 2 3 namespace DesignPatterns\Tests\Visitor\Tests; 4 5 6 use DesignPatterns\Behavioral\Visitor; use PHPUnit\Framework\TestCase; 7 8 9 10 11 12 13 class VisitorTest extends TestCase { /** * @var Visitor\RoleVisitor */ private $visitor; 14 protected function setUp() { $this->visitor = new Visitor\RoleVisitor(); } 15 16 17 18 19 public function provideRoles() { return [ [new Visitor\User('Dominik')], [new Visitor\Group('Administrators')], ]; } 20 21 22 23 24 25 26 27 /** * @dataProvider provideRoles * * @param Visitor\Role $role */ public function testVisitSomeRole(Visitor\Role $role) { $role->accept($this->visitor); $this->assertSame($role, $this->visitor->getVisited()[0]); } 28 29 30 31 32 33 34 35 36 37 38 } 118 Chapter 1. Patterns
123. DesignPatternsPHP Documentation, Release 1.0 1.4 More 1.4.1 Service Locator THIS IS CONSIDERED TO BE AN ANTI-PATTERN! Service Locator is considered for some people an anti-pattern. It violates the Dependency Inversion principle. Service Locator hides class’ dependencies instead of exposing them as you would do using the Dependency Injection. In case of changes of those dependencies you risk to break the functionality of classes which are using them, making your system difficult to maintain. Purpose To implement a loosely coupled architecture in order to get better testable, maintainable and extendable code. DI pattern and Service Locator pattern are an implementation of the Inverse of Control pattern. Usage With ServiceLocator you can register a service for a given interface. By using the interface you can retrieve the service and use it in the classes of the application without knowing its implementation. You can configure and inject the Service Locator object on bootstrap. Examples • Zend Framework 2 uses Service Locator to create and share services used in the framework(i.e. EventManager, ModuleManager, all custom user services provided by modules, etc. . . ) 1.4. More 119
124. DesignPatternsPHP Documentation, Release 1.0 UML Diagram Code You can also find this code on GitHub ServiceLocator.php 1 <?php 2 3 namespace DesignPatterns\More\ServiceLocator; 4 5 class ServiceLocator (continues on next page) 120 Chapter 1. Patterns
125. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 6 7 8 9 10 { /** * @var array */ private $services = []; 11 12 13 14 15 /** * @var array */ private $instantiated = []; 16 17 18 19 20 /** * @var array */ private $shared = []; 21 22 23 24 25 26 27 28 29 30 31 32 33 34 /** * instead of supplying a class here, you could also store a service for an ˓ interface → * * @param string $class * @param object $service * @param bool $share */ public function addInstance(string $class, $service, bool $share = true) { $this->services[$class] = $service; $this->instantiated[$class] = $service; $this->shared[$class] = $share; } 35 36 37 38 39 40 41 42 43 44 45 46 47 /** * instead of supplying a class here, you could also store a service for an ˓ interface → * * @param string $class * @param array $params * @param bool $share */ public function addClass(string $class, array $params, bool $share = true) { $this->services[$class] = $params; $this->shared[$class] = $share; } 48 49 50 51 52 public function has(string $interface): bool { return isset($this->services[$interface]) || isset($this->instantiated[ ˓ $interface]); → } 53 54 55 56 57 58 59 /** * @param string $class * * @return object */ public function get(string $class) (continues on next page) 1.4. More 121
126. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) { 60 if (isset($this->instantiated[$class]) && $this->shared[$class]) { return $this->instantiated[$class]; } 61 62 63 64 $args = $this->services[$class]; 65 66 switch (count($args)) { case 0: $object = new $class(); break; case 1: $object = new $class($args[0]); break; case 2: $object = new $class($args[0], $args[1]); break; case 3: $object = new $class($args[0], $args[1], $args[2]); break; default: throw new \OutOfRangeException('Too many arguments given'); } 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 if ($this->shared[$class]) { $this->instantiated[$class] = $object; } 84 85 86 87 return $object; 88 } 89 90 } LogService.php 1 <?php 2 3 namespace DesignPatterns\More\ServiceLocator; 4 5 6 7 class LogService { } Test Tests/ServiceLocatorTest.php 1 <?php 2 3 namespace DesignPatterns\More\ServiceLocator\Tests; 4 5 6 7 use DesignPatterns\More\ServiceLocator\LogService; use DesignPatterns\More\ServiceLocator\ServiceLocator; use PHPUnit\Framework\TestCase; 8 9 class ServiceLocatorTest extends TestCase (continues on next page) 122 Chapter 1. Patterns
127. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 10 { /** * @var ServiceLocator */ private $serviceLocator; 11 12 13 14 15 public function setUp() { $this->serviceLocator = new ServiceLocator(); } 16 17 18 19 20 public function testHasServices() { $this->serviceLocator->addInstance(LogService::class, new LogService()); 21 22 23 24 $this->assertTrue($this->serviceLocator->has(LogService::class)); $this->assertFalse($this->serviceLocator->has(self::class)); 25 26 } 27 28 public function testGetWillInstantiateLogServiceIfNoInstanceHasBeenCreatedYet() { $this->serviceLocator->addClass(LogService::class, []); $logger = $this->serviceLocator->get(LogService::class); 29 30 31 32 33 $this->assertInstanceOf(LogService::class, $logger); 34 } 35 36 } 1.4.2 Repository Purpose Mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects. Repository encapsulates the set of objects persisted in a data store and the operations performed over them, providing a more object-oriented view of the persistence layer. Repository also supports the objective of achieving a clean separation and one-way dependency between the domain and data mapping layers. Examples • Doctrine 2 ORM: there is Repository that mediates between Entity and DBAL and contains methods to retrieve objects • Laravel Framework 1.4. More 123
128. DesignPatternsPHP Documentation, Release 1.0 UML Diagram Code You can also find this code on GitHub Post.php 1 <?php 2 3 namespace DesignPatterns\More\Repository\Domain; 4 5 6 7 8 9 10 class Post { /** * @var PostId */ private $id; 11 (continues on next page) 124 Chapter 1. Patterns
129. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 12 13 14 15 /** * @var PostStatus */ private $status; 16 17 18 19 20 /** * @var string */ private $title; 21 22 23 24 25 /** * @var string */ private $text; 26 27 28 29 30 31 32 33 34 35 public static function draft(PostId $id, string $title, string $text): Post { return new self( $id, PostStatus::fromString(PostStatus::STATE_DRAFT), $title, $text ); } 36 37 38 39 40 41 42 43 44 45 public static function fromState(array $state): Post { return new self( PostId::fromInt($state['id']), PostStatus::fromInt($state['statusId']), $state['title'], $state['text'] ); } 46 47 48 49 50 51 52 53 54 55 56 57 58 59 /** * @param PostId $id * @param PostStatus $status * @param string $title * @param string $text */ private function __construct(PostId $id, PostStatus $status, string $title, ˓ string $text) → { $this->id = $id; $this->status = $status; $this->text = $text; $this->title = $title; } 60 61 62 63 64 public function getId(): PostId { return $this->id; } 65 66 67 public function getStatus(): PostStatus { (continues on next page) 1.4. More 125
130. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) return $this->status; 68 } 69 70 public function getText(): string { return $this->text; } 71 72 73 74 75 public function getTitle(): string { return $this->title; } 76 77 78 79 80 } PostId.php 1 <?php 2 3 namespace DesignPatterns\More\Repository\Domain; 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /** * This is a perfect example of a value object that is identifiable by it's value ˓ alone and → * is guaranteed to be valid each time an instance is created. Another important ˓ property of value objects → * is immutability. * * Notice also the use of a named constructor (fromInt) which adds a little context ˓ when creating an instance. → */ class PostId { /** * @var int */ private $id; 18 public static function fromInt(int $id) { self::ensureIsValid($id); 19 20 21 22 return new self($id); 23 } 24 25 private function __construct(int $id) { $this->id = $id; } 26 27 28 29 30 public function toInt(): int { return $this->id; } 31 32 33 34 35 private static function ensureIsValid(int $id) { if ($id <= 0) { 36 37 38 (continues on next page) 126 Chapter 1. Patterns
131. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) throw new \InvalidArgumentException('Invalid PostId given'); 39 } 40 } 41 42 } PostStatus.php 1 <?php 2 3 namespace DesignPatterns\More\Repository\Domain; 4 5 6 7 8 9 10 11 12 /** * Like PostId, this is a value object which holds the value of the current status of ˓ a Post. It can be constructed → * either from a string or int and is able to validate itself. An instance can then ˓ be converted back to int or string. → */ class PostStatus { const STATE_DRAFT_ID = 1; const STATE_PUBLISHED_ID = 2; 13 14 15 const STATE_DRAFT = 'draft'; const STATE_PUBLISHED = 'published'; 16 17 18 19 20 private static $validStates = [ self::STATE_DRAFT_ID => self::STATE_DRAFT, self::STATE_PUBLISHED_ID => self::STATE_PUBLISHED, ]; 21 22 23 24 25 /** * @var int */ private $id; 26 27 28 29 30 /** * @var string */ private $name; 31 32 33 34 public static function fromInt(int $statusId) { self::ensureIsValidId($statusId); 35 return new self($statusId, self::$validStates[$statusId]); 36 37 } 38 39 40 41 public static function fromString(string $status) { self::ensureIsValidName($status); 42 return new self(array_search($status, self::$validStates), $status); 43 44 } 45 46 47 48 private function __construct(int $id, string $name) { $this->id = $id; (continues on next page) 1.4. More 127
132. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) $this->name = $name; 49 } 50 51 public function toInt(): int { return $this->id; } 52 53 54 55 56 57 58 59 60 61 62 63 64 /** * there is a reason that I avoid using __toString() as it operates outside of ˓ the stack in PHP → * and is therefor not able to operate well with exceptions */ public function toString(): string { return $this->name; } 65 private static function ensureIsValidId(int $status) { if (!in_array($status, array_keys(self::$validStates), true)) { throw new \InvalidArgumentException('Invalid status id given'); } } 66 67 68 69 70 71 72 73 private static function ensureIsValidName(string $status) { if (!in_array($status, self::$validStates, true)) { throw new \InvalidArgumentException('Invalid status name given'); } } 74 75 76 77 78 79 80 } PostRepository.php 1 <?php 2 3 namespace DesignPatterns\More\Repository; 4 5 6 use DesignPatterns\More\Repository\Domain\Post; use DesignPatterns\More\Repository\Domain\PostId; 7 8 9 10 11 12 13 14 15 16 17 18 /** * This class is situated between Entity layer (class Post) and access object layer ˓ (Persistence). → * * Repository encapsulates the set of objects persisted in a data store and the ˓ operations performed over them → * providing a more object-oriented view of the persistence layer * * Repository also supports the objective of achieving a clean separation and one-way ˓ dependency → * between the domain and data mapping layers */ class PostRepository { (continues on next page) 128 Chapter 1. Patterns
133. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) /** * @var Persistence */ private $persistence; 19 20 21 22 23 public function __construct(Persistence $persistence) { $this->persistence = $persistence; } 24 25 26 27 28 public function generateId(): PostId { return PostId::fromInt($this->persistence->generateId()); } 29 30 31 32 33 34 35 36 37 38 39 40 public function findById(PostId $id): Post { try { $arrayData = $this->persistence->retrieve($id->toInt()); } catch (\OutOfBoundsException $e) { throw new \OutOfBoundsException(sprintf('Post with id %d does not exist', ˓ $id->toInt()), 0, $e); → } 41 return Post::fromState($arrayData); 42 } 43 44 public function save(Post $post) { $this->persistence->persist([ 'id' => $post->getId()->toInt(), 'statusId' => $post->getStatus()->toInt(), 'text' => $post->getText(), 'title' => $post->getTitle(), ]); } 45 46 47 48 49 50 51 52 53 54 } Persistence.php 1 <?php 2 3 namespace DesignPatterns\More\Repository; 4 5 6 7 interface Persistence { public function generateId(): int; 8 public function persist(array $data); 9 10 public function retrieve(int $id): array; 11 12 public function delete(int $id); 13 14 } InMemoryPersistence.php 1.4. More 129
134. DesignPatternsPHP Documentation, Release 1.0 1 <?php 2 3 namespace DesignPatterns\More\Repository; 4 5 6 7 8 9 10 class InMemoryPersistence implements Persistence { /** * @var array */ private $data = []; 11 /** * @var int */ private $lastId = 0; 12 13 14 15 16 public function generateId(): int { $this->lastId++; 17 18 19 20 return $this->lastId; 21 } 22 23 public function persist(array $data) { $this->data[$this->lastId] = $data; } 24 25 26 27 28 public function retrieve(int $id): array { if (!isset($this->data[$id])) { throw new \OutOfBoundsException(sprintf('No data found for ID %d', $id)); } 29 30 31 32 33 34 return $this->data[$id]; 35 } 36 37 public function delete(int $id) { if (!isset($this->data[$id])) { throw new \OutOfBoundsException(sprintf('No data found for ID %d', $id)); } 38 39 40 41 42 43 unset($this->data[$id]); 44 } 45 46 } Test Tests/PostRepositoryTest.php 1 <?php 2 3 namespace DesignPatterns\More\Repository\Tests; 4 5 use DesignPatterns\More\Repository\Domain\PostId; (continues on next page) 130 Chapter 1. Patterns
135. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) 6 7 8 9 10 use use use use use DesignPatterns\More\Repository\Domain\PostStatus; DesignPatterns\More\Repository\InMemoryPersistence; DesignPatterns\More\Repository\Domain\Post; DesignPatterns\More\Repository\PostRepository; PHPUnit\Framework\TestCase; 11 12 13 14 15 16 17 class PostRepositoryTest extends TestCase { /** * @var PostRepository */ private $repository; 18 protected function setUp() { $this->repository = new PostRepository(new InMemoryPersistence()); } 19 20 21 22 23 public function testCanGenerateId() { $this->assertEquals(1, $this->repository->generateId()->toInt()); } 24 25 26 27 28 /** * @expectedException \OutOfBoundsException * @expectedExceptionMessage Post with id 42 does not exist */ public function testThrowsExceptionWhenTryingToFindPostWhichDoesNotExist() { $this->repository->findById(PostId::fromInt(42)); } 29 30 31 32 33 34 35 36 37 public function testCanPersistPostDraft() { $postId = $this->repository->generateId(); $post = Post::draft($postId, 'Repository Pattern', 'Design Patterns PHP'); $this->repository->save($post); 38 39 40 41 42 43 $this->repository->findById($postId); 44 45 $this->assertEquals($postId, $this->repository->findById($postId)->getId()); $this->assertEquals(PostStatus::STATE_DRAFT, $post->getStatus()->toString()); 46 47 } 48 49 } 1.4.3 Entity-Attribute-Value (EAV) The Entity–attribute–value (EAV) pattern in order to implement EAV model with PHP. Purpose The Entity–attribute–value (EAV) model is a data model to describe entities where the number of attributes (properties, parameters) that can be used to describe them is potentially vast, but the number that will actually apply to a given entity is relatively modest. 1.4. More 131
136. DesignPatternsPHP Documentation, Release 1.0 132 Chapter 1. Patterns
137. DesignPatternsPHP Documentation, Release 1.0 UML Diagram 1.4. More 133
138. DesignPatternsPHP Documentation, Release 1.0 Code You can also find this code on GitHub Entity.php 1 <?php 2 3 namespace DesignPatterns\More\EAV; 4 5 6 7 8 9 10 class Entity { /** * @var \SplObjectStorage */ private $values; 11 /** * @var string */ private $name; 12 13 14 15 16 /** * @param string $name * @param Value[] $values */ public function __construct(string $name, $values) { $this->values = new \SplObjectStorage(); $this->name = $name; 17 18 19 20 21 22 23 24 25 foreach ($values as $value) { $this->values->attach($value); } 26 27 28 } 29 30 public function __toString(): string { $text = [$this->name]; 31 32 33 34 foreach ($this->values as $value) { $text[] = (string) $value; } 35 36 37 38 return join(', ', $text); 39 } 40 41 } Attribute.php 1 <?php 2 3 namespace DesignPatterns\More\EAV; 4 5 6 7 8 class Attribute { /** * @var \SplObjectStorage (continues on next page) 134 Chapter 1. Patterns
139. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) */ private $values; 9 10 11 /** * @var string */ private $name; 12 13 14 15 16 public function __construct(string $name) { $this->values = new \SplObjectStorage(); $this->name = $name; } 17 18 19 20 21 22 public function addValue(Value $value) { $this->values->attach($value); } 23 24 25 26 27 /** * @return \SplObjectStorage */ public function getValues(): \SplObjectStorage { return $this->values; } 28 29 30 31 32 33 34 35 public function __toString(): string { return $this->name; } 36 37 38 39 40 } Value.php 1 <?php 2 3 namespace DesignPatterns\More\EAV; 4 5 6 7 8 9 10 class Value { /** * @var Attribute */ private $attribute; 11 12 13 14 15 /** * @var string */ private $name; 16 17 18 19 20 public function __construct(Attribute $attribute, string $name) { $this->name = $name; $this->attribute = $attribute; 21 22 $attribute->addValue($this); (continues on next page) 1.4. More 135
140. DesignPatternsPHP Documentation, Release 1.0 (continued from previous page) } 23 24 public function __toString(): string { return sprintf('%s: %s', $this->attribute, $this->name); } 25 26 27 28 29 } Test Tests/EAVTest.php 1 <?php 2 3 namespace DesignPatterns\More\EAV\Tests; 4 5 6 7 8 use use use use DesignPatterns\More\EAV\Attribute; DesignPatterns\More\EAV\Entity; DesignPatterns\More\EAV\Value; PHPUnit\Framework\TestCase; 9 10 11 12 13 14 15 16 class EAVTest extends TestCase { public function testCanAddAttributeToEntity() { $colorAttribute = new Attribute('color'); $colorSilver = new Value($colorAttribute, 'silver'); $colorBlack = new Value($colorAttribute, 'black'); 17 $memoryAttribute = new Attribute('memory'); $memory8Gb = new Value($memoryAttribute, '8GB'); 18 19 20 $entity = new Entity('MacBook Pro', [$colorSilver, $colorBlack, $memory8Gb]); 21 22 23 ˓ → 24 25 $this->assertEquals('MacBook Pro, color: silver, color: black, memory: 8GB', (string) $entity); } } 136 Chapter 1. Patterns
141. CHAPTER 2 Contribute If you encounter any bugs or missing translations, please feel free to fork and send a pull request with your changes. To establish a consistent code quality, please check your code using PHP CodeSniffer against PSR2 standard using ./vendor/bin/phpcs -p --standard=PSR2 --ignore=vendor .. 137

首页 - Wiki
Copyright © 2011-2024 iteam. Current version is 2.139.0. UTC+08:00, 2024-12-27 08:32
浙ICP备14020137号-1 $访客地图$