HateoasBuilder   F
last analyzed

Coupling/Cohesion

Components 1
Dependencies 33

Complexity

Total Complexity 48

Size/Duplication

Total Lines 472
Duplicated Lines 5.93 %

Importance

Changes 0
Metric Value
wmc 48
lcom 1
cbo 33
dl 28
loc 472
rs 3.2371
c 0
b 0
f 0

24 Methods

Rating   Name   Duplication   Size   Complexity  
A create() 0 4 1
A buildHateoas() 0 6 1
A __construct() 0 9 2
A setDefaultXmlSerializer() 0 4 1
A setUrlGenerator() 0 6 1
A setMetadataDirs() 0 12 3
A addMetadataDir() 14 14 3
A addMetadataDirs() 0 8 2
A replaceMetadataDir() 14 14 3
B build() 0 58 7
A setXmlSerializer() 0 6 1
A setJsonSerializer() 0 6 1
A setDefaultJsonSerializer() 0 4 1
A setExpressionContextVariable() 0 6 1
A setExpressionLanguage() 0 6 1
A registerExpressionFunction() 0 6 1
A addRelationProviderResolver() 0 6 1
A addConfigurationExtension() 0 6 1
A setDebug() 0 6 1
A setCacheDir() 0 14 3
A includeInterfaceMetadata() 0 6 1
B buildMetadataFactory() 0 35 5
A getExpressionLanguage() 0 8 2
A createDir() 0 10 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like HateoasBuilder 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 HateoasBuilder, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Hateoas;
4
5
use Doctrine\Common\Annotations\AnnotationReader;
6
use Doctrine\Common\Annotations\FileCacheReader;
7
use Hateoas\Configuration\Metadata\ConfigurationExtensionInterface;
8
use Hateoas\Configuration\Metadata\Driver\AnnotationDriver;
9
use Hateoas\Configuration\Metadata\Driver\ExtensionDriver;
10
use Hateoas\Configuration\Metadata\Driver\YamlDriver;
11
use Hateoas\Configuration\Metadata\Driver\XmlDriver;
12
use Hateoas\Configuration\Provider\Resolver\MethodResolver;
13
use Hateoas\Configuration\Provider\Resolver\ChainResolver;
14
use Hateoas\Configuration\Provider\RelationProvider;
15
use Hateoas\Configuration\Provider\Resolver\RelationProviderResolverInterface;
16
use Hateoas\Configuration\Provider\Resolver\StaticMethodResolver;
17
use Hateoas\Configuration\RelationsRepository;
18
use Hateoas\Expression\ExpressionEvaluator;
19
use Hateoas\Expression\ExpressionFunctionInterface;
20
use Hateoas\Expression\LinkExpressionFunction;
21
use Hateoas\Factory\EmbeddedsFactory;
22
use Hateoas\Factory\LinkFactory;
23
use Hateoas\Factory\LinksFactory;
24
use Hateoas\Helper\LinkHelper;
25
use Hateoas\UrlGenerator\UrlGeneratorInterface;
26
use Hateoas\UrlGenerator\UrlGeneratorRegistry;
27
use Hateoas\Serializer\EventSubscriber\JsonEventSubscriber;
28
use Hateoas\Serializer\EventSubscriber\XmlEventSubscriber;
29
use Hateoas\Serializer\ExclusionManager;
30
use Hateoas\Serializer\JsonHalSerializer;
31
use Hateoas\Serializer\JsonSerializerInterface;
32
use Hateoas\Serializer\JMSSerializerMetadataAwareInterface;
33
use Hateoas\Serializer\Metadata\InlineDeferrer;
34
use Hateoas\Serializer\XmlSerializer;
35
use Hateoas\Serializer\XmlSerializerInterface;
36
use JMS\Serializer\EventDispatcher\EventDispatcherInterface;
37
use JMS\Serializer\SerializerBuilder;
38
use Metadata\Cache\FileCache;
39
use Metadata\Driver\DriverChain;
40
use Metadata\Driver\FileLocator;
41
use Metadata\MetadataFactory;
42
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
43
44
/**
45
 * @author Adrien Brault <adrien.brault@gmail.com>
46
 *
47
 * Some code (metadata things) from this class has been taken from
48
 * https://github.com/schmittjoh/serializer/blob/a29f1e5083654ba2c126acd94ddb2287069b0b5b/src/JMS/Serializer/SerializerBuilder.php
49
 */
