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