Excluder::excludePropertyBySerializationStrategy()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 3
nop 1
dl 0
loc 9
ccs 5
cts 5
cp 1
crap 3
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\Internal;
10
11
use ReflectionProperty;
12
use Tebru\AnnotationReader\AnnotationCollection;
13
use Tebru\Gson\Annotation\Exclude;
14
use Tebru\Gson\Annotation\Expose;
15
use Tebru\Gson\Annotation\Since;
16
use Tebru\Gson\Annotation\Until;
17
use Tebru\Gson\Exclusion\ClassDeserializationExclusionStrategy;
18
use Tebru\Gson\Exclusion\ClassSerializationExclusionStrategy;
19
use Tebru\Gson\Exclusion\DeserializationExclusionData;
20
use Tebru\Gson\Exclusion\DeserializationExclusionDataAware;
21
use Tebru\Gson\Exclusion\ExclusionStrategy;
22
use Tebru\Gson\Exclusion\PropertyDeserializationExclusionStrategy;
23
use Tebru\Gson\Exclusion\PropertySerializationExclusionStrategy;
24
use Tebru\Gson\Exclusion\SerializationExclusionData;
25
use Tebru\Gson\Exclusion\SerializationExclusionDataAware;
26
use Tebru\Gson\Internal\Data\Property;
27
28
/**
29
 * Class Excluder
30
 *
31
 * @author Nate Brunette <[email protected]>
32
 */
