LaravelReadQuery::getResourceSet()   B
last analyzed

Complexity

Conditions 7
Paths 64

Size

Total Lines 66
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 7
Bugs 0 Features 0
Metric Value
eloc 36
c 7
b 0
f 0
dl 0
loc 66
rs 8.4106
cc 7
nc 64
nop 9

How to fix   Long Method    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace 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
        $sourceEntityInstance = $this->checkSourceInstance($sourceEntityInstance, $resourceSet);
215
216
        $this->checkAuth($sourceEntityInstance);
217
        $modelLoad = null;
218
        if ($sourceEntityInstance instanceof Model) {
219
            $modelLoad = $sourceEntityInstance->getEagerLoad();
220
        } elseif ($sourceEntityInstance instanceof Relation) {
221
            /** @var MetadataTrait $model */
222
            $model     = $sourceEntityInstance->getRelated();
223
            $modelLoad = $model->getEagerLoad();
224
        }
225
        if (!(isset($modelLoad))) {
226
            throw new InvalidOperationException('');
227
        }
228
229
        $this->processKeyDescriptor($sourceEntityInstance, $keyDescriptor);
230
        foreach ($whereCondition as $fieldName => $fieldValue) {
231
            $sourceEntityInstance = $sourceEntityInstance->where($fieldName, $fieldValue);
232
        }
233
234
        $sourceEntityInstance = $sourceEntityInstance->get();
235
        return $sourceEntityInstance->first();
236
    }
237
238
    /**
239
     * Get related resource for a resource
240
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
241
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity.
242
     *
243
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
244
     * @param Model            $sourceEntityInstance the source entity instance
245
     * @param ResourceSet      $targetResourceSet    The entity set containing the entity pointed to by the nav property
246
     * @param ResourceProperty $targetProperty       The navigation property to fetch
247
     *
248
     * @throws ODataException
249
     * @throws InvalidOperationException
250
     * @throws \ReflectionException
251
     * @return Model|null                The related resource if found else null
252
     */
253
    public function getRelatedResourceReference(
254
        /* @noinspection PhpUnusedParameterInspection */
255
        ResourceSet $sourceResourceSet,
256
        Model $sourceEntityInstance,
257
        ResourceSet $targetResourceSet,
258
        ResourceProperty $targetProperty
259
    ) {
260
        $this->checkAuth($sourceEntityInstance);
261
262
        $propertyName = $targetProperty->getName();
263
        $propertyName = $this->getLaravelRelationName($propertyName);
264
        /** @var Model|null $result */
265
        $result = $sourceEntityInstance->{$propertyName}()->first();
266
        if (null === $result) {
267
            return null;
268
        }
269
        if ($targetProperty->getResourceType()->getInstanceType()->getName() != get_class($result)) {
270
            return null;
271
        }
272
        return $result;
273
    }
274
275
    /**
276
     * Gets a related entity instance from an entity set identified by a key
277
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33).
278
     *
279
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
280
     * @param Model            $sourceEntityInstance the source entity instance
281
     * @param ResourceSet      $targetResourceSet    The entity set containing the entity to fetch
282
     * @param ResourceProperty $targetProperty       the metadata of the target property
283
     * @param KeyDescriptor    $keyDescriptor        The key identifying the entity to fetch
284
     *
285
     * @throws InvalidOperationException
286
     * @throws \Exception
287
     * @return Model|null                Returns entity instance if found else null
288
     */
289
    public function getResourceFromRelatedResourceSet(
290
        /* @noinspection PhpUnusedParameterInspection */
291
        ResourceSet $sourceResourceSet,
292
        Model $sourceEntityInstance,
293
        ResourceSet $targetResourceSet,
294
        ResourceProperty $targetProperty,
295
        KeyDescriptor $keyDescriptor
296
    ) {
297
        $propertyName = $targetProperty->getName();
298
        if (!method_exists($sourceEntityInstance, $propertyName)) {
299
            $msg = 'Relation method, ' . $propertyName . ', does not exist on supplied entity.';
300
            throw new InvalidArgumentException($msg);
301
        }
302
        // take key descriptor and turn it into where clause here, rather than in getResource call
303
        /** @var Relation $sourceEntityInstance */
304
        $sourceEntityInstance = $sourceEntityInstance->{$propertyName}();
305
        $this->processKeyDescriptor($sourceEntityInstance, $keyDescriptor);
306
        $result = $this->getResource($sourceResourceSet, null, [], [], $sourceEntityInstance);
307
        return $result;
308
    }
309
310
    /**
311
     * @param Model|Relation|null $sourceEntityInstance
312
     * @param Model|null          $checkInstance
313
     *
314
     * @throws ODataException
315
     */
316
    private function checkAuth($sourceEntityInstance, $checkInstance = null): void
317
    {
318
        $check = array_reduce([$sourceEntityInstance, $checkInstance], function ($carry, $item) {
319
            if (null === $carry && ($item instanceof Model || $item instanceof Relation)) {
320
                return $item;
321
            } else {
322
                return $carry;
323
            }
324
        }, null);
325
326
327
        /** @var class-string|null $sourceName */
328
        $sourceName = null !== $check ? get_class($check) : null;
329
        if (!$this->getAuth()->canAuth(ActionVerb::READ(), $sourceName, $check)) {
330
            throw new ODataException('Access denied', 403);
331
        }
332
    }
