Passed
Pull Request — master (#198)
by Alex
04:20
created

LaravelReadQuery::getRelatedResourceSet()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 29
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 13
c 3
b 0
f 0
dl 0
loc 29
rs 9.8333
cc 1
nc 1
nop 10

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace AlgoWeb\PODataLaravel\Query;
6
7
use AlgoWeb\PODataLaravel\Enums\ActionVerb;
8
use AlgoWeb\PODataLaravel\Models\MetadataTrait;
9
use Illuminate\Database\Eloquent\Builder;
10
use Illuminate\Database\Eloquent\Model;
11
use Illuminate\Database\Eloquent\Relations\Relation;
12
use Illuminate\Support\Collection;
13
use Illuminate\Support\Facades\App;
14
use POData\Common\InvalidOperationException;
15
use POData\Common\ODataException;
16
use POData\Providers\Metadata\ResourceProperty;
17
use POData\Providers\Metadata\ResourceSet;
18
use POData\Providers\Query\QueryResult;
19
use POData\Providers\Query\QueryType;
20
use POData\UriProcessor\QueryProcessor\ExpressionParser\FilterInfo;
21
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
22
use POData\UriProcessor\QueryProcessor\SkipTokenParser\SkipTokenInfo;
23
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
24
use Symfony\Component\Process\Exception\InvalidArgumentException;
25
26
class LaravelReadQuery extends LaravelBaseQuery
27
{
28
    use LaravelReadQueryUtilityTrait;
29
30
    /**
31
     * Gets collection of entities belongs to an entity set
32
     * IE: http://host/EntitySet
33
     *  http://host/EntitySet?$skip=10&$top=5&filter=Prop gt Value.
34
     *
35
     * @param QueryType                $queryType            Is this is a query for a count, entities,
36
     *                                                       or entities-with-count?
37
     * @param ResourceSet              $resourceSet          The entity set containing the entities to fetch
38
     * @param FilterInfo|null          $filterInfo           The $filter parameter of the OData query.  NULL if absent
39
     * @param null|InternalOrderByInfo $orderBy              sorted order if we want to get the data in some
40
     *                                                       specific order
41
     * @param int|null                 $top                  number of records which need to be retrieved
42
     * @param int|null                 $skip                 number of records which need to be skipped
43
     * @param SkipTokenInfo|null       $skipToken            value indicating what records to skip
44
     * @param string[]|null            $eagerLoad            array of relations to eager load
45
     * @param Model|Relation|null      $sourceEntityInstance Starting point of query
46
     *
47
     * @throws InvalidArgumentException
48
     * @throws InvalidOperationException
49
     * @throws \ReflectionException
50
     * @throws ODataException
51
     * @return QueryResult
52
     */
53
    public function getResourceSet(
54
        QueryType $queryType,
55
        ResourceSet $resourceSet,
56
        FilterInfo $filterInfo = null,
57
        $orderBy = null,
58
        $top = null,
59
        $skip = null,
60
        SkipTokenInfo $skipToken = null,
61
        array $eagerLoad = null,
62
        $sourceEntityInstance = null
63
    ) {
64
        $rawLoad = $this->processEagerLoadList($eagerLoad);
65
66
        $sourceEntityInstance = $this->checkSourceInstance($sourceEntityInstance, $resourceSet);
67
68
        /** @var MetadataTrait $model */
69
        $model     = $sourceEntityInstance instanceof Model ? $sourceEntityInstance : $sourceEntityInstance->getRelated();
70
        $modelLoad = $model->getEagerLoad();
71
        $tableName = $model->getTable();
72
        $rawLoad   = array_unique(array_merge($rawLoad, $modelLoad));
73
74
        $checkInstance = $sourceEntityInstance instanceof Model ? $sourceEntityInstance : null;
75
        $this->checkAuth($sourceEntityInstance, $checkInstance);
76
77
        $result          = new QueryResult();
78
        $result->results = null;
79
        $result->count   = null;
80
81
        $sourceEntityInstance = $this->buildOrderBy($sourceEntityInstance, $tableName, $orderBy);
82
83
        // throttle up for trench run
84
        if (null != $skipToken) {
85
            $sourceEntityInstance = $this->processSkipToken($skipToken, $sourceEntityInstance);
86
        }
87
88
        if (!isset($skip)) {
89
            $skip = 0;
90
        }
91
        if (!isset($top)) {
92
            $top = PHP_INT_MAX;
93
        }
94
95
        $nullFilter = !isset($filterInfo);
96
        $isvalid    = null;
97
        if (isset($filterInfo)) {
98
            $method  = 'return ' . $filterInfo->getExpressionAsString() . ';';
99
            $clln    = $resourceSet->getResourceType()->getName();
100
            $isvalid = function ($inputD) use ($clln, $method) {
101
                ${$clln} = $inputD;
102
                return eval($method);
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
103
            };
104
        }
105
106
        list($bulkSetCount, $resultSet, $resultCount, $skip) = $this->applyFiltering(
107
            $sourceEntityInstance,
108
            $nullFilter,
109
            $rawLoad,
110
            $top,
111
            $skip,
112
            $isvalid
113
        );
114
115
        $resultSet = $resultSet->take($top);
116
117
        $this->packageResourceSetResults($queryType, $skip, $result, $resultSet, $resultCount, $bulkSetCount);
118
        return $result;
119
    }
120
121
    /**
122
     * Get related resource set for a resource
123
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection
124
     * http://host/EntitySet?$expand=NavigationPropertyToCollection.
125
     *
126
     * @param QueryType          $queryType            Is this is a query for a count, entities, or entities-with-count
127
     * @param ResourceSet        $sourceResourceSet    The entity set containing the source entity
128
     * @param Model              $sourceEntityInstance The source entity instance
129
     * @param ResourceSet        $targetResourceSet    The resource set pointed to by the navigation property
130
     * @param ResourceProperty   $targetProperty       The navigation property to retrieve
131
     * @param FilterInfo|null    $filter               The $filter parameter of the OData query.  NULL if none specified
132
     * @param mixed|null         $orderBy              sorted order if we want to get the data in some specific order
133
     * @param int|null           $top                  number of records which need to be retrieved
134
     * @param int|null           $skip                 number of records which need to be skipped
135
     * @param SkipTokenInfo|null $skipToken            value indicating what records to skip
136
     *
137
     * @throws InvalidOperationException
138
     * @throws ODataException
139
     * @throws \ReflectionException
140
     * @return QueryResult
141
     */
142
    public function getRelatedResourceSet(
143
        QueryType $queryType,
144
        ResourceSet $sourceResourceSet,
145
        Model $sourceEntityInstance,
146
        /* @noinspection PhpUnusedParameterInspection */
147
        ResourceSet $targetResourceSet,
148
        ResourceProperty $targetProperty,
149
        FilterInfo $filter = null,
150
        $orderBy = null,
151
        $top = null,
152
        $skip = null,
153
        SkipTokenInfo $skipToken = null
154
    ) {
155
        $this->checkAuth($sourceEntityInstance);
156
157
        $propertyName = $targetProperty->getName();
158
        /** @var Relation $results */
159
        $results = $sourceEntityInstance->{$propertyName}();
160
161
        return $this->getResourceSet(
162
            $queryType,
163
            $sourceResourceSet,
164
            $filter,
165
            $orderBy,
166
            $top,
167
            $skip,
168
            $skipToken,
169
            null,
170
            $results
171
        );
172
    }
173
174
    /**
175
     * Gets an entity instance from an entity set identified by a key
176
     * IE: http://host/EntitySet(1L)
177
     * http://host/EntitySet(KeyA=2L,KeyB='someValue').
178
     *
179
     * @param ResourceSet        $resourceSet   The entity set containing the entity to fetch
180
     * @param KeyDescriptor|null $keyDescriptor The key identifying the entity to fetch
181
     * @param string[]|null      $eagerLoad     array of relations to eager load
182
     *
183
     * @throws \Exception;
184
     * @return Model|null  Returns entity instance if found else null
185
     */
186
    public function getResourceFromResourceSet(
187
        ResourceSet $resourceSet,
188
        KeyDescriptor $keyDescriptor = null,
189
        array $eagerLoad = null
190
    ) {
191
        return $this->getResource($resourceSet, $keyDescriptor, [], $eagerLoad);
192
    }
193
194
195
    /**
196
     * Common method for getResourceFromRelatedResourceSet() and getResourceFromResourceSet().
197
     *
198
     * @param ResourceSet           $resourceSet
199
     * @param KeyDescriptor|null    $keyDescriptor
200
     * @param Model|Relation|null   $sourceEntityInstance Starting point of query
201
     * @param array<string, string> $whereCondition
202
     * @param string[]|null         $eagerLoad            array of relations to eager load
203
     *
204
     * @throws \Exception
205
     * @return Model|null
206
     */
207
    public function getResource(
208
        ResourceSet $resourceSet,
209
        KeyDescriptor $keyDescriptor = null,
210
        array $whereCondition = [],
211
        array $eagerLoad = null,
212
        $sourceEntityInstance = null
213
    ): ?Model {
214
215
        $sourceEntityInstance = $this->checkSourceInstance($sourceEntityInstance, $resourceSet);
216
217
        $this->checkAuth($sourceEntityInstance);
218
        $modelLoad = null;
219
        if ($sourceEntityInstance instanceof Model) {
220
            $modelLoad = $sourceEntityInstance->getEagerLoad();
221
        } elseif ($sourceEntityInstance instanceof Relation) {
222
            /** @var MetadataTrait $model */
223
            $model     = $sourceEntityInstance->getRelated();
224
            $modelLoad = $model->getEagerLoad();
225
        }
226
        if (!(isset($modelLoad))) {
227
            throw new InvalidOperationException('');
228
        }
229
230
        $this->processKeyDescriptor($sourceEntityInstance, $keyDescriptor);
231
        foreach ($whereCondition as $fieldName => $fieldValue) {
232
            $sourceEntityInstance = $sourceEntityInstance->where($fieldName, $fieldValue);
233
        }
234
235
        $sourceEntityInstance = $sourceEntityInstance->get();
236
        return $sourceEntityInstance->first();
237
    }
238
239
    /**
240
     * Get related resource for a resource
241
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
242
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity.
243
     *
244
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
245
     * @param Model            $sourceEntityInstance the source entity instance
246
     * @param ResourceSet      $targetResourceSet    The entity set containing the entity pointed to by the nav property
247
     * @param ResourceProperty $targetProperty       The navigation property to fetch
248
     *
249
     * @throws ODataException
250
     * @throws InvalidOperationException
251
     * @throws \ReflectionException
252
     * @return Model|null                The related resource if found else null
253
     */
254
    public function getRelatedResourceReference(
255
        /* @noinspection PhpUnusedParameterInspection */
256
        ResourceSet $sourceResourceSet,
257
        Model $sourceEntityInstance,
258
        ResourceSet $targetResourceSet,
259
        ResourceProperty $targetProperty
260
    ) {
261
        $this->checkAuth($sourceEntityInstance);
262
263
        $propertyName = $targetProperty->getName();
264
        $propertyName = $this->getLaravelRelationName($propertyName);
265
        /** @var Model|null $result */
266
        $result = $sourceEntityInstance->{$propertyName}()->first();
267
        if (null === $result) {
268
            return null;
269
        }
270
        if ($targetProperty->getResourceType()->getInstanceType()->getName() != get_class($result)) {
271
            return null;
272
        }
273
        return $result;
274
    }
275
276
    /**
277
     * Gets a related entity instance from an entity set identified by a key
278
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33).
279
     *
280
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
281
     * @param Model            $sourceEntityInstance the source entity instance
282
     * @param ResourceSet      $targetResourceSet    The entity set containing the entity to fetch
283
     * @param ResourceProperty $targetProperty       the metadata of the target property
284
     * @param KeyDescriptor    $keyDescriptor        The key identifying the entity to fetch
285
     *
286
     * @throws InvalidOperationException
287
     * @throws \Exception
288
     * @return Model|null                Returns entity instance if found else null
289
     */
290
    public function getResourceFromRelatedResourceSet(
291
        /* @noinspection PhpUnusedParameterInspection */
292
        ResourceSet $sourceResourceSet,
293
        Model $sourceEntityInstance,
294
        ResourceSet $targetResourceSet,
295
        ResourceProperty $targetProperty,
296
        KeyDescriptor $keyDescriptor
297
    ) {
298
        $propertyName = $targetProperty->getName();
299
        if (!method_exists($sourceEntityInstance, $propertyName)) {
300
            $msg = 'Relation method, ' . $propertyName . ', does not exist on supplied entity.';
301
            throw new InvalidArgumentException($msg);
302
        }
303
        // take key descriptor and turn it into where clause here, rather than in getResource call
304
        /** @var Relation $sourceEntityInstance */
305
        $sourceEntityInstance = $sourceEntityInstance->{$propertyName}();
306
        $this->processKeyDescriptor($sourceEntityInstance, $keyDescriptor);
307
        $result = $this->getResource($sourceResourceSet, null, [], [], $sourceEntityInstance);
308
        return $result;
309
    }
310
311
    /**
312
     * @param Model|Relation|null $sourceEntityInstance
313
     * @param null|mixed          $checkInstance
314
     *
315
     * @throws ODataException
316
     */
317
    private function checkAuth($sourceEntityInstance, $checkInstance = null): void
318
    {
319
        $check = array_reduce([$sourceEntityInstance, $checkInstance], function ($carry, $item) {
320
            if ($item instanceof Model || $item instanceof Relation) {
321
                return $item;
322
            }
323
        }, null);
324
        /** @var class-string|null $sourceName */
325
        $sourceName = null !== $check ? get_class($check) : null;
326
        if (!$this->getAuth()->canAuth(ActionVerb::READ(), $sourceName, $check)) {
327
            throw new ODataException('Access denied', 403);
328
        }
329
    }
330
331
    /**
332
     * @param  Model|Builder             $sourceEntityInstance
333
     * @param  bool                      $nullFilter
334
     * @param  string[]                  $rawLoad
335
     * @param  int                       $top
336
     * @param  int                       $skip
337
     * @param  callable|null             $isvalid
338
     * @throws InvalidOperationException
339
     * @return mixed[]
340
     */
341
    protected function applyFiltering(
342
        $sourceEntityInstance,
343
        bool $nullFilter,
344
        array $rawLoad = [],
345
        int $top = PHP_INT_MAX,
346
        int $skip = 0,
347
        callable $isvalid = null
348
    ) {
349
        $bulkSetCount = $sourceEntityInstance->count();
350
        $bigSet       = 20000 < $bulkSetCount;
351
352
        if ($nullFilter) {
353
            // default no-filter case, palm processing off to database engine - is a lot faster
354
            $resultSet = $sourceEntityInstance
355
                ->skip($skip)
356
                ->take($top)->with($rawLoad)
357
                ->get();
358
            $resultCount = $bulkSetCount;
359
        } elseif ($bigSet) {
360
            if (!(isset($isvalid))) {
361
                $msg = 'Filter closure not set';
362
                throw new InvalidOperationException($msg);
363
            }
364
            $resultSet = new Collection([]);
365
            $rawCount  = 0;
366
            $rawTop    = min($top, $bulkSetCount);
367
368
            // loop thru, chunk by chunk, to reduce chances of exhausting memory
369
            $sourceEntityInstance->chunk(
370
                5000,
371
                function (Collection $results) use ($isvalid, &$skip, &$resultSet, &$rawCount, $rawTop) {
372
                    // apply filter
373
                    $results = $results->filter($isvalid);
374
                    // need to iterate through full result set to find count of items matching filter,
375
                    // so we can't bail out early
376
                    $rawCount += $results->count();
377
                    // now bolt on filtrate to accumulating result set if we haven't accumulated enough bitz
378
                    if ($rawTop > $resultSet->count() + $skip) {
379
                        $resultSet = collect(array_merge($resultSet->all(), $results->all()));
380
                        $sliceAmount = min($skip, $resultSet->count());
381
                        $resultSet = $resultSet->slice($sliceAmount);
382
                        $skip -= $sliceAmount;
383
                    }
384
                }
385
            );
386
387
            // clean up residual to-be-skipped records
388
            $resultSet   = $resultSet->slice($skip);
389
            $resultCount = $rawCount;
390
        } else {
391
            /** @var Collection $resultSet */
392
            $resultSet   = $sourceEntityInstance->with($rawLoad)->get();
393
            $resultSet   = $resultSet->filter($isvalid);
394
            $resultCount = $resultSet->count();
395
396
            $resultSet = $resultSet->slice($skip);
397
        }
398
        return [$bulkSetCount, $resultSet, $resultCount, $skip];
399
    }
400
401
    /**
402
     * @param  Model|Relation           $sourceEntityInstance
403
     * @param  string                   $tableName
404
     * @param  InternalOrderByInfo|null $orderBy
405
     * @return mixed
406
     */
407
    protected function buildOrderBy($sourceEntityInstance, string $tableName, InternalOrderByInfo $orderBy = null)
408
    {
409
        if (null != $orderBy) {
410
            foreach ($orderBy->getOrderByInfo()->getOrderByPathSegments() as $order) {
411
                foreach ($order->getSubPathSegments() as $subOrder) {
412
                    $subName              = $subOrder->getName();
413
                    $subName              = $tableName . '.' . $subName;
414
                    $sourceEntityInstance = $sourceEntityInstance->orderBy(
415
                        $subName,
416
                        $order->isAscending() ? 'asc' : 'desc'
417
                    );
418
                }
419
            }
420
        }
421
        return $sourceEntityInstance;
422
    }
423
424
    /**
425
     * @param QueryType             $queryType
426
     * @param int                   $skip
427
     * @param QueryResult           $result
428
     * @param Collection|Model[]    $resultSet
429
     * @param int                   $resultCount
430
     * @param int                   $bulkSetCount
431
     */
432
    protected function packageResourceSetResults(
433
        QueryType $queryType,
434
        int $skip,
435
        QueryResult $result,
436
        $resultSet,
437
        int $resultCount,
438
        int $bulkSetCount
439
    ): void {
440
        $qVal = $queryType;
441
        if (QueryType::ENTITIES() == $qVal || QueryType::ENTITIES_WITH_COUNT() == $qVal) {
442
            $result->results = [];
443
            foreach ($resultSet as $res) {
444
                $result->results[] = $res;
445
            }
446
        }
447
        if (QueryType::COUNT() == $qVal || QueryType::ENTITIES_WITH_COUNT() == $qVal) {
448
            $result->count = $resultCount;
449
        }
450
        $hazMore         = $bulkSetCount > $skip + count($resultSet);
451
        $result->hasMore = $hazMore;
452
    }
453
}
454