Passed
Pull Request — master (#182)
by Alex
05:02
created

LaravelReadQuery::getResourceSet()   F

Complexity

Conditions 19
Paths 2052

Size

Total Lines 96
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Importance

Changes 12
Bugs 1 Features 0
Metric Value
eloc 56
c 12
b 1
f 0
dl 0
loc 96
rs 0.3499
cc 19
nc 2052
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 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
    /**
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
     * @return QueryResult
46
     * @throws InvalidArgumentException
47
     * @throws InvalidOperationException
48
     * @throws \ReflectionException
49
     * @throws ODataException
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
        $this->checkSourceInstance($sourceEntityInstance);
65
        if (null == $sourceEntityInstance) {
66
            $sourceEntityInstance = $this->getSourceEntityInstance($resourceSet);
67
        }
68
69
        /** @var MetadataTrait $model */
70
        $model = $sourceEntityInstance instanceof Model ? $sourceEntityInstance : $sourceEntityInstance->getRelated();
71
        $modelLoad = $model->getEagerLoad();
72
        $keyName = $model->getKeyName();
73
        $tableName = $model->getTable();
74
75
        if (null === $keyName) {
0 ignored issues
show
introduced by
The condition null === $keyName is always false.
Loading history...
76
            throw new InvalidOperationException('Key name not retrieved');
77
        }
78
        $rawLoad = array_values(array_unique(array_merge($rawLoad, $modelLoad)));
79
80
        $checkInstance = $sourceEntityInstance instanceof Model ? $sourceEntityInstance : null;
81
        $this->checkAuth($sourceEntityInstance, $checkInstance);
82
83
        $result          = new QueryResult();
84
        $result->results = null;
85
        $result->count   = null;
86
87
        if (null != $orderBy) {
88
            foreach ($orderBy->getOrderByInfo()->getOrderByPathSegments() as $order) {
89
                foreach ($order->getSubPathSegments() as $subOrder) {
90
                    $subName = $subOrder->getName();
91
                    $subName = $tableName.'.'.$subName;
92
                    $sourceEntityInstance = $sourceEntityInstance->orderBy(
93
                        $subName,
94
                        $order->isAscending() ? 'asc' : 'desc'
95
                    );
96
                }
97
            }
98
        }
99
100
        // throttle up for trench run
101
        if (null != $skipToken) {
102
            $sourceEntityInstance = $this->processSkipToken($skipToken, $sourceEntityInstance);
103
        }
104
105
        if (!isset($skip)) {
106
            $skip = 0;
107
        }
108
        if (!isset($top)) {
109
            $top = PHP_INT_MAX;
110
        }
111
112
        $nullFilter = true;
113
        $isvalid = null;
114
        if (isset($filterInfo)) {
115
            $method = 'return ' . $filterInfo->getExpressionAsString() . ';';
116
            $clln = '$' . $resourceSet->getResourceType()->getName();
117
            $isvalid = create_function($clln, $method);
0 ignored issues
show
Deprecated Code introduced by
The function create_function() has been deprecated: 7.2 ( Ignorable by Annotation )

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

117
            $isvalid = /** @scrutinizer ignore-deprecated */ create_function($clln, $method);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
118
            $nullFilter = false;
119
        }
120
121
        list($bulkSetCount, $resultSet, $resultCount, $skip) = $this->applyFiltering(
122
            $top,
123
            $skip,
124
            $sourceEntityInstance,
125
            $nullFilter,
126
            $rawLoad,
127
            $isvalid
128
        );
129
130
        if (isset($top)) {
131
            $resultSet = $resultSet->take($top);
132
        }
133
134
        $qVal = $queryType->getValue();
135
        if (QueryType::ENTITIES()->getValue() == $qVal || QueryType::ENTITIES_WITH_COUNT()->getValue() == $qVal) {
136
            $result->results = [];
137
            foreach ($resultSet as $res) {
138
                $result->results[] = $res;
139
            }
140
        }
141
        if (QueryType::COUNT()->getValue() == $qVal || QueryType::ENTITIES_WITH_COUNT()->getValue() == $qVal) {
142
            $result->count = $resultCount;
143
        }
144
        $hazMore = $bulkSetCount > $skip+count($resultSet);
145
        $result->hasMore = $hazMore;
146
        return $result;
147
    }
