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

Alt Repository UML Diagram

4.2.4. Code

You can also find this code on GitHub

Post.php

  1. <?php
  2.  
  3. namespace DesignPatterns\More\Repository\Domain;
  4.  
  5. class Post
  6. {
  7. /**
  8. * @var PostId
  9. */
  10. private $id;
  11.  
  12. /**
  13. * @var PostStatus
  14. */
  15. private $status;
  16.  
  17. /**
  18. * @var string
  19. */
  20. private $title;
  21.  
  22. /**
  23. * @var string
  24. */
  25. private $text;
  26.  
  27. public static function draft(PostId $id, string $title, string $text): Post
  28. {
  29. return new self(
  30. $id,
  31. PostStatus::fromString(PostStatus::STATE_DRAFT),
  32. $title,
  33. $text
  34. );
  35. }
  36.  
  37. public static function fromState(array $state): Post
  38. {
  39. return new self(
  40. PostId::fromInt($state['id']),
  41. PostStatus::fromInt($state['statusId']),
  42. $state['title'],
  43. $state['text']
  44. );
  45. }
  46.  
  47. /**
  48. * @param PostId $id
  49. * @param PostStatus $status
  50. * @param string $title
  51. * @param string $text
  52. */
  53. private function __construct(PostId $id, PostStatus $status, string $title, string $text)
  54. {
  55. $this->id = $id;
  56. $this->status = $status;
  57. $this->text = $text;
  58. $this->title = $title;
  59. }
  60.  
  61. public function getId(): PostId
  62. {
  63. return $this->id;
  64. }
  65.  
  66. public function getStatus(): PostStatus
  67. {
  68. return $this->status;
  69. }
  70.  
  71. public function getText(): string
  72. {
  73. return $this->text;
  74. }
  75.  
  76. public function getTitle(): string
  77. {
  78. return $this->title;
  79. }
  80. }

PostId.php

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

PostStatus.php

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

PostRepository.php

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

Persistence.php

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

InMemoryPersistence.php

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

4.2.5. Test

Tests/PostRepositoryTest.php

  1. <?php
  2.  
  3. namespace DesignPatterns\More\Repository\Tests;
  4.  
  5. use DesignPatterns\More\Repository\Domain\PostId;
  6. use DesignPatterns\More\Repository\Domain\PostStatus;
  7. use DesignPatterns\More\Repository\InMemoryPersistence;
  8. use DesignPatterns\More\Repository\Domain\Post;
  9. use DesignPatterns\More\Repository\PostRepository;
  10. use PHPUnit\Framework\TestCase;
  11.  
  12. class PostRepositoryTest extends TestCase
  13. {
  14. /**
  15. * @var PostRepository
  16. */
  17. private $repository;
  18.  
  19. protected function setUp()
  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. /**
  30. * @expectedException \OutOfBoundsException
  31. * @expectedExceptionMessage Post with id 42 does not exist
  32. */
  33. public function testThrowsExceptionWhenTryingToFindPostWhichDoesNotExist()
  34. {
  35. $this->repository->findById(PostId::fromInt(42));
  36. }
  37.  
  38. public function testCanPersistPostDraft()
  39. {
  40. $postId = $this->repository->generateId();
  41. $post = Post::draft($postId, 'Repository Pattern', 'Design Patterns PHP');
  42. $this->repository->save($post);
  43.  
  44. $this->repository->findById($postId);
  45.  
  46. $this->assertEquals($postId, $this->repository->findById($postId)->getId());
  47. $this->assertEquals(PostStatus::STATE_DRAFT, $post->getStatus()->toString());
  48. }
  49. }