Completed
Push — master ( cd78cf...a44f10 )
by Julián
05:26
created

JsonEventSerializer::serialize()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 31
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 17
nc 3
nop 1
dl 0
loc 31
rs 9.7
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * event-sourcing-async (https://github.com/phpgears/event-sourcing-async).
5
 * Async decorator for Event Sourcing events.
6
 *
7
 * @license MIT
8
 * @link https://github.com/phpgears/event-sourcing-async
9
 * @author Julián Gutiérrez <[email protected]>
10
 */
11
12
declare(strict_types=1);
13
14
namespace Gears\EventSourcing\Async\Serializer;
15
16
use Gears\Event\Async\Serializer\EventSerializer;
17
use Gears\Event\Async\Serializer\Exception\EventSerializationException;
18
use Gears\Event\Event;
19
use Gears\EventSourcing\Aggregate\AggregateVersion;
20
use Gears\EventSourcing\Event\AggregateEvent;
21
use Gears\Identity\Identity;
22
23
final class JsonEventSerializer implements EventSerializer
24
{
25
    /**
26
     * JSON encoding options.
27
     * Preserve float values and encode &, ', ", < and > characters in the resulting JSON.
28
     */
29
    private const JSON_ENCODE_OPTIONS = \JSON_UNESCAPED_UNICODE
30
        | \JSON_UNESCAPED_SLASHES
31
        | \JSON_PRESERVE_ZERO_FRACTION
32
        | \JSON_HEX_AMP
33
        | \JSON_HEX_APOS
34
        | \JSON_HEX_QUOT
35
        | \JSON_HEX_TAG;
36
37
    /**
38
     * JSON decoding options.
39
     * Decode large integers as string values.
40
     */
41
    private const JSON_DECODE_OPTIONS = \JSON_BIGINT_AS_STRING;
42
43
    /**
44
     * \DateTime::RFC3339_EXTENDED cannot handle microseconds on \DateTimeImmutable::createFromFormat.
45
     *
46
     * @see https://stackoverflow.com/a/48949373
47
     */
48
    private const DATE_RFC3339_EXTENDED = 'Y-m-d\TH:i:s.uP';
49
50
    /**
51
     * {@inheritdoc}
52
     */
53
    public function serialize(Event $event): string
54
    {
55
        if (!$event instanceof AggregateEvent) {
56
            throw new EventSerializationException(\sprintf(
57
                'Aggregate event class %s does not implement %s',
58
                \get_class($event),
59
                AggregateEvent::class
60
            ));
61
        }
62
63
        $serialized = \json_encode(
64
            [
65
                'class' => \get_class($event),
66
                'payload' => $event->getPayload(),
67
                'createdAt' => $event->getCreatedAt()->format(static::DATE_RFC3339_EXTENDED),
68
                'attributes' => $this->getSerializationAttributes($event),
69
            ],
70
            static::JSON_ENCODE_OPTIONS
71
        );
72
73
        // @codeCoverageIgnoreStart
74
        if ($serialized === false || \json_last_error() !== \JSON_ERROR_NONE) {
75
            throw new EventSerializationException(\sprintf(
76
                'Error serializing event %s due to %s',
77
                \get_class($event),
78
                \lcfirst(\json_last_error_msg())
79
            ));
80
        }
81
        // @codeCoverageIgnoreEnd
82
83
        return $serialized;
84
    }
85
86
    /**
87
     * Get serialization attributes.
88
     *
89
     * @param AggregateEvent $event
90
     *
91
     * @return array<string, mixed>
92
     */
93
    private function getSerializationAttributes(AggregateEvent $event): array
94
    {
95
        $aggregateId = $event->getAggregateId();
96
97
        return [
98
            'aggregateIdClass' => \get_class($aggregateId),
99
            'aggregateId' => $aggregateId->getValue(),
100
            'aggregateVersion' => $event->getAggregateVersion()->getValue(),
101
            'metadata' => $event->getMetadata(),
102
        ];
103
    }
104
105
    /**
106
     * {@inheritdoc}
107
     */
108
    public function fromSerialized(string $serialized): Event
109
    {
110
        ['class' => $eventClass, 'payload' => $payload, 'createdAt' => $createdAt, 'attributes' => $attributes] =
111
            $this->getEventDefinition($serialized);
112
113
        if (!\class_exists($eventClass)) {
114
            throw new EventSerializationException(\sprintf('Aggregate event class %s cannot be found', $eventClass));
115
        }
116
117
        if (!\in_array(AggregateEvent::class, \class_implements($eventClass), true)) {
118
            throw new EventSerializationException(\sprintf(
119
                'Aggregate event class must implement %s, %s given',
120
                AggregateEvent::class,
121
                $eventClass
122
            ));
123
        }
124
125
        $createdAt = \DateTimeImmutable::createFromFormat(self::DATE_RFC3339_EXTENDED, $createdAt);
126
127
        try {
128
            /* @var AggregateEvent $eventClass */
129
            return $eventClass::reconstitute($payload, $createdAt, $this->getDeserializationAttributes($attributes));
0 ignored issues
show
Bug introduced by
It seems like $createdAt can also be of type false; however, parameter $createdAt of Gears\Event\Event::reconstitute() does only seem to accept DateTimeImmutable, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

129
            return $eventClass::reconstitute($payload, /** @scrutinizer ignore-type */ $createdAt, $this->getDeserializationAttributes($attributes));
Loading history...
130
        } catch (\Exception $exception) {
131
            throw new EventSerializationException('Error reconstituting aggregate event', 0, $exception);
132
        }
133
    }
134
135
    /**
136
     * Get event definition from serialization.
137
     *
138
     * @param string $serialized
139
     *
140
     * @throws EventSerializationException
141
     *
142
     * @return array<string, mixed>
143
     */
144
    private function getEventDefinition(string $serialized): array
145
    {
146
        $definition = $this->getDeserializationDefinition($serialized);
147
148
        if (!isset($definition['class'], $definition['payload'], $definition['createdAt'], $definition['attributes'])
149
            || \count(\array_diff(\array_keys($definition), ['class', 'payload', 'createdAt', 'attributes'])) !== 0
150
            || !\is_string($definition['class'])
151
            || !\is_array($definition['payload'])
152
            || !\is_string($definition['createdAt'])
153
            || !\is_array($definition['attributes'])
154
        ) {
155
            throw new EventSerializationException('Malformed JSON serialized aggregate event');
156
        }
157
158
        return $definition;
159
    }
160
161
    /**
162
     * Get deserialization definition.
163
     *
164
     * @param string $serialized
165
     *
166
     * @return array<string, mixed>
167
     */
168
    private function getDeserializationDefinition(string $serialized): array
169
    {
170
        if (\trim($serialized) === '') {
171
            throw new EventSerializationException('Malformed JSON serialized aggregate event: empty string');
172
        }
173
174
        $definition = \json_decode($serialized, true, 512, static::JSON_DECODE_OPTIONS);
175
176
        // @codeCoverageIgnoreStart
177
        if ($definition === null || \json_last_error() !== \JSON_ERROR_NONE) {
178
            throw new EventSerializationException(\sprintf(
179
                'Event deserialization failed due to error %s: %s',
180
                \json_last_error(),
181
                \lcfirst(\json_last_error_msg())
182
            ));
183
        }
184
        // @codeCoverageIgnoreEnd
185
186
        return $definition;
187
    }
188
189
    /**
190
     * Get deserialization attributes.
191
     *
192
     * @param array<string, mixed> $attributes
193
     *
194
     * @return array<string, mixed>
195
     */
196
    private function getDeserializationAttributes(array $attributes): array
197
    {
198
        /* @var Identity $identityClass */
199
        $identityClass = $attributes['aggregateIdClass'] ?? null;
200
201
        if ($identityClass === null) {
202
            throw new EventSerializationException(
203
                'Malformed JSON serialized event: Aggregate event identity class is not defined'
204
            );
205
        }
206
207
        if (!\class_exists($identityClass)
208
            || !\in_array(Identity::class, \class_implements($identityClass), true)
209
        ) {
210
            throw new EventSerializationException(\sprintf(
211
                'Aggregate event identity class %s does not implement %s',
212
                $identityClass,
213
                Identity::class
214
            ));
215
        }
216
217
        return [
218
            'aggregateId' => $identityClass::fromString($attributes['aggregateId']),
219
            'aggregateVersion' => new AggregateVersion($attributes['aggregateVersion']),
220
            'metadata' => $attributes['metadata'],
221
        ];
222
    }
223
}
224