仓库模式

目的

在领域对象和数据映射层之间引入一个中间层,使用类似集合的接口来访问领域对象。仓库封装了已持久化存储的对象集合,以及对它们要执行的操作,并提供了一个更加面向对象的持久层视图。仓库还实现了特定领域对象和数据映射层之间分离和单向依赖的目标。

使用场景

  • Doctrine 2 的 ORM:提供了介于实体对象和 DBAL 之间的中间层,同时包含获取这些对象的方法
  • Laravel 框架

UML 类图

仓库模式类图

代码

Post.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?php declare(strict_types = 1);

namespace DesignPatterns\Extend\Repository\Domain;

class Post
{
private PostId $id;
private PostStatus $status;
private string $title;
private string $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']
);
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php declare(strict_types = 1);

namespace DesignPatterns\Extend\Repository\Domain;

use InvalidArgumentException;

/**
* 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
{
private int $id;

public static function fromInt(int $id): PostId
{
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<?php declare(strict_types = 1);

namespace DesignPatterns\Extend\Repository\Domain;

use InvalidArgumentException;

/**
* 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 array $validStates = [
self::STATE_DRAFT_ID => self::STATE_DRAFT,
self::STATE_PUBLISHED_ID => self::STATE_PUBLISHED,
];

private int $id;
private string $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);
$state = array_search($status, self::$validStates);

if ($state === false) {
throw new InvalidArgumentException('Invalid state given!');
}

return new self($state, $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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php declare(strict_types = 1);

namespace DesignPatterns\Extend\Repository;

use OutOfBoundsException;
use DesignPatterns\Extend\Repository\Domain\Post;
use DesignPatterns\Extend\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
{
private Persistence $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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php declare(strict_types = 1);

namespace DesignPatterns\Extend\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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php declare(strict_types = 1);

namespace DesignPatterns\Extend\Repository;

use OutOfBoundsException;

class InMemoryPersistence implements Persistence
{
private array $data = [];
private int $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]);
}
}

测试

Tests/PostRepositoryTest.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php declare(strict_types = 1);

namespace DesignPatterns\Extend\Repository\Tests;

use OutOfBoundsException;
use DesignPatterns\Extend\Repository\Domain\PostId;
use DesignPatterns\Extend\Repository\Domain\PostStatus;
use DesignPatterns\Extend\Repository\InMemoryPersistence;
use DesignPatterns\Extend\Repository\Domain\Post;
use DesignPatterns\Extend\Repository\PostRepository;
use PHPUnit\Framework\TestCase;

class PostRepositoryTest extends TestCase
{
private PostRepository $repository;

protected function setUp(): void
{
$this->repository = new PostRepository(new InMemoryPersistence());
}

public function testCanGenerateId()
{
$this->assertEquals(1, $this->repository->generateId()->toInt());
}

public function testThrowsExceptionWhenTryingToFindPostWhichDoesNotExist()
{
$this->expectException(OutOfBoundsException::class);
$this->expectExceptionMessage('Post with id 42 does not exist');

$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());
}
}