Passed
Pull Request — master (#198)
by Alex
06:32
created

LaravelReadQuery::getResourceSet()   B

Complexity

Conditions 7
Paths 64

Size

Total Lines 66
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 9
Bugs 0 Features 0
Metric Value
eloc 36
c 9
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\Collection;
11
use Illuminate\Database\Eloquent\Model;
12
use Illuminate\Database\Eloquent\Relations\Relation;
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|null    $resourceSet
199
     * @param KeyDescriptor|null  $keyDescriptor
200
     * @param Model|Relation|null $sourceEntityInstance Starting point of query
201
     * @param array               $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 = null,
209
        KeyDescriptor $keyDescriptor = null,
210
        array $whereCondition = [],
211
        array $eagerLoad = null,
212
        $sourceEntityInstance = null
213
    ) : ?Model {
214
        if (null == $resourceSet && null == $sourceEntityInstance) {
215
            $msg = 'Must supply at least one of a resource set and source entity.';
216
            throw new \Exception($msg);
217
        }
218
219
        $sourceEntityInstance = $this->checkSourceInstance($sourceEntityInstance, $resourceSet);
220
221
        $this->checkAuth($sourceEntityInstance);
222
        $modelLoad = null;
223
        if ($sourceEntityInstance instanceof Model) {
224
            $modelLoad = $sourceEntityInstance->getEagerLoad();
225
        } elseif ($sourceEntityInstance instanceof Relation) {
226
            /** @var MetadataTrait $model */
227
            $model     = $sourceEntityInstance->getRelated();
228
            $modelLoad = $model->getEagerLoad();
229
        }
230
        if (!(isset($modelLoad))) {
231
            throw new InvalidOperationException('');
232
        }
233
234
        $this->processKeyDescriptor($sourceEntityInstance, $keyDescriptor);
235
        foreach ($whereCondition as $fieldName => $fieldValue) {
236
            $sourceEntityInstance = $sourceEntityInstance->where($fieldName, $fieldValue);
237
        }
238
239
        $sourceEntityInstance = $sourceEntityInstance->get();
240
        return $sourceEntityInstance->first();
241
    }
242
243
    /**
244
     * Get related resource for a resource
245
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
246
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity.
247
     *
248
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
249
     * @param Model            $sourceEntityInstance the source entity instance
250
     * @param ResourceSet      $targetResourceSet    The entity set containing the entity pointed to by the nav property
251
     * @param ResourceProperty $targetProperty       The navigation property to fetch
252
     *
253
     * @throws ODataException
254
     * @throws InvalidOperationException
255
     * @throws \ReflectionException
256
     * @return Model|null                The related resource if found else null
257
     */
258
    public function getRelatedResourceReference(
259
        /* @noinspection PhpUnusedParameterInspection */
260
        ResourceSet $sourceResourceSet,
261
        Model $sourceEntityInstance,
262
        ResourceSet $targetResourceSet,
263
        ResourceProperty $targetProperty
264
    ) {
265
        $this->checkAuth($sourceEntityInstance);
266
267
        $propertyName = $targetProperty->getName();
268
        $propertyName = $this->getLaravelRelationName($propertyName);
269
        /** @var Model|null $result */
270
        $result = $sourceEntityInstance->{$propertyName}()->first();
271
        if (null === $result) {
272
            return null;
273
        }
274
        if ($targetProperty->getResourceType()->getInstanceType()->getName() != get_class($result)) {
275
            return null;
276
        }
277
        return $result;
278
    }
279
280
    /**
281
     * Gets a related entity instance from an entity set identified by a key
282
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33).
283
     *
284
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
285
     * @param Model            $sourceEntityInstance the source entity instance
286
     * @param ResourceSet      $targetResourceSet    The entity set containing the entity to fetch
287
     * @param ResourceProperty $targetProperty       the metadata of the target property
288
     * @param KeyDescriptor    $keyDescriptor        The key identifying the entity to fetch
289
     *
290
     * @throws InvalidOperationException
291
     * @throws \Exception
292
     * @return Model|null                Returns entity instance if found else null
293
     */
294
    public function getResourceFromRelatedResourceSet(
295
        /* @noinspection PhpUnusedParameterInspection */
296
        ResourceSet $sourceResourceSet,
297
        Model $sourceEntityInstance,
298
        ResourceSet $targetResourceSet,
299
        ResourceProperty $targetProperty,
300
        KeyDescriptor $keyDescriptor
301
    ) {
302
        $propertyName = $targetProperty->getName();
303
        if (!method_exists($sourceEntityInstance, $propertyName)) {
304
            $msg = 'Relation method, ' . $propertyName . ', does not exist on supplied entity.';
305
            throw new InvalidArgumentException($msg);
306
        }
307
        // take key descriptor and turn it into where clause here, rather than in getResource call
308
        /** @var Relation $sourceEntityInstance */
309
        $sourceEntityInstance = $sourceEntityInstance->{$propertyName}();
310
        $this->processKeyDescriptor($sourceEntityInstance, $keyDescriptor);
311
        $result = $this->getResource(null, null, [], [], $sourceEntityInstance);
312
        return $result;
313
    }
314
315
    /**
316
     * @param Model|Relation|null $sourceEntityInstance
317
     * @param null|mixed          $checkInstance
318
     *
319
     * @throws ODataException
320
     */
321
    private function checkAuth($sourceEntityInstance, $checkInstance = null) : void
322
    {
323
        $check = array_reduce([$sourceEntityInstance, $checkInstance], function ($carry, $item) {
324
            if ($item instanceof Model || $item instanceof Relation) {
325
                return $item;
326
            }
327
        }, null);
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  array                     $rawLoad
338
     * @param  int                       $top
339
     * @param  int                       $skip
340
     * @param  callable|null             $isvalid
341
     * @throws InvalidOperationException
342
     * @return array
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 array       $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