Completed
Push — master ( b333c5...030b4a )
by Christopher
03:45
created

ProvidersWrapper::updateResource()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 12
nc 1
nop 5
1
<?php
2
3
namespace POData\Providers;
4
5
use POData\Providers\Metadata\ResourceTypeKind;
6
use POData\Providers\Metadata\ResourceSetWrapper;
7
use POData\Providers\Metadata\ResourceType;
8
use POData\Providers\Metadata\ResourceProperty;
9
use POData\Providers\Metadata\ResourceSet;
10
use POData\Providers\Metadata\ResourceAssociationSet;
11
use POData\Configuration\ServiceConfiguration;
12
use POData\UriProcessor\QueryProcessor\SkipTokenParser\InternalSkipTokenInfo;
13
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
14
use POData\Common\ODataException;
15
use POData\Common\Messages;
16
use POData\Providers\Metadata\EdmSchemaVersion;
17
use POData\Providers\Query\IQueryProvider;
18
use POData\Providers\Metadata\IMetadataProvider;
19
use POData\Providers\Expression\IExpressionProvider;
20
use POData\Common\InvalidOperationException;
21
use POData\UriProcessor\QueryProcessor\ExpressionParser\FilterInfo;
22
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
23
use POData\Providers\Query\QueryResult;
24
use POData\Providers\Query\QueryType;
25
26
/**
27
 * Class ProvidersWrapper.
28
 *
29
 * A wrapper class over IMetadataProvider and IQueryProvider implementations, All calls to implementation of methods
30
 * of these interfaces should go through this wrapper class so that wrapper methods of this class can perform validation
31
 */
