Completed
Push — master ( cb11f6...01dae9 )
by Nate
02:55
created

GsonBuilder::addDiscriminator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

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