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\Exception\XmlErrorException; 10use JMS\Serializer\Expression\CompilableExpressionEvaluatorInterface; 11use JMS\Serializer\Metadata\ClassMetadata; 12use JMS\Serializer\Metadata\ExpressionPropertyMetadata; 13use JMS\Serializer\Metadata\PropertyMetadata; 14use JMS\Serializer\Metadata\VirtualPropertyMetadata; 15use JMS\Serializer\Naming\PropertyNamingStrategyInterface; 16use JMS\Serializer\Type\Parser; 17use JMS\Serializer\Type\ParserInterface; 18use Metadata\ClassMetadata as BaseClassMetadata; 19use Metadata\Driver\AbstractFileDriver; 20use Metadata\Driver\FileLocatorInterface; 21use Metadata\MethodMetadata; 22 23class XmlDriver 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 $path): ?BaseClassMetadata 45 { 46 $previous = libxml_use_internal_errors(true); 47 libxml_clear_errors(); 48 49 $elem = simplexml_load_file($path); 50 libxml_use_internal_errors($previous); 51 52 if (false === $elem) { 53 throw new InvalidMetadataException('Invalid XML content for metadata', 0, new XmlErrorException(libxml_get_last_error())); 54 } 55 56 $metadata = new ClassMetadata($name = $class->name); 57 if (!$elems = $elem->xpath("./class[@name = '" . $name . "']")) { 58 throw new InvalidMetadataException(sprintf('Could not find class %s inside XML element.', $name)); 59 } 60 $elem = reset($elems); 61 62 $metadata->fileResources[] = $path; 63 $fileResource = $class->getFilename(); 64 if (false !== $fileResource) { 65 $metadata->fileResources[] = $fileResource; 66 } 67 $exclusionPolicy = strtoupper((string) $elem->attributes()->{'exclusion-policy'}) ?: 'NONE'; 68 $exclude = $elem->attributes()->exclude; 69 $excludeAll = null !== $exclude ? 'true' === strtolower((string) $exclude) : false; 70 $classAccessType = (string) ($elem->attributes()->{'access-type'} ?: PropertyMetadata::ACCESS_TYPE_PROPERTY); 71 72 $propertiesMetadata = []; 73 $propertiesNodes = []; 74 75 if (null !== $accessorOrder = $elem->attributes()->{'accessor-order'}) { 76 $metadata->setAccessorOrder((string) $accessorOrder, preg_split('/\s*,\s*/', (string) $elem->attributes()->{'custom-accessor-order'})); 77 } 78 79 if (null !== $xmlRootName = $elem->attributes()->{'xml-root-name'}) { 80 $metadata->xmlRootName = (string) $xmlRootName; 81 } 82 83 if (null !== $xmlRootNamespace = $elem->attributes()->{'xml-root-namespace'}) { 84 $metadata->xmlRootNamespace = (string) $xmlRootNamespace; 85 } 86 if (null !== $xmlRootPrefix = $elem->attributes()->{'xml-root-prefix'}) { 87 $metadata->xmlRootPrefix = (string) $xmlRootPrefix; 88 } 89 90 $readOnlyClass = 'true' === strtolower((string) $elem->attributes()->{'read-only'}); 91 92 $discriminatorFieldName = (string) $elem->attributes()->{'discriminator-field-name'}; 93 $discriminatorMap = []; 94 foreach ($elem->xpath('./discriminator-class') as $entry) { 95 if (!isset($entry->attributes()->value)) { 96 throw new InvalidMetadataException('Each discriminator-class element must have a "value" attribute.'); 97 } 98 99 $discriminatorMap[(string) $entry->attributes()->value] = (string) $entry; 100 } 101 102 if ('true' === (string) $elem->attributes()->{'discriminator-disabled'}) { 103 $metadata->discriminatorDisabled = true; 104 } elseif (!empty($discriminatorFieldName) || !empty($discriminatorMap)) { 105 $discriminatorGroups = []; 106 foreach ($elem->xpath('./discriminator-groups/group') as $entry) { 107 $discriminatorGroups[] = (string) $entry; 108 } 109 $metadata->setDiscriminator($discriminatorFieldName, $discriminatorMap, $discriminatorGroups); 110 } 111 112 foreach ($elem->xpath('./xml-namespace') as $xmlNamespace) { 113 if (!isset($xmlNamespace->attributes()->uri)) { 114 throw new InvalidMetadataException('The prefix attribute must be set for all xml-namespace elements.'); 115 } 116 117 if (isset($xmlNamespace->attributes()->prefix)) { 118 $prefix = (string) $xmlNamespace->attributes()->prefix; 119 } else { 120 $prefix = null; 121 } 122 123 $metadata->registerNamespace((string) $xmlNamespace->attributes()->uri, $prefix); 124 } 125 126 foreach ($elem->xpath('./xml-discriminator') as $xmlDiscriminator) { 127 if (isset($xmlDiscriminator->attributes()->attribute)) { 128 $metadata->xmlDiscriminatorAttribute = 'true' === (string) $xmlDiscriminator->attributes()->attribute; 129 } 130 if (isset($xmlDiscriminator->attributes()->cdata)) { 131 $metadata->xmlDiscriminatorCData = 'true' === (string) $xmlDiscriminator->attributes()->cdata; 132 } 133 if (isset($xmlDiscriminator->attributes()->namespace)) { 134 $metadata->xmlDiscriminatorNamespace = (string) $xmlDiscriminator->attributes()->namespace; 135 } 136 } 137 138 foreach ($elem->xpath('./virtual-property') as $method) { 139 if (isset($method->attributes()->expression)) { 140 $virtualPropertyMetadata = new ExpressionPropertyMetadata( 141 $name, 142 (string) $method->attributes()->name, 143 $this->parseExpression((string) $method->attributes()->expression) 144 ); 145 } else { 146 if (!isset($method->attributes()->method)) { 147 throw new InvalidMetadataException('The method attribute must be set for all virtual-property elements.'); 148 } 149 $virtualPropertyMetadata = new VirtualPropertyMetadata($name, (string) $method->attributes()->method); 150 } 151 152 $propertiesMetadata[] = $virtualPropertyMetadata; 153 $propertiesNodes[] = $method; 154 } 155 156 if (!$excludeAll) { 157 foreach ($class->getProperties() as $property) { 158 if ($property->class !== $name || (isset($property->info) && $property->info['class'] !== $name)) { 159 continue; 160 } 161 162 $propertiesMetadata[] = new PropertyMetadata($name, $pName = $property->getName()); 163 $pElems = $elem->xpath("./property[@name = '" . $pName . "']"); 164 165 $propertiesNodes[] = $pElems ? reset($pElems) : null; 166 } 167 168 foreach ($propertiesMetadata as $propertyKey => $pMetadata) { 169 $isExclude = false; 170 $isExpose = $pMetadata instanceof VirtualPropertyMetadata 171 || $pMetadata instanceof ExpressionPropertyMetadata; 172 173 $pElem = $propertiesNodes[$propertyKey]; 174 if (!empty($pElem)) { 175 if (null !== $exclude = $pElem->attributes()->exclude) { 176 $isExclude = 'true' === strtolower((string) $exclude); 177 } 178 179 if ($isExclude) { 180 continue; 181 } 182 183 if (null !== $expose = $pElem->attributes()->expose) { 184 $isExpose = 'true' === strtolower((string) $expose); 185 } 186 187 if (null !== $excludeIf = $pElem->attributes()->{'exclude-if'}) { 188 $pMetadata->excludeIf = $this->parseExpression((string) $excludeIf); 189 } 190 191 if (null !== $skip = $pElem->attributes()->{'skip-when-empty'}) { 192 $pMetadata->skipWhenEmpty = 'true' === strtolower((string) $skip); 193 } 194 195 if (null !== $excludeIf = $pElem->attributes()->{'expose-if'}) { 196 $pMetadata->excludeIf = $this->parseExpression('!(' . (string) $excludeIf . ')'); 197 $isExpose = true; 198 } 199 200 if (null !== $version = $pElem->attributes()->{'since-version'}) { 201 $pMetadata->sinceVersion = (string) $version; 202 } 203 204 if (null !== $version = $pElem->attributes()->{'until-version'}) { 205 $pMetadata->untilVersion = (string) $version; 206 } 207 208 if (null !== $serializedName = $pElem->attributes()->{'serialized-name'}) { 209 $pMetadata->serializedName = (string) $serializedName; 210 } 211 212 if (null !== $type = $pElem->attributes()->type) { 213 $pMetadata->setType($this->typeParser->parse((string) $type)); 214 } elseif (isset($pElem->type)) { 215 $pMetadata->setType($this->typeParser->parse((string) $pElem->type)); 216 } 217 218 if (null !== $groups = $pElem->attributes()->groups) { 219 $pMetadata->groups = preg_split('/\s*,\s*/', trim((string) $groups)); 220 } elseif (isset($pElem->groups)) { 221 $pMetadata->groups = (array) $pElem->groups->value; 222 } 223 224 if (isset($pElem->{'xml-list'})) { 225 $pMetadata->xmlCollection = true; 226 227 $colConfig = $pElem->{'xml-list'}; 228 if (isset($colConfig->attributes()->inline)) { 229 $pMetadata->xmlCollectionInline = 'true' === (string) $colConfig->attributes()->inline; 230 } 231 232 if (isset($colConfig->attributes()->{'entry-name'})) { 233 $pMetadata->xmlEntryName = (string) $colConfig->attributes()->{'entry-name'}; 234 } 235 236 if (isset($colConfig->attributes()->{'skip-when-empty'})) { 237 $pMetadata->xmlCollectionSkipWhenEmpty = 'true' === (string) $colConfig->attributes()->{'skip-when-empty'}; 238 } else { 239 $pMetadata->xmlCollectionSkipWhenEmpty = true; 240 } 241 242 if (isset($colConfig->attributes()->namespace)) { 243 $pMetadata->xmlEntryNamespace = (string) $colConfig->attributes()->namespace; 244 } 245 } 246 247 if (isset($pElem->{'xml-map'})) { 248 $pMetadata->xmlCollection = true; 249 250 $colConfig = $pElem->{'xml-map'}; 251 if (isset($colConfig->attributes()->inline)) { 252 $pMetadata->xmlCollectionInline = 'true' === (string) $colConfig->attributes()->inline; 253 } 254 255 if (isset($colConfig->attributes()->{'entry-name'})) { 256 $pMetadata->xmlEntryName = (string) $colConfig->attributes()->{'entry-name'}; 257 } 258 259 if (isset($colConfig->attributes()->namespace)) { 260 $pMetadata->xmlEntryNamespace = (string) $colConfig->attributes()->namespace; 261 } 262 263 if (isset($colConfig->attributes()->{'key-attribute-name'})) { 264 $pMetadata->xmlKeyAttribute = (string) $colConfig->attributes()->{'key-attribute-name'}; 265 } 266 } 267 268 if (isset($pElem->{'xml-element'})) { 269 $colConfig = $pElem->{'xml-element'}; 270 if (isset($colConfig->attributes()->cdata)) { 271 $pMetadata->xmlElementCData = 'true' === (string) $colConfig->attributes()->cdata; 272 } 273 274 if (isset($colConfig->attributes()->namespace)) { 275 $pMetadata->xmlNamespace = (string) $colConfig->attributes()->namespace; 276 } 277 } 278 279 if (isset($pElem->attributes()->{'xml-attribute'})) { 280 $pMetadata->xmlAttribute = 'true' === (string) $pElem->attributes()->{'xml-attribute'}; 281 } 282 283 if (isset($pElem->attributes()->{'xml-attribute-map'})) { 284 $pMetadata->xmlAttributeMap = 'true' === (string) $pElem->attributes()->{'xml-attribute-map'}; 285 } 286 287 if (isset($pElem->attributes()->{'xml-value'})) { 288 $pMetadata->xmlValue = 'true' === (string) $pElem->attributes()->{'xml-value'}; 289 } 290 291 if (isset($pElem->attributes()->{'xml-key-value-pairs'})) { 292 $pMetadata->xmlKeyValuePairs = 'true' === (string) $pElem->attributes()->{'xml-key-value-pairs'}; 293 } 294 295 if (isset($pElem->attributes()->{'max-depth'})) { 296 $pMetadata->maxDepth = (int) $pElem->attributes()->{'max-depth'}; 297 } 298 299 //we need read-only before setter and getter set, because that method depends on flag being set 300 if (null !== $readOnly = $pElem->attributes()->{'read-only'}) { 301 $pMetadata->readOnly = 'true' === strtolower((string) $readOnly); 302 } else { 303 $pMetadata->readOnly = $pMetadata->readOnly || $readOnlyClass; 304 } 305 306 $getter = $pElem->attributes()->{'accessor-getter'}; 307 $setter = $pElem->attributes()->{'accessor-setter'}; 308 $pMetadata->setAccessor( 309 (string) ($pElem->attributes()->{'access-type'} ?: $classAccessType), 310 $getter ? (string) $getter : null, 311 $setter ? (string) $setter : null 312 ); 313 314 if (null !== $inline = $pElem->attributes()->inline) { 315 $pMetadata->inline = 'true' === strtolower((string) $inline); 316 } 317 } 318 319 if ($pMetadata->inline) { 320 $metadata->isList = $metadata->isList || PropertyMetadata::isCollectionList($pMetadata->type); 321 $metadata->isMap = $metadata->isMap || PropertyMetadata::isCollectionMap($pMetadata->type); 322 } 323 324 if (!$pMetadata->serializedName) { 325 $pMetadata->serializedName = $this->namingStrategy->translateName($pMetadata); 326 } 327 328 if (!empty($pElem) && null !== $name = $pElem->attributes()->name) { 329 $pMetadata->name = (string) $name; 330 } 331 332 if ((ExclusionPolicy::NONE === (string) $exclusionPolicy && !$isExclude) 333 || (ExclusionPolicy::ALL === (string) $exclusionPolicy && $isExpose) 334 ) { 335 $metadata->addPropertyMetadata($pMetadata); 336 } 337 } 338 } 339 340 foreach ($elem->xpath('./callback-method') as $method) { 341 if (!isset($method->attributes()->type)) { 342 throw new InvalidMetadataException('The type attribute must be set for all callback-method elements.'); 343 } 344 if (!isset($method->attributes()->name)) { 345 throw new InvalidMetadataException('The name attribute must be set for all callback-method elements.'); 346 } 347 348 switch ((string) $method->attributes()->type) { 349 case 'pre-serialize': 350 $metadata->addPreSerializeMethod(new MethodMetadata($name, (string) $method->attributes()->name)); 351 break; 352 353 case 'post-serialize': 354 $metadata->addPostSerializeMethod(new MethodMetadata($name, (string) $method->attributes()->name)); 355 break; 356 357 case 'post-deserialize': 358 $metadata->addPostDeserializeMethod(new MethodMetadata($name, (string) $method->attributes()->name)); 359 break; 360 361 case 'handler': 362 if (!isset($method->attributes()->format)) { 363 throw new InvalidMetadataException('The format attribute must be set for "handler" callback methods.'); 364 } 365 if (!isset($method->attributes()->direction)) { 366 throw new InvalidMetadataException('The direction attribute must be set for "handler" callback methods.'); 367 } 368 369 break; 370 371 default: 372 throw new InvalidMetadataException(sprintf('The type "%s" is not supported.', $method->attributes()->name)); 373 } 374 } 375 376 return $metadata; 377 } 378 379 protected function getExtension(): string 380 { 381 return 'xml'; 382 } 383} 384