Passed
Push — master ( 37395a...ba940f )
by Nate
01:20 queued 11s
created

GsonBuilder::requireExclusionCheckAnnotation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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