Test Failed
Pull Request — master (#72)
by Alex
02:43
created

LaravelReadQuery::getResource()   C

Complexity

Conditions 8
Paths 21

Size

Total Lines 31
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 31
rs 5.3846
c 0
b 0
f 0
cc 8
eloc 20
nc 21
nop 4
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
61
        $eagerLoad = [];
62
63
        $this->checkSourceInstance($sourceEntityInstance);
64
        if (null == $sourceEntityInstance) {
65
            $sourceEntityInstance = $this->getSourceEntityInstance($resourceSet);
66
        }
67
68
        $hasSkipToken = null !== $skipToken && is_string($skipToken) && !empty($skipToken);
69
        $skipTokenColumn = null;
70
        $skipTokenDirection = '<';
71
72
        if ($sourceEntityInstance instanceof Model) {
73
            $eagerLoad = $sourceEntityInstance->getEagerLoad();
74
            $skipTokenColumn = $hasSkipToken ? $sourceEntityInstance->getKeyName() : null;
75
76
        } elseif ($sourceEntityInstance instanceof Relation) {
77
            $eagerLoad = $sourceEntityInstance->getRelated()->getEagerLoad();
78
            $skipTokenColumn = $hasSkipToken ? $sourceEntityInstance->getRelated()->getKeyName() : null;
79
        }
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
        if (!isset($skip)) {
100
            $skip = 0;
101
        }
102
103
        if (!isset($top)) {
104
            $top = PHP_INT_MAX;
105
        }
106
107
        if (null == $filterInfo && null == $orderBy && 0 == $skip && $hasSkipToken) {
108
            $skip = $sourceEntityInstance->where($skipTokenColumn, $skipTokenDirection, $skipToken)->count();
109
        }
110
111
        $nullFilter = true;
112
        $isvalid = null;
113
        if (isset($filterInfo)) {
114
            $method = "return ".$filterInfo->getExpressionAsString().";";
115
            $clln = "$".$resourceSet->getResourceType()->getName();
116
            $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...
117
            $nullFilter = false;
118
        }
119
120
        $bulkSetCount = $sourceEntityInstance->count();
121
        $bigSet = 20000 < $bulkSetCount;
122
123
        if ($nullFilter) {
124
            // default no-filter case, palm processing off to database engine - is a lot faster
125
            $resultSet = $sourceEntityInstance->skip($skip)->take($top)->with($eagerLoad)->get();
126
            $resultCount = $bulkSetCount;
127
        } elseif ($bigSet) {
128
            assert(isset($isvalid), "Filter closure not set");
129
            $resultSet = collect([]);
130
            $rawCount = 0;
131
            $rawTop = null === $top ? $bulkSetCount : $top;
132
133
            // loop thru, chunk by chunk, to reduce chances of exhausting memory
134
            $sourceEntityInstance->chunk(
135
                5000,
136
                function ($results) use ($isvalid, &$skip, &$resultSet, &$rawCount, $rawTop) {
137
                    // apply filter
138
                    $results = $results->filter($isvalid);
139
                    // need to iterate through full result set to find count of items matching filter,
140
                    // so we can't bail out early
141
                    $rawCount += $results->count();
142
                    // now bolt on filtrate to accumulating result set if we haven't accumulated enough bitz
143
                    if ($rawTop > $resultSet->count() + $skip) {
144
                        $resultSet = collect(array_merge($resultSet->all(), $results->all()));
145
                        $sliceAmount = min($skip, $resultSet->count());
146
                        $resultSet = $resultSet->slice($sliceAmount);
147
                        $skip -= $sliceAmount;
148
                    }
149
                }
150
            );
151
152
            // clean up residual to-be-skipped records
153
            $resultSet = $resultSet->slice($skip);
154
            $resultCount = $rawCount;
155
        } else {
156
            if ($sourceEntityInstance instanceof Model) {
157
                $sourceEntityInstance = $sourceEntityInstance->getQuery();
158
            }
159
            $resultSet = $sourceEntityInstance->with($eagerLoad)->get();
160
            $resultSet = $resultSet->filter($isvalid);
161
            $resultCount = $resultSet->count();
162
163
            if (isset($skip)) {
164
                $resultSet = $resultSet->slice($skip);
165
            }
166
        }
167
168
        if (isset($top)) {
169
            $resultSet = $resultSet->take($top);
170
        }
171
172
173
        if (QueryType::ENTITIES() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) {
174
            $result->results = array();
175
            foreach ($resultSet as $res) {
176
                $result->results[] = $res;
177
            }
178
        }
179
        if (QueryType::COUNT() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) {
180
            $result->count = $resultCount;
181
        }
182
        return $result;
183
    }
