Completed
Push — v0.7 ( 21297d...0d3226 )
by Nate
02:43
created

GsonBuilder::enableCache()   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 1
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\Context\ReaderContext;
24
use Tebru\Gson\Context\WriterContext;
25
use Tebru\Gson\Exclusion\DeserializationExclusionDataAware;
26
use Tebru\Gson\Exclusion\ExclusionStrategy;
27
use Tebru\Gson\Exclusion\SerializationExclusionDataAware;
28
use Tebru\Gson\Internal\AccessorMethodProvider;
29
use Tebru\Gson\Internal\AccessorStrategyFactory;
30
use Tebru\Gson\Internal\ConstructorConstructor;
31
use Tebru\Gson\Internal\Data\ClassMetadataFactory;
32
use Tebru\Gson\Internal\Data\ReflectionPropertySetFactory;
33
use Tebru\Gson\Internal\DiscriminatorDeserializer;
34
use Tebru\Gson\Internal\Excluder;
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\TypeAdapterProvider;
39
use Tebru\Gson\Internal\TypeTokenFactory;
40
use Tebru\Gson\TypeAdapter\Factory\ArrayTypeAdapterFactory;
41
use Tebru\Gson\TypeAdapter\Factory\BooleanTypeAdapterFactory;
42
use Tebru\Gson\TypeAdapter\Factory\CustomWrappedTypeAdapterFactory;
43
use Tebru\Gson\TypeAdapter\Factory\DateTimeTypeAdapterFactory;
44
use Tebru\Gson\TypeAdapter\Factory\FloatTypeAdapterFactory;
45
use Tebru\Gson\TypeAdapter\Factory\IntegerTypeAdapterFactory;
46
use Tebru\Gson\TypeAdapter\Factory\NullTypeAdapterFactory;
47
use Tebru\Gson\TypeAdapter\Factory\ReflectionTypeAdapterFactory;
48
use Tebru\Gson\TypeAdapter\Factory\StringTypeAdapterFactory;
49
use Tebru\Gson\TypeAdapter\Factory\WildcardTypeAdapterFactory;
50
use Tebru\Gson\TypeAdapter\Factory\WrappedTypeAdapterFactory;
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 when loaded
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
     * True if the [@see ExclusionCheck] annotation is required to use non-cached exclusion strategies
125
     *
126
     * @var bool
127
     */
128
    private $requireExclusionCheck = false;
129
130
    /**
131
     * An array of [@see ExclusionStrategy] objects
132
     *
133
     * @var ExclusionStrategy[]
134
     */
135
    private $exclusionStrategies = [];
136
137
    /**
138
     * An array of Cacheable [@see ExclusionStrategy] objects
139
     *
140
     * @var ExclusionStrategy[]
141
     */
142
    private $cachedExclusionStrategies = [];
143
144
    /**
145
     * @var ReaderContext
146
     */
147
    private $readerContext;
148
149
    /**
150
     * @var WriterContext
151
     */
152
    private $writerContext;
153
154
    /**
155
     * @var bool|null
156
     */
157
    private $enableScalarAdapters;
158
159
    /**
160
     * Default format for DateTimes
161
     *
162
     * @var string
163
     */
164
    private $dateTimeFormat = DateTime::ATOM;
165
166
    /**
167
     * A cache interface to be used in place of defaults
168
     *
169
     * If this is set, [@see GsonBuilder::$enableCache] will be ignored
170
     *
171
     * @var CacheInterface
172
     */
173
    private $cache;
174
175
    /**
176
     * True if we should be caching
177
     *
178
     * @var bool
179
     */
180
    private $enableCache = false;
181
182
    /**
183
     * Cache directory, if set this enabled filesystem caching
184
     *
185
     * @var string
186
     */
187
    private $cacheDir;
188
189
    /**
190
     * Add a custom type adapter
191
     *
192
     * @param TypeAdapterFactory $typeAdapterFactory
193
     * @return GsonBuilder
194
     */
195 18
    public function addTypeAdapterFactory(TypeAdapterFactory $typeAdapterFactory): GsonBuilder