32
class ProvidersWrapper
33
{
34
    /**
35
     * Holds reference to IMetadataProvider implementation.
36
     *
37
     * @var IMetadataProvider
38
     */
39
    private $metaProvider;
40
41
    /**
42
     * Holds reference to IQueryProvider implementation.
43
     *
44
     * @var IQueryProvider
45
     */
46
    private $queryProvider;
47
48
    /**
49
     * Holds reference to IServiceConfiguration implementation.
50
     *
51
     * @var ServiceConfiguration
52
     */
53
    private $config;
54
55
    /**
56
     * Cache for ResourceProperties of a resource type that belongs to a
57
     * resource set. An entry (ResourceProperty collection) in this cache
58
     * contains only the visible properties of ResourceType.
59
     *
60
     * @var array(string, array(string, ResourceProperty))
61
     */
62
    private $propertyCache;
63
64
    /**
65
     * Cache for ResourceSetWrappers. If ResourceSet is invisible value will
66
     * be null.
67
     *
68
     * @var ResourceSetWrapper[] indexed by resource set name
69
     */
70
    private $setWrapperCache;
71
72
    /**
73
     * Cache for ResourceTypes.
74
     *
75
     * @var ResourceType[] indexed by resource type name
76
     */
77
    private $typeCache;
78
79
    /**
80
     * Cache for ResourceAssociationSet. If ResourceAssociationSet is invisible
81
     * value will be null.
82
     *
83
     * @var ResourceAssociationSet[] indexed by name
84
     */
85
    private $associationSetCache;
86
87
    /**
88
     * Creates a new instance of ProvidersWrapper.
89
     *
90
     * @param IMetadataProvider    $metadataProvider Reference to IMetadataProvider implementation
91
     * @param IQueryProvider       $queryProvider    Reference to IQueryProvider implementation
92
     * @param ServiceConfiguration $configuration    Reference to IServiceConfiguration implementation
93
     */
94
    public function __construct(IMetadataProvider $metadataProvider, IQueryProvider $queryProvider, ServiceConfiguration $configuration)
95
    {
96
        $this->metaProvider = $metadataProvider;
97
        $this->queryProvider = $queryProvider;
98
        $this->config = $configuration;
99
        $this->setWrapperCache = array();
100
        $this->typeCache = array();
101
        $this->associationSetCache = array();
102
        $this->propertyCache = array();
103
    }
104
105
    //Wrappers for IMetadataProvider methods
106
107
    /**
108
     * To get the Container name for the data source,
109
     * Note: Wrapper for IMetadataProvider::getContainerName method
110
     * implementation.
111
     *
112
     * @return string that contains the name of the container
113
     *
114
     * @throws ODataException Exception if implementation returns empty container name
115
     */
116
    public function getContainerName()
117
    {
118
        $containerName = $this->metaProvider->getContainerName();
119
        if (empty($containerName)) {
120
            throw new ODataException(
121
                Messages::providersWrapperContainerNameMustNotBeNullOrEmpty(),
122
                500
123
            );
124
        }
125
126
        return $containerName;
127
    }
128
129
    /**
130
     * To get Namespace name for the data source,
131
     * Note: Wrapper for IMetadataProvider::getContainerNamespace method implementation.
132
     *
133
     * @return string that contains the namespace name
134
     *
135
     * @throws ODataException Exception if implementation returns empty container namespace
136
     */
137
    public function getContainerNamespace()
138
    {
139
        $containerNamespace = $this->metaProvider->getContainerNamespace();
140
        if (empty($containerNamespace)) {
141
            throw new ODataException(
142
                Messages::providersWrapperContainerNamespaceMustNotBeNullOrEmpty(),
143
                500
144
            );
145
        }
146
147
        return $containerNamespace;
148
    }
149
150
    /**
151
     * To get the data service configuration.
152
     *
153
     * @return ServiceConfiguration
154
     */
155
    public function getConfiguration()
156
    {
157
        return $this->config;
158
    }
159
160
    /**
161
     *  To get all entity set information,
162
     *  Note: Wrapper for IMetadataProvider::getResourceSets method implementation,
163
     *  This method returns array of ResourceSetWrapper instances but the corresponding IDSMP method returns array of ResourceSet instances.
164
     *
165
     *  @return ResourceSetWrapper[] The ResourceSetWrappers for the visible ResourceSets
166
     *
167
     *  @throws ODataException when two resource sets with the same name are encountered
168
     */
169
    public function getResourceSets()
170
    {
171
        $resourceSets = $this->metaProvider->getResourceSets();
172
        $resourceSetWrappers = array();
173
        $resourceSetNames = array();
174
        foreach ($resourceSets as $resourceSet) {
175
            $name = $resourceSet->getName();
176
            if (in_array($name, $resourceSetNames)) {
177
                throw new ODataException(Messages::providersWrapperEntitySetNameShouldBeUnique($name), 500);
178
            }
179
180
            $resourceSetNames[] = $name;
181
            $resourceSetWrapper = $this->_validateResourceSetAndGetWrapper($resourceSet);
182
            if (!is_null($resourceSetWrapper)) {
183
                $resourceSetWrappers[] = $resourceSetWrapper;
184
            }
185
        }
186
187
        return $resourceSetWrappers;
188
    }
189
190
    /**
191
     * To get all resource types in the data source,
192
     * Note: Wrapper for IMetadataProvider::getTypes method implementation.
193
     *
194
     * @return ResourceType[]
195
     */
196
    public function getTypes()
197
    {
198
        $resourceTypes = $this->metaProvider->getTypes();
199
        $resourceTypeNames = array();
200
        foreach ($resourceTypes as $resourceType) {
201
            if (in_array($resourceType->getName(), $resourceTypeNames)) {
202
                throw new ODataException(
203
                    Messages::providersWrapperEntityTypeNameShouldBeUnique($resourceType->getName()),
204
                    500
205
                );
206
            }
207
208
            $resourceTypeNames[] = $resourceType->getName();
209
            $this->_validateResourceType($resourceType);
210
        }
211
212
        return $resourceTypes;
213
    }
214
215
    /**
216
     * To get a resource set based on the specified resource set name which is
217
     * visible,
218
     * Note: Wrapper for IMetadataProvider::resolveResourceSet method
219
     * implementation.
220
     *
221
     * @param string $name Name of the resource set
222
     *
223
     * @return ResourceSetWrapper|null Returns resource set with the given name if found, NULL if resource set is set to invisible or not found
224
     */
225
    public function resolveResourceSet($name)
226
    {
227
        if (array_key_exists($name, $this->setWrapperCache)) {
228
            return $this->setWrapperCache[$name];
229
        }
230
231
        $resourceSet = $this->metaProvider->resolveResourceSet($name);
232
        if (is_null($resourceSet)) {
233
            return null;
234
        }
235
236
        return $this->_validateResourceSetAndGetWrapper($resourceSet);
237
    }
238
239
    /**
240
     * To get a resource type based on the resource set name,
241
     * Note: Wrapper for IMetadataProvider::resolveResourceType
242
     * method implementation.
243
     *
244
     * @param string $name Name of the resource set
245
     *
246
     * @return ResourceType|null resource type with the given resource set name if found else NULL
247
     *
248
     * @throws ODataException If the ResourceType is invalid
249
     */
250
    public function resolveResourceType($name)
251
    {
252
        $resourceType = $this->metaProvider->resolveResourceType($name);
253
        if (is_null($resourceType)) {
254
            return null;
255
        }
256
257
        return $this->_validateResourceType($resourceType);
258
    }
259
260
    /**
261
     * The method must return a collection of all the types derived from
262
     * $resourceType The collection returned should NOT include the type
263
     * passed in as a parameter
264
     * Note: Wrapper for IMetadataProvider::getDerivedTypes
265
     * method implementation.
266
     *
267
     * @param ResourceType $resourceType Resource to get derived resource types from
268
     *
269
     * @return ResourceType[]
270
     *
271
     * @throws InvalidOperationException when the meat provider doesn't return an array
272
     */
273
    public function getDerivedTypes(ResourceType $resourceType)
274
    {
275
        $derivedTypes = $this->metaProvider->getDerivedTypes($resourceType);
276
        if (!is_array($derivedTypes)) {
277
            throw new InvalidOperationException(Messages::metadataAssociationTypeSetInvalidGetDerivedTypesReturnType($resourceType->getName()));
278
        }
279
280
        foreach ($derivedTypes as $derivedType) {
281
            $this->_validateResourceType($derivedType);
282
        }
283
284
        return $derivedTypes;
285
    }
286
287
    /**
288
     * Returns true if $resourceType represents an Entity Type which has derived
289
     * Entity Types, else false.
290
     * Note: Wrapper for IMetadataProvider::hasDerivedTypes method
291
     * implementation.
292
     *
293
     * @param ResourceType $resourceType Resource to check for derived resource
294
     *                                   types
295
     *
296
     * @return bool
297
     *
298
     * @throws ODataException If the ResourceType is invalid
299
     */
300
    public function hasDerivedTypes(ResourceType $resourceType)
301
    {
302
        $this->_validateResourceType($resourceType);
303
304
        return $this->metaProvider->hasDerivedTypes($resourceType);
305
    }
306
307
    /**
308
     * Gets the ResourceAssociationSet instance for the given source association end,
309
     * Note: Wrapper for IMetadataProvider::getResourceAssociationSet
310
     * method implementation.
311
     *
312
     * @param ResourceSet      $set      Resource set of the source association end
313
     * @param ResourceType     $type     Resource type of the source association end
314
     * @param ResourceProperty $property Resource property of the source association end
315
     *
316
     * @return ResourceAssociationSet|null Returns ResourceAssociationSet for the source
317
     *                                     association end, NULL if no such
318
     *                                     association end or resource set in the
319
     *                                     other end of the association is invisible
320
     */
321
    public function getResourceAssociationSet(
322
        ResourceSet $set,
323
        ResourceType $type,
324
        ResourceProperty $property
325
    ) {
326
        $type = $this->_getResourceTypeWherePropertyIsDeclared($type, $property);
327
        $cacheKey = $set->getName() . '_' . $type->getName() . '_' . $property->getName();
328
329
        if (array_key_exists($cacheKey, $this->associationSetCache)) {
330
            return $this->associationSetCache[$cacheKey];
331
        }
332
333
        $associationSet = $this->metaProvider->getResourceAssociationSet(
334
            $set,
335
            $type,
0 ignored issues
show
Bug introduced by
It seems like $type defined by $this->_getResourceTypeW...lared($type, $property) on line 326 can be null; however, POData\Providers\Metadat...esourceAssociationSet() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
336
            $property
337
        );
338
339
        if (!is_null($associationSet)) {
340
            $thisAssociationSetEnd = $associationSet->getResourceAssociationSetEnd(
341
                $set,
342
                $type,
0 ignored issues
show
Bug introduced by
It seems like $type defined by $this->_getResourceTypeW...lared($type, $property) on line 326 can be null; however, POData\Providers\Metadat...urceAssociationSetEnd() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
343
                $property
344
            );
345
346
            $relatedAssociationSetEnd = $associationSet->getRelatedResourceAssociationSetEnd(
347
                $set,
348
                $type,
0 ignored issues
show
Bug introduced by
It seems like $type defined by $this->_getResourceTypeW...lared($type, $property) on line 326 can be null; however, POData\Providers\Metadat...urceAssociationSetEnd() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
349
                $property
350
            );
351
352
            //If $thisAssociationSetEnd or $relatedAssociationSetEnd
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
353
            //is null means the associationset
354
            //we got from the IDSMP::getResourceAssociationSet is invalid.
355
            //AssociationSet::getResourceAssociationSetEnd
356
            //return null, if AssociationSet's End1 or End2's resourceset name
0 ignored issues
show
Unused Code Comprehensibility introduced by
36% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
357
            //is not matching with the name of
358
            //resource set wrapper (param1) and resource type is not assignable
359
            //from given resource type (param2)
360
            if (is_null($thisAssociationSetEnd) || is_null($relatedAssociationSetEnd)) {
361
                throw new ODataException(
362
                    Messages::providersWrapperIDSMPGetResourceSetReturnsInvalidResourceSet(
363
                        $set->getName(),
364
                        $type->getFullName(),
365
                        $property->getName()
366
                    ),
367
                    500
368
                );
369
            }
370
371
            $relatedResourceSetWrapper = $this->_validateResourceSetAndGetWrapper(
372
                $relatedAssociationSetEnd->getResourceSet()
373
            );
374
            if ($relatedResourceSetWrapper === null) {
375
                $associationSet = null;
376
            } else {
377
                $this->_validateResourceType($thisAssociationSetEnd->getResourceType());
378
                $this->_validateResourceType($relatedAssociationSetEnd->getResourceType());
379
            }
380
        }
381
382
        $this->associationSetCache[$cacheKey] = $associationSet;
383
384
        return $associationSet;
385
    }
386
387
    /**
388
     * Gets the target resource set wrapper for the given navigation property,
389
     * source resource set wrapper and the source resource type.
390
     *
391
     * @param ResourceSetWrapper $resourceSetWrapper         Source resource set
392
     * @param ResourceType       $resourceType               Source resource type
393
     * @param ResourceProperty   $navigationResourceProperty Navigation property
394
     *
395
     * @return ResourceSetWrapper|null Returns instance of ResourceSetWrapper
396
     *                                 (describes the entity set and associated configuration) for the
397
     *                                 given navigation property. returns NULL if resourceset for the
398
     *                                 navigation property is invisible or if metadata provider returns
399
     *                                 null resource association set
400
     */
401
    public function getResourceSetWrapperForNavigationProperty(
402
        ResourceSetWrapper $resourceSetWrapper,
403
        ResourceType $resourceType,
404
        ResourceProperty $navigationResourceProperty
405
    ) {
406
        $associationSet = $this->getResourceAssociationSet(
407
            $resourceSetWrapper,
408
            $resourceType,
409
            $navigationResourceProperty
410
        );
411
412
        if (!is_null($associationSet)) {
413
            $relatedAssociationSetEnd = $associationSet->getRelatedResourceAssociationSetEnd(
414
                $resourceSetWrapper->getResourceSet(),
415
                $resourceType,
416
                $navigationResourceProperty
417
            );
418
419
            return $this->_validateResourceSetAndGetWrapper(
420
                $relatedAssociationSetEnd->getResourceSet()
421
            );
422
        }
423
424
        return null;
425
    }
426
427
    /**
428
     * Gets the visible resource properties for the given resource type from the given resource set wrapper.
429
     *
430
     * @param ResourceSetWrapper $setWrapper   Resource set wrapper in question
431
     * @param ResourceType       $resourceType Resource type in question
432
     *
433
     * @return ResourceProperty[] Collection of visible resource properties from the given resource set wrapper and resource type
434
     */
435
    public function getResourceProperties(ResourceSetWrapper $setWrapper, ResourceType $resourceType)
436
    {
437
        if ($resourceType->getResourceTypeKind() != ResourceTypeKind::ENTITY) {
438
            //Complex resource type
439
            return $resourceType->getAllProperties();
440
        }
441
        //TODO: move this to doctrine annotations
442
        $cacheKey = $setWrapper->getName() . '_' . $resourceType->getFullName();
443
        if (!array_key_exists($cacheKey, $this->propertyCache)) {
444
            //Fill the cache
445
            $this->propertyCache[$cacheKey] = array();
446
            foreach ($resourceType->getAllProperties() as $resourceProperty) {
447
                //Check whether this is a visible navigation property
448
                //TODO: is this broken?? see #87
449
                if ($resourceProperty->getTypeKind() == ResourceTypeKind::ENTITY
450
                    && !is_null($this->getResourceSetWrapperForNavigationProperty($setWrapper, $resourceType, $resourceProperty))
451
                ) {
452
                    $this->propertyCache[$cacheKey][$resourceProperty->getName()] = $resourceProperty;
453
                } else {
454
                    //primitive, bag or complex property
455
                    $this->propertyCache[$cacheKey][$resourceProperty->getName()] = $resourceProperty;
456
                }
457
            }
458
        }
459
460
        return $this->propertyCache[$cacheKey];
461
    }
462
463
    /**
464
     * Wrapper function over _validateResourceSetAndGetWrapper function.
465
     *
466
     * @param ResourceSet $resourceSet see the comments of _validateResourceSetAndGetWrapper
467
     *
468
     * @return ResourceSetWrapper|null see the comments of _validateResourceSetAndGetWrapper
469
     */
470
    public function validateResourceSetAndGetWrapper(ResourceSet $resourceSet)
471
    {
472
        return $this->_validateResourceSetAndGetWrapper($resourceSet);
473
    }
474
475
    /**
476
     * Gets the Edm Schema version compliance to the metadata.
477
     *
478
     * @return EdmSchemaVersion
479
     */
480
    public function getEdmSchemaVersion()
481
    {
482
        //The minimal schema version for custom provider is 1.1
483
        return EdmSchemaVersion::VERSION_1_DOT_1;
484
    }
485
486
    /**
487
     * This function perform the following operations
488
     *  (1) If the cache contain an entry [key, value] for the resourceset then
489
     *      return the entry-value
490
     *  (2) If the cache not contain an entry for the resourceset then validate
491
     *      the resourceset
492
     *            (a) If valid add entry as [resouceset_name, resourceSetWrapper]
493
     *            (b) if not valid add entry as [resouceset_name, null]
494
     *  Note: validating a resourceset means checking the resourceset is visible
495
     *  or not using configuration.
496
     *
497
     * @param ResourceSet $resourceSet The resourceset to validate and get the
498
     *                                 wrapper for
499
     *
500
     * @return ResourceSetWrapper|null Returns an instance if a resource set with the given name is visible
501
     */
502
    private function _validateResourceSetAndGetWrapper(ResourceSet $resourceSet)
503
    {
504
        $cacheKey = $resourceSet->getName();
505
        if (array_key_exists($cacheKey, $this->setWrapperCache)) {
506
            return $this->setWrapperCache[$cacheKey];
507
        }
508
509
        $this->_validateResourceType($resourceSet->getResourceType());
510
        $wrapper = new ResourceSetWrapper($resourceSet, $this->config);
511
        if ($wrapper->isVisible()) {
512
            $this->setWrapperCache[$cacheKey] = $wrapper;
513
        } else {
514
            $this->setWrapperCache[$cacheKey] = null;
515
        }
516
517
        return $this->setWrapperCache[$cacheKey];
518
    }
519
520
    /**
521
     * Validates the given instance of ResourceType.
522
     *
523
     * @param ResourceType $resourceType The ResourceType to validate
524
     *
525
     * @return ResourceType
526
     *
527
     * @throws ODataException Exception if $resourceType is invalid
528
     */
529
    private function _validateResourceType(ResourceType $resourceType)
530
    {
531
        $cacheKey = $resourceType->getName();
532
        if (array_key_exists($cacheKey, $this->typeCache)) {
533
            return $this->typeCache[$cacheKey];
534
        }
535
536
        //TODO: Do validation if any for the ResourceType
537
        $this->typeCache[$cacheKey] = $resourceType;
538
539
        return $resourceType;
540
    }
541
542
    /**
543
     * Gets the resource type on which the resource property is declared on,
544
     * If property is not declared in the given resource type, then this
545
     * function drill down to the inheritance hierarchy of the given resource
546
     * type to find out the base class in which the property is declared.
547
     *
548
     * @param ResourceType     $resourceType     The resource type to start looking
549
     * @param ResourceProperty $resourceProperty The resource property in question
550
     *
551
     * @return ResourceType|null Returns reference to the ResourceType on which
552
     *                           the $resourceProperty is declared, NULL if
553
     *                           $resourceProperty is not declared anywhere
554
     *                           in the inheritance hierarchy
555
     */
556
    private function _getResourceTypeWherePropertyIsDeclared(
557
        ResourceType $resourceType,
558
        ResourceProperty $resourceProperty
559
    ) {
560
        $type = $resourceType;
561
        while ($type !== null) {
562
            if ($type->resolvePropertyDeclaredOnThisType($resourceProperty->getName()) !== null) {
563
                break;
564
            }
565
566
            $type = $type->getBaseType();
567
        }
568
569
        return $type;
570
    }
571
572
    /**
573
     * Gets the underlying custom expression provider, the end developer is
574
     * responsible for implementing IExpressionProvider if he choose for.
575
     *
576
     * @return IExpressionProvider Instance of IExpressionProvider implementation
577
     */
578
    public function getExpressionProvider()
579
    {
580
        $expressionProvider = $this->queryProvider->getExpressionProvider();
581
        if (is_null($expressionProvider)) {
582
            throw ODataException::createInternalServerError(Messages::providersWrapperExpressionProviderMustNotBeNullOrEmpty());
583
        }
584
585
        if (!$expressionProvider instanceof IExpressionProvider) {
586
            throw ODataException::createInternalServerError(Messages::providersWrapperInvalidExpressionProviderInstance());
587
        }
588
589
        return $expressionProvider;
590
    }
591
592
    /**
593
     * Indicates if the QueryProvider can handle ordered paging, this means respecting order, skip, and top parameters
594
     * If the query provider can not handle ordered paging, it must return the entire result set and POData will
595
     * perform the ordering and paging.
596
     *
597
     * @return bool True if the query provider can handle ordered paging, false if POData should perform the paging
598
     */
599
    public function handlesOrderedPaging()
600
    {
601
        return $this->queryProvider->handlesOrderedPaging();
602
    }
603
604
    /**
605
     * @param QueryResult $queryResult
606
     * @param string      $methodName
607
     */
608
    private function ValidateQueryResult($queryResult, QueryType $queryType, $methodName)
609
    {
610
        if (!$queryResult instanceof QueryResult) {
611
            throw ODataException::createInternalServerError(
612
                Messages::queryProviderReturnsNonQueryResult($methodName)
613
            );
614
        }
615
616
        if ($queryType == QueryType::COUNT() || $queryType == QueryType::ENTITIES_WITH_COUNT()) {
617
            //and the provider is supposed to handle the ordered paging they must return a count!
618
            if ($this->queryProvider->handlesOrderedPaging() && !is_numeric($queryResult->count)) {
619
                throw ODataException::createInternalServerError(
620
                    Messages::queryProviderResultCountMissing($methodName, $queryType)
621
                );
622
            }
623
624
            //If POData is supposed to handle the ordered aging they must return results! (possibly empty)
625
            if (!$this->queryProvider->handlesOrderedPaging() && !is_array($queryResult->results)) {
626
                throw ODataException::createInternalServerError(
627
                    Messages::queryProviderResultsMissing($methodName, $queryType)
628
                );
629
            }
630
        }
631
632
        if (($queryType == QueryType::ENTITIES() || $queryType == QueryType::ENTITIES_WITH_COUNT()) && !is_array($queryResult->results)) {
633
            throw ODataException::createInternalServerError(
634
                Messages::queryProviderResultsMissing($methodName, $queryType)
635
            );
636
        }
637
    }
638
639
    /**
640
     * Gets collection of entities belongs to an entity set.
641
     *
642
     * @param QueryType             $queryType   indicates if this is a query for a count, entities, or entities with a count
643
     * @param ResourceSet           $resourceSet The entity set containing the entities that need to be fetched
644
     * @param FilterInfo            $filterInfo  represents the $filter parameter of the OData query.  NULL if no $filter specified
645
     * @param InternalOrderByInfo   $orderBy     The orderBy information
646
     * @param int                   $top         The top count
647
     * @param int                   $skip        The skip count
648
     * @param InternalSkipTokenInfo $skipToken   The skip token
649
     *
650
     * @return QueryResult
651
     */
652
    public function getResourceSet(QueryType $queryType, ResourceSet $resourceSet, FilterInfo $filterInfo = null, InternalOrderByInfo $orderBy = null, $top = null, $skip = null, InternalSkipTokenInfo $skipToken = null)
653
    {
654
        $queryResult = $this->queryProvider->getResourceSet(
655
            $queryType,
656
            $resourceSet,
657
            $filterInfo,
658
            $orderBy,
659
            $top,
660
            $skip,
661
            $skipToken
0 ignored issues
show
Unused Code introduced by
The call to IQueryProvider::getResourceSet() has too many arguments starting with $skipToken.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
662
        );
663
664
        $this->validateQueryResult($queryResult, $queryType, 'IQueryProvider::getResourceSet');
665
666
        return $queryResult;
667
    }
668
669
    /**
670
     * Gets an entity instance from an entity set identified by a key.
671
     *
672
     * @param ResourceSet   $resourceSet   The entity set containing the entity to fetch
673
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch
674
     *
675
     * @return object|null Returns entity instance if found else null
676
     */
677
    public function getResourceFromResourceSet(ResourceSet $resourceSet, KeyDescriptor $keyDescriptor)
678
    {
679
        $entityInstance = $this->queryProvider->getResourceFromResourceSet($resourceSet, $keyDescriptor);
680
        $this->_validateEntityInstance(
681
            $entityInstance,
0 ignored issues
show
Bug introduced by
It seems like $entityInstance defined by $this->queryProvider->ge...rceSet, $keyDescriptor) on line 679 can also be of type null; however, POData\Providers\Provide...alidateEntityInstance() does only seem to accept object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
682
            $resourceSet,
683
            $keyDescriptor,
684
            'IQueryProvider::getResourceFromResourceSet'
685
        );
686
687
        return $entityInstance;
688
    }
