Passed
Pull Request — master (#214)
by Alex
06:15
created

LaravelReadQuery::getRelatedResourceSet()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 29
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 13
c 4
b 0
f 0
dl 0
loc 29
rs 9.8333
cc 1
nc 1
nop 10

How to fix   Many Parameters   

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\Enums\ActionVerb;
6
use AlgoWeb\PODataLaravel\Models\MetadataTrait;
7
use Illuminate\Database\Eloquent\Builder;
8
use Illuminate\Database\Eloquent\Collection;
9
use Illuminate\Database\Eloquent\Model;
10
use Illuminate\Database\Eloquent\Relations\Relation;
11
use Illuminate\Support\Facades\App;
12
use POData\Common\InvalidOperationException;
13
use POData\Common\ODataException;
14
use POData\Providers\Metadata\ResourceProperty;
15
use POData\Providers\Metadata\ResourceSet;
16
use POData\Providers\Query\QueryResult;
17
use POData\Providers\Query\QueryType;
18
use POData\UriProcessor\QueryProcessor\ExpressionParser\FilterInfo;
19
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
20
use POData\UriProcessor\QueryProcessor\SkipTokenParser\SkipTokenInfo;
21
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
22
use Symfony\Component\Process\Exception\InvalidArgumentException;
23
24
class LaravelReadQuery extends LaravelBaseQuery
25
{
26
    use LaravelReadQueryUtilityTrait;
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
     * @throws InvalidArgumentException
46
     * @throws InvalidOperationException
47
     * @throws \ReflectionException
48
     * @throws ODataException
49
     * @return QueryResult
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
        $sourceEntityInstance = $this->checkSourceInstance($sourceEntityInstance, $resourceSet);
65
66
        /** @var MetadataTrait $model */
67
        $model = $sourceEntityInstance instanceof Model ? $sourceEntityInstance : $sourceEntityInstance->getRelated();
68
        $modelLoad = $model->getEagerLoad();
69
        $tableName = $model->getTable();
70
        $rawLoad = array_values(array_unique(array_merge($rawLoad, $modelLoad)));
71
72
        $checkInstance = $sourceEntityInstance instanceof Model ? $sourceEntityInstance : null;
73
        $this->checkAuth($sourceEntityInstance, $checkInstance);
74
75
        $result          = new QueryResult();
76
        $result->results = null;
77
        $result->count   = null;
78
79
        $sourceEntityInstance = $this->buildOrderBy($sourceEntityInstance, $tableName, $orderBy);
80
81
        // throttle up for trench run
82
        if (null != $skipToken) {
83
            $sourceEntityInstance = $this->processSkipToken($skipToken, $sourceEntityInstance);
84
        }
85
86
        if (!isset($skip)) {
87
            $skip = 0;
88
        }
89
        if (!isset($top)) {
90
            $top = PHP_INT_MAX;
91
        }
92
93
        $nullFilter = !isset($filterInfo);
94
        $isvalid = null;
95
        if (isset($filterInfo)) {
96
            $method = 'return ' . $filterInfo->getExpressionAsString() . ';';
97
            $clln = $resourceSet->getResourceType()->getName();
98
            $isvalid = function ($inputD) use ($clln, $method) {
99
                $$clln = $inputD;
100
                return eval($method);
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
101
            };
102
        }
103
104
        list($bulkSetCount, $resultSet, $resultCount, $skip) = $this->applyFiltering(
105
            $sourceEntityInstance, $nullFilter, $rawLoad, $top, $skip, $isvalid
106
        );
107
108
        if (isset($top)) {
109
            $resultSet = $resultSet->take($top);
110
        }
111
112
        $this->packageResourceSetResults($queryType, $skip, $result, $resultSet, $resultCount, $bulkSetCount);
113
        return $result;
114
    }
115
116
    /**
117
     * Get related resource set for a resource
118
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection
119
     * http://host/EntitySet?$expand=NavigationPropertyToCollection.
120
     *
121
     * @param QueryType          $queryType            Is this is a query for a count, entities, or entities-with-count
122
     * @param ResourceSet        $sourceResourceSet    The entity set containing the source entity
123
     * @param Model              $sourceEntityInstance The source entity instance
124
     * @param ResourceSet        $targetResourceSet    The resource set pointed to by the navigation property
125
     * @param ResourceProperty   $targetProperty       The navigation property to retrieve
126
     * @param FilterInfo|null    $filter               The $filter parameter of the OData query.  NULL if none specified
127
     * @param mixed|null         $orderBy              sorted order if we want to get the data in some specific order
128
     * @param int|null           $top                  number of records which need to be retrieved
129
     * @param int|null           $skip                 number of records which need to be skipped
130
     * @param SkipTokenInfo|null $skipToken            value indicating what records to skip
131
     *
132
     * @throws InvalidOperationException
133
     * @throws ODataException
134
     * @throws \ReflectionException
135
     * @return QueryResult
136
     */
137
    public function getRelatedResourceSet(
138
        QueryType $queryType,
139
        ResourceSet $sourceResourceSet,
140
        Model $sourceEntityInstance,
141
        /* @noinspection PhpUnusedParameterInspection */
142
        ResourceSet $targetResourceSet,
143
        ResourceProperty $targetProperty,
144
        FilterInfo $filter = null,
145
        $orderBy = null,
146
        $top = null,
147
        $skip = null,
148
        SkipTokenInfo $skipToken = null
149
    ) {
150
        $this->checkAuth($sourceEntityInstance);
151
152
        $propertyName = $targetProperty->getName();
153
        /** @var Relation $results */
154
        $results = $sourceEntityInstance->$propertyName();
155
156
        return $this->getResourceSet(
157
            $queryType,
158
            $sourceResourceSet,
159
            $filter,
160
            $orderBy,
161
            $top,
162
            $skip,
163
            $skipToken,
164
            null,
165
            $results
166
        );
167
    }
168
169
    /**
170
     * Gets an entity instance from an entity set identified by a key
171
     * IE: http://host/EntitySet(1L)
172
     * http://host/EntitySet(KeyA=2L,KeyB='someValue').
173
     *
174
     * @param ResourceSet        $resourceSet   The entity set containing the entity to fetch
175
     * @param KeyDescriptor|null $keyDescriptor The key identifying the entity to fetch
176
     * @param string[]|null      $eagerLoad     array of relations to eager load
177
     *
178
     * @throws \Exception;
179
     * @return Model|null  Returns entity instance if found else null
180
     */
181
    public function getResourceFromResourceSet(
182
        ResourceSet $resourceSet,
183
        KeyDescriptor $keyDescriptor = null,
184
        array $eagerLoad = null
185
    ) {
186
        return $this->getResource($resourceSet, $keyDescriptor, [], $eagerLoad);
187
    }
188
189
190
    /**
191
     * Common method for getResourceFromRelatedResourceSet() and getResourceFromResourceSet().
192
     *
193
     * @param ResourceSet|null    $resourceSet
194
     * @param KeyDescriptor|null  $keyDescriptor
195
     * @param Model|Relation|null $sourceEntityInstance Starting point of query
196
     * @param array               $whereCondition
197
     * @param string[]|null       $eagerLoad            array of relations to eager load
198
     *
199
     * @throws \Exception
200
     * @return Model|null
201
     */
202
    public function getResource(
203
        ResourceSet $resourceSet = null,
204
        KeyDescriptor $keyDescriptor = null,
205
        array $whereCondition = [],
206
        array $eagerLoad = null,
207
        $sourceEntityInstance = null
208
    ) {
209
        if (null == $resourceSet && null == $sourceEntityInstance) {
210
            $msg = 'Must supply at least one of a resource set and source entity.';
211
            throw new \Exception($msg);
212
        }
213
214
        $sourceEntityInstance = $this->checkSourceInstance($sourceEntityInstance, $resourceSet);
215
216
        $this->checkAuth($sourceEntityInstance);
217
        $modelLoad = null;
218
        if ($sourceEntityInstance instanceof Model) {
219
            $modelLoad = $sourceEntityInstance->getEagerLoad();
220
        } elseif ($sourceEntityInstance instanceof Relation) {
221
            /** @var MetadataTrait $model */
222
            $model = $sourceEntityInstance->getRelated();
223
            $modelLoad = $model->getEagerLoad();
224
        }
225
        if (!(isset($modelLoad))) {
226
            throw new InvalidOperationException('');
227
        }
228
229
        $this->processKeyDescriptor($sourceEntityInstance, $keyDescriptor);
230
        foreach ($whereCondition as $fieldName => $fieldValue) {
231
            $sourceEntityInstance = $sourceEntityInstance->where($fieldName, $fieldValue);
232
        }
233
234
        $sourceEntityInstance = $sourceEntityInstance->get();
235
        $sourceCount = $sourceEntityInstance->count();
236
        if (0 == $sourceCount) {
237
            return null;
238
        }
239
        $result = $sourceEntityInstance->first();
240
241
        return $result;
242
    }
243
244
    /**
245
     * Get related resource for a resource
246
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
247
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity.
248
     *
249
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
250
     * @param Model            $sourceEntityInstance the source entity instance
251
     * @param ResourceSet      $targetResourceSet    The entity set containing the entity pointed to by the nav property
252
     * @param ResourceProperty $targetProperty       The navigation property to fetch
253
     *
254
     * @throws ODataException
255
     * @throws InvalidOperationException
256
     * @throws \ReflectionException
257
     * @return Model|null                The related resource if found else null
258
     */
259
    public function getRelatedResourceReference(
260
        /* @noinspection PhpUnusedParameterInspection */
261
        ResourceSet $sourceResourceSet,
262
        Model $sourceEntityInstance,
263
        ResourceSet $targetResourceSet,
264
        ResourceProperty $targetProperty
265
    ) {
266
        $this->checkAuth($sourceEntityInstance);
267
268
        $propertyName = $targetProperty->getName();
269
        $propertyName = $this->getLaravelRelationName($propertyName);
270
        /** @var Model|null $result */
271
        $result = $sourceEntityInstance->$propertyName()->first();
272
        if (null === $result) {
273
            return null;
274
        }
275
        if ($targetProperty->getResourceType()->getInstanceType()->getName() != get_class($result)) {
276
            return null;
277
        }
278
        return $result;
279
    }
280
281
    /**
282
     * Gets a related entity instance from an entity set identified by a key
283
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33).
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 to fetch
288
     * @param ResourceProperty $targetProperty       the metadata of the target property
289
     * @param KeyDescriptor    $keyDescriptor        The key identifying the entity to fetch
290
     *
291
     * @throws InvalidOperationException
292
     * @throws \Exception
293
     * @return Model|null                Returns entity instance if found else null
294
     */
295
    public function getResourceFromRelatedResourceSet(
296
        /* @noinspection PhpUnusedParameterInspection */
297
        ResourceSet $sourceResourceSet,
298
        Model $sourceEntityInstance,
299
        ResourceSet $targetResourceSet,
300
        ResourceProperty $targetProperty,
301
        KeyDescriptor $keyDescriptor
302
    ) {
303
        $propertyName = $targetProperty->getName();
304
        if (!method_exists($sourceEntityInstance, $propertyName)) {
305
            $msg = 'Relation method, ' . $propertyName . ', does not exist on supplied entity.';
306
            throw new InvalidArgumentException($msg);
307
        }
308
        // take key descriptor and turn it into where clause here, rather than in getResource call
309
        /** @var Relation $sourceEntityInstance */
310
        $sourceEntityInstance = $sourceEntityInstance->$propertyName();
311
        $this->processKeyDescriptor($sourceEntityInstance, $keyDescriptor);
312
        $result = $this->getResource(null, null, [], [], $sourceEntityInstance);
313
        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...
314
            $msg = 'GetResourceFromRelatedResourceSet must return an entity or null';
315
            throw new InvalidOperationException($msg);
316
        }
317
        return $result;
318
    }