196
    {
197 18
        $this->typeAdapterFactories[] = $typeAdapterFactory;
198
199 18
        return $this;
200
    }
201
202
    /**
203
     * Adds a [@see Discriminator] as a type adapter factory
204
     *
205
     * @param string $type
206
     * @param Discriminator $discriminator
207
     * @return GsonBuilder
208
     */
209 3
    public function addDiscriminator(string $type, Discriminator $discriminator): GsonBuilder
210
    {
211 3
        $this->typeAdapterFactories[] = new CustomWrappedTypeAdapterFactory(
212 3
            TypeToken::create($type),
213 3
            true,
214 3
            null,
215 3
            new DiscriminatorDeserializer($discriminator)
216
        );
217
218 3
        return $this;
219
    }
220
221
    /**
222
     * Add custom handling for a specific type
223
     *
224
     * Handler objects may be of types: TypeAdapter, JsonSerializer, or JsonDeserializer. Passing
225
     * $strict=true will match the specified type exactly, as opposed to checking anywhere in the
226
     * inheritance chain.
227
     *
228
     * @param string $type
229
     * @param $handler
230
     * @param bool $strict
231
     * @return GsonBuilder
232
     */
233 7
    public function registerType(string $type, $handler, bool $strict = false): GsonBuilder
234
    {
235 7
        if ($handler instanceof TypeAdapter) {
236 3
            $this->typeAdapterFactories[] = new WrappedTypeAdapterFactory($handler, TypeToken::create($type), $strict);
237
238 3
            return $this;
239
        }
240
241 4
        if ($handler instanceof JsonSerializer && $handler instanceof JsonDeserializer) {
242 1
            $this->typeAdapterFactories[] = new CustomWrappedTypeAdapterFactory(TypeToken::create($type), $strict, $handler, $handler);
243
244 1
            return $this;
245
        }
246
247 3
        if ($handler instanceof JsonSerializer) {
248 1
            $this->typeAdapterFactories[] = new CustomWrappedTypeAdapterFactory(TypeToken::create($type), $strict, $handler);
249
250 1
            return $this;
251
        }
252
253 2
        if ($handler instanceof JsonDeserializer) {
254 1
            $this->typeAdapterFactories[] = new CustomWrappedTypeAdapterFactory(TypeToken::create($type), $strict, null, $handler);
255
256 1
            return $this;
257
        }
258
259 1
        throw new InvalidArgumentException(sprintf('Handler of type "%s" is not supported', get_class($handler)));
260
    }
261
262
    /**
263
     * Add an [@see InstanceCreator] for a given type
264
     *
265
     * @param string $type
266
     * @param InstanceCreator $instanceCreator
267
     * @return GsonBuilder
268
     */
269 2
    public function addInstanceCreator(string $type, InstanceCreator $instanceCreator): GsonBuilder
270
    {
271 2
        $phpType = TypeToken::create($type);
272 2
        $this->instanceCreators[$phpType->rawType] = $instanceCreator;
273
274 2
        return $this;
275
    }
276
277
    /**
278
     * Add a visitor that will be called when [@see ClassMetadata] is first loaded
279
     *
280
     * @param ClassMetadataVisitor $classMetadataVisitor
281
     * @return GsonBuilder
282
     */
283 2
    public function addClassMetadataVisitor(ClassMetadataVisitor $classMetadataVisitor): GsonBuilder
284
    {
285 2
        $this->classMetadataVisitors[] = $classMetadataVisitor;
286
287 2
        return $this;
288
    }
289
290
    /**
291
     * Set the version to be used with [@see Since] and [@see Until] annotations
292
     *
293
     * @param string $version
294
     * @return GsonBuilder
295
     */
296 4
    public function setVersion(string $version): GsonBuilder
297
    {
298 4
        $this->version = $version;
299
300 4
        return $this;
301
    }
