Completed
Push — v4.1 ( 7fde6d...3f8c67 )
by Masiukevich
07:23
created

SymfonyMessageSerializer::denormalize()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 2
dl 0
loc 15
ccs 6
cts 6
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Messages serializer implementation.
5
 *
6
 * @author  Maksim Masiukevich <[email protected]>
7
 * @license MIT
8
 * @license https://opensource.org/licenses/MIT
9
 */
10
11
declare(strict_types = 1);
12
13
namespace ServiceBus\MessageSerializer\Symfony;
14
15
use ServiceBus\MessageSerializer\Exceptions\DecodeMessageFailed;
16
use ServiceBus\MessageSerializer\Exceptions\DenormalizeFailed;
17
use ServiceBus\MessageSerializer\Exceptions\EncodeMessageFailed;
18
use ServiceBus\MessageSerializer\Exceptions\NormalizationFailed;
19
use ServiceBus\MessageSerializer\MessageDecoder;
20
use ServiceBus\MessageSerializer\MessageEncoder;
21
use ServiceBus\MessageSerializer\Symfony\Extensions\EmptyDataNormalizer;
22
use ServiceBus\MessageSerializer\Symfony\Extensions\PropertyNameConverter;
23
use ServiceBus\MessageSerializer\Symfony\Extensions\PropertyNormalizerWrapper;
24
use ServiceBus\MessageSerializer\Symfony\Extractor\CombinedExtractor;
25
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
26
use Symfony\Component\Serializer as SymfonySerializer;
27
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
28
use function ServiceBus\Common\jsonDecode;
29
use function ServiceBus\Common\jsonEncode;
30
31
/**
32
 *
33
 */
34
final class SymfonyMessageSerializer implements MessageEncoder, MessageDecoder
35
{
36
    /**
37
     * Symfony normalizer\denormalizer.
38
     *
39
     * @var SymfonySerializer\Serializer
40
     */
41
    private $normalizer;
42
43
    /**
44
     * @param SymfonySerializer\Normalizer\DenormalizerInterface[]|SymfonySerializer\Normalizer\NormalizerInterface[] $normalizers
45
     */
46 15
    public function __construct(array $normalizers = [])
47
    {
48 15
        $extractor = \PHP_VERSION_ID >= 70400 ? new CombinedExtractor() : new PhpDocExtractor();
49
50
        $defaultNormalizers = [
51 15
            new DateTimeNormalizer(['datetime_format' => 'c']),
52 15
            new SymfonySerializer\Normalizer\ArrayDenormalizer(),
53 15
            new PropertyNormalizerWrapper(null, new PropertyNameConverter(), $extractor),
54 15
            new EmptyDataNormalizer(),
55
        ];
56
57
        /** @psalm-var array<array-key, (\Symfony\Component\Serializer\Normalizer\NormalizerInterface|\Symfony\Component\Serializer\Normalizer\DenormalizerInterface)> $normalizers */
58 15
        $normalizers = \array_merge($normalizers, $defaultNormalizers);
59
60 15
        $this->normalizer = new SymfonySerializer\Serializer($normalizers);
61 15
    }
62
63
    /**
64
     * {@inheritdoc}
65
     */
66 9
    public function encode(object $message): string
67
    {
68
        try
69
        {
70 9
            $data = ['message' => $this->normalize($message), 'namespace' => \get_class($message)];
71
72 9
            return jsonEncode($data);
73
        }
74 1
        catch (\Throwable $throwable)
75
        {
76 1
            throw new EncodeMessageFailed(
77 1
                \sprintf('Message serialization failed: %s', $throwable->getMessage()),
78 1
                (int) $throwable->getCode(),
79
                $throwable
80
            );
81
        }
82
    }
83
84
    /**
85
     * {@inheritdoc}
86
     */
87 13
    public function decode(string $serializedMessage): object
88
    {
89
        try
90
        {
91 13
            $data = jsonDecode($serializedMessage);
92
93 13
            self::validateUnserializedData($data);
94
95
            /** @var object $object */
96 8
            $object = $this->denormalize($data['message'], $data['namespace']);
97
98 8
            return $object;
99
        }
100 5
        catch (\Throwable $throwable)
101
        {
102 5
            throw new DecodeMessageFailed(
103 5
                \sprintf('Message deserialization failed: %s', $throwable->getMessage()),
104 5
                (int) $throwable->getCode(),
105
                $throwable
106
            );
107
        }
108
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113 9
    public function denormalize(array $payload, string $class): object
114
    {
115
        try
116
        {
117
            /** @var object $object */
118 9
            $object = $this->normalizer->denormalize(
119 9
                $payload,
120
                $class
121
            );
122
123 8
            return $object;
124
        }
125 1
        catch (\Throwable $throwable)
126
        {
127 1
            throw new DenormalizeFailed($throwable->getMessage(), (int) $throwable->getCode(), $throwable);
128
        }
129
    }
130
131
    /**
132
     * {@inheritdoc}
133
     */
134 9
    public function normalize(object $message): array
135
    {
136
        try
137
        {
138 9
            $data = $this->normalizer->normalize($message);
139
140 9
            if (\is_array($data) === true)
141
            {
142
                /** @psalm-var array<string, mixed> $data */
143
144 9
                return $data;
145
            }
146
147
            // @codeCoverageIgnoreStart
148
            throw new \UnexpectedValueException(
149
                \sprintf(
150
                    'The normalization was to return the array. Type "%s" was obtained when object "%s" was normalized',
151
                    \gettype($data),
152
                    \get_class($message)
153
                )
154
            );
155
            // @codeCoverageIgnoreEnd
156
        }
157
        catch (\Throwable $throwable)
158
        {
159
            throw new NormalizationFailed($throwable->getMessage(), (int) $throwable->getCode(), $throwable);
160
        }
161
    }
162
163
    /**
164
     * @psalm-assert array{message:array<string, string|int|float|array|null>, namespace:class-string} $data
165
     *
166
     * @throws \UnexpectedValueException
167
     */
168 13
    private static function validateUnserializedData(array $data): void
169
    {
170
        /** Let's check if there are mandatory fields */
171
        if (
172 13
            isset($data['namespace']) === false ||
173 13
            isset($data['message']) === false
174
        ) {
175 2
            throw new \UnexpectedValueException(
176 2
                'The serialized data must contains a "namespace" field (indicates the message class) and "message" (indicates the message parameters)'
177
            );
178
        }
179
180 11
        if (\is_array($data['message']) === false)
181
        {
182 1
            throw new \UnexpectedValueException('"message" field from serialized data should be an array');
183
        }
184
185 10
        if (\is_string($data['namespace']) === false)
186
        {
187 1
            throw new \UnexpectedValueException('"namespace" field from serialized data should be a string');
188
        }
189
190
        /**
191
         * Let's check if the specified class exists.
192
         *
193
         * @psalm-suppress DocblockTypeContradiction
194
         */
195 9
        if ($data['namespace'] === '' || \class_exists($data['namespace']) === false)
196
        {
197 1
            throw new \UnexpectedValueException(
198 1
                \sprintf('Class "%s" not found', $data['namespace'])
199
            );
200
        }
201 8
    }
202
}
203