689
690
    /**
691
     * Puts an entity instance to entity set identified by a key.
692
     *
693
     * @param ResourceSet   $resourceSet   The entity set containing the entity to update
694
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to update
695
     *
696
     * @return bool|null Returns result of executiong query
697
     */
698
    public function putResource(
699
        ResourceSet $resourceSet,
700
        KeyDescriptor $keyDescriptor,
701
        $data
702
    ) {
703
        $queryResult = $this->queryProvider->putResource(
0 ignored issues
show
Bug introduced by
The method putResource() does not seem to exist on object<POData\Providers\Query\IQueryProvider>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
704
            $resourceSet,
705
            $keyDescriptor,
706
            $data
707
        );
708
709
        return $queryResult;
710
    }
711
712
    /**
713
     * Get related resource set for a resource.
714
     *
715
     * @param QueryType        $queryType         indicates if this is a query for a count, entities, or entities with a count
716
     * @param ResourceSet      $sourceResourceSet The entity set containing the source entity
717
     * @param object           $sourceEntity      The source entity instance
718
     * @param ResourceSet      $targetResourceSet The resource set of containing the target of the navigation property
719
     * @param ResourceProperty $targetProperty    The navigation property to retrieve
720
     * @param FilterInfo       $filterInfo        represents the $filter parameter of the OData query.  NULL if no $filter specified
721
     * @param mixed            $orderBy           sorted order if we want to get the data in some specific order
722
     * @param int              $top               number of records which  need to be skip
723
     * @param string           $skip              value indicating what records to skip
724
     *
725
     * @return QueryResult
726
     *
727
     * @throws ODataException
728
     */