302
303
    /**
304
     * Set the property modifiers that should be excluded based on [@see \ReflectionProperty]
305
     *
306
     * This number is a bitmap, so ReflectionProperty::IS_STATIC will exclude all static properties.
307
     * Likewise, passing (ReflectionProperty::IS_STATIC | ReflectionProperty::IS_PRIVATE) will exclude
308
     * all static and private properties.
309
     *
310
     * @param int $modifiers
311
     * @return GsonBuilder
312
     */
313 2
    public function setExcludedModifier(int $modifiers): GsonBuilder
314
    {
315 2
        $this->excludedModifiers = $modifiers;
316
317 2
        return $this;
318
    }
319
320
    /**
321
     * Require the [@see Expose] annotation to serialize or deserialize property
322
     *
323
     * @return GsonBuilder
324
     */
325 2
    public function requireExposeAnnotation(): GsonBuilder
326
    {
327 2
        $this->requireExpose = true;
328
329 2
        return $this;
330
    }
331
332
    /**
333
     * Require the [@see ExclusionCheck] annotation to use non-cached exclusion strategies
334
     *
335
     * @return GsonBuilder
336
     */
337 1
    public function requireExclusionCheckAnnotation(): GsonBuilder
338
    {
339 1
        $this->requireExclusionCheck = true;
340
341 1
        return $this;
342
    }
343
344
    /**
345
     * Add an [@see ExclusionStrategy]
346
     *
347
     * @param ExclusionStrategy $exclusionStrategy
348
     * @return GsonBuilder
349
     */
350 3
    public function addExclusion(ExclusionStrategy $exclusionStrategy): GsonBuilder
351
    {
352 3
        if (!$exclusionStrategy->shouldCache()) {
353 1
            $this->exclusionStrategies[] = $exclusionStrategy;
354 1
            return $this;
355
        }
356
357
        if (
358 2
            $exclusionStrategy instanceof SerializationExclusionDataAware
359 2
            || $exclusionStrategy instanceof DeserializationExclusionDataAware
360
        ) {
361 1
            throw new LogicException('Gson: Cacheable exclusion strategies must not implement *DataAware interfaces');
362
        }
363
364 1
        $this->cachedExclusionStrategies[] = $exclusionStrategy;
365
366 1
        return $this;
367
    }
368
369
    /**
370
     * Set a custom property naming strategy
371
     *
372
     * @param PropertyNamingStrategy $propertyNamingStrategy
373
     * @return GsonBuilder
374
     */
375 1
    public function setPropertyNamingStrategy(PropertyNamingStrategy $propertyNamingStrategy): GsonBuilder
376
    {
377 1
        $this->propertyNamingStrategy = $propertyNamingStrategy;
378
379 1
        return $this;
380
    }
381
382
    /**
383
     * Set one of [@see PropertyNamingPolicy]
384
     *
385
     * @param string $policy
386
     * @return GsonBuilder
387
     */
388 1
    public function setPropertyNamingPolicy(string $policy): GsonBuilder
389
    {
390 1
        $this->propertyNamingPolicy = $policy;
391
392 1
        return $this;
393
    }
394
395
    /**
396
     * Set a custom method naming strategy
397
     *
398
     * @param MethodNamingStrategy $methodNamingStrategy
399
     * @return GsonBuilder
400
     */
401 1
    public function setMethodNamingStrategy(MethodNamingStrategy $methodNamingStrategy): GsonBuilder
402
    {
403 1
        $this->methodNamingStrategy = $methodNamingStrategy;
404
405 1
        return $this;
406
    }
407
408 1
    public function setReaderContext(ReaderContext $context): GsonBuilder
409
    {
410 1
        $this->readerContext = $context;
411
412 1
        return $this;
413
    }
414
415 2
    public function setWriterContext(WriterContext $context)
416
    {
417 2
        $this->writerContext = $context;
418
419 2
        return $this;
420
    }
421
422 4
    public function setEnableScalarAdapters(bool $enableScalarAdapters): GsonBuilder
423
    {
424 4
        $this->enableScalarAdapters = $enableScalarAdapters;
425
426 4
        return $this;
427
    }
428
429
    /**
430
     * Set the default datetime format
431
     *
432
     * @param string $format
433
     * @return GsonBuilder
434
     */
