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