Test Failed
Push — master ( 59872a...100324 )
by Alex
04:04
created

LaravelQuery::createUpdateCoreWrapper()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5.3256

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 21
ccs 13
cts 17
cp 0.7647
rs 8.7624
cc 5
eloc 13
nc 8
nop 4
crap 5.3256
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(
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(
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, $data, $verb, $source);
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, $data, $verb, $source);
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 $data
379 3
     * @param $verb
380
     * @param Model|null $source
381
     * @return mixed
382
     * @throws InvalidOperationException
383
     * @throws ODataException
384
     */
385 3
    private function createUpdateCoreWrapper(ResourceSet $sourceResourceSet, $data, $verb, Model $source = null)
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
        $payload = $this->createUpdateDeleteCore($source, $data, $class, $verb);
394
395
        $success = isset($payload['id']);
396
397 3
        if ($success) {
398 3
            try {
399 3
                return $class::findOrFail($payload['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
            $source = $sourceEntityInstance->results;
469
            $source = (is_array($source)) ? $source[0] : $source;
470
            return $source;
471
        }
472
        return $sourceEntityInstance;
473
    }
474
}
475