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

LaravelReadQuery::checkAuth()   B

Complexity

Conditions 6
Paths 32

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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