ResourceType   F
last analyzed

Complexity

Total Complexity 105

Size/Duplication

Total Lines 870
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 225
c 1
b 0
f 0
dl 0
loc 870
rs 2
wmc 105

36 Methods

Rating   Name   Duplication   Size   Complexity  
A getInstanceType() 0 8 3
A hasETagProperties() 0 5 1
A hasNamedStream() 0 13 2
A getName() 0 3 1
A isAbstract() 0 3 1
A getAllProperties() 0 14 3
C addProperty() 0 57 12
A getPropertiesDeclaredOnThisType() 0 3 1
A getResourceTypeKind() 0 3 1
A isMediaLinkEntry() 0 3 1
A getCustomState() 0 3 1
A getNamespace() 0 3 1
A isAssignableFrom() 0 12 3
A tryResolveNamedStreamByName() 0 6 2
A hasBaseType() 0 3 1
A resolveProperty() 0 6 2
A getAllNamedStreams() 0 15 3
A validateType() 0 7 3
A setCustomState() 0 3 1
A getETagProperties() 0 11 4
A setMediaLinkEntry() 0 9 2
A __wakeup() 0 9 3
A getKeyProperties() 0 16 5
A resolvePropertyDeclaredOnThisType() 0 6 2
A tryResolveNamedStreamDeclaredOnThisTypeByName() 0 6 2
A __construct() 0 18 2
A addNamedStream() 0 28 4
A unpackEntityForPropertyGetSet() 0 6 4
C getPrimitiveResourceType() 0 34 14
A __sleep() 0 12 4
A getPropertyValue() 0 4 1
A getNamedStreamsDeclaredOnThisType() 0 3 1
A getBaseType() 0 3 1
B hasBagProperty() 0 74 11
A getFullName() 0 3 1
A setPropertyValue() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like ResourceType often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ResourceType, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace POData\Providers\Metadata;
6
7
use InvalidArgumentException;
8
use POData\Common\InvalidOperationException;
9
use POData\Common\Messages;
10
use POData\Common\ReflectionHandler;
11
use POData\Providers\Metadata\Type\Binary;
12
use POData\Providers\Metadata\Type\Boolean;
13
use POData\Providers\Metadata\Type\Byte;
14
use POData\Providers\Metadata\Type\DateTime;
15
use POData\Providers\Metadata\Type\Decimal;
16
use POData\Providers\Metadata\Type\Double;
17
use POData\Providers\Metadata\Type\EdmPrimitiveType;
18
use POData\Providers\Metadata\Type\Guid;
19
use POData\Providers\Metadata\Type\Int16;
20
use POData\Providers\Metadata\Type\Int32;
21
use POData\Providers\Metadata\Type\Int64;
22
use POData\Providers\Metadata\Type\IType;
23
use POData\Providers\Metadata\Type\SByte;
24
use POData\Providers\Metadata\Type\Single;
25
use POData\Providers\Metadata\Type\StringType;
26
use POData\Providers\Query\QueryResult;
27
use ReflectionClass;
28
use ReflectionException;
29
30
/**
31
 * Class ResourceType.
32
 *
33
 * A type to describe an entity type, complex type or primitive type.
34
 */
