1<?php
2
3declare(strict_types=1);
4
5namespace JMS\Serializer\Metadata\Driver;
6
7use JMS\Serializer\Annotation\ExclusionPolicy;
8use JMS\Serializer\Exception\InvalidMetadataException;
9use JMS\Serializer\Expression\CompilableExpressionEvaluatorInterface;
10use JMS\Serializer\Metadata\ClassMetadata;
11use JMS\Serializer\Metadata\ExpressionPropertyMetadata;
12use JMS\Serializer\Metadata\PropertyMetadata;
13use JMS\Serializer\Metadata\VirtualPropertyMetadata;
14use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
15use JMS\Serializer\Type\Parser;
16use JMS\Serializer\Type\ParserInterface;
17use Metadata\ClassMetadata as BaseClassMetadata;
18use Metadata\Driver\AbstractFileDriver;
19use Metadata\Driver\FileLocatorInterface;
20use Metadata\MethodMetadata;
21use Symfony\Component\Yaml\Yaml;
22
23class YamlDriver extends AbstractFileDriver
24{
25    use ExpressionMetadataTrait;
26
27    /**
28     * @var ParserInterface
29     */
30    private $typeParser;
31    /**
32     * @var PropertyNamingStrategyInterface
33     */
34    private $namingStrategy;
35
36    public function __construct(FileLocatorInterface $locator, PropertyNamingStrategyInterface $namingStrategy, ?ParserInterface $typeParser = null, ?CompilableExpressionEvaluatorInterface $expressionEvaluator = null)
37    {
38        parent::__construct($locator);
39        $this->typeParser = $typeParser ?? new Parser();
40        $this->namingStrategy = $namingStrategy;
41        $this->expressionEvaluator = $expressionEvaluator;
42    }
43
44    protected function loadMetadataFromFile(\ReflectionClass $class, string $file): ?BaseClassMetadata
45    {
46        $config = Yaml::parse(file_get_contents($file));
47
48        if (!isset($config[$name = $class->name])) {
49            throw new InvalidMetadataException(sprintf('Expected metadata for class %s to be defined in %s.', $class->name, $file));
50        }
51
52        $config = $config[$name];
53        $metadata = new ClassMetadata($name);
54        $metadata->fileResources[] = $file;
55        $fileResource =  $class->getFilename();
56        if (false !== $fileResource) {
57            $metadata->fileResources[] = $fileResource;
58        }
59
60        $exclusionPolicy = isset($config['exclusion_policy']) ? strtoupper($config['exclusion_policy']) : 'NONE';
61        $excludeAll = isset($config['exclude']) ? (bool) $config['exclude'] : false;
62        $classAccessType = $config['access_type'] ?? PropertyMetadata::ACCESS_TYPE_PROPERTY;
63        $readOnlyClass = isset($config['read_only']) ? (bool) $config['read_only'] : false;
64        $this->addClassProperties($metadata, $config);
65
66        $propertiesMetadata = [];
67        if (array_key_exists('virtual_properties', $config)) {
68            foreach ($config['virtual_properties'] as $methodName => $propertySettings) {
69                if (isset($propertySettings['exp'])) {
70                    $virtualPropertyMetadata = new ExpressionPropertyMetadata(
71                        $name,
72                        $methodName,
73                        $this->parseExpression($propertySettings['exp'])
74                    );
75                    unset($propertySettings['exp']);
76                } else {
77                    if (!$class->hasMethod($methodName)) {
78                        throw new InvalidMetadataException('The method ' . $methodName . ' not found in class ' . $class->name);
79                    }
80                    $virtualPropertyMetadata = new VirtualPropertyMetadata($name, $methodName);
81                }
82
83                $pName = !empty($propertySettings['name']) ? $propertySettings['name'] : $virtualPropertyMetadata->name;
84
85                $propertiesMetadata[$pName] = $virtualPropertyMetadata;
86                $config['properties'][$pName] = $propertySettings;
87            }
88        }
89
90        if (!$excludeAll) {
91            foreach ($class->getProperties() as $property) {
92                if ($property->class !== $name || (isset($property->info) && $property->info['class'] !== $name)) {
93                    continue;
94                }
95
96                $pName = $property->getName();
97                $propertiesMetadata[$pName] = new PropertyMetadata($name, $pName);
98            }
99
100            foreach ($propertiesMetadata as $pName => $pMetadata) {
101                $isExclude = false;
102                $isExpose = $pMetadata instanceof VirtualPropertyMetadata
103                    || $pMetadata instanceof ExpressionPropertyMetadata
104                    || (isset($config['properties']) && array_key_exists($pName, $config['properties']));
105
106                if (isset($config['properties'][$pName])) {
107                    $pConfig = $config['properties'][$pName];
108
109                    if (isset($pConfig['exclude'])) {
110                        $isExclude = (bool) $pConfig['exclude'];
111                    }
112
113                    if ($isExclude) {
114                        continue;
115                    }
116
117                    if (isset($pConfig['expose'])) {
118                        $isExpose = (bool) $pConfig['expose'];
119                    }
120
121                    if (isset($pConfig['skip_when_empty'])) {
122                        $pMetadata->skipWhenEmpty = (bool) $pConfig['skip_when_empty'];
123                    }
124
125                    if (isset($pConfig['since_version'])) {
126                        $pMetadata->sinceVersion = (string) $pConfig['since_version'];
127                    }
128
129                    if (isset($pConfig['until_version'])) {
130                        $pMetadata->untilVersion = (string) $pConfig['until_version'];
131                    }
132
133                    if (isset($pConfig['exclude_if'])) {
134                        $pMetadata->excludeIf = $this->parseExpression((string) $pConfig['exclude_if']);
135                    }
136
137                    if (isset($pConfig['expose_if'])) {
138                        $pMetadata->excludeIf = $this->parseExpression('!(' . $pConfig['expose_if'] . ')');
139                    }
140
141                    if (isset($pConfig['serialized_name'])) {
142                        $pMetadata->serializedName = (string) $pConfig['serialized_name'];
143                    }
144
145                    if (isset($pConfig['type'])) {
146                        $pMetadata->setType($this->typeParser->parse((string) $pConfig['type']));
147                    }
148
149                    if (isset($pConfig['groups'])) {
150                        $pMetadata->groups = $pConfig['groups'];
151                    }
152
153                    if (isset($pConfig['xml_list'])) {
154                        $pMetadata->xmlCollection = true;
155
156                        $colConfig = $pConfig['xml_list'];
157                        if (isset($colConfig['inline'])) {
158                            $pMetadata->xmlCollectionInline = (bool) $colConfig['inline'];
159                        }
160
161                        if (isset($colConfig['entry_name'])) {
162                            $pMetadata->xmlEntryName = (string) $colConfig['entry_name'];
163                        }
164
165                        if (isset($colConfig['skip_when_empty'])) {
166                            $pMetadata->xmlCollectionSkipWhenEmpty = (bool) $colConfig['skip_when_empty'];
167                        } else {
168                            $pMetadata->xmlCollectionSkipWhenEmpty = true;
169                        }
170
171                        if (isset($colConfig['namespace'])) {
172                            $pMetadata->xmlEntryNamespace = (string) $colConfig['namespace'];
173                        }
174                    }
175
176                    if (isset($pConfig['xml_map'])) {
177                        $pMetadata->xmlCollection = true;
178
179                        $colConfig = $pConfig['xml_map'];
180                        if (isset($colConfig['inline'])) {
181                            $pMetadata->xmlCollectionInline = (bool) $colConfig['inline'];
182                        }
183
184                        if (isset($colConfig['entry_name'])) {
185                            $pMetadata->xmlEntryName = (string) $colConfig['entry_name'];
186                        }
187
188                        if (isset($colConfig['namespace'])) {
189                            $pMetadata->xmlEntryNamespace = (string) $colConfig['namespace'];
190                        }
191
192                        if (isset($colConfig['key_attribute_name'])) {
193                            $pMetadata->xmlKeyAttribute = $colConfig['key_attribute_name'];
194                        }
195                    }
196
197                    if (isset($pConfig['xml_element'])) {
198                        $colConfig = $pConfig['xml_element'];
199                        if (isset($colConfig['cdata'])) {
200                            $pMetadata->xmlElementCData = (bool) $colConfig['cdata'];
201                        }
202
203                        if (isset($colConfig['namespace'])) {
204                            $pMetadata->xmlNamespace = (string) $colConfig['namespace'];
205                        }
206                    }
207
208                    if (isset($pConfig['xml_attribute'])) {
209                        $pMetadata->xmlAttribute = (bool) $pConfig['xml_attribute'];
210                    }
211
212                    if (isset($pConfig['xml_attribute_map'])) {
213                        $pMetadata->xmlAttributeMap = (bool) $pConfig['xml_attribute_map'];
214                    }
215
216                    if (isset($pConfig['xml_value'])) {
217                        $pMetadata->xmlValue = (bool) $pConfig['xml_value'];
218                    }
219
220                    if (isset($pConfig['xml_key_value_pairs'])) {
221                        $pMetadata->xmlKeyValuePairs = (bool) $pConfig['xml_key_value_pairs'];
222                    }
223
224                    //we need read_only before setter and getter set, because that method depends on flag being set
225                    if (isset($pConfig['read_only'])) {
226                        $pMetadata->readOnly = (bool) $pConfig['read_only'];
227                    } else {
228                        $pMetadata->readOnly = $pMetadata->readOnly || $readOnlyClass;
229                    }
230
231                    $pMetadata->setAccessor(
232                        $pConfig['access_type'] ?? $classAccessType,
233                        $pConfig['accessor']['getter'] ?? null,
234                        $pConfig['accessor']['setter'] ?? null
235                    );
236
237                    if (isset($pConfig['inline'])) {
238                        $pMetadata->inline = (bool) $pConfig['inline'];
239                    }
240
241                    if (isset($pConfig['max_depth'])) {
242                        $pMetadata->maxDepth = (int) $pConfig['max_depth'];
243                    }
244                }
245
246                if (!$pMetadata->serializedName) {
247                    $pMetadata->serializedName = $this->namingStrategy->translateName($pMetadata);
248                }
249
250                if ($pMetadata->inline) {
251                    $metadata->isList = $metadata->isList || PropertyMetadata::isCollectionList($pMetadata->type);
252                    $metadata->isMap = $metadata->isMap || PropertyMetadata::isCollectionMap($pMetadata->type);
253                }
254
255                if (isset($config['properties'][$pName])) {
256                    $pConfig = $config['properties'][$pName];
257
258                    if (isset($pConfig['name'])) {
259                        $pMetadata->name = (string) $pConfig['name'];
260                    }
261                }
262
263                if ((ExclusionPolicy::NONE === $exclusionPolicy && !$isExclude)
264                    || (ExclusionPolicy::ALL === $exclusionPolicy && $isExpose)
265                ) {
266                    $metadata->addPropertyMetadata($pMetadata);
267                }
268            }
269        }
270
271        if (isset($config['callback_methods'])) {
272            $cConfig = $config['callback_methods'];
273
274            if (isset($cConfig['pre_serialize'])) {
275                $metadata->preSerializeMethods = $this->getCallbackMetadata($class, $cConfig['pre_serialize']);
276            }
277            if (isset($cConfig['post_serialize'])) {
278                $metadata->postSerializeMethods = $this->getCallbackMetadata($class, $cConfig['post_serialize']);
279            }
280            if (isset($cConfig['post_deserialize'])) {
281                $metadata->postDeserializeMethods = $this->getCallbackMetadata($class, $cConfig['post_deserialize']);
282            }
283        }
284
285        return $metadata;
286    }
287
288    protected function getExtension(): string
289    {
290        return 'yml';
291    }
292
293    private function addClassProperties(ClassMetadata $metadata, array $config): void
294    {
295        if (isset($config['custom_accessor_order']) && !isset($config['accessor_order'])) {
296            $config['accessor_order'] = 'custom';
297        }
298
299        if (isset($config['accessor_order'])) {
300            $metadata->setAccessorOrder($config['accessor_order'], $config['custom_accessor_order'] ?? []);
301        }
302
303        if (isset($config['xml_root_name'])) {
304            $metadata->xmlRootName = (string) $config['xml_root_name'];
305        }
306
307        if (isset($config['xml_root_prefix'])) {
308            $metadata->xmlRootPrefix = (string) $config['xml_root_prefix'];
309        }
310
311        if (isset($config['xml_root_namespace'])) {
312            $metadata->xmlRootNamespace = (string) $config['xml_root_namespace'];
313        }
314
315        if (array_key_exists('xml_namespaces', $config)) {
316            foreach ($config['xml_namespaces'] as $prefix => $uri) {
317                $metadata->registerNamespace($uri, $prefix);
318            }
319        }
320
321        if (isset($config['discriminator'])) {
322            if (isset($config['discriminator']['disabled']) && true === $config['discriminator']['disabled']) {
323                $metadata->discriminatorDisabled = true;
324            } else {
325                if (!isset($config['discriminator']['field_name'])) {
326                    throw new InvalidMetadataException('The "field_name" attribute must be set for discriminators.');
327                }
328
329                if (!isset($config['discriminator']['map']) || !\is_array($config['discriminator']['map'])) {
330                    throw new InvalidMetadataException('The "map" attribute must be set, and be an array for discriminators.');
331                }
332                $groups = $config['discriminator']['groups'] ?? [];
333                $metadata->setDiscriminator($config['discriminator']['field_name'], $config['discriminator']['map'], $groups);
334
335                if (isset($config['discriminator']['xml_attribute'])) {
336                    $metadata->xmlDiscriminatorAttribute = (bool) $config['discriminator']['xml_attribute'];
337                }
338                if (isset($config['discriminator']['xml_element'])) {
339                    if (isset($config['discriminator']['xml_element']['cdata'])) {
340                        $metadata->xmlDiscriminatorCData = (bool) $config['discriminator']['xml_element']['cdata'];
341                    }
342                    if (isset($config['discriminator']['xml_element']['namespace'])) {
343                        $metadata->xmlDiscriminatorNamespace = (string) $config['discriminator']['xml_element']['namespace'];
344                    }
345                }
346            }
347        }
348    }
349
350    /**
351     * @param string|string[] $config
352     */
353    private function getCallbackMetadata(\ReflectionClass $class, $config): array
354    {
355        if (\is_string($config)) {
356            $config = [$config];
357        } elseif (!\is_array($config)) {
358            throw new InvalidMetadataException(sprintf('callback methods expects a string, or an array of strings that represent method names, but got %s.', json_encode($config['pre_serialize'])));
359        }
360
361        $methods = [];
362        foreach ($config as $name) {
363            if (!$class->hasMethod($name)) {
364                throw new InvalidMetadataException(sprintf('The method %s does not exist in class %s.', $name, $class->name));
365            }
366
367            $methods[] = new MethodMetadata($class->name, $name);
368        }
369
370        return $methods;
371    }
372}
373