Passed
Pull Request — master (#202)
by Alex
06:30
created

LaravelReadQuery::checkAuth()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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