Completed
Push — master ( 9f24e3...f5ae7e )
by Alex
16s queued 12s
created

LaravelReadQuery::getResourceSet()   B

Complexity

Conditions 7
Paths 64

Size

Total Lines 66
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 8
Bugs 0 Features 0
Metric Value
eloc 36
c 8
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 null|mixed          $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 ($item instanceof Model || $item instanceof Relation) {
320
                return $item;
321
            }
322
        }, null);
323
        /** @var class-string|null $sourceName */
324
        $sourceName = null !== $check ? get_class($check) : null;
325
        if (!$this->getAuth()->canAuth(ActionVerb::READ(), $sourceName, $check)) {
326
            throw new ODataException('Access denied', 403);
327
        }
328
    }
329
330
    /**
331
     * @param  Model|Builder             $sourceEntityInstance
332
     * @param  bool                      $nullFilter
333
     * @param  string[]                  $rawLoad
334
     * @param  int                       $top
335
     * @param  int                       $skip
336
     * @param  callable|null             $isvalid
337
     * @throws InvalidOperationException
338
     * @return mixed[]
339
     */
340
    protected function applyFiltering(
341
        $sourceEntityInstance,
342
        bool $nullFilter,
343
        array $rawLoad = [],
344
        int $top = PHP_INT_MAX,
345
        int $skip = 0,
346
        callable $isvalid = null
347
    ) {
348
        $bulkSetCount = $sourceEntityInstance->count();
349
        $bigSet       = 20000 < $bulkSetCount;
350
351
        if ($nullFilter) {
352
            // default no-filter case, palm processing off to database engine - is a lot faster
353
            $resultSet = $sourceEntityInstance
354
                ->skip($skip)
355
                ->take($top)->with($rawLoad)
356
                ->get();
357
            $resultCount = $bulkSetCount;
358
        } elseif ($bigSet) {
359
            if (!(isset($isvalid))) {
360
                $msg = 'Filter closure not set';
361
                throw new InvalidOperationException($msg);
362
            }
363
            $resultSet = new Collection([]);
364
            $rawCount  = 0;
365
            $rawTop    = min($top, $bulkSetCount);
366
367
            // loop thru, chunk by chunk, to reduce chances of exhausting memory
368
            $sourceEntityInstance->chunk(
369
                5000,
370
                function (Collection $results) use ($isvalid, &$skip, &$resultSet, &$rawCount, $rawTop) {
371
                    // apply filter
372
                    $results = $results->filter($isvalid);
373
                    // need to iterate through full result set to find count of items matching filter,
374
                    // so we can't bail out early
375
                    $rawCount += $results->count();
376
                    // now bolt on filtrate to accumulating result set if we haven't accumulated enough bitz
377
                    if ($rawTop > $resultSet->count() + $skip) {
378
                        $resultSet = collect(array_merge($resultSet->all(), $results->all()));
379
                        $sliceAmount = min($skip, $resultSet->count());
380
                        $resultSet = $resultSet->slice($sliceAmount);
381
                        $skip -= $sliceAmount;
382
                    }
383
                }
384
            );
385
386
            // clean up residual to-be-skipped records
387
            $resultSet   = $resultSet->slice($skip);
388
            $resultCount = $rawCount;
389
        } else {
390
            /** @var Collection $resultSet */
391
            $resultSet   = $sourceEntityInstance->with($rawLoad)->get();
392
            $resultSet   = $resultSet->filter($isvalid);
393
            $resultCount = $resultSet->count();
394
395
            $resultSet = $resultSet->slice($skip);
396
        }
397
        return [$bulkSetCount, $resultSet, $resultCount, $skip];
398
    }
399
400
    /**
401
     * @param  Model|Relation           $sourceEntityInstance
402
     * @param  string                   $tableName
403
     * @param  InternalOrderByInfo|null $orderBy
404
     * @return mixed
405
     */
406
    protected function buildOrderBy($sourceEntityInstance, string $tableName, InternalOrderByInfo $orderBy = null)
407
    {
408
        if (null != $orderBy) {
409
            foreach ($orderBy->getOrderByInfo()->getOrderByPathSegments() as $order) {
410
                foreach ($order->getSubPathSegments() as $subOrder) {
411
                    $subName              = $subOrder->getName();
412
                    $subName              = $tableName . '.' . $subName;
413
                    $sourceEntityInstance = $sourceEntityInstance->orderBy(
414
                        $subName,
415
                        $order->isAscending() ? 'asc' : 'desc'
416
                    );
417
                }
418
            }
419
        }
420
        return $sourceEntityInstance;
421
    }
422
423
    /**
424
     * @param QueryType             $queryType
425
     * @param int                   $skip
426
     * @param QueryResult           $result
427
     * @param Collection|Model[]    $resultSet
428
     * @param int                   $resultCount
429
     * @param int                   $bulkSetCount
430
     */
431
    protected function packageResourceSetResults(
432
        QueryType $queryType,
433
        int $skip,
434
        QueryResult $result,
435
        $resultSet,
436
        int $resultCount,
437
        int $bulkSetCount
438
    ): void {
439
        $qVal = $queryType;
440
        if (QueryType::ENTITIES() == $qVal || QueryType::ENTITIES_WITH_COUNT() == $qVal) {
441
            $result->results = [];
442
            foreach ($resultSet as $res) {
443
                $result->results[] = $res;
444
            }
445
        }
446
        if (QueryType::COUNT() == $qVal || QueryType::ENTITIES_WITH_COUNT() == $qVal) {
447
            $result->count = $resultCount;
448
        }
449
        $hazMore         = $bulkSetCount > $skip + count($resultSet);
450
        $result->hasMore = $hazMore;
451
    }
452
}
453