Completed
Pull Request — master (#929)
by Asmir
02:40
created

getSerializationNavigatorFactory()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 1
dl 0
loc 8
ccs 7
cts 7
cp 1
crap 1
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * Copyright 2016 Johannes M. Schmitt <[email protected]>
7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this file except in compliance with the License.
10
 * You may obtain a copy of the License at
11
 *
12
 *     http://www.apache.org/licenses/LICENSE-2.0
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 */
20
21
namespace JMS\Serializer;
22
23
use Doctrine\Common\Annotations\AnnotationReader;
24
use Doctrine\Common\Annotations\CachedReader;
25
use Doctrine\Common\Annotations\Reader;
26
use Doctrine\Common\Cache\FilesystemCache;
27
use JMS\Parser\AbstractParser;
28
use JMS\Serializer\Accessor\AccessorStrategyInterface;
29
use JMS\Serializer\Accessor\DefaultAccessorStrategy;
30
use JMS\Serializer\Accessor\ExpressionAccessorStrategy;
31
use JMS\Serializer\Builder\DefaultDriverFactory;
32
use JMS\Serializer\Builder\DriverFactoryInterface;
33
use JMS\Serializer\Construction\ObjectConstructorInterface;
34
use JMS\Serializer\Construction\UnserializeObjectConstructor;
35
use JMS\Serializer\ContextFactory\CallableDeserializationContextFactory;
36
use JMS\Serializer\ContextFactory\CallableSerializationContextFactory;
37
use JMS\Serializer\ContextFactory\DeserializationContextFactoryInterface;
38
use JMS\Serializer\ContextFactory\SerializationContextFactoryInterface;
39
use JMS\Serializer\EventDispatcher\EventDispatcher;
40
use JMS\Serializer\EventDispatcher\EventDispatcherInterface;
41
use JMS\Serializer\EventDispatcher\Subscriber\DoctrineProxySubscriber;
42
use JMS\Serializer\Exception\InvalidArgumentException;
43
use JMS\Serializer\Exception\RuntimeException;
44
use JMS\Serializer\Expression\ExpressionEvaluatorInterface;
45
use JMS\Serializer\GraphNavigator\Factory\DeserializationGraphNavigatorFactory;
46
use JMS\Serializer\GraphNavigator\Factory\GraphNavigatorFactoryInterface;
47
use JMS\Serializer\GraphNavigator\Factory\SerializationGraphNavigatorFactory;
48
use JMS\Serializer\Handler\ArrayCollectionHandler;
49
use JMS\Serializer\Handler\DateHandler;
50
use JMS\Serializer\Handler\HandlerRegistry;
51
use JMS\Serializer\Handler\HandlerRegistryInterface;
52
use JMS\Serializer\Handler\StdClassHandler;
53
use JMS\Serializer\Naming\CamelCaseNamingStrategy;
54
use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
55
use JMS\Serializer\Naming\SerializedNameAnnotationStrategy;
56
use JMS\Serializer\VisitorFactory\DeserializationVisitorFactory;
57
use JMS\Serializer\VisitorFactory\JsonDeserializationVisitorFactory;
58
use JMS\Serializer\VisitorFactory\JsonSerializationVisitorFactory;
59
use JMS\Serializer\VisitorFactory\SerializationVisitorFactory;
60
use JMS\Serializer\VisitorFactory\XmlDeserializationVisitorFactory;
61
use JMS\Serializer\VisitorFactory\XmlSerializationVisitorFactory;
62
use Metadata\Cache\FileCache;
63
use Metadata\MetadataFactory;
64
use Metadata\MetadataFactoryInterface;
65
66
/**
67
 * Builder for serializer instances.
68
 *
69
 * This object makes serializer construction a breeze for projects that do not use
70
 * any special dependency injection container.
71
 *
72
 * @author Johannes M. Schmitt <[email protected]>
73
 */
74
class SerializerBuilder
75
{
76
    private $metadataDirs = [];
77
    private $handlerRegistry;
78
    private $handlersConfigured = false;
79
    private $eventDispatcher;
80
    private $listenersConfigured = false;
81
    private $objectConstructor;
82
    private $serializationVisitors;
83
    private $deserializationVisitors;
84
    private $visitorsAdded = false;
85
    private $propertyNamingStrategy;
86
    private $debug = false;
87
    private $cacheDir;
88
    private $annotationReader;
89
    private $includeInterfaceMetadata = false;
90
    private $driverFactory;
91
    private $serializationContextFactory;
92
    private $deserializationContextFactory;
93
    private $typeParser;
94
95
    /**
96
     * @var ExpressionEvaluatorInterface
97
     */
98
    private $expressionEvaluator;
99
100
    /**
101
     * @var AccessorStrategyInterface
102
     */
103
    private $accessorStrategy;
104
105 315
    public static function create(...$args)
106
    {
107 315
        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

107
        return new static(/** @scrutinizer ignore-type */ ...$args);
Loading history...
108
    }
109
110 315
    public function __construct(HandlerRegistryInterface $handlerRegistry = null, EventDispatcherInterface $eventDispatcher = null)
111
    {
112 315
        $this->typeParser = new TypeParser();
113 315
        $this->handlerRegistry = $handlerRegistry ?: new HandlerRegistry();
114 315
        $this->eventDispatcher = $eventDispatcher ?: new EventDispatcher();
115 315
        $this->serializationVisitors = [];
116 315
        $this->deserializationVisitors = [];
117
118 315
        if ($handlerRegistry) {
119 270
            $this->handlersConfigured = true;
120
        }
121 315
        if ($eventDispatcher) {
122 270
            $this->listenersConfigured = true;
123
        }
124 315
    }
125
126
    public function setAccessorStrategy(AccessorStrategyInterface $accessorStrategy)
127
    {
128
        $this->accessorStrategy = $accessorStrategy;
129
    }
130
131 315
    private function getAccessorStrategy()
132
    {
133 315
        if (!$this->accessorStrategy) {
134 315
            $this->accessorStrategy = new DefaultAccessorStrategy();
135
136 315
            if ($this->expressionEvaluator) {
137 23
                $this->accessorStrategy = new ExpressionAccessorStrategy($this->expressionEvaluator, $this->accessorStrategy);
138
            }
139
        }
140 315
        return $this->accessorStrategy;
141
    }
142
143 23
    public function setExpressionEvaluator(ExpressionEvaluatorInterface $expressionEvaluator)
144
    {
145 23
        $this->expressionEvaluator = $expressionEvaluator;
146
147 23
        return $this;
148
    }
149
150 1
    public function setTypeParser(AbstractParser $parser)
151
    {
152 1
        $this->typeParser = $parser;
153
154 1
        return $this;
155
    }
156
157
    public function setAnnotationReader(Reader $reader)
158
    {
159
        $this->annotationReader = $reader;
160
161
        return $this;
162
    }
163
164
    public function setDebug($bool)
165
    {
166
        $this->debug = (boolean)$bool;
167
168
        return $this;
169
    }
170
171 1
    public function setCacheDir($dir)
172
    {
173 1
        if (!is_dir($dir)) {
174 1
            $this->createDir($dir);
175
        }
176 1
        if (!is_writable($dir)) {
177
            throw new InvalidArgumentException(sprintf('The cache directory "%s" is not writable.', $dir));
178
        }
179
180 1
        $this->cacheDir = $dir;
181
182 1
        return $this;
183
    }
184
185 68
    public function addDefaultHandlers()
186
    {
187 68
        $this->handlersConfigured = true;
188 68
        $this->handlerRegistry->registerSubscribingHandler(new DateHandler());
189 68
        $this->handlerRegistry->registerSubscribingHandler(new StdClassHandler());
190 68
        $this->handlerRegistry->registerSubscribingHandler(new ArrayCollectionHandler());
191
192 68
        return $this;
193
    }
194
195 3
    public function configureHandlers(\Closure $closure)
196
    {
197 3
        $this->handlersConfigured = true;
198 3
        $closure($this->handlerRegistry);
199
200 3
        return $this;
201
    }
202
203 70
    public function addDefaultListeners()
204
    {
205 70
        $this->listenersConfigured = true;
206 70
        $this->eventDispatcher->addSubscriber(new DoctrineProxySubscriber());
207
208 70
        return $this;
209
    }
210
211 1
    public function configureListeners(\Closure $closure)
212
    {
213 1
        $this->listenersConfigured = true;
214 1
        $closure($this->eventDispatcher);
215
216 1
        return $this;
217
    }
218
219 3
    public function setObjectConstructor(ObjectConstructorInterface $constructor)
220
    {
221 3
        $this->objectConstructor = $constructor;
222
223 3
        return $this;
224
    }
225
226
    public function setPropertyNamingStrategy(PropertyNamingStrategyInterface $propertyNamingStrategy)
227
    {
228
        $this->propertyNamingStrategy = $propertyNamingStrategy;
229
230
        return $this;
231
    }
232
233 2
    public function setSerializationVisitor($format, SerializationVisitorFactory $visitor)
234
    {
235 2
        $this->visitorsAdded = true;
236 2
        $this->serializationVisitors[$format] = $visitor;
237
238 2
        return $this;
239
    }
240
241
    public function setDeserializationVisitor($format, DeserializationVisitorFactory $visitor)
242
    {
243
        $this->visitorsAdded = true;
244
        $this->deserializationVisitors[$format] = $visitor;
245
246
        return $this;
247
    }
248
249 314
    public function addDefaultSerializationVisitors()
250
    {
251 314
        $this->visitorsAdded = true;
252 314
        $this->serializationVisitors = [
253 314
            'xml' => new XmlSerializationVisitorFactory(),
254 314
            'json' => new JsonSerializationVisitorFactory(),
255
        ];
256
257 314
        return $this;
258
    }
259
260 314
    public function addDefaultDeserializationVisitors()
261
    {
262 314
        $this->visitorsAdded = true;
263 314
        $this->deserializationVisitors = [
264 314
            'xml' => new XmlDeserializationVisitorFactory(),
265 314
            'json' => new JsonDeserializationVisitorFactory(),
266
        ];
267
268 314
        return $this;
269
    }
270
271
    /**
272
     * @param Boolean $include Whether to include the metadata from the interfaces
273
     *
274
     * @return SerializerBuilder
275
     */
276 1
    public function includeInterfaceMetadata($include)
277
    {
278 1
        $this->includeInterfaceMetadata = (Boolean)$include;
279
280 1
        return $this;
281
    }
282
283
    /**
284
     * Sets a map of namespace prefixes to directories.
285
     *
286
     * This method overrides any previously defined directories.
287
     *
288
     * @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...
289
     *
290
     * @return SerializerBuilder
291
     *
292
     * @throws InvalidArgumentException When a directory does not exist
293
     */
294
    public function setMetadataDirs(array $namespacePrefixToDirMap)
295
    {
296
        foreach ($namespacePrefixToDirMap as $dir) {
297
            if (!is_dir($dir)) {
298
                throw new InvalidArgumentException(sprintf('The directory "%s" does not exist.', $dir));
299
            }
300
        }
301
302
        $this->metadataDirs = $namespacePrefixToDirMap;
303
304
        return $this;
305
    }
306
307
    /**
308
     * Adds a directory where the serializer will look for class metadata.
309
     *
310
     * The namespace prefix will make the names of the actual metadata files a bit shorter. For example, let's assume
311
     * that you have a directory where you only store metadata files for the ``MyApplication\Entity`` namespace.
312
     *
313
     * If you use an empty prefix, your metadata files would need to look like:
314
     *
315
     * ``my-dir/MyApplication.Entity.SomeObject.yml``
316
     * ``my-dir/MyApplication.Entity.OtherObject.xml``
317
     *
318
     * If you use ``MyApplication\Entity`` as prefix, your metadata files would need to look like:
319
     *
320
     * ``my-dir/SomeObject.yml``
321
     * ``my-dir/OtherObject.yml``
322
     *
323
     * Please keep in mind that you currently may only have one directory per namespace prefix.
324
     *
325
     * @param string $dir The directory where metadata files are located.
326
     * @param string $namespacePrefix An optional prefix if you only store metadata for specific namespaces in this directory.
327
     *
328
     * @return SerializerBuilder
329
     *
330
     * @throws InvalidArgumentException When a directory does not exist
331
     * @throws InvalidArgumentException When a directory has already been registered
332
     */
333
    public function addMetadataDir($dir, $namespacePrefix = '')
334
    {
335
        if (!is_dir($dir)) {
336
            throw new InvalidArgumentException(sprintf('The directory "%s" does not exist.', $dir));
337
        }
338
339
        if (isset($this->metadataDirs[$namespacePrefix])) {
340
            throw new InvalidArgumentException(sprintf('There is already a directory configured for the namespace prefix "%s". Please use replaceMetadataDir() to override directories.', $namespacePrefix));
341
        }
342
343
        $this->metadataDirs[$namespacePrefix] = $dir;
344
345
        return $this;
346
    }
347
348
    /**
349
     * Adds a map of namespace prefixes to directories.
350
     *
351
     * @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...
352
     *
353
     * @return SerializerBuilder
354
     */
355
    public function addMetadataDirs(array $namespacePrefixToDirMap)
356
    {
357
        foreach ($namespacePrefixToDirMap as $prefix => $dir) {
358
            $this->addMetadataDir($dir, $prefix);
359
        }
360
361
        return $this;
362
    }
363
364
    /**
365
     * Similar to addMetadataDir(), but overrides an existing entry.
366
     *
367
     * @param string $dir
368
     * @param string $namespacePrefix
369
     *
370
     * @return SerializerBuilder
371
     *
372
     * @throws InvalidArgumentException When a directory does not exist
373
     * @throws InvalidArgumentException When no directory is configured for the ns prefix
374
     */
375
    public function replaceMetadataDir($dir, $namespacePrefix = '')
376
    {
377
        if (!is_dir($dir)) {
378
            throw new InvalidArgumentException(sprintf('The directory "%s" does not exist.', $dir));
379
        }
380
381
        if (!isset($this->metadataDirs[$namespacePrefix])) {
382
            throw new InvalidArgumentException(sprintf('There is no directory configured for namespace prefix "%s". Please use addMetadataDir() for adding new directories.', $namespacePrefix));
383
        }
384
385
        $this->metadataDirs[$namespacePrefix] = $dir;
386
387
        return $this;
388
    }
389
390 12
    public function setMetadataDriverFactory(DriverFactoryInterface $driverFactory)
391
    {
392 12
        $this->driverFactory = $driverFactory;
393
394 12
        return $this;
395
    }
396
397
    /**
398
     * @param SerializationContextFactoryInterface|callable $serializationContextFactory
399
     *
400
     * @return self
401
     */
402 5
    public function setSerializationContextFactory($serializationContextFactory)
403
    {
404 5
        if ($serializationContextFactory instanceof SerializationContextFactoryInterface) {
405 3
            $this->serializationContextFactory = $serializationContextFactory;
406 2
        } elseif (is_callable($serializationContextFactory)) {
407 2
            $this->serializationContextFactory = new CallableSerializationContextFactory(
408 2
                $serializationContextFactory
409
            );
410
        } else {
411
            throw new InvalidArgumentException('expected SerializationContextFactoryInterface or callable.');
412
        }
413
414 5
        return $this;
415
    }
416
417
    /**
418
     * @param DeserializationContextFactoryInterface|callable $deserializationContextFactory
419
     *
420
     * @return self
421
     */
422 3
    public function setDeserializationContextFactory($deserializationContextFactory)
423
    {
424 3
        if ($deserializationContextFactory instanceof DeserializationContextFactoryInterface) {
425 3
            $this->deserializationContextFactory = $deserializationContextFactory;
426
        } elseif (is_callable($deserializationContextFactory)) {
427
            $this->deserializationContextFactory = new CallableDeserializationContextFactory(
428
                $deserializationContextFactory
429
            );
430
        } else {
431
            throw new InvalidArgumentException('expected DeserializationContextFactoryInterface or callable.');
432
        }
433
434 3
        return $this;
435
    }
436
437 315
    public function build()
438
    {
439 315
        $annotationReader = $this->annotationReader;
440 315
        if (null === $annotationReader) {
441 315
            $annotationReader = new AnnotationReader();
442
443 315
            if (null !== $this->cacheDir) {
444 1
                $this->createDir($this->cacheDir . '/annotations');
445 1
                $annotationsCache = new FilesystemCache($this->cacheDir . '/annotations');
446 1
                $annotationReader = new CachedReader($annotationReader, $annotationsCache, $this->debug);
447
            }
448
        }
449
450 315
        if (null === $this->driverFactory) {
451 303
            $this->initializePropertyNamingStrategy();
452 303
            $this->driverFactory = new DefaultDriverFactory($this->propertyNamingStrategy, $this->typeParser);
453
        }
454
455 315
        $metadataDriver = $this->driverFactory->createDriver($this->metadataDirs, $annotationReader);
456 315
        $metadataFactory = new MetadataFactory($metadataDriver, null, $this->debug);
457
458 315
        $metadataFactory->setIncludeInterfaces($this->includeInterfaceMetadata);
459
460 315
        if (null !== $this->cacheDir) {
461 1
            $this->createDir($this->cacheDir . '/metadata');
462 1
            $metadataFactory->setCache(new FileCache($this->cacheDir . '/metadata'));
463
        }
464
465 315
        if (!$this->handlersConfigured) {
466 68
            $this->addDefaultHandlers();
467
        }
468
469 315
        if (!$this->listenersConfigured) {
470 70
            $this->addDefaultListeners();
471
        }
472
473 315
        if (!$this->visitorsAdded) {
474 314
            $this->addDefaultSerializationVisitors();
475 314
            $this->addDefaultDeserializationVisitors();
476
        }
477
        $navigatorFactories = [
478 315
            GraphNavigatorInterface::DIRECTION_SERIALIZATION => $this->getSerializationNavigatorFactory($metadataFactory),
479 315
            GraphNavigatorInterface::DIRECTION_DESERIALIZATION => $this->getDeserializationNavigatorFactory($metadataFactory),
480
        ];
481
482 315
        $serializer = new Serializer(
483 315
            $metadataFactory,
484 315
            $navigatorFactories,
485 315
            $this->serializationVisitors,
486 315
            $this->deserializationVisitors,
487 315
            $this->serializationContextFactory,
488 315
            $this->deserializationContextFactory,
489 315
            $this->typeParser
490
        );
491
492 315
        return $serializer;
493
    }
494
495 315
    private function getSerializationNavigatorFactory(MetadataFactoryInterface $metadataFactory): GraphNavigatorFactoryInterface
496
    {
497 315
        return new SerializationGraphNavigatorFactory(
498 315
            $metadataFactory,
499 315
            $this->handlerRegistry,
500 315
            $this->getAccessorStrategy(),
501 315
            $this->eventDispatcher,
502 315
            $this->expressionEvaluator
503
        );
504
    }
505
506 315
    private function getDeserializationNavigatorFactory(MetadataFactoryInterface $metadataFactory): GraphNavigatorFactoryInterface
507
    {
508 315
        return new DeserializationGraphNavigatorFactory(
509 315
            $metadataFactory,
510 315
            $this->handlerRegistry,
511 315
            $this->objectConstructor ?: new UnserializeObjectConstructor(),
512 315
            $this->getAccessorStrategy(),
513 315
            $this->eventDispatcher,
514 315
            $this->expressionEvaluator
515
        );
516
    }
517
518 303
    private function initializePropertyNamingStrategy()
519
    {
520 303
        if (null !== $this->propertyNamingStrategy) {
521
            return;
522
        }
523
524 303
        $this->propertyNamingStrategy = new SerializedNameAnnotationStrategy(new CamelCaseNamingStrategy());
525 303
    }
526
527 1
    private function createDir($dir)
528
    {
529 1
        if (is_dir($dir)) {
530
            return;
531
        }
532
533 1
        if (false === @mkdir($dir, 0777, true) && false === is_dir($dir)) {
534
            throw new RuntimeException(sprintf('Could not create directory "%s".', $dir));
535
        }
536 1
    }
537
}
538
539