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