Test Failed
Pull Request — master (#90)
by Alex
03:42 queued 17s
created

LaravelReadQuery::checkSourceInstance()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 6
rs 9.2
cc 4
eloc 3
nc 2
nop 1
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\InvalidOperationException;
12
use POData\Common\ODataException;
13
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
14
use POData\UriProcessor\QueryProcessor\SkipTokenParser\SkipTokenInfo;
15
use Symfony\Component\Process\Exception\InvalidArgumentException;
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\ResourcePathProcessor\SegmentParser\KeyDescriptor;
22
23
class LaravelReadQuery
24
{
25
    const PK = 'PrimaryKey';
26
27
    protected $auth;
28
29
    public function __construct(AuthInterface $auth = null)
30
    {
31
        $this->auth = isset($auth) ? $auth : new NullAuthProvider();
32
    }
33
34
    /**
35
     * Gets collection of entities belongs to an entity set
36
     * IE: http://host/EntitySet
37
     *  http://host/EntitySet?$skip=10&$top=5&filter=Prop gt Value
38
     *
39
     * @param QueryType                $queryType   Is this is a query for a count, entities, or entities-with-count
40
     * @param ResourceSet              $resourceSet The entity set containing the entities to fetch
41
     * @param FilterInfo|null          $filterInfo  The $filter parameter of the OData query.  NULL if none specified
42
     * @param null|InternalOrderByInfo $orderBy     sorted order if we want to get the data in some specific order
43
     * @param integer|null             $top         number of records which need to be retrieved
44
     * @param integer|null             $skip        number of records which need to be skipped
45
     * @param SkipTokenInfo|null       $skipToken   value indicating what records to skip
46
     * @param Model|Relation|null $sourceEntityInstance Starting point of query
47
     *
48
     * @return QueryResult
49
     */
50
    public function getResourceSet(
51
        QueryType $queryType,
52
        ResourceSet $resourceSet,
53
        $filterInfo = null,
54
        $orderBy = null,
55
        $top = null,
56
        $skip = null,
57
        $skipToken = null,
58
        $sourceEntityInstance = null
59
    ) {
60
        if (null != $filterInfo && !($filterInfo instanceof FilterInfo)) {
61
            throw new InvalidArgumentException('Filter info must be either null or instance of FilterInfo.');
62
        }
63
        if (null != $skipToken && !($skipToken instanceof SkipTokenInfo)) {
64
            throw new InvalidArgumentException('Skip token must be either null or instance of SkipTokenInfo.');
65
        }
66
67
        $eagerLoad = [];
68
69
        $this->checkSourceInstance($sourceEntityInstance);
70
        if (null == $sourceEntityInstance) {
71
            $sourceEntityInstance = $this->getSourceEntityInstance($resourceSet);
72
        }
73
74
        $keyName = null;
75
        if ($sourceEntityInstance instanceof Model) {
76
            $eagerLoad = $sourceEntityInstance->getEagerLoad();
77
            $keyName = $sourceEntityInstance->getKeyName();
78
        } elseif ($sourceEntityInstance instanceof Relation) {
79
            $eagerLoad = $sourceEntityInstance->getRelated()->getEagerLoad();
80
            $keyName = $sourceEntityInstance->getRelated()->getKeyName();
81
        }
82
        assert(isset($keyName));
83
84
        $checkInstance = $sourceEntityInstance instanceof Model ? $sourceEntityInstance : null;
85
        $this->checkAuth($sourceEntityInstance, $checkInstance);
86
87
        $result          = new QueryResult();
88
        $result->results = null;
89
        $result->count   = null;
90
91
        if (null != $orderBy) {
92
            foreach ($orderBy->getOrderByInfo()->getOrderByPathSegments() as $order) {
93
                foreach ($order->getSubPathSegments() as $subOrder) {
94
                    $subName = $subOrder->getName();
95
                    $subName = (self::PK == $subName) ? $sourceEntityInstance->getKeyName() : $subName;
96
                    $sourceEntityInstance = $sourceEntityInstance->orderBy(
97
                        $subName,
98
                        $order->isAscending() ? 'asc' : 'desc'
99
                    );
100
                }
101
            }
102
        }
103
104
        // throttle up for trench run
105
        if (null != $skipToken) {
106
            $parameters = [];
107
            $processed = [];
108
            $segments = $skipToken->getOrderByInfo()->getOrderByPathSegments();
109
            $values = $skipToken->getOrderByKeysInToken();
110
            $numValues = count($values);
111
            assert($numValues == count($segments));
112
113
            for ($i = 0; $i < $numValues; $i++) {
114
                $relation = $segments[$i]->isAscending() ? '>' : '<';
115
                $name = $segments[$i]->getSubPathSegments()[0]->getName();
116
                $parameters[$name] = ['direction' => $relation, 'value' => trim($values[$i][0], '\'')];
117
            }
118
119
            foreach ($parameters as $name => $line) {
120
                $processed[$name] = ['direction' => $line['direction'], 'value' => $line['value']];
121
                $sourceEntityInstance = $sourceEntityInstance
122
                    ->orWhere(function ($query) use ($processed) {
123
                        foreach ($processed as $key => $proc) {
124
                            $query->where($key, $proc['direction'], $proc['value']);
125
                        }
126
                    });
127
                // now we've handled the later-in-order segment for this key, drop it back to equality in prep
128
                // for next key - same-in-order for processed keys and later-in-order for next
129
                $processed[$name]['direction'] = '=';
130
            }
131
        }
132
133
        if (!isset($skip)) {
134
            $skip = 0;
135
        }
136
        if (!isset($top)) {
137
            $top = PHP_INT_MAX;
138
        }
139
140
        $nullFilter = true;
141
        $isvalid = null;
142
        if (isset($filterInfo)) {
143
            $method = 'return '.$filterInfo->getExpressionAsString().';';
144
            $clln = '$'.$resourceSet->getResourceType()->getName();
145
            $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...
146
            $nullFilter = false;
147
        }
148
149
        $bulkSetCount = $sourceEntityInstance->count();
150
        $bigSet = 20000 < $bulkSetCount;
151
152
        if ($nullFilter) {
153
            // default no-filter case, palm processing off to database engine - is a lot faster
154
            $resultSet = $sourceEntityInstance->skip($skip)->take($top)->with($eagerLoad)->get();
155
            $resultCount = $bulkSetCount;
156
        } elseif ($bigSet) {
157
            assert(isset($isvalid), 'Filter closure not set');
158
            $resultSet = collect([]);
159
            $rawCount = 0;
160
            $rawTop = null === $top ? $bulkSetCount : $top;
161
162
            // loop thru, chunk by chunk, to reduce chances of exhausting memory
163
            $sourceEntityInstance->chunk(
164
                5000,
165
                function ($results) use ($isvalid, &$skip, &$resultSet, &$rawCount, $rawTop) {
166
                    // apply filter
167
                    $results = $results->filter($isvalid);
168
                    // need to iterate through full result set to find count of items matching filter,
169
                    // so we can't bail out early
170
                    $rawCount += $results->count();
171
                    // now bolt on filtrate to accumulating result set if we haven't accumulated enough bitz
172
                    if ($rawTop > $resultSet->count() + $skip) {
173
                        $resultSet = collect(array_merge($resultSet->all(), $results->all()));
174
                        $sliceAmount = min($skip, $resultSet->count());
175
                        $resultSet = $resultSet->slice($sliceAmount);
176
                        $skip -= $sliceAmount;
177
                    }
178
                }
179
            );
180
181
            // clean up residual to-be-skipped records
182
            $resultSet = $resultSet->slice($skip);
183
            $resultCount = $rawCount;
184
        } else {
185
            if ($sourceEntityInstance instanceof Model) {
186
                $sourceEntityInstance = $sourceEntityInstance->getQuery();
187
            }
188
            $resultSet = $sourceEntityInstance->with($eagerLoad)->get();
189
            $resultSet = $resultSet->filter($isvalid);
190
            $resultCount = $resultSet->count();
191
192
            if (isset($skip)) {
193
                $resultSet = $resultSet->slice($skip);
194
            }
195
        }
196
197
        if (isset($top)) {
198
            $resultSet = $resultSet->take($top);
199
        }
200
201
        if (QueryType::ENTITIES() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) {
202
            $result->results = [];
203
            foreach ($resultSet as $res) {
204
                $result->results[] = $res;
205
            }
206
        }
207
        if (QueryType::COUNT() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) {
208
            $result->count = $resultCount;
209
        }
210
        $hazMore = $bulkSetCount > $skip + count($resultSet);
211
        $result->hasMore = $hazMore;
212
        return $result;
213
    }
214
215
    /**
216
     * Get related resource set for a resource
217
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection
218
     * http://host/EntitySet?$expand=NavigationPropertyToCollection
219
     *
220
     * @param QueryType          $queryType            Is this is a query for a count, entities, or entities-with-count
221
     * @param ResourceSet        $sourceResourceSet    The entity set containing the source entity
222
     * @param Model              $sourceEntityInstance The source entity instance
223
     * @param ResourceSet        $targetResourceSet    The resource set pointed to by the navigation property
224
     * @param ResourceProperty   $targetProperty       The navigation property to retrieve
225
     * @param FilterInfo|null    $filter               The $filter parameter of the OData query.  NULL if none specified
226
     * @param mixed|null         $orderBy              sorted order if we want to get the data in some specific order
227
     * @param integer|null       $top                  number of records which need to be retrieved
228
     * @param integer|null       $skip                 number of records which need to be skipped
229
     * @param SkipTokenInfo|null $skipToken            value indicating what records to skip
230
     *
231
     * @return QueryResult
232
     *
233
     */
234
    public function getRelatedResourceSet(
235
        QueryType $queryType,
236
        ResourceSet $sourceResourceSet,
237
        Model $sourceEntityInstance,
238
        ResourceSet $targetResourceSet,
239
        ResourceProperty $targetProperty,
240
        FilterInfo $filter = null,
241
        $orderBy = null,
242
        $top = null,
243
        $skip = null,
244
        SkipTokenInfo $skipToken = null
245
    ) {
246
        $this->checkAuth($sourceEntityInstance);
247
248
        $propertyName = $targetProperty->getName();
249
        $results = $sourceEntityInstance->$propertyName();
250
251
        return $this->getResourceSet(
252
            $queryType,
253
            $sourceResourceSet,
254
            $filter,
255
            $orderBy,
256
            $top,
257
            $skip,
258
            $skipToken,
259
            $results
260
        );
261
    }
262
263
    /**
264
     * Gets an entity instance from an entity set identified by a key
265
     * IE: http://host/EntitySet(1L)
266
     * http://host/EntitySet(KeyA=2L,KeyB='someValue')
267
     *
268
     * @param ResourceSet           $resourceSet    The entity set containing the entity to fetch
269
     * @param KeyDescriptor|null    $keyDescriptor  The key identifying the entity to fetch
270
     *
271
     * @return Model|null Returns entity instance if found else null
272
     */
273
    public function getResourceFromResourceSet(
274
        ResourceSet $resourceSet,
275
        KeyDescriptor $keyDescriptor = null
276
    ) {
277
        return $this->getResource($resourceSet, $keyDescriptor);
278
    }
279
280
281
    /**
282
     * Common method for getResourceFromRelatedResourceSet() and getResourceFromResourceSet()
283
     * @param ResourceSet|null      $resourceSet
284
     * @param KeyDescriptor|null    $keyDescriptor
285
     * @param Model|Relation|null   $sourceEntityInstance   Starting point of query
286
     *
287
     * @return Model|null
288
     */
289
    public function getResource(
290
        ResourceSet $resourceSet = null,
291
        KeyDescriptor $keyDescriptor = null,
292
        array $whereCondition = [],
293
        $sourceEntityInstance = null
294
    ) {
295
        if (null == $resourceSet && null == $sourceEntityInstance) {
296
            throw new \Exception('Must supply at least one of a resource set and source entity.');
297
        }
298
299
        $this->checkSourceInstance($sourceEntityInstance);
300
301
        if (null == $sourceEntityInstance) {
302
            assert(null != $resourceSet);
303
            $sourceEntityInstance = $this->getSourceEntityInstance($resourceSet);
304
        }
305
306
        $this->checkAuth($sourceEntityInstance);
307
308
        $this->processKeyDescriptor($sourceEntityInstance, $keyDescriptor);
309
        foreach ($whereCondition as $fieldName => $fieldValue) {
310
            $sourceEntityInstance = $sourceEntityInstance->where($fieldName, $fieldValue);
311
        }
312
        $sourceEntityInstance = $sourceEntityInstance->get();
313
        $sourceCount = $sourceEntityInstance->count();
314
        if (0 == $sourceCount) {
315
            return null;
316
        }
317
        $result = $sourceEntityInstance->first();
318
        $result->PrimaryKey = $result->getKey();
319
320
        return $result;
321
    }
322
323
    /**
324
     * Get related resource for a resource
325
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
326
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity
327
     *
328
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
329
     * @param Model  $sourceEntityInstance The source entity instance.
330
     * @param ResourceSet $targetResourceSet The entity set containing the entity pointed to by the navigation property
331
     * @param ResourceProperty $targetProperty The navigation property to fetch
332
     *
333
     * @return object|null The related resource if found else null
334
     */
335
    public function getRelatedResourceReference(
336
        ResourceSet $sourceResourceSet,
337
        Model $sourceEntityInstance,
338
        ResourceSet $targetResourceSet,
339
        ResourceProperty $targetProperty
340
    ) {
341
        $this->checkAuth($sourceEntityInstance);
342
343
        $propertyName = $targetProperty->getName();
344
        $result = $sourceEntityInstance->$propertyName;
345
        $result->PrimaryKey = $result->getKey();
346
        return $result;
347
    }
348
349
    /**
350
     * Gets a related entity instance from an entity set identified by a key
351
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33)
352
     *
353
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
354
     * @param Model $sourceEntityInstance The source entity instance.
355
     * @param ResourceSet $targetResourceSet The entity set containing the entity to fetch
356
     * @param ResourceProperty $targetProperty The metadata of the target property.
357
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch
358
     *
359
     * @return Model|null Returns entity instance if found else null
360
     */
361
    public function getResourceFromRelatedResourceSet(
362
        ResourceSet $sourceResourceSet,
363
        Model $sourceEntityInstance,
364
        ResourceSet $targetResourceSet,
365
        ResourceProperty $targetProperty,
366
        KeyDescriptor $keyDescriptor
367
    ) {
368
        $propertyName = $targetProperty->getName();
369
        if (!method_exists($sourceEntityInstance, $propertyName)) {
370
            $msg = 'Relation method, '.$propertyName.', does not exist on supplied entity.';
371
            throw new InvalidArgumentException($msg);
372
        }
373
        // take key descriptor and turn it into where clause here, rather than in getResource call
374
        $sourceEntityInstance = $sourceEntityInstance->$propertyName();
375
        $this->processKeyDescriptor($sourceEntityInstance, $keyDescriptor);
376
        $result = $this->getResource(null, null, [], $sourceEntityInstance);
377
        assert(
378
            $result instanceof Model || null == $result,
379
            'GetResourceFromRelatedResourceSet must return an entity or null'
380
        );
381
        return $result;
382
    }
383
384
385
    /**
386
     * @param ResourceSet $resourceSet
387
     * @return mixed
388
     */
389
    protected function getSourceEntityInstance(ResourceSet $resourceSet)
390
    {
391
        $entityClassName = $resourceSet->getResourceType()->getInstanceType()->name;
392
        return App::make($entityClassName);
393
    }
394
395
    /**
396
     * @param Model|Relation|null $source
397
     */
398
    protected function checkSourceInstance($source)
399
    {
400
        if (!(null == $source || $source instanceof Model || $source instanceof Relation)) {
401
            throw new InvalidArgumentException('Source entity instance must be null, a model, or a relation.');
402
        }
403
    }
404
405
    protected function getAuth()
406
    {
407
        return $this->auth;
408
    }
409
410
    /**
411
     * @param $sourceEntityInstance
412
     * @throws ODataException
413
     */
414
    private function checkAuth($sourceEntityInstance, $checkInstance = null)
415
    {
416
        $check = $checkInstance instanceof Model ? $checkInstance
417
            : $checkInstance instanceof Relation ? $checkInstance
418
                : $sourceEntityInstance instanceof Model ? $sourceEntityInstance
419
                    : $sourceEntityInstance instanceof Relation ? $sourceEntityInstance
420
                        : null;
421
        if (!$this->getAuth()->canAuth(ActionVerb::READ(), get_class($sourceEntityInstance), $check)) {
422
            throw new ODataException('Access denied', 403);
423
        }
424
    }
425
426
    /**
427
     * @param $sourceEntityInstance
428
     * @param KeyDescriptor|null    $keyDescriptor
429
     * @throws InvalidOperationException
430
     */
431
    private function processKeyDescriptor(&$sourceEntityInstance, KeyDescriptor $keyDescriptor = null)
432
    {
433
        if ($keyDescriptor) {
434
            foreach ($keyDescriptor->getValidatedNamedValues() as $key => $value) {
435
                $key = (self::PK == $key) ? $sourceEntityInstance->getKeyName() : $key;
436
                $trimValue = trim($value[0], '\'');
437
                $sourceEntityInstance = $sourceEntityInstance->where($key, $trimValue);
438
            }
439
        }
440
    }
441
}
442