148
149
    /**
150
     * Get related resource set for a resource
151
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection
152
     * http://host/EntitySet?$expand=NavigationPropertyToCollection.
153
     *
154
     * @param QueryType          $queryType            Is this is a query for a count, entities, or entities-with-count
155
     * @param ResourceSet        $sourceResourceSet    The entity set containing the source entity
156
     * @param Model              $sourceEntityInstance The source entity instance
157
     * @param ResourceSet        $targetResourceSet    The resource set pointed to by the navigation property
158
     * @param ResourceProperty   $targetProperty       The navigation property to retrieve
159
     * @param FilterInfo|null    $filter               The $filter parameter of the OData query.  NULL if none specified
160
     * @param mixed|null         $orderBy              sorted order if we want to get the data in some specific order
161
     * @param int|null           $top                  number of records which need to be retrieved
162
     * @param int|null           $skip                 number of records which need to be skipped
163
     * @param SkipTokenInfo|null $skipToken            value indicating what records to skip
164
     *
165
     * @return QueryResult
166
     * @throws InvalidOperationException
167
     * @throws ODataException
168
     * @throws \ReflectionException
169
     */
170
    public function getRelatedResourceSet(
171
        QueryType $queryType,
172
        ResourceSet $sourceResourceSet,
173
        Model $sourceEntityInstance,
174
        /** @noinspection PhpUnusedParameterInspection */
175
        ResourceSet $targetResourceSet,
176
        ResourceProperty $targetProperty,
177
        FilterInfo $filter = null,
178
        $orderBy = null,
179
        $top = null,
180
        $skip = null,
181
        SkipTokenInfo $skipToken = null
182
    ) {
183
        $this->checkAuth($sourceEntityInstance);
184
185
        $propertyName = $targetProperty->getName();
186
        $results = $sourceEntityInstance->$propertyName();
187
188
        return $this->getResourceSet(
189
            $queryType,
190
            $sourceResourceSet,
191
            $filter,
192
            $orderBy,
193
            $top,
194
            $skip,
195
            $skipToken,
196
            null,
197
            $results
198
        );
199
    }
200
201
    /**
202
     * Gets an entity instance from an entity set identified by a key
203
     * IE: http://host/EntitySet(1L)
204
     * http://host/EntitySet(KeyA=2L,KeyB='someValue').
205
     *
206
     * @param ResourceSet        $resourceSet   The entity set containing the entity to fetch
207
     * @param KeyDescriptor|null $keyDescriptor The key identifying the entity to fetch
208
     * @param string[]|null      $eagerLoad     array of relations to eager load
209
     *
210
     * @return Model|null Returns entity instance if found else null
211
     * @throws \Exception;
212
     */
213
    public function getResourceFromResourceSet(
214
        ResourceSet $resourceSet,
215
        KeyDescriptor $keyDescriptor = null,
216
        array $eagerLoad = null
217
    ) {
218
        return $this->getResource($resourceSet, $keyDescriptor, [], $eagerLoad);
219
    }
220
221
222
    /**
223
     * Common method for getResourceFromRelatedResourceSet() and getResourceFromResourceSet().
224
     *
225
     * @param ResourceSet|null    $resourceSet
226
     * @param KeyDescriptor|null  $keyDescriptor
227
     * @param Model|Relation|null $sourceEntityInstance Starting point of query
228
     * @param array               $whereCondition
229
     * @param string[]|null       $eagerLoad            array of relations to eager load
230
     *
231
     * @return Model|null
232
     * @throws \Exception
233
     */