319
320
    /**
321
     * @param Model|Relation|null $sourceEntityInstance
322
     * @param null|mixed          $checkInstance
323
     *
324
     * @throws ODataException
325
     */
326
    private function checkAuth($sourceEntityInstance, $checkInstance = null)
327
    {
328
        $check = array_reduce([$sourceEntityInstance, $checkInstance], function ($carry, $item) {
329
            if ($item instanceof Model || $item instanceof Relation) {
330
                return $item;
331
            }
332
        }, null);
333
        $sourceName = null !== $check ? get_class($check) : null;
334
        if (!$this->getAuth()->canAuth(ActionVerb::READ(), $sourceName, $check)) {
335
            throw new ODataException('Access denied', 403);
336
        }
337
    }
338
339
    /**
340
     * @param Model|Builder $sourceEntityInstance
341
     * @param bool $nullFilter
342
     * @param array $rawLoad
343
     * @param int $top
344
     * @param int $skip
345
     * @param  callable|null $isvalid
346
     * @return array
347
     * @throws InvalidOperationException
348
     */
349
    protected function applyFiltering(
350
        $sourceEntityInstance,
351
        bool $nullFilter,
352
        array $rawLoad = [],
353
        int $top = PHP_INT_MAX,
354
        int $skip = 0,
355
        callable $isvalid = null
356
    ) {
357
        $bulkSetCount = $sourceEntityInstance->count();
358
        $bigSet = 20000 < $bulkSetCount;
359
360
        if ($nullFilter) {
361
            // default no-filter case, palm processing off to database engine - is a lot faster
362
            $resultSet = $sourceEntityInstance->skip($skip)->take($top)->with($rawLoad)
363
                ->get();
364
            $resultCount = $bulkSetCount;
365
        } elseif ($bigSet) {
366
            if (!(isset($isvalid))) {
367
                $msg = 'Filter closure not set';
368
                throw new InvalidOperationException($msg);
369
            }
370
            $resultSet = new Collection([]);
371
            $rawCount = 0;
372
            $rawTop = min($top, $bulkSetCount);
373
374
            // loop thru, chunk by chunk, to reduce chances of exhausting memory
375
            $sourceEntityInstance->chunk(
376
                5000,
377
                function (Collection $results) use ($isvalid, &$skip, &$resultSet, &$rawCount, $rawTop) {
378
                    // apply filter
379
                    $results = $results->filter($isvalid);
380
                    // need to iterate through full result set to find count of items matching filter,
381
                    // so we can't bail out early
382
                    $rawCount += $results->count();
383
                    // now bolt on filtrate to accumulating result set if we haven't accumulated enough bitz
384
                    if ($rawTop > $resultSet->count() + $skip) {
385
                        $resultSet = collect(array_merge($resultSet->all(), $results->all()));
386
                        $sliceAmount = min($skip, $resultSet->count());
387
                        $resultSet = $resultSet->slice($sliceAmount);
388
                        $skip -= $sliceAmount;
389
                    }
390
                }
391
            );
392
393
            // clean up residual to-be-skipped records
394
            $resultSet = $resultSet->slice($skip);
395
            $resultCount = $rawCount;
396
        } else {
397
            /** @var Collection $resultSet */
398
            $resultSet = $sourceEntityInstance->with($rawLoad)->get();
399
            $resultSet = $resultSet->filter($isvalid);
400
            $resultCount = $resultSet->count();
401
402
            $resultSet = $resultSet->slice($skip);
403
        }
404
        return [$bulkSetCount, $resultSet, $resultCount, $skip];
405
    }
