Passed
Pull Request — master (#214)
by Alex
06:15
created

LaravelReadQuery::applyFiltering()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 56
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 31
c 3
b 0
f 0
dl 0
loc 56
rs 9.1128
cc 5
nc 4
nop 6

How to fix   Long Method   

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:

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