Design Patterns PHP
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.
- 2.1. Adapter / Wrapper
- 2.2. Bridge
- 2.3. Composite
- 2.4. Data Mapper
- 2.5. Decorator
- 2.6. Dependency Injection
- 2.7. Facade
- 2.8. Fluent Interface
- 2.9. Flyweight
- 2.10. Proxy
- 2.11. Registry
2.1. Adapter / Wrapper
2.1.1. 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.
2.1.2. Examples
- DB Client libraries adapter
- using multiple different webservices and adapters normalize data so that the outcome is the same for all
2.1.3. UML Diagram
2.1.4. Code
You can also find this code on GitHub
BookInterface.php
<?php namespace DesignPatterns\Structural\Adapter; interface BookInterface { public function turnPage(); public function open(); public function getPage(): int; }
Book.php
<?php namespace DesignPatterns\Structural\Adapter; class Book implements BookInterface { /** * @var int */ private $page; public function open() { $this->page = 1; } public function turnPage() { $this->page++; } public function getPage(): int { return $this->page; } }
<?php namespace DesignPatterns\Structural\Adapter; class Book implements BookInterface { /** * @var int */ private $page; public function open() { $this->page = 1; } public function turnPage() { $this->page++; } public function getPage(): int { return $this->page; } }
EBookAdapter.php
<?php namespace DesignPatterns\Structural\Adapter; /** * 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 */ class EBookAdapter implements BookInterface { /** * @var EBookInterface */ protected $eBook; /** * @param EBookInterface $eBook */ public function __construct(EBookInterface $eBook) { $this->eBook = $eBook; } /** * This class makes the proper translation from one interface to another. */ public function open() { $this->eBook->unlock(); } public function turnPage() { $this->eBook->pressNext(); } /** * 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
<?php namespace DesignPatterns\Structural\Adapter; interface EBookInterface { public function unlock(); public function pressNext(); /** * returns current page and total number of pages, like [10, 100] is page 10 of 100 * * @return int[] */ public function getPage(): array; }
Kindle.php
<?php namespace DesignPatterns\Structural\Adapter; /** * 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; /** * @var int */ private $totalPages = 100; public function pressNext() { $this->page++; } public function unlock() { } /** * 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]; } }
2.1.5. Test
Tests/AdapterTest.php
<?php namespace DesignPatterns\Structural\Adapter\Tests; use DesignPatterns\Structural\Adapter\Book; use DesignPatterns\Structural\Adapter\EBookAdapter; use DesignPatterns\Structural\Adapter\Kindle; use PHPUnit\Framework\TestCase; class AdapterTest extends TestCase { public function testCanTurnPageOnBook() { $book = new Book(); $book->open(); $book->turnPage(); $this->assertEquals(2, $book->getPage()); } public function testCanTurnPageOnKindleLikeInANormalBook() { $kindle = new Kindle(); $book = new EBookAdapter($kindle); $book->open(); $book->turnPage(); $this->assertEquals(2, $book->getPage()); } }
2.2. Bridge
2.2.1. Purpose
Decouple an abstraction from its implementation so that the two can vary independently.
2.2.2. Examples
2.2.3. UML Diagram
2.2.4. Code
You can also find this code on GitHub
FormatterInterface.php
<?php namespace DesignPatterns\Structural\Bridge; class PlainTextFormatter implements FormatterInterface { public function format(string $text) { return $text; } }
PlainTextFormatter.php
HtmlFormatter.php
<?php namespace DesignPatterns\Structural\Bridge; class HtmlFormatter implements FormatterInterface { public function format(string $text) { return sprintf('<p>%s</p>', $text); } }
Service.php
<?php namespace DesignPatterns\Structural\Bridge; abstract class Service { /** * @var FormatterInterface */ protected $implementation; /** * @param FormatterInterface $printer */ public function __construct(FormatterInterface $printer) { $this->implementation = $printer; } /** * @param FormatterInterface $printer */ public function setImplementation(FormatterInterface $printer) { $this->implementation = $printer; } abstract public function get(); }
HelloWorldService.php
<?php namespace DesignPatterns\Structural\Bridge; class HelloWorldService extends Service { public function get() { return $this->implementation->format('Hello World'); } }
2.2.5. Test
Tests/BridgeTest.php
<?php namespace DesignPatterns\Structural\Bridge\Tests; use DesignPatterns\Structural\Bridge\HelloWorldService; use DesignPatterns\Structural\Bridge\HtmlFormatter; use DesignPatterns\Structural\Bridge\PlainTextFormatter; use PHPUnit\Framework\TestCase; class BridgeTest extends TestCase { public function testCanPrintUsingThePlainTextPrinter() { $service = new HelloWorldService(new PlainTextFormatter()); $this->assertEquals('Hello World', $service->get()); // now change the implementation and use the HtmlFormatter instead $service->setImplementation(new HtmlFormatter()); $this->assertEquals('<p>Hello World</p>', $service->get()); } }
2.3. Composite
2.3.1. Purpose
To treat a group of objects the same way as a single instance of the object.
2.3.2. 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 callsrender()
on them Zend_Config
: a tree of
configuration options, each one is aZend_Config
object itself
2.3.3. UML Diagram
2.3.4. Code
You can also find this code on GitHub
RenderableInterface.php
<?php namespace DesignPatterns\Structural\Composite; interface RenderableInterface { public function render(): string; }
Form.php
<?php namespace DesignPatterns\Structural\Composite; /** * 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; /** * 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>'; foreach ($this->elements as $element) { $formCode .= $element->render(); } $formCode .= '</form>'; return $formCode; } /** * @param RenderableInterface $element */ public function addElement(RenderableInterface $element) { $this->elements[] = $element; } }
InputElement.php
<?php namespace DesignPatterns\Structural\Composite; class InputElement implements RenderableInterface { public function render(): string { return '<input type="text" />'; } }
TextElement.php
<?php namespace DesignPatterns\Structural\Composite; class TextElement implements RenderableInterface { /** * @var string */ private $text; public function __construct(string $text) { $this->text = $text; } public function render(): string { return $this->text; } }
2.3.5. Test
Tests/CompositeTest.php
<?php namespace DesignPatterns\Structural\Composite\Tests; use DesignPatterns\Structural\Composite; use PHPUnit\Framework\TestCase; 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); // This is just an example, in a real world scenario it is important to remember that web browsers do not // currently support nested forms $this->assertEquals( '<form>Email:<input type="text" /><form>Password:<input type="text" /></form></form>', $form->render() ); } }
2.4. Data Mapper
2.4.1. 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 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.
2.4.2. Examples
- DB Object Relational Mapper (ORM) : Doctrine2 uses DAO named as “EntityRepository”
2.4.3. UML Diagram
2.4.4. Code
You can also find this code on GitHub
User.php
<?php namespace DesignPatterns\Structural\DataMapper; class User { /** * @var string */ private $username; /** * @var string */ private $email; public static function fromState(array $state): User { // validate state before accessing keys! return new self( $state['username'], $state['email'] ); } public function __construct(string $username, string $email) { // validate parameters before setting them! $this->username = $username; $this->email = $email; } /** * @return string */ public function getUsername() { return $this->username; } /** * @return string */ public function getEmail() { return $this->email; } }
UserMapper.php
<?php namespace DesignPatterns\Structural\DataMapper; class UserMapper { /** * @var StorageAdapter */ private $adapter; /** * @param StorageAdapter $storage */ public function __construct(StorageAdapter $storage) { $this->adapter = $storage; } /** * 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); if ($result === null) { throw new \InvalidArgumentException("User #$id not found"); } return $this->mapRowToUser($result); } private function mapRowToUser(array $row): User { return User::fromState($row); } }
StorageAdapter.php
<?php namespace DesignPatterns\Structural\DataMapper; class StorageAdapter { /** * @var array */ private $data = []; public function __construct(array $data) { $this->data = $data; } /** * @param int $id * * @return array|null */ public function find(int $id) { if (isset($this->data[$id])) { return $this->data[$id]; } return null; } }
2.4.5. Test
Tests/DataMapperTest.php
<?php namespace DesignPatterns\Structural\DataMapper\Tests; use DesignPatterns\Structural\DataMapper\StorageAdapter; use DesignPatterns\Structural\DataMapper\User; use DesignPatterns\Structural\DataMapper\UserMapper; use PHPUnit\Framework\TestCase; class DataMapperTest extends TestCase { public function testCanMapUserFromStorage() { $storage = new StorageAdapter([1 => ['username' => 'domnikl', 'email' => '[email protected]']]); $mapper = new UserMapper($storage); $user = $mapper->findById(1); $this->assertInstanceOf(User::class, $user); } /** * @expectedException \InvalidArgumentException */ public function testWillNotMapInvalidData() { $storage = new StorageAdapter([]); $mapper = new UserMapper($storage); $mapper->findById(1); } }
2.5. Decorator
2.5.1. Purpose
To dynamically add new functionality to class instances.
2.5.2. 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)
2.5.3. UML Diagram
2.5.4. Code
You can also find this code on GitHub
Booking.php
<?php namespace DesignPatterns\Structural\Decorator; interface Booking { public function calculatePrice(): int; public function getDescription(): string; }
BookingDecorator.php
<?php namespace DesignPatterns\Structural\Decorator; abstract class BookingDecorator implements Booking { /** * @var Booking */ protected $booking; public function __construct(Booking $booking) { $this->booking = $booking; } }
DoubleRoomBooking.php
<?php namespace DesignPatterns\Structural\Decorator; class DoubleRoomBooking implements Booking { public function calculatePrice(): int { return 40; } public function getDescription(): string { return 'double room'; } }
ExtraBed.php
<?php namespace DesignPatterns\Structural\Decorator; class ExtraBed extends BookingDecorator { private const PRICE = 30; public function calculatePrice(): int { return $this->booking->calculatePrice() + self::PRICE; } public function getDescription(): string { return $this->booking->getDescription() . ' with extra bed'; } }
WiFi.php
<?php namespace DesignPatterns\Structural\Decorator; class WiFi extends BookingDecorator { private const PRICE = 2; public function calculatePrice(): int { return $this->booking->calculatePrice() + self::PRICE; } public function getDescription(): string { return $this->booking->getDescription() . ' with wifi'; } }
2.5.5. Test
Tests/DecoratorTest.php
<?php namespace DesignPatterns\Structural\Decorator\Tests; use DesignPatterns\Structural\Decorator\DoubleRoomBooking; use DesignPatterns\Structural\Decorator\ExtraBed; use DesignPatterns\Structural\Decorator\WiFi; use PHPUnit\Framework\TestCase; class DecoratorTest extends TestCase { public function testCanCalculatePriceForBasicDoubleRoomBooking() { $booking = new DoubleRoomBooking(); $this->assertEquals(40, $booking->calculatePrice()); $this->assertEquals('double room', $booking->getDescription()); } public function testCanCalculatePriceForDoubleRoomBookingWithWiFi() { $booking = new DoubleRoomBooking(); $booking = new WiFi($booking); $this->assertEquals(42, $booking->calculatePrice()); $this->assertEquals('double room with wifi', $booking->getDescription()); } public function testCanCalculatePriceForDoubleRoomBookingWithWiFiAndExtraBed() { $booking = new DoubleRoomBooking(); $booking = new WiFi($booking); $booking = new ExtraBed($booking); $this->assertEquals(72, $booking->calculatePrice()); $this->assertEquals('double room with wifi with extra bed', $booking->getDescription()); } }
2.6. Dependency Injection
2.6.1. Purpose
To implement a loosely coupled architecture in order to get better testable, maintainable and extendable code.
2.6.2. 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.
2.6.3. 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 theConnection
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)
2.6.4. UML Diagram
2.6.5. Code
You can also find this code on GitHub
DatabaseConfiguration.php
<?php namespace DesignPatterns\Structural\DependencyInjection; class DatabaseConfiguration { /** * @var string */ private $host; /** * @var int */ private $port; /** * @var string */ private $username; /** * @var string */ private $password; public function __construct(string $host, int $port, string $username, string $password) { $this->host = $host; $this->port = $port; $this->username = $username; $this->password = $password; } public function getHost(): string { return $this->host; } public function getPort(): int { return $this->port; } public function getUsername(): string { return $this->username; } public function getPassword(): string { return $this->password; } }
DatabaseConnection.php
<?php namespace DesignPatterns\Structural\DependencyInjection; class DatabaseConnection { /** * @var DatabaseConfiguration */ private $configuration; /** * @param DatabaseConfiguration $config */ public function __construct(DatabaseConfiguration $config) { $this->configuration = $config; } public 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 return sprintf( '%s:%[email protected]%s:%d', $this->configuration->getUsername(), $this->configuration->getPassword(), $this->configuration->getHost(), $this->configuration->getPort() ); } }
2.6.6. Test
Tests/DependencyInjectionTest.php
<?php namespace DesignPatterns\Structural\DependencyInjection\Tests; use DesignPatterns\Structural\DependencyInjection\DatabaseConfiguration; use DesignPatterns\Structural\DependencyInjection\DatabaseConnection; use PHPUnit\Framework\TestCase; class DependencyInjectionTest extends TestCase { public function testDependencyInjection() { $config = new DatabaseConfiguration('localhost', 3306, 'domnikl', '1234'); $connection = new DatabaseConnection($config); $this->assertEquals('domnikl:[email protected]:3306', $connection->getDsn()); } }
2.7. Facade
2.7.1. 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.
2.7.2. UML Diagram
2.7.3. Code
You can also find this code on GitHub
Facade.php
<?php namespace DesignPatterns\Structural\Facade; class Facade { /** * @var OsInterface */ private $os; /** * @var BiosInterface */ private $bios; /** * @param BiosInterface $bios * @param OsInterface $os */ public function __construct(BiosInterface $bios, OsInterface $os) { $this->bios = $bios; $this->os = $os; } public function turnOn() { $this->bios->execute(); $this->bios->waitForKeyPress(); $this->bios->launch($this->os); } public function turnOff() { $this->os->halt(); $this->bios->powerDown(); } }
OsInterface.php
<?php namespace DesignPatterns\Structural\Facade; interface OsInterface { public function halt(); public function getName(): string; }
BiosInterface.php
<?php namespace DesignPatterns\Structural\Facade; interface BiosInterface { public function execute(); public function waitForKeyPress(); public function launch(OsInterface $os); public function powerDown(); }
2.7.4. Test
Tests/FacadeTest.php
<?php namespace DesignPatterns\Structural\Facade\Tests; use DesignPatterns\Structural\Facade\Facade; use DesignPatterns\Structural\Facade\OsInterface; use PHPUnit\Framework\TestCase; class FacadeTest extends TestCase { public function testComputerOn() { /** @var OsInterface|\PHPUnit_Framework_MockObject_MockObject $os */ $os = $this->createMock('DesignPatterns\Structural\Facade\OsInterface'); $os->method('getName') ->will($this->returnValue('Linux')); $bios = $this->getMockBuilder('DesignPatterns\Structural\Facade\BiosInterface') ->setMethods(['launch', 'execute', 'waitForKeyPress']) ->disableAutoload() ->getMock(); $bios->expects($this->once()) ->method('launch') ->with($os); $facade = new Facade($bios, $os); // the facade interface is simple $facade->turnOn(); // but you can also access the underlying components $this->assertEquals('Linux', $os->getName()); } }
2.8. Fluent Interface
2.8.1. Purpose
To write code that is easy readable just like sentences in a natural language (like English).
2.8.2. 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
2.8.3. UML Diagram
2.8.4. Code
You can also find this code on GitHub
Sql.php
<?php namespace DesignPatterns\Structural\FluentInterface; class Sql { /** * @var array */ private $fields = []; /** * @var array */ private $from = []; /** * @var array */ private $where = []; public function select(array $fields): Sql { $this->fields = $fields; return $this; } public function from(string $table, string $alias): Sql { $this->from[] = $table.' AS '.$alias; return $this; } public function where(string $condition): Sql { $this->where[] = $condition; return $this; } public function __toString(): string { return sprintf( 'SELECT %s FROM %s WHERE %s', join(', ', $this->fields), join(', ', $this->from), join(' AND ', $this->where) ); } }
2.8.5. Test
Tests/FluentInterfaceTest.php
<?php namespace DesignPatterns\Structural\FluentInterface\Tests; use DesignPatterns\Structural\FluentInterface\Sql; use PHPUnit\Framework\TestCase; class FluentInterfaceTest extends TestCase { public function testBuildSQL() { $query = (new Sql()) ->select(['foo', 'bar']) ->from('foobar', 'f') ->where('f.bar = ?'); $this->assertEquals('SELECT foo, bar FROM foobar AS f WHERE f.bar = ?', (string) $query); } }
2.9. Flyweight
2.9.1. 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.
2.9.2. UML Diagram
2.9.3. Code
You can also find this code on GitHub
FlyweightInterface.php
<?php namespace DesignPatterns\Structural\Flyweight; interface FlyweightInterface { public function render(string $extrinsicState): string; }
CharacterFlyweight.php
<?php namespace DesignPatterns\Structural\Flyweight; /** * 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; public function __construct(string $name) { $this->name = $name; } 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. return sprintf('Character %s with font %s', $this->name, $font); } }
FlyweightFactory.php
<?php namespace DesignPatterns\Structural\Flyweight; /** * 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 { /** * @var CharacterFlyweight[] */ private $pool = []; public function get(string $name): CharacterFlyweight { if (!isset($this->pool[$name])) { $this->pool[$name] = new CharacterFlyweight($name); } return $this->pool[$name]; } public function count(): int { return count($this->pool); } }
2.9.4. Test
Tests/FlyweightTest.php
<?php namespace DesignPatterns\Structural\Flyweight\Tests; use DesignPatterns\Structural\Flyweight\FlyweightFactory; use PHPUnit\Framework\TestCase; 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']; public function testFlyweight() { $factory = new FlyweightFactory(); foreach ($this->characters as $char) { foreach ($this->fonts as $font) { $flyweight = $factory->get($char); $rendered = $flyweight->render($font); $this->assertEquals(sprintf('Character %s with font %s', $char, $font), $rendered); } } // 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 $this->assertCount(count($this->characters), $factory); } }
2.10. Proxy
2.10.1. Purpose
To interface to anything that is expensive or impossible to duplicate.
2.10.2. 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
2.10.3. UML Diagram
2.10.4. Code
You can also find this code on GitHub
BankAccount.php
<?php namespace DesignPatterns\Structural\Proxy; interface BankAccount { public function deposit(int $amount); public function getBalance(): int; }
HeavyBankAccount.php
<?php namespace DesignPatterns\Structural\Proxy; class HeavyBankAccount implements BankAccount { /** * @var int[] */ private $transactions = []; public function deposit(int $amount) { $this->transactions[] = $amount; } public 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 return array_sum($this->transactions); } }
BankAccountProxy.php
<?php namespace DesignPatterns\Structural\Proxy; class BankAccountProxy extends HeavyBankAccount implements BankAccount { /** * @var int */ private $balance; public 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 if ($this->balance === null) { $this->balance = parent::getBalance(); } return $this->balance; } }
2.10.5. Test
2.11. Registry
2.11.1. 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!
2.11.2. Examples
- Zend Framework 1:
Zend_Registry
holds the application’s logger object, front controller etc. - Yii Framework:
CWebApplication
holds all the application components, such asCWebUser
,
CUrlManager
, etc.
2.11.3. UML Diagram
2.11.4. Code
You can also find this code on GitHub
Registry.php
<?php namespace DesignPatterns\Structural\Registry; abstract class Registry { const LOGGER = 'logger'; /** * 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 = []; /** * @var array */ private static $allowedKeys = [ self::LOGGER, ]; /** * @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'); } self::$storedValues[$key] = $value; } /** * @param string $key * * @return mixed */ public static function get(string $key) { if (!in_array($key, self::$allowedKeys) || !isset(self::$storedValues[$key])) { throw new \InvalidArgumentException('Invalid key given'); } return self::$storedValues[$key]; } }
2.11.5. Test
Tests/RegistryTest.php
<?php namespace DesignPatterns\Structural\Registry\Tests; use DesignPatterns\Structural\Registry\Registry; use stdClass; use PHPUnit\Framework\TestCase; class RegistryTest extends TestCase { public function testSetAndGetLogger() { $key = Registry::LOGGER; $logger = new stdClass(); Registry::set($key, $logger); $storedLogger = Registry::get($key); $this->assertSame($logger, $storedLogger); $this->assertInstanceOf(stdClass::class, $storedLogger); } /** * @expectedException \InvalidArgumentException */ public function testThrowsExceptionWhenTryingToSetInvalidKey() { Registry::set('foobar', new stdClass()); } /** * 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); } }