Completed
Pull Request — master (#1289)
by Johannes
13:58
created

SerializerBuilder::setDocBlockTypeResolver()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1.037

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 5
rs 10
ccs 2
cts 3
cp 0.6667
crap 1.037
1
<?php
2
3
declare(strict_types=1);
4
5
namespace JMS\Serializer;
6
7
use Doctrine\Common\Annotations\AnnotationReader;
8
use Doctrine\Common\Annotations\CachedReader;
9
use Doctrine\Common\Annotations\Reader;
10
use Doctrine\Common\Cache\FilesystemCache;
11
use JMS\Serializer\Accessor\AccessorStrategyInterface;
12
use JMS\Serializer\Accessor\DefaultAccessorStrategy;
13
use JMS\Serializer\Builder\DefaultDriverFactory;
14
use JMS\Serializer\Builder\DriverFactoryInterface;
15
use JMS\Serializer\Construction\ObjectConstructorInterface;
16
use JMS\Serializer\Construction\UnserializeObjectConstructor;
17
use JMS\Serializer\ContextFactory\CallableDeserializationContextFactory;
18
use JMS\Serializer\ContextFactory\CallableSerializationContextFactory;
19
use JMS\Serializer\ContextFactory\DeserializationContextFactoryInterface;
20
use JMS\Serializer\ContextFactory\SerializationContextFactoryInterface;
21
use JMS\Serializer\EventDispatcher\EventDispatcher;
22
use JMS\Serializer\EventDispatcher\EventDispatcherInterface;
23
use JMS\Serializer\EventDispatcher\Subscriber\DoctrineProxySubscriber;
24
use JMS\Serializer\Exception\InvalidArgumentException;
25
use JMS\Serializer\Exception\RuntimeException;
26
use JMS\Serializer\Expression\CompilableExpressionEvaluatorInterface;
27
use JMS\Serializer\Expression\ExpressionEvaluatorInterface;
28
use JMS\Serializer\GraphNavigator\Factory\DeserializationGraphNavigatorFactory;
29
use JMS\Serializer\GraphNavigator\Factory\GraphNavigatorFactoryInterface;
30
use JMS\Serializer\GraphNavigator\Factory\SerializationGraphNavigatorFactory;
31
use JMS\Serializer\Handler\ArrayCollectionHandler;
32
use JMS\Serializer\Handler\DateHandler;
33
use JMS\Serializer\Handler\HandlerRegistry;
34
use JMS\Serializer\Handler\HandlerRegistryInterface;
35
use JMS\Serializer\Handler\IteratorHandler;
36
use JMS\Serializer\Handler\StdClassHandler;
37
use JMS\Serializer\Metadata\Driver\DocBlockDriverFactory;
38
use JMS\Serializer\Naming\CamelCaseNamingStrategy;
39
use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
40
use JMS\Serializer\Naming\SerializedNameAnnotationStrategy;
41
use JMS\Serializer\Type\Parser;
42
use JMS\Serializer\Type\ParserInterface;
43
use JMS\Serializer\Visitor\Factory\DeserializationVisitorFactory;
44
use JMS\Serializer\Visitor\Factory\JsonDeserializationVisitorFactory;
45
use JMS\Serializer\Visitor\Factory\JsonSerializationVisitorFactory;
46
use JMS\Serializer\Visitor\Factory\SerializationVisitorFactory;
47
use JMS\Serializer\Visitor\Factory\XmlDeserializationVisitorFactory;
48
use JMS\Serializer\Visitor\Factory\XmlSerializationVisitorFactory;
49
use Metadata\Cache\CacheInterface;
50
use Metadata\Cache\FileCache;
51
use Metadata\MetadataFactory;
52
use Metadata\MetadataFactoryInterface;
53
54
/**
55
 * Builder for serializer instances.
56
 *
57
 * This object makes serializer construction a breeze for projects that do not use
58
 * any special dependency injection container.
59
 *
60
 * @author Johannes M. Schmitt <[email protected]>
61
 */
62
final class SerializerBuilder
63
{
64
    /**
65
     * @var string[]
66
     */
67
    private $metadataDirs = [];
68
69
    /**
70
     * @var HandlerRegistryInterface
71
     */
72
    private $handlerRegistry;
73
74
    /**
75
     * @var bool
76
     */
77
    private $handlersConfigured = false;
78
79
    /**
80
     * @var EventDispatcherInterface
81
     */
82
    private $eventDispatcher;
83
84
    /**
85
     * @var bool
86
     */
87
    private $listenersConfigured = false;
88
89
    /**
90
     * @var ObjectConstructorInterface
91
     */
92
    private $objectConstructor;
93
94
    /**
95
     * @var SerializationVisitorFactory[]
96 330
     */
97
    private $serializationVisitors;
98 330
99
    /**
100
     * @var DeserializationVisitorFactory[]
101 330
     */
102
    private $deserializationVisitors;
103 330
104 330
    /**
105 330
     * @var bool
106 330
     */
107 330
    private $visitorsAdded = false;
108
109 330
    /**
110 284
     * @var PropertyNamingStrategyInterface
111
     */
112 330
    private $propertyNamingStrategy;
113 284
114
    /**
115 330
     * @var bool
116
     */
117
    private $debug = false;
118
119
    /**
120
     * @var string
121
     */
122
    private $cacheDir;
123 330
124
    /**
125 330
     * @var AnnotationReader
126 330
     */
127
    private $annotationReader;
128 330
129
    /**
130
     * @var bool
131 25
     */
132
    private $includeInterfaceMetadata = false;
133 25
134
    /**
135 25
     * @var DriverFactoryInterface
136
     */
137
    private $driverFactory;
138 1
139
    /**
140 1
     * @var SerializationContextFactoryInterface
141
     */
142 1
    private $serializationContextFactory;
143
144
    /**
145
     * @var DeserializationContextFactoryInterface
146
     */
147
    private $deserializationContextFactory;
148
149
    /**
150
     * @var ParserInterface
151
     */
152
    private $typeParser;
153
154
    /**
155
     * @var ExpressionEvaluatorInterface
156
     */
157
    private $expressionEvaluator;
158
159 1
    /**
160
     * @var AccessorStrategyInterface
161 1
     */
162 1
    private $accessorStrategy;
163
164 1
    /**
165
     * @var CacheInterface
166
     */
167
    private $metadataCache;
168 1
169
    /**
170 1
     * @var bool
171
     */
172
    private $docBlockTyperResolver;
173 71
174
    /**
175 71
     * @param mixed ...$args
176 71
     *
177 71
     * @return SerializerBuilder
178 71
     */
179
    public static function create(...$args): self
180 71
    {
181
        return new static(...$args);
0 ignored issues
show
Bug introduced by
$args is expanded, but the parameter $handlerRegistry of JMS\Serializer\SerializerBuilder::__construct() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

181
        return new static(/** @scrutinizer ignore-type */ ...$args);
Loading history...
182
    }
183 3
184
    public function __construct(?HandlerRegistryInterface $handlerRegistry = null, ?EventDispatcherInterface $eventDispatcher = null)
185 3
    {
186 3
        $this->typeParser = new Parser();
187
        $this->handlerRegistry = $handlerRegistry ?: new HandlerRegistry();
188 3
        $this->eventDispatcher = $eventDispatcher ?: new EventDispatcher();
189
        $this->serializationVisitors = [];
190
        $this->deserializationVisitors = [];
191 73
192
        if ($handlerRegistry) {
193 73
            $this->handlersConfigured = true;
194 73
        }
195
196 73
        if ($eventDispatcher) {
197
            $this->listenersConfigured = true;
198
        }
199 1
    }
200
201 1
    public function setAccessorStrategy(AccessorStrategyInterface $accessorStrategy): self
202 1
    {
203
        $this->accessorStrategy = $accessorStrategy;
204 1
205
        return $this;
206
    }
207 4
208
    private function getAccessorStrategy(): AccessorStrategyInterface
209 4
    {
210
        if (!$this->accessorStrategy) {
211 4
            $this->accessorStrategy = new DefaultAccessorStrategy($this->expressionEvaluator);
212
        }
213
214
        return $this->accessorStrategy;
215
    }
216
217
    public function setExpressionEvaluator(ExpressionEvaluatorInterface $expressionEvaluator): self
218
    {
219
        $this->expressionEvaluator = $expressionEvaluator;
220
221 2
        return $this;
222
    }
223 2
224 2
    public function setTypeParser(ParserInterface $parser): self
225
    {
226 2
        $this->typeParser = $parser;
227
228
        return $this;
229
    }
230
231
    public function setAnnotationReader(Reader $reader): self
232
    {
233
        $this->annotationReader = $reader;
0 ignored issues
show
Documentation Bug introduced by
$reader is of type Doctrine\Common\Annotations\Reader, but the property $annotationReader was declared to be of type Doctrine\Common\Annotations\AnnotationReader. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
234
235
        return $this;
236
    }
237 329
238
    public function setDebug(bool $bool): self
239 329
    {
240 329
        $this->debug = $bool;
241 329
242 329
        return $this;
243
    }
244
245 329
    public function setCacheDir(string $dir): self
246
    {
247
        if (!is_dir($dir)) {
248 329
            $this->createDir($dir);
249
        }
250 329
251 329
        if (!is_writable($dir)) {
252 329
            throw new InvalidArgumentException(sprintf('The cache directory "%s" is not writable.', $dir));
253 329
        }
254
255
        $this->cacheDir = $dir;
256 329
257
        return $this;
258
    }
259
260
    public function addDefaultHandlers(): self
261
    {
262
        $this->handlersConfigured = true;
263
        $this->handlerRegistry->registerSubscribingHandler(new DateHandler());
264 1
        $this->handlerRegistry->registerSubscribingHandler(new StdClassHandler());
265
        $this->handlerRegistry->registerSubscribingHandler(new ArrayCollectionHandler());
266 1
        $this->handlerRegistry->registerSubscribingHandler(new IteratorHandler());
267
268 1
        return $this;
269
    }
270
271
    public function configureHandlers(\Closure $closure): self
272
    {
273
        $this->handlersConfigured = true;
274
        $closure($this->handlerRegistry);
275
276
        return $this;
277
    }
278
279
    public function addDefaultListeners(): self
280
    {
281
        $this->listenersConfigured = true;
282 2
        $this->eventDispatcher->addSubscriber(new DoctrineProxySubscriber());
283
284 2
        return $this;
285 2
    }
286 2
287
    public function configureListeners(\Closure $closure): self
288
    {
289
        $this->listenersConfigured = true;
290 2
        $closure($this->eventDispatcher);
291
292 2
        return $this;
293
    }
294
295
    public function setObjectConstructor(ObjectConstructorInterface $constructor): self
296
    {
297
        $this->objectConstructor = $constructor;
298
299
        return $this;
300
    }
301
302
    public function setPropertyNamingStrategy(PropertyNamingStrategyInterface $propertyNamingStrategy): self
303
    {
304
        $this->propertyNamingStrategy = $propertyNamingStrategy;
305
306
        return $this;
307
    }
308
309
    public function setSerializationVisitor(string $format, SerializationVisitorFactory $visitor): self
310
    {
311
        $this->visitorsAdded = true;
312
        $this->serializationVisitors[$format] = $visitor;
313
314
        return $this;
315
    }
316
317
    public function setDeserializationVisitor(string $format, DeserializationVisitorFactory $visitor): self
318
    {
319
        $this->visitorsAdded = true;
320
        $this->deserializationVisitors[$format] = $visitor;
321
322
        return $this;
323
    }
324
325
    public function addDefaultSerializationVisitors(): self
326
    {
327
        $this->visitorsAdded = true;
328
        $this->serializationVisitors = [
329
            'xml' => new XmlSerializationVisitorFactory(),
330
            'json' => new JsonSerializationVisitorFactory(),
331
        ];
332
333
        return $this;
334
    }
335
336
    public function addDefaultDeserializationVisitors(): self
337
    {
338
        $this->visitorsAdded = true;
339
        $this->deserializationVisitors = [
340
            'xml' => new XmlDeserializationVisitorFactory(),
341
            'json' => new JsonDeserializationVisitorFactory(),
342
        ];
343
344
        return $this;
345
    }
346
347
    /**
348
     * @param bool $include Whether to include the metadata from the interfaces
349
     *
350
     * @return SerializerBuilder
351
     */
352
    public function includeInterfaceMetadata(bool $include): self
353
    {
354
        $this->includeInterfaceMetadata = $include;
355
356
        return $this;
357
    }
358
359
    /**
360
     * Sets a map of namespace prefixes to directories.
361
     *
362
     * This method overrides any previously defined directories.
363
     *
364
     * @param array <string,string> $namespacePrefixToDirMap
0 ignored issues
show
Documentation Bug introduced by
The doc comment <string,string> at position 0 could not be parsed: Unknown type name '<' at position 0 in <string,string>.
Loading history...
365
     *
366
     * @return SerializerBuilder
367
     *
368
     * @throws InvalidArgumentException When a directory does not exist.
369
     */
370
    public function setMetadataDirs(array $namespacePrefixToDirMap): self
371
    {
372
        foreach ($namespacePrefixToDirMap as $dir) {
373
            if (!is_dir($dir)) {
374
                throw new InvalidArgumentException(sprintf('The directory "%s" does not exist.', $dir));
375
            }
376
        }
377
378 13
        $this->metadataDirs = $namespacePrefixToDirMap;
379
380 13
        return $this;
381
    }
382 13
383
    /**
384
     * Adds a directory where the serializer will look for class metadata.
385
     *
386
     * The namespace prefix will make the names of the actual metadata files a bit shorter. For example, let's assume
387
     * that you have a directory where you only store metadata files for the ``MyApplication\Entity`` namespace.
388
     *
389
     * If you use an empty prefix, your metadata files would need to look like:
390 5
     *
391
     * ``my-dir/MyApplication.Entity.SomeObject.yml``
392 5
     * ``my-dir/MyApplication.Entity.OtherObject.xml``
393 3
     *
394 2
     * If you use ``MyApplication\Entity`` as prefix, your metadata files would need to look like:
395 2
     *
396 2
     * ``my-dir/SomeObject.yml``
397
     * ``my-dir/OtherObject.yml``
398
     *
399
     * Please keep in mind that you currently may only have one directory per namespace prefix.
400
     *
401
     * @param string $dir             The directory where metadata files are located.
402 5
     * @param string $namespacePrefix An optional prefix if you only store metadata for specific namespaces in this directory.
403
     *
404
     * @return SerializerBuilder
405
     *
406
     * @throws InvalidArgumentException When a directory does not exist.
407
     * @throws InvalidArgumentException When a directory has already been registered.
408
     */
409
    public function addMetadataDir(string $dir, string $namespacePrefix = ''): self
410 3
    {
411
        if (!is_dir($dir)) {
412 3
            throw new InvalidArgumentException(sprintf('The directory "%s" does not exist.', $dir));
413 3
        }
414
415
        if (isset($this->metadataDirs[$namespacePrefix])) {
416
            throw new InvalidArgumentException(sprintf('There is already a directory configured for the namespace prefix "%s". Please use replaceMetadataDir() to override directories.', $namespacePrefix));
417
        }
418
419
        $this->metadataDirs[$namespacePrefix] = $dir;
420
421
        return $this;
422 3
    }
423
424
    /**
425
     * Adds a map of namespace prefixes to directories.
426
     *
427
     * @param array <string,string> $namespacePrefixToDirMap
0 ignored issues
show
Documentation Bug introduced by
The doc comment <string,string> at position 0 could not be parsed: Unknown type name '<' at position 0 in <string,string>.
Loading history...
428
     *
429
     * @return SerializerBuilder
430
     */
431 330
    public function addMetadataDirs(array $namespacePrefixToDirMap): self
432
    {
433 330
        foreach ($namespacePrefixToDirMap as $prefix => $dir) {
434 330
            $this->addMetadataDir($dir, $prefix);
435 330
        }
436
437 330
        return $this;
438 1
    }
439 1
440 1
    /**
441
     * Similar to addMetadataDir(), but overrides an existing entry.
442
     *
443
     * @return SerializerBuilder
444 330
     *
445 318
     * @throws InvalidArgumentException When a directory does not exist.
446 318
     * @throws InvalidArgumentException When no directory is configured for the ns prefix.
447
     */
448
    public function replaceMetadataDir(string $dir, string $namespacePrefix = ''): self
449 330
    {
450 330
        if (!is_dir($dir)) {
451
            throw new InvalidArgumentException(sprintf('The directory "%s" does not exist.', $dir));
452 330
        }
453
454 330
        if (!isset($this->metadataDirs[$namespacePrefix])) {
455
            throw new InvalidArgumentException(sprintf('There is no directory configured for namespace prefix "%s". Please use addMetadataDir() for adding new directories.', $namespacePrefix));
456 330
        }
457 1
458 1
        $this->metadataDirs[$namespacePrefix] = $dir;
459
460
        return $this;
461 330
    }
462 71
463
    public function setMetadataDriverFactory(DriverFactoryInterface $driverFactory): self
464
    {
465 330
        $this->driverFactory = $driverFactory;
466 73
467
        return $this;
468
    }
469 330
470 329
    /**
471 329
     * @param SerializationContextFactoryInterface|callable $serializationContextFactory
472
     */
473
    public function setSerializationContextFactory($serializationContextFactory): self
474 330
    {
475 330
        if ($serializationContextFactory instanceof SerializationContextFactoryInterface) {
476
            $this->serializationContextFactory = $serializationContextFactory;
477
        } elseif (is_callable($serializationContextFactory)) {
478 330
            $this->serializationContextFactory = new CallableSerializationContextFactory(
479 330
                $serializationContextFactory
480 330
            );
481 330
        } else {
482 330
            throw new InvalidArgumentException('expected SerializationContextFactoryInterface or callable.');
483 330
        }
484 330
485 330
        return $this;
486
    }
487
488 330
    /**
489
     * @param DeserializationContextFactoryInterface|callable $deserializationContextFactory
490
     */
491 330
    public function setDeserializationContextFactory($deserializationContextFactory): self
492
    {
493 330
        if ($deserializationContextFactory instanceof DeserializationContextFactoryInterface) {
494 330
            $this->deserializationContextFactory = $deserializationContextFactory;
495 330
        } elseif (is_callable($deserializationContextFactory)) {
496 330
            $this->deserializationContextFactory = new CallableDeserializationContextFactory(
497 330
                $deserializationContextFactory
498 330
            );
499
        } else {
500
            throw new InvalidArgumentException('expected DeserializationContextFactoryInterface or callable.');
501
        }
502 330
503
        return $this;
504 330
    }
505 330
506 330
    public function setMetadataCache(CacheInterface $cache): self
507 330
    {
508 330
        $this->metadataCache = $cache;
509 330
510 330
        return $this;
511
    }
512
513
    public function setDocBlockTypeResolver(bool $docBlockTypeResolver): self
514 318
    {
515
        $this->docBlockTyperResolver = $docBlockTypeResolver;
516 318
517
        return $this;
518
    }
519
520 318
    public function build(): Serializer
521 318
    {
522
        $annotationReader = $this->annotationReader;
523 1
        if (null === $annotationReader) {
524
            $annotationReader = new AnnotationReader();
525 1
526
            if (null !== $this->cacheDir) {
527
                $this->createDir($this->cacheDir . '/annotations');
528
                $annotationsCache = new FilesystemCache($this->cacheDir . '/annotations');
529 1
                $annotationReader = new CachedReader($annotationReader, $annotationsCache, $this->debug);
530
            }
531
        }
532 1
533
        if (null === $this->driverFactory) {
534
            $this->initializePropertyNamingStrategy();
535
            $this->driverFactory = new DefaultDriverFactory(
536
                $this->propertyNamingStrategy,
537
                $this->typeParser,
538
                $this->expressionEvaluator instanceof CompilableExpressionEvaluatorInterface ? $this->expressionEvaluator : null
539
            );
540
        }
541
542
        if ($this->docBlockTyperResolver) {
543
            $this->driverFactory = new DocBlockDriverFactory($this->driverFactory, $this->typeParser);
544
        }
545
546
        $metadataDriver = $this->driverFactory->createDriver($this->metadataDirs, $annotationReader);
547
        $metadataFactory = new MetadataFactory($metadataDriver, null, $this->debug);
548
549
        $metadataFactory->setIncludeInterfaces($this->includeInterfaceMetadata);
550
551
        if (null !== $this->metadataCache) {
552
            $metadataFactory->setCache($this->metadataCache);
553
        } elseif (null !== $this->cacheDir) {
554
            $this->createDir($this->cacheDir . '/metadata');
555
            $metadataFactory->setCache(new FileCache($this->cacheDir . '/metadata'));
556
        }
557
558
        if (!$this->handlersConfigured) {
559
            $this->addDefaultHandlers();
560
        }
561
562
        if (!$this->listenersConfigured) {
563
            $this->addDefaultListeners();
564
        }
565
566
        if (!$this->visitorsAdded) {
567
            $this->addDefaultSerializationVisitors();
568
            $this->addDefaultDeserializationVisitors();
569
        }
570
571
        $navigatorFactories = [
572
            GraphNavigatorInterface::DIRECTION_SERIALIZATION => $this->getSerializationNavigatorFactory($metadataFactory),
573
            GraphNavigatorInterface::DIRECTION_DESERIALIZATION => $this->getDeserializationNavigatorFactory($metadataFactory),
574
        ];
575
576
        return new Serializer(
577
            $metadataFactory,
578
            $navigatorFactories,
579
            $this->serializationVisitors,
580
            $this->deserializationVisitors,
581
            $this->serializationContextFactory,
582
            $this->deserializationContextFactory,
583
            $this->typeParser
584
        );
585
    }
586
587
    private function getSerializationNavigatorFactory(MetadataFactoryInterface $metadataFactory): GraphNavigatorFactoryInterface
588
    {
589
        return new SerializationGraphNavigatorFactory(
590
            $metadataFactory,
591
            $this->handlerRegistry,
592
            $this->getAccessorStrategy(),
593
            $this->eventDispatcher,
594
            $this->expressionEvaluator
595
        );
596
    }
597
598
    private function getDeserializationNavigatorFactory(MetadataFactoryInterface $metadataFactory): GraphNavigatorFactoryInterface
599
    {
600
        return new DeserializationGraphNavigatorFactory(
601
            $metadataFactory,
602
            $this->handlerRegistry,
603
            $this->objectConstructor ?: new UnserializeObjectConstructor(),
604
            $this->getAccessorStrategy(),
605
            $this->eventDispatcher,
606
            $this->expressionEvaluator
607
        );
608
    }
609
610
    private function initializePropertyNamingStrategy(): void
611
    {
612
        if (null !== $this->propertyNamingStrategy) {
613
            return;
614
        }
615
616
        $this->propertyNamingStrategy = new SerializedNameAnnotationStrategy(new CamelCaseNamingStrategy());
617
    }
618
619
    private function createDir(string $dir): void
620
    {
621
        if (is_dir($dir)) {
622
            return;
623
        }
624
625
        if (false === @mkdir($dir, 0777, true) && false === is_dir($dir)) {
626
            throw new RuntimeException(sprintf('Could not create directory "%s".', $dir));
627
        }
628
    }
629
}
630