729
    public function getRelatedResourceSet(
730
        QueryType $queryType,
731
        ResourceSet $sourceResourceSet,
732
        $sourceEntity,
733
        ResourceSet $targetResourceSet,
734
        ResourceProperty $targetProperty,
735
        $filterInfo,
736
        $orderBy,
737
        $top,
738
        $skip
739
    ) {
740
        $queryResult = $this->queryProvider->getRelatedResourceSet(
741
            $queryType,
742
            $sourceResourceSet,
743
            $sourceEntity,
744
            $targetResourceSet,
745
            $targetProperty,
746
            $filterInfo,
747
            $orderBy,
748
            $top,
749
            $skip
750
        );
751
752
        $this->validateQueryResult($queryResult, $queryType, 'IQueryProvider::getRelatedResourceSet');
753
754
        return $queryResult;
755
    }
756
757
    /**
758
     * Gets a related entity instance from an entity set identified by a key.
759
     *
760
     * @param ResourceSet      $sourceResourceSet The entity set related to the entity to be fetched
761
     * @param object           $sourceEntity      The related entity instance
762
     * @param ResourceSet      $targetResourceSet The entity set from which entity needs to be fetched
763
     * @param ResourceProperty $targetProperty    The metadata of the target property
764
     * @param KeyDescriptor    $keyDescriptor     The key to identify the entity to be fetched
765
     *
766
     * @return object|null Returns entity instance if found else null
767
     */