333
334
    /**
335
     * @param  Model|Builder             $sourceEntityInstance
336
     * @param  bool                      $nullFilter
337
     * @param  string[]                  $rawLoad
338
     * @param  int                       $top
339
     * @param  int                       $skip
340
     * @param  callable|null             $isvalid
341
     * @throws InvalidOperationException
342
     * @return mixed[]
343
     */
344
    protected function applyFiltering(
345
        $sourceEntityInstance,
346
        bool $nullFilter,
347
        array $rawLoad = [],
348
        int $top = PHP_INT_MAX,
349
        int $skip = 0,
350
        callable $isvalid = null
351
    ) {
352
        $bulkSetCount = $sourceEntityInstance->count();
353
        $bigSet       = 20000 < $bulkSetCount;
354
355
        if ($nullFilter) {
356
            // default no-filter case, palm processing off to database engine - is a lot faster
357
            $resultSet = $sourceEntityInstance
358
                ->skip($skip)
359
                ->take($top)->with($rawLoad)
360
                ->get();
361
            $resultCount = $bulkSetCount;
362
        } elseif ($bigSet) {
363
            if (!(isset($isvalid))) {
364
                $msg = 'Filter closure not set';
365
                throw new InvalidOperationException($msg);
366
            }
367
            $resultSet = new Collection([]);
368
            $rawCount  = 0;
369
            $rawTop    = min($top, $bulkSetCount);
370
371
            // loop thru, chunk by chunk, to reduce chances of exhausting memory
372
            $sourceEntityInstance->chunk(
373
                5000,
374
                function (Collection $results) use ($isvalid, &$skip, &$resultSet, &$rawCount, $rawTop) {
375
                    // apply filter
376
                    $results = $results->filter($isvalid);
377
                    // need to iterate through full result set to find count of items matching filter,
378
                    // so we can't bail out early
379
                    $rawCount += $results->count();
380
                    // now bolt on filtrate to accumulating result set if we haven't accumulated enough bitz
381
                    if ($rawTop > $resultSet->count() + $skip) {
382
                        $resultSet = collect(array_merge($resultSet->all(), $results->all()));
383
                        $sliceAmount = min($skip, $resultSet->count());
384
                        $resultSet = $resultSet->slice($sliceAmount);
385
                        $skip -= $sliceAmount;
386
                    }
387
                }
388
            );
389
390
            // clean up residual to-be-skipped records
391
            $resultSet   = $resultSet->slice($skip);
392
            $resultCount = $rawCount;
393
        } else {
394
            /** @var Collection $resultSet */
395
            $resultSet   = $sourceEntityInstance->with($rawLoad)->get();
396
            $resultSet   = $resultSet->filter($isvalid);
397
            $resultCount = $resultSet->count();
398
399
            $resultSet = $resultSet->slice($skip);
400
        }
401
        return [$bulkSetCount, $resultSet, $resultCount, $skip];
402
    }
403
404
    /**
405
     * @param  Model|Relation           $sourceEntityInstance
406
     * @param  string                   $tableName
407
     * @param  InternalOrderByInfo|null $orderBy
408
     * @return mixed
409
     */
410
    protected function buildOrderBy($sourceEntityInstance, string $tableName, InternalOrderByInfo $orderBy = null)
411
    {
412
        if (null != $orderBy) {
413
            foreach ($orderBy->getOrderByInfo()->getOrderByPathSegments() as $order) {
414
                foreach ($order->getSubPathSegments() as $subOrder) {
415
                    $subName              = $subOrder->getName();
416
                    $subName              = $tableName . '.' . $subName;
417
                    $sourceEntityInstance = $sourceEntityInstance->orderBy(
418
                        $subName,
419
                        $order->isAscending() ? 'asc' : 'desc'
420
                    );
421
                }
422
            }
423
        }
424
        return $sourceEntityInstance;
425
    }
426
427
    /**
428
     * @param QueryType             $queryType
429
     * @param int                   $skip
430
     * @param QueryResult           $result
431
     * @param Collection|Model[]    $resultSet
432
     * @param int                   $resultCount
433
     * @param int                   $bulkSetCount
434
     */
435
    protected function packageResourceSetResults(
436
        QueryType $queryType,
437
        int $skip,
438
        QueryResult $result,
439
        $resultSet,
440
        int $resultCount,
441
        int $bulkSetCount
442
    ): void {
443
        $qVal = $queryType;
444
        if (QueryType::ENTITIES() == $qVal || QueryType::ENTITIES_WITH_COUNT() == $qVal) {
445
            $result->results = [];
446
            foreach ($resultSet as $res) {
447
                $result->results[] = $res;
448
            }
449
        }
450
        if (QueryType::COUNT() == $qVal || QueryType::ENTITIES_WITH_COUNT() == $qVal) {
451
            $result->count = $resultCount;
452
        }
453
        $hazMore         = $bulkSetCount > $skip + count($resultSet);
454
        $result->hasMore = $hazMore;
455
    }
456
}
457