50
class HateoasBuilder
51
{
52
    /**
53
     * @var SerializerBuilder
54
     */
55
    private $serializerBuilder;
56
57
    /**
58
     * @var ExpressionLanguage
59
     */
60
    private $expressionLanguage;
61
62
    /**
63
     * @var array
64
     */
65
    private $contextVariables = array();
66
67
    /**
68
     * ExpressionFunctionInterface[]
69
     */
70
    private $expressionFunctions = array();
71
72
    /**
73
     * @var XmlSerializerInterface
74
     */
75
    private $xmlSerializer;
76
77
    /**
78
     * @var JsonSerializerInterface
79
     */
80
    private $jsonSerializer;
81
82
    /**
83
     * @var UrlGeneratorRegistry
84
     */
85
    private $urlGeneratorRegistry;
86
87
    private $configurationExtensions = array();
88
89
    private $chainResolver;
90
91
    private $metadataDirs = array();
92
93
    private $debug = false;
94
95
    private $cacheDir;
96
97
    private $annotationReader;
98
99
    private $includeInterfaceMetadata = false;
100
101
    /**
102
     * @param SerializerBuilder $serializerBuilder
103
     *
104
     * @return HateoasBuilder
105
     */
106
    public static function create(SerializerBuilder $serializerBuilder = null)
107
    {
108
        return new static($serializerBuilder);
109
    }
110
111
    /**
112
     * @return Hateoas
113
     */
114
    public static function buildHateoas()
115
    {
116
        $builder = static::create();
117
118
        return $builder->build();
119
    }
120
121
    public function __construct(SerializerBuilder $serializerBuilder = null)
122
    {
123
        $this->serializerBuilder    = $serializerBuilder ?: SerializerBuilder::create();
124
        $this->urlGeneratorRegistry = new UrlGeneratorRegistry();
125
        $this->chainResolver        = new ChainResolver(array(
126
            new MethodResolver(),
127
            new StaticMethodResolver(),
128
        ));
129
    }
130
131
    /**
132
     * Build a configured Hateoas instance.
133
     *
134
     * @return Hateoas
135
     */
136
    public function build()
137
    {
138
        $metadataFactory     = $this->buildMetadataFactory();
139
        $relationProvider    = new RelationProvider($metadataFactory, $this->chainResolver);
140
        $relationsRepository = new RelationsRepository($metadataFactory, $relationProvider);
141
        $expressionEvaluator = new ExpressionEvaluator($this->getExpressionLanguage(), $this->contextVariables);
142
        $linkFactory         = new LinkFactory($expressionEvaluator, $this->urlGeneratorRegistry);
143
        $exclusionManager    = new ExclusionManager($expressionEvaluator);
144
        $linksFactory        = new LinksFactory($relationsRepository, $linkFactory, $exclusionManager);
145
        $embeddedsFactory    = new EmbeddedsFactory($relationsRepository, $expressionEvaluator, $exclusionManager);
146
        $linkHelper          = new LinkHelper($linkFactory, $relationsRepository);
147
148
        // Register Hateoas core functions
149
        $expressionEvaluator->registerFunction(new LinkExpressionFunction($linkHelper));
150
151
        // Register user functions
152
        foreach ($this->expressionFunctions as $expressionFunction) {
153
            $expressionEvaluator->registerFunction($expressionFunction);
154
        }
155
156
        if (null === $this->xmlSerializer) {
157
            $this->setDefaultXmlSerializer();
158
        }
159
160
        if (null === $this->jsonSerializer) {
161
            $this->setDefaultJsonSerializer();
162
        }
163
164
        $inlineDeferrers  = array();
165
        $eventSubscribers = array(
166
            new XmlEventSubscriber($this->xmlSerializer, $linksFactory, $embeddedsFactory),
167
            new JsonEventSubscriber(
168
                $this->jsonSerializer,
169
                $linksFactory,
170
                $embeddedsFactory,
171
                $inlineDeferrers[] = new InlineDeferrer(),
172
                $inlineDeferrers[] = new InlineDeferrer()
173
            ),
174
        );
175
176
        $this->serializerBuilder
177
            ->addDefaultListeners()
178
            ->configureListeners(function (EventDispatcherInterface $dispatcher) use ($eventSubscribers) {
179
                foreach ($eventSubscribers as $eventSubscriber) {
180
                    $dispatcher->addSubscriber($eventSubscriber);
181
                }
182
            })
183
        ;
184
185
        $jmsSerializer = $this->serializerBuilder->build();
186
        foreach (array_merge($inlineDeferrers, array($this->jsonSerializer, $this->xmlSerializer)) as $serializer) {
187
            if ($serializer instanceof JMSSerializerMetadataAwareInterface) {
188
                $serializer->setMetadataFactory($jmsSerializer->getMetadataFactory());
189
            }
190
        }
191
192
        return new Hateoas($jmsSerializer, $linkHelper);
193
    }
194
195
    /**
196
     * @param XmlSerializerInterface $xmlSerializer
197
     *
198
     * @return HateoasBuilder
199
     */
200
    public function setXmlSerializer(XmlSerializerInterface $xmlSerializer)
201
    {
202
        $this->xmlSerializer = $xmlSerializer;
203
204
        return $this;
205
    }
206
207
    /**
208
     * Set the default XML serializer (`XmlSerializer`).
209
     *
210
     * @return HateoasBuilder
211
     */
212
    public function setDefaultXmlSerializer()
213
    {
214
        return $this->setXmlSerializer(new XmlSerializer());
215
    }
216
217
    /**
218
     * @param JsonSerializerInterface $jsonSerializer
219
     *
220
     * @return HateoasBuilder
221
     */
222
    public function setJsonSerializer(JsonSerializerInterface $jsonSerializer)
223
    {
224
        $this->jsonSerializer = $jsonSerializer;
225
226
        return $this;
227
    }
228
229
    /**
230
     * Set the default JSON serializer (`JsonHalSerializer`).
231
     *
232
     * @return HateoasBuilder
233
     */
234
    public function setDefaultJsonSerializer()
235
    {
236
        return $this->setJsonSerializer(new JsonHalSerializer());
237
    }
238
239
    /**
240
     * Add a new URL generator. If you pass `null` as name, it will be the
241
     * default URL generator.
242
     *
243
     * @param string|null           $name
244
     * @param UrlGeneratorInterface $urlGenerator
245
     *
246
     * @return HateoasBuilder
247
     */
248
    public function setUrlGenerator($name, UrlGeneratorInterface $urlGenerator)
249
    {
250
        $this->urlGeneratorRegistry->set($name, $urlGenerator);
251
252
        return $this;
253
    }
254
255
    /**
256
     * Add a new expression context variable.
257
     *
258
     * @param string $name
259
     * @param mixed  $value
260
     *
261
     * @return HateoasBuilder
262
     */
263
    public function setExpressionContextVariable($name, $value)
264
    {
265
        $this->contextVariables[$name] = $value;
266
267
        return $this;
268
    }
269
270
    /**
271
     * @param ExpressionLanguage $expressionLanguage
272
     *
273
     * @return HateoasBuilder
274
     */
275
    public function setExpressionLanguage(ExpressionLanguage $expressionLanguage)
276
    {
277
        $this->expressionLanguage = $expressionLanguage;
278
279
        return $this;
280
    }
281
282
    /**
283
     * @param ExpressionFunctionInterface $expressionFunction
284
     *
285
     * @return HateoasBuilder
286
     */
287
    public function registerExpressionFunction(ExpressionFunctionInterface $expressionFunction)
288
    {
289
        $this->expressionFunctions[] = $expressionFunction;
290
291
        return $this;
292
    }
293
294
    /**
295
     * Add a new relation provider resolver.
296
     *
297
     * @param RelationProviderResolverInterface $resolver
298
     *
299
     * @return HateoasBuilder
300
     */
301
    public function addRelationProviderResolver(RelationProviderResolverInterface $resolver)
302
    {
303
        $this->chainResolver->addResolver($resolver);
304
305
        return $this;
306
    }
307
308
    /**
309
     * @param ConfigurationExtensionInterface $configurationExtension
310
     *
311
     * @return HateoasBuilder
312
     */
313
    public function addConfigurationExtension(ConfigurationExtensionInterface $configurationExtension)
314
    {
315
        $this->configurationExtensions[] = $configurationExtension;
316
317
        return $this;
318
    }
319
320
    /**
321
     * @param boolean $debug
322
     *
323
     * @return HateoasBuilder
324
     */
325
    public function setDebug($debug)
326
    {
327
        $this->debug = (boolean) $debug;
328
329
        return $this;
330
    }
331
332
    /**
333
     * @param string $dir
334
     *
335
     * @return HateoasBuilder
336
     */
337
    public function setCacheDir($dir)
338
    {
339
        if (!is_dir($dir)) {
340
            $this->createDir($dir);
341
        }
342
343
        if (!is_writable($dir)) {
344
            throw new \InvalidArgumentException(sprintf('The cache directory "%s" is not writable.', $dir));
345
        }
346
347
        $this->cacheDir = $dir;
348
349
        return $this;
350
    }
351
352
    /**
353
     * @param boolean $include Whether to include the metadata from the interfaces
354
     *
355
     * @return HateoasBuilder
356
     */
357
    public function includeInterfaceMetadata($include)
358
    {
359
        $this->includeInterfaceMetadata = (boolean) $include;
360
361
        return $this;
362
    }
363
364
    /**
365
     * Set a map of namespace prefixes to directories.
366
     *
367
     * This method overrides any previously defined directories.
368
     *
369
     * @param array $namespacePrefixToDirMap
370
     *
371
     * @return HateoasBuilder
372
     */
373
    public function setMetadataDirs(array $namespacePrefixToDirMap)
374
    {
375
        foreach ($namespacePrefixToDirMap as $dir) {
376
            if (!is_dir($dir)) {
377
                throw new \InvalidArgumentException(sprintf('The directory "%s" does not exist.', $dir));
378
            }
379
        }
380
381
        $this->metadataDirs = $namespacePrefixToDirMap;
382
383
        return $this;
384
    }
385
386
    /**
387
     * Add a directory where the serializer will look for class metadata.
388
     *
389
     * The namespace prefix will make the names of the actual metadata files a bit shorter. For example, let's assume
390
     * that you have a directory where you only store metadata files for the ``MyApplication\Entity`` namespace.
391
     *
392
     * If you use an empty prefix, your metadata files would need to look like:
393
     *
394
     * ``my-dir/MyApplication.Entity.SomeObject.yml``
395
     * ``my-dir/MyApplication.Entity.OtherObject.yml``
396
     *
397
     * If you use ``MyApplication\Entity`` as prefix, your metadata files would need to look like:
398
     *
399
     * ``my-dir/SomeObject.yml``
400
     * ``my-dir/OtherObject.yml``
401
     *
402
     * Please keep in mind that you currently may only have one directory per namespace prefix.
403
     *
404
     * @param string $dir             The directory where metadata files are located.
405
     * @param string $namespacePrefix An optional prefix if you only store metadata for specific namespaces in this directory.
406
     *
407
     * @return HateoasBuilder
408
     */
409 View Code Duplication
    public function addMetadataDir($dir, $namespacePrefix = '')
0 ignored issues
show
Duplication introduced by Adrien Brault
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
410
    {
411
        if (!is_dir($dir)) {
412
            throw new \InvalidArgumentException(sprintf('The directory "%s" does not exist.', $dir));
413
        }
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
    }
423
424
    /**
425
     * Add a map of namespace prefixes to directories.
426
     *
427
     * @param array $namespacePrefixToDirMap
428
     *
429
     * @return HateoasBuilder
430
     */
431
    public function addMetadataDirs(array $namespacePrefixToDirMap)
432
    {
433
        foreach ($namespacePrefixToDirMap as $prefix => $dir) {
434
            $this->addMetadataDir($dir, $prefix);
435
        }
436
437
        return $this;
438
    }
439
440
    /**
441
     * Similar to addMetadataDir(), but overrides an existing entry.
442
     *
443
     * @param string $dir
444
     * @param string $namespacePrefix
445
     *
446
     * @return HateoasBuilder
447
     */
448 View Code Duplication
    public function replaceMetadataDir($dir, $namespacePrefix = '')
0 ignored issues
show
Duplication introduced by Adrien Brault
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
449
    {
450
        if (!is_dir($dir)) {
451
            throw new \InvalidArgumentException(sprintf('The directory "%s" does not exist.', $dir));
452
        }
453
454
        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
        }
457
458
        $this->metadataDirs[$namespacePrefix] = $dir;
459
460
        return $this;
461
    }
