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

LaravelQuery::createUpdateDeleteProcessOutput()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 20
ccs 0
cts 0
cp 0
rs 8.2222
cc 7
eloc 12
nc 7
nop 1
crap 56
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