addPrimitivePropertyInternal()   B
last analyzed

Complexity

Conditions 8
Paths 18

Size

Total Lines 53
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 28
c 0
b 0
f 0
nc 18
nop 8
dl 0
loc 53
rs 8.4444

How to fix   Long Method    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace POData\Providers\Metadata;
6
7
use AlgoWeb\ODataMetadata\MetadataManager;
8
use AlgoWeb\ODataMetadata\MetadataV3\edm\TComplexTypeType;
9
use AlgoWeb\ODataMetadata\MetadataV3\edm\TEntityTypeType;
10
use ErrorException;
11
use Exception;
12
use InvalidArgumentException;
13
use POData\Common\InvalidOperationException;
14
use POData\Pluralizer;
15
use POData\Providers\Metadata\Type\EdmPrimitiveType;
16
use POData\Providers\Metadata\Type\IType;
17
use ReflectionClass;
18
use ReflectionException;
19
use stdClass;
20
21
/**
22
 * Class SimpleMetadataProvider.
23
 */
24
class SimpleMetadataProvider implements IMetadataProvider
25
{
26
    public $oDataEntityMap     = [];
27
    protected $resourceSets    = [];
28
    protected $resourceTypes   = [];
29
    protected $associationSets = [];
30
    protected $containerName;
31
    protected $namespaceName;
32
    protected $singletons = [];
33
    private $metadataManager;
34
    private $typeSetMapping = [];
35
    private $baseTypes      = [];
36
37
    /**
38
     * @param string $containerName container name for the data source
39
     * @param string $namespaceName namespace for the data source
40
     */
41
    public function __construct(string $containerName, string $namespaceName)
42
    {
43
        $this->containerName   = $containerName;
44
        $this->namespaceName   = $namespaceName;
45
        $this->metadataManager = new MetadataManager($namespaceName, $containerName);
46
    }
47
48
    //Begin Implementation of IMetadataProvider
49
50
    /**
51
     * @throws Exception
52
     * @return string|null
53
     */
54
    public function getXML(): ?string
55
    {
56
        return $this->getMetadataManager()->getEdmxXML();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getMetadataManager()->getEdmxXML() returns the type JMS\Serializer\Serializer which is incompatible with the type-hinted return null|string.
Loading history...
57
    }
58
59
    /**
60
     * @return MetadataManager
61
     */
62
    public function getMetadataManager(): MetadataManager
63
    {
64
        return $this->metadataManager;
65
    }
66
67
    /**
68
     * get the Container name for the data source.
69
     *
70
     * @return string container name
71
     */
72
    public function getContainerName(): string
73
    {
74
        return $this->containerName;
75
    }
76
77
    /**
78
     * get Namespace name for the data source.
79
     *
80
     * @return string namespace
81
     */
82
    public function getContainerNamespace(): string
83
    {
84
        return $this->namespaceName;
85
    }
86
87
    /**
88
     * get all entity set information.
89
     *
90
     * @param null|mixed $params
91
     *
92
     * @throws ErrorException
93
     * @return ResourceSet[]
94
     */
95
    public function getResourceSets($params = null): array
96
    {
97
        $parameters = [];
98
        if (is_string($params)) {
99
            $parameters[] = $params;
100
        } elseif (isset($params) && !is_array($params)) {
101
            throw new ErrorException('Input parameter must be absent, null, string or array');
102
        } else {
103
            $parameters = $params;
104
        }
105
        if (!is_array($parameters) || 0 == count($parameters)) {
106
            return array_values($this->resourceSets);
107
        }
108
        assert(is_array($parameters));
109
        $return  = [];
110
        $counter = 0;
111
        foreach ($this->resourceSets as $resource) {
112
            $resName = $resource->getName();
113
            if (in_array($resName, $parameters)) {
114
                $return[] = $resource;
115
                $counter++;
116
            }
117
        }
118
        assert($counter == count($return));
119
120
        return $return;
121
    }
122
123
    /**
124
     * get all resource types in the data source.
125
     *
126
     * @return ResourceType[]
127
     */
128
    public function getTypes(): array
129
    {
130
        return array_values($this->resourceTypes);
131
    }
132
133
    /**
134
     * get a resource set based on the specified resource set name.
135
     *
136
     * @param string $name Name of the resource set
137
     *
138
     * @return ResourceSet|null resource set with the given name if found else NULL
139
     */
140
    public function resolveResourceSet(string $name): ?ResourceSet
141
    {
142
        if (array_key_exists($name, $this->resourceSets)) {
143
            return $this->resourceSets[$name];
144
        }
145
        return null;
146
    }
147
148
    /**
149
     * get a resource type based on the resource type name.
150
     *
151
     * @param string $name Name of the resource type
152
     *
153
     * @return ResourceType|null resource type with the given resource type name if found else NULL
154
     */
155
    public function resolveResourceType(string $name): ?ResourceType
156
    {
157
        if (array_key_exists($name, $this->resourceTypes)) {
158
            return $this->resourceTypes[$name];
159
        }
160
        return null;
161
    }
162
163
    /**
164
     * get a singleton based on the specified singleton name.
165
     *
166
     * @param string $name Name of the resource set
167
     *
168
     * @return ResourceFunctionType|null singleton with the given name if found else NULL
169
     */
170
    public function resolveSingleton(string $name): ?ResourceFunctionType
171
    {
172
        if (array_key_exists($name, $this->singletons)) {
173
            return $this->singletons[$name];
174
        }
175
        return null;
176
    }
177
178
    /**
179
     * get a resource set based on the specified resource association set name.
180
     *
181
     * @param string $name Name of the resource association set
182
     *
183
     * @return ResourceAssociationSet|null resource association set with the given name if found else NULL
184
     */
185
    public function resolveAssociationSet(string $name): ?ResourceAssociationSet
186
    {
187
        if (array_key_exists($name, $this->associationSets)) {
188
            return $this->associationSets[$name];
189
        }
190
        return null;
191
    }
192
193
    /**
194
     * Get number of association sets hooked up.
195
     *
196
     * @return int
197
     */
198
    public function getAssociationCount(): int
199
    {
200
        return count($this->associationSets);
201
    }
202
203
    /**
204
     * The method must return a collection of all the types derived from
205
     * $resourceType The collection returned should NOT include the type
206
     * passed in as a parameter.
207
     *
208
     * @param ResourceEntityType $resourceType Resource to get derived resource types from
209
     *
210
     * @return ResourceType[]
211
     */
212
    public function getDerivedTypes(ResourceEntityType $resourceType): array
213
    {
214
        $ret = [];
215
        foreach ($this->resourceTypes as $rType) {
216
            if ($rType->getBaseType() == $resourceType) {
217
                $ret[] = $rType;
218
            }
219
        }
220
        return $ret;
221
    }
222
223
    /**
224
     * @param ResourceEntityType $resourceType Resource to check for derived resource types
225
     *
226
     * @return bool true if $resourceType represents an Entity Type which has derived Entity Types, else false
227
     */
228
    public function hasDerivedTypes(ResourceEntityType $resourceType): bool
229
    {
230
        if (in_array($resourceType, $this->baseTypes)) {
231
            return true;
232
        }
233
        return false;
234
    }
235
236
    //End Implementation of IMetadataProvider
237
238
    /**
239
     * Gets the ResourceAssociationSet instance for the given source
240
     * association end.
241
     *
242
     * @param ResourceSet        $sourceResourceSet      Resource set of the source association end
243
     * @param ResourceEntityType $sourceResourceType     Resource type of the source association end
244
     * @param ResourceProperty   $targetResourceProperty Resource property of the target association end
245
     *
246
     * @throws InvalidOperationException
247
     * @return ResourceAssociationSet|null
248
     */
249
    public function getResourceAssociationSet(
250
        ResourceSet $sourceResourceSet,
251
        ResourceEntityType $sourceResourceType,
252
        ResourceProperty $targetResourceProperty
253
    ): ?ResourceAssociationSet {
254
        //e.g.
255
        //ResourceSet => Representing 'Customers' entity set
256
        //ResourceType => Representing'Customer' entity type
257
        //ResourceProperty => Representing 'Orders' property
258
        //We have created ResourceAssociationSet while adding
259
        //ResourceSetReference or ResourceReference
260
        //and kept in $this->associationSets
261
        //$metadata->addResourceSetReferenceProperty(
262
        //             $customersEntityType,
263
        //             'Orders',
264
        //             $ordersResourceSet
265
        //             );
266
267
        $targetResourceSet = $targetResourceProperty->getResourceType()->getCustomState();
268
        if (null === $targetResourceSet) {
269
            throw new InvalidOperationException(
270
                'Failed to retrieve the custom state from ' . $targetResourceProperty->getResourceType()->getName()
271
            );
272
        }
273
        assert($targetResourceSet instanceof ResourceSet);
274
275
        //Customer_Orders_Orders, Order_Customer_Customers
276
        $key = ResourceAssociationSet::keyName(
277
            $sourceResourceType,
278
            $targetResourceProperty->getName(),
279
            $targetResourceSet
280
        );
281
282
        $associationSet = array_key_exists($key, $this->associationSets) ? $this->associationSets[$key] : null;
283
        assert(
284
            null == $associationSet || $associationSet instanceof ResourceAssociationSet,
285
            'Retrieved resource association must be either null or an instance of ResourceAssociationSet'
286
        );
287
        return $associationSet;
288
    }
289
290
    /**
291
     * Add an entity type.
292
     *
293
     * @param  ReflectionClass           $refClass   reflection class of the entity
294
     * @param  string                    $name       name of the entity
295
     * @param  null|string               $pluralName Optional custom resource set name
296
     * @param  mixed                     $isAbstract
297
     * @param  null|mixed                $baseType
298
     * @throws InvalidOperationException when the name is already in use
299
     * @return ResourceEntityType        when the name is already in use
300
     * @internal param string $namespace namespace of the data source
301
     */
302
    public function addEntityType(
303
        ReflectionClass $refClass,
304
        string $name,
305
        string $pluralName = null,
306
        bool $isAbstract = false,
307
        $baseType = null
308
    ): ResourceEntityType {
309
        $result = $this->createResourceType(
310
            $refClass,
311
            $name,
312
            ResourceTypeKind::ENTITY(),
313
            $isAbstract,
314
            $baseType,
315
            $pluralName
316
        );
317
        assert($result instanceof ResourceEntityType);
318
        return $result;
319
    }
320
321
    /**
322
     * @param  ReflectionClass                        $refClass
323
     * @param  string                                 $name
324
     * @param  ResourceTypeKind                       $typeKind
325
     * @param  mixed                                  $isAbstract
326
     * @param  null|mixed                             $baseType
327
     * @param  null|mixed                             $pluralName
328
     * @throws InvalidOperationException
329
     * @return ResourceEntityType|ResourceComplexType
330
     * @internal param null|string $namespace
331
     * @internal param null|ResourceType $baseResourceType
332
     */
333
    private function createResourceType(
334
        ReflectionClass $refClass,
335
        string $name,
336
        ResourceTypeKind $typeKind,
337
        bool $isAbstract = false,
338
        ResourceType $baseType = null,
339
        string $pluralName = null
340
    ): ResourceType {
341
        if (array_key_exists($name, $this->resourceTypes)) {
342
            throw new InvalidOperationException('Type with same name already added');
343
        }
344
        if (null !== $baseType) {
345
            $baseTEntityType   = $this->oDataEntityMap[$baseType->getFullName()];
346
            $this->baseTypes[] = $baseType;
347
        } else {
348
            $baseTEntityType = null;
349
        }
350
351
        $type = null;
352
        if ($typeKind == ResourceTypeKind::ENTITY()) {
353
            list($oet, $entitySet) = $this->getMetadataManager()
354
                ->addEntityType($name, $baseTEntityType, $isAbstract, 'Public', null, null, $pluralName);
355
            assert($oet instanceof TEntityTypeType, 'Entity type ' . $name . ' not successfully added');
356
            $type                              = new ResourceEntityType($refClass, $oet, $this);
357
            $typeName                          = $type->getFullName();
358
            $returnName                        = MetadataManager::getResourceSetNameFromResourceType($typeName);
359
            $this->oDataEntityMap[$typeName]   = $oet;
360
            $this->typeSetMapping[$name]       = $entitySet;
361
            $this->typeSetMapping[$typeName]   = $entitySet;
362
            $this->typeSetMapping[$returnName] = $entitySet;
363
        } elseif ($typeKind == ResourceTypeKind::COMPLEX()) {
364
            $complex = new TComplexTypeType();
365
            $complex->setName($name);
366
            $type = new ResourceComplexType($refClass, $complex);
367
        }
368
        assert(null != $type, 'Type variable must not be null');
369
370
        $this->resourceTypes[$name] = $type;
371
        ksort($this->resourceTypes);
372
373
        return $type;
374
    }
375
376
    /**
377
     * Add a complex type.
378
     *
379
     * @param  ReflectionClass           $refClass reflection class of the complex entity type
380
     * @param  string                    $name     name of the entity
381
     * @throws InvalidOperationException when the name is already in use
382
     * @return ResourceComplexType
383
     *
384
     * @internal param string $namespace namespace of the data source
385
     * @internal param ResourceType $baseResourceType base resource type
386
     */
387
    public function addComplexType(ReflectionClass $refClass, string $name): ResourceComplexType
388
    {
389
        $result = $this->createResourceType($refClass, $name, ResourceTypeKind::COMPLEX());
390
        assert($result instanceof ResourceComplexType);
391
        return $result;
392
    }
393
394
    /**
395
     * @param string|null        $name         name of the resource set (now taken from resource type)
396
     * @param ResourceEntityType $resourceType resource type
397
     *
398
     * @throws InvalidOperationException
399
     * @return ResourceSet
400
     */
401
    public function addResourceSet(?string $name, ResourceEntityType $resourceType): ResourceSet
402
    {
403
        $typeName   = $resourceType->getFullName();
404
        $pluralName = Pluralizer::plural($typeName);
405
        if (null === $name) {
406
            $returnName = $pluralName;
407
        } else {
408
            // handle case where $name is a fully-qualified PHP class name
409
            $nameBits = explode('\\', $name);
410
            $numBits  = count($nameBits);
411
412
            if ($typeName == $nameBits[$numBits - 1]) {
413
                $returnName = $pluralName;
414
            } else {
415
                $returnName = $name;
416
            }
417
        }
418
419
        if (array_key_exists($returnName, $this->resourceSets)) {
420
            throw new InvalidOperationException('Resource Set already added');
421
        }
422
        $resourceType->validateType();
423
424
        $this->resourceSets[$returnName] = new ResourceSet($returnName, $resourceType);
425
426
        //No support for multiple ResourceSet with same EntityType
427
        //So keeping reference to the 'ResourceSet' with the entity type
428
        $resourceType->setCustomState($this->resourceSets[$returnName]);
429
        ksort($this->resourceSets);
430
431
        return $this->resourceSets[$returnName];
432
    }
433
434
    /**
435
     * To add a Key-primitive property to a resource (Complex/Entity).
436
     *
437
     * @param  ResourceType              $resourceType resource type to which key property
438
     *                                                 is to be added
439
     * @param  string                    $name         name of the key property
440
     * @param  EdmPrimitiveType          $typeCode     type of the key property
441
     * @throws InvalidOperationException
442
     * @throws ReflectionException
443
     */
444
    public function addKeyProperty(ResourceType $resourceType, string $name, EdmPrimitiveType $typeCode): void
445
    {
446
        $this->addPrimitivePropertyInternal($resourceType, $name, $typeCode, true);
447
    }
448
449
    /**
450
     * To add a Key/NonKey-primitive property to a resource (complex/entity).
451
     *
452
     * @param ResourceType     $resourceType   Resource type
453
     * @param string           $name           name of the property
454
     * @param EdmPrimitiveType $typeCode       type of property
455
     * @param bool             $isKey          property is key or not
456
     * @param bool             $isBag          property is bag or not
457
     * @param bool             $isETagProperty property is etag or not
458
     * @param null|mixed       $defaultValue
459
     * @param bool             $nullable
460
     *
461
     * @throws InvalidOperationException
462
     * @throws ReflectionException
463
     */
464
    private function addPrimitivePropertyInternal(
465
        ResourceType $resourceType,
466
        string $name,
467
        EdmPrimitiveType $typeCode = null,
468
        bool $isKey = false,
469
        bool $isBag = false,
470
        bool $isETagProperty = false,
471
        $defaultValue = null,
472
        bool $nullable = false
473
    ): void {
474
        if ($isETagProperty && $isBag) {
475
            throw new InvalidOperationException(
476
                'Only primitive property can be etag property, bag property cannot be etag property.'
477
            );
478
        }
479
480
        $this->checkInstanceProperty($name, $resourceType);
481
482
        // check that property and resource name don't up and collide - would violate OData spec
483
        if (strtolower($name) == strtolower($resourceType->getName())) {
484
            throw new InvalidOperationException(
485
                'Property name must be different from resource name.'
486
            );
487
        }
488
489
        $primitiveResourceType = ResourceType::getPrimitiveResourceType($typeCode);
0 ignored issues
show
Bug introduced by
It seems like $typeCode can also be of type null; however, parameter $typeCode of POData\Providers\Metadat...PrimitiveResourceType() does only seem to accept POData\Providers\Metadata\Type\EdmPrimitiveType, maybe add an additional type check? ( Ignorable by Annotation )

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

489
        $primitiveResourceType = ResourceType::getPrimitiveResourceType(/** @scrutinizer ignore-type */ $typeCode);
Loading history...
490
491
        $kind = $isKey ? ResourcePropertyKind::PRIMITIVE()->setKEY(true) : ResourcePropertyKind::PRIMITIVE();
492
        if ($isBag) {
493
            $kind = $kind->setBAG(true);
494
        }
495
496
        if ($isETagProperty) {
497
            $kind = $kind->setETAG(true);
498
        }
499
500
        $kind = new ResourcePropertyKind($kind);
501
502
        $resourceProperty = new ResourceProperty(
503
            $name,
504
            null,
505
            $kind,
506
            $primitiveResourceType
507
        );
508
        $resourceType->addProperty($resourceProperty);
509
        if (array_key_exists($resourceType->getFullName(), $this->oDataEntityMap)) {
510
            $this->metadataManager->addPropertyToEntityType(
511
                $this->oDataEntityMap[$resourceType->getFullName()],
512
                $name,
513
                $primitiveResourceType->getFullName(),
514
                $defaultValue,
515
                $nullable,
516
                $isKey
517
            );
518
        }
519
    }
520
521
    /**
522
     * @param string       $name
523
     * @param ResourceType $resourceType
524
     *
525
     * @throws InvalidOperationException
526
     * @throws ReflectionException
527
     */
528
    private function checkInstanceProperty(string $name, ResourceType $resourceType): void
529
    {
530
        $instance       = $resourceType->getInstanceType();
531
        $hasMagicGetter = $instance instanceof IType || $instance->hasMethod('__get');
532
        if ($instance instanceof ReflectionClass) {
533
            $hasMagicGetter |= $instance->isInstance(new stdClass());
534
        }
535
536
        if (!$hasMagicGetter) {
537
            try {
538
                if ($instance instanceof ReflectionClass) {
539
                    $instance->getProperty($name);
540
                }
541
            } catch (ReflectionException $exception) {
542
                throw new InvalidOperationException(
543
                    'Can\'t add a property which does not exist on the instance type.'
544
                );
545
            }
546
        }
547
    }
548
549
    /**
550
     * To add a NonKey-primitive property (Complex/Entity).
551
     *
552
     * @param  ResourceType              $resourceType resource type to which key property
553
     *                                                 is to be added
554
     * @param  string                    $name         name of the key property
555
     * @param  EdmPrimitiveType          $typeCode     type of the key property
556
     * @param  bool                      $isBag        property is bag or not
557
     * @param  null|mixed                $defaultValue
558
     * @param  bool                      $nullable
559
     * @throws InvalidOperationException
560
     * @throws ReflectionException
561
     */
562
    public function addPrimitiveProperty(
563
        ResourceType $resourceType,
564
        string $name,
565
        EdmPrimitiveType $typeCode,
566
        bool $isBag = false,
567
        $defaultValue = null,
568
        bool $nullable = false
569
    ): void {
570
        $this->addPrimitivePropertyInternal(
571
            $resourceType,
572
            $name,
573
            $typeCode,
574
            false,
575
            $isBag,
576
            false,
577
            $defaultValue,
578
            $nullable
579
        );
580
    }
581
582
    /**
583
     * To add a non-key etag property.
584
     *
585
     * @param  ResourceType              $resourceType resource type to which key property
586
     *                                                 is to be added
587
     * @param  string                    $name         name of the property
588
     * @param  EdmPrimitiveType          $typeCode     type of the etag property
589
     * @param  null|mixed                $defaultValue
590
     * @param  mixed                     $nullable
591
     * @throws InvalidOperationException
592
     * @throws ReflectionException
593
     */
594
    public function addETagProperty(
595
        ResourceType $resourceType,
596
        string $name,
597
        EdmPrimitiveType $typeCode,
598
        $defaultValue = null,
599
        bool $nullable = false
600
    ): void {
601
        $this->addPrimitivePropertyInternal(
602
            $resourceType,
603
            $name,
604
            $typeCode,
605
            false,
606
            false,
607
            true,
608
            $defaultValue,
609
            $nullable
610
        );
611
    }
612
613
    /**
614
     * To add a resource reference property.
615
     *
616
     * @param  ResourceEntityType        $resourceType      The resource type to add the resource
617
     *                                                      reference property to
618
     * @param  string                    $name              The name of the property to add
619
     * @param  ResourceSet               $targetResourceSet The resource set the resource reference
620
     *                                                      property points to
621
     * @param  bool                      $flip
622
     * @param  bool                      $many
623
     * @param  ResourceEntityType|null   $concreteType      Underlying concrete resource reference type, if set
624
     * @throws InvalidOperationException
625
     * @throws ReflectionException
626
     */
627
    public function addResourceReferenceProperty(
628
        ResourceEntityType $resourceType,
629
        string $name,
630
        ResourceSet $targetResourceSet,
631
        bool $flip = false,
632
        bool $many = false,
633
        ResourceEntityType $concreteType = null
634
    ): void {
635
        $this->addReferencePropertyInternal(
636
            $resourceType,
637
            $name,
638
            $targetResourceSet,
639
            $flip ? '0..1' : '1',
640
            $many,
641
            $concreteType
642
        );
643
    }
644
645
    /**
646
     * To add a navigation property (resource set or resource reference)
647
     * to a resource type.
648
     *
649
     * @param ResourceEntityType $sourceResourceType The resource type to add the resource reference
650
     *                                               or resource reference set property to
651
     * @param string             $name               The name of the property to add
652
     * @param ResourceSet        $targetResourceSet  The resource set the
653
     *                                               resource reference or reference
654
     *                                               set property points to
655
     * @param string             $resourceMult       The multiplicity of relation being added
656
     * @param bool               $many
657
     *
658
     * @param  ResourceEntityType|null   $concreteType
659
     * @throws InvalidOperationException
660
     * @throws ReflectionException
661
     */
662
    private function addReferencePropertyInternal(
663
        ResourceEntityType $sourceResourceType,
664
        string $name,
665
        ResourceSet $targetResourceSet,
666
        string $resourceMult,
667
        bool $many = false,
668
        ResourceEntityType $concreteType = null
669
    ): void {
670
        $allowedMult   = ['*', '1', '0..1'];
671
        $backMultArray = ['*' => '*', '1' => '0..1', '0..1' => '1'];
672
        $this->checkInstanceProperty($name, $sourceResourceType);
673
674
        // check that property and resource name don't up and collide - would violate OData spec
675
        if (strtolower($name) == strtolower($sourceResourceType->getName())) {
676
            throw new InvalidOperationException(
677
                'Property name must be different from resource name.'
678
            );
679
        }
680
        assert(in_array($resourceMult, $allowedMult), 'Supplied multiplicity ' . $resourceMult . ' not valid');
681
682
        $resourcePropertyKind = ('*' == $resourceMult)
683
            ? ResourcePropertyKind::RESOURCESET_REFERENCE()
684
            : ResourcePropertyKind::RESOURCE_REFERENCE();
685
        $targetResourceType     = $targetResourceSet->getResourceType();
686
        $sourceResourceProperty = new ResourceProperty($name, null, $resourcePropertyKind, $targetResourceType);
687
        $sourceResourceType->addProperty($sourceResourceProperty);
688
689
        //Create instance of AssociationSet for this relationship
690
        $sourceResourceSet = $sourceResourceType->getCustomState();
691
        if (!$sourceResourceSet instanceof ResourceSet) {
692
            throw new InvalidOperationException(
693
                'Failed to retrieve the custom state from '
694
                . $sourceResourceType->getName()
695
            );
696
        }
697
698
        //Customer_Orders_Orders, Order_Customer_Customers
699
        //(source type::name _ source property::name _ target set::name)
700
        $setKey = ResourceAssociationSet::keyName($sourceResourceType, $name, $targetResourceSet);
701
        //$setKey = $sourceResourceType->getName() . '_' . $name . '_' . $targetResourceType->getName();
702
        $set = new ResourceAssociationSet(
703
            $setKey,
704
            new ResourceAssociationSetEnd(
705
                $sourceResourceSet,
706
                $sourceResourceType,
707
                $sourceResourceProperty
708
            ),
709
            new ResourceAssociationSetEnd($targetResourceSet, $targetResourceType, null, $concreteType)
710
        );
711
        $mult     = $resourceMult;
712
        $backMult = $many ? '*' : $backMultArray[$resourceMult];
713
        if (false === $many && '*' == $backMult && '*' == $resourceMult) {
714
            $backMult = '1';
715
        }
716
        $this->getMetadataManager()->addNavigationPropertyToEntityType(
717
            $this->oDataEntityMap[$sourceResourceType->getFullName()],
718
            $mult,
719
            $name,
720
            $this->oDataEntityMap[$targetResourceType->getFullName()],
721
            $backMult
722
        );
723
        $this->associationSets[$setKey] = $set;
724
    }
725
726
    /**
727
     * To add a 1:N resource reference property.
728
     *
729
     * @param  ResourceEntityType        $sourceResourceType The resource type to add the resource
730
     *                                                       reference property from
731
     * @param  ResourceEntityType        $targetResourceType The resource type to add the resource
732
     *                                                       reference property to
733
     * @param  string                    $sourceProperty     The name of the property to add, on source type
734
     * @param  string                    $targetProperty     The name of the property to add, on target type
735
     * @param  bool                      $nullable           Is singleton side of relation nullable?
736
     * @throws InvalidOperationException
737
     * @throws ReflectionException
738
     */
739
    public function addResourceReferencePropertyBidirectional(
740
        ResourceEntityType $sourceResourceType,
741
        ResourceEntityType $targetResourceType,
742
        string $sourceProperty,
743
        string $targetProperty,
744
        bool $nullable = false
745
    ): void {
746
        $this->addReferencePropertyInternalBidirectional(
747
            $sourceResourceType,
748
            $targetResourceType,
749
            $sourceProperty,
750
            $targetProperty,
751
            '*',
752
            true === $nullable ? '0..1' : '1'
753
        );
754
        // verify resource property types are what we expect them to be
755
        $sourceResourceKind = $sourceResourceType->resolveProperty($sourceProperty)->getKind();
756
        assert(
757
            ResourcePropertyKind::RESOURCE_REFERENCE() == $sourceResourceKind,
758
            '1 side of 1:N relationship not pointing to resource reference'
759
        );
760
        $targetResourceKind = $targetResourceType->resolveProperty($targetProperty)->getKind();
761
        assert(
762
            ResourcePropertyKind::RESOURCESET_REFERENCE() == $targetResourceKind,
763
            'N side of 1:N relationship not pointing to resource set reference'
764
        );
765
    }
766
767
    /**
768
     * To add a navigation property (resource set or resource reference)
769
     * to a resource type.
770
     *
771
     * @param ResourceEntityType $sourceResourceType The source resource type to add
772
     *                                               the resource reference
773
     *                                               or resource reference set property to
774
     * @param ResourceEntityType $targetResourceType The target resource type to add
775
     *                                               the resource reference
776
     *                                               or resource reference set property to
777
     * @param string             $sourceProperty     The name of the
778
     *                                               property to add to source type
779
     * @param string             $targetProperty     The name of the
780
     *                                               property to add to target type
781
     * @param string             $sourceMultiplicity The multiplicity at the source end of relation
782
     * @param string             $targetMultiplicity The multiplicity at the target end of relation
783
     *
784
     * @throws InvalidOperationException
785
     * @throws ReflectionException
786
     */
787
    private function addReferencePropertyInternalBidirectional(
788
        ResourceEntityType $sourceResourceType,
789
        ResourceEntityType $targetResourceType,
790
        string $sourceProperty,
791
        string $targetProperty,
792
        string $sourceMultiplicity,
793
        string $targetMultiplicity
794
    ): void {
795
        $this->checkInstanceProperty($sourceProperty, $sourceResourceType);
796
        $this->checkInstanceProperty($targetProperty, $targetResourceType);
797
798
        // check that property and resource name don't up and collide - would violate OData spec
799
        if (strtolower($sourceProperty) == strtolower($sourceResourceType->getName())) {
800
            throw new InvalidOperationException(
801
                'Source property name must be different from source resource name.'
802
            );
803
        }
804
        if (strtolower($targetProperty) == strtolower($targetResourceType->getName())) {
805
            throw new InvalidOperationException(
806
                'Target property name must be different from target resource name.'
807
            );
808
        }
809
810
        //Create instance of AssociationSet for this relationship
811
        $sourceResourceSet = $sourceResourceType->getCustomState();
812
        if (!$sourceResourceSet instanceof ResourceSet) {
813
            throw new InvalidOperationException(
814
                'Failed to retrieve the custom state from '
815
                . $sourceResourceType->getName()
816
            );
817
        }
818
        $targetResourceSet = $targetResourceType->getCustomState();
819
        if (!$targetResourceSet instanceof ResourceSet) {
820
            throw new InvalidOperationException(
821
                'Failed to retrieve the custom state from '
822
                . $targetResourceType->getName()
823
            );
824
        }
825
826
        //Customer_Orders_Orders, Order_Customer_Customers
827
        $fwdSetKey = ResourceAssociationSet::keyName($sourceResourceType, $sourceProperty, $targetResourceSet);
828
        $revSetKey = ResourceAssociationSet::keyName($targetResourceType, $targetProperty, $sourceResourceSet);
829
        if (isset($this->associationSets[$fwdSetKey]) && isset($this->associationSets[$revSetKey])) {
830
            return;
831
        }
832
        $sourceKind = ('*' == $sourceMultiplicity)
833
            ? ResourcePropertyKind::RESOURCESET_REFERENCE()
834
            : ResourcePropertyKind::RESOURCE_REFERENCE();
835
        $targetKind = ('*' == $targetMultiplicity)
836
            ? ResourcePropertyKind::RESOURCESET_REFERENCE()
837
            : ResourcePropertyKind::RESOURCE_REFERENCE();
838
839
        $sourceResourceProperty = new ResourceProperty($sourceProperty, null, $targetKind, $targetResourceType);
840
        assert(
841
            $targetKind == $sourceResourceProperty->getKind(),
842
            'Resource property kind mismatch between $targetKind and $sourceResourceProperty'
843
        );
844
        $sourceResourceType->addProperty($sourceResourceProperty, false);
845
        $targetResourceProperty = new ResourceProperty($targetProperty, null, $sourceKind, $sourceResourceType);
846
        assert(
847
            $sourceKind == $targetResourceProperty->getKind(),
848
            'Resource property kind mismatch between $sourceKind and $targetResourceProperty'
849
        );
850
        $targetResourceType->addProperty($targetResourceProperty, false);
851
852
        //TODO: Audit this, figure out how it makes metadata go sproing
853
        $fwdSet = new ResourceAssociationSet(
854
            $fwdSetKey,
855
            new ResourceAssociationSetEnd($sourceResourceSet, $sourceResourceType, $sourceResourceProperty),
856
            new ResourceAssociationSetEnd($targetResourceSet, $targetResourceType, $targetResourceProperty)
857
        );
858
        $revSet = new ResourceAssociationSet(
859
            $revSetKey,
860
            new ResourceAssociationSetEnd($targetResourceSet, $targetResourceType, $targetResourceProperty),
861
            new ResourceAssociationSetEnd($sourceResourceSet, $sourceResourceType, $sourceResourceProperty)
862
        );
863
        $sourceName = $sourceResourceType->getFullName();
864
        $targetName = $targetResourceType->getFullName();
865
        $this->getMetadataManager()->addNavigationPropertyToEntityType(
866
            $this->oDataEntityMap[$sourceName],
867
            $sourceMultiplicity,
868
            $sourceProperty,
869
            $this->oDataEntityMap[$targetName],
870
            $targetMultiplicity,
871
            $targetProperty
872
        );
873
        $this->associationSets[$fwdSetKey] = $fwdSet;
874
        $this->associationSets[$revSetKey] = $revSet;
875
    }
876
877
    /**
878
     * To add a resource set reference property.
879
     *
880
     * @param  ResourceEntityType        $resourceType      The resource type to add the
881
     *                                                      resource reference set property to
882
     * @param  string                    $name              The name of the property to add
883
     * @param  ResourceSet               $targetResourceSet The resource set the resource
884
     *                                                      reference set property points to
885
     * @param  ResourceEntityType|null   $concreteType      Underlying concrete resource type, if set
886
     * @param  mixed                     $single
887
     * @throws InvalidOperationException
888
     * @throws ReflectionException
889
     */
890
    public function addResourceSetReferenceProperty(
891
        ResourceEntityType $resourceType,
892
        string $name,
893
        ResourceSet $targetResourceSet,
894
        ResourceEntityType $concreteType = null,
895
        bool $single = false
896
    ): void {
897
        $this->addReferencePropertyInternal(
898
            $resourceType,
899
            $name,
900
            $targetResourceSet,
901
            '*',
902
            (true === $single) ? false : true,
903
            $concreteType
904
        );
905
    }
906
907
    /**
908
     * To add a M:N resource reference property.
909
     *
910
     * @param  ResourceEntityType        $sourceResourceType The resource type to add the resource
911
     *                                                       reference property from
912
     * @param  ResourceEntityType        $targetResourceType The resource type to add the resource
913
     *                                                       reference property to
914
     * @param  string                    $sourceProperty     The name of the property to add, on source type
915
     * @param  string                    $targetProperty     The name of the property to add, on target type
916
     * @throws InvalidOperationException
917
     * @throws ReflectionException
918
     */
919
    public function addResourceSetReferencePropertyBidirectional(
920
        ResourceEntityType $sourceResourceType,
921
        ResourceEntityType $targetResourceType,
922
        string $sourceProperty,
923
        string $targetProperty
924
    ): void {
925
        $this->addReferencePropertyInternalBidirectional(
926
            $sourceResourceType,
927
            $targetResourceType,
928
            $sourceProperty,
929
            $targetProperty,
930
            '*',
931
            '*'
932
        );
933
        // verify resource property types are what we expect them to be
934
        $sourceResourceKind = $sourceResourceType->resolveProperty($sourceProperty)->getKind();
935
        assert(
936
            ResourcePropertyKind::RESOURCESET_REFERENCE() == $sourceResourceKind,
937
            'M side of M:N relationship not pointing to resource set reference'
938
        );
939
        $targetResourceKind = $targetResourceType->resolveProperty($targetProperty)->getKind();
940
        assert(
941
            ResourcePropertyKind::RESOURCESET_REFERENCE() == $targetResourceKind,
942
            'N side of M:N relationship not pointing to resource set reference'
943
        );
944
    }
945
946
    /**
947
     * To add a 1-1 resource reference.
948
     *
949
     * @param  ResourceEntityType        $sourceResourceType The resource type to add the resource
950
     *                                                       reference property from
951
     * @param  ResourceEntityType        $targetResourceType The resource type to add the resource
952
     *                                                       reference property to
953
     * @param  string                    $sourceProperty     The name of the property to add, on source type
954
     * @param  string                    $targetProperty     The name of the property to add, on target type
955
     * @throws InvalidOperationException
956
     * @throws ReflectionException
957
     */
958
    public function addResourceReferenceSinglePropertyBidirectional(
959
        ResourceEntityType $sourceResourceType,
960
        ResourceEntityType $targetResourceType,
961
        string $sourceProperty,
962
        string $targetProperty
963
    ): void {
964
        $this->addReferencePropertyInternalBidirectional(
965
            $sourceResourceType,
966
            $targetResourceType,
967
            $sourceProperty,
968
            $targetProperty,
969
            '1',
970
            '0..1'
971
        );
972
        // verify resource property types are what we expect them to be
973
        $sourceResourceKind = $sourceResourceType->resolveProperty($sourceProperty)->getKind();
974
        assert(
975
            ResourcePropertyKind::RESOURCE_REFERENCE() == $sourceResourceKind,
976
            '1 side of 1:1 relationship not pointing to resource reference'
977
        );
978
        $targetResourceKind = $targetResourceType->resolveProperty($targetProperty)->getKind();
979
        assert(
980
            ResourcePropertyKind::RESOURCE_REFERENCE() == $targetResourceKind,
981
            '0..1 side of 1:1 relationship not pointing to resource reference'
982
        );
983
    }
984
985
    /**
986
     * To add a complex property to entity or complex type.
987
     *
988
     * @param ResourceType        $targetResourceType  The resource type to which the complex property needs to add
989
     * @param string              $name                name of the complex property
990
     * @param ResourceComplexType $complexResourceType complex resource type
991
     * @param bool                $isBag               complex type is bag or not
992
     *
993
     * @throws ReflectionException
994
     * @throws InvalidOperationException
995
     * @return ResourceProperty
996
     */
997
    public function addComplexProperty(
998
        ResourceType $targetResourceType,
999
        string $name,
1000
        ResourceComplexType $complexResourceType,
1001
        bool $isBag = false
1002
    ): ResourceProperty {
1003
        if ($targetResourceType->getResourceTypeKind() != ResourceTypeKind::ENTITY()
1004
            && $targetResourceType->getResourceTypeKind() != ResourceTypeKind::COMPLEX()
1005
        ) {
1006
            throw new InvalidOperationException('Complex property can be added to an entity or another complex type');
1007
        }
1008
1009
        // check that property and resource name don't up and collide - would violate OData spec
1010
        if (strtolower($name) == strtolower($targetResourceType->getName())) {
1011
            throw new InvalidOperationException(
1012
                'Property name must be different from resource name.'
1013
            );
1014
        }
1015
1016
        $this->checkInstanceProperty($name, $targetResourceType);
1017
1018
        $kind = ResourcePropertyKind::COMPLEX_TYPE();
1019
        if ($isBag) {
1020
            $kind = $kind->setBAG(true);
1021
        }
1022
1023
        $kind = new ResourcePropertyKind($kind);
1024
1025
        $resourceProperty = new ResourceProperty(
1026
            $name,
1027
            null,
1028
            $kind,
1029
            $complexResourceType
1030
        );
1031
        $targetResourceType->addProperty($resourceProperty);
1032
1033
        return $resourceProperty;
1034
    }
1035
1036
    /**
1037
     * @param  string       $name
1038
     * @param  ResourceType $returnType
1039
     * @param  array|string $functionName
1040
     * @return void
1041
     */
1042
    public function createSingleton(string $name, ResourceType $returnType, $functionName): void
1043
    {
1044
        $msg = null;
1045
        if (array_key_exists($name, $this->singletons)) {
1046
            $msg = 'Singleton name already exists';
1047
            throw new InvalidArgumentException($msg);
1048
        }
1049
        if (array_key_exists($name, $this->resourceSets)) {
1050
            $msg = 'Resource set with same name, ' . $name . ', exists';
1051
            throw new InvalidArgumentException($msg);
1052
        }
1053
        $typeName = $returnType->getFullName();
1054
        if (!array_key_exists($typeName, $this->oDataEntityMap)) {
1055
            $msg = 'Mapping not defined for ' . $typeName;
1056
            throw new InvalidArgumentException($msg);
1057
        }
1058
        $metaReturn   = $this->oDataEntityMap[$typeName];
1059
        $anonymousSet = $this->typeSetMapping[$typeName];
1060
        $singleton    = $this->getMetadataManager()->createSingleton($name, $metaReturn, $anonymousSet);
1061
        assert($singleton->isOK($msg), $msg);
1062
        $type = new ResourceFunctionType($functionName, $singleton, $returnType);
1063
        // Since singletons should take no args, enforce it here
1064
        assert(0 == count($type->getParms()));
1065
        $this->singletons[$name] = $type;
1066
    }
1067
1068
    /**
1069
     * @return array
1070
     */
1071
    public function getSingletons(): array
1072
    {
1073
        return $this->singletons;
1074
    }
1075
1076
    /**
1077
     * @param  string $name
1078
     * @return mixed
1079
     */
1080
    public function callSingleton(string $name)
1081
    {
1082
        if (!array_key_exists($name, $this->singletons)) {
1083
            $msg = 'Requested singleton does not exist';
1084
            throw new InvalidArgumentException($msg);
1085
        }
1086
1087
        return $this->singletons[$name]->get();
1088
    }
1089
}
1090