Completed
Push — master ( 67ee93...84505b )
by Ivannis Suárez
07:55
created

Projector::onPostPersist()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 35
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 35
rs 8.439
c 0
b 0
f 0
cc 5
eloc 16
nc 5
nop 1
1
<?php
2
3
/**
4
 * This file is part of the Cubiche package.
5
 *
6
 * Copyright (c) Cubiche
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Cubiche\Domain\EventSourcing\Projector;
13
14
use Cubiche\Core\Cqrs\ReadModelInterface;
15
use Cubiche\Domain\EventPublisher\DomainEventSubscriberInterface;
16
use Cubiche\Domain\EventSourcing\Event\PostPersistEvent;
17
use Cubiche\Domain\EventSourcing\EventStore\EventStoreInterface;
18
use Cubiche\Domain\EventSourcing\EventStore\EventStream;
19
use Cubiche\Domain\EventSourcing\Versioning\VersionManager;
20
use Cubiche\Domain\Model\IdInterface;
21
use Cubiche\Domain\Repository\QueryRepositoryInterface;
22
23
/**
24
 * Projector class.
25
 *
26
 * @author Ivannis Suárez Jerez <[email protected]>
27
 */
28
abstract class Projector implements DomainEventSubscriberInterface
29
{
30
    /**
31
     * @var QueryRepositoryInterface
32
     */
33
    protected $repository;
34
35
    /**
36
     * @var EventStoreInterface
37
     */
38
    protected $eventStore;
39
40
    /**
41
     * Projector constructor.
42
     *
43
     * @param QueryRepositoryInterface $repository
44
     * @param EventStoreInterface      $eventStore
45
     */
46
    public function __construct(QueryRepositoryInterface $repository, EventStoreInterface $eventStore)
47
    {
48
        $this->repository = $repository;
49
        $this->eventStore = $eventStore;
50
    }
51
52
    /**
53
     * @param PostPersistEvent $event
54
     */
55
    public function onPostPersist(PostPersistEvent $event)
56
    {
57
        // skip if the aggregate is not my write model class
58
        if (is_a($event->aggregate(), $this->writeModelClass())) {
59
            /** @var ReadModelInterface $readModel */
60
            $readModel = $this->repository->get($event->aggregate()->id());
61
            $eventStream = $event->eventStream();
62
63
            // there is a projected read model?
64
            if ($readModel !== null) {
65
                // something change and has to be removed?
66
                if ($this->shouldBeRemoved($eventStream)) {
67
                    // remove it
68
                    $this->remove($readModel);
69
70
                    return;
71
                }
72
            } else {
73
                // the write model should be projected?
74
                if (!$this->shouldBeProjected($eventStream)) {
75
                    return;
76
                }
77
78
                // we have a write model that has never been projected
79
                // so, we need the complete stream history
80
                $eventStream = $this->loadHistory(
81
                    $event->aggregate()->id(),
82
                    $eventStream->streamName(),
83
                    $event->aggregateClassName()
84
                );
85
            }
86
87
            $this->projectAndPersistEvents($eventStream, $readModel);
88
        }
89
    }
90
91
    /**
92
     * @param EventStream        $eventStream
93
     * @param ReadModelInterface $readModel
94
     */
95
    protected function projectAndPersistEvents(EventStream $eventStream, ReadModelInterface $readModel = null)
96
    {
97
        // get the read model with the new changes
98
        $readModel = $this->projectEventStream($eventStream, $readModel);
99
100
        // persist the read model
101
        $this->persist($readModel);
0 ignored issues
show
Bug introduced by
It seems like $readModel defined by $this->projectEventStrea...ventStream, $readModel) on line 98 can be null; however, Cubiche\Domain\EventSour...or\Projector::persist() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
102
    }
103
104
    /**
105
     * Projects all the events into the read model.
106
     *
107
     * @param EventStream        $eventStream
108
     * @param ReadModelInterface $readModel
109
     *
110
     * @return ReadModelInterface|null
111
     */
112
    protected function projectEventStream(EventStream $eventStream, ReadModelInterface $readModel = null)
113
    {
114
        foreach ($eventStream->events() as $event) {
115
            $classParts = explode('\\', get_class($event));
116
            $method = 'project'.end($classParts);
117
118
            if (method_exists($this, $method)) {
119
                $result = $this->$method($event, $readModel);
120
                if ($result !== null) {
121
                    $readModel = $result;
122
                }
123
            }
124
        }
125
126
        return $readModel;
127
    }
128
129
    /**
130
     * @param ReadModelInterface $readModel
131
     */
132
    protected function persist(ReadModelInterface $readModel)
133
    {
134
        $this->repository->persist($readModel);
135
    }
136
137
    /**
138
     * @param ReadModelInterface $readModel
139
     */
140
    protected function remove(ReadModelInterface $readModel)
141
    {
142
        $this->repository->remove($readModel);
143
    }
144
145
    /**
146
     * Load a aggregate history from the storage.
147
     *
148
     * @param IdInterface $id
149
     * @param string      $streamName
150
     * @param string      $aggregateClassName
151
     *
152
     * @return EventStream
153
     */
154
    protected function loadHistory(IdInterface $id, $streamName, $aggregateClassName)
155
    {
156
        $applicationVersion = VersionManager::currentApplicationVersion();
157
        $aggregateVersion = VersionManager::versionOfClass($aggregateClassName, $applicationVersion);
158
159
        return $this->eventStore->load($streamName, $id, $aggregateVersion, $applicationVersion);
160
    }
161
162
    /**
163
     * @return string
164
     */
165
    abstract protected function writeModelClass();
166
167
    /**
168
     * @param EventStream $eventStream
169
     *
170
     * @return bool
171
     */
172
    abstract protected function shouldBeProjected(EventStream $eventStream);
173
174
    /**
175
     * @param EventStream $eventStream
176
     *
177
     * @return bool
178
     */
179
    abstract protected function shouldBeRemoved(EventStream $eventStream);
180
181
    /**
182
     * {@inheritdoc}
183
     */
184
    public static function getSubscribedEvents()
185
    {
186
        return array(
187
            PostPersistEvent::class => array('onPostPersist', 250),
188
        );
189
    }
190
}
191