4.2. Repository

4.2.1. 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.

4.2.2. Examples

  • Doctrine 2 ORM: there is Repository that mediates between Entity and DBAL and contains methods to retrieve objects

  • Laravel Framework

4.2.3. UML Diagram

Alt Repository UML Diagram

4.2.4. Code

You can also find this code on GitHub

Post.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\More\Repository\Domain;
 6
 7class Post
 8{
 9    public static function draft(PostId $id, string $title, string $text): Post
10    {
11        return new self(
12            $id,
13            PostStatus::fromString(PostStatus::STATE_DRAFT),
14            $title,
15            $text
16        );
17    }
18
19    public static function fromState(array $state): Post
20    {
21        return new self(
22            PostId::fromInt($state['id']),
23            PostStatus::fromInt($state['statusId']),
24            $state['title'],
25            $state['text']
26        );
27    }
28
29    private function __construct(
30        private PostId $id,
31        private PostStatus $status,
32        private string $title,
33        private string $text
34    ) {
35    }
36
37    public function getId(): PostId
38    {
39        return $this->id;
40    }
41
42    public function getStatus(): PostStatus
43    {
44        return $this->status;
45    }
46
47    public function getText(): string
48    {
49        return $this->text;
50    }
51
52    public function getTitle(): string
53    {
54        return $this->title;
55    }
56}

PostId.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\More\Repository\Domain;
 6
 7use InvalidArgumentException;
 8
 9/**
10 * This is a perfect example of a value object that is identifiable by it's value alone and
11 * is guaranteed to be valid each time an instance is created. Another important property of value objects
12 * is immutability.
13 *
14 * Notice also the use of a named constructor (fromInt) which adds a little context when creating an instance.
15 */
16class PostId
17{
18    public static function fromInt(int $id): PostId
19    {
20        self::ensureIsValid($id);
21
22        return new self($id);
23    }
24
25    private function __construct(private int $id)
26    {
27    }
28
29    public function toInt(): int
30    {
31        return $this->id;
32    }
33
34    private static function ensureIsValid(int $id)
35    {
36        if ($id <= 0) {
37            throw new InvalidArgumentException('Invalid PostId given');
38        }
39    }
40}

PostStatus.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\More\Repository\Domain;
 6
 7use InvalidArgumentException;
 8
 9/**
10 * Like PostId, this is a value object which holds the value of the current status of a Post. It can be constructed
11 * either from a string or int and is able to validate itself. An instance can then be converted back to int or string.
12 */
13class PostStatus
14{
15    public const STATE_DRAFT_ID = 1;
16    public const STATE_PUBLISHED_ID = 2;
17
18    public const STATE_DRAFT = 'draft';
19    public const STATE_PUBLISHED = 'published';
20
21    private static array $validStates = [
22        self::STATE_DRAFT_ID => self::STATE_DRAFT,
23        self::STATE_PUBLISHED_ID => self::STATE_PUBLISHED,
24    ];
25
26    public static function fromInt(int $statusId)
27    {
28        self::ensureIsValidId($statusId);
29
30        return new self($statusId, self::$validStates[$statusId]);
31    }
32
33    public static function fromString(string $status)
34    {
35        self::ensureIsValidName($status);
36        $state = array_search($status, self::$validStates);
37
38        if ($state === false) {
39            throw new InvalidArgumentException('Invalid state given!');
40        }
41
42        return new self($state, $status);
43    }
44
45    private function __construct(private int $id, private string $name)
46    {
47    }
48
49    public function toInt(): int
50    {
51        return $this->id;
52    }
53
54    /**
55     * there is a reason that I avoid using __toString() as it operates outside of the stack in PHP
56     * and is therefore not able to operate well with exceptions
57     */
58    public function toString(): string
59    {
60        return $this->name;
61    }
62
63    private static function ensureIsValidId(int $status)
64    {
65        if (!in_array($status, array_keys(self::$validStates), true)) {
66            throw new InvalidArgumentException('Invalid status id given');
67        }
68    }
69
70
71    private static function ensureIsValidName(string $status)
72    {
73        if (!in_array($status, self::$validStates, true)) {
74            throw new InvalidArgumentException('Invalid status name given');
75        }
76    }
77}

