Passed
Push — master ( 21a5dc...1ca6d2 )
by Nate
40s
created

GsonBuilder::addExclusion()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 3
nop 1
dl 0
loc 17
ccs 9
cts 9
cp 1
crap 4
rs 9.9666
c 0
b 0
f 0
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 Symfony\Component\Cache\Simple\ArrayCache;
18
use Symfony\Component\Cache\Simple\ChainCache;
19
use Symfony\Component\Cache\Simple\NullCache;
20
use Symfony\Component\Cache\Simple\PhpFilesCache;
21
use Tebru\AnnotationReader\AnnotationReaderAdapter;
22
use Tebru\Gson\Exclusion\DeserializationExclusionDataAware;
23
use Tebru\Gson\Exclusion\ExclusionStrategy;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Tebru\Gson\ExclusionStrategy. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

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