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