234
    public function getResource(
235
        ResourceSet $resourceSet = null,
236
        KeyDescriptor $keyDescriptor = null,
237
        array $whereCondition = [],
238
        array $eagerLoad = null,
239
        $sourceEntityInstance = null
240
    ) {
241
        if (null == $resourceSet && null == $sourceEntityInstance) {
242
            $msg = 'Must supply at least one of a resource set and source entity.';
243
            throw new \Exception($msg);
244
        }
245
246
        $this->checkSourceInstance($sourceEntityInstance);
247
248
        if (null == $sourceEntityInstance) {
249
            $sourceEntityInstance = $this->getSourceEntityInstance(/** @scrutinizer ignore-type */$resourceSet);
250
        }
251
252
        $this->checkAuth($sourceEntityInstance);
253
        $modelLoad = null;
254
        if ($sourceEntityInstance instanceof Model) {
255
            $modelLoad = $sourceEntityInstance->getEagerLoad();
256
        } elseif ($sourceEntityInstance instanceof Relation) {
257
            /** @var MetadataTrait $model */
258
            $model = $sourceEntityInstance->getRelated();
259
            $modelLoad = $model->getEagerLoad();
260
        }
261
        if (!(isset($modelLoad))) {
262
            throw new InvalidOperationException('');
263
        }
264
265
        $this->processKeyDescriptor($sourceEntityInstance, $keyDescriptor);
266
        foreach ($whereCondition as $fieldName => $fieldValue) {
267
            $sourceEntityInstance = $sourceEntityInstance->where($fieldName, $fieldValue);
268
        }
269
270
        $sourceEntityInstance = $sourceEntityInstance->get();
271
        $sourceCount = $sourceEntityInstance->count();
272
        if (0 == $sourceCount) {
273
            return null;
274
        }
275
        $result = $sourceEntityInstance->first();
276
277
        return $result;
278
    }
279
280
    /**
281
     * Get related resource for a resource
282
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
283
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity.
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 pointed to by the nav property
288
     * @param ResourceProperty $targetProperty       The navigation property to fetch
289
     *
290
     * @return Model|null The related resource if found else null
291
     * @throws ODataException
292
     * @throws InvalidOperationException
293
     * @throws \ReflectionException
294
     */
295
    public function getRelatedResourceReference(
296
        /** @noinspection PhpUnusedParameterInspection */
297
        ResourceSet $sourceResourceSet,
298
        Model $sourceEntityInstance,
299
        ResourceSet $targetResourceSet,
300
        ResourceProperty $targetProperty
301
    ) {
302
        $this->checkAuth($sourceEntityInstance);
303
304
        $propertyName = $targetProperty->getName();
305
        $propertyName = $this->getLaravelRelationName($propertyName);
306
        $result = $sourceEntityInstance->$propertyName()->first();
307
        if (null === $result) {
308
            return null;
309
        }
310
        if (!$result instanceof Model) {
311
            throw new InvalidOperationException('Model not retrieved from Eloquent relation');
312
        }
313
        if ($targetProperty->getResourceType()->getInstanceType()->getName() != get_class($result)) {
314
            return null;
315
        }
316
        return $result;
317
    }
318
319
    /**
320
     * Gets a related entity instance from an entity set identified by a key
321
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33).
322
     *
323
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
324
     * @param Model            $sourceEntityInstance the source entity instance
325
     * @param ResourceSet      $targetResourceSet    The entity set containing the entity to fetch
326
     * @param ResourceProperty $targetProperty       the metadata of the target property
327
     * @param KeyDescriptor    $keyDescriptor        The key identifying the entity to fetch
328
     *
329
     * @return Model|null Returns entity instance if found else null
330
     * @throws InvalidOperationException
331
     * @throws \Exception
332
     */
333
    public function getResourceFromRelatedResourceSet(
334
        /** @noinspection PhpUnusedParameterInspection */
335
        ResourceSet $sourceResourceSet,
336
        Model $sourceEntityInstance,
337
        ResourceSet $targetResourceSet,
338
        ResourceProperty $targetProperty,
339
        KeyDescriptor $keyDescriptor
340
    ) {
341
        $propertyName = $targetProperty->getName();
342
        if (!method_exists($sourceEntityInstance, $propertyName)) {
343
            $msg = 'Relation method, ' . $propertyName . ', does not exist on supplied entity.';
344
            throw new InvalidArgumentException($msg);
345
        }
346
        // take key descriptor and turn it into where clause here, rather than in getResource call
347
        $sourceEntityInstance = $sourceEntityInstance->$propertyName();
348
        $this->processKeyDescriptor($sourceEntityInstance, $keyDescriptor);
349
        $result = $this->getResource(null, null, [], [], $sourceEntityInstance);
350
        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...
351
            $msg = 'GetResourceFromRelatedResourceSet must return an entity or null';
352
            throw new InvalidOperationException($msg);
353
        }
