LaravelReadQuery   A
last analyzed

Complexity

Total Complexity 41

Size/Duplication

Total Lines 429
Duplicated Lines 0 %

Importance

Changes 17
Bugs 0 Features 0
Metric Value
wmc 41
eloc 144
c 17
b 0
f 0
dl 0
loc 429
rs 9.1199

10 Methods

Rating   Name   Duplication   Size   Complexity  
A getResourceFromResourceSet() 0 6 1
A buildOrderBy() 0 15 5
A packageResourceSetResults() 0 20 6
A checkAuth() 0 15 6
A getResourceFromRelatedResourceSet() 0 19 2
A getResource() 0 29 5
A getRelatedResourceSet() 0 29 1
A applyFiltering() 0 58 5
B getResourceSet() 0 66 7
A getRelatedResourceReference() 0 20 3

How to fix   Complexity   

Complex Class

Complex classes like LaravelReadQuery often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use LaravelReadQuery, and based on these observations, apply Extract Interface, too.

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