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

LaravelReadQuery::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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