354
        return $result;
355
    }
356
357
    /**
358
     * @param  ResourceSet $resourceSet
359
     * @return mixed
360
     * @throws \ReflectionException
361
     */
362
    protected function getSourceEntityInstance(ResourceSet $resourceSet)
363
    {
364
        $entityClassName = $resourceSet->getResourceType()->getInstanceType()->name;
365
        return App::make($entityClassName);
366
    }
367
368
    /**
369
     * @param Model|Relation|null $source
370
     */
371
    protected function checkSourceInstance($source)
372
    {
373
        if (!(null == $source || $source instanceof Model || $source instanceof Relation)) {
0 ignored issues
show
introduced by
$source is always a sub-type of Illuminate\Database\Eloquent\Relations\Relation.
Loading history...
374
            $msg = 'Source entity instance must be null, a model, or a relation.';
375
            throw new InvalidArgumentException($msg);
376
        }
377
    }
378
379
    /**
380
     * @param Model|Relation|null $sourceEntityInstance
381
     * @param null|mixed $checkInstance
382
     *
383
     * @throws ODataException
384
     */
385
    private function checkAuth($sourceEntityInstance, $checkInstance = null)
386
    {
387
        $check = $checkInstance instanceof Model ? $checkInstance
388
            : $checkInstance instanceof Relation ? $checkInstance
389
                : $sourceEntityInstance instanceof Model ? $sourceEntityInstance
390
                    : $sourceEntityInstance instanceof Relation ? $sourceEntityInstance
391
                        : null;
392
        if (!$this->getAuth()->canAuth(ActionVerb::READ(), $sourceEntityInstance, $check)) {
393
            throw new ODataException('Access denied', 403);
394
        }
395
    }
396
397
    /**
398
     * @param Model|Builder $sourceEntityInstance
399
     * @param  KeyDescriptor|null        $keyDescriptor
400
     * @throws InvalidOperationException
401
     */
402
    private function processKeyDescriptor(&$sourceEntityInstance, KeyDescriptor $keyDescriptor = null)
403
    {
404
        if ($keyDescriptor) {
405
            $table = ($sourceEntityInstance instanceof Model) ? $sourceEntityInstance->getTable().'.' : '';
406
            foreach ($keyDescriptor->getValidatedNamedValues() as $key => $value) {
407
                $trimValue = trim($value[0], '\'');
408
                $sourceEntityInstance = $sourceEntityInstance->where($table.$key, $trimValue);
409
            }
410
        }
411
    }
412
413
    /**
414
     * @param  string[]|null $eagerLoad
415
     * @return array
416
     * @throws InvalidOperationException
417
     */
418
    private function processEagerLoadList(array $eagerLoad = null)
