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

getResourceFromRelatedResourceSet()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 21
rs 9.3142
cc 3
eloc 11
nc 2
nop 5
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