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