419
    {
420
        $load = (null === $eagerLoad) ? [] : $eagerLoad;
421
        $rawLoad = [];
422
        foreach ($load as $line) {
423
            if (!is_string($line)) {
424
                throw new InvalidOperationException('Eager-load elements must be non-empty strings');
425
            }
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
    /**
452
     * @param SkipTokenInfo $skipToken
453
     * @param Model|Builder $sourceEntityInstance
454
     * @return mixed
455
     * @throws InvalidOperationException
456
     */
457
    protected function processSkipToken(SkipTokenInfo $skipToken, $sourceEntityInstance)
458
    {
459
        $parameters = [];
460
        $processed = [];
461
        $segments = $skipToken->getOrderByInfo()->getOrderByPathSegments();
462
        $values = $skipToken->getOrderByKeysInToken();
463
        $numValues = count($values);
464
        if ($numValues != count($segments)) {
465
            $msg = 'Expected '.count($segments).', got '.$numValues;
466
            throw new InvalidOperationException($msg);
467
        }
468
469
        for ($i = 0; $i < $numValues; $i++) {
470
            $relation = $segments[$i]->isAscending() ? '>' : '<';
471
            $name = $segments[$i]->getSubPathSegments()[0]->getName();
472
            $parameters[$name] = ['direction' => $relation, 'value' => trim($values[$i][0], '\'')];
473
        }
474
475
        foreach ($parameters as $name => $line) {
476
            $processed[$name] = ['direction' => $line['direction'], 'value' => $line['value']];
477
            $sourceEntityInstance = $sourceEntityInstance
478
                ->orWhere(
479
                    function (Builder $query) use ($processed) {
480
                        foreach ($processed as $key => $proc) {
481
                            $query->where($key, $proc['direction'], $proc['value']);
482
                        }
483
                    }
484
                );
485
            // now we've handled the later-in-order segment for this key, drop it back to equality in prep
486
            // for next key - same-in-order for processed keys and later-in-order for next
487
            $processed[$name]['direction'] = '=';
488
        }
489
        return $sourceEntityInstance;
490
    }
491
492
    /**
493
     * @param $top
494
     * @param $skip
495
     * @param Model|Builder $sourceEntityInstance
496
     * @param $nullFilter
497
     * @param $rawLoad
498
     * @param callable|null $isvalid
499
     * @return array
500
     * @throws InvalidOperationException
501
     */
502
    protected function applyFiltering(
503
        $top,
504
        $skip,
505
        $sourceEntityInstance,
506
        $nullFilter,
507
        $rawLoad,
508
        callable $isvalid = null
509
    ) {
510
        $bulkSetCount = $sourceEntityInstance->count();
511
        $bigSet = 20000 < $bulkSetCount;
512
513
        if ($nullFilter) {
514
            // default no-filter case, palm processing off to database engine - is a lot faster
515
            $resultSet = $sourceEntityInstance->skip($skip)->take($top)->with($rawLoad)->get();
516
            $resultCount = $bulkSetCount;
517
        } elseif ($bigSet) {
518
            if (!(isset($isvalid))) {
519
                $msg = 'Filter closure not set';
520
                throw new InvalidOperationException($msg);
521
            }
522
            $resultSet = new Collection([]);
523
            $rawCount = 0;
524
            $rawTop = null === $top ? $bulkSetCount : $top;
525
526
            // loop thru, chunk by chunk, to reduce chances of exhausting memory
527
            $sourceEntityInstance->chunk(
528
                5000,
529
                function (Collection $results) use ($isvalid, &$skip, &$resultSet, &$rawCount, $rawTop) {
530
                    // apply filter
531
                    $results = $results->filter($isvalid);
532
                    // need to iterate through full result set to find count of items matching filter,
533
                    // so we can't bail out early
534
                    $rawCount += $results->count();
535
                    // now bolt on filtrate to accumulating result set if we haven't accumulated enough bitz
536
                    if ($rawTop > $resultSet->count() + $skip) {
537
                        $resultSet = collect(array_merge($resultSet->all(), $results->all()));
538
                        $sliceAmount = min($skip, $resultSet->count());
539
                        $resultSet = $resultSet->slice($sliceAmount);
540
                        $skip -= $sliceAmount;
541
                    }
542
                }
543
            );
544
545
            // clean up residual to-be-skipped records
546
            $resultSet = $resultSet->slice($skip);
547
            $resultCount = $rawCount;
548
        } else {
549
            if ($sourceEntityInstance instanceof Model) {
550
                /** @var Builder $sourceEntityInstance */
551
                $sourceEntityInstance = $sourceEntityInstance->getQuery();
552
            }
553
            /** @var Collection $resultSet */
554
            $resultSet = $sourceEntityInstance->with($rawLoad)->get();
555
            $resultSet = $resultSet->filter($isvalid);
556
            $resultCount = $resultSet->count();
557
558
            if (isset($skip)) {
559
                $resultSet = $resultSet->slice($skip);
560
            }
561
        }
562
        return [$bulkSetCount, $resultSet, $resultCount, $skip];
563
    }
564
}
565