SimpleMetadataProvider::getAssociationCount()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
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