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