184
185
    /**
186
     * Get related resource set for a resource
187
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection
188
     * http://host/EntitySet?$expand=NavigationPropertyToCollection
189
     *
190
     * @param QueryType $queryType indicates if this is a query for a count, entities, or entities with a count
191
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
192
     * @param object $sourceEntityInstance The source entity instance.
193
     * @param ResourceSet $targetResourceSet The resource set of containing the target of the navigation property
194
     * @param ResourceProperty $targetProperty The navigation property to retrieve
195
     * @param FilterInfo $filter represents the $filter parameter of the OData query.  NULL if no $filter specified
196
     * @param mixed $orderBy sorted order if we want to get the data in some specific order
197
     * @param int $top number of records which  need to be skip
198
     * @param String $skip value indicating what records to skip
199
     *
200
     * @return QueryResult
201
     *
202
     */
203
    public function getRelatedResourceSet(
204
        QueryType $queryType,
205
        ResourceSet $sourceResourceSet,
206
        $sourceEntityInstance,
207
        ResourceSet $targetResourceSet,
208
        ResourceProperty $targetProperty,
209
        $filter = null,
210
        $orderBy = null,
211
        $top = null,
212
        $skip = null,
213
        $skipToken = null
0 ignored issues
show
Unused Code introduced by
The parameter $skipToken is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
214
    ) {
215
        if (!($sourceEntityInstance instanceof Model)) {
216
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
217
        }
218
219
        assert(null != $sourceEntityInstance, "Source instance must not be null");
220
        $this->checkSourceInstance($sourceEntityInstance);
221
222
        $this->checkAuth($sourceEntityInstance);
223
224
        $propertyName = $targetProperty->getName();
225
        $results = $sourceEntityInstance->$propertyName();
226
227
        return $this->getResourceSet(
228
            $queryType,
229
            $sourceResourceSet,
230
            $filter,
231
            $orderBy,
232
            $top,
233
            $skip,
234
            $results
235
        );
236
    }
237
238
    /**
239
     * Gets an entity instance from an entity set identified by a key
240
     * IE: http://host/EntitySet(1L)
241
     * http://host/EntitySet(KeyA=2L,KeyB='someValue')
242
     *
243
     * @param ResourceSet $resourceSet The entity set containing the entity to fetch
244
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch
245
     *
246
     * @return object|null Returns entity instance if found else null
247
     */
248
    public function getResourceFromResourceSet(
249
        ResourceSet $resourceSet,
250
        KeyDescriptor $keyDescriptor = null
251
    ) {
252
        return $this->getResource($resourceSet, $keyDescriptor);
253
    }
254
255
256
    /**
257
     * Common method for getResourceFromRelatedResourceSet() and getResourceFromResourceSet()
258
     * @param ResourceSet|null $resourceSet
259
     * @param KeyDescriptor|null $keyDescriptor
260
     * @param Model|Relation|null $sourceEntityInstance Starting point of query
261
     */
262
    public function getResource(
263
        ResourceSet $resourceSet = null,
264
        KeyDescriptor $keyDescriptor = null,
265
        array $whereCondition = [],
266
        $sourceEntityInstance = null
267
    ) {
268
        if (null == $resourceSet && null == $sourceEntityInstance) {
269
            throw new \Exception('Must supply at least one of a resource set and source entity.');
270
        }
271
272
        $this->checkSourceInstance($sourceEntityInstance);
273
274
        if (null == $sourceEntityInstance) {
275
            assert(null != $resourceSet);
276
            $sourceEntityInstance = $this->getSourceEntityInstance($resourceSet);
277
        }
278
279
        $this->checkAuth($sourceEntityInstance);
280
281
        if ($keyDescriptor) {
282
            foreach ($keyDescriptor->getValidatedNamedValues() as $key => $value) {
283
                $trimValue = trim($value[0], "\"'");
284
                $sourceEntityInstance = $sourceEntityInstance->where($key, $trimValue);
285
            }
286
        }
287
        foreach ($whereCondition as $fieldName => $fieldValue) {
288
            $sourceEntityInstance = $sourceEntityInstance->where($fieldName, $fieldValue);
289
        }
290
        $sourceEntityInstance = $sourceEntityInstance->get();
291
        return (0 == $sourceEntityInstance->count()) ? null : $sourceEntityInstance->first();
292
    }