768
    public function getResourceFromRelatedResourceSet(
769
        ResourceSet $sourceResourceSet,
770
        $sourceEntity,
771
        ResourceSet $targetResourceSet,
772
        ResourceProperty $targetProperty,
773
        KeyDescriptor $keyDescriptor
774
    ) {
775
        $entityInstance = $this->queryProvider->getResourceFromRelatedResourceSet(
776
            $sourceResourceSet,
777
            $sourceEntity,
778
            $targetResourceSet,
779
            $targetProperty,
780
            $keyDescriptor
781
        );
782
783
        $this->_validateEntityInstance(
784
            $entityInstance,
0 ignored issues
show
Bug introduced by
It seems like $entityInstance defined by $this->queryProvider->ge...operty, $keyDescriptor) on line 775 can also be of type null; however, POData\Providers\Provide...alidateEntityInstance() does only seem to accept object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
785
            $targetResourceSet,
786
            $keyDescriptor,
787
            'IQueryProvider::getResourceFromRelatedResourceSet'
788
        );
789
790
        return $entityInstance;
791
    }
792
793
    /**
794
     * Get related resource for a resource.
795
     *
796
     * @param ResourceSet      $sourceResourceSet The source resource set
797
     * @param object           $sourceEntity      The source resource
798
     * @param ResourceSet      $targetResourceSet The resource set of the navigation
799
     *                                            property
800
     * @param ResourceProperty $targetProperty    The navigation property to be
801
     *                                            retrieved
802
     *
803
     * @return object|null The related resource if exists else null
804
     */