33
final class Excluder
34
{
35
    /**
36
     * Version, if set, will be used with [@see Since] and [@see Until] annotations
37
     *
38
     * @var string
39
     */
40
    private $version;
41
42
    /**
43
     * Which modifiers are excluded
44
     *
45
     * By default only static properties are excluded
46
     *
47
     * @var int
48
     */
49
    private $excludedModifiers = ReflectionProperty::IS_STATIC;
50
51
    /**
52
     * If this is true, properties will need to explicitly have an [@see Expose] annotation
53
     * to be serialized or deserialized
54
     *
55
     * @var bool
56
     */
57
    private $requireExpose = false;
58
59
    /**
60
     * Class Exclusion strategies during serialization
61
     *
62
     * @var ClassSerializationExclusionStrategy[]
63
     */
64
    private $classSerializationStrategies = [];
65
66
    /**
67
     * Property Exclusion strategies during serialization
68
     *
69
     * @var PropertySerializationExclusionStrategy[]
70
     */
71
    private $propertySerializationStrategies = [];
72
73
    /**
74
     * Class Exclusion strategies during deserialization
75
     *
76
     * @var ClassDeserializationExclusionStrategy[]
77
     */
78
    private $classDeserializationStrategies = [];
79
80
    /**
81
     * Property Exclusion strategies during deserialization
82
     *
83
     * @var PropertyDeserializationExclusionStrategy[]
84
     */
85
    private $propertyDeserializationStrategies = [];
86
87
    /**
88
     * Exclusion strategies that can be cached
89
     *
90
     * @var ExclusionStrategy[]
91
     */
92
    private $cachedStrategies = [];
93
94
95
    /**
96
     * Set the version to test against
97
     *
98
     * @param string $version
99
     * @return Excluder
100
     */
101 15
    public function setVersion(?string $version): Excluder
102
    {
103 15
        $this->version = $version;
104
105 15
        return $this;
106
    }
107
108
    /**
109
     * Set an integer representing the property modifiers that should be excluded
110
     *
111
     * @param int $modifiers
112
     * @return Excluder
113
     */
114 5
    public function setExcludedModifiers(int $modifiers): Excluder
115
    {
116 5
        $this->excludedModifiers = $modifiers;
117
118 5
        return $this;
119
    }
120
121
    /**
122
     * Require the [@see Expose] annotation to serialize properties
123
     *
124
     * @param bool $requireExpose
125
     * @return Excluder
126
     */
127 8
    public function setRequireExpose(bool $requireExpose): Excluder
128
    {
129 8
        $this->requireExpose = $requireExpose;
130
131 8
        return $this;
132
    }
133
134
    /**
135
     * Add an exclusion strategy
136
     *
137
     * @param ExclusionStrategy $strategy
138
     * @return void
139
     */
140 5
    public function addExclusionStrategy(ExclusionStrategy $strategy): void
141
    {
142 5
        if ($strategy instanceof ClassSerializationExclusionStrategy) {
143 3
            $this->classSerializationStrategies[] = $strategy;
144
        }
145
146 5
        if ($strategy instanceof PropertySerializationExclusionStrategy) {
147 3
            $this->propertySerializationStrategies[] = $strategy;
148
        }
149
150 5
        if ($strategy instanceof ClassDeserializationExclusionStrategy) {
151 3
            $this->classDeserializationStrategies[] = $strategy;
152
        }
153
154 5
        if ($strategy instanceof PropertyDeserializationExclusionStrategy) {
155 3
            $this->propertyDeserializationStrategies[] = $strategy;
156
        }
157 5
    }
158
159
    /**
160
     * Add an exclusion strategy that can be cached
161
     *
162
     * @param ExclusionStrategy $strategy
163
     */
164 1
    public function addCachedExclusionStrategy(ExclusionStrategy $strategy): void
165
    {
166 1
        $this->cachedStrategies[] = $strategy;
167 1
    }
168
169
    /**
170
     * Compile time exclusion checking of classes to determine if we should exclude during serialization
171
     *
172
     * @param DefaultClassMetadata $classMetadata
173
     * @return bool
174
     */
175 13
    public function excludeClassSerialize(DefaultClassMetadata $classMetadata): bool
176
    {
177 13
        foreach ($this->cachedStrategies as $strategy) {
178 1
            if ($strategy instanceof ClassSerializationExclusionStrategy && $strategy->skipSerializingClass($classMetadata)) {
179 1
                return true;
180
            }
181
        }
182
183 12
        return $this->excludeClass($classMetadata, true);
184
    }
185
186
    /**
187
     * Compile time exclusion checking of classes to determine if we should exclude during deserialization
188
     *
189
     * @param DefaultClassMetadata $classMetadata
190
     * @return bool
191
     */
192 13
    public function excludeClassDeserialize(DefaultClassMetadata $classMetadata): bool
193
    {
194 13
        foreach ($this->cachedStrategies as $strategy) {
195 1
            if ($strategy instanceof ClassDeserializationExclusionStrategy && $strategy->skipDeserializingClass($classMetadata)) {
196 1
                return true;
197
            }
198
        }
199
200 12
        return $this->excludeClass($classMetadata, false);
201
    }
202
203
    /**
204
     * Add [@see SerializationExclusionData] to class exclusion strategies
205
     *
206
     * @param SerializationExclusionData $exclusionData
207
     */
208 1
    public function applyClassSerializationExclusionData(SerializationExclusionData $exclusionData): void
209
    {
210 1
        foreach ($this->classSerializationStrategies as $exclusionStrategy) {
211 1
            if ($exclusionStrategy instanceof SerializationExclusionDataAware) {
212 1
                $exclusionStrategy->setSerializationExclusionData($exclusionData);
213
            }
214
        }
215 1
    }
216
217
    /**
218
     * Add [@see SerializationExclusionData] to property exclusion strategies
219
     *
220
     * @param SerializationExclusionData $exclusionData
221
     */
222 1
    public function applyPropertySerializationExclusionData(SerializationExclusionData $exclusionData): void
223
    {
224 1
        foreach ($this->propertySerializationStrategies as $exclusionStrategy) {
225 1
            if ($exclusionStrategy instanceof SerializationExclusionDataAware) {
226 1
                $exclusionStrategy->setSerializationExclusionData($exclusionData);
227
            }
228
        }
229 1
    }
230
231
    /**
232
     * Add [@see SerializationExclusionData] to class deserialization strategies
233
     *
234
     * @param DeserializationExclusionData $exclusionData
235
     */
236 1
    public function applyClassDeserializationExclusionData(DeserializationExclusionData $exclusionData): void
237
    {
238 1
        foreach ($this->classDeserializationStrategies as $exclusionStrategy) {
239 1
            if ($exclusionStrategy instanceof DeserializationExclusionDataAware) {
240 1
                $exclusionStrategy->setDeserializationExclusionData($exclusionData);
241
            }
242
        }
243 1
    }
244
245
    /**
246
     * Add [@see SerializationExclusionData] to property deserialization strategies
247
     *
248
     * @param DeserializationExclusionData $exclusionData
249
     */
250 1
    public function applyPropertyDeserializationExclusionData(DeserializationExclusionData $exclusionData): void
251
    {
252 1
        foreach ($this->propertyDeserializationStrategies as $exclusionStrategy) {
253 1
            if ($exclusionStrategy instanceof DeserializationExclusionDataAware) {
254 1
                $exclusionStrategy->setDeserializationExclusionData($exclusionData);
255
            }
256
        }
257 1
    }
258
259
    /**
260
     * Runtime exclusion checking of classes by strategy during serialization
261
     *
262
     * Uses user-defined strategies
263
     *
264
     * @param DefaultClassMetadata $classMetadata
265
     * @return bool
266
     */
267 2
    public function excludeClassBySerializationStrategy(DefaultClassMetadata $classMetadata): bool
268
    {
269 2
        foreach ($this->classSerializationStrategies as $exclusionStrategy) {
270 1
            if ($exclusionStrategy->skipSerializingClass($classMetadata)) {
271 1
                return true;
272
            }
273
        }
274
275 1
        return false;
276
    }
277
278
    /**
279
     * Runtime exclusion checking of classes by strategy during deserialization
280
     *
281
     * Uses user-defined strategies
282
     *
283
     * @param DefaultClassMetadata $classMetadata
284
     * @return bool
285
     */
286 2
    public function excludeClassByDeserializationStrategy(DefaultClassMetadata $classMetadata): bool
287
    {
288 2
        foreach ($this->classDeserializationStrategies as $exclusionStrategy) {
289 1
            if ($exclusionStrategy->skipDeserializingClass($classMetadata)) {
290 1
                return true;
291
            }
292
        }
293
294 1
        return false;
295
    }
296
297
    /**
298
     * Compile time exclusion checking of properties
299
     *
300
     * @param Property $property
301
     * @return bool
302
     */
303 24
    public function excludePropertySerialize(Property $property): bool
304
    {
305
        // exclude the property if the property modifiers are found in the excluded modifiers
306 24
        if (0 !== ($this->excludedModifiers & $property->modifiers)) {
307 3
            return true;
308
        }
309
310 21
        foreach ($this->cachedStrategies as $strategy) {
311 1
            if ($strategy instanceof PropertySerializationExclusionStrategy && $strategy->skipSerializingProperty($property)) {
312 1
                return true;
313
            }
314
        }
315
316 20
        return $this->excludeProperty($property, true);
317
    }
318
319
    /**
320
     * Compile time exclusion checking of properties
321
     *
322
     * @param Property $property
323
     * @return bool
324
     */
325 24
    public function excludePropertyDeserialize(Property $property): bool
326
    {
327
        // exclude the property if the property modifiers are found in the excluded modifiers
328 24
        if (0 !== ($this->excludedModifiers & $property->modifiers)) {
329 3
            return true;
330
        }
331
332 21
        foreach ($this->cachedStrategies as $strategy) {
333 1
            if ($strategy instanceof PropertyDeserializationExclusionStrategy && $strategy->skipDeserializingProperty($property)) {
334 1
                return true;
335
            }
336
        }
337
338 20
        return $this->excludeProperty($property, false);
339
    }
340
341
    /**
342
     * Runtime exclusion checking of properties by strategy during serialization
343
     *
344
     * Uses user-defined strategies
345
     *
346
     * @param Property $property
347
     * @return bool
348
     */
349 2
    public function excludePropertyBySerializationStrategy(Property $property): bool
350
    {
351 2
        foreach ($this->propertySerializationStrategies as $exclusionStrategy) {
352 1
            if ($exclusionStrategy->skipSerializingProperty($property)) {
353 1
                return true;
354
            }
355
        }
356
357 1
        return false;
358
    }
359
360
    /**
361
     * Runtime exclusion checking of properties by strategy during deserialization
362
     *
363
     * Uses user-defined strategies
364
     *
365
     * @param Property $property
366
     * @return bool
367
     */
368 2
    public function excludePropertyByDeserializationStrategy(Property $property): bool
369
    {
370 2
        foreach ($this->propertyDeserializationStrategies as $exclusionStrategy) {
371 1
            if ($exclusionStrategy->skipDeserializingProperty($property)) {
372 1
                return true;
373
            }
374
        }
375
376 1
        return false;
377
    }
378
379
    /**
380
     * Returns true if class serialization strategies exist
381
     *
382
     * @return bool
383
     */
384 5
    public function hasClassSerializationStrategies(): bool
385
    {
386 5
        return $this->classSerializationStrategies !== [];
387
    }
388
389
    /**
390
     * Returns true if property serialization strategies exist
391
     *
392
     * @return bool
393
     */
394 5
    public function hasPropertySerializationStrategies(): bool
395
    {
396 5
        return $this->propertySerializationStrategies !== [];
397
    }
398
399
    /**
400
     * Returns true if class deserialization strategies exist
401
     *
402
     * @return bool
403
     */
404 5
    public function hasClassDeserializationStrategies(): bool
405
    {
406 5
        return $this->classDeserializationStrategies !== [];
407
    }
408
409
    /**
410
     * Returns true if property deserialization strategies exist
411
     *
412
     * @return bool
413
     */
414 5
    public function hasPropertyDeserializationStrategies(): bool
415
    {
416 5
        return $this->propertyDeserializationStrategies !== [];
417
    }
418
419
    /**
420
     * Check if we should exclude an entire class. Returns true if the class has an [@Exclude] annotation
421
     * unless one of the properties has an [@Expose] annotation
422
     *
423
     * @param DefaultClassMetadata $classMetadata
424
     * @param bool $serialize
425
     * @return bool
426
     */
427 12
    private function excludeClass(DefaultClassMetadata $classMetadata, bool $serialize): bool
428
    {
429 12
        $annotations = $classMetadata->annotations;
430
431
        // exclude if version doesn't match
432 12
        if (!$this->validVersion($annotations)) {
433 3
            return true;
434
        }
435
436
        // exclude if requireExpose is set and class doesn't have an expose annotation
437 9
        $expose = $annotations->get(Expose::class);
438 9
        if ($this->requireExpose && ($expose !== null && !$expose->shouldExpose($serialize))) {
0 ignored issues
show
Bug introduced by
The method shouldExpose() does not exist on Tebru\AnnotationReader\AbstractAnnotation. It seems like you code against a sub-type of Tebru\AnnotationReader\AbstractAnnotation such as Tebru\Gson\Annotation\Expose. ( Ignorable by Annotation )

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

438
        if ($this->requireExpose && ($expose !== null && !$expose->/** @scrutinizer ignore-call */ shouldExpose($serialize))) {
Loading history...
439 1
            return true;
440
        }
441
442
        // don't exclude if exclude annotation doesn't exist or only exists for the other direction
443 9
        $exclude = $annotations->get(Exclude::class);
444 9
        if ($exclude === null || !$exclude->shouldExclude($serialize)) {
0 ignored issues
show
Bug introduced by
The method shouldExclude() does not exist on Tebru\AnnotationReader\AbstractAnnotation. It seems like you code against a sub-type of Tebru\AnnotationReader\AbstractAnnotation such as Tebru\Gson\Annotation\Exclude. ( Ignorable by Annotation )

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

444
        if ($exclude === null || !$exclude->/** @scrutinizer ignore-call */ shouldExclude($serialize)) {
Loading history...
445 8
            return false;
446
        }
447
448
        // don't exclude if the annotation exists, but a property is exposed
449 2
        foreach ($classMetadata->properties->toArray() as $property) {
450 1
            $expose = $property->annotations->get(Expose::class);
451 1
            if ($expose !== null && $expose->shouldExpose($serialize)) {
452 1
                return false;
453
            }
454
        }
455
456
        // exclude if an annotation is set and no properties are exposed
457 1
        return true;
458
    }
459
460
    /**
461
     * Checks various annotations to see if the property should be excluded
462
     *
463
     * - [@see Since] / [@see Until]
464
     * - [@see Exclude]
465
     * - [@see Expose] (if requireExpose is set)
466
     *
467
     * @param Property $property
468
     * @param bool $serialize
469
     * @return bool
470
     *
471
     */
472 20
    private function excludeProperty(Property $property, bool $serialize): bool
473
    {
474 20
        $annotations = $property->getAnnotations();
475 20
        if (!$this->validVersion($annotations)) {
476 3
            return true;
477
        }
478
479
        // exclude from annotation
480 17
        $exclude = $annotations->get(Exclude::class);
481 17
        if (null !== $exclude && $exclude->shouldExclude($serialize)) {
482 3
            return true;
483
        }
484
485 16
        $classExclude = $property->classMetadata->annotations->get(Exclude::class);
486
487
        // if we need an expose annotation
488 16
        if ($this->requireExpose || ($classExclude !== null && $classExclude->shouldExclude($serialize))) {
489 6
            $expose = $annotations->get(Expose::class);
490 6
            if (null === $expose || !$expose->shouldExpose($serialize)) {
491 4
                return true;
492
            }
493
        }
494
495 14
        return false;
496
    }
497
498
    /**
499
     * Returns true if the set version is valid for [@see Since] and [@see Until] annotations
500
     *
501
     * @param AnnotationCollection $annotations
502
     * @return bool
503
     */
504 30
    private function validVersion(AnnotationCollection $annotations): bool
505
    {
506 30
        return !$this->shouldSkipSince($annotations) && !$this->shouldSkipUntil($annotations);
507
    }
508
509
    /**
510
     * Returns true if we should skip based on the [@see Since] annotation
511
     *
512
     * @param AnnotationCollection $annotations
513
     * @return bool
514
     */
515 30
    private function shouldSkipSince(AnnotationCollection $annotations): bool
516
    {
517 30
        $sinceAnnotation = $annotations->get(Since::class);
518
519
        return
520 30
            null !== $sinceAnnotation
521 30
            && null !== $this->version
522 30
            && version_compare($this->version, $sinceAnnotation->getValue(), '<');
523
    }
524
525
    /**
526
     * Returns true if we should skip based on the [@see Until] annotation
527
     *
528
     * @param AnnotationCollection $annotations
529
     * @return bool
530
     */
531 28
    private function shouldSkipUntil(AnnotationCollection $annotations): bool
532
    {
533 28
        $untilAnnotation = $annotations->get(Until::class);
534
535
        return
536 28
            null !== $untilAnnotation
537 28
            && null !== $this->version
538 28
            && version_compare($this->version, $untilAnnotation->getValue(), '>=');
539
    }
540
}
541