Passed
Push — master ( cbefbc...3619f4 )
by Frank
16:49 queued 06:49
created

src/EventSourcedAggregateRootRepository.php (1 issue)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace EventSauce\EventSourcing;
6
7
use Generator;
8
use Throwable;
9
use function assert;
10
use function count;
11
12
/**
13
 * @template T of AggregateRoot
14
 *
15
 * @template-implements AggregateRootRepository<T>
16
 */
17
class EventSourcedAggregateRootRepository implements AggregateRootRepository
18
{
19
    /** @var class-string<T> */
20
    private string $aggregateRootClassName;
21
    private MessageRepository $messages;
22
    private MessageDecorator $decorator;
23
    private MessageDispatcher $dispatcher;
24
25
    /**
26
     * @param class-string<T> $aggregateRootClassName
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
27
     */
28
    public function __construct(
29
        string $aggregateRootClassName,
30
        MessageRepository $messageRepository,
31
        MessageDispatcher $dispatcher = null,
32
        MessageDecorator $decorator = null
33
    ) {
34
        $this->aggregateRootClassName = $aggregateRootClassName;
35
        $this->messages = $messageRepository;
36
        $this->dispatcher = $dispatcher ?: new SynchronousMessageDispatcher();
37
        $this->decorator = $decorator ?: new DefaultHeadersDecorator();
38
    }
39
40
    public function retrieve(AggregateRootId $aggregateRootId): object
41
    {
42
        try {
43
            /** @var AggregateRoot $className */
44
            /** @phpstan-var class-string<T> $className */
45
            $className = $this->aggregateRootClassName;
46
            $events = $this->retrieveAllEvents($aggregateRootId);
47
48
            return $className::reconstituteFromEvents($aggregateRootId, $events);
49
        } catch (Throwable $throwable) {
50
            throw UnableToReconstituteAggregateRoot::becauseOf($throwable->getMessage(), $throwable);
51
        }
52
    }
53
54
    private function retrieveAllEvents(AggregateRootId $aggregateRootId): Generator
55
    {
56
        /** @var Generator<Message> $messages */
57
        $messages = $this->messages->retrieveAll($aggregateRootId);
58
59
        foreach ($messages as $message) {
60
            yield $message->event();
61
        }
62
63
        return $messages->getReturn();
64
    }
65
66
    public function persist(object $aggregateRoot): void
67
    {
68
        assert($aggregateRoot instanceof AggregateRoot, 'Expected $aggregateRoot to be an instance of ' . AggregateRoot::class);
69
70
        $this->persistEvents(
71
            $aggregateRoot->aggregateRootId(),
72
            $aggregateRoot->aggregateRootVersion(),
73
            ...$aggregateRoot->releaseEvents()
74
        );
75
    }
76
77
    public function persistEvents(AggregateRootId $aggregateRootId, int $aggregateRootVersion, object ...$events): void
78
    {
79
        if (0 === count($events)) {
80
            return;
81
        }
82
83
        // decrease the aggregate root version by the number of raised events
84
        // so the version of each message represents the version at the time
85
        // of recording.
86
        $aggregateRootVersion = $aggregateRootVersion - count($events);
87
        $metadata = [Header::AGGREGATE_ROOT_ID => $aggregateRootId];
88
        $messages = array_map(fn (object $event) => $this->decorator->decorate(new Message(
89
            $event,
90
            $metadata + [Header::AGGREGATE_ROOT_VERSION => ++$aggregateRootVersion]
91
        )), $events);
92
93
        $this->messages->persist(...$messages);
94
        $this->dispatcher->dispatch(...$messages);
95
    }
96
}
97