805
    public function getRelatedResourceReference(
806
        ResourceSet $sourceResourceSet,
807
        $sourceEntity,
808
        ResourceSet $targetResourceSet,
809
        ResourceProperty $targetProperty
810
    ) {
811
        $entityInstance = $this->queryProvider->getRelatedResourceReference(
812
            $sourceResourceSet,
813
            $sourceEntity,
814
            $targetResourceSet,
815
            $targetProperty
816
        );
817
818
        // we will not throw error if the resource reference is null
819
        // e.g. Orders(1234)/Customer => Customer can be null, this is
820
        // allowed if Customer is last segment. consider the following:
821
        // Orders(1234)/Customer/Orders => here if Customer is null then
822
        // the UriProcessor will throw error.
823
        if (!is_null($entityInstance)) {
824
            $targetResourceType
825
                = $targetResourceSet
826
                    ->getResourceType();
827
            $entityName
828
                = $targetResourceType
0 ignored issues
show
Bug introduced by
The method getName does only exist in ReflectionClass, but not in POData\Providers\Metadata\Type\IType.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
829
                    ->getInstanceType()
830
                    ->getName();
831 View Code Duplication
            if (!is_object($entityInstance)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
832
                || !($entityInstance instanceof $entityName)
833
            ) {
834
                throw ODataException::createInternalServerError(
835
                    Messages::providersWrapperIDSQPMethodReturnsUnExpectedType(
836
                        $entityName,
837
                        'IQueryProvider::getRelatedResourceReference'
838
                    )
839
                );
840
            }
841
            foreach ($targetProperty->getResourceType()->getKeyProperties()
842
 as $keyName => $resourceProperty) {
843
                try {
844
                    $keyValue = $targetResourceType->getPropertyValue($entityInstance, $keyName);
845
                    if (is_null($keyValue)) {
846
                        throw ODataException::createInternalServerError(
847
                            Messages::providersWrapperIDSQPMethodReturnsInstanceWithNullKeyProperties('IDSQP::getRelatedResourceReference')
848
                        );
849
                    }
850
                } catch (\ReflectionException $reflectionException) {
851
                    //throw ODataException::createInternalServerError(
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
852
                    //    Messages::orderByParserFailedToAccessOrInitializeProperty(
853
                    //        $resourceProperty->getName(), $resourceType->getName()
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
854
                    //    )
855
                    //);
856
                }
857
            }
858
        }
859
860
        return $entityInstance;
861
    }
