Passed
Push — master ( ba77f6...229fac )
by Alex
07:41
created

LaravelReadQuery::getResourceSet()   F

Complexity

Conditions 24
Paths 3074

Size

Total Lines 108
Code Lines 68

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 108
rs 2
c 0
b 0
f 0
cc 24
eloc 68
nc 3074
nop 9

How to fix   Long Method    Complexity    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
namespace AlgoWeb\PODataLaravel\Query;
4
5
use AlgoWeb\PODataLaravel\Auth\NullAuthProvider;
6
use AlgoWeb\PODataLaravel\Enums\ActionVerb;
7
use AlgoWeb\PODataLaravel\Interfaces\AuthInterface;
8
use Illuminate\Database\Eloquent\Model;
9
use Illuminate\Database\Eloquent\Relations\Relation;
10
use Illuminate\Support\Facades\App;
11
use POData\Common\InvalidOperationException;
12
use POData\Common\ODataException;
13
use POData\Providers\Metadata\ResourceProperty;
14
use POData\Providers\Metadata\ResourceSet;
15
use POData\Providers\Query\QueryResult;
16
use POData\Providers\Query\QueryType;
17
use POData\UriProcessor\QueryProcessor\ExpressionParser\FilterInfo;
18
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
19
use POData\UriProcessor\QueryProcessor\SkipTokenParser\SkipTokenInfo;
20
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
21
use Symfony\Component\Process\Exception\InvalidArgumentException;
22
23
class LaravelReadQuery
24
{
25
    const PK = 'PrimaryKey';
26
27
    protected $auth;
28
29
    public function __construct(AuthInterface $auth = null)
30
    {
31
        $this->auth = isset($auth) ? $auth : new NullAuthProvider();
32
    }
33
34
    /**
35
     * Gets collection of entities belongs to an entity set
36
     * IE: http://host/EntitySet
37
     *  http://host/EntitySet?$skip=10&$top=5&filter=Prop gt Value.
38
     *
39
     * @param QueryType                $queryType            Is this is a query for a count, entities,
40
     *                                                       or entities-with-count?
41
     * @param ResourceSet              $resourceSet          The entity set containing the entities to fetch
42
     * @param FilterInfo|null          $filterInfo           The $filter parameter of the OData query.  NULL if absent
43
     * @param null|InternalOrderByInfo $orderBy              sorted order if we want to get the data in some
44
     *                                                       specific order
45
     * @param int|null                 $top                  number of records which need to be retrieved
46
     * @param int|null                 $skip                 number of records which need to be skipped
47
     * @param SkipTokenInfo|null       $skipToken            value indicating what records to skip
48
     * @param string[]|null            $eagerLoad            array of relations to eager load
49
     * @param Model|Relation|null      $sourceEntityInstance Starting point of query
50
     *
51
     * @return QueryResult
52
     */
53
    public function getResourceSet(
54
        QueryType $queryType,
55
        ResourceSet $resourceSet,
56
        $filterInfo = null,
57
        $orderBy = null,
58
        $top = null,
59
        $skip = null,
60
        $skipToken = null,
61
        array $eagerLoad = null,
62
        $sourceEntityInstance = null
63
    ) {
64
        if (null != $filterInfo && !($filterInfo instanceof FilterInfo)) {
65
            $msg = 'Filter info must be either null or instance of FilterInfo.';
66
            throw new InvalidArgumentException($msg);
67
        }
68
        if (null != $skipToken && !($skipToken instanceof SkipTokenInfo)) {
69
            $msg = 'Skip token must be either null or instance of SkipTokenInfo.';
70
            throw new InvalidArgumentException($msg);
71
        }
72
        $rawLoad = $this->processEagerLoadList($eagerLoad);
73
        $modelLoad = [];
74
75
        $this->checkSourceInstance($sourceEntityInstance);
76
        if (null == $sourceEntityInstance) {
77
            $sourceEntityInstance = $this->getSourceEntityInstance($resourceSet);
78
        }
79
80
        $keyName = null;
81
        $tableName = null;
82
        if ($sourceEntityInstance instanceof Model) {
83
            $modelLoad = $sourceEntityInstance->getEagerLoad();
84
            $keyName = $sourceEntityInstance->getKeyName();
85
            $tableName = $sourceEntityInstance->getTable();
86
        } elseif ($sourceEntityInstance instanceof Relation) {
87
            $modelLoad = $sourceEntityInstance->getRelated()->getEagerLoad();
88
            $keyName = $sourceEntityInstance->getRelated()->getKeyName();
89
            $tableName = $sourceEntityInstance->getRelated()->getTable();
90
        }
91
        assert(isset($keyName));
0 ignored issues
show
Bug introduced by
The call to assert() has too few arguments starting with description. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

91
        /** @scrutinizer ignore-call */ 
92
        assert(isset($keyName));

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
92
        $rawLoad = array_values(array_unique(array_merge($rawLoad, $modelLoad)));
0 ignored issues
show
Bug introduced by
It seems like $modelLoad can also be of type Illuminate\Database\Eloquent\Builder; however, parameter $array2 of array_merge() does only seem to accept null|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

92
        $rawLoad = array_values(array_unique(array_merge($rawLoad, /** @scrutinizer ignore-type */ $modelLoad)));
Loading history...
93
94
        $checkInstance = $sourceEntityInstance instanceof Model ? $sourceEntityInstance : null;
95
        $this->checkAuth($sourceEntityInstance, $checkInstance);
96
97
        $result          = new QueryResult();
98
        $result->results = null;
99
        $result->count   = null;
100
101
        if (null != $orderBy) {
102
            foreach ($orderBy->getOrderByInfo()->getOrderByPathSegments() as $order) {
103
                foreach ($order->getSubPathSegments() as $subOrder) {
104
                    $subName = $subOrder->getName();
105
                    $subName = (self::PK == $subName) ? $keyName : $subName;
106
                    $subName = $tableName.'.'.$subName;
107
                    $sourceEntityInstance = $sourceEntityInstance->orderBy(
108
                        $subName,
109
                        $order->isAscending() ? 'asc' : 'desc'
110
                    );
111
                }
112
            }
113
        }
114
115
        // throttle up for trench run
116
        if (null != $skipToken) {
117
            $sourceEntityInstance = $this->processSkipToken($skipToken, $sourceEntityInstance, $keyName);
118
        }
119
120
        if (!isset($skip)) {
121
            $skip = 0;
122
        }
123
        if (!isset($top)) {
124
            $top = PHP_INT_MAX;
125
        }
126
127
        $nullFilter = true;
128
        $isvalid = null;
129
        if (isset($filterInfo)) {
130
            $method = 'return ' . $filterInfo->getExpressionAsString() . ';';
131
            $clln = '$' . $resourceSet->getResourceType()->getName();
132
            $isvalid = create_function($clln, $method);
0 ignored issues
show
Security Best Practice introduced by
The use of create_function is highly discouraged, better use a closure.

create_function can pose a great security vulnerability as it is similar to eval, and could be used for arbitrary code execution. We highly recommend to use a closure instead.

// Instead of
$function = create_function('$a, $b', 'return $a + $b');

// Better use
$function = function($a, $b) { return $a + $b; }
Loading history...
133
            $nullFilter = false;
134
        }
135
136
        list($bulkSetCount, $resultSet, $resultCount, $skip) = $this->applyFiltering(
137
            $top,
138
            $skip,
139
            $sourceEntityInstance,
140
            $nullFilter,
141
            $rawLoad,
142
            $isvalid
143
        );
144
145
        if (isset($top)) {
146
            $resultSet = $resultSet->take($top);
147
        }
148
149
        if (QueryType::ENTITIES() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) {
150
            $result->results = [];
151
            foreach ($resultSet as $res) {
152
                $result->results[] = $res;
153
            }
154
        }
155
        if (QueryType::COUNT() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) {
156
            $result->count = $resultCount;
157
        }
158
        $hazMore = $bulkSetCount > $skip+count($resultSet);
159
        $result->hasMore = $hazMore;
160
        return $result;
161
    }
162
163
    /**
164
     * Get related resource set for a resource
165
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection
166
     * http://host/EntitySet?$expand=NavigationPropertyToCollection.
167
     *
168
     * @param QueryType          $queryType            Is this is a query for a count, entities, or entities-with-count
169
     * @param ResourceSet        $sourceResourceSet    The entity set containing the source entity
170
     * @param Model              $sourceEntityInstance The source entity instance
171
     * @param ResourceSet        $targetResourceSet    The resource set pointed to by the navigation property
172
     * @param ResourceProperty   $targetProperty       The navigation property to retrieve
173
     * @param FilterInfo|null    $filter               The $filter parameter of the OData query.  NULL if none specified
174
     * @param mixed|null         $orderBy              sorted order if we want to get the data in some specific order
175
     * @param int|null           $top                  number of records which need to be retrieved
176
     * @param int|null           $skip                 number of records which need to be skipped
177
     * @param SkipTokenInfo|null $skipToken            value indicating what records to skip
178
     *
179
     * @return QueryResult
180
     */
181
    public function getRelatedResourceSet(
182
        QueryType $queryType,
183
        ResourceSet $sourceResourceSet,
184
        Model $sourceEntityInstance,
185
        ResourceSet $targetResourceSet,
186
        ResourceProperty $targetProperty,
187
        FilterInfo $filter = null,
188
        $orderBy = null,
189
        $top = null,
190
        $skip = null,
191
        SkipTokenInfo $skipToken = null
192
    ) {
193
        $this->checkAuth($sourceEntityInstance);
194
195
        $propertyName = $targetProperty->getName();
196
        $results = $sourceEntityInstance->$propertyName();
197
198
        return $this->getResourceSet(
199
            $queryType,
200
            $sourceResourceSet,
201
            $filter,
202
            $orderBy,
203
            $top,
204
            $skip,
205
            $skipToken,
206
            null,
207
            $results
208
        );
209
    }
210
211
    /**
212
     * Gets an entity instance from an entity set identified by a key
213
     * IE: http://host/EntitySet(1L)
214
     * http://host/EntitySet(KeyA=2L,KeyB='someValue').
215
     *
216
     * @param ResourceSet        $resourceSet   The entity set containing the entity to fetch
217
     * @param KeyDescriptor|null $keyDescriptor The key identifying the entity to fetch
218
     * @param string[]|null      $eagerLoad     array of relations to eager load
219
     *
220
     * @return Model|null Returns entity instance if found else null
221
     */
222
    public function getResourceFromResourceSet(
223
        ResourceSet $resourceSet,
224
        KeyDescriptor $keyDescriptor = null,
225
        array $eagerLoad = null
226
    ) {
227
        return $this->getResource($resourceSet, $keyDescriptor, [], $eagerLoad);
228
    }
229
230
231
    /**
232
     * Common method for getResourceFromRelatedResourceSet() and getResourceFromResourceSet().
233
     *
234
     * @param ResourceSet|null    $resourceSet
235
     * @param KeyDescriptor|null  $keyDescriptor
236
     * @param Model|Relation|null $sourceEntityInstance Starting point of query
237
     *                                                  $param array               $whereCondition
238
     * @param string[]|null       $eagerLoad            array of relations to eager load
239
     *
240
     * @return Model|null
241
     */
242
    public function getResource(
243
        ResourceSet $resourceSet = null,
244
        KeyDescriptor $keyDescriptor = null,
245
        array $whereCondition = [],
246
        array $eagerLoad = null,
247
        $sourceEntityInstance = null
248
    ) {
249
        if (null == $resourceSet && null == $sourceEntityInstance) {
250
            $msg = 'Must supply at least one of a resource set and source entity.';
251
            throw new \Exception($msg);
252
        }
253
254
        $this->checkSourceInstance($sourceEntityInstance);
255
        $rawLoad = $this->processEagerLoadList($eagerLoad);
256
257
        if (null == $sourceEntityInstance) {
258
            assert(null != $resourceSet);
0 ignored issues
show
Bug introduced by
The call to assert() has too few arguments starting with description. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

258
            /** @scrutinizer ignore-call */ 
259
            assert(null != $resourceSet);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
259
            $sourceEntityInstance = $this->getSourceEntityInstance($resourceSet);
0 ignored issues
show
Bug introduced by
It seems like $resourceSet can also be of type null; however, parameter $resourceSet of AlgoWeb\PODataLaravel\Qu...tSourceEntityInstance() does only seem to accept POData\Providers\Metadata\ResourceSet, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

259
            $sourceEntityInstance = $this->getSourceEntityInstance(/** @scrutinizer ignore-type */ $resourceSet);
Loading history...
260
        }
261
262
        $this->checkAuth($sourceEntityInstance);
263
        if ($sourceEntityInstance instanceof Model) {
264
            $modelLoad = $sourceEntityInstance->getEagerLoad();
265
        } elseif ($sourceEntityInstance instanceof Relation) {
266
            $modelLoad = $sourceEntityInstance->getRelated()->getEagerLoad();
267
        }
268
        assert(isset($modelLoad));
269
270
        $this->processKeyDescriptor($sourceEntityInstance, $keyDescriptor);
271
        foreach ($whereCondition as $fieldName => $fieldValue) {
272
            $sourceEntityInstance = $sourceEntityInstance->where($fieldName, $fieldValue);
273
        }
274
275
        $rawLoad = array_values(array_unique(array_merge($rawLoad, $modelLoad)));
0 ignored issues
show
Bug introduced by
It seems like $modelLoad can also be of type Illuminate\Database\Eloquent\Builder; however, parameter $array2 of array_merge() does only seem to accept null|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

275
        $rawLoad = array_values(array_unique(array_merge($rawLoad, /** @scrutinizer ignore-type */ $modelLoad)));
Loading history...
Unused Code introduced by
The assignment to $rawLoad is dead and can be removed.
Loading history...
Comprehensibility Best Practice introduced by
The variable $modelLoad does not seem to be defined for all execution paths leading up to this point.
Loading history...
276
        $sourceEntityInstance = $sourceEntityInstance->get();
277
        $sourceCount = $sourceEntityInstance->count();
278
        if (0 == $sourceCount) {
279
            return null;
280
        }
281
        $result = $sourceEntityInstance->first();
282
        $result->PrimaryKey = $result->getKey();
283
284
        return $result;
285
    }
286
287
    /**
288
     * Get related resource for a resource
289
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
290
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity.
291
     *
292
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
293
     * @param Model            $sourceEntityInstance the source entity instance
294
     * @param ResourceSet      $targetResourceSet    The entity set containing the entity pointed to by the nav property
295
     * @param ResourceProperty $targetProperty       The navigation property to fetch
296
     *
297
     * @return object|null The related resource if found else null
298
     */
299
    public function getRelatedResourceReference(
300
        ResourceSet $sourceResourceSet,
301
        Model $sourceEntityInstance,
302
        ResourceSet $targetResourceSet,
303
        ResourceProperty $targetProperty
304
    ) {
305
        $this->checkAuth($sourceEntityInstance);
306
307
        $propertyName = $targetProperty->getName();
308
        $propertyName = $this->getLaravelRelationName($propertyName);
309
        $result = $sourceEntityInstance->$propertyName;
310
        if (null === $result) {
311
            return null;
312
        }
313
        assert($result instanceof Model, get_class($result));
314
        if ($targetProperty->getResourceType()->getInstanceType()->getName() != get_class($result)) {
315
            return null;
316
        }
317
        return $result;
318
    }
319
320
    /**
321
     * Gets a related entity instance from an entity set identified by a key
322
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33).
323
     *
324
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
325
     * @param Model            $sourceEntityInstance the source entity instance
326
     * @param ResourceSet      $targetResourceSet    The entity set containing the entity to fetch
327
     * @param ResourceProperty $targetProperty       the metadata of the target property
328
     * @param KeyDescriptor    $keyDescriptor        The key identifying the entity to fetch
329
     *
330
     * @return Model|null Returns entity instance if found else null
331
     */
332
    public function getResourceFromRelatedResourceSet(
333
        ResourceSet $sourceResourceSet,
334
        Model $sourceEntityInstance,
335
        ResourceSet $targetResourceSet,
336
        ResourceProperty $targetProperty,
337
        KeyDescriptor $keyDescriptor
338
    ) {
339
        $propertyName = $targetProperty->getName();
340
        if (!method_exists($sourceEntityInstance, $propertyName)) {
341
            $msg = 'Relation method, ' . $propertyName . ', does not exist on supplied entity.';
342
            throw new InvalidArgumentException($msg);
343
        }
344
        // take key descriptor and turn it into where clause here, rather than in getResource call
345
        $sourceEntityInstance = $sourceEntityInstance->$propertyName();
346
        $this->processKeyDescriptor($sourceEntityInstance, $keyDescriptor);
347
        $result = $this->getResource(null, null, [], [], $sourceEntityInstance);
348
        assert(
349
            $result instanceof Model || null == $result,
350
            'GetResourceFromRelatedResourceSet must return an entity or null'
351
        );
352
        return $result;
353
    }
354
355
356
    /**
357
     * @param  ResourceSet $resourceSet
358
     * @return mixed
359
     */
360
    protected function getSourceEntityInstance(ResourceSet $resourceSet)
361
    {
362
        $entityClassName = $resourceSet->getResourceType()->getInstanceType()->name;
363
        return App::make($entityClassName);
364
    }
365
366
    /**
367
     * @param Model|Relation|null $source
368
     */
369
    protected function checkSourceInstance($source)
370
    {
371
        if (!(null == $source || $source instanceof Model || $source instanceof Relation)) {
372
            $msg = 'Source entity instance must be null, a model, or a relation.';
373
            throw new InvalidArgumentException($msg);
374
        }
375
    }
376
377
    protected function getAuth()
378
    {
379
        return $this->auth;
380
    }
381
382
    /**
383
     * @param $sourceEntityInstance
384
     * @param null|mixed $checkInstance
385
     *
386
     * @throws ODataException
387
     */
388
    private function checkAuth($sourceEntityInstance, $checkInstance = null)
389
    {
390
        $check = $checkInstance instanceof Model ? $checkInstance
391
            : $checkInstance instanceof Relation ? $checkInstance
392
                : $sourceEntityInstance instanceof Model ? $sourceEntityInstance
393
                    : $sourceEntityInstance instanceof Relation ? $sourceEntityInstance
394
                        : null;
395
        if (!$this->getAuth()->canAuth(ActionVerb::READ(), get_class($sourceEntityInstance), $check)) {
0 ignored issues
show
Bug introduced by
The method READ() does not exist on AlgoWeb\PODataLaravel\Enums\ActionVerb. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

395
        if (!$this->getAuth()->canAuth(ActionVerb::/** @scrutinizer ignore-call */ READ(), get_class($sourceEntityInstance), $check)) {
Loading history...
396
            throw new ODataException('Access denied', 403);
397
        }
398
    }
399
400
    /**
401
     * @param $sourceEntityInstance
402
     * @param  KeyDescriptor|null        $keyDescriptor
403
     * @throws InvalidOperationException
404
     */
405
    private function processKeyDescriptor(&$sourceEntityInstance, KeyDescriptor $keyDescriptor = null)
406
    {
407
        if ($keyDescriptor) {
408
            foreach ($keyDescriptor->getValidatedNamedValues() as $key => $value) {
409
                $key = (self::PK == $key) ? $sourceEntityInstance->getKeyName() : $key;
410
                $trimValue = trim($value[0], '\'');
411
                $sourceEntityInstance = $sourceEntityInstance->where($key, $trimValue);
412
            }
413
        }
414
    }
415
416
    /**
417
     * @param  string[]|null $eagerLoad
418
     * @return array
419
     */
420
    private function processEagerLoadList(array $eagerLoad = null)
421
    {
422
        $load = (null === $eagerLoad) ? [] : $eagerLoad;
423
        $rawLoad = [];
424
        foreach ($load as $line) {
425
            assert(is_string($line), 'Eager-load elements must be non-empty strings');
426
            $lineParts = explode('/', $line);
427
            $numberOfParts = count($lineParts);
428
            for ($i = 0; $i<$numberOfParts; $i++) {
429
                $lineParts[$i] = $this->getLaravelRelationName($lineParts[$i]);
430
            }
431
            $remixLine = implode('.', $lineParts);
432
            $rawLoad[] = $remixLine;
433
        }
434
        return $rawLoad;
435
    }
436
437
    /**
438
     * @param  string $odataProperty
439
     * @return string
440
     */
441
    private function getLaravelRelationName($odataProperty)
442
    {
443
        $laravelProperty = $odataProperty;
444
        $pos = strrpos($laravelProperty, '_');
445
        if ($pos !== false) {
446
            $laravelProperty = substr($laravelProperty, 0, $pos);
447
        }
448
        return $laravelProperty;
449
    }
450
451
    protected function processSkipToken($skipToken, $sourceEntityInstance, $keyName)
452
    {
453
        $parameters = [];
454
        $processed = [];
455
        $segments = $skipToken->getOrderByInfo()->getOrderByPathSegments();
456
        $values = $skipToken->getOrderByKeysInToken();
457
        $numValues = count($values);
458
        assert($numValues == count($segments));
0 ignored issues
show
Bug introduced by
The call to assert() has too few arguments starting with description. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

458
        /** @scrutinizer ignore-call */ 
459
        assert($numValues == count($segments));

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
459
460
        for ($i = 0; $i < $numValues; $i++) {
461
            $relation = $segments[$i]->isAscending() ? '>' : '<';
462
            $name = $segments[$i]->getSubPathSegments()[0]->getName();
463
            $name = (self::PK == $name) ? $keyName : $name;
464
            $parameters[$name] = ['direction' => $relation, 'value' => trim($values[$i][0], '\'')];
465
        }
466
467
        foreach ($parameters as $name => $line) {
468
            $processed[$name] = ['direction' => $line['direction'], 'value' => $line['value']];
469
            $sourceEntityInstance = $sourceEntityInstance
470
                ->orWhere(
471
                    function ($query) use ($processed) {
472
                        foreach ($processed as $key => $proc) {
473
                            $query->where($key, $proc['direction'], $proc['value']);
474
                        }
475
                    }
476
                );
477
            // now we've handled the later-in-order segment for this key, drop it back to equality in prep
478
            // for next key - same-in-order for processed keys and later-in-order for next
479
            $processed[$name]['direction'] = '=';
480
        }
481
        return $sourceEntityInstance;
482
    }
483
484
    protected function applyFiltering($top, $skip, $sourceEntityInstance, $nullFilter, $rawLoad, $isvalid)
485
    {
486
        $bulkSetCount = $sourceEntityInstance->count();
487
        $bigSet = 20000 < $bulkSetCount;
488
489
        if ($nullFilter) {
490
            // default no-filter case, palm processing off to database engine - is a lot faster
491
            $resultSet = $sourceEntityInstance->skip($skip)->take($top)->with($rawLoad)->get();
492
            $resultCount = $bulkSetCount;
493
            return array($bulkSetCount, $resultSet, $resultCount, $skip);
494
        } elseif ($bigSet) {
495
            assert(isset($isvalid), 'Filter closure not set');
496
            $resultSet = collect([]);
497
            $rawCount = 0;
498
            $rawTop = null === $top ? $bulkSetCount : $top;
499
500
            // loop thru, chunk by chunk, to reduce chances of exhausting memory
501
            $sourceEntityInstance->chunk(
502
                5000,
503
                function ($results) use ($isvalid, &$skip, &$resultSet, &$rawCount, $rawTop) {
504
                    // apply filter
505
                    $results = $results->filter($isvalid);
506
                    // need to iterate through full result set to find count of items matching filter,
507
                    // so we can't bail out early
508
                    $rawCount += $results->count();
509
                    // now bolt on filtrate to accumulating result set if we haven't accumulated enough bitz
510
                    if ($rawTop > $resultSet->count() + $skip) {
511
                        $resultSet = collect(array_merge($resultSet->all(), $results->all()));
512
                        $sliceAmount = min($skip, $resultSet->count());
513
                        $resultSet = $resultSet->slice($sliceAmount);
514
                        $skip -= $sliceAmount;
515
                    }
516
                }
517
            );
518
519
            // clean up residual to-be-skipped records
520
            $resultSet = $resultSet->slice($skip);
521
            $resultCount = $rawCount;
522
            return array($bulkSetCount, $resultSet, $resultCount, $skip);
523
        } else {
524
            if ($sourceEntityInstance instanceof Model) {
525
                $sourceEntityInstance = $sourceEntityInstance->getQuery();
526
            }
527
            $resultSet = $sourceEntityInstance->with($rawLoad)->get();
528
            $resultSet = $resultSet->filter($isvalid);
529
            $resultCount = $resultSet->count();
530
531
            if (isset($skip)) {
532
                $resultSet = $resultSet->slice($skip);
533
                return array($bulkSetCount, $resultSet, $resultCount, $skip);
534
            }
535
            return array($bulkSetCount, $resultSet, $resultCount, $skip);
536
        }
537
    }
538
}
539