Test Failed
Pull Request — master (#82)
by Alex
16:38
created

getResourceFromRelatedResourceSet()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 25
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 25
rs 8.5806
cc 4
eloc 19
nc 3
nop 5
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 Illuminate\Database\Eloquent\Model;
9
use Illuminate\Database\Eloquent\Relations\Relation;
10
use Illuminate\Support\Facades\App;
11
use POData\Common\ODataException;
12
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
13
use POData\UriProcessor\QueryProcessor\SkipTokenParser\SkipTokenInfo;
14
use Symfony\Component\Process\Exception\InvalidArgumentException;
15
use POData\Providers\Metadata\ResourceProperty;
16
use POData\Providers\Metadata\ResourceSet;
17
use POData\Providers\Query\QueryResult;
18
use POData\Providers\Query\QueryType;
19
use POData\UriProcessor\QueryProcessor\ExpressionParser\FilterInfo;
20
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
21
22
class LaravelReadQuery
23
{
24
    protected $auth;
25
26
    public function __construct(AuthInterface $auth = null)
27
    {
28
        $this->auth = isset($auth) ? $auth : new NullAuthProvider();
29
    }
30
31
    /**
32
     * Gets collection of entities belongs to an entity set
33
     * IE: http://host/EntitySet
34
     *  http://host/EntitySet?$skip=10&$top=5&filter=Prop gt Value
35
     *
36
     * @param QueryType                 $queryType   indicates if this is a query for a count, entities, or entities with a count
37
     * @param ResourceSet               $resourceSet The entity set containing the entities to fetch
38
     * @param FilterInfo                $filterInfo  represents the $filter parameter of the OData query.  NULL if no $filter specified
39
     * @param null|InternalOrderByInfo  $orderBy     sorted order if we want to get the data in some specific order
40
     * @param int                       $top         number of records which need to be retrieved
41
     * @param int                       $skip        number of records which need to be skipped
42
     * @param SkipTokenInfo|null        $skipToken   value indicating what records to skip
43
     * @param Model|Relation|null $sourceEntityInstance Starting point of query
44
     *
45
     * @return QueryResult
46
     */
47
    public function getResourceSet(
48
        QueryType $queryType,
49
        ResourceSet $resourceSet,
50
        $filterInfo = null,
51
        $orderBy = null,
52
        $top = null,
53
        $skip = null,
54
        $skipToken = null,
55
        $sourceEntityInstance = null
56
    ) {
57
        if (null != $filterInfo && !($filterInfo instanceof FilterInfo)) {
58
            throw new InvalidArgumentException('Filter info must be either null or instance of FilterInfo.');
59
        }
60
        if (null != $skipToken && !($skipToken instanceof SkipTokenInfo)) {
61
            throw new InvalidArgumentException('Skip token must be either null or instance of SkipTokenInfo.');
62
        }
63
64
        $eagerLoad = [];
65
66
        $this->checkSourceInstance($sourceEntityInstance);
67
        if (null == $sourceEntityInstance) {
68
            $sourceEntityInstance = $this->getSourceEntityInstance($resourceSet);
69
        }
70
71
        $keyName = null;
72
        if ($sourceEntityInstance instanceof Model) {
73
            $eagerLoad = $sourceEntityInstance->getEagerLoad();
74
            $keyName = $sourceEntityInstance->getKeyName();
75
        } elseif ($sourceEntityInstance instanceof Relation) {
76
            $eagerLoad = $sourceEntityInstance->getRelated()->getEagerLoad();
77
            $keyName = $sourceEntityInstance->getRelated()->getKeyName();
78
        }
79
        assert(isset($keyName));
80
81
        $checkInstance = $sourceEntityInstance instanceof Model ? $sourceEntityInstance : null;
82
        $this->checkAuth($sourceEntityInstance, $checkInstance);
83
84
        $result          = new QueryResult();
85
        $result->results = null;
86
        $result->count   = null;
87
88
        if (null != $orderBy) {
89
            foreach ($orderBy->getOrderByInfo()->getOrderByPathSegments() as $order) {
90
                foreach ($order->getSubPathSegments() as $subOrder) {
91
                    $sourceEntityInstance = $sourceEntityInstance->orderBy(
92
                        $subOrder->getName(),
93
                        $order->isAscending() ? 'asc' : 'desc'
94
                    );
95
                }
96
            }
97
        }
98
99
        // throttle up for trench run
100
        if (null != $skipToken) {
101
            $parameters = [];
102
            $processed = [];
103
            $segments = $skipToken->getOrderByInfo()->getOrderByPathSegments();
104
            $values = $skipToken->getOrderByKeysInToken();
105
            $numValues = count($values);
106
            assert($numValues == count($segments));
107
108
            for ($i = 0; $i < $numValues; $i++) {
109
                $relation = $segments[$i]->isAscending() ? '>' : '<';
110
                $name = $segments[$i]->getSubPathSegments()[0]->getName();
111
                $parameters[$name] = ['direction' => $relation, 'value' => trim($values[$i][0], "\'")];
112
            }
113
114
            foreach ($parameters as $name => $line) {
115
                $processed[$name] = ['direction' => $line['direction'], 'value' => $line['value']];
116
                $sourceEntityInstance = $sourceEntityInstance
117
                    ->orWhere(function ($query) use ($processed) {
118
                        foreach ($processed as $key => $proc) {
119
                            $query->where($key, $proc['direction'], $proc['value']);
120
                        }
121
                    });
122
                // now we've handled the later-in-order segment for this key, drop it back to equality in prep
123
                // for next key - same-in-order for processed keys and later-in-order for next
124
                $processed[$name]['direction'] = '=';
125
            }
126
        }
127
128
        if (!isset($skip)) {
129
            $skip = 0;
130
        }
131
        if (!isset($top)) {
132
            $top = PHP_INT_MAX;
133
        }
134
135
        $nullFilter = true;
136
        $isvalid = null;
137
        if (isset($filterInfo)) {
138
            $method = "return ".$filterInfo->getExpressionAsString().";";
139
            $clln = "$".$resourceSet->getResourceType()->getName();
140
            $isvalid = create_function($clln, $method);
0 ignored issues
show
Security Best Practice introduced by
The use of create_function is highly discouraged, better use a closure.

create_function can pose a great security vulnerability as it is similar to eval, and could be used for arbitrary code execution. We highly recommend to use a closure instead.

// Instead of
$function = create_function('$a, $b', 'return $a + $b');

// Better use
$function = function($a, $b) { return $a + $b; }
Loading history...
141
            $nullFilter = false;
142
        }
143
144
        $bulkSetCount = $sourceEntityInstance->count();
145
        $bigSet = 20000 < $bulkSetCount;
146
147
        if ($nullFilter) {
148
            // default no-filter case, palm processing off to database engine - is a lot faster
149
            $resultSet = $sourceEntityInstance->skip($skip)->take($top)->with($eagerLoad)->get();
150
            $resultCount = $bulkSetCount;
151
        } elseif ($bigSet) {
152
            assert(isset($isvalid), "Filter closure not set");
153
            $resultSet = collect([]);
154
            $rawCount = 0;
155
            $rawTop = null === $top ? $bulkSetCount : $top;
156
157
            // loop thru, chunk by chunk, to reduce chances of exhausting memory
158
            $sourceEntityInstance->chunk(
159
                5000,
160
                function ($results) use ($isvalid, &$skip, &$resultSet, &$rawCount, $rawTop) {
161
                    // apply filter
162
                    $results = $results->filter($isvalid);
163
                    // need to iterate through full result set to find count of items matching filter,
164
                    // so we can't bail out early
165
                    $rawCount += $results->count();
166
                    // now bolt on filtrate to accumulating result set if we haven't accumulated enough bitz
167
                    if ($rawTop > $resultSet->count() + $skip) {
168
                        $resultSet = collect(array_merge($resultSet->all(), $results->all()));
169
                        $sliceAmount = min($skip, $resultSet->count());
170
                        $resultSet = $resultSet->slice($sliceAmount);
171
                        $skip -= $sliceAmount;
172
                    }
173
                }
174
            );
175
176
            // clean up residual to-be-skipped records
177
            $resultSet = $resultSet->slice($skip);
178
            $resultCount = $rawCount;
179
        } else {
180
            if ($sourceEntityInstance instanceof Model) {
181
                $sourceEntityInstance = $sourceEntityInstance->getQuery();
182
            }
183
            $resultSet = $sourceEntityInstance->with($eagerLoad)->get();
184
            $resultSet = $resultSet->filter($isvalid);
185
            $resultCount = $resultSet->count();
186
187
            if (isset($skip)) {
188
                $resultSet = $resultSet->slice($skip);
189
            }
190
        }
191
192
        if (isset($top)) {
193
            $resultSet = $resultSet->take($top);
194
        }
195
196
        if (QueryType::ENTITIES() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) {
197
            $result->results = [];
198
            foreach ($resultSet as $res) {
199
                $result->results[] = $res;
200
            }
201
        }
202
        if (QueryType::COUNT() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) {
203
            $result->count = $resultCount;
204
        }
205
        $hazMore = $bulkSetCount > $skip + count($resultSet);
206
        $result->hasMore = $hazMore;
207
        return $result;
208
    }
209
210
    /**
211
     * Get related resource set for a resource
212
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection
213
     * http://host/EntitySet?$expand=NavigationPropertyToCollection
214
     *
215
     * @param QueryType $queryType indicates if this is a query for a count, entities, or entities with a count
216
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
217
     * @param object $sourceEntityInstance The source entity instance.
218
     * @param ResourceSet $targetResourceSet The resource set of containing the target of the navigation property
219
     * @param ResourceProperty $targetProperty The navigation property to retrieve
220
     * @param FilterInfo $filter represents the $filter parameter of the OData query.  NULL if no $filter specified
221
     * @param mixed $orderBy sorted order if we want to get the data in some specific order
222
     * @param int $top number of records which  need to be skip
223
     * @param String $skip value indicating what records to skip
224
     *
225
     * @return QueryResult
226
     *
227
     */
228
    public function getRelatedResourceSet(
229
        QueryType $queryType,
230
        ResourceSet $sourceResourceSet,
231
        $sourceEntityInstance,
232
        ResourceSet $targetResourceSet,
233
        ResourceProperty $targetProperty,
234
        $filter = null,
235
        $orderBy = null,
236
        $top = null,
237
        $skip = null,
238
        $skipToken = null
239
    ) {
240
        if (!($sourceEntityInstance instanceof Model)) {
241
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
242
        }
243
244
        assert(null != $sourceEntityInstance, "Source instance must not be null");
245
        $this->checkSourceInstance($sourceEntityInstance);
246
247
        $this->checkAuth($sourceEntityInstance);
248
249
        $propertyName = $targetProperty->getName();
250
        $results = $sourceEntityInstance->$propertyName();
251
252
        return $this->getResourceSet(
253
            $queryType,
254
            $sourceResourceSet,
255
            $filter,
256
            $orderBy,
257
            $top,
258
            $skip,
259
            $skipToken,
260
            $results
261
        );
262
    }
263
264
    /**
265
     * Gets an entity instance from an entity set identified by a key
266
     * IE: http://host/EntitySet(1L)
267
     * http://host/EntitySet(KeyA=2L,KeyB='someValue')
268
     *
269
     * @param ResourceSet $resourceSet The entity set containing the entity to fetch
270
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch
271
     *
272
     * @return object|null Returns entity instance if found else null
273
     */
274
    public function getResourceFromResourceSet(
275
        ResourceSet $resourceSet,
276
        KeyDescriptor $keyDescriptor = null
277
    ) {
278
        return $this->getResource($resourceSet, $keyDescriptor);
279
    }
280
281
282
    /**
283
     * Common method for getResourceFromRelatedResourceSet() and getResourceFromResourceSet()
284
     * @param ResourceSet|null $resourceSet
285
     * @param KeyDescriptor|null $keyDescriptor
286
     * @param Model|Relation|null $sourceEntityInstance Starting point of query
287
     */
288
    public function getResource(
289
        ResourceSet $resourceSet = null,
290
        KeyDescriptor $keyDescriptor = null,
291
        array $whereCondition = [],
292
        $sourceEntityInstance = null
293
    ) {
294
        if (null == $resourceSet && null == $sourceEntityInstance) {
295
            throw new \Exception('Must supply at least one of a resource set and source entity.');
296
        }
297
298
        $this->checkSourceInstance($sourceEntityInstance);
299
300
        if (null == $sourceEntityInstance) {
301
            assert(null != $resourceSet);
302
            $sourceEntityInstance = $this->getSourceEntityInstance($resourceSet);
303
        }
304
305
        $this->checkAuth($sourceEntityInstance);
306
307
        $this->processKeyDescriptor($sourceEntityInstance, $keyDescriptor);
308
        foreach ($whereCondition as $fieldName => $fieldValue) {
309
            $sourceEntityInstance = $sourceEntityInstance->where($fieldName, $fieldValue);
310
        }
311
        $sourceEntityInstance = $sourceEntityInstance->get();
312
        return (0 == $sourceEntityInstance->count()) ? null : $sourceEntityInstance->first();
313
    }
314
315
    /**
316
     * Get related resource for a resource
317
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
318
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity
319
     *
320
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
321
     * @param object $sourceEntityInstance The source entity instance.
322
     * @param ResourceSet $targetResourceSet The entity set containing the entity pointed to by the navigation property
323
     * @param ResourceProperty $targetProperty The navigation property to fetch
324
     *
325
     * @return object|null The related resource if found else null
326
     */
327
    public function getRelatedResourceReference(
328
        ResourceSet $sourceResourceSet,
329
        $sourceEntityInstance,
330
        ResourceSet $targetResourceSet,
331
        ResourceProperty $targetProperty
332
    ) {
333
        if (!($sourceEntityInstance instanceof Model)) {
334
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
335
        }
336
        $this->checkSourceInstance($sourceEntityInstance);
337
338
        $this->checkAuth($sourceEntityInstance);
339
340
        $propertyName = $targetProperty->getName();
341
        return $sourceEntityInstance->$propertyName;
342
    }
343
344
    /**
345
     * Gets a related entity instance from an entity set identified by a key
346
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33)
347
     *
348
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
349
     * @param object $sourceEntityInstance The source entity instance.
350
     * @param ResourceSet $targetResourceSet The entity set containing the entity to fetch
351
     * @param ResourceProperty $targetProperty The metadata of the target property.
352
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch
353
     *
354
     * @return object|null Returns entity instance if found else null
355
     */
356
    public function getResourceFromRelatedResourceSet(
357
        ResourceSet $sourceResourceSet,
358
        $sourceEntityInstance,
359
        ResourceSet $targetResourceSet,
360
        ResourceProperty $targetProperty,
361
        KeyDescriptor $keyDescriptor
362
    ) {
363
        if (!($sourceEntityInstance instanceof Model)) {
364
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
365
        }
366
        $propertyName = $targetProperty->getName();
367
        if (!method_exists($sourceEntityInstance, $propertyName)) {
368
            $msg = 'Relation method, '.$propertyName.', does not exist on supplied entity.';
369
            throw new InvalidArgumentException($msg);
370
        }
371
        // take key descriptor and turn it into where clause here, rather than in getResource call
372
        $sourceEntityInstance = $sourceEntityInstance->$propertyName();
373
        $this->processKeyDescriptor($sourceEntityInstance, $keyDescriptor);
374
        $result = $this->getResource(null, null, [], $sourceEntityInstance);
375
        assert(
376
            $result instanceof Model || null == $result,
377
            'GetResourceFromRelatedResourceSet must return an entity or null'
378
        );
379
        return $result;
380
    }
381
382
383
    /**
384
     * @param ResourceSet $resourceSet
385
     * @return mixed
386
     */
387
    protected function getSourceEntityInstance(ResourceSet $resourceSet)
388
    {
389
        $entityClassName = $resourceSet->getResourceType()->getInstanceType()->name;
390
        return App::make($entityClassName);
391
    }
392
393
    /**
394
     * @param Model|Relation|null $source
395
     */
396
    protected function checkSourceInstance($source)
397
    {
398
        if (!(null == $source || $source instanceof Model || $source instanceof Relation)) {
399
            throw new InvalidArgumentException('Source entity instance must be null, a model, or a relation.');
400
        }
401
    }
402
403
    protected function getAuth()
404
    {
405
        return $this->auth;
406
    }
407
408
    /**
409
     * @param $sourceEntityInstance
410
     * @throws ODataException
411
     */
412
    private function checkAuth($sourceEntityInstance, $checkInstance = null)
413
    {
414
        $check = $checkInstance instanceof Model ? $checkInstance
415
            : $checkInstance instanceof Relation ? $checkInstance
416
                : $sourceEntityInstance instanceof Model ? $sourceEntityInstance
417
                    : $sourceEntityInstance instanceof Relation ? $sourceEntityInstance
418
                        : null;
419
        if (!$this->getAuth()->canAuth(ActionVerb::READ(), get_class($sourceEntityInstance), $check)) {
420
            throw new ODataException("Access denied", 403);
421
        }
422
    }
423
424
    /**
425
     * @param $sourceEntityInstance
426
     * @param KeyDescriptor $keyDescriptor
427
     * @throws \POData\Common\InvalidOperationException
428
     */
429
    private function processKeyDescriptor(&$sourceEntityInstance, KeyDescriptor $keyDescriptor = null)
430
    {
431
        if ($keyDescriptor) {
432
            foreach ($keyDescriptor->getValidatedNamedValues() as $key => $value) {
433
                $trimValue = trim($value[0], "\"'");
434
                $sourceEntityInstance = $sourceEntityInstance->where($key, $trimValue);
435
            }
436
        }
437
    }
438
}
439