862
863
    /**
864
     * Validate the given entity instance.
865
     *
866
     * @param object        $entityInstance Entity instance to validate
867
     * @param ResourceSet   &$resourceSet   Resource set to which the entity
868
     *                                      instance belongs to
869
     * @param KeyDescriptor &$keyDescriptor The key descriptor
870
     * @param string        $methodName     Method from which this function
871
     *                                      invoked
872
     *
873
     * @throws ODataException
874
     */
875
    private function _validateEntityInstance(
876
        $entityInstance,
877
        ResourceSet & $resourceSet,
878
        KeyDescriptor & $keyDescriptor,
879
        $methodName
880
    ) {
881
        if (is_null($entityInstance)) {
882
            throw ODataException::createResourceNotFoundError($resourceSet->getName());
883
        }
884
885
        $resourceType = $resourceSet->getResourceType();
886
        $entityName = $resourceType->getInstanceType()->getName();
0 ignored issues
show
Bug introduced by
The method getName does only exist in ReflectionClass, but not in POData\Providers\Metadata\Type\IType.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
887 View Code Duplication
        if (!is_object($entityInstance)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
888
            || !($entityInstance instanceof $entityName)
889
        ) {
890
            throw ODataException::createInternalServerError(
891
                Messages::providersWrapperIDSQPMethodReturnsUnExpectedType(
892
                    $entityName,
893
                    $methodName
894
                )
895
            );
896
        }
897
898
        foreach ($keyDescriptor->getValidatedNamedValues()
899
 as $keyName => $valueDescription) {
900
            try {
901
                $keyValue = $resourceType->getPropertyValue($entityInstance, $keyName);
902
                if (is_null($keyValue)) {
903
                    throw ODataException::createInternalServerError(
904
                        Messages::providersWrapperIDSQPMethodReturnsInstanceWithNullKeyProperties($methodName)
905
                    );
906
                }
907
908
                $convertedValue
909
                    = $valueDescription[1]->convert($valueDescription[0]);
910
                if ($keyValue != $convertedValue) {
911
                    throw ODataException::createInternalServerError(
912
                        Messages::providersWrapperIDSQPMethodReturnsInstanceWithNonMatchingKeys($methodName)
913
                    );
914
                }
915
            } catch (\ReflectionException $reflectionException) {
916
                //throw ODataException::createInternalServerError(
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
917
                //  Messages::orderByParserFailedToAccessOrInitializeProperty(
918
                //      $resourceProperty->getName(), $resourceType->getName()
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
919
                //  )
920
                //);
921
            }
922
        }
923
    }