435 1
    public function setDateTimeFormat(string $format): GsonBuilder
436
    {
437 1
        $this->dateTimeFormat = $format;
438
439 1
        return $this;
440
    }
441
442
    /**
443
     * Override default cache adapters
444
     *
445
     * @param CacheInterface $cache
446
     * @return GsonBuilder
447
     */
448 1
    public function setCache(CacheInterface $cache): GsonBuilder
449
    {
450 1
        $this->cache = $cache;
451
452 1
        return $this;
453
    }
454
455
    /**
456
     * Set whether caching is enabled
457
     *
458
     * @param bool $enableCache
459
     * @return GsonBuilder
460
     */
461 2
    public function enableCache(bool $enableCache): GsonBuilder
462
    {
463 2
        $this->enableCache = $enableCache;
464
465 2
        return $this;
466
    }
467
    /**
468
     * Setting a cache directory will turn on filesystem caching
469
     *
470
     * @param string $cacheDir
471
     * @return GsonBuilder
472
     */
473 2
    public function setCacheDir(string $cacheDir): GsonBuilder
474
    {
475 2
        $this->cacheDir = $cacheDir.'/gson';
476
477 2
        return $this;
478
    }
479
480
    /**
481
     * Builds a new [@see Gson] object based on configuration set
482
     *
483
     * @return Gson
484
     * @throws LogicException
485
     */
486 48
    public function build(): Gson
487
    {
488 48
        if (null === $this->cacheDir && true === $this->enableCache) {
489 1
            throw new LogicException('Cannot enable cache without a cache directory');
490
        }
491
492 47
        $readerContext = $this->readerContext ?? new ReaderContext();
493 47
        $writerContext = $this->writerContext ?? new WriterContext();
494 47
        if ($this->enableScalarAdapters !== null) {
495 4
            $readerContext->setEnableScalarAdapters($this->enableScalarAdapters);
496 4
            $writerContext->setEnableScalarAdapters($this->enableScalarAdapters);
497
        }
498
499 47
        if ($readerContext->enableScalarAdapters() !== $writerContext->enableScalarAdapters()) {
500 1
            throw new LogicException('The "enableScalarAdapter" values for the reader and writer contexts must match');
501
        }
502
503 46
        $propertyNamingStrategy = $this->propertyNamingStrategy ?? new DefaultPropertyNamingStrategy($this->propertyNamingPolicy);
504 46
        $methodNamingStrategy = $this->methodNamingStrategy ?? new UpperCaseMethodNamingStrategy();
505
506 46
        if ($this->cache === null) {
507 45
            $this->cache = false === $this->enableCache
508 44
                ? new ArrayCache(0, false)
0 ignored issues
show
Deprecated Code introduced by
The class Symfony\Component\Cache\Simple\ArrayCache has been deprecated: since Symfony 4.3, use ArrayAdapter and type-hint for CacheInterface instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

508
                ? /** @scrutinizer ignore-deprecated */ new ArrayCache(0, false)
Loading history...
509 1
                : new ChainCache([new ArrayCache(0, false), new PhpFilesCache('', 0, $this->cacheDir)]);
0 ignored issues
show
Deprecated Code introduced by
The class Symfony\Component\Cache\Simple\ArrayCache has been deprecated: since Symfony 4.3, use ArrayAdapter and type-hint for CacheInterface instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

509
                : new ChainCache([/** @scrutinizer ignore-deprecated */ new ArrayCache(0, false), new PhpFilesCache('', 0, $this->cacheDir)]);
Loading history...
Deprecated Code introduced by
The class Symfony\Component\Cache\Simple\PhpFilesCache has been deprecated: since Symfony 4.3, use PhpFilesAdapter and type-hint for CacheInterface instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

509
                : new ChainCache([new ArrayCache(0, false), /** @scrutinizer ignore-deprecated */ new PhpFilesCache('', 0, $this->cacheDir)]);
Loading history...
Deprecated Code introduced by
The class Symfony\Component\Cache\Simple\ChainCache has been deprecated: since Symfony 4.3, use ChainAdapter and type-hint for CacheInterface instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

509
                : /** @scrutinizer ignore-deprecated */ new ChainCache([new ArrayCache(0, false), new PhpFilesCache('', 0, $this->cacheDir)]);
Loading history...
510
        }
511
512
        // no need to cache the annotations as they get cached with the class/properties
513 46
        $annotationReader = new AnnotationReaderAdapter(new AnnotationReader(), new NullCache());
0 ignored issues
show
Deprecated Code introduced by
The class Symfony\Component\Cache\Simple\NullCache has been deprecated: since Symfony 4.3, use NullAdapter and type-hint for CacheInterface instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

513
        $annotationReader = new AnnotationReaderAdapter(new AnnotationReader(), /** @scrutinizer ignore-deprecated */ new NullCache());
Loading history...
514 46
        $excluder = new Excluder();
515 46
        $excluder->setVersion($this->version);
516 46
        $excluder->setExcludedModifiers($this->excludedModifiers);
517 46
        $excluder->setRequireExpose($this->requireExpose);
518
519 46
        foreach ($this->exclusionStrategies as $strategy) {
520 1
            $excluder->addExclusionStrategy($strategy);
521
        }
522
523 46
        foreach ($this->cachedExclusionStrategies as $strategy) {
524 1
            $excluder->addCachedExclusionStrategy($strategy);
525
        }
526
527 46
        $classMetadataFactory = new ClassMetadataFactory(
528 46
            new ReflectionPropertySetFactory(),
529 46
            $annotationReader,
530 46
            new PropertyNamer($propertyNamingStrategy),
531 46
            new AccessorMethodProvider($methodNamingStrategy),
532 46
            new AccessorStrategyFactory(),
533 46
            new TypeTokenFactory(),
534 46
            $excluder,
535 46
            $this->cache
536
        );
537 46
        $constructorConstructor = new ConstructorConstructor($this->instanceCreators);
538 46
        $typeAdapterProvider = new TypeAdapterProvider(
539 46
            $this->getTypeAdapterFactories(
540 46
                $classMetadataFactory,
541 46
                $excluder,
542 46
                $constructorConstructor,
543 46
                $readerContext->enableScalarAdapters()
544
            ),
545 46
            $constructorConstructor
546
        );
547
548 46
        return new Gson($typeAdapterProvider, $readerContext, $writerContext);
549
    }
