Completed
Pull Request — master (#888)
by Robert
03:06
created

SerializerBuilder   F

Complexity

Total Complexity 59

Size/Duplication

Total Lines 464
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 32

Test Coverage

Coverage 70.8%

Importance

Changes 0
Metric Value
wmc 59
lcom 1
cbo 32
dl 0
loc 464
ccs 131
cts 185
cp 0.708
rs 1.3043
c 0
b 0
f 0

31 Methods

Rating   Name   Duplication   Size   Complexity  
A create() 0 4 1
A __construct() 0 9 1
A setAccessorStrategy() 0 4 1
A getAccessorStrategy() 0 11 3
A setExpressionEvaluator() 0 6 1
A setAnnotationReader() 0 6 1
A setPropertyUpdater() 0 6 1
A setDebug() 0 6 1
A setCacheDir() 0 13 3
A addDefaultHandlers() 0 11 1
A configureHandlers() 0 7 1
A addDefaultListeners() 0 7 1
A configureListeners() 0 7 1
A setObjectConstructor() 0 6 1
A setPropertyNamingStrategy() 0 6 1
A setAdvancedNamingStrategy() 0 6 1
A setSerializationVisitor() 0 7 1
A setDeserializationVisitor() 0 7 1
A addDefaultSerializationVisitors() 0 13 1
A addDefaultDeserializationVisitors() 0 12 1
A includeInterfaceMetadata() 0 6 1
A setMetadataDirs() 0 12 3
A addMetadataDir() 0 14 3
A addMetadataDirs() 0 8 2
A replaceMetadataDir() 0 14 3
A setMetadataDriverFactory() 0 6 1
A setSerializationContextFactory() 0 14 3
A setDeserializationContextFactory() 0 14 3
C build() 0 61 10
A initializePropertyNamingStrategy() 0 8 2
A createDir() 0 10 4

How to fix   Complexity   

Complex Class

Complex classes like SerializerBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SerializerBuilder, and based on these observations, apply Extract Interface, too.

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