462
463
    private function buildMetadataFactory()
464
    {
465
        $annotationReader = $this->annotationReader;
466
467
        if (null === $annotationReader) {
468
            $annotationReader = new AnnotationReader();
469
470
            if (null !== $this->cacheDir) {
471
                $this->createDir($this->cacheDir.'/annotations');
472
                $annotationReader = new FileCacheReader($annotationReader, $this->cacheDir.'/annotations', $this->debug);
0 ignored issues
show
Deprecated Code introduced by Adrien Brault
The class Doctrine\Common\Annotations\FileCacheReader has been deprecated with message: the FileCacheReader is deprecated and will be removed in version 2.0.0 of doctrine/annotations. Please use the {@see \Doctrine\Common\Annotations\CachedReader} instead.

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
473
            }
474
        }
475
476
        if (!empty($this->metadataDirs)) {
477
            $fileLocator    = new FileLocator($this->metadataDirs);
478
            $metadataDriver = new DriverChain(array(
479
                new YamlDriver($fileLocator),
480
                new XmlDriver($fileLocator),
481
                new AnnotationDriver($annotationReader),
482
            ));
483
        } else {
484
            $metadataDriver = new AnnotationDriver($annotationReader);
485
        }
486
487
        $metadataDriver  = new ExtensionDriver($metadataDriver, $this->configurationExtensions);
488
        $metadataFactory = new MetadataFactory($metadataDriver, null, $this->debug);
489
        $metadataFactory->setIncludeInterfaces($this->includeInterfaceMetadata);
490
491
        if (null !== $this->cacheDir) {
492
            $this->createDir($this->cacheDir.'/metadata');
493
            $metadataFactory->setCache(new FileCache($this->cacheDir.'/metadata'));
494
        }
495
496
        return $metadataFactory;
497
    }
498
499
    /**
500
     * @param string $dir
501
     */
502
    private function createDir($dir)
503
    {
504
        if (is_dir($dir)) {
505
            return;
506
        }
507
508
        if (false === @mkdir($dir, 0777, true) && !is_dir($dir)) {
509
            throw new \RuntimeException(sprintf('Could not create directory "%s".', $dir));
510
        }
511
    }
512
513
    private function getExpressionLanguage()
514
    {
515
        if (null === $this->expressionLanguage) {
516
            $this->expressionLanguage = new ExpressionLanguage();
517
        }
518
519
        return $this->expressionLanguage;
520
    }
521
}
522