1<?php 2 3declare(strict_types=1); 4 5namespace JMS\Serializer; 6 7use Doctrine\Common\Annotations\AnnotationReader; 8use Doctrine\Common\Annotations\CachedReader; 9use Doctrine\Common\Annotations\Reader; 10use Doctrine\Common\Cache\FilesystemCache; 11use JMS\Serializer\Accessor\AccessorStrategyInterface; 12use JMS\Serializer\Accessor\DefaultAccessorStrategy; 13use JMS\Serializer\Builder\DefaultDriverFactory; 14use JMS\Serializer\Builder\DriverFactoryInterface; 15use JMS\Serializer\Construction\ObjectConstructorInterface; 16use JMS\Serializer\Construction\UnserializeObjectConstructor; 17use JMS\Serializer\ContextFactory\CallableDeserializationContextFactory; 18use JMS\Serializer\ContextFactory\CallableSerializationContextFactory; 19use JMS\Serializer\ContextFactory\DeserializationContextFactoryInterface; 20use JMS\Serializer\ContextFactory\SerializationContextFactoryInterface; 21use JMS\Serializer\EventDispatcher\EventDispatcher; 22use JMS\Serializer\EventDispatcher\EventDispatcherInterface; 23use JMS\Serializer\EventDispatcher\Subscriber\DoctrineProxySubscriber; 24use JMS\Serializer\Exception\InvalidArgumentException; 25use JMS\Serializer\Exception\RuntimeException; 26use JMS\Serializer\Expression\CompilableExpressionEvaluatorInterface; 27use JMS\Serializer\Expression\ExpressionEvaluatorInterface; 28use JMS\Serializer\GraphNavigator\Factory\DeserializationGraphNavigatorFactory; 29use JMS\Serializer\GraphNavigator\Factory\GraphNavigatorFactoryInterface; 30use JMS\Serializer\GraphNavigator\Factory\SerializationGraphNavigatorFactory; 31use JMS\Serializer\Handler\ArrayCollectionHandler; 32use JMS\Serializer\Handler\DateHandler; 33use JMS\Serializer\Handler\HandlerRegistry; 34use JMS\Serializer\Handler\HandlerRegistryInterface; 35use JMS\Serializer\Handler\IteratorHandler; 36use JMS\Serializer\Handler\StdClassHandler; 37use JMS\Serializer\Naming\CamelCaseNamingStrategy; 38use JMS\Serializer\Naming\PropertyNamingStrategyInterface; 39use JMS\Serializer\Naming\SerializedNameAnnotationStrategy; 40use JMS\Serializer\Type\Parser; 41use JMS\Serializer\Type\ParserInterface; 42use JMS\Serializer\Visitor\Factory\DeserializationVisitorFactory; 43use JMS\Serializer\Visitor\Factory\JsonDeserializationVisitorFactory; 44use JMS\Serializer\Visitor\Factory\JsonSerializationVisitorFactory; 45use JMS\Serializer\Visitor\Factory\SerializationVisitorFactory; 46use JMS\Serializer\Visitor\Factory\XmlDeserializationVisitorFactory; 47use JMS\Serializer\Visitor\Factory\XmlSerializationVisitorFactory; 48use Metadata\Cache\CacheInterface; 49use Metadata\Cache\FileCache; 50use Metadata\MetadataFactory; 51use Metadata\MetadataFactoryInterface; 52 53/** 54 * Builder for serializer instances. 55 * 56 * This object makes serializer construction a breeze for projects that do not use 57 * any special dependency injection container. 58 * 59 * @author Johannes M. Schmitt <schmittjoh@gmail.com> 60 */ 61final class SerializerBuilder 62{ 63 /** 64 * @var string[] 65 */ 66 private $metadataDirs = []; 67 68 /** 69 * @var HandlerRegistryInterface 70 */ 71 private $handlerRegistry; 72 73 /** 74 * @var bool 75 */ 76 private $handlersConfigured = false; 77 78 /** 79 * @var EventDispatcherInterface 80 */ 81 private $eventDispatcher; 82 83 /** 84 * @var bool 85 */ 86 private $listenersConfigured = false; 87 88 /** 89 * @var ObjectConstructorInterface 90 */ 91 private $objectConstructor; 92 93 /** 94 * @var SerializationVisitorFactory[] 95 */ 96 private $serializationVisitors; 97 98 /** 99 * @var DeserializationVisitorFactory[] 100 */ 101 private $deserializationVisitors; 102 103 /** 104 * @var bool 105 */ 106 private $visitorsAdded = false; 107 108 /** 109 * @var PropertyNamingStrategyInterface 110 */ 111 private $propertyNamingStrategy; 112 113 /** 114 * @var bool 115 */ 116 private $debug = false; 117 118 /** 119 * @var string 120 */ 121 private $cacheDir; 122 123 /** 124 * @var AnnotationReader 125 */ 126 private $annotationReader; 127 128 /** 129 * @var bool 130 */ 131 private $includeInterfaceMetadata = false; 132 133 /** 134 * @var DriverFactoryInterface 135 */ 136 private $driverFactory; 137 138 /** 139 * @var SerializationContextFactoryInterface 140 */ 141 private $serializationContextFactory; 142 143 /** 144 * @var DeserializationContextFactoryInterface 145 */ 146 private $deserializationContextFactory; 147 148 /** 149 * @var ParserInterface 150 */ 151 private $typeParser; 152 153 /** 154 * @var ExpressionEvaluatorInterface 155 */ 156 private $expressionEvaluator; 157 158 /** 159 * @var AccessorStrategyInterface 160 */ 161 private $accessorStrategy; 162 163 /** 164 * @var CacheInterface 165 */ 166 private $metadataCache; 167 168 /** 169 * @param mixed ...$args 170 * 171 * @return SerializerBuilder 172 */ 173 public static function create(...$args): self 174 { 175 return new static(...$args); 176 } 177 178 public function __construct(?HandlerRegistryInterface $handlerRegistry = null, ?EventDispatcherInterface $eventDispatcher = null) 179 { 180 $this->typeParser = new Parser(); 181 $this->handlerRegistry = $handlerRegistry ?: new HandlerRegistry(); 182 $this->eventDispatcher = $eventDispatcher ?: new EventDispatcher(); 183 $this->serializationVisitors = []; 184 $this->deserializationVisitors = []; 185 186 if ($handlerRegistry) { 187 $this->handlersConfigured = true; 188 } 189 if ($eventDispatcher) { 190 $this->listenersConfigured = true; 191 } 192 } 193 194 public function setAccessorStrategy(AccessorStrategyInterface $accessorStrategy): self 195 { 196 $this->accessorStrategy = $accessorStrategy; 197 return $this; 198 } 199 200 private function getAccessorStrategy(): AccessorStrategyInterface 201 { 202 if (!$this->accessorStrategy) { 203 $this->accessorStrategy = new DefaultAccessorStrategy($this->expressionEvaluator); 204 } 205 return $this->accessorStrategy; 206 } 207 208 public function setExpressionEvaluator(ExpressionEvaluatorInterface $expressionEvaluator): self 209 { 210 $this->expressionEvaluator = $expressionEvaluator; 211 212 return $this; 213 } 214 215 public function setTypeParser(ParserInterface $parser): self 216 { 217 $this->typeParser = $parser; 218 219 return $this; 220 } 221 222 public function setAnnotationReader(Reader $reader): self 223 { 224 $this->annotationReader = $reader; 225 226 return $this; 227 } 228 229 public function setDebug(bool $bool): self 230 { 231 $this->debug = $bool; 232 233 return $this; 234 } 235 236 public function setCacheDir(string $dir): self 237 { 238 if (!is_dir($dir)) { 239 $this->createDir($dir); 240 } 241 if (!is_writable($dir)) { 242 throw new InvalidArgumentException(sprintf('The cache directory "%s" is not writable.', $dir)); 243 } 244 245 $this->cacheDir = $dir; 246 247 return $this; 248 } 249 250 public function addDefaultHandlers(): self 251 { 252 $this->handlersConfigured = true; 253 $this->handlerRegistry->registerSubscribingHandler(new DateHandler()); 254 $this->handlerRegistry->registerSubscribingHandler(new StdClassHandler()); 255 $this->handlerRegistry->registerSubscribingHandler(new ArrayCollectionHandler()); 256 $this->handlerRegistry->registerSubscribingHandler(new IteratorHandler()); 257 258 return $this; 259 } 260 261 public function configureHandlers(\Closure $closure): self 262 { 263 $this->handlersConfigured = true; 264 $closure($this->handlerRegistry); 265 266 return $this; 267 } 268 269 public function addDefaultListeners(): self 270 { 271 $this->listenersConfigured = true; 272 $this->eventDispatcher->addSubscriber(new DoctrineProxySubscriber()); 273 274 return $this; 275 } 276 277 public function configureListeners(\Closure $closure): self 278 { 279 $this->listenersConfigured = true; 280 $closure($this->eventDispatcher); 281 282 return $this; 283 } 284 285 public function setObjectConstructor(ObjectConstructorInterface $constructor): self 286 { 287 $this->objectConstructor = $constructor; 288 289 return $this; 290 } 291 292 public function setPropertyNamingStrategy(PropertyNamingStrategyInterface $propertyNamingStrategy): self 293 { 294 $this->propertyNamingStrategy = $propertyNamingStrategy; 295 296 return $this; 297 } 298 299 public function setSerializationVisitor(string $format, SerializationVisitorFactory $visitor): self 300 { 301 $this->visitorsAdded = true; 302 $this->serializationVisitors[$format] = $visitor; 303 304 return $this; 305 } 306 307 public function setDeserializationVisitor(string $format, DeserializationVisitorFactory $visitor): self 308 { 309 $this->visitorsAdded = true; 310 $this->deserializationVisitors[$format] = $visitor; 311 312 return $this; 313 } 314 315 public function addDefaultSerializationVisitors(): self 316 { 317 $this->visitorsAdded = true; 318 $this->serializationVisitors = [ 319 'xml' => new XmlSerializationVisitorFactory(), 320 'json' => new JsonSerializationVisitorFactory(), 321 ]; 322 323 return $this; 324 } 325 326 public function addDefaultDeserializationVisitors(): self 327 { 328 $this->visitorsAdded = true; 329 $this->deserializationVisitors = [ 330 'xml' => new XmlDeserializationVisitorFactory(), 331 'json' => new JsonDeserializationVisitorFactory(), 332 ]; 333 334 return $this; 335 } 336 337 /** 338 * @param bool $include Whether to include the metadata from the interfaces 339 * 340 * @return SerializerBuilder 341 */ 342 public function includeInterfaceMetadata(bool $include): self 343 { 344 $this->includeInterfaceMetadata = $include; 345 346 return $this; 347 } 348 349 /** 350 * Sets a map of namespace prefixes to directories. 351 * 352 * This method overrides any previously defined directories. 353 * 354 * @param array <string,string> $namespacePrefixToDirMap 355 * 356 * @return SerializerBuilder 357 * 358 * @throws InvalidArgumentException When a directory does not exist. 359 */ 360 public function setMetadataDirs(array $namespacePrefixToDirMap): self 361 { 362 foreach ($namespacePrefixToDirMap as $dir) { 363 if (!is_dir($dir)) { 364 throw new InvalidArgumentException(sprintf('The directory "%s" does not exist.', $dir)); 365 } 366 } 367 368 $this->metadataDirs = $namespacePrefixToDirMap; 369 370 return $this; 371 } 372 373 /** 374 * Adds a directory where the serializer will look for class metadata. 375 * 376 * The namespace prefix will make the names of the actual metadata files a bit shorter. For example, let's assume 377 * that you have a directory where you only store metadata files for the ``MyApplication\Entity`` namespace. 378 * 379 * If you use an empty prefix, your metadata files would need to look like: 380 * 381 * ``my-dir/MyApplication.Entity.SomeObject.yml`` 382 * ``my-dir/MyApplication.Entity.OtherObject.xml`` 383 * 384 * If you use ``MyApplication\Entity`` as prefix, your metadata files would need to look like: 385 * 386 * ``my-dir/SomeObject.yml`` 387 * ``my-dir/OtherObject.yml`` 388 * 389 * Please keep in mind that you currently may only have one directory per namespace prefix. 390 * 391 * @param string $dir The directory where metadata files are located. 392 * @param string $namespacePrefix An optional prefix if you only store metadata for specific namespaces in this directory. 393 * 394 * @return SerializerBuilder 395 * 396 * @throws InvalidArgumentException When a directory does not exist. 397 * @throws InvalidArgumentException When a directory has already been registered. 398 */ 399 public function addMetadataDir(string $dir, string $namespacePrefix = ''): self 400 { 401 if (!is_dir($dir)) { 402 throw new InvalidArgumentException(sprintf('The directory "%s" does not exist.', $dir)); 403 } 404 405 if (isset($this->metadataDirs[$namespacePrefix])) { 406 throw new InvalidArgumentException(sprintf('There is already a directory configured for the namespace prefix "%s". Please use replaceMetadataDir() to override directories.', $namespacePrefix)); 407 } 408 409 $this->metadataDirs[$namespacePrefix] = $dir; 410 411 return $this; 412 } 413 414 /** 415 * Adds a map of namespace prefixes to directories. 416 * 417 * @param array <string,string> $namespacePrefixToDirMap 418 * 419 * @return SerializerBuilder 420 */ 421 public function addMetadataDirs(array $namespacePrefixToDirMap): self 422 { 423 foreach ($namespacePrefixToDirMap as $prefix => $dir) { 424 $this->addMetadataDir($dir, $prefix); 425 } 426 427 return $this; 428 } 429 430 /** 431 * Similar to addMetadataDir(), but overrides an existing entry. 432 * 433 * @return SerializerBuilder 434 * 435 * @throws InvalidArgumentException When a directory does not exist. 436 * @throws InvalidArgumentException When no directory is configured for the ns prefix. 437 */ 438 public function replaceMetadataDir(string $dir, string $namespacePrefix = ''): self 439 { 440 if (!is_dir($dir)) { 441 throw new InvalidArgumentException(sprintf('The directory "%s" does not exist.', $dir)); 442 } 443 444 if (!isset($this->metadataDirs[$namespacePrefix])) { 445 throw new InvalidArgumentException(sprintf('There is no directory configured for namespace prefix "%s". Please use addMetadataDir() for adding new directories.', $namespacePrefix)); 446 } 447 448 $this->metadataDirs[$namespacePrefix] = $dir; 449 450 return $this; 451 } 452 453 public function setMetadataDriverFactory(DriverFactoryInterface $driverFactory): self 454 { 455 $this->driverFactory = $driverFactory; 456 457 return $this; 458 } 459 460 /** 461 * @param SerializationContextFactoryInterface|callable $serializationContextFactory 462 */ 463 public function setSerializationContextFactory($serializationContextFactory): self 464 { 465 if ($serializationContextFactory instanceof SerializationContextFactoryInterface) { 466 $this->serializationContextFactory = $serializationContextFactory; 467 } elseif (is_callable($serializationContextFactory)) { 468 $this->serializationContextFactory = new CallableSerializationContextFactory( 469 $serializationContextFactory 470 ); 471 } else { 472 throw new InvalidArgumentException('expected SerializationContextFactoryInterface or callable.'); 473 } 474 475 return $this; 476 } 477 478 /** 479 * @param DeserializationContextFactoryInterface|callable $deserializationContextFactory 480 */ 481 public function setDeserializationContextFactory($deserializationContextFactory): self 482 { 483 if ($deserializationContextFactory instanceof DeserializationContextFactoryInterface) { 484 $this->deserializationContextFactory = $deserializationContextFactory; 485 } elseif (is_callable($deserializationContextFactory)) { 486 $this->deserializationContextFactory = new CallableDeserializationContextFactory( 487 $deserializationContextFactory 488 ); 489 } else { 490 throw new InvalidArgumentException('expected DeserializationContextFactoryInterface or callable.'); 491 } 492 493 return $this; 494 } 495 496 public function setMetadataCache(CacheInterface $cache): self 497 { 498 $this->metadataCache = $cache; 499 return $this; 500 } 501 502 public function build(): SerializerInterface 503 { 504 $annotationReader = $this->annotationReader; 505 if (null === $annotationReader) { 506 $annotationReader = new AnnotationReader(); 507 508 if (null !== $this->cacheDir) { 509 $this->createDir($this->cacheDir . '/annotations'); 510 $annotationsCache = new FilesystemCache($this->cacheDir . '/annotations'); 511 $annotationReader = new CachedReader($annotationReader, $annotationsCache, $this->debug); 512 } 513 } 514 515 if (null === $this->driverFactory) { 516 $this->initializePropertyNamingStrategy(); 517 $this->driverFactory = new DefaultDriverFactory( 518 $this->propertyNamingStrategy, 519 $this->typeParser, 520 $this->expressionEvaluator instanceof CompilableExpressionEvaluatorInterface ? $this->expressionEvaluator : null 521 ); 522 } 523 524 $metadataDriver = $this->driverFactory->createDriver($this->metadataDirs, $annotationReader); 525 $metadataFactory = new MetadataFactory($metadataDriver, null, $this->debug); 526 527 $metadataFactory->setIncludeInterfaces($this->includeInterfaceMetadata); 528 529 if (null !== $this->metadataCache) { 530 $metadataFactory->setCache($this->metadataCache); 531 } elseif (null !== $this->cacheDir) { 532 $this->createDir($this->cacheDir . '/metadata'); 533 $metadataFactory->setCache(new FileCache($this->cacheDir . '/metadata')); 534 } 535 536 if (!$this->handlersConfigured) { 537 $this->addDefaultHandlers(); 538 } 539 540 if (!$this->listenersConfigured) { 541 $this->addDefaultListeners(); 542 } 543 544 if (!$this->visitorsAdded) { 545 $this->addDefaultSerializationVisitors(); 546 $this->addDefaultDeserializationVisitors(); 547 } 548 $navigatorFactories = [ 549 GraphNavigatorInterface::DIRECTION_SERIALIZATION => $this->getSerializationNavigatorFactory($metadataFactory), 550 GraphNavigatorInterface::DIRECTION_DESERIALIZATION => $this->getDeserializationNavigatorFactory($metadataFactory), 551 ]; 552 553 return new Serializer( 554 $metadataFactory, 555 $navigatorFactories, 556 $this->serializationVisitors, 557 $this->deserializationVisitors, 558 $this->serializationContextFactory, 559 $this->deserializationContextFactory, 560 $this->typeParser 561 ); 562 } 563 564 private function getSerializationNavigatorFactory(MetadataFactoryInterface $metadataFactory): GraphNavigatorFactoryInterface 565 { 566 return new SerializationGraphNavigatorFactory( 567 $metadataFactory, 568 $this->handlerRegistry, 569 $this->getAccessorStrategy(), 570 $this->eventDispatcher, 571 $this->expressionEvaluator 572 ); 573 } 574 575 private function getDeserializationNavigatorFactory(MetadataFactoryInterface $metadataFactory): GraphNavigatorFactoryInterface 576 { 577 return new DeserializationGraphNavigatorFactory( 578 $metadataFactory, 579 $this->handlerRegistry, 580 $this->objectConstructor ?: new UnserializeObjectConstructor(), 581 $this->getAccessorStrategy(), 582 $this->eventDispatcher, 583 $this->expressionEvaluator 584 ); 585 } 586 587 private function initializePropertyNamingStrategy(): void 588 { 589 if (null !== $this->propertyNamingStrategy) { 590 return; 591 } 592 593 $this->propertyNamingStrategy = new SerializedNameAnnotationStrategy(new CamelCaseNamingStrategy()); 594 } 595 596 private function createDir(string $dir): void 597 { 598 if (is_dir($dir)) { 599 return; 600 } 601 602 if (false === @mkdir($dir, 0777, true) && false === is_dir($dir)) { 603 throw new RuntimeException(sprintf('Could not create directory "%s".', $dir)); 604 } 605 } 606} 607