293
294
    /**
295
     * Get related resource for a resource
296
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
297
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity
298
     *
299
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
300
     * @param object $sourceEntityInstance The source entity instance.
301
     * @param ResourceSet $targetResourceSet The entity set containing the entity pointed to by the navigation property
302
     * @param ResourceProperty $targetProperty The navigation property to fetch
303
     *
304
     * @return object|null The related resource if found else null
305
     */
306 View Code Duplication
    public function getRelatedResourceReference(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
307
        ResourceSet $sourceResourceSet,
308
        $sourceEntityInstance,
309
        ResourceSet $targetResourceSet,
310
        ResourceProperty $targetProperty
311
    ) {
312
        if (!($sourceEntityInstance instanceof Model)) {
313
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
314
        }
315
        $this->checkSourceInstance($sourceEntityInstance);
316
317
        $this->checkAuth($sourceEntityInstance);
318
319
        $propertyName = $targetProperty->getName();
320
        return $sourceEntityInstance->$propertyName;
321
    }
322
323
    /**
324
     * Gets a related entity instance from an entity set identified by a key
325
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33)
326
     *
327
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
328
     * @param object $sourceEntityInstance The source entity instance.
329
     * @param ResourceSet $targetResourceSet The entity set containing the entity to fetch
330
     * @param ResourceProperty $targetProperty The metadata of the target property.
331
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch
332
     *
333
     * @return object|null Returns entity instance if found else null
334
     */
335 View Code Duplication
    public function getResourceFromRelatedResourceSet(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
336
        ResourceSet $sourceResourceSet,
337
        $sourceEntityInstance,
338
        ResourceSet $targetResourceSet,
339
        ResourceProperty $targetProperty,
340
        KeyDescriptor $keyDescriptor
341
    ) {
342
        if (!($sourceEntityInstance instanceof Model)) {
343
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
344
        }
345
        $propertyName = $targetProperty->getName();
346
        return $this->getResource(null, $keyDescriptor, [], $sourceEntityInstance->$propertyName);
347
    }
348
349
350
    /**
351
     * @param ResourceSet $resourceSet
352
     * @return mixed
353
     */
354
    protected function getSourceEntityInstance(ResourceSet $resourceSet)
355
    {
356
        $entityClassName = $resourceSet->getResourceType()->getInstanceType()->name;
357
        return App::make($entityClassName);
358
    }
359
360
    /**
361
     * @param Model|Relation|null $source
362
     */
363
    protected function checkSourceInstance($source)
364
    {
365
        if (!(null == $source || $source instanceof Model || $source instanceof Relation)) {
366
            throw new InvalidArgumentException('Source entity instance must be null, a model, or a relation.');
367
        }
368
    }
369
370
    protected function getAuth()
371
    {
372
        return $this->auth;
373
    }
374
375
    /**
376
     * @param $sourceEntityInstance
377
     * @throws ODataException
378
     */
379
    private function checkAuth($sourceEntityInstance, $checkInstance = null)
380
    {
381
        $check = $checkInstance instanceof Model ? $checkInstance
382
            : $checkInstance instanceof Relation ? $checkInstance
383
                : $sourceEntityInstance instanceof Model ? $sourceEntityInstance
384
                    : $sourceEntityInstance instanceof Relation ? $sourceEntityInstance
385
                        : null;
386
        if (!$this->getAuth()->canAuth(ActionVerb::READ(), get_class($sourceEntityInstance), $check)) {
387
            throw new ODataException("Access denied", 403);
388
        }
389
    }
390
}
391