Completed
Push — master ( 817152...5c67c4 )
by Alex
15s queued 11s
created

LaravelReadQuery::getResourceFromResourceSet()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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