Completed
Push — master ( 2226c5...19582b )
by Nate
02:37
created

GsonBuilder::setPropertyNamingPolicy()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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