Completed
Push — caching ( 6c9b2f )
by Nate
03:39
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 Tebru\AnnotationReader\AnnotationReaderAdapter;
18
use Tebru\Gson\Annotation\ExclusionCheck;
0 ignored issues
show
Bug introduced by
The type Tebru\Gson\Annotation\ExclusionCheck was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
19
use Tebru\Gson\Context\ReaderContext;
20
use Tebru\Gson\Context\WriterContext;
21
use Tebru\Gson\Exclusion\DeserializationExclusionDataAware;
0 ignored issues
show
Bug introduced by
The type Tebru\Gson\Exclusion\Des...ationExclusionDataAware was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
22
use Tebru\Gson\Exclusion\ExclusionStrategy;
23
use Tebru\Gson\Exclusion\SerializationExclusionDataAware;
0 ignored issues
show
Bug introduced by
The type Tebru\Gson\Exclusion\Ser...ationExclusionDataAware was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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

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

498
        $excluder = /** @scrutinizer ignore-call */ new Excluder();

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
499
        $excluder->setVersion($this->version);
500
        $excluder->setExcludedModifiers($this->excludedModifiers);
501
        $excluder->setRequireExpose($this->requireExpose);
502
503
        foreach ($this->exclusionStrategies as $strategy) {
504
            $excluder->addExclusionStrategy($strategy);
505
        }
506
507
        $classMetadataFactory = new ClassMetadataFactory(
508
            new ReflectionPropertySetFactory(),
509
            $annotationReader,
510
            new PropertyNamer($propertyNamingStrategy),
511
            new AccessorMethodProvider($methodNamingStrategy),
512
            new AccessorStrategyFactory(),
513
            new TypeTokenFactory(),
514
            $excluder,
515
            $this->cache
516
        );
517
        $constructorConstructor = new ConstructorConstructor($this->instanceCreators);
518
        $typeAdapterProvider = new TypeAdapterProvider(
519
            $this->getTypeAdapterFactories(
520
                $classMetadataFactory,
521
                $excluder,
522
                $constructorConstructor,
523
                $readerContext->enableScalarAdapters()
524
            ),
525
            $constructorConstructor
526
        );
527
528
        $readerContext->setTypeAdapterProvider($typeAdapterProvider);
529
        $writerContext->setTypeAdapterProvider($typeAdapterProvider);
530
531
        $readerContext->setDateFormat($this->dateTimeFormat);
532
        $writerContext->setDateFormat($this->dateTimeFormat);
533
534
        $readerContext->setExcluder($excluder);
535
        $writerContext->setExcluder($excluder);
536
537
        return new Gson($typeAdapterProvider, $readerContext, $writerContext);
538
    }
539
540
    /**
541
     * Merges default factories with user provided factories
542
     *
543
     * @param ClassMetadataFactory $classMetadataFactory
544
     * @param Excluder $excluder
545
     * @param ConstructorConstructor $constructorConstructor
546
     * @param bool $enableScalarAdapters
547
     * @return array|TypeAdapterFactory[]
548
     */
549
    private function getTypeAdapterFactories(
550
        ClassMetadataFactory $classMetadataFactory,
551
        Excluder $excluder,
552
        ConstructorConstructor $constructorConstructor,
553
        bool $enableScalarAdapters
554
    ): array {
555
        $scalarFactories = [];
556
        if ($enableScalarAdapters) {
557
            $scalarFactories = [
558
                new StringTypeAdapter(),
559
                new IntegerTypeAdapter(),
560
                new FloatTypeAdapter(),
561
                new BooleanTypeAdapter(),
562
                new NullTypeAdapter(),
563
            ];
564
        }
565
566
        return array_merge(
567
            $this->typeAdapterFactories,
568
            $scalarFactories,
569
            [
570
                new DateTimeTypeAdapterFactory(),
571
                new ArrayTypeAdapterFactory($enableScalarAdapters),
572
                new ReflectionTypeAdapterFactory(
573
                    $constructorConstructor,
574
                    $classMetadataFactory,
575
                    $excluder,
0 ignored issues
show
Bug introduced by
$excluder of type Tebru\Gson\Internal\Excluder is incompatible with the type array expected by parameter $classMetadataVisitors of Tebru\Gson\TypeAdapter\F...rFactory::__construct(). ( Ignorable by Annotation )

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

575
                    /** @scrutinizer ignore-type */ $excluder,
Loading history...
576
                    $this->requireExclusionCheck,
0 ignored issues
show
Unused Code introduced by
The call to Tebru\Gson\TypeAdapter\F...rFactory::__construct() has too many arguments starting with $this->requireExclusionCheck. ( Ignorable by Annotation )

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

576
                /** @scrutinizer ignore-call */ 
577
                new ReflectionTypeAdapterFactory(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
577
                    $this->classMetadataVisitors,
578
                    $enableScalarAdapters
579
                ),
580
                new WildcardTypeAdapterFactory(),
581
            ]
582
        );
583
    }
584
}
585