AbstractAggregateRoot::__serialize()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
/*
4
 * event-sourcing (https://github.com/phpgears/event-sourcing).
5
 * Event Sourcing base.
6
 *
7
 * @license MIT
8
 * @link https://github.com/phpgears/event-sourcing
9
 * @author Julián Gutiérrez <[email protected]>
10
 */
11
12
declare(strict_types=1);
13
14
namespace Gears\EventSourcing\Aggregate;
15
16
use Gears\Aggregate\EventBehaviour;
17
use Gears\EventSourcing\Aggregate\Exception\AggregateException;
18
use Gears\EventSourcing\Aggregate\Exception\AggregateVersionException;
19
use Gears\EventSourcing\Aggregate\Serializer\Exception\AggregateSerializationException;
20
use Gears\EventSourcing\Event\AggregateEvent;
21
use Gears\EventSourcing\Event\AggregateEventIteratorStream;
22
use Gears\EventSourcing\Event\AggregateEventStream;
23
use Gears\Identity\Identity;
24
25
/**
26
 * Abstract aggregate root class.
27
 *
28
 * @SuppressWarnings(PHPMD.LongVariable)
29
 */
30
abstract class AbstractAggregateRoot implements AggregateRoot
31
{
32
    use AggregateBehaviour, EventBehaviour;
33
34
    /**
35
     * @var \ArrayObject<string, AggregateEvent>|null
36
     */
37
    private $recordedAggregateEvents;
38
39
    /**
40
     * Prevent aggregate root direct instantiation.
41
     */
42
    final protected function __construct()
43
    {
44
        $this->version = new AggregateVersion(0);
45
    }
46
47
    /**
48
     * Set aggregate identity.
49
     *
50
     * @param Identity $identity
51
     */
52
    final protected function setIdentity(Identity $identity): void
53
    {
54
        $this->identity = $identity;
55
    }
56
57
    /**
58
     * {@inheritdoc}
59
     */
60
    final public static function reconstituteFromEventStream(AggregateEventStream $eventStream): self
61
    {
62
        $instance = new static();
63
        $instance->replayAggregateEventStream($eventStream);
64
65
        if ($instance->getVersion()->isEqualTo(new AggregateVersion(0))) {
66
            throw new AggregateException('Aggregate cannot be reconstituted from empty event stream');
67
        }
68
69
        return $instance;
70
    }
71
72
    /**
73
     * {@inheritdoc}
74
     *
75
     * @throws AggregateVersionException
76
     */
77
    final public function replayAggregateEventStream(AggregateEventStream $eventStream): void
78
    {
79
        foreach ($eventStream as $event) {
80
            $aggregateVersion = $event->getAggregateVersion();
81
82
            if (!$aggregateVersion->isEqualTo($this->version->getNext())) {
83
                throw new AggregateVersionException(\sprintf(
84
                    'Aggregate event "%s" cannot be replayed, event version is "%s" and aggregate is "%s"',
85
                    \get_class($event),
86
                    $aggregateVersion->getValue(),
87
                    $this->version->getValue()
88
                ));
89
            }
90
91
            $this->applyAggregateEvent($event);
92
93
            $this->version = $aggregateVersion;
94
        }
95
    }
96
97
    /**
98
     * Record aggregate event.
99
     *
100
     * @param AggregateEvent $event
101
     *
102
     * @throws AggregateVersionException
103
     */
104
    final protected function recordAggregateEvent(AggregateEvent $event): void
105
    {
106
        if (!$event->getAggregateVersion()->isEqualTo(new AggregateVersion(0))) {
107
            throw new AggregateVersionException(\sprintf(
108
                'Only new aggregate events can be recorded, event "%s" with version "%s" given',
109
                \get_class($event),
110
                $event->getAggregateVersion()->getValue()
111
            ));
112
        }
113
114
        $this->applyAggregateEvent($event);
115
116
        $this->version = $this->version->getNext();
117
118
        /** @var AggregateEvent $recordedEvent */
119
        $recordedEvent = $event::reconstitute(
120
            $event->getPayload(),
121
            $event->getCreatedAt(),
122
            [
123
                'aggregateId' => $event->getAggregateId(),
124
                'aggregateVersion' => $this->version,
125
                'metadata' => $event->getMetadata(),
126
            ]
127
        );
128
129
        if ($this->recordedAggregateEvents === null) {
130
            $this->recordedAggregateEvents = new \ArrayObject();
131
        }
132
133
        $this->recordedAggregateEvents->append($recordedEvent);
134
    }
135
136
    /**
137
     * Apply aggregate event.
138
     *
139
     * @param AggregateEvent $event
140
     *
141
     * @throws AggregateException
142
     */
143
    final protected function applyAggregateEvent(AggregateEvent $event): void
144
    {
145
        $method = $this->getAggregateEventApplyMethodName($event);
146
147
        if (!\method_exists($this, $method)) {
148
            throw new AggregateException(\sprintf(
149
                'Aggregate event handling method "%s" for event "%s" does not exist',
150
                $method,
151
                \get_class($event)
152
            ));
153
        }
154
155
        /** @var callable $callable */
156
        $callable = [$this, $method];
157
158
        \call_user_func($callable, $event);
159
    }
160
161
    /**
162
     * Get event apply method name.
163
     *
164
     * @param AggregateEvent $event
165
     *
166
     * @return string
167
     */
168
    protected function getAggregateEventApplyMethodName(AggregateEvent $event): string
169
    {
170
        $typeParts = \explode('\\', $event->getEventType());
171
        /** @var string $eventType */
172
        $eventType = \end($typeParts);
173
174
        return 'apply' . \str_replace(' ', '', \ucwords(\strtr($eventType, '_-', '  ')));
175
    }
176
177
    /**
178
     * {@inheritdoc}
179
     */
180
    final public function getRecordedAggregateEvents(): AggregateEventStream
181
    {
182
        return new AggregateEventIteratorStream(
183
            $this->recordedAggregateEvents !== null
184
                ? $this->recordedAggregateEvents->getIterator()
185
                : new \EmptyIterator()
186
        );
187
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192
    final public function clearRecordedAggregateEvents(): void
193
    {
194
        $this->recordedAggregateEvents = null;
195
    }
196
197
    /**
198
     * {@inheritdoc}
199
     */
200
    final public function collectRecordedAggregateEvents(): AggregateEventStream
201
    {
202
        $recordedEvents = new AggregateEventIteratorStream(
203
            $this->recordedAggregateEvents !== null
204
                ? $this->recordedAggregateEvents->getIterator()
205
                : new \EmptyIterator()
206
        );
207
208
        $this->recordedAggregateEvents = null;
209
210
        return $recordedEvents;
211
    }
212
213
    /**
214
     * @return array<string, mixed>
215
     */
216
    final public function __serialize(): array
217
    {
218
        return $this->getSerializationAttributes();
219
    }
220
221
    /**
222
     * @param array<string, mixed> $data
223
     */
224
    final public function __unserialize(array $data): void
225
    {
226
        $this->unserializeAttributes($data);
227
    }
228
229
    /**
230
     * {@inheritdoc}
231
     */
232
    final public function serialize(): string
233
    {
234
        return \serialize($this->getSerializationAttributes());
235
    }
236
237
    /**
238
     * {@inheritdoc}
239
     *
240
     * @param mixed $serialized
241
     */
242
    final public function unserialize($serialized): void
243
    {
244
        $this->unserializeAttributes(\unserialize($serialized));
245
    }
246
247
    /**
248
     * Get serialization data.
249
     *
250
     * @throws AggregateSerializationException
251
     *
252
     * @return array<string, mixed>
253
     */
254
    private function getSerializationAttributes(): array
255
    {
256
        if (($this->recordedAggregateEvents !== null && $this->recordedAggregateEvents->count() !== 0)
257
            || ($this->recordedEvents !== null && $this->recordedEvents->count() !== 0)
258
        ) {
259
            throw new AggregateSerializationException('Aggregate root with recorded events cannot be serialized');
260
        }
261
262
        $attributes = [];
263
        foreach ((new \ReflectionObject($this))->getProperties() as $reflectionProperty) {
264
            if (!$reflectionProperty->isStatic()) {
265
                $reflectionProperty->setAccessible(true);
266
                $attributes[$reflectionProperty->getName()] = $reflectionProperty->getValue($this);
267
            }
268
        }
269
270
        $attributes['identity'] = $this->identity;
271
        $attributes['version'] = $this->version;
272
273
        return $attributes;
274
    }
275
276
    /**
277
     * Unserialize attributes.
278
     *
279
     * @param array<string, mixed> $attributes
280
     */
281
    private function unserializeAttributes(array $attributes): void
282
    {
283
        foreach ($attributes as $attribute => $value) {
284
            if (!\in_array($attribute, ['recordedAggregateEvents', 'recordedEvents'], true)) {
285
                $this->{$attribute} = $value;
286
            }
287
        }
288
    }
289
}
290