GsonBuilder   A
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 551
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 40
eloc 145
c 2
b 0
f 0
dl 0
loc 551
ccs 145
cts 145
cp 1
rs 9.2

22 Methods

Rating   Name   Duplication   Size   Complexity  
B build() 0 63 10
A enableCache() 0 5 1
A setMethodNamingStrategy() 0 5 1
A setVersion() 0 5 1
A setPropertyNamingPolicy() 0 5 1
A addDiscriminator() 0 10 1
A requireExclusionCheckAnnotation() 0 5 1
A registerType() 0 27 6
A setWriterContext() 0 5 1
A setCache() 0 5 1
A getTypeAdapterFactories() 0 31 2
A requireExposeAnnotation() 0 5 1
A setDateTimeFormat() 0 5 1
A addClassMetadataVisitor() 0 5 1
A setPropertyNamingStrategy() 0 5 1
A addTypeAdapterFactory() 0 5 1
A setEnableScalarAdapters() 0 5 1
A addExclusion() 0 17 4
A setExcludedModifier() 0 5 1
A addInstanceCreator() 0 6 1
A setCacheDir() 0 5 1
A setReaderContext() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like GsonBuilder 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.

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 GsonBuilder, and based on these observations, apply Extract Interface, too.

1
<?php
2
/*
3
 * Copyright (c) Nate Brunette.
4
 * Distributed under the MIT License (http://opensource.org/licenses/MIT)
5
 */
6
7
declare(strict_types=1);
8
9
namespace Tebru\Gson;
10
11
use DateTime;
12
use Doctrine\Common\Annotations\AnnotationReader;
13
use InvalidArgumentException;
14
use LogicException;
15
use Psr\SimpleCache\CacheInterface;
16
use ReflectionProperty;
17
use Tebru\AnnotationReader\AnnotationReaderAdapter;
18
use Tebru\Gson\Annotation\ExclusionCheck;
19
use Tebru\Gson\Context\ReaderContext;
20
use Tebru\Gson\Context\WriterContext;
21
use Tebru\Gson\Exclusion\DeserializationExclusionDataAware;
22
use Tebru\Gson\Exclusion\ExclusionStrategy;
23
use Tebru\Gson\Exclusion\SerializationExclusionDataAware;
24
use Tebru\Gson\Internal\AccessorMethodProvider;
25
use Tebru\Gson\Internal\AccessorStrategyFactory;
26
use Tebru\Gson\Internal\ConstructorConstructor;
27
use Tebru\Gson\Internal\Data\ClassMetadataFactory;
28
use Tebru\Gson\Internal\Data\ReflectionPropertySetFactory;
29
use Tebru\Gson\Internal\CacheProvider;
30
use Tebru\Gson\Internal\DiscriminatorDeserializer;
31
use Tebru\Gson\Internal\Excluder;
32
use Tebru\Gson\Internal\Naming\DefaultPropertyNamingStrategy;
33
use Tebru\Gson\Internal\Naming\PropertyNamer;
34
use Tebru\Gson\Internal\Naming\UpperCaseMethodNamingStrategy;
35
use Tebru\Gson\Internal\TypeAdapterProvider;
36
use Tebru\Gson\Internal\TypeTokenFactory;
37
use Tebru\Gson\TypeAdapter\BooleanTypeAdapter;
38
use Tebru\Gson\TypeAdapter\Factory\ArrayTypeAdapterFactory;
39
use Tebru\Gson\TypeAdapter\Factory\CustomWrappedTypeAdapterFactory;
40
use Tebru\Gson\TypeAdapter\Factory\DateTimeTypeAdapterFactory;
41
use Tebru\Gson\TypeAdapter\Factory\ReflectionTypeAdapterFactory;
42
use Tebru\Gson\TypeAdapter\Factory\WildcardTypeAdapterFactory;
43
use Tebru\Gson\TypeAdapter\Factory\WrappedTypeAdapterFactory;
44
use Tebru\Gson\TypeAdapter\FloatTypeAdapter;
45
use Tebru\Gson\TypeAdapter\IntegerTypeAdapter;
46
use Tebru\Gson\TypeAdapter\NullTypeAdapter;
47
use Tebru\Gson\TypeAdapter\StringTypeAdapter;
48
use Tebru\PhpType\TypeToken;
49
50
/**
51
 * Class GsonBuilder
52
 *
53
 * @author Nate Brunette <[email protected]>
54
 */
