Test Failed
Pull Request — master (#90)
by Alex
03:42 queued 17s
created

LaravelQuery::getResourceFromResourceSet()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
cc 1
eloc 4
nc 1
nop 2
crap 1
1
<?php
2
3
namespace AlgoWeb\PODataLaravel\Query;
4
5
use AlgoWeb\PODataLaravel\Enums\ActionVerb;
6
use Illuminate\Database\Eloquent\Relations\Relation;
7
use POData\Common\InvalidOperationException;
8
use POData\Providers\Metadata\ResourceProperty;
9
use POData\Providers\Metadata\ResourceSet;
10
use POData\UriProcessor\QueryProcessor\Expression\Parser\IExpressionProvider;
11
use POData\UriProcessor\QueryProcessor\ExpressionParser\FilterInfo;
12
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
13
use POData\UriProcessor\QueryProcessor\SkipTokenParser\SkipTokenInfo;
14
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
15
use POData\Providers\Query\IQueryProvider;
16
use POData\Providers\Expression\MySQLExpressionProvider;
17
use POData\Providers\Query\QueryType;
18
use POData\Providers\Query\QueryResult;
19
use POData\Providers\Expression\PHPExpressionProvider;
20
use \POData\Common\ODataException;
21
use AlgoWeb\PODataLaravel\Interfaces\AuthInterface;
22
use AlgoWeb\PODataLaravel\Auth\NullAuthProvider;
23
use Illuminate\Support\Facades\App;
24
use Illuminate\Database\Eloquent\Model;
25
use Symfony\Component\Process\Exception\InvalidArgumentException;
26 5
27
class LaravelQuery implements IQueryProvider
28
{
29 5
    protected $expression;
30 5
    protected $auth;
31 5
    protected $reader;
32 5
    public $queryProviderClassName;
33
    private $verbMap = [];
34
35
    public function __construct(AuthInterface $auth = null)
36
    {
37
        /* MySQLExpressionProvider();*/
38
        $this->expression = new LaravelExpressionProvider(); //PHPExpressionProvider('expression');
39
        $this->queryProviderClassName = get_class($this);
40
        $this->auth = isset($auth) ? $auth : new NullAuthProvider();
41
        $this->reader = new LaravelReadQuery($this->auth);
42
        $this->verbMap['create'] = ActionVerb::CREATE();
43
        $this->verbMap['update'] = ActionVerb::UPDATE();
44
        $this->verbMap['delete'] = ActionVerb::DELETE();
45
    }
46
47
    /**
48
     * Indicates if the QueryProvider can handle ordered paging, this means respecting order, skip, and top parameters
49
     * If the query provider can not handle ordered paging, it must return the entire result set and POData will
50
     * perform the ordering and paging
51
     *
52
     * @return Boolean True if the query provider can handle ordered paging, false if POData should perform the paging
53
     */
54
    public function handlesOrderedPaging()
55
    {
56
        return true;
57
    }
58
59
    /**
60
     * Gets the expression provider used by to compile OData expressions into expression used by this query provider.
61
     *
62
     * @return \POData\Providers\Expression\IExpressionProvider
63
     */
64
    public function getExpressionProvider()
65
    {
66
        return $this->expression;
67
    }
68
69
    /**
70 3
     * Gets the LaravelReadQuery instance used to handle read queries (repetitious, nyet?)
71
     *
72
     * @return LaravelReadQuery
73
     */
74
    public function getReader()
75
    {
76
        return $this->reader;
77
    }
78
79 3
    /**
80
     * Gets collection of entities belongs to an entity set
81
     * IE: http://host/EntitySet
82 3
     *  http://host/EntitySet?$skip=10&$top=5&filter=Prop gt Value
83 1
     *
84 1
     * @param QueryType                $queryType   Is this is a query for a count, entities, or entities-with-count
85
     * @param ResourceSet              $resourceSet The entity set containing the entities to fetch
86 3
     * @param FilterInfo|null          $filterInfo  The $filter parameter of the OData query.  NULL if none specified
87 3
     * @param null|InternalOrderByInfo $orderBy     sorted order if we want to get the data in some specific order
88 3
     * @param integer|null             $top         number of records which need to be retrieved
89
     * @param integer|null             $skip        number of records which need to be skipped
90 3
     * @param SkipTokenInfo|null       $skipToken   value indicating what records to skip
91
     * @param Model|Relation|null       $sourceEntityInstance Starting point of query
92
     *
93
     * @return QueryResult
94
     */
95 View Code Duplication
    public function getResourceSet(
1 ignored issue
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...
96
        QueryType $queryType,
97 1
        ResourceSet $resourceSet,
98
        $filterInfo = null,
99
        $orderBy = null,
100 3
        $top = null,
101 1
        $skip = null,
102 1
        $skipToken = null,
103 3
        $sourceEntityInstance = null
104 1
    ) {
105 1
        $source = $this->unpackSourceEntity($sourceEntityInstance);
106
        return $this->getReader()->getResourceSet(
107 3
            $queryType,
108
            $resourceSet,
109 3
            $filterInfo,
110
            $orderBy,
111
            $top,
112
            $skip,
113
            $skipToken,
114
            $source
115
        );
116
    }
117 3
    /**
118 1
     * Gets an entity instance from an entity set identified by a key
119 1
     * IE: http://host/EntitySet(1L)
120 1
     * http://host/EntitySet(KeyA=2L,KeyB='someValue')
121 1
     *
122 1
     * @param ResourceSet           $resourceSet    The entity set containing the entity to fetch
123 3
     * @param KeyDescriptor|null    $keyDescriptor  The key identifying the entity to fetch
124 3
     *
125
     * @return Model|null Returns entity instance if found else null
126
     */
127 3
    public function getResourceFromResourceSet(
128 3
        ResourceSet $resourceSet,
129 3
        KeyDescriptor $keyDescriptor = null
130
    ) {
131
        return $this->getReader()->getResourceFromResourceSet($resourceSet, $keyDescriptor);
132
    }
133
134
    /**
135
     * Get related resource set for a resource
136
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection
137
     * http://host/EntitySet?$expand=NavigationPropertyToCollection
138
     *
139
     * @param QueryType          $queryType            Is this is a query for a count, entities, or entities-with-count
140
     * @param ResourceSet        $sourceResourceSet    The entity set containing the source entity
141
     * @param object             $sourceEntityInstance The source entity instance
142
     * @param ResourceSet        $targetResourceSet    The resource set pointed to by the navigation property
143
     * @param ResourceProperty   $targetProperty       The navigation property to retrieve
144
     * @param FilterInfo|null    $filter               The $filter parameter of the OData query.  NULL if none specified
145
     * @param mixed|null         $orderBy              sorted order if we want to get the data in some specific order
146
     * @param integer|null       $top                  number of records which need to be retrieved
147
     * @param integer|null       $skip                 number of records which need to be skipped
148
     * @param SkipTokenInfo|null $skipToken            value indicating what records to skip
149
     *
150
     * @return QueryResult
151
     *
152
     */
153 View Code Duplication
    public function getRelatedResourceSet(
1 ignored issue
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...
154
        QueryType $queryType,
155
        ResourceSet $sourceResourceSet,
156
        $sourceEntityInstance,
157
        ResourceSet $targetResourceSet,
158
        ResourceProperty $targetProperty,
159
        FilterInfo $filter = null,
160
        $orderBy = null,
161
        $top = null,
162
        $skip = null,
163
        $skipToken = null
164
    ) {
165
        $source = $this->unpackSourceEntity($sourceEntityInstance);
166
        return $this->getReader()->getRelatedResourceSet(
167
            $queryType,
168
            $sourceResourceSet,
169
            $source,
170
            $targetResourceSet,
171
            $targetProperty,
172
            $filter,
173
            $orderBy,
174
            $top,
175
            $skip,
176
            $skipToken
177
        );
178
    }
179
180
    /**
181
     * Gets a related entity instance from an entity set identified by a key
182
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33)
183
     *
184
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
185
     * @param object $sourceEntityInstance The source entity instance.
186
     * @param ResourceSet $targetResourceSet The entity set containing the entity to fetch
187
     * @param ResourceProperty $targetProperty The metadata of the target property.
188
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch
189
     *
190
     * @return Model|null Returns entity instance if found else null
191
     */
192
    public function getResourceFromRelatedResourceSet(
193
        ResourceSet $sourceResourceSet,
194
        $sourceEntityInstance,
195
        ResourceSet $targetResourceSet,
196
        ResourceProperty $targetProperty,
197
        KeyDescriptor $keyDescriptor
198
    ) {
199 3
        $source = $this->unpackSourceEntity($sourceEntityInstance);
200
        return $this->getReader()->getResourceFromRelatedResourceSet(
201
            $sourceResourceSet,
202
            $source,
203
            $targetResourceSet,
204
            $targetProperty,
205
            $keyDescriptor
206
        );
207
    }
208
209
    /**
210 3
     * Get related resource for a resource
211 3
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
212
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity
213 3
     *
214 3
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
215 3
     * @param object $sourceEntityInstance The source entity instance.
216 3
     * @param ResourceSet $targetResourceSet The entity set containing the entity pointed to by the navigation property
217 3
     * @param ResourceProperty $targetProperty The navigation property to fetch
218 3
     *
219 3
     * @return object|null The related resource if found else null
220
     */
221 3
    public function getRelatedResourceReference(
222
        ResourceSet $sourceResourceSet,
223
        $sourceEntityInstance,
224
        ResourceSet $targetResourceSet,
225
        ResourceProperty $targetProperty
226
    ) {
227
        $source = $this->unpackSourceEntity($sourceEntityInstance);
228
229
        $result = $this->getReader()->getRelatedResourceReference(
230
            $sourceResourceSet,
231
            $source,
232
            $targetResourceSet,
233
            $targetProperty
234
        );
235
        return $result;
236
    }
237
238
    /**
239
     * Updates a resource
240
     *
241
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
242
     * @param object           $sourceEntityInstance The source entity instance
243
     * @param KeyDescriptor    $keyDescriptor        The key identifying the entity to fetch
244
     * @param object           $data                 The New data for the entity instance.
245
     * @param bool             $shouldUpdate        Should undefined values be updated or reset to default
246
     *
247
     * @return object|null The new resource value if it is assignable or throw exception for null.
248
     */
249
    public function updateResource(
250
        ResourceSet $sourceResourceSet,
251
        $sourceEntityInstance,
252
        KeyDescriptor $keyDescriptor,
253
        $data,
254
        $shouldUpdate = false
255
    ) {
256
        $source = $this->unpackSourceEntity($sourceEntityInstance);
257
258
        $verb = 'update';
259
        return $this->createUpdateCoreWrapper($sourceResourceSet, $source, $data, $verb);
260
    }
261
    /**
262
     * Delete resource from a resource set.
263
     * @param ResourceSet      $sourceResourceSet
264
     * @param object           $sourceEntityInstance
265
     *
266
     * return bool true if resources sucessfully deteled, otherwise false.
267
     */
268
    public function deleteResource(
269
        ResourceSet $sourceResourceSet,
270
        $sourceEntityInstance
271
    ) {
272
        $source = $this->unpackSourceEntity($sourceEntityInstance);
273
274
        $verb = 'delete';
275
        if (!($source instanceof Model)) {
276
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
277
        }
278
279
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->getName();
280
        $id = $source->getKey();
281
        $name = $source->getKeyName();
282
        $data = [$name => $id];
283
284
        $data = $this->createUpdateDeleteCore($source, $data, $class, $verb);
285
286
        $success = isset($data['id']);
287
        if ($success) {
288
            return true;
289
        }
290
        throw new ODataException('Target model not successfully deleted', 422);
291
    }
292 1
    /**
293
     * @param ResourceSet      $resourceSet   The entity set containing the entity to fetch
294
     * @param object           $sourceEntityInstance The source entity instance
295
     * @param object           $data                 The New data for the entity instance.
296
     *
297
     * returns object|null returns the newly created model if sucessful or null if model creation failed.
298
     */
299 1
    public function createResourceforResourceSet(
300 1
        ResourceSet $resourceSet,
301
        $sourceEntityInstance,
302 1
        $data
303
    ) {
304 1
        $source = $this->unpackSourceEntity($sourceEntityInstance);
305
306 1
        $verb = 'create';
307
        return $this->createUpdateCoreWrapper($resourceSet, $source, $data, $verb);
308
    }
309 1
310
    /**
311
     * @param $sourceEntityInstance
312
     * @param $data
313
     * @param $class
314
     * @param string $verb
315
     * @return array|mixed
316
     * @throws ODataException
317
     * @throws InvalidOperationException
318 1
     */
319
    private function createUpdateDeleteCore($sourceEntityInstance, $data, $class, $verb)
320
    {
321
        $raw = App::make('metadataControllers');
322 1
        $map = $raw->getMetadata();
323 1
324 1
        if (!array_key_exists($class, $map)) {
325 1
            throw new \POData\Common\InvalidOperationException('Controller mapping missing for class '.$class.'.');
326 1
        }
327
        $goal = $raw->getMapping($class, $verb);
328 1
        if (null == $goal) {
329
            throw new \POData\Common\InvalidOperationException(
330 1
                'Controller mapping missing for '.$verb.' verb on class '.$class.'.'
331 1
            );
332
        }
333
334 1
        assert(null !== $data, 'Data must not be null');
335
        if (is_object($data)) {
336
            $arrayData = (array) $data;
337
        } else {
338
            $arrayData = $data;
339
        }
340
        if (!is_array($arrayData)) {
341
            throw \POData\Common\ODataException::createPreConditionFailedError(
342
                'Data not resolvable to key-value array.'
343 1
            );
344
        }
345
346
        $controlClass = $goal['controller'];
347
        $method = $goal['method'];
348 1
        $paramList = $goal['parameters'];
349 1
        $controller = App::make($controlClass);
350
        $parms = $this->createUpdateDeleteProcessInput($sourceEntityInstance, $arrayData, $paramList);
351 1
        unset($data);
352
353 1
        $result = call_user_func_array(array($controller, $method), $parms);
354
355 1
        return $this->createUpdateDeleteProcessOutput($result);
356
    }
357
358 1
    /**
359
     * Puts an entity instance to entity set identified by a key.
360
     *
361
     * @param ResourceSet $resourceSet The entity set containing the entity to update
362
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to update
363
     * @param $data
364
     *
365
     * @return bool|null Returns result of executing query
366
     */
367
    public function putResource(
368
        ResourceSet $resourceSet,
369
        KeyDescriptor $keyDescriptor,
370 3
        $data
371
    ) {
372 3
        // TODO: Implement putResource() method.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
373 3
        return true;
374
    }
375 3
376
    /**
377
     * @param ResourceSet $sourceResourceSet
378 3
     * @param $source
379 3
     * @param $data
380
     * @param $verb
381
     * @return mixed
382
     * @throws ODataException
383
     * @throws \POData\Common\InvalidOperationException
384
     */
385 3
    private function createUpdateCoreWrapper(ResourceSet $sourceResourceSet, Model $source = null, $data, $verb)
0 ignored issues
show
Coding Style introduced by
Parameters which have default values should be placed at the end.

If you place a parameter with a default value before a parameter with a default value, the default value of the first parameter will never be used as it will always need to be passed anyway:

// $a must always be passed; it's default value is never used.
function someFunction($a = 5, $b) { }
Loading history...
386
    {
387
        $lastWord = 'update' == $verb ? 'updated' : 'created';
388 3
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->getName();
389 2
        if (!$this->auth->canAuth($this->verbMap[$verb], $class, $source)) {
390 2
            throw new ODataException('Access denied', 403);
391 3
        }
392
393
        $data = $this->createUpdateDeleteCore($source, $data, $class, $verb);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $data. This often makes code more readable.
Loading history...
394
395
        $success = isset($data['id']);
396
397 3
        if ($success) {
398 3
            try {
399 3
                return $class::findOrFail($data['id']);
400 3
            } catch (\Exception $e) {
401 3
                throw new ODataException($e->getMessage(), 500);
402
            }
403 3
        }
404 3
        throw new ODataException('Target model not successfully '.$lastWord, 422);
405 3
    }
406 3
407 2
    /**
408 2
     * @param $sourceEntityInstance
409 2
     * @param $data
410 2
     * @param $paramList
411 2
     * @return array
412
     */
413
    private function createUpdateDeleteProcessInput(Model $sourceEntityInstance, $data, $paramList)
414
    {
415 2
        $parms = [];
416
417
        foreach ($paramList as $spec) {
418 3
            $varType = isset($spec['type']) ? $spec['type'] : null;
419 3
            $varName = $spec['name'];
420
            if (null == $varType) {
421 3
                $parms[] = ('id' == $varName) ? $sourceEntityInstance->getKey() : $sourceEntityInstance->$varName;
422
                continue;
423 3
            }
424
            // TODO: Give this smarts and actively pick up instantiation details
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
425
            $var = new $varType();
426 3
            if ($spec['isRequest']) {
427 3
                $var->setMethod('POST');
428 3
                $var->request = new \Symfony\Component\HttpFoundation\ParameterBag($data);
429 3
            }
430
            $parms[] = $var;
431 3
        }
432
        return $parms;
433
    }
434 3
435
    /**
436
     * @param $result
437
     * @return array|mixed
438
     * @throws ODataException
439 3
     */
440
    private function createUpdateDeleteProcessOutput($result)
441
    {
442
        if (!($result instanceof \Illuminate\Http\JsonResponse)) {
443
            throw ODataException::createInternalServerError('Controller response not well-formed json.');
444
        }
445
        $outData = $result->getData();
446
        if (is_object($outData)) {
447
            $outData = (array)$outData;
448
        }
449
450
        if (!is_array($outData)) {
451
            throw ODataException::createInternalServerError('Controller response does not have an array.');
452
        }
453
        if (!(key_exists('id', $outData) && key_exists('status', $outData) && key_exists('errors', $outData))) {
454
            throw ODataException::createInternalServerError(
455
                'Controller response array missing at least one of id, status and/or errors fields.'
456
            );
457
        }
458
        return $outData;
459
    }
460
461
    /**
462
     * @param $sourceEntityInstance
463
     * @return mixed|null|\object[]
464
     */
465
    private function unpackSourceEntity($sourceEntityInstance)
466
    {
467
        if ($sourceEntityInstance instanceof QueryResult) {
468
            $sourceEntityInstance = $sourceEntityInstance->results;
469
            $sourceEntityInstance = (is_array($sourceEntityInstance))
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $sourceEntityInstance. This often makes code more readable.
Loading history...
470
                ? $sourceEntityInstance[0] : $sourceEntityInstance;
471
        }
472
        return $sourceEntityInstance;
473
    }
474
}
475