Issues (1)

src/Serializer/JsonEventSerializer.php (1 issue)

Labels
Severity
1
<?php
2
3
/*
4
 * event-async (https://github.com/phpgears/event-async).
5
 * Async decorator for Event bus.
6
 *
7
 * @license MIT
8
 * @link https://github.com/phpgears/event-async
9
 * @author Julián Gutiérrez <[email protected]>
10
 */
11
12
declare(strict_types=1);
13
14
namespace Gears\Event\Async\Serializer;
15
16
use Gears\Event\Async\Serializer\Exception\EventSerializationException;
17
use Gears\Event\Event;
18
19
final class JsonEventSerializer implements EventSerializer
20
{
21
    /**
22
     * JSON encoding options.
23
     * Preserve float values and encode &, ', ", < and > characters in the resulting JSON.
24
     */
25
    private const JSON_ENCODE_OPTIONS = \JSON_UNESCAPED_UNICODE
26
        | \JSON_UNESCAPED_SLASHES
27
        | \JSON_PRESERVE_ZERO_FRACTION
28
        | \JSON_HEX_AMP
29
        | \JSON_HEX_APOS
30
        | \JSON_HEX_QUOT
31
        | \JSON_HEX_TAG;
32
33
    /**
34
     * JSON decoding options.
35
     * Decode large integers as string values.
36
     */
37
    private const JSON_DECODE_OPTIONS = \JSON_BIGINT_AS_STRING;
38
39
    /**
40
     * \DateTime::RFC3339_EXTENDED cannot handle microseconds on \DateTimeImmutable::createFromFormat.
41
     *
42
     * @see https://stackoverflow.com/a/48949373
43
     */
44
    private const DATE_RFC3339_EXTENDED = 'Y-m-d\TH:i:s.uP';
45
46
    /**
47
     * {@inheritdoc}
48
     */
49
    public function serialize(Event $event): string
50
    {
51
        $serialized = \json_encode(
52
            [
53
                'class' => \get_class($event),
54
                'payload' => $event->getPayload(),
55
                'createdAt' => $event->getCreatedAt()->format(static::DATE_RFC3339_EXTENDED),
56
                'attributes' => $this->getSerializationAttributes($event),
57
            ],
58
            static::JSON_ENCODE_OPTIONS
59
        );
60
61
        // @codeCoverageIgnoreStart
62
        if ($serialized === false || \json_last_error() !== \JSON_ERROR_NONE) {
63
            throw new EventSerializationException(\sprintf(
64
                'Error serializing event %s due to %s',
65
                \get_class($event),
66
                \lcfirst(\json_last_error_msg())
67
            ));
68
        }
69
        // @codeCoverageIgnoreEnd
70
71
        return $serialized;
72
    }
73
74
    /**
75
     * Get serialization attributes.
76
     *
77
     * @param Event $event
78
     *
79
     * @return array<string, mixed>
80
     */
81
    private function getSerializationAttributes(Event $event): array
82
    {
83
        return [
84
            'metadata' => $event->getMetadata(),
85
        ];
86
    }
87
88
    /**
89
     * {@inheritdoc}
90
     */
91
    public function fromSerialized(string $serialized): Event
92
    {
93
        ['class' => $eventClass, 'payload' => $payload, 'createdAt' => $createdAt, 'attributes' => $attributes] =
94
            $this->getEventDefinition($serialized);
95
96
        if (!\class_exists($eventClass)) {
97
            throw new EventSerializationException(\sprintf('Event class %s cannot be found', $eventClass));
98
        }
99
100
        if (!\in_array(Event::class, \class_implements($eventClass), true)) {
101
            throw new EventSerializationException(\sprintf(
102
                'Event class must implement %s, %s given',
103
                Event::class,
104
                $eventClass
105
            ));
106
        }
107
108
        $createdAt = \DateTimeImmutable::createFromFormat(self::DATE_RFC3339_EXTENDED, $createdAt);
109
110
        // @codeCoverageIgnoreStart
111
        try {
112
            /* @var Event $eventClass */
113
            return $eventClass::reconstitute($payload, $createdAt, $this->getDeserializationAttributes($attributes));
0 ignored issues
show
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

113
            return $eventClass::reconstitute($payload, /** @scrutinizer ignore-type */ $createdAt, $this->getDeserializationAttributes($attributes));
Loading history...
114
        } catch (\Exception $exception) {
115
            throw new EventSerializationException('Error reconstituting event', 0, $exception);
116
        }
117
        // @codeCoverageIgnoreEnd
118
    }
119
120
    /**
121
     * Get event definition from serialization.
122
     *
123
     * @param string $serialized
124
     *
125
     * @throws EventSerializationException
126
     *
127
     * @return array<string, mixed>
128
     */
129
    private function getEventDefinition(string $serialized): array
130
    {
131
        $definition = $this->getDeserializationDefinition($serialized);
132
133
        if (!isset($definition['class'], $definition['payload'], $definition['createdAt'], $definition['attributes'])
134
            || \count(\array_diff(\array_keys($definition), ['class', 'payload', 'createdAt', 'attributes'])) !== 0
135
            || !\is_string($definition['class'])
136
            || !\is_array($definition['payload'])
137
            || !\is_string($definition['createdAt'])
138
            || !\is_array($definition['attributes'])
139
        ) {
140
            throw new EventSerializationException('Malformed JSON serialized event');
141
        }
142
143
        return $definition;
144
    }
145
146
    /**
147
     * Get deserialization definition.
148
     *
149
     * @param string $serialized
150
     *
151
     * @return array<string, mixed>
152
     */
153
    private function getDeserializationDefinition(string $serialized): array
154
    {
155
        if (\trim($serialized) === '') {
156
            throw new EventSerializationException('Malformed JSON serialized event: empty string');
157
        }
158
159
        $definition = \json_decode($serialized, true, 512, static::JSON_DECODE_OPTIONS);
160
161
        // @codeCoverageIgnoreStart
162
        if ($definition === null || \json_last_error() !== \JSON_ERROR_NONE) {
163
            throw new EventSerializationException(\sprintf(
164
                'Event deserialization failed due to error %s: %s',
165
                \json_last_error(),
166
                \lcfirst(\json_last_error_msg())
167
            ));
168
        }
169
        // @codeCoverageIgnoreEnd
170
171
        return $definition;
172
    }
173
174
    /**
175
     * Get deserialization attributes.
176
     *
177
     * @param array<string, mixed> $attributes
178
     *
179
     * @return array<string, mixed>
180
     */
181
    private function getDeserializationAttributes(array $attributes): array
182
    {
183
        return [
184
            'metadata' => $attributes['metadata'],
185
        ];
186
    }
187
}
188