1<?php
2
3declare(strict_types=1);
4
5namespace JMS\Serializer\Metadata;
6
7use JMS\Serializer\Exception\InvalidMetadataException;
8use JMS\Serializer\Ordering\AlphabeticalPropertyOrderingStrategy;
9use JMS\Serializer\Ordering\CustomPropertyOrderingStrategy;
10use JMS\Serializer\Ordering\IdenticalPropertyOrderingStrategy;
11use Metadata\MergeableClassMetadata;
12use Metadata\MergeableInterface;
13use Metadata\MethodMetadata;
14use Metadata\PropertyMetadata as BasePropertyMetadata;
15
16/**
17 * Class Metadata used to customize the serialization process.
18 *
19 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
20 */
21class ClassMetadata extends MergeableClassMetadata
22{
23    public const ACCESSOR_ORDER_UNDEFINED = 'undefined';
24    public const ACCESSOR_ORDER_ALPHABETICAL = 'alphabetical';
25    public const ACCESSOR_ORDER_CUSTOM = 'custom';
26
27    /** @var \ReflectionMethod[] */
28    public $preSerializeMethods = [];
29
30    /** @var \ReflectionMethod[] */
31    public $postSerializeMethods = [];
32
33    /** @var \ReflectionMethod[] */
34    public $postDeserializeMethods = [];
35
36    /**
37     * @var string
38     */
39    public $xmlRootName;
40
41    /**
42     * @var string
43     */
44    public $xmlRootNamespace;
45
46    /**
47     * @var string
48     */
49    public $xmlRootPrefix;
50    /**
51     * @var string[]
52     */
53    public $xmlNamespaces = [];
54
55    /**
56     * @var string
57     */
58    public $accessorOrder;
59
60    /**
61     * @var string[]
62     */
63    public $customOrder;
64
65    /**
66     * @internal
67     *
68     * @var bool
69     */
70    public $usingExpression = false;
71
72    /**
73     * @internal
74     *
75     * @var bool
76     */
77    public $isList = false;
78
79    /**
80     * @internal
81     *
82     * @var bool
83     */
84    public $isMap = false;
85
86    /**
87     * @var bool
88     */
89    public $discriminatorDisabled = false;
90
91    /**
92     * @var string
93     */
94    public $discriminatorBaseClass;
95    /**
96     * @var string
97     */
98    public $discriminatorFieldName;
99    /**
100     * @var string
101     */
102    public $discriminatorValue;
103
104    /**
105     * @var string[]
106     */
107    public $discriminatorMap = [];
108
109    /**
110     * @var string[]
111     */
112    public $discriminatorGroups = [];
113
114    /**
115     * @var bool
116     */
117    public $xmlDiscriminatorAttribute = false;
118
119    /**
120     * @var bool
121     */
122    public $xmlDiscriminatorCData = true;
123
124    /**
125     * @var string
126     */
127    public $xmlDiscriminatorNamespace;
128
129    public function setDiscriminator(string $fieldName, array $map, array $groups = []): void
130    {
131        if (empty($fieldName)) {
132            throw new InvalidMetadataException('The $fieldName cannot be empty.');
133        }
134
135        if (empty($map)) {
136            throw new InvalidMetadataException('The discriminator map cannot be empty.');
137        }
138
139        $this->discriminatorBaseClass = $this->name;
140        $this->discriminatorFieldName = $fieldName;
141        $this->discriminatorMap = $map;
142        $this->discriminatorGroups = $groups;
143
144        $this->handleDiscriminatorProperty();
145    }
146
147    private function getReflection(): \ReflectionClass
148    {
149        return new \ReflectionClass($this->name);
150    }
151
152    /**
153     * Sets the order of properties in the class.
154     *
155     * @param array $customOrder
156     *
157     * @throws InvalidMetadataException When the accessor order is not valid.
158     * @throws InvalidMetadataException When the custom order is not valid.
159     */
160    public function setAccessorOrder(string $order, array $customOrder = []): void
161    {
162        if (!in_array($order, [self::ACCESSOR_ORDER_UNDEFINED, self::ACCESSOR_ORDER_ALPHABETICAL, self::ACCESSOR_ORDER_CUSTOM], true)) {
163            throw new InvalidMetadataException(sprintf('The accessor order "%s" is invalid.', $order));
164        }
165
166        foreach ($customOrder as $name) {
167            if (!\is_string($name)) {
168                throw new InvalidMetadataException(sprintf('$customOrder is expected to be a list of strings, but got element of value %s.', json_encode($name)));
169            }
170        }
171
172        $this->accessorOrder = $order;
173        $this->customOrder = array_flip($customOrder);
174        $this->sortProperties();
175    }
176
177    public function addPropertyMetadata(BasePropertyMetadata $metadata): void
178    {
179        parent::addPropertyMetadata($metadata);
180        $this->sortProperties();
181        if ($metadata instanceof PropertyMetadata && $metadata->excludeIf) {
182            $this->usingExpression = true;
183        }
184    }
185
186    public function addPreSerializeMethod(MethodMetadata $method): void
187    {
188        $this->preSerializeMethods[] = $method;
189    }
190
191    public function addPostSerializeMethod(MethodMetadata $method): void
192    {
193        $this->postSerializeMethods[] = $method;
194    }
195
196    public function addPostDeserializeMethod(MethodMetadata $method): void
197    {
198        $this->postDeserializeMethods[] = $method;
199    }
200
201    public function merge(MergeableInterface $object): void
202    {
203        if (!$object instanceof ClassMetadata) {
204            throw new InvalidMetadataException('$object must be an instance of ClassMetadata.');
205        }
206        parent::merge($object);
207
208        $this->preSerializeMethods = array_merge($this->preSerializeMethods, $object->preSerializeMethods);
209        $this->postSerializeMethods = array_merge($this->postSerializeMethods, $object->postSerializeMethods);
210        $this->postDeserializeMethods = array_merge($this->postDeserializeMethods, $object->postDeserializeMethods);
211        $this->xmlRootName = $object->xmlRootName;
212        $this->xmlRootNamespace = $object->xmlRootNamespace;
213        $this->xmlNamespaces = array_merge($this->xmlNamespaces, $object->xmlNamespaces);
214
215        if ($object->accessorOrder) {
216            $this->accessorOrder = $object->accessorOrder;
217            $this->customOrder = $object->customOrder;
218        }
219
220        if ($object->discriminatorFieldName && $this->discriminatorFieldName) {
221            throw new InvalidMetadataException(sprintf(
222                'The discriminator of class "%s" would overwrite the discriminator of the parent class "%s". Please define all possible sub-classes in the discriminator of %s.',
223                $object->name,
224                $this->discriminatorBaseClass,
225                $this->discriminatorBaseClass
226            ));
227        } elseif (!$this->discriminatorFieldName && $object->discriminatorFieldName) {
228            $this->discriminatorFieldName = $object->discriminatorFieldName;
229            $this->discriminatorMap = $object->discriminatorMap;
230        }
231
232        if (null !== $object->discriminatorDisabled) {
233            $this->discriminatorDisabled = $object->discriminatorDisabled;
234        }
235
236        if ($object->discriminatorMap) {
237            $this->discriminatorFieldName = $object->discriminatorFieldName;
238            $this->discriminatorMap = $object->discriminatorMap;
239            $this->discriminatorBaseClass = $object->discriminatorBaseClass;
240        }
241
242        $this->handleDiscriminatorProperty();
243
244        $this->sortProperties();
245    }
246
247    public function registerNamespace(string $uri, ?string $prefix = null): void
248    {
249        if (!\is_string($uri)) {
250            throw new InvalidMetadataException(sprintf('$uri is expected to be a strings, but got value %s.', json_encode($uri)));
251        }
252
253        if (null !== $prefix) {
254            if (!\is_string($prefix)) {
255                throw new InvalidMetadataException(sprintf('$prefix is expected to be a strings, but got value %s.', json_encode($prefix)));
256            }
257        } else {
258            $prefix = '';
259        }
260
261        $this->xmlNamespaces[$prefix] = $uri;
262    }
263
264    /**
265     * @return string
266     *
267     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint
268     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingReturnTypeHint
269     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.UselessReturnAnnotation
270     */
271    public function serialize()
272    {
273        $this->sortProperties();
274
275        return serialize([
276            $this->preSerializeMethods,
277            $this->postSerializeMethods,
278            $this->postDeserializeMethods,
279            $this->xmlRootName,
280            $this->xmlRootNamespace,
281            $this->xmlNamespaces,
282            $this->accessorOrder,
283            $this->customOrder,
284            $this->discriminatorDisabled,
285            $this->discriminatorBaseClass,
286            $this->discriminatorFieldName,
287            $this->discriminatorValue,
288            $this->discriminatorMap,
289            $this->discriminatorGroups,
290            parent::serialize(),
291            'discriminatorGroups' => $this->discriminatorGroups,
292            'xmlDiscriminatorAttribute' => $this->xmlDiscriminatorAttribute,
293            'xmlDiscriminatorCData' => $this->xmlDiscriminatorCData,
294            'usingExpression' => $this->usingExpression,
295            'xmlDiscriminatorNamespace' => $this->xmlDiscriminatorNamespace,
296            'xmlRootPrefix' => $this->xmlRootPrefix,
297            'isList' => $this->isList,
298            'isMap' => $this->isMap,
299        ]);
300    }
301
302    /**
303     * @param string $str
304     *
305     * @return void
306     *
307     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint
308     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingReturnTypeHint
309     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.UselessReturnAnnotation
310     */
311    public function unserialize($str)
312    {
313        $unserialized = unserialize($str);
314
315        [
316            $this->preSerializeMethods,
317            $this->postSerializeMethods,
318            $this->postDeserializeMethods,
319            $this->xmlRootName,
320            $this->xmlRootNamespace,
321            $this->xmlNamespaces,
322            $this->accessorOrder,
323            $this->customOrder,
324            $this->discriminatorDisabled,
325            $this->discriminatorBaseClass,
326            $this->discriminatorFieldName,
327            $this->discriminatorValue,
328            $this->discriminatorMap,
329            $this->discriminatorGroups,
330            $parentStr,
331        ] = $unserialized;
332
333        if (isset($unserialized['discriminatorGroups'])) {
334            $this->discriminatorGroups = $unserialized['discriminatorGroups'];
335        }
336        if (isset($unserialized['usingExpression'])) {
337            $this->usingExpression = $unserialized['usingExpression'];
338        }
339
340        if (isset($unserialized['xmlDiscriminatorAttribute'])) {
341            $this->xmlDiscriminatorAttribute = $unserialized['xmlDiscriminatorAttribute'];
342        }
343
344        if (isset($unserialized['xmlDiscriminatorNamespace'])) {
345            $this->xmlDiscriminatorNamespace = $unserialized['xmlDiscriminatorNamespace'];
346        }
347
348        if (isset($unserialized['xmlDiscriminatorCData'])) {
349            $this->xmlDiscriminatorCData = $unserialized['xmlDiscriminatorCData'];
350        }
351
352        if (isset($unserialized['xmlRootPrefix'])) {
353            $this->xmlRootPrefix = $unserialized['xmlRootPrefix'];
354        }
355
356        if (isset($unserialized['isList'])) {
357            $this->isList = $unserialized['isList'];
358        }
359
360        if (isset($unserialized['isMap'])) {
361            $this->isMap = $unserialized['isMap'];
362        }
363
364        parent::unserialize($parentStr);
365    }
366
367    private function handleDiscriminatorProperty(): void
368    {
369        if ($this->discriminatorMap
370            && !$this->getReflection()->isAbstract()
371            && !$this->getReflection()->isInterface()
372        ) {
373            if (false === $typeValue = array_search($this->name, $this->discriminatorMap, true)) {
374                throw new InvalidMetadataException(sprintf(
375                    'The sub-class "%s" is not listed in the discriminator of the base class "%s".',
376                    $this->name,
377                    $this->discriminatorBaseClass
378                ));
379            }
380
381            $this->discriminatorValue = $typeValue;
382
383            if (isset($this->propertyMetadata[$this->discriminatorFieldName])
384                && !$this->propertyMetadata[$this->discriminatorFieldName] instanceof StaticPropertyMetadata
385            ) {
386                throw new InvalidMetadataException(sprintf(
387                    'The discriminator field name "%s" of the base-class "%s" conflicts with a regular property of the sub-class "%s".',
388                    $this->discriminatorFieldName,
389                    $this->discriminatorBaseClass,
390                    $this->name
391                ));
392            }
393
394            $discriminatorProperty = new StaticPropertyMetadata(
395                $this->name,
396                $this->discriminatorFieldName,
397                $typeValue,
398                $this->discriminatorGroups
399            );
400            $discriminatorProperty->serializedName = $this->discriminatorFieldName;
401            $discriminatorProperty->xmlAttribute = $this->xmlDiscriminatorAttribute;
402            $discriminatorProperty->xmlElementCData = $this->xmlDiscriminatorCData;
403            $discriminatorProperty->xmlNamespace = $this->xmlDiscriminatorNamespace;
404            $this->propertyMetadata[$this->discriminatorFieldName] = $discriminatorProperty;
405        }
406    }
407
408    private function sortProperties(): void
409    {
410        switch ($this->accessorOrder) {
411            case self::ACCESSOR_ORDER_UNDEFINED:
412                $this->propertyMetadata = (new IdenticalPropertyOrderingStrategy())->order($this->propertyMetadata);
413                break;
414
415            case self::ACCESSOR_ORDER_ALPHABETICAL:
416                $this->propertyMetadata = (new AlphabeticalPropertyOrderingStrategy())->order($this->propertyMetadata);
417                break;
418
419            case self::ACCESSOR_ORDER_CUSTOM:
420                $this->propertyMetadata = (new CustomPropertyOrderingStrategy($this->customOrder))->order($this->propertyMetadata);
421                break;
422        }
423    }
424}
425