Completed
Push — class-metadata-visitor ( 0a5a84 )
by Nate
03:32
created

GsonBuilder::addExclusion()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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