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

LaravelQuery::deleteResource()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3.009

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 22
ccs 9
cts 10
cp 0.9
rs 9.2
cc 3
eloc 15
nc 3
nop 2
crap 3.009
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
            $sourceEntityInstance = $this->getSourceEntityInstance($resourceSet);
0 ignored issues
show
Bug introduced by
It seems like $resourceSet defined by parameter $resourceSet on line 157 can be null; however, AlgoWeb\PODataLaravel\Qu...tSourceEntityInstance() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
168
        }
169
170
        if ($keyDescriptor) {
171
            foreach ($keyDescriptor->getValidatedNamedValues() as $key => $value) {
172
                $trimValue = trim($value[0], "\"'");
173
                $sourceEntityInstance = $sourceEntityInstance->where($key, $trimValue);
174
            }
175
        }
176
        foreach ($whereCondition as $fieldName => $fieldValue) {
177
            $sourceEntityInstance = $sourceEntityInstance->where($fieldName, $fieldValue);
178
        }
179
        $sourceEntityInstance = $sourceEntityInstance->get();
180
        return (0 == $sourceEntityInstance->count()) ? null : $sourceEntityInstance->first();
181
    }
182
183
    /**
184
     * Get related resource set for a resource
185
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection
186
     * http://host/EntitySet?$expand=NavigationPropertyToCollection
187
     *
188
     * @param QueryType $queryType indicates if this is a query for a count, entities, or entities with a count
189
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
190
     * @param object $sourceEntityInstance The source entity instance.
191
     * @param ResourceSet $targetResourceSet The resource set of containing the target of the navigation property
192
     * @param ResourceProperty $targetProperty The navigation property to retrieve
193
     * @param FilterInfo $filter represents the $filter parameter of the OData query.  NULL if no $filter specified
194
     * @param mixed $orderBy sorted order if we want to get the data in some specific order
195
     * @param int $top number of records which  need to be skip
196
     * @param String $skip value indicating what records to skip
197
     *
198
     * @return QueryResult
199 3
     *
200
     */
201
    public function getRelatedResourceSet(
202
        QueryType $queryType,
203
        ResourceSet $sourceResourceSet,
204
        $sourceEntityInstance,
205
        ResourceSet $targetResourceSet,
206
        ResourceProperty $targetProperty,
207
        $filter = null,
208
        $orderBy = null,
209
        $top = null,
210 3
        $skip = null
211 3
    ) {
212
        if (!($sourceEntityInstance instanceof Model)) {
213 3
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
214 3
        }
215 3
216 3
        $propertyName = $targetProperty->getName();
217 3
        $results = $sourceEntityInstance->$propertyName();
218 3
        $relatedClass = $results->getRelated();
219 3
        $sourceEntityInstance = new $relatedClass();
220
221 3
        return $this->getResourceSet(
222
            $queryType,
223
            $sourceResourceSet,
224
            $filter,
225
            $orderBy,
226
            $top,
227
            $skip,
228
            $sourceEntityInstance
229
        );
230
    }
231
232
    /**
233
     * Gets a related entity instance from an entity set identified by a key
234
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33)
235
     *
236
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
237
     * @param object $sourceEntityInstance The source entity instance.
238
     * @param ResourceSet $targetResourceSet The entity set containing the entity to fetch
239
     * @param ResourceProperty $targetProperty The metadata of the target property.
240
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch
241
     *
242
     * @return object|null Returns entity instance if found else null
243
     */
244
    public function getResourceFromRelatedResourceSet(
245
        ResourceSet $sourceResourceSet,
246
        $sourceEntityInstance,
247
        ResourceSet $targetResourceSet,
248
        ResourceProperty $targetProperty,
249
        KeyDescriptor $keyDescriptor
250
    ) {
251
        if (!($sourceEntityInstance instanceof Model)) {
252
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
253
        }
254
        $propertyName = $targetProperty->getName();
255
        return $this->getResource(null, $keyDescriptor, [], $sourceEntityInstance->$propertyName);
256
    }
257
258
    /**
259
     * Get related resource for a resource
260
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
261
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity
262
     *
263
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
264
     * @param object $sourceEntityInstance The source entity instance.
265
     * @param ResourceSet $targetResourceSet The entity set containing the entity pointed to by the navigation property
266
     * @param ResourceProperty $targetProperty The navigation property to fetch
267
     *
268
     * @return object|null The related resource if found else null
269
     */