550
551
    /**
552
     * Merges default factories with user provided factories
553
     *
554
     * @param ClassMetadataFactory $classMetadataFactory
555
     * @param Excluder $excluder
556
     * @param ConstructorConstructor $constructorConstructor
557
     * @param bool $enableScalarAdapters
558
     * @return array|TypeAdapterFactory[]
559
     */
560 46
    private function getTypeAdapterFactories(
561
        ClassMetadataFactory $classMetadataFactory,
562
        Excluder $excluder,
563
        ConstructorConstructor $constructorConstructor,
564
        bool $enableScalarAdapters
565
    ): array {
566 46
        $scalarFactories = [];
567 46
        if ($enableScalarAdapters) {
568
            $scalarFactories = [
569 4
                new StringTypeAdapterFactory(),
570 4
                new IntegerTypeAdapterFactory(),
571 4
                new FloatTypeAdapterFactory(),
572 4
                new BooleanTypeAdapterFactory(),
573 4
                new NullTypeAdapterFactory(),
574
            ];
575
        }
576
577 46
        return array_merge(
578 46
            $this->typeAdapterFactories,
579 46
            $scalarFactories,
580
            [
581 46
                new DateTimeTypeAdapterFactory($this->dateTimeFormat),
582 46
                new ArrayTypeAdapterFactory($enableScalarAdapters),
583 46
                new ReflectionTypeAdapterFactory(
584 46
                    $constructorConstructor,
585 46
                    $classMetadataFactory,
586 46
                    $excluder,
587 46
                    $this->requireExclusionCheck,
588 46
                    $this->classMetadataVisitors
589
                ),
590 46
                new WildcardTypeAdapterFactory(),
591
            ]
592
        );
593
    }
594
}
595