Test Failed
Pull Request — master (#36)
by Alex
06:10
created

LaravelQuery::getRelatedResourceSet()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 30
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 30
ccs 10
cts 10
cp 1
rs 8.8571
cc 2
eloc 24
nc 2
nop 9
crap 2

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace AlgoWeb\PODataLaravel\Query;
4
5
use POData\Providers\Metadata\ResourceProperty;
6
use POData\Providers\Metadata\ResourceSet;
7
use POData\UriProcessor\QueryProcessor\Expression\Parser\IExpressionProvider;
8
use POData\UriProcessor\QueryProcessor\ExpressionParser\FilterInfo;
9
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
10
use POData\Providers\Query\IQueryProvider;
11
use POData\Providers\Expression\MySQLExpressionProvider;
12
use POData\Providers\Query\QueryType;
13
use POData\Providers\Query\QueryResult;
14
use POData\Providers\Expression\PHPExpressionProvider;
15
use \POData\Common\ODataException;
16
use AlgoWeb\PODataLaravel\Interfaces\AuthInterface;
17
use AlgoWeb\PODataLaravel\Auth\NullAuthProvider;
18
use Illuminate\Support\Facades\App;
19
use Illuminate\Database\Eloquent\Model;
20
use Symfony\Component\Process\Exception\InvalidArgumentException;
21
22
class LaravelQuery implements IQueryProvider
23
{
24
    protected $expression;
25
    protected $auth;
26 5
    public $queryProviderClassName;
27
28
    public function __construct(AuthInterface $auth = null)
29 5
    {
30 5
        /* MySQLExpressionProvider();*/
31 5
        $this->expression = new LaravelExpressionProvider(); //PHPExpressionProvider('expression');
32 5
        $this->queryProviderClassName = get_class($this);
33
        $this->auth = isset($auth) ? $auth : new NullAuthProvider();
34
    }
35
36
    /**
37
     * Indicates if the QueryProvider can handle ordered paging, this means respecting order, skip, and top parameters
38
     * If the query provider can not handle ordered paging, it must return the entire result set and POData will
39
     * perform the ordering and paging
40
     *
41
     * @return Boolean True if the query provider can handle ordered paging, false if POData should perform the paging
42
     */
43
    public function handlesOrderedPaging()
44
    {
45
        return true;
46
    }
47
48
    /**
49
     * Gets the expression provider used by to compile OData expressions into expression used by this query provider.
50
     *
51
     * @return \POData\Providers\Expression\IExpressionProvider
52
     */
53
    public function getExpressionProvider()
54
    {
55
        return $this->expression;
56
    }
57
58
    /**
59
     * Gets collection of entities belongs to an entity set
60
     * IE: http://host/EntitySet
61
     *  http://host/EntitySet?$skip=10&$top=5&filter=Prop gt Value
62
     *
63
     * @param QueryType $queryType indicates if this is a query for a count, entities, or entities with a count
64
     * @param ResourceSet $resourceSet The entity set containing the entities to fetch
65
     * @param FilterInfo $filterInfo represents the $filter parameter of the OData query.  NULL if no $filter specified
66
     * @param mixed $orderBy sorted order if we want to get the data in some specific order
67
     * @param int $top number of records which  need to be skip
68
     * @param String $skipToken value indicating what records to skip
69
     *
70 3
     * @return QueryResult
71
     */
72
    public function getResourceSet(
73
        QueryType $queryType,
74
        ResourceSet $resourceSet,
75
        $filterInfo = null,
76
        $orderBy = null,
77
        $top = null,
78
        $skipToken = null,
79 3
        Model $sourceEntityInstance = null
80
    ) {
81
        if (null != $filterInfo && !($filterInfo instanceof FilterInfo)) {
82 3
            throw new InvalidArgumentException('Filter info must be either null or instance of FilterInfo.');
83 1
        }
84 1
85
        if (null == $sourceEntityInstance) {
86 3
            $sourceEntityInstance = $this->getSourceEntityInstance($resourceSet);
87 3
        }
88 3
89
        $result          = new QueryResult();
90 3
        $result->results = null;
91
        $result->count   = null;
92
93
        if (null != $orderBy) {
94
            foreach ($orderBy->getOrderByInfo()->getOrderByPathSegments() as $order) {
95
                foreach ($order->getSubPathSegments() as $subOrder) {
96
                    $sourceEntityInstance = $sourceEntityInstance->orderBy(
97 1
                        $subOrder->getName(),
98
                        $order->isAscending() ? 'asc' : 'desc'
99
                    );
100 3
                }
101 1
            }
102 1
        }
103 3
104 1
        $resultSet = $sourceEntityInstance->get();
105 1
106
        if (isset($filterInfo)) {
107 3
            $method = "return ".$filterInfo->getExpressionAsString().";";
108
            $clln = "$".$resourceSet->getResourceType()->getName();
109 3
            $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...
110
            $resultSet = $resultSet->filter($isvalid);
111
        }
112
113
        $resultCount = $resultSet->count();
114
115
        if (isset($skipToken)) {
116
            $resultSet = $resultSet->slice($skipToken);
117 3
        }
118 1
        if (isset($top)) {
119 1
            $resultSet = $resultSet->take($top);
120 1
        }
121 1
122 1
123 3
        if (QueryType::ENTITIES() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) {
124 3
            $result->results = array();
125
            foreach ($resultSet as $res) {
126
                $result->results[] = $res;
127 3
            }
128 3
        }
129 3
        if (QueryType::COUNT() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) {
130
            $result->count = $resultCount;
131
        }
132
        return $result;
133
    }
134
    /**
135
     * Gets an entity instance from an entity set identified by a key
136
     * IE: http://host/EntitySet(1L)
137
     * http://host/EntitySet(KeyA=2L,KeyB='someValue')
138
     *
139
     * @param ResourceSet $resourceSet The entity set containing the entity to fetch
140
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch
141
     *
142
     * @return object|null Returns entity instance if found else null
143
     */
144
    public function getResourceFromResourceSet(
145
        ResourceSet $resourceSet,
146
        KeyDescriptor $keyDescriptor = null
147
    ) {
148
        return $this->getResource($resourceSet, $keyDescriptor);
149
    }
150
151
    /**
152
     * Common method for getResourceFromRelatedResourceSet() and getResourceFromResourceSet()
153
     * @param ResourceSet|null $resourceSet
154
     * @param null|KeyDescriptor $keyDescriptor
155
     */
156
    protected function getResource(
157
        ResourceSet $resourceSet = null,
158
        KeyDescriptor $keyDescriptor = null,
159
        array $whereCondition = [],
160
        Model $sourceEntityInstance = null
161
    ) {
162
        if (null == $resourceSet && null == $sourceEntityInstance) {
163
            throw new \Exception('Must supply at least one of a resource set and source entity.');
164
        }
165
166
        if (null == $sourceEntityInstance) {
167
            assert(null != $resourceSet);
168
            $sourceEntityInstance = $this->getSourceEntityInstance($resourceSet);
169
        }
170
171
        if ($keyDescriptor) {
172
            foreach ($keyDescriptor->getValidatedNamedValues() as $key => $value) {
173
                $trimValue = trim($value[0], "\"'");
174
                $sourceEntityInstance = $sourceEntityInstance->where($key, $trimValue);
175
            }
176
        }
177
        foreach ($whereCondition as $fieldName => $fieldValue) {
178
            $sourceEntityInstance = $sourceEntityInstance->where($fieldName, $fieldValue);
179
        }
180
        $sourceEntityInstance = $sourceEntityInstance->get();
181
        return (0 == $sourceEntityInstance->count()) ? null : $sourceEntityInstance->first();
182
    }
183
184
    /**
185
     * Get related resource set for a resource
186
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection
187
     * http://host/EntitySet?$expand=NavigationPropertyToCollection
188
     *
189
     * @param QueryType $queryType indicates if this is a query for a count, entities, or entities with a count
190
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
191
     * @param object $sourceEntityInstance The source entity instance.
192
     * @param ResourceSet $targetResourceSet The resource set of containing the target of the navigation property
193
     * @param ResourceProperty $targetProperty The navigation property to retrieve
194
     * @param FilterInfo $filter represents the $filter parameter of the OData query.  NULL if no $filter specified
195
     * @param mixed $orderBy sorted order if we want to get the data in some specific order
196
     * @param int $top number of records which  need to be skip
197
     * @param String $skip value indicating what records to skip
198
     *
199 3
     * @return QueryResult
200
     *
201
     */
202
    public function getRelatedResourceSet(
203
        QueryType $queryType,
204
        ResourceSet $sourceResourceSet,
205
        $sourceEntityInstance,
206
        ResourceSet $targetResourceSet,
207
        ResourceProperty $targetProperty,
208
        $filter = null,
209
        $orderBy = null,
210 3
        $top = null,
211 3
        $skip = null
212
    ) {
213 3
        if (!($sourceEntityInstance instanceof Model)) {
214 3
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
215 3
        }
216 3
217 3
        $propertyName = $targetProperty->getName();
218 3
        $results = $sourceEntityInstance->$propertyName();
219 3
        $relatedClass = $results->getRelated();
220
        $sourceEntityInstance = new $relatedClass();
221 3
222
        return $this->getResourceSet(
223
            $queryType,
224
            $sourceResourceSet,
225
            $filter,
226
            $orderBy,
227
            $top,
228
            $skip,
229
            $sourceEntityInstance
230
        );
231
    }
232
233
    /**
234
     * Gets a related entity instance from an entity set identified by a key
235
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33)
236
     *
237
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
238
     * @param object $sourceEntityInstance The source entity instance.
239
     * @param ResourceSet $targetResourceSet The entity set containing the entity to fetch
240
     * @param ResourceProperty $targetProperty The metadata of the target property.
241
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch
242
     *
243
     * @return object|null Returns entity instance if found else null
244
     */
245
    public function getResourceFromRelatedResourceSet(
246
        ResourceSet $sourceResourceSet,
247
        $sourceEntityInstance,
248
        ResourceSet $targetResourceSet,
249
        ResourceProperty $targetProperty,
250
        KeyDescriptor $keyDescriptor
251
    ) {
252
        if (!($sourceEntityInstance instanceof Model)) {
253
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
254
        }
255
        $propertyName = $targetProperty->getName();
256
        return $this->getResource(null, $keyDescriptor, [], $sourceEntityInstance->$propertyName);
257
    }
258
259
    /**
260
     * Get related resource for a resource
261
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
262
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity
263
     *
264
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
265
     * @param object $sourceEntityInstance The source entity instance.
266
     * @param ResourceSet $targetResourceSet The entity set containing the entity pointed to by the navigation property
267
     * @param ResourceProperty $targetProperty The navigation property to fetch
268
     *
269
     * @return object|null The related resource if found else null
270
     */
271
    public function getRelatedResourceReference(
272
        ResourceSet $sourceResourceSet,
273
        $sourceEntityInstance,
274
        ResourceSet $targetResourceSet,
275
        ResourceProperty $targetProperty
276
    ) {
277
        if (!($sourceEntityInstance instanceof Model)) {
278
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
279
        }
280
        $propertyName = $targetProperty->getName();
281
        return $sourceEntityInstance->$propertyName;
282
    }
283
284
    /**
285
     * @param ResourceSet $resourceSet
286
     * @return mixed
287
     */
288
    protected function getSourceEntityInstance(ResourceSet $resourceSet)
289
    {
290
        $entityClassName = $resourceSet->getResourceType()->getInstanceType()->name;
291
        return App::make($entityClassName);
292 1
    }
293
294
    /**
295
     * Updates a resource
296
     *
297
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
298
     * @param object           $sourceEntityInstance The source entity instance
299 1
     * @param KeyDescriptor    $keyDescriptor        The key identifying the entity to fetch
300 1
     * @param object           $data                 The New data for the entity instance.
301
     * @param bool             $shouldUpdate        Should undefined values be updated or reset to default
302 1
     *
303
     * @return object|null The new resource value if it is assignable or throw exception for null.
304 1
     */
305
    public function updateResource(
306 1
        ResourceSet $sourceResourceSet,
307
        $sourceEntityInstance,
308
        KeyDescriptor $keyDescriptor,
309 1
        $data,
310
        $shouldUpdate = false
311
    ) {
312
        $verb = 'update';
313
        return $this->createUpdateCoreWrapper($sourceResourceSet, $sourceEntityInstance, $data, $verb);
314
    }
315
    /**
316
     * Delete resource from a resource set.
317
     * @param ResourceSet|null $sourceResourceSet
318 1
     * @param object           $sourceEntityInstance
319
     *
320
     * return bool true if resources sucessfully deteled, otherwise false.
321
     */
322 1
    public function deleteResource(
323 1
        ResourceSet $sourceResourceSet,
324 1
        $sourceEntityInstance
325 1
    ) {
326 1
        $verb = 'delete';
327
        if (!($sourceEntityInstance instanceof Model)) {
328 1
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
329
        }
330 1
331 1
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->name;
332
        $id = $sourceEntityInstance->getKey();
333
        $name = $sourceEntityInstance->getKeyName();
334 1
        $data = [$name => $id];
335
336
        $data = $this->createUpdateDeleteCore($sourceEntityInstance, $data, $class, $verb);
337
338
        $success = isset($data['id']);
339
        if ($success) {
340
            return true;
341
        }
342
        throw new ODataException('Target model not successfully deleted', 422);
343 1
    }
344
    /**
345
     * @param ResourceSet      $resourceSet   The entity set containing the entity to fetch
346
     * @param object           $sourceEntityInstance The source entity instance
347
     * @param object           $data                 The New data for the entity instance.
348 1
     *
349 1
     * returns object|null returns the newly created model if sucessful or null if model creation failed.
350
     */
351 1
    public function createResourceforResourceSet(
352
        ResourceSet $resourceSet,
353 1
        $sourceEntityInstance,
354
        $data
355 1
    ) {
356
        $verb = 'create';
357
        return $this->createUpdateCoreWrapper($resourceSet, $sourceEntityInstance, $data, $verb);
358 1
    }
359
360
    /**
361
     * @param $sourceEntityInstance
362
     * @param $data
363
     * @param $class
364
     * @param string $verb
365
     * @return array|mixed
366
     * @throws ODataException
367
     * @throws \POData\Common\InvalidOperationException
368
     */
369
    private function createUpdateDeleteCore($sourceEntityInstance, $data, $class, $verb)
370 3
    {
371
        $raw = App::make('metadataControllers');
372 3
        $map = $raw->getMetadata();
373 3
374
        if (!array_key_exists($class, $map)) {
375 3
            throw new \POData\Common\InvalidOperationException('Controller mapping missing for class '.$class.'.');
376
        }
377
        $goal = $raw->getMapping($class, $verb);
378 3
        if (null == $goal) {
379 3
            throw new \POData\Common\InvalidOperationException(
380
                'Controller mapping missing for '.$verb.' verb on class '.$class.'.'
381
            );
382
        }
383
384
        if (null == $data) {
385 3
            $data = [];
386
        } elseif (is_object($data)) {
387
            $data = (array) $data;
388 3
        }
389 2
        if (!is_array($data)) {
390 2
            throw \POData\Common\ODataException::createPreConditionFailedError(
391 3
                'Data not resolvable to key-value array.'
392
            );
393
        }
394
395
        $controlClass = $goal['controller'];
396
        $method = $goal['method'];
397 3
        $paramList = $goal['parameters'];
398 3
        $controller = App::make($controlClass);
399 3
        $parms = $this->createUpdateDeleteProcessInput($sourceEntityInstance, $data, $paramList);
400 3
        unset($data);
401 3
402
        $result = call_user_func_array(array($controller, $method), $parms);
403 3
404 3
        return $this->createUpdateDeleteProcessOutput($result);
405 3
    }
406 3
407 2
    /**
408 2
     * Puts an entity instance to entity set identified by a key.
409 2
     *
410 2
     * @param ResourceSet $resourceSet The entity set containing the entity to update
411 2
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to update
412
     * @param $data
413
     *
414
     * @return bool|null Returns result of executiong query
415 2
     */
416
    public function putResource(
417
        ResourceSet $resourceSet,
418 3
        KeyDescriptor $keyDescriptor,
419 3
        $data
420
    ) {
421 3
        // TODO: Implement putResource() method.
422
    }
423 3
424
    /**
425
     * @param ResourceSet $sourceResourceSet
426 3
     * @param $sourceEntityInstance
427 3
     * @param $data
428 3
     * @param $verb
429 3
     * @return mixed
430
     * @throws ODataException
431 3
     * @throws \POData\Common\InvalidOperationException
432
     */
433
    private function createUpdateCoreWrapper(ResourceSet $sourceResourceSet, $sourceEntityInstance, $data, $verb)
434 3
    {
435
        $lastWord = 'update' == $verb ? 'updated' : 'created';
436
        if (!(null == $sourceEntityInstance || $sourceEntityInstance instanceof Model)) {
437
            throw new InvalidArgumentException('Source entity must either be null or an Eloquent model.');
438
        }
439 3
440
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->name;
441
442
        $data = $this->createUpdateDeleteCore($sourceEntityInstance, $data, $class, $verb);
443
444
        $success = isset($data['id']);
445
446
        if ($success) {
447
            return $class::findOrFail($data['id']);
448
        }
449
        throw new ODataException('Target model not successfully '.$lastWord, 422);
450
    }
451
452
    /**
453
     * @param $sourceEntityInstance
454
     * @param $data
455
     * @param $paramList
456
     * @return array
457
     */
458
    private function createUpdateDeleteProcessInput($sourceEntityInstance, $data, $paramList)
459
    {
460
        $parms = [];
461
462
        foreach ($paramList as $spec) {
463
            $varType = isset($spec['type']) ? $spec['type'] : null;
464
            $varName = $spec['name'];
465
            if (null == $varType) {
466
                $parms[] = $sourceEntityInstance->$varName;
467
                continue;
468
            }
469
            // TODO: Give this smarts and actively pick up instantiation details
470
            $var = new $varType();
471
            if ($spec['isRequest']) {
472
                $var->setMethod('POST');
473
                $var->request = new \Symfony\Component\HttpFoundation\ParameterBag($data);
474
            }
475
            $parms[] = $var;
476
        }
477
        return $parms;
478
    }
479
480
    /**
481
     * @param $result
482
     * @return array|mixed
483
     * @throws ODataException
484
     */
485
    private function createUpdateDeleteProcessOutput($result)
486
    {
487
        if (!($result instanceof \Illuminate\Http\JsonResponse)) {
488
            throw ODataException::createInternalServerError('Controller response not well-formed json.');
489
        }
490
        $outData = $result->getData();
491
        if (is_object($outData)) {
492
            $outData = (array)$outData;
493
        }
494
495
        if (!is_array($outData)) {
496
            throw ODataException::createInternalServerError('Controller response does not have an array.');
497
        }
498
        if (!(key_exists('id', $outData) && key_exists('status', $outData) && key_exists('errors', $outData))) {
499
            throw ODataException::createInternalServerError(
500
                'Controller response array missing at least one of id, status and/or errors fields.'
501
            );
502
        }
503
        return $outData;
504
    }
505
}
506