Passed
Pull Request — master (#182)
by Alex
07:12
created

LaravelReadQuery::getAuth()   A

Complexity

Conditions 1
Paths 1

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 1
eloc 1
nc 1
nop 0
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 = [];
64
65
        $this->checkSourceInstance($sourceEntityInstance);
66
        if (null == $sourceEntityInstance) {
67
            $sourceEntityInstance = $this->getSourceEntityInstance($resourceSet);
68
        }
69
70
        $keyName = null;
71
        $tableName = null;
72
        if ($sourceEntityInstance instanceof Model) {
73
            $modelLoad = $sourceEntityInstance->getEagerLoad();
74
            $keyName = $sourceEntityInstance->getKeyName();
75
            $tableName = $sourceEntityInstance->getTable();
76
        } elseif ($sourceEntityInstance instanceof Relation) {
77
            /** @var MetadataTrait $model */
78
            $model = $sourceEntityInstance->getRelated();
79
            $modelLoad = $model->getEagerLoad();
80
            $keyName = $sourceEntityInstance->getRelated()->getKeyName();
81
            $tableName = $sourceEntityInstance->getRelated()->getTable();
82
        }
83
        if (null === $keyName) {
84
            throw new InvalidOperationException('Key name not retrieved');
85
        }
86
        $rawLoad = array_values(array_unique(array_merge($rawLoad, $modelLoad)));
87
88
        $checkInstance = $sourceEntityInstance instanceof Model ? $sourceEntityInstance : null;
89
        $this->checkAuth($sourceEntityInstance, $checkInstance);
90
91
        $result          = new QueryResult();
92
        $result->results = null;
93
        $result->count   = null;
94
95
        if (null != $orderBy) {
96
            foreach ($orderBy->getOrderByInfo()->getOrderByPathSegments() as $order) {
97
                foreach ($order->getSubPathSegments() as $subOrder) {
98
                    $subName = $subOrder->getName();
99
                    $subName = $tableName.'.'.$subName;
100
                    $sourceEntityInstance = $sourceEntityInstance->orderBy(
101
                        $subName,
102
                        $order->isAscending() ? 'asc' : 'desc'
103
                    );
104
                }
105
            }
106
        }
107
108
        // throttle up for trench run
109
        if (null != $skipToken) {
110
            $sourceEntityInstance = $this->processSkipToken($skipToken, $sourceEntityInstance);
111
        }
112
113
        if (!isset($skip)) {
114
            $skip = 0;
115
        }
116
        if (!isset($top)) {
117
            $top = PHP_INT_MAX;
118
        }
119
120
        $nullFilter = true;
121
        $isvalid = null;
122
        if (isset($filterInfo)) {
123
            $method = 'return ' . $filterInfo->getExpressionAsString() . ';';
124
            $clln = '$' . $resourceSet->getResourceType()->getName();
125
            $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

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