270
    public function getRelatedResourceReference(
271
        ResourceSet $sourceResourceSet,
272
        $sourceEntityInstance,
273
        ResourceSet $targetResourceSet,
274
        ResourceProperty $targetProperty
275
    ) {
276
        if (!($sourceEntityInstance instanceof Model)) {
277
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
278
        }
279
        $propertyName = $targetProperty->getName();
280
        return $sourceEntityInstance->$propertyName;
281
    }
282
283
    /**
284
     * @param ResourceSet $resourceSet
285
     * @return mixed
286
     */
287
    protected function getSourceEntityInstance(ResourceSet $resourceSet)
288
    {
289
        $entityClassName = $resourceSet->getResourceType()->getInstanceType()->name;
290
        return App::make($entityClassName);
291
    }
292 1
293
    /**
294
     * Updates a resource
295
     *
296
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
297
     * @param object           $sourceEntityInstance The source entity instance
298
     * @param KeyDescriptor    $keyDescriptor        The key identifying the entity to fetch
299 1
     * @param object           $data                 The New data for the entity instance.
300 1
     * @param bool             $shouldUpdate        Should undefined values be updated or reset to default
301
     *
302 1
     * @return object|null The new resource value if it is assignable or throw exception for null.
303
     */
304 1
    public function updateResource(
305
        ResourceSet $sourceResourceSet,
306 1
        $sourceEntityInstance,
307
        KeyDescriptor $keyDescriptor,
308
        $data,
309 1
        $shouldUpdate = false
310
    ) {
311
        $verb = 'update';
312
        return $this->createUpdateCoreWrapper($sourceResourceSet, $sourceEntityInstance, $data, $verb);
313
    }
314
    /**
315
     * Delete resource from a resource set.
316
     * @param ResourceSet|null $sourceResourceSet
317
     * @param object           $sourceEntityInstance
318 1
     *
319
     * return bool true if resources sucessfully deteled, otherwise false.
320
     */
321
    public function deleteResource(
322 1
        ResourceSet $sourceResourceSet,
323 1
        $sourceEntityInstance
324 1
    ) {
325 1
        $verb = 'delete';
326 1
        if (!($sourceEntityInstance instanceof Model)) {
327
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
328 1
        }
329
330 1
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->name;
331 1
        $id = $sourceEntityInstance->getKey();
332
        $name = $sourceEntityInstance->getKeyName();
333
        $data = [$name => $id];
334 1
335
        $data = $this->createUpdateDeleteCore($sourceEntityInstance, $data, $class, $verb);
336
337
        $success = isset($data['id']);
338
        if ($success) {
339
            return true;
340
        }
341
        throw new ODataException('Target model not successfully deleted', 422);
342
    }
343 1
    /**
344
     * @param ResourceSet      $resourceSet   The entity set containing the entity to fetch
345
     * @param object           $sourceEntityInstance The source entity instance
346
     * @param object           $data                 The New data for the entity instance.
347
     *
348 1
     * returns object|null returns the newly created model if sucessful or null if model creation failed.
349 1
     */
350
    public function createResourceforResourceSet(
351 1
        ResourceSet $resourceSet,
352
        $sourceEntityInstance,
353 1
        $data
354
    ) {
355 1
        $verb = 'create';
356
        return $this->createUpdateCoreWrapper($resourceSet, $sourceEntityInstance, $data, $verb);
357
    }
358 1
359
    /**
360
     * @param $sourceEntityInstance
361
     * @param $data
362
     * @param $class
363
     * @param string $verb
364
     * @return array|mixed
365
     * @throws ODataException
366
     * @throws \POData\Common\InvalidOperationException
367
     */
368
    private function createUpdateDeleteCore($sourceEntityInstance, $data, $class, $verb)
