Test Setup Failed
Pull Request — master (#52)
by Alex
03:03
created

LaravelReadQuery::getResourceFromResourceSet()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 2
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 Symfony\Component\Process\Exception\InvalidArgumentException;
13
use POData\Providers\Metadata\ResourceProperty;
14
use POData\Providers\Metadata\ResourceSet;
15
use POData\Providers\Query\QueryResult;
16
use POData\Providers\Query\QueryType;
17
use POData\UriProcessor\QueryProcessor\ExpressionParser\FilterInfo;
18
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
19
20
class LaravelReadQuery
21
{
22
    protected $auth;
23
24
    public function __construct(AuthInterface $auth = null)
25
    {
26
        $this->auth = isset($auth) ? $auth : new NullAuthProvider();
27
    }
28
29
    /**
30
     * Gets collection of entities belongs to an entity set
31
     * IE: http://host/EntitySet
32
     *  http://host/EntitySet?$skip=10&$top=5&filter=Prop gt Value
33
     *
34
     * @param QueryType $queryType indicates if this is a query for a count, entities, or entities with a count
35
     * @param ResourceSet $resourceSet The entity set containing the entities to fetch
36
     * @param FilterInfo $filterInfo represents the $filter parameter of the OData query.  NULL if no $filter specified
37
     * @param mixed $orderBy sorted order if we want to get the data in some specific order
38
     * @param int $top number of records which  need to be skip
39
     * @param String $skipToken value indicating what records to skip
40
     * @param Model|Relation|null $sourceEntityInstance Starting point of query
41
     *
42
     * @return QueryResult
43
     */
44
    public function getResourceSet(
45
        QueryType $queryType,
46
        ResourceSet $resourceSet,
47
        $filterInfo = null,
48
        $orderBy = null,
49
        $top = null,
50
        $skipToken = null,
51
        $sourceEntityInstance = null
52
    ) {
53
        if (null != $filterInfo && !($filterInfo instanceof FilterInfo)) {
54
            throw new InvalidArgumentException('Filter info must be either null or instance of FilterInfo.');
55
        }
56
57
        $this->checkSourceInstance($sourceEntityInstance);
58
        if (null == $sourceEntityInstance) {
59
            $sourceEntityInstance = $this->getSourceEntityInstance($resourceSet);
60
        }
61
62
        $checkInstance = $sourceEntityInstance instanceof Model ? $sourceEntityInstance : null;
63 View Code Duplication
        if (!$this->getAuth()->canAuth(ActionVerb::READ(), get_class($sourceEntityInstance), $checkInstance)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
64
            throw new ODataException("Access denied", 403);
65
        }
66
67
        $result          = new QueryResult();
68
        $result->results = null;
69
        $result->count   = null;
70
71
        if (null != $orderBy) {
72
            foreach ($orderBy->getOrderByInfo()->getOrderByPathSegments() as $order) {
73
                foreach ($order->getSubPathSegments() as $subOrder) {
74
                    $sourceEntityInstance = $sourceEntityInstance->orderBy(
75
                        $subOrder->getName(),
76
                        $order->isAscending() ? 'asc' : 'desc'
77
                    );
78
                }
79
            }
80
        }
81
82
        if (!isset($skipToken)) {
83
            $skipToken = 0;
84
        }
85
        if (!isset($top)) {
86
            $top = PHP_INT_MAX;
87
        }
88
89
        $nullFilter = true;
90
        $isvalid = null;
91
        if (isset($filterInfo)) {
92
            $method = "return ".$filterInfo->getExpressionAsString().";";
93
            $clln = "$".$resourceSet->getResourceType()->getName();
94
            $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...
95
            $nullFilter = false;
96
        }
97
98
        $bulkSetCount = $sourceEntityInstance->count();
99
        $bigSet = 20000 < $bulkSetCount;
100
101
        if ($nullFilter) {
102
            // default no-filter case, palm processing off to database engine - is a lot faster
103
            $resultSet = $sourceEntityInstance->skip($skipToken)->take($top)->get();
104
            $resultCount = $bulkSetCount;
105
        } elseif ($bigSet) {
106
            assert(isset($isvalid), "Filter closure not set");
107
            $resultSet = collect([]);
108
            $rawCount = 0;
109
            $rawTop = null === $top ? $bulkSetCount : $top;
110
111
            // loop thru, chunk by chunk, to reduce chances of exhausting memory
112
            $sourceEntityInstance->chunk(
113
                5000,
114
                function ($results) use ($isvalid, &$skipToken, &$resultSet, &$rawCount, $rawTop) {
115
                    // apply filter
116
                    $results = $results->filter($isvalid);
117
                    // need to iterate through full result set to find count of items matching filter,
118
                    // so we can't bail out early
119
                    $rawCount += $results->count();
120
                    // now bolt on filtrate to accumulating result set if we haven't accumulated enough bitz
121
                    if ($rawTop > $resultSet->count() + $skipToken) {
122
                        $resultSet = collect(array_merge($resultSet->all(), $results->all()));
123
                        $sliceAmount = min($skipToken, $resultSet->count());
124
                        $resultSet = $resultSet->slice($sliceAmount);
125
                        $skipToken -= $sliceAmount;
126
                    }
127
                }
128
            );
129
130
            // clean up residual to-be-skipped records
131
            $resultSet = $resultSet->slice($skipToken);
132
            $resultCount = $rawCount;
133
        } else {
134
            $resultSet = $sourceEntityInstance->get();
135
            $resultSet = $resultSet->filter($isvalid);
136
            $resultCount = $resultSet->count();
137
138
            if (isset($skipToken)) {
139
                $resultSet = $resultSet->slice($skipToken);
140
            }
141
        }
142
143
        if (isset($top)) {
144
            $resultSet = $resultSet->take($top);
145
        }
146
147
148
        if (QueryType::ENTITIES() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) {
149
            $result->results = array();
150
            foreach ($resultSet as $res) {
151
                $result->results[] = $res;
152
            }
153
        }
154
        if (QueryType::COUNT() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) {
155
            $result->count = $resultCount;
156
        }
157
        return $result;
158
    }
159
160
    /**
161
     * Get related resource set for a resource
162
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection
163
     * http://host/EntitySet?$expand=NavigationPropertyToCollection
164
     *
165
     * @param QueryType $queryType indicates if this is a query for a count, entities, or entities with a count
166
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
167
     * @param object $sourceEntityInstance The source entity instance.
168
     * @param ResourceSet $targetResourceSet The resource set of containing the target of the navigation property
169
     * @param ResourceProperty $targetProperty The navigation property to retrieve
170
     * @param FilterInfo $filter represents the $filter parameter of the OData query.  NULL if no $filter specified
171
     * @param mixed $orderBy sorted order if we want to get the data in some specific order
172
     * @param int $top number of records which  need to be skip
173
     * @param String $skip value indicating what records to skip
174
     *
175
     * @return QueryResult
176
     *
177
     */
178
    public function getRelatedResourceSet(
179
        QueryType $queryType,
180
        ResourceSet $sourceResourceSet,
181
        $sourceEntityInstance,
182
        ResourceSet $targetResourceSet,
183
        ResourceProperty $targetProperty,
184
        $filter = null,
185
        $orderBy = null,
186
        $top = null,
187
        $skip = null
188
    ) {
189
        if (!($sourceEntityInstance instanceof Model)) {
190
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
191
        }
192
193
        assert(null != $sourceEntityInstance, "Source instance must not be null");
194
        $this->checkSourceInstance($sourceEntityInstance);
195
196 View Code Duplication
        if (!$this->getAuth()->canAuth(ActionVerb::READ(), get_class($sourceEntityInstance), $sourceEntityInstance)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
197
            throw new ODataException("Access denied", 403);
198
        }
199
200
        $propertyName = $targetProperty->getName();
201
        $results = $sourceEntityInstance->$propertyName();
202
203
        return $this->getResourceSet(
204
            $queryType,
205
            $sourceResourceSet,
206
            $filter,
207
            $orderBy,
208
            $top,
209
            $skip,
210
            $results
211
        );
212
    }
213
214
    /**
215
     * Gets an entity instance from an entity set identified by a key
216
     * IE: http://host/EntitySet(1L)
217
     * http://host/EntitySet(KeyA=2L,KeyB='someValue')
218
     *
219
     * @param ResourceSet $resourceSet The entity set containing the entity to fetch
220
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch
221
     *
222
     * @return object|null Returns entity instance if found else null
223
     */
224
    public function getResourceFromResourceSet(
225
        ResourceSet $resourceSet,
226
        KeyDescriptor $keyDescriptor = null
227
    ) {
228
        return $this->getResource($resourceSet, $keyDescriptor);
229
    }
230
231
232
    /**
233
     * Common method for getResourceFromRelatedResourceSet() and getResourceFromResourceSet()
234
     * @param ResourceSet|null $resourceSet
235
     * @param KeyDescriptor|null $keyDescriptor
236
     * @param Model|Relation|null $sourceEntityInstance Starting point of query
237
     */
238
    public function getResource(
239
        ResourceSet $resourceSet = null,
240
        KeyDescriptor $keyDescriptor = null,
241
        array $whereCondition = [],
242
        $sourceEntityInstance = null
243
    ) {
244
        if (null == $resourceSet && null == $sourceEntityInstance) {
245
            throw new \Exception('Must supply at least one of a resource set and source entity.');
246
        }
247
248
        $this->checkSourceInstance($sourceEntityInstance);
249
250
        if (null == $sourceEntityInstance) {
251
            assert(null != $resourceSet);
252
            $sourceEntityInstance = $this->getSourceEntityInstance($resourceSet);
253
        }
254
255 View Code Duplication
        if (!$this->getAuth()->canAuth(ActionVerb::READ(), get_class($sourceEntityInstance), $sourceEntityInstance)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
256
            throw new ODataException("Access denied", 403);
257
        }
258
259
        if ($keyDescriptor) {
260
            foreach ($keyDescriptor->getValidatedNamedValues() as $key => $value) {
261
                $trimValue = trim($value[0], "\"'");
262
                $sourceEntityInstance = $sourceEntityInstance->where($key, $trimValue);
263
            }
264
        }
265
        foreach ($whereCondition as $fieldName => $fieldValue) {
266
            $sourceEntityInstance = $sourceEntityInstance->where($fieldName, $fieldValue);
267
        }
268
        $sourceEntityInstance = $sourceEntityInstance->get();
269
        return (0 == $sourceEntityInstance->count()) ? null : $sourceEntityInstance->first();
270
    }
271
272
    /**
273
     * Get related resource for a resource
274
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
275
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity
276
     *
277
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
278
     * @param object $sourceEntityInstance The source entity instance.
279
     * @param ResourceSet $targetResourceSet The entity set containing the entity pointed to by the navigation property
280
     * @param ResourceProperty $targetProperty The navigation property to fetch
281
     *
282
     * @return object|null The related resource if found else null
283
     */
284
    public function getRelatedResourceReference(
285
        ResourceSet $sourceResourceSet,
286
        $sourceEntityInstance,
287
        ResourceSet $targetResourceSet,
288
        ResourceProperty $targetProperty
289
    ) {
290
        if (!($sourceEntityInstance instanceof Model)) {
291
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
292
        }
293
        $this->checkSourceInstance($sourceEntityInstance);
294
295 View Code Duplication
        if (!$this->getAuth()->canAuth(ActionVerb::READ(), get_class($sourceEntityInstance), $sourceEntityInstance)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
296
            throw new ODataException("Access denied", 403);
297
        }
298
299
        $propertyName = $targetProperty->getName();
300
        return $sourceEntityInstance->$propertyName;
301
    }
302
303
    /**
304
     * Gets a related entity instance from an entity set identified by a key
305
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33)
306
     *
307
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
308
     * @param object $sourceEntityInstance The source entity instance.
309
     * @param ResourceSet $targetResourceSet The entity set containing the entity to fetch
310
     * @param ResourceProperty $targetProperty The metadata of the target property.
311
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch
312
     *
313
     * @return object|null Returns entity instance if found else null
314
     */
315
    public function getResourceFromRelatedResourceSet(
316
        ResourceSet $sourceResourceSet,
317
        $sourceEntityInstance,
318
        ResourceSet $targetResourceSet,
319
        ResourceProperty $targetProperty,
320
        KeyDescriptor $keyDescriptor
321
    ) {
322
        if (!($sourceEntityInstance instanceof Model)) {
323
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
324
        }
325
        $propertyName = $targetProperty->getName();
326
        return $this->getResource(null, $keyDescriptor, [], $sourceEntityInstance->$propertyName);
327
    }
328
329
330
    /**
331
     * @param ResourceSet $resourceSet
332
     * @return mixed
333
     */
334
    protected function getSourceEntityInstance(ResourceSet $resourceSet)
335
    {
336
        $entityClassName = $resourceSet->getResourceType()->getInstanceType()->name;
337
        return App::make($entityClassName);
338
    }
339
340
    /**
341
     * @param Model|Relation|null $source
342
     */
343
    protected function checkSourceInstance($source)
344
    {
345
        if (!(null == $source || $source instanceof Model || $source instanceof Relation)) {
346
            throw new InvalidArgumentException('Source entity instance must be null, a model, or a relation.');
347
        }
348
    }
349
350
    protected function getAuth()
351
    {
352
        return $this->auth;
353
    }
354
}
355