4.2. Repository
4.2.1. Purpose
Mediates between the domain and data mapping layers using acollection-like interface for accessing domain objects. Repositoryencapsulates the set of objects persisted in a data store and theoperations performed over them, providing a more object-oriented view ofthe persistence layer. Repository also supports the objective ofachieving a clean separation and one-way dependency between the domainand data mapping layers.
4.2.2. Examples
- Doctrine 2 ORM: there is Repository that mediates between Entity andDBAL and contains methods to retrieve objects
- Laravel Framework
4.2.3. UML Diagram
4.2.4. Code
You can also find this code on GitHub
Post.php
- <?php
- namespace DesignPatterns\More\Repository\Domain;
- class Post
- {
- /**
- * @var PostId
- */
- private $id;
- /**
- * @var PostStatus
- */
- private $status;
- /**
- * @var string
- */
- private $title;
- /**
- * @var string
- */
- private $text;
- public static function draft(PostId $id, string $title, string $text): Post
- {
- return new self(
- $id,
- PostStatus::fromString(PostStatus::STATE_DRAFT),
- $title,
- $text
- );
- }
- public static function fromState(array $state): Post
- {
- return new self(
- PostId::fromInt($state['id']),
- PostStatus::fromInt($state['statusId']),
- $state['title'],
- $state['text']
- );
- }
- /**
- * @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;
- }
- public function getId(): PostId
- {
- return $this->id;
- }
- public function getStatus(): PostStatus
- {
- return $this->status;
- }
- public function getText(): string
- {
- return $this->text;
- }
- public function getTitle(): string
- {
- return $this->title;
- }
- }
PostId.php
- <?php
- namespace DesignPatterns\More\Repository\Domain;
- /**
- * 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;
- public static function fromInt(int $id)
- {
- self::ensureIsValid($id);
- return new self($id);
- }
- private function __construct(int $id)
- {
- $this->id = $id;
- }
- public function toInt(): int
- {
- return $this->id;
- }
- private static function ensureIsValid(int $id)
- {
- if ($id <= 0) {
- throw new \InvalidArgumentException('Invalid PostId given');
- }
- }
- }
PostStatus.php
- <?php
- namespace DesignPatterns\More\Repository\Domain;
- /**
- * 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;
- const STATE_DRAFT = 'draft';
- const STATE_PUBLISHED = 'published';
- private static $validStates = [
- self::STATE_DRAFT_ID => self::STATE_DRAFT,
- self::STATE_PUBLISHED_ID => self::STATE_PUBLISHED,
- ];
- /**
- * @var int
- */
- private $id;
- /**
- * @var string
- */
- private $name;
- public static function fromInt(int $statusId)
- {
- self::ensureIsValidId($statusId);
- return new self($statusId, self::$validStates[$statusId]);
- }
- public static function fromString(string $status)
- {
- self::ensureIsValidName($status);
- return new self(array_search($status, self::$validStates), $status);
- }
- private function __construct(int $id, string $name)
- {
- $this->id = $id;
- $this->name = $name;
- }
- public function toInt(): int
- {
- return $this->id;
- }
- /**
- * 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;
- }
- private static function ensureIsValidId(int $status)
- {
- if (!in_array($status, array_keys(self::$validStates), true)) {
- throw new \InvalidArgumentException('Invalid status id given');
- }
- }
- private static function ensureIsValidName(string $status)
- {
- if (!in_array($status, self::$validStates, true)) {
- throw new \InvalidArgumentException('Invalid status name given');
- }
- }
- }
PostRepository.php
- <?php
- namespace DesignPatterns\More\Repository;
- use DesignPatterns\More\Repository\Domain\Post;
- use DesignPatterns\More\Repository\Domain\PostId;
- /**
- * 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
- {
- /**
- * @var Persistence
- */
- private $persistence;
- public function __construct(Persistence $persistence)
- {
- $this->persistence = $persistence;
- }
- public function generateId(): PostId
- {
- return PostId::fromInt($this->persistence->generateId());
- }
- 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);
- }
- return Post::fromState($arrayData);
- }
- public function save(Post $post)
- {
- $this->persistence->persist([
- 'id' => $post->getId()->toInt(),
- 'statusId' => $post->getStatus()->toInt(),
- 'text' => $post->getText(),
- 'title' => $post->getTitle(),
- ]);
- }
- }
Persistence.php
- <?php
- namespace DesignPatterns\More\Repository;
- interface Persistence
- {
- public function generateId(): int;
- public function persist(array $data);
- public function retrieve(int $id): array;
- public function delete(int $id);
- }
InMemoryPersistence.php
- <?php
- namespace DesignPatterns\More\Repository;
- class InMemoryPersistence implements Persistence
- {
- /**
- * @var array
- */
- private $data = [];
- /**
- * @var int
- */
- private $lastId = 0;
- public function generateId(): int
- {
- $this->lastId++;
- return $this->lastId;
- }
- public function persist(array $data)
- {
- $this->data[$this->lastId] = $data;
- }
- public function retrieve(int $id): array
- {
- if (!isset($this->data[$id])) {
- throw new \OutOfBoundsException(sprintf('No data found for ID %d', $id));
- }
- return $this->data[$id];
- }
- public function delete(int $id)
- {
- if (!isset($this->data[$id])) {
- throw new \OutOfBoundsException(sprintf('No data found for ID %d', $id));
- }
- unset($this->data[$id]);
- }
- }
4.2.5. Test
Tests/PostRepositoryTest.php
- <?php
- namespace DesignPatterns\More\Repository\Tests;
- use DesignPatterns\More\Repository\Domain\PostId;
- use DesignPatterns\More\Repository\Domain\PostStatus;
- use DesignPatterns\More\Repository\InMemoryPersistence;
- use DesignPatterns\More\Repository\Domain\Post;
- use DesignPatterns\More\Repository\PostRepository;
- use PHPUnit\Framework\TestCase;
- class PostRepositoryTest extends TestCase
- {
- /**
- * @var PostRepository
- */
- private $repository;
- protected function setUp()
- {
- $this->repository = new PostRepository(new InMemoryPersistence());
- }
- public function testCanGenerateId()
- {
- $this->assertEquals(1, $this->repository->generateId()->toInt());
- }
- /**
- * @expectedException \OutOfBoundsException
- * @expectedExceptionMessage Post with id 42 does not exist
- */
- public function testThrowsExceptionWhenTryingToFindPostWhichDoesNotExist()
- {
- $this->repository->findById(PostId::fromInt(42));
- }
- public function testCanPersistPostDraft()
- {
- $postId = $this->repository->generateId();
- $post = Post::draft($postId, 'Repository Pattern', 'Design Patterns PHP');
- $this->repository->save($post);
- $this->repository->findById($postId);
- $this->assertEquals($postId, $this->repository->findById($postId)->getId());
- $this->assertEquals(PostStatus::STATE_DRAFT, $post->getStatus()->toString());
- }
- }