PostRepository.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\More\Repository;
 6
 7use OutOfBoundsException;
 8use DesignPatterns\More\Repository\Domain\Post;
 9use DesignPatterns\More\Repository\Domain\PostId;
10
11/**
12 * This class is situated between Entity layer (class Post) and access object layer (Persistence).
13 *
14 * Repository encapsulates the set of objects persisted in a data store and the operations performed over them
15 * providing a more object-oriented view of the persistence layer
16 *
17 * Repository also supports the objective of achieving a clean separation and one-way dependency
18 * between the domain and data mapping layers
19 */
20class PostRepository
21{
22    public function __construct(private Persistence $persistence)
23    {
24    }
25
26    public function generateId(): PostId
27    {
28        return PostId::fromInt($this->persistence->generateId());
29    }
30
31    public function findById(PostId $id): Post
32    {
33        try {
34            $arrayData = $this->persistence->retrieve($id->toInt());
35        } catch (OutOfBoundsException $e) {
36            throw new OutOfBoundsException(sprintf('Post with id %d does not exist', $id->toInt()), 0, $e);
37        }
38
39        return Post::fromState($arrayData);
40    }
41
42    public function save(Post $post)
43    {
44        $this->persistence->persist([
45            'id' => $post->getId()->toInt(),
46            'statusId' => $post->getStatus()->toInt(),
47            'text' => $post->getText(),
48            'title' => $post->getTitle(),
49        ]);
50    }
51}

Persistence.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\More\Repository;
 6
 7interface Persistence
 8{
 9    public function generateId(): int;
10
11    public function persist(array $data);
12
13    public function retrieve(int $id): array;
14
15    public function delete(int $id);
16}

InMemoryPersistence.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\More\Repository;
 6
 7use OutOfBoundsException;
 8
 9class InMemoryPersistence implements Persistence
10{
11    private array $data = [];
12    private int $lastId = 0;
13
14    public function generateId(): int
15    {
16        $this->lastId++;
17
18        return $this->lastId;
19    }
20
21    public function persist(array $data)
22    {
23        $this->data[$this->lastId] = $data;
24    }
25
26    public function retrieve(int $id): array
27    {
28        if (!isset($this->data[$id])) {
29            throw new OutOfBoundsException(sprintf('No data found for ID %d', $id));
30        }
31
32        return $this->data[$id];
33    }
34
35    public function delete(int $id)
36    {
37        if (!isset($this->data[$id])) {
38            throw new OutOfBoundsException(sprintf('No data found for ID %d', $id));
39        }
40
41        unset($this->data[$id]);
42    }
43}

4.2.5. Test

Tests/PostRepositoryTest.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\More\Repository\Tests;
 6
 7use OutOfBoundsException;
 8use DesignPatterns\More\Repository\Domain\PostId;
 9use DesignPatterns\More\Repository\Domain\PostStatus;
10use DesignPatterns\More\Repository\InMemoryPersistence;
11use DesignPatterns\More\Repository\Domain\Post;
12use DesignPatterns\More\Repository\PostRepository;
13use PHPUnit\Framework\TestCase;
14
15class PostRepositoryTest extends TestCase
16{
17    private PostRepository $repository;
18
19    protected function setUp(): void
20    {
21        $this->repository = new PostRepository(new InMemoryPersistence());
22    }
23
24    public function testCanGenerateId()
25    {
26        $this->assertEquals(1, $this->repository->generateId()->toInt());
27    }
28
29    public function testThrowsExceptionWhenTryingToFindPostWhichDoesNotExist()
30    {
31        $this->expectException(OutOfBoundsException::class);
32        $this->expectExceptionMessage('Post with id 42 does not exist');
33
34        $this->repository->findById(PostId::fromInt(42));
35    }
36
37    public function testCanPersistPostDraft()
38    {
39        $postId = $this->repository->generateId();
40        $post = Post::draft($postId, 'Repository Pattern', 'Design Patterns PHP');
41        $this->repository->save($post);
42
43        $this->repository->findById($postId);
44
45        $this->assertEquals($postId, $this->repository->findById($postId)->getId());
46        $this->assertEquals(PostStatus::STATE_DRAFT, $post->getStatus()->toString());
47    }
48}