55
class GsonBuilder
56
{
57
    /**
58
     * Array of type adapter factories
59
     *
60
     * @var TypeAdapterFactory[]
61
     */
62
    private $typeAdapterFactories = [];
63
64
    /**
65
     * @var InstanceCreator[]
66
     */
67
    private $instanceCreators = [];
68
69
    /**
70
     * List of visitors to manipulate class metadata when loaded
71
     *
72
     * @var ClassMetadataVisitor[]
73
     */
74
    private $classMetadataVisitors = [];
75
76
    /**
77
     * Strategy for converting property names to serialized names
78
     *
79
     * @var PropertyNamingStrategy
80
     */
81
    private $propertyNamingStrategy;
82
83
    /**
84
     * Property naming policy
85
     *
86
     * Defaults to converting camel case to snake case
87
     *
88
     * @var string
89
     */
90
    private $propertyNamingPolicy = PropertyNamingPolicy::LOWER_CASE_WITH_UNDERSCORES;
91
92
    /**
93
     * Strategy for converting property names to method names
94
     *
95
     * @var MethodNamingStrategy
96
     */
97
    private $methodNamingStrategy;
98
99
    /**
100
     * The version that should be used with Since/Until annotations
101
     *
102
     * @var string
103
     */
104
    private $version;
105
106
    /**
107
     * Modifiers from [@see ReflectionProperty] that should be excluded
108
     *
109
     * @var int
110
     */
111
    private $excludedModifiers = ReflectionProperty::IS_STATIC;
112
113
    /**
114
     * True if the [@see Expose] annotation is required for serialization/deserialization
115
     *
116
     * @var bool
117
     */
118
    private $requireExpose = false;
119
120
    /**
121
     * True if the [@see ExclusionCheck] annotation is required to use non-cached exclusion strategies
122
     *
123
     * @var bool
124
     */
125
    private $requireExclusionCheck = false;
126
127
    /**
128
     * An array of [@see ExclusionStrategy] objects
129
     *
130
     * @var ExclusionStrategy[]
131
     */
132
    private $exclusionStrategies = [];
133
134
    /**
135
     * An array of Cacheable [@see ExclusionStrategy] objects
136
     *
137
     * @var ExclusionStrategy[]
138
     */
139
    private $cachedExclusionStrategies = [];
140
141
    /**
142
     * @var ReaderContext
143
     */
144
    private $readerContext;
145
146
    /**
147
     * @var WriterContext
148
     */
149
    private $writerContext;
150
151
    /**
152
     * @var bool|null
153
     */
154
    private $enableScalarAdapters;
155
156
    /**
157
     * Default format for DateTimes
158
     *
159
     * @var string
160
     */
161
    private $dateTimeFormat = DateTime::ATOM;
162
163
    /**
164
     * A cache interface to be used in place of defaults
165
     *
166
     * If this is set, [@see GsonBuilder::$enableCache] will be ignored
167
     *
168
     * @var CacheInterface
169
     */
170
    private $cache;
171
172
    /**
173
     * True if we should be caching
174
     *
175
     * @var bool
176
     */
177
    private $enableCache = false;
178
179
    /**
180
     * Cache directory, if set this enabled filesystem caching
181
     *
182
     * @var string
183
     */
184
    private $cacheDir;
185
186
    /**
187
     * Add a custom type adapter
188
     *
189
     * @param TypeAdapterFactory $typeAdapterFactory
190
     * @return GsonBuilder
191
     */
192 19
    public function addTypeAdapterFactory(TypeAdapterFactory $typeAdapterFactory): GsonBuilder
193
    {
194 19
        $this->typeAdapterFactories[] = $typeAdapterFactory;
195
196 19
        return $this;
197
    }
198
199
    /**
200
     * Adds a [@see Discriminator] as a type adapter factory
201
     *
202
     * @param string $type
203
     * @param Discriminator $discriminator
204
     * @return GsonBuilder
205
     */
206 3
    public function addDiscriminator(string $type, Discriminator $discriminator): GsonBuilder
207
    {
208 3
        $this->typeAdapterFactories[] = new CustomWrappedTypeAdapterFactory(
209 3
            TypeToken::create($type),
210 3
            true,
211 3
            null,
212 3
            new DiscriminatorDeserializer($discriminator)
213
        );
214
215 3
        return $this;
216
    }
217
218
    /**
219
     * Add custom handling for a specific type
220
     *
221
     * Handler objects may be of types: TypeAdapter, JsonSerializer, or JsonDeserializer. Passing
222
     * $strict=true will match the specified type exactly, as opposed to checking anywhere in the
223
     * inheritance chain.
224
     *
225
     * @param string $type
226
     * @param $handler
227
     * @param bool $strict
228
     * @return GsonBuilder
229
     */
230 7
    public function registerType(string $type, $handler, bool $strict = false): GsonBuilder
231
    {
232 7
        if ($handler instanceof TypeAdapter) {
233 3
            $this->typeAdapterFactories[] = new WrappedTypeAdapterFactory($handler, TypeToken::create($type), $strict);
234
235 3
            return $this;
236
        }
237
238 4
        if ($handler instanceof JsonSerializer && $handler instanceof JsonDeserializer) {
239 1
            $this->typeAdapterFactories[] = new CustomWrappedTypeAdapterFactory(TypeToken::create($type), $strict, $handler, $handler);
240
241 1
            return $this;
242
        }
243
244 3
        if ($handler instanceof JsonSerializer) {
245 1
            $this->typeAdapterFactories[] = new CustomWrappedTypeAdapterFactory(TypeToken::create($type), $strict, $handler);
246
247 1
            return $this;
248
        }
249
250 2
        if ($handler instanceof JsonDeserializer) {
251 1
            $this->typeAdapterFactories[] = new CustomWrappedTypeAdapterFactory(TypeToken::create($type), $strict, null, $handler);
252
253 1
            return $this;
254
        }
255
256 1
        throw new InvalidArgumentException(sprintf('Handler of type "%s" is not supported', get_class($handler)));
257
    }
258
259
    /**
260
     * Add an [@see InstanceCreator] for a given type
261
     *
262
     * @param string $type
263
     * @param InstanceCreator $instanceCreator
264
     * @return GsonBuilder
265
     */
266 2
    public function addInstanceCreator(string $type, InstanceCreator $instanceCreator): GsonBuilder
267
    {
268 2
        $phpType = TypeToken::create($type);
269 2
        $this->instanceCreators[$phpType->rawType] = $instanceCreator;
270
271 2
        return $this;
272
    }
273
274
    /**
275
     * Add a visitor that will be called when [@see ClassMetadata] is first loaded
276
     *
277
     * @param ClassMetadataVisitor $classMetadataVisitor
278
     * @return GsonBuilder
279
     */
280 2
    public function addClassMetadataVisitor(ClassMetadataVisitor $classMetadataVisitor): GsonBuilder
281
    {
282 2
        $this->classMetadataVisitors[] = $classMetadataVisitor;
283
284 2
        return $this;
285
    }
286
287
    /**
288
     * Set the version to be used with [@see Since] and [@see Until] annotations
289
     *
290
     * @param string $version
291
     * @return GsonBuilder
292
     */
293 4
    public function setVersion(string $version): GsonBuilder
294
    {
295 4
        $this->version = $version;
296
297 4
        return $this;
298
    }
299
300
    /**
301
     * Set the property modifiers that should be excluded based on [@see \ReflectionProperty]
302
     *
303
     * This number is a bitmap, so ReflectionProperty::IS_STATIC will exclude all static properties.
304
     * Likewise, passing (ReflectionProperty::IS_STATIC | ReflectionProperty::IS_PRIVATE) will exclude
305
     * all static and private properties.
306
     *
307
     * @param int $modifiers
308
     * @return GsonBuilder
309
     */
310 2
    public function setExcludedModifier(int $modifiers): GsonBuilder
311
    {
312 2
        $this->excludedModifiers = $modifiers;
313
314 2
        return $this;
315
    }
316
317
    /**
318
     * Require the [@see Expose] annotation to serialize or deserialize property
319
     *
320
     * @return GsonBuilder
321
     */
322 2
    public function requireExposeAnnotation(): GsonBuilder
323
    {
324 2
        $this->requireExpose = true;
325
326 2
        return $this;
327
    }
328
329
    /**
330
     * Require the [@see ExclusionCheck] annotation to use non-cached exclusion strategies
331
     *
332
     * @return GsonBuilder
333
     */
334 1
    public function requireExclusionCheckAnnotation(): GsonBuilder
335
    {
336 1
        $this->requireExclusionCheck = true;
337
338 1
        return $this;
339
    }
340
341
    /**
342
     * Add an [@see ExclusionStrategy]
343
     *
344
     * @param ExclusionStrategy $exclusionStrategy
345
     * @return GsonBuilder
346
     */
347 3
    public function addExclusion(ExclusionStrategy $exclusionStrategy): GsonBuilder
348
    {
349 3
        if (!$exclusionStrategy->shouldCache()) {
350 1
            $this->exclusionStrategies[] = $exclusionStrategy;
351 1
            return $this;
352
        }
353
354
        if (
355 2
            $exclusionStrategy instanceof SerializationExclusionDataAware
356 2
            || $exclusionStrategy instanceof DeserializationExclusionDataAware
357
        ) {
358 1
            throw new LogicException('Gson: Cacheable exclusion strategies must not implement *DataAware interfaces');
359
        }
360
361 1
        $this->cachedExclusionStrategies[] = $exclusionStrategy;
362
363 1
        return $this;
364
    }
365
366
    /**
367
     * Set a custom property naming strategy
368
     *
369
     * @param PropertyNamingStrategy $propertyNamingStrategy
370
     * @return GsonBuilder
371
     */
372 1
    public function setPropertyNamingStrategy(PropertyNamingStrategy $propertyNamingStrategy): GsonBuilder
373
    {
374 1
        $this->propertyNamingStrategy = $propertyNamingStrategy;
375
376 1
        return $this;
377
    }
378
379
    /**
380
     * Set one of [@see PropertyNamingPolicy]
381
     *
382
     * @param string $policy
383
     * @return GsonBuilder
384
     */
385 1
    public function setPropertyNamingPolicy(string $policy): GsonBuilder
386
    {
387 1
        $this->propertyNamingPolicy = $policy;
388
389 1
        return $this;
390
    }
391
392
    /**
393
     * Set a custom method naming strategy
394
     *
395
     * @param MethodNamingStrategy $methodNamingStrategy
396
     * @return GsonBuilder
397
     */
398 1
    public function setMethodNamingStrategy(MethodNamingStrategy $methodNamingStrategy): GsonBuilder
399
    {
400 1
        $this->methodNamingStrategy = $methodNamingStrategy;
401
402 1
        return $this;
403
    }
404
405
    /**
406
     * Set context to use during deserialization
407
     *
408
     * @param ReaderContext $context
409
     * @return GsonBuilder
410
     */
411 1
    public function setReaderContext(ReaderContext $context): GsonBuilder
412
    {
413 1
        $this->readerContext = $context;
414
415 1
        return $this;
416
    }
417
418
    /**
419
     * Set context to use during serialization
420
     *
421
     * @param WriterContext $context
422
     * @return GsonBuilder
423
     */
424 2
    public function setWriterContext(WriterContext $context): GsonBuilder
425
    {
426 2
        $this->writerContext = $context;
427
428 2
        return $this;
429
    }
430
431
    /**
432
     * Enable or disable scalar type adapters
433
     *
434
     * @param bool $enableScalarAdapters
435
     * @return GsonBuilder
436
     */
437 6
    public function setEnableScalarAdapters(bool $enableScalarAdapters): GsonBuilder
438
    {
439 6
        $this->enableScalarAdapters = $enableScalarAdapters;
440
441 6
        return $this;
442
    }
443
444
    /**
445
     * Set the default datetime format
446
     *
447
     * @param string $format
448
     * @return GsonBuilder
449
     */
450 1
    public function setDateTimeFormat(string $format): GsonBuilder
451
    {
452 1
        $this->dateTimeFormat = $format;
453
454 1
        return $this;
455
    }
456
457
    /**
458
     * Override default cache adapters
459
     *
460
     * @param CacheInterface $cache
461
     * @return GsonBuilder
462
     */
463 1
    public function setCache(CacheInterface $cache): GsonBuilder
464
    {
465 1
        $this->cache = $cache;
466
467 1
        return $this;
468
    }
469
470
    /**
471
     * Set whether caching is enabled
472
     *
473
     * @param bool $enableCache
474
     * @return GsonBuilder
475
     */
476 2
    public function enableCache(bool $enableCache): GsonBuilder
477
    {
478 2
        $this->enableCache = $enableCache;
479
480 2
        return $this;
481
    }
482
    /**
483
     * Setting a cache directory will turn on filesystem caching
484
     *
485
     * @param string $cacheDir
486
     * @return GsonBuilder
487
     */
488 2
    public function setCacheDir(string $cacheDir): GsonBuilder
489
    {
490 2
        $this->cacheDir = $cacheDir.'/gson';
491
492 2
        return $this;
493
    }
494
495
    /**
496
     * Builds a new [@see Gson] object based on configuration set
497
     *
498
     * @return Gson
499
     * @throws LogicException
500
     */
501 49
    public function build(): Gson
502
    {
503 49
        if ($this->enableCache === true && ($this->cacheDir === null && $this->cache === null)) {
504 1
            throw new LogicException('Cannot enable cache without a cache directory');
505
        }
506
507 48
        $readerContext = $this->readerContext ?? new ReaderContext();
508 48
        $writerContext = $this->writerContext ?? new WriterContext();
509 48
        if ($this->enableScalarAdapters !== null) {
510 6
            $readerContext->setEnableScalarAdapters($this->enableScalarAdapters);
511 6
            $writerContext->setEnableScalarAdapters($this->enableScalarAdapters);
512
        }
513
514 48
        if ($readerContext->enableScalarAdapters() !== $writerContext->enableScalarAdapters()) {
515 1
            throw new LogicException('The "enableScalarAdapter" values for the reader and writer contexts must match');
516
        }
517
518 47
        $propertyNamingStrategy = $this->propertyNamingStrategy ?? new DefaultPropertyNamingStrategy($this->propertyNamingPolicy);
519 47
        $methodNamingStrategy = $this->methodNamingStrategy ?? new UpperCaseMethodNamingStrategy();
520
521 47
        if ($this->cache === null) {
522 46
            $this->cache = false === $this->enableCache
523 45
                ? CacheProvider::createMemoryCache()
524 1
                : CacheProvider::createFileCache($this->cacheDir);
525
        }
526
527
        // no need to cache the annotations as they get cached with the class/properties
528 47
        $annotationReader = new AnnotationReaderAdapter(new AnnotationReader(), CacheProvider::createNullCache());
529 47
        $excluder = new Excluder();
530 47
        $excluder->setVersion($this->version);
531 47
        $excluder->setExcludedModifiers($this->excludedModifiers);
532 47
        $excluder->setRequireExpose($this->requireExpose);
533
534 47
        foreach ($this->exclusionStrategies as $strategy) {
535 1
            $excluder->addExclusionStrategy($strategy);
536
        }
537
538 47
        foreach ($this->cachedExclusionStrategies as $strategy) {
539 1
            $excluder->addCachedExclusionStrategy($strategy);
540
        }
541
542 47
        $classMetadataFactory = new ClassMetadataFactory(
543 47
            new ReflectionPropertySetFactory(),
544 47
            $annotationReader,
545 47
            new PropertyNamer($propertyNamingStrategy),
546 47
            new AccessorMethodProvider($methodNamingStrategy),
547 47
            new AccessorStrategyFactory(),
548 47
            new TypeTokenFactory(),
549 47
            $excluder,
550 47
            $this->cache
551
        );
552 47
        $constructorConstructor = new ConstructorConstructor($this->instanceCreators);
553 47
        $typeAdapterProvider = new TypeAdapterProvider(
554 47
            $this->getTypeAdapterFactories(
555 47
                $classMetadataFactory,
556 47
                $excluder,
557 47
                $constructorConstructor,
558 47
                $readerContext->enableScalarAdapters()
559
            ),
560 47
            $constructorConstructor
561
        );
562
563 47
        return new Gson($typeAdapterProvider, $readerContext, $writerContext);
564
    }
565
566
    /**
567
     * Merges default factories with user provided factories
568
     *
569
     * @param ClassMetadataFactory $classMetadataFactory
570
     * @param Excluder $excluder
571
     * @param ConstructorConstructor $constructorConstructor
572
     * @param bool $enableScalarAdapters
573
     * @return array|TypeAdapterFactory[]
574
     */
575 47
    private function getTypeAdapterFactories(
576
        ClassMetadataFactory $classMetadataFactory,
577
        Excluder $excluder,
578
        ConstructorConstructor $constructorConstructor,
579
        bool $enableScalarAdapters
580
    ): array {
581 47
        $scalarFactories = [];
582 47
        if ($enableScalarAdapters) {
583
            $scalarFactories = [
584 45
                new StringTypeAdapter(),
585 45
                new IntegerTypeAdapter(),
586 45
                new FloatTypeAdapter(),
587 45
                new BooleanTypeAdapter(),
588 45
                new NullTypeAdapter(),
589
            ];
590
        }
591
592 47
        return array_merge(
593 47
            $this->typeAdapterFactories,
594 47
            $scalarFactories,
595
            [
596 47
                new DateTimeTypeAdapterFactory($this->dateTimeFormat),
597 47
                new ArrayTypeAdapterFactory($enableScalarAdapters),
598 47
                new ReflectionTypeAdapterFactory(
599 47
                    $constructorConstructor,
600 47
                    $classMetadataFactory,
601 47
                    $excluder,
602 47
                    $this->requireExclusionCheck,
603 47
                    $this->classMetadataVisitors
604
                ),
605 47
                new WildcardTypeAdapterFactory(),
606
            ]
607
        );
608
    }
609
}
610