406
407
    /**
408
     * @param $sourceEntityInstance
409
     * @param string $tableName
410
     * @param InternalOrderByInfo|null $orderBy
411
     * @return mixed
412
     */
413
    protected function buildOrderBy($sourceEntityInstance, string $tableName, InternalOrderByInfo $orderBy = null)
414
    {
415
        if (null != $orderBy) {
416
            foreach ($orderBy->getOrderByInfo()->getOrderByPathSegments() as $order) {
417
                foreach ($order->getSubPathSegments() as $subOrder) {
418
                    $subName = $subOrder->getName();
419
                    $subName = $tableName . '.' . $subName;
420
                    $sourceEntityInstance = $sourceEntityInstance->orderBy(
421
                        $subName,
422
                        $order->isAscending() ? 'asc' : 'desc'
423
                    );
424
                }
425
            }
426
        }
427
        return $sourceEntityInstance;
428
    }
429
430
    /**
431
     * @param QueryType $queryType
432
     * @param int $skip
433
     * @param QueryResult $result
434
     * @param $resultSet
435
     * @param int $resultCount
436
     * @param int $bulkSetCount
437
     */
438
    protected function packageResourceSetResults(
439
        QueryType $queryType,
440
        int $skip,
441
        QueryResult $result,
442
        $resultSet,
443
        int $resultCount,
444
        int $bulkSetCount
445
    ) {
446
        $qVal = $queryType;
447
        if (QueryType::ENTITIES() == $qVal || QueryType::ENTITIES_WITH_COUNT() == $qVal) {
448
            $result->results = [];
449
            foreach ($resultSet as $res) {
450
                $result->results[] = $res;
451
            }
452
        }
453
        if (QueryType::COUNT() == $qVal || QueryType::ENTITIES_WITH_COUNT() == $qVal) {
454
            $result->count = $resultCount;
455
        }
456
        $hazMore = $bulkSetCount > $skip + count($resultSet);
457
        $result->hasMore = $hazMore;
458
    }
459
}
460