369
    {
370 3
        $raw = App::make('metadataControllers');
371
        $map = $raw->getMetadata();
372 3
373 3
        if (!array_key_exists($class, $map)) {
374
            throw new \POData\Common\InvalidOperationException('Controller mapping missing for class '.$class.'.');
375 3
        }
376
        $goal = $raw->getMapping($class, $verb);
377
        if (null == $goal) {
378 3
            throw new \POData\Common\InvalidOperationException(
379 3
                'Controller mapping missing for '.$verb.' verb on class '.$class.'.'
380
            );
381
        }
382
383
        if (null == $data) {
384
            $data = [];
385 3
        } elseif (is_object($data)) {
386
            $data = (array) $data;
387
        }
388 3
        if (!is_array($data)) {
389 2
            throw \POData\Common\ODataException::createPreConditionFailedError(
390 2
                'Data not resolvable to key-value array.'
391 3
            );
392
        }
393
394
        $controlClass = $goal['controller'];
395
        $method = $goal['method'];
396
        $paramList = $goal['parameters'];
397 3
        $controller = App::make($controlClass);
398 3
        $parms = $this->createUpdateDeleteProcessInput($sourceEntityInstance, $data, $paramList);
399 3
        unset($data);
400 3
401 3
        $result = call_user_func_array(array($controller, $method), $parms);
402
403 3
        return $this->createUpdateDeleteProcessOutput($result);
404 3
    }
405 3
406 3
    /**
407 2
     * Puts an entity instance to entity set identified by a key.
408 2
     *
409 2
     * @param ResourceSet $resourceSet The entity set containing the entity to update
410 2
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to update
411 2
     * @param $data
412
     *
413
     * @return bool|null Returns result of executiong query
414
     */
415 2
    public function putResource(
416
        ResourceSet $resourceSet,
417
        KeyDescriptor $keyDescriptor,
418 3
        $data
419 3
    ) {
420
        // TODO: Implement putResource() method.
421 3
    }
422
423 3
    /**
424
     * @param ResourceSet $sourceResourceSet
425
     * @param $sourceEntityInstance
426 3
     * @param $data
427 3
     * @param $verb
428 3
     * @return mixed
429 3
     * @throws ODataException
430
     * @throws \POData\Common\InvalidOperationException
431 3
     */
432
    private function createUpdateCoreWrapper(ResourceSet $sourceResourceSet, $sourceEntityInstance, $data, $verb)
433
    {
434 3
        $lastWord = 'update' == $verb ? 'updated' : 'created';
435
        if (!(null == $sourceEntityInstance || $sourceEntityInstance instanceof Model)) {
436
            throw new InvalidArgumentException('Source entity must either be null or an Eloquent model.');
437
        }
438
439 3
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->name;
440
441
        $data = $this->createUpdateDeleteCore($sourceEntityInstance, $data, $class, $verb);
442
443
        $success = isset($data['id']);
444
445
        if ($success) {
446
            return $class::findOrFail($data['id']);
447
        }
448
        throw new ODataException('Target model not successfully '.$lastWord, 422);
449
    }
450
451
    /**
452
     * @param $sourceEntityInstance
453
     * @param $data
454
     * @param $paramList
455
     * @return array
456
     */
457
    private function createUpdateDeleteProcessInput($sourceEntityInstance, $data, $paramList)
458
    {
459
        $parms = [];
460
461
        foreach ($paramList as $spec) {
462
            $varType = isset($spec['type']) ? $spec['type'] : null;
463
            $varName = $spec['name'];
464
            if (null == $varType) {
465
                $parms[] = $sourceEntityInstance->$varName;
466
                continue;
467
            }
468
            // TODO: Give this smarts and actively pick up instantiation details
469
            $var = new $varType();
470
            if ($spec['isRequest']) {
471
                $var->setMethod('POST');
472
                $var->request = new \Symfony\Component\HttpFoundation\ParameterBag($data);
473
            }
474
            $parms[] = $var;
475
        }
476
        return $parms;
477
    }
478
479
    /**
480
     * @param $result
481
     * @return array|mixed
482
     * @throws ODataException
483
     */
484
    private function createUpdateDeleteProcessOutput($result)
485
    {
486
        if (!($result instanceof \Illuminate\Http\JsonResponse)) {
487
            throw ODataException::createInternalServerError('Controller response not well-formed json.');
488
        }
489
        $outData = $result->getData();
490
        if (is_object($outData)) {
491
            $outData = (array)$outData;
492
        }
493
494
        if (!is_array($outData)) {
495
            throw ODataException::createInternalServerError('Controller response does not have an array.');
496
        }
497
        if (!(key_exists('id', $outData) && key_exists('status', $outData) && key_exists('errors', $outData))) {
498
            throw ODataException::createInternalServerError(
499
                'Controller response array missing at least one of id, status and/or errors fields.'
500
            );
501
        }
502
        return $outData;
503
    }
504
}
505