Completed
Pull Request — master (#40)
by Nate
04:43 queued 02:33
created

Excluder::excludeProperty()   B

Complexity

Conditions 9
Paths 5

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 9

Importance

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

437
        if ($this->requireExpose && ($expose !== null && !$expose->/** @scrutinizer ignore-call */ shouldExpose($serialize))) {
Loading history...
438 1
            return true;
439
        }
440
441
        // don't exclude if exclude annotation doesn't exist or only exists for the other direction
442 9
        $exclude = $classMetadata->getAnnotation(Exclude::class);
443 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

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