924
    
925
        /**
926
     * Updates a resource
927
     *
928
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
929
     * @param object           $sourceEntityInstance The source entity instance
930
     * @param KeyDescriptor    $keyDescriptor        The key identifying the entity to fetch
931
     * @param object           $data                 The New data for the entity instance.
932
     * @param bool             $shouldUpdate        Should undefined values be updated or reset to default
933
     *
934
     * @return object|null The new resource value if it is assignable or throw exception for null.
935
     */
936
    public function updateResource(
937
        ResourceSet $sourceResourceSet,
938
        $sourceEntityInstance,
939
        KeyDescriptor $keyDescriptor,
940
        $data,
941
        $shouldUpdate = false
942
    ) {
943
        return $this->queryProvider->updateResource(
944
        $sourceResourceSet,
945
        $sourceEntityInstance,
946
        $keyDescriptor,
947
        $data,
948
        $shouldUpdate
949
    );
950
    }
951
    /**
952
     * Delete resource from a resource set.
953
     * @param ResourceSet|null $sourceResourceSet
954
     * @param object           $sourceEntityInstance
955
     *
956
     * return bool true if resources sucessfully deteled, otherwise false.
957
     */
958
    public function deleteResource(
959
        ResourceSet $sourceResourceSet,
960
        $sourceEntityInstance
961
    ) {
962
        return $this->queryProvider->deleteResource(
963
        $sourceResourceSet,
964
        $sourceEntityInstance
965
    );
966
    }
967
    /**
968
     * @param ResourceSet      $resourceSet   The entity set containing the entity to fetch
969
     * @param object           $sourceEntityInstance The source entity instance
970
     * @param object           $data                 The New data for the entity instance.
971
     *
972
     * returns object|null returns the newly created model if sucessful or null if model creation failed.
973
     */
974
    public function createResourceforResourceSet(
975
        ResourceSet $resourceSet,
976
        $sourceEntityInstance,
977
        $data
978
    ) {
979
        return $this->queryProvider->createResourceforResourceSet(
980
        $resourceSet,
981
        $sourceEntityInstance,
982
        $data
983
    );
984
    }
985
986
    /**
987
     * Assert that the given condition is true.
988
     *
989
     * @param bool   $condition         Condition to be asserted
990
     * @param string $conditionAsString String containing message incase
991
     *                                  if assertion fails
992
     *
993
     * @throws InvalidOperationException Incase if assertion fails
994
     */
995
    protected function assert($condition, $conditionAsString)
996
    {
997
        if (!$condition) {
998
            throw new InvalidOperationException("Unexpected state, expecting $conditionAsString");
999
        }
1000
    }
1001
}
1002