35
abstract class ResourceType
36
{
37
    /**
38
     * Name of the resource described by this class instance.
39
     *
40
     * @var string
41
     */
42
    protected $name;
43
44
    /**
45
     * Namespace name in which resource described by this class instance
46
     * belongs to.
47
     *
48
     * @var string
49
     */
50
    protected $namespaceName;
51
52
    /**
53
     * The fully qualified name of the resource described by this class instance.
54
     *
55
     * @var string
56
     */
57
    protected $fullName;
58
59
    /**
60
     * The type the resource described by this class instance.
61
     * Note: either Entity or Complex Type.
62
     *
63
     * @var ResourceTypeKind
64
     */
65
    protected $resourceTypeKind;
66
67
    /**
68
     * @var bool
69
     */
70
    protected $abstractType;
71
72
    /**
73
     * Reference to ResourceType instance for base type, if any.
74
     *
75
     * @var ResourceType
76
     */
77
    protected $baseType;
78
79
    /**
80
     * Collection of ResourceProperty for all properties declared on the
81
     * resource described by this class instance (This does not include
82
     * base type properties).
83
     *
84
     * @var ResourceProperty[] indexed by name
85
     */
86
    protected $propertiesDeclaredOnThisType = [];
87
88
    /**
89
     * Collection of ResourceStreamInfo for all named streams declared on
90
     * the resource described by this class instance (This does not include
91
     * base type properties).
92
     *
93
     * @var ResourceStreamInfo[] indexed by name
94
     */
95
    protected $namedStreamsDeclaredOnThisType = [];
96
97
    /**
98
     * Collection of ResourceProperty for all properties declared on this type.
99
     * and base types.
100
     *
101
     * @var ResourceProperty[] indexed by name
102
     */
103
    protected $allProperties = [];
104
105
    /**
106
     * Collection of ResourceStreamInfo for all named streams declared on this type.
107
     * and base types.
108
     *
109
     * @var ResourceStreamInfo[]
110
     */
111
    protected $allNamedStreams = [];
112
113
    /**
114
     * Collection of properties which has etag defined subset of $_allProperties.
115
     *
116
     * @var ResourceProperty[]
117
     */
118
    protected $eTagProperties = [];
119
120
    /**
121
     * Collection of key properties subset of $_allProperties.
122
     *
123
     * @var ResourceProperty[]
124
     */
125
    protected $keyProperties = [];
126
127
    /**
128
     * Whether the resource type described by this class instance is a MLE or not.
129
     *
130
     * @var bool
131
     */
132
    protected $isMediaLinkEntry = false;
133
134
    /**
135
     * Whether the resource type described by this class instance has bag properties
136
     * Note: This has been initialized with null, later in hasBagProperty method,
137
     * this flag will be set to boolean value.
138
     *
139
     * @var bool
140
     */
141
    protected $hasBagProperty = null;
142
143
    /**
144
     * Whether the resource type described by this class instance has named streams
145
     * Note: This has been initialised with null, later in hasNamedStreams method,
146
     * this flag will be set to boolean value.
147
     *
148
     * @var bool
149
     */
150
    protected $hasNamedStreams = null;
151
152
    /**
153
     * ReflectionClass (for complex/Entity) or IType (for Primitive) instance for
154
     * the resource (type) described by this class instance.
155
     *
156
     * @var ReflectionClass|IType|string
157
     */
158
    protected $type;
159
160
    /**
161
     * To store any custom information related to this class instance.
162
     *
163
     * @var object
164
     */
165
    protected $customState;
166
167
    /**
168
     * Array to detect looping in bag's complex type.
169
     *
170
     * @var array
171
     */
172
    protected $arrayToDetectLoopInComplexBag;
173
174
    /**
175
     * Create new instance of ResourceType.
176
     *
177
     * @param ReflectionClass|IType $instanceType     Instance type for the resource,
178
     *                                                for entity and
179
     *                                                complex this will
180
     *                                                be 'ReflectionClass' and for
181
     *                                                primitive type this
182
     *                                                will be IType
183
     * @param ResourceTypeKind      $resourceTypeKind Kind of resource (Entity, Complex or Primitive)
184
     * @param string                $name             Name of the resource
185
     * @param string|null           $namespaceName    Namespace of the resource
186
     * @param ResourceType|null     $baseType         Base type of the resource, if exists
187
     * @param bool                  $isAbstract       Whether resource is abstract
188
     *
189
     * @throws InvalidArgumentException
190
     */
191
    protected function __construct(
192
        $instanceType,
193
        ResourceTypeKind $resourceTypeKind,
194
        string $name,
195
        string $namespaceName = null,
196
        ResourceType $baseType = null,
197
        bool $isAbstract = false
198
    ) {
199
        $this->type                          = $instanceType;
200
        $this->resourceTypeKind              = $resourceTypeKind;
201
        $this->name                          = $name;
202
        $this->baseType                      = $baseType;
203
        $this->namespaceName                 = $namespaceName;
204
        $this->fullName                      = null === $namespaceName ? $name : $namespaceName . '.' . $name;
205
        $this->abstractType                  = $isAbstract;
206
        $this->isMediaLinkEntry              = false;
207
        $this->customState                   = null;
208
        $this->arrayToDetectLoopInComplexBag = [];
209
        //TODO: Set MLE if base type has MLE Set
210
    }
211
212
    /**
213
     * Get predefined ResourceType for a primitive type.
214
     *
215
     * @param EdmPrimitiveType $typeCode Type code of primitive type
216
     *
217
     * @throws InvalidArgumentException
218
     * @return ResourcePrimitiveType
219
     */
220
    public static function getPrimitiveResourceType(EdmPrimitiveType $typeCode): ResourcePrimitiveType
221
    {
222
        switch ($typeCode) {
223
            case EdmPrimitiveType::BINARY():
224
                return new ResourcePrimitiveType(new Binary());
225
            case EdmPrimitiveType::BOOLEAN():
226
                return new ResourcePrimitiveType(new Boolean());
227
            case EdmPrimitiveType::BYTE():
228
                return new ResourcePrimitiveType(new Byte());
229
            case EdmPrimitiveType::DATETIME():
230
                return new ResourcePrimitiveType(new DateTime());
231
            case EdmPrimitiveType::DECIMAL():
232
                return new ResourcePrimitiveType(new Decimal());
233
            case EdmPrimitiveType::DOUBLE():
234
                return new ResourcePrimitiveType(new Double());
235
            case EdmPrimitiveType::GUID():
236
                return new ResourcePrimitiveType(new Guid());
237
            case EdmPrimitiveType::INT16():
238
                return new ResourcePrimitiveType(new Int16());
239
            case EdmPrimitiveType::INT32():
240
                return new ResourcePrimitiveType(new Int32());
241
            case EdmPrimitiveType::INT64():
242
                return new ResourcePrimitiveType(new Int64());
243
            case EdmPrimitiveType::SBYTE():
244
                return new ResourcePrimitiveType(new SByte());
245
            case EdmPrimitiveType::SINGLE():
246
                return new ResourcePrimitiveType(new Single());
247
            case EdmPrimitiveType::STRING():
248
                return new ResourcePrimitiveType(new StringType());
249
            default:
250
                throw new InvalidArgumentException(
251
                    Messages::commonNotValidPrimitiveEDMType(
252
                        '$typeCode',
253
                        'getPrimitiveResourceType'
254
                    )
255
                );
256
        }
257
    }
258
259
    /**
260
     * Get reference to ResourceType for base class.
261
     *
262
     * @return ResourceType|null
263
     */
264
    public function getBaseType(): ?ResourceType
265
    {
266
        return $this->baseType;
267
    }
268
269
    /**
270
     * To check whether this resource type has base type.
271
     *
272
     * @return bool True if base type is defined, false otherwise
273
     */
274
    public function hasBaseType(): bool
275
    {
276
        return null !== $this->baseType;
277
    }
278
279
    /**
280
     * To get custom state object for this type.
281
     *
282
     * @return object
283
     */
284
    public function getCustomState()
285
    {
286
        return $this->customState;
287
    }
288
289
    /**
290
     * To set custom state object for this type.
291
     *
292
     * @param ResourceSet $object The custom object
293
     */
294
    public function setCustomState($object): void
295
    {
296
        $this->customState = $object;
297
    }
298
299
    /**
300
     * Get the instance type. If the resource type describes a complex or entity type,
301
     * then this function returns reference to ReflectionClass instance for the type.
302
     * If resource type describes a primitive type, then this function returns ITYpe.
303
     *
304
     * @throws ReflectionException
305
     * @return ReflectionClass|IType
306
     */
307
    public function getInstanceType()
308
    {
309
        if (is_string($this->type)) {
310
            $this->__wakeup();
311
        }
312
        assert($this->type instanceof ReflectionClass || $this->type instanceof IType);
313
314
        return $this->type;
315
    }
316
317
    /**
318
     * @throws ReflectionException
319
     */
320
    public function __wakeup()
321
    {
322
        if (is_string($this->type)) {
323
            $this->type = new ReflectionClass($this->type);
324
        }
325
326
        assert(
327
            $this->type instanceof ReflectionClass || $this->type instanceof IType,
328
            '_type neither instance of reflection class nor IType'
329
        );
330
    }
331
332
    /**
333
     * Get name of the type described by this resource type.
334
     *
335
     * @return string
336
     */
337
    public function getName(): string
338
    {
339
        return $this->name;
340
    }
341
342
    /**
343
     * Get the namespace under which the type described by this resource type is
344
     * defined.
345
     *
346
     * @return string
347
     */
348
    public function getNamespace(): string
349
    {
350
        return $this->namespaceName;
351
    }
352
353
    /**
354
     * To get the kind of type described by this resource class.
355
     *
356
     * @return ResourceTypeKind
357
     */
358
    public function getResourceTypeKind(): ResourceTypeKind
359
    {
360
        return $this->resourceTypeKind;
361
    }
362
363
    /**
364
     * To check whether the type described by this resource type is MLE.
365
     *
366
     * @return bool True if type is MLE else False
367
     */
368
    public function isMediaLinkEntry(): bool
369
    {
370
        return $this->isMediaLinkEntry;
371
    }
372
373
    /**
374
     * Set the resource type as MLE or non-MLE.
375
     *
376
     * @param bool $isMLE True to set as MLE, false for non-MLE
377
     *
378
     * @throws InvalidOperationException
379
     */
380
    public function setMediaLinkEntry(bool $isMLE): void
381
    {
382
        if (ResourceTypeKind::ENTITY() != $this->resourceTypeKind) {
383
            throw new InvalidOperationException(
384
                Messages::resourceTypeHasStreamAttributeOnlyAppliesToEntityType()
385
            );
386
        }
387
388
        $this->isMediaLinkEntry = $isMLE;
389
    }
390
391
    /**
392
     * Add a property belongs to this resource type instance.
393
     *
394
     * @param ResourceProperty $property Property to add
395
     * @param bool             $throw    Throw exception on name collision?
396
     *
397
     * @throws InvalidOperationException
398
     */
399
    public function addProperty(ResourceProperty $property, bool $throw = true): void
400
    {
401
        if (ResourceTypeKind::PRIMITIVE() == $this->resourceTypeKind) {
402
            throw new InvalidOperationException(
403
                Messages::resourceTypeNoAddPropertyForPrimitive()
404
            );
405
        }
406
407
        $name = $property->getName();
408
        foreach (array_keys($this->propertiesDeclaredOnThisType) as $propertyName) {
409
            if (0 == strcasecmp($propertyName, $name)) {
410
                if (false === $throw) {
411
                    return;
412
                }
413
                throw new InvalidOperationException(
414
                    Messages::resourceTypePropertyWithSameNameAlreadyExists(
415
                        $propertyName,
416
                        $this->name
417
                    )
418
                );
419
            }
420
        }
421
422
        if ($property->isKindOf(ResourcePropertyKind::KEY())) {
423
            if (ResourceTypeKind::ENTITY() != $this->resourceTypeKind) {
424
                throw new InvalidOperationException(
425
                    Messages::resourceTypeKeyPropertiesOnlyOnEntityTypes()
426
                );
427
            }
428
429
            $base        = $this->baseType;
430
            $derivedGood = null === $base || ($base instanceof ResourceEntityType && $base->isAbstract());
431
432
            if (!$derivedGood) {
433
                throw new InvalidOperationException(
434
                    Messages::resourceTypeNoKeysInDerivedTypes()
435
                );
436
            }
437
        }
438
439
        if ($property->isKindOf(ResourcePropertyKind::ETAG())
440
            && (ResourceTypeKind::ENTITY() != $this->resourceTypeKind)
441
        ) {
442
            throw new InvalidOperationException(
443
                Messages::resourceTypeETagPropertiesOnlyOnEntityTypes()
444
            );
445
        }
446
447
        //Check for Base class properties
448
        $this->propertiesDeclaredOnThisType[$name] = $property;
449
        // Set $this->allProperties to null, this is very important because the
450
        // first call to getAllProperties will initialise $this->allProperties,
451
        // further call to getAllProperties will not reinitialize _allProperties
452
        // so if addProperty is called after calling getAllProperties then the
453
        // property just added will not be reflected in $this->allProperties
454
        unset($this->allProperties);
455
        $this->allProperties = [];
456
    }
457
458
    /**
459
     * To check whether the type described by this resource type is abstract or not.
460
     *
461
     * @return bool True if type is abstract else False
462
     */
463
    public function isAbstract(): bool
464
    {
465
        return $this->abstractType;
466
    }
467
468
    /**
469
     * Get collection properties belongs to this resource type (excluding base class
470
     * properties). This function returns  empty array in case of resource type
471
     * for primitive types.
472
     *
473
     * @return ResourceProperty[]
474
     */
475
    public function getPropertiesDeclaredOnThisType(): array
476
    {
477
        return $this->propertiesDeclaredOnThisType;
478
    }
479
480
    /**
481
     * To check this type has any eTag properties.
482
     *
483
     * @return bool
484
     */
485
    public function hasETagProperties(): bool
486
    {
487
        $properties = $this->getETagProperties();
488
489
        return !empty($properties);
490
    }
491
492
    /**
493
     * Get collection of e-tag properties belongs to this type.
494
     *
495
     * @return ResourceProperty[]
496
     */
497
    public function getETagProperties(): array
498
    {
499
        if (empty($this->eTagProperties)) {
500
            foreach ($this->getAllProperties() as $propertyName => $resourceProperty) {
501
                if ($resourceProperty->isKindOf(ResourcePropertyKind::ETAG())) {
502
                    $this->eTagProperties[$propertyName] = $resourceProperty;
503
                }
504
            }
505
        }
506
507
        return $this->eTagProperties;
508
    }
509
510
    /**
511
     * Get collection properties belongs to this resource type including base class
512
     * properties. This function returns  empty array in case of resource type
513
     * for primitive types.
514
     *
515
     * @return ResourceProperty[]
516
     */
517
    public function getAllProperties(): array
518
    {
519
        if (empty($this->allProperties)) {
520
            if (null != $this->baseType) {
521
                $this->allProperties = $this->baseType->getAllProperties();
522
            }
523
524
            $this->allProperties = array_merge(
525
                $this->allProperties,
526
                $this->propertiesDeclaredOnThisType
527
            );
528
        }
529
530
        return $this->allProperties;
531
    }
532
533
    /**
534
     * Try to get ResourceProperty for a property defined for this resource type
535
     * excluding base class properties.
536
     *
537
     * @param string $propertyName The name of the property to resolve
538
     *
539
     * @return ResourceProperty|null
540
     */
541
    public function resolvePropertyDeclaredOnThisType(string $propertyName): ?ResourceProperty
542
    {
543
        if (array_key_exists($propertyName, $this->propertiesDeclaredOnThisType)) {
544
            return $this->propertiesDeclaredOnThisType[$propertyName];
545
        }
546
        return null;
547
    }
548
549
    /**
550
     * Try to get ResourceProperty for a property defined for this resource type
551
     * including base class properties.
552
     *
553
     * @param string $propertyName The name of the property to resolve
554
     *
555
     * @return ResourceProperty|null
556
     */
557
    public function resolveProperty(string $propertyName): ?ResourceProperty
558
    {
559
        if (array_key_exists($propertyName, $this->getAllProperties())) {
560
            return $this->allProperties[$propertyName];
561
        }
562
        return null;
563
    }
564
565
    /**
566
     * Add a named stream belongs to this resource type instance.
567
     *
568
     * @param ResourceStreamInfo $namedStream ResourceStreamInfo instance describing the named stream to add
569
     *
570
     * @throws InvalidOperationException
571
     */
572
    public function addNamedStream(ResourceStreamInfo $namedStream): void
573
    {
574
        if ($this->resourceTypeKind != ResourceTypeKind::ENTITY()) {
575
            throw new InvalidOperationException(
576
                Messages::resourceTypeNamedStreamsOnlyApplyToEntityType()
577
            );
578
        }
579
580
        $name = $namedStream->getName();
581
        foreach (array_keys($this->namedStreamsDeclaredOnThisType) as $namedStreamName) {
582
            if (0 == strcasecmp($namedStreamName, $name)) {
583
                throw new InvalidOperationException(
584
                    Messages::resourceTypeNamedStreamWithSameNameAlreadyExists(
585
                        $name,
586
                        $this->name
587
                    )
588
                );
589
            }
590
        }
591
592
        $this->namedStreamsDeclaredOnThisType[$name] = $namedStream;
593
        // Set $this->allNamedStreams to null, the first call to getAllNamedStreams
594
        // will initialize $this->allNamedStreams, further call to
595
        // getAllNamedStreams will not reinitialize _allNamedStreams
596
        // so if addNamedStream is called after calling getAllNamedStreams then the
597
        // property just added will not be reflected in $this->allNamedStreams
598
        unset($this->allNamedStreams);
599
        $this->allNamedStreams = [];
600
    }
601
602
    /**
603
     * Get collection of ResourceStreamInfo describing the named streams belongs
604
     * to this resource type (excluding base class properties).
605
     *
606
     * @return ResourceStreamInfo[]
607
     */
608
    public function getNamedStreamsDeclaredOnThisType(): array
609
    {
610
        return $this->namedStreamsDeclaredOnThisType;
611
    }
612
613
    /**
614
     * Try to get ResourceStreamInfo for a named stream defined for this
615
     * resource type excluding base class named streams.
616
     *
617
     * @param string $namedStreamName Name of the named stream to resolve
618
     *
619
     * @return ResourceStreamInfo|null
620
     */
621
    public function tryResolveNamedStreamDeclaredOnThisTypeByName(string $namedStreamName): ?ResourceStreamInfo
622
    {
623
        if (array_key_exists($namedStreamName, $this->namedStreamsDeclaredOnThisType)) {
624
            return $this->namedStreamsDeclaredOnThisType[$namedStreamName];
625
        }
626
        return null;
627
    }
628
629
    /**
630
     * Try to get ResourceStreamInfo for a named stream defined for this resource
631
     * type including base class named streams.
632
     *
633
     * @param string $namedStreamName Name of the named stream to resolve
634
     *
635
     * @return ResourceStreamInfo|null
636
     */
637
    public function tryResolveNamedStreamByName(string $namedStreamName): ?ResourceStreamInfo
638
    {
639
        if (array_key_exists($namedStreamName, $this->getAllNamedStreams())) {
640
            return $this->allNamedStreams[$namedStreamName];
641
        }
642
        return null;
643
    }
644
645
    /**
646
     * Get collection of ResourceStreamInfo describing the named streams belongs
647
     * to this resource type including base class named streams.
648
     *
649
     * @return ResourceStreamInfo[]
650
     */
651
    public function getAllNamedStreams(): array
652
    {
653
        if (empty($this->allNamedStreams)) {
654
            if (null != $this->baseType) {
655
                $this->allNamedStreams = $this->baseType->getAllNamedStreams();
656
            }
657
658
            $this->allNamedStreams
659
                = array_merge(
660
                    $this->allNamedStreams,
661
                    $this->namedStreamsDeclaredOnThisType
662
                );
663
        }
664
665
        return $this->allNamedStreams;
666
    }
667
668
    /**
669
     * Check this resource type instance has named stream associated with it
670
     * Note: This is an internal method used by library. Devs don't use this.
671
     *
672
     * @return bool true if resource type instance has named stream else false
673
     */
674
    public function hasNamedStream(): bool
675
    {
676
        // Note: Calling this method will initialize _allNamedStreams
677
        // and _hasNamedStreams flag to a boolean value
678
        // from null depending on the current state of _allNamedStreams
679
        // array, so method should be called only after adding all
680
        // named streams
681
        if (null === $this->hasNamedStreams) {
682
            $this->getAllNamedStreams();
683
            $this->hasNamedStreams = !empty($this->allNamedStreams);
684
        }
685
686
        return $this->hasNamedStreams;
687
    }
688
689
    /**
690
     * Check this resource type instance has bag property associated with it
691
     * Note: This is an internal method used by library. Devs don't use this.
692
     *
693
     * @param array &$arrayToDetectLoopInComplexType array for detecting loop
694
     *
695
     * @return bool|null true if resource type instance has bag property else false
696
     */
697
    public function hasBagProperty(array &$arrayToDetectLoopInComplexType): ?bool
698
    {
699
        // Note: Calling this method will initialize _bagProperties
700
        // and _hasBagProperty flag to a boolean value
701
        // from null depending on the current state of
702
        // _propertiesDeclaredOnThisType array, so method
703
        // should be called only after adding all properties
704
        if (null !== $this->hasBagProperty) {
705
            return $this->hasBagProperty;
706
        }
707
708
        if (null != $this->baseType && $this->baseType->hasBagProperty($arrayToDetectLoopInComplexType)) {
709
            $this->hasBagProperty = true;
710
        } else {
711
            foreach ($this->propertiesDeclaredOnThisType as $resourceProperty) {
712
                $hasBagInComplex = false;
713
                if ($resourceProperty->isKindOf(ResourcePropertyKind::COMPLEX_TYPE())) {
714
                    //We can say current ResourceType ("this")
715
                    //is contains a bag property if:
716
                    //1. It contain a property of kind bag.
717
                    //2. It contains a normal complex property
718
                    //with a sub property of kind bag.
719
                    //The second case can be further expanded, i.e.
720
                    //if the normal complex property
721
                    //has a normal complex sub property with a
722
                    //sub property of kind bag.
723
                    //So for complex type we recursively call this
724
                    //function to check for bag.
725
                    //Shown below how looping can happen in complex type:
726
                    //Customer ResourceType (id1)
727
                    //{
728
                    //  ....
729
                    //  Address: Address ResourceType (id2)
730
                    //  {
731
                    //    .....
732
                    //    AltAddress: Address ResourceType (id2)
733
                    //    {
734
                    //      ...
735
                    //    }
736
                    //  }
737
                    //}
738
739
                    //Here the resource type of Customer::Address and
740
                    //Customer::Address::AltAddress
741
                    //are same, this is a loop, we need to detect
742
                    //this and avoid infinite recursive loop.
743
744
                    $count     = count($arrayToDetectLoopInComplexType);
745
                    $foundLoop = false;
746
                    for ($i = 0; $i < $count; ++$i) {
747
                        if ($arrayToDetectLoopInComplexType[$i] === $resourceProperty->getResourceType()) {
748
                            $foundLoop = true;
749
                            break;
750
                        }
751
                    }
752
753
                    if (!$foundLoop) {
754
                        $arrayToDetectLoopInComplexType[$count] = $resourceProperty->getResourceType();
755
                        $hasBagInComplex                        = $resourceProperty
756
                            ->getResourceType()
757
                            ->hasBagProperty($arrayToDetectLoopInComplexType);
758
                        unset($arrayToDetectLoopInComplexType[$count]);
759
                    }
760
                }
761
762
                if ($resourceProperty->isKindOf(ResourcePropertyKind::BAG()) ||
763
                    $hasBagInComplex) {
764
                    $this->hasBagProperty = true;
765
                    break;
766
                }
767
            }
768
        }
769
770
        return $this->hasBagProperty;
771
    }
772
773
    /**
774
     * Validate the type.
775
     *
776
     *
777
     * @throws InvalidOperationException
778
     */
779
    public function validateType(): void
780
    {
781
        $keyProperties = $this->getKeyProperties();
782
        if (($this->resourceTypeKind == ResourceTypeKind::ENTITY()) && empty($keyProperties)) {
783
            throw new InvalidOperationException(
784
                Messages::resourceTypeMissingKeyPropertiesForEntity(
785
                    $this->getFullName()
786
                )
787
            );
788
        }
789
    }
790
791
    /**
792
     * Get collection key properties belongs to this resource type. This
793
     * function returns non-empty array only for resource type representing
794
     * an entity type.
795
     *
796
     * @return ResourceProperty[]
797
     */
798
    public function getKeyProperties(): array
799
    {
800
        if (empty($this->keyProperties)) {
801
            $baseType = $this;
802
            while (null != $baseType->baseType) {
803
                $baseType = $baseType->baseType;
804
            }
805
806
            foreach ($baseType->propertiesDeclaredOnThisType as $propertyName => $resourceProperty) {
807
                if ($resourceProperty->isKindOf(ResourcePropertyKind::KEY())) {
808
                    $this->keyProperties[$propertyName] = $resourceProperty;
809
                }
810
            }
811
        }
812
813
        return $this->keyProperties;
814
    }
815
816
    /**
817
     * Get full name (namespacename.typename) of the type described by this resource
818
     * type.
819
     *
820
     * @return string
821
     */
822
    public function getFullName(): string
823
    {
824
        return $this->fullName;
825
    }
826
827
    /**
828
     * To check the type described by this resource type is assignable from
829
     * a type described by another resource type. Or this type is a sub-type
830
     * of (derived from the) given resource type.
831
     *
832
     * @param ResourceType $resourceType Another resource type
833
     *
834
     * @return bool
835
     */
836
    public function isAssignableFrom(ResourceType $resourceType): bool
837
    {
838
        $base = $this;
839
        while (null != $base) {
840
            if ($resourceType == $base) {
841
                return true;
842
            }
843
844
            $base = $base->baseType;
845
        }
846
847
        return false;
848
    }
849
850
    /**
851
     * @param mixed  $entity
852
     * @param string $property
853
     * @param mixed  $value
854
     *
855
     * @throws ReflectionException
856
     * @return ResourceType
857
     */
858
    public function setPropertyValue($entity, string $property, $value)
859
    {
860
        $targ = $this->unpackEntityForPropertyGetSet($entity);
861
        ReflectionHandler::setProperty($targ, $property, $value);
862
863
        return $this;
864
    }
865
866
    /**
867
     * @param $entity
868
     * @return mixed|null
869
     */
870
    private function unpackEntityForPropertyGetSet($entity)
871
    {
872
        $targ = ($entity instanceof QueryResult)
873
            ? (is_array($entity->results) && 0 < count($entity->results)) ? $entity->results[0] : null
874
            : $entity;
875
        return $targ;
876
    }
877
878
    /**
879
     * @param $entity
880
     * @param $property
881
     * @throws ReflectionException
882
     * @return mixed
883
     */
884
    public function getPropertyValue($entity, $property)
885
    {
886
        $targ = $this->unpackEntityForPropertyGetSet($entity);
887
        return ReflectionHandler::getProperty($targ, $property);
888
    }
889
890
    /**
891
     * @return array
892
     */
893
    public function __sleep()
894
    {
895
        if (null == $this->type || $this->type instanceof IType) {
896
            return array_keys(get_object_vars($this));
897
        }
898
        if (is_object($this->type)) {
899
            $this->type = $this->type->name;
900
        }
901
        assert(is_string($this->type), 'Type name should be a string at end of serialisation');
902
        $result = array_keys(get_object_vars($this));
903
904
        return $result;
905
    }
906
}
907