Completed
Pull Request — master (#126)
by Alex
01:49
created

LaravelQuery   F

Complexity

Total Complexity 88

Size/Duplication

Total Lines 809
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 20

Test Coverage

Coverage 60.47%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 88
c 3
b 0
f 0
lcom 2
cbo 20
dl 0
loc 809
ccs 104
cts 172
cp 0.6047
rs 1.263

31 Methods

Rating   Name   Duplication   Size   Complexity  
A getResourceFromRelatedResourceSet() 0 16 1
A getRelatedResourceReference() 0 16 1
A updateResource() 0 12 1
A __construct() 0 13 2
A handlesOrderedPaging() 0 4 1
A getExpressionProvider() 0 4 1
A getReader() 0 4 1
A getMetadataProvider() 0 4 1
A getControllerContainer() 0 5 1
B getResourceSet() 0 24 1
A getResourceFromResourceSet() 0 7 1
B getRelatedResourceSet() 0 26 1
B deleteResource() 0 24 3
A createResourceforResourceSet() 0 10 1
B createUpdateDeleteCore() 0 38 5
A putResource() 0 8 1
B createUpdateCoreWrapper() 0 21 5
B createUpdateDeleteProcessInput() 0 21 6
B createUpdateDeleteProcessOutput() 0 20 7
A unpackSourceEntity() 0 9 3
B createBulkResourceforResourceSet() 0 30 5
B updateBulkResource() 0 40 6
B hookSingleModel() 0 24 5
B unhookSingleModel() 0 39 6
B isModelHookInputsOk() 0 18 5
A getOptionalVerbMapping() 0 8 1
D prepareBulkRequestInput() 0 33 9
B processBulkCustom() 0 31 4
A startTransaction() 0 4 1
A commitTransaction() 0 4 1
A rollBackTransaction() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like LaravelQuery often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use LaravelQuery, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace AlgoWeb\PODataLaravel\Query;
4
5
use AlgoWeb\PODataLaravel\Auth\NullAuthProvider;
6
use AlgoWeb\PODataLaravel\Controllers\MetadataControllerContainer;
7
use AlgoWeb\PODataLaravel\Enums\ActionVerb;
8
use AlgoWeb\PODataLaravel\Interfaces\AuthInterface;
9
use AlgoWeb\PODataLaravel\Providers\MetadataProvider;
10
use Illuminate\Database\Eloquent\Model;
11
use Illuminate\Database\Eloquent\Relations\BelongsTo;
12
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
13
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
14
use Illuminate\Database\Eloquent\Relations\Relation;
15
use Illuminate\Support\Facades\App;
16
use Illuminate\Support\Facades\DB;
17
use POData\Common\InvalidOperationException;
18
use POData\Common\ODataException;
19
use POData\Providers\Metadata\ResourceProperty;
20
use POData\Providers\Metadata\ResourceSet;
21
use POData\Providers\Query\IQueryProvider;
22
use POData\Providers\Query\QueryResult;
23
use POData\Providers\Query\QueryType;
24
use POData\UriProcessor\QueryProcessor\ExpressionParser\FilterInfo;
25
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
26 5
use POData\UriProcessor\QueryProcessor\SkipTokenParser\SkipTokenInfo;
27
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
28
use Symfony\Component\Process\Exception\InvalidArgumentException;
29 5
30 5
class LaravelQuery implements IQueryProvider
31 5
{
32 5
    protected $expression;
33
    protected $auth;
34
    protected $reader;
35
    public $queryProviderClassName;
36
    private $verbMap = [];
37
    protected $metadataProvider;
38
    protected $controllerContainer;
39
40
    public function __construct(AuthInterface $auth = null)
41
    {
42
        /* MySQLExpressionProvider();*/
43
        $this->expression = new LaravelExpressionProvider(); //PHPExpressionProvider('expression');
44
        $this->queryProviderClassName = get_class($this);
45
        $this->auth = isset($auth) ? $auth : new NullAuthProvider();
46
        $this->reader = new LaravelReadQuery($this->auth);
47
        $this->verbMap['create'] = ActionVerb::CREATE();
48
        $this->verbMap['update'] = ActionVerb::UPDATE();
49
        $this->verbMap['delete'] = ActionVerb::DELETE();
50
        $this->metadataProvider = new MetadataProvider(App::make('app'));
51
        $this->controllerContainer = App::make('metadataControllers');
52
    }
53
54
    /**
55
     * Indicates if the QueryProvider can handle ordered paging, this means respecting order, skip, and top parameters
56
     * If the query provider can not handle ordered paging, it must return the entire result set and POData will
57
     * perform the ordering and paging.
58
     *
59
     * @return Boolean True if the query provider can handle ordered paging, false if POData should perform the paging
60
     */
61
    public function handlesOrderedPaging()
62
    {
63
        return true;
64
    }
65
66
    /**
67
     * Gets the expression provider used by to compile OData expressions into expression used by this query provider.
68
     *
69
     * @return \POData\Providers\Expression\IExpressionProvider
70 3
     */
71
    public function getExpressionProvider()
72
    {
73
        return $this->expression;
74
    }
75
76
    /**
77
     * Gets the LaravelReadQuery instance used to handle read queries (repetitious, nyet?).
78
     *
79 3
     * @return LaravelReadQuery
80
     */
81
    public function getReader()
82 3
    {
83 1
        return $this->reader;
84 1
    }
85
86 3
    /**
87 3
     * Dig out local copy of POData-Laravel metadata provider.
88 3
     *
89
     * @return MetadataProvider
90 3
     */
91
    public function getMetadataProvider()
92
    {
93
        return $this->metadataProvider;
94
    }
95
96
    /**
97 1
     * Dig out local copy of controller metadata mapping.
98
     *
99
     * @return MetadataControllerContainer
100 3
     */
101 1
    public function getControllerContainer()
102 1
    {
103 3
        assert(null !== $this->controllerContainer, get_class($this->controllerContainer));
104 1
        return $this->controllerContainer;
105 1
    }
106
107 3
    /**
108
     * Gets collection of entities belongs to an entity set
109 3
     * IE: http://host/EntitySet
110
     *  http://host/EntitySet?$skip=10&$top=5&filter=Prop gt Value.
111
     *
112
     * @param QueryType                $queryType            Is this is a query for a count, entities,
113
     *                                                       or entities-with-count?
114
     * @param ResourceSet              $resourceSet          The entity set containing the entities to fetch
115
     * @param FilterInfo|null          $filterInfo           The $filter parameter of the OData query.  NULL if absent
116
     * @param null|InternalOrderByInfo $orderBy              sorted order if we want to get the data in some
117 3
     *                                                       specific order
118 1
     * @param int|null                 $top                  number of records which need to be retrieved
119 1
     * @param int|null                 $skip                 number of records which need to be skipped
120 1
     * @param SkipTokenInfo|null       $skipToken            value indicating what records to skip
121 1
     * @param string[]|null            $eagerLoad            array of relations to eager load
122 1
     * @param Model|Relation|null      $sourceEntityInstance Starting point of query
123 3
     *
124 3
     * @return QueryResult
125
     */
126
    public function getResourceSet(
127 3
        QueryType $queryType,
128 3
        ResourceSet $resourceSet,
129 3
        $filterInfo = null,
130
        $orderBy = null,
131
        $top = null,
132
        $skip = null,
133
        $skipToken = null,
134
        array $eagerLoad = null,
135
        $sourceEntityInstance = null
136
    ) {
137
        $source = $this->unpackSourceEntity($sourceEntityInstance);
138
        return $this->getReader()->getResourceSet(
139
            $queryType,
140
            $resourceSet,
141
            $filterInfo,
142
            $orderBy,
143
            $top,
144
            $skip,
145
            $skipToken,
146
            $eagerLoad,
147
            $source
148
        );
149
    }
150
    /**
151
     * Gets an entity instance from an entity set identified by a key
152
     * IE: http://host/EntitySet(1L)
153
     * http://host/EntitySet(KeyA=2L,KeyB='someValue').
154
     *
155
     * @param ResourceSet        $resourceSet   The entity set containing the entity to fetch
156
     * @param KeyDescriptor|null $keyDescriptor The key identifying the entity to fetch
157
     * @param string[]|null      $eagerLoad     array of relations to eager load
158
     *
159
     * @return Model|null Returns entity instance if found else null
160
     */
161
    public function getResourceFromResourceSet(
162
        ResourceSet $resourceSet,
163
        KeyDescriptor $keyDescriptor = null,
164
        array $eagerLoad = null
165
    ) {
166
        return $this->getReader()->getResourceFromResourceSet($resourceSet, $keyDescriptor, $eagerLoad);
167
    }
168
169
    /**
170
     * Get related resource set for a resource
171
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection
172
     * http://host/EntitySet?$expand=NavigationPropertyToCollection.
173
     *
174
     * @param QueryType          $queryType            Is this is a query for a count, entities, or entities-with-count
175
     * @param ResourceSet        $sourceResourceSet    The entity set containing the source entity
176
     * @param object             $sourceEntityInstance The source entity instance
177
     * @param ResourceSet        $targetResourceSet    The resource set pointed to by the navigation property
178
     * @param ResourceProperty   $targetProperty       The navigation property to retrieve
179
     * @param FilterInfo|null    $filter               The $filter parameter of the OData query.  NULL if none specified
180
     * @param mixed|null         $orderBy              sorted order if we want to get the data in some specific order
181
     * @param int|null           $top                  number of records which need to be retrieved
182
     * @param int|null           $skip                 number of records which need to be skipped
183
     * @param SkipTokenInfo|null $skipToken            value indicating what records to skip
184
     *
185
     * @return QueryResult
186
     */
187
    public function getRelatedResourceSet(
188
        QueryType $queryType,
189
        ResourceSet $sourceResourceSet,
190
        $sourceEntityInstance,
191
        ResourceSet $targetResourceSet,
192
        ResourceProperty $targetProperty,
193
        FilterInfo $filter = null,
194
        $orderBy = null,
195
        $top = null,
196
        $skip = null,
197
        $skipToken = null
198
    ) {
199 3
        $source = $this->unpackSourceEntity($sourceEntityInstance);
200
        return $this->getReader()->getRelatedResourceSet(
201
            $queryType,
202
            $sourceResourceSet,
203
            $source,
204
            $targetResourceSet,
205
            $targetProperty,
206
            $filter,
207
            $orderBy,
208
            $top,
209
            $skip,
210 3
            $skipToken
211 3
        );
212
    }
213 3
214 3
    /**
215 3
     * Gets a related entity instance from an entity set identified by a key
216 3
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33).
217 3
     *
218 3
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
219 3
     * @param object           $sourceEntityInstance the source entity instance
220
     * @param ResourceSet      $targetResourceSet    The entity set containing the entity to fetch
221 3
     * @param ResourceProperty $targetProperty       the metadata of the target property
222
     * @param KeyDescriptor    $keyDescriptor        The key identifying the entity to fetch
223
     *
224
     * @return Model|null Returns entity instance if found else null
225
     */
226
    public function getResourceFromRelatedResourceSet(
227
        ResourceSet $sourceResourceSet,
228
        $sourceEntityInstance,
229
        ResourceSet $targetResourceSet,
230
        ResourceProperty $targetProperty,
231
        KeyDescriptor $keyDescriptor
232
    ) {
233
        $source = $this->unpackSourceEntity($sourceEntityInstance);
234
        return $this->getReader()->getResourceFromRelatedResourceSet(
235
            $sourceResourceSet,
236
            $source,
237
            $targetResourceSet,
238
            $targetProperty,
239
            $keyDescriptor
240
        );
241
    }
242
243
    /**
244
     * Get related resource for a resource
245
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
246
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity.
247
     *
248
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
249
     * @param object           $sourceEntityInstance the source entity instance
250
     * @param ResourceSet      $targetResourceSet    The entity set containing the entity pointed to by the nav property
251
     * @param ResourceProperty $targetProperty       The navigation property to fetch
252
     *
253
     * @return object|null The related resource if found else null
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use null|Model.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
254
     */
255
    public function getRelatedResourceReference(
256
        ResourceSet $sourceResourceSet,
257
        $sourceEntityInstance,
258
        ResourceSet $targetResourceSet,
259
        ResourceProperty $targetProperty
260
    ) {
261
        $source = $this->unpackSourceEntity($sourceEntityInstance);
262
263
        $result = $this->getReader()->getRelatedResourceReference(
264
            $sourceResourceSet,
265
            $source,
266
            $targetResourceSet,
267
            $targetProperty
268
        );
269
        return $result;
270
    }
271
272
    /**
273
     * Updates a resource.
274
     *
275
     * @param ResourceSet   $sourceResourceSet    The entity set containing the source entity
276
     * @param object        $sourceEntityInstance The source entity instance
277
     * @param KeyDescriptor $keyDescriptor        The key identifying the entity to fetch
278
     * @param object        $data                 the New data for the entity instance
279
     * @param bool          $shouldUpdate         Should undefined values be updated or reset to default
280
     *
281
     * @return object|null the new resource value if it is assignable or throw exception for null
282
     */
283
    public function updateResource(
284
        ResourceSet $sourceResourceSet,
285
        $sourceEntityInstance,
286
        KeyDescriptor $keyDescriptor,
287
        $data,
288
        $shouldUpdate = false
289
    ) {
290
        $source = $this->unpackSourceEntity($sourceEntityInstance);
291
292 1
        $verb = 'update';
293
        return $this->createUpdateCoreWrapper($sourceResourceSet, $data, $verb, $source);
294
    }
295
    /**
296
     * Delete resource from a resource set.
297
     *
298
     * @param ResourceSet $sourceResourceSet
299 1
     * @param object      $sourceEntityInstance
300 1
     *
301
     * return bool true if resources sucessfully deteled, otherwise false
302 1
     */
303
    public function deleteResource(
304 1
        ResourceSet $sourceResourceSet,
305
        $sourceEntityInstance
306 1
    ) {
307
        $source = $this->unpackSourceEntity($sourceEntityInstance);
308
309 1
        $verb = 'delete';
310
        if (!($source instanceof Model)) {
311
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
312
        }
313
314
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->getName();
315
        $id = $source->getKey();
316
        $name = $source->getKeyName();
317
        $data = [$name => $id];
318 1
319
        $data = $this->createUpdateDeleteCore($source, $data, $class, $verb);
320
321
        $success = isset($data['id']);
322 1
        if ($success) {
323 1
            return true;
324 1
        }
325 1
        throw new ODataException('Target model not successfully deleted', 422);
326 1
    }
327
    /**
328 1
     * @param ResourceSet $resourceSet          The entity set containing the entity to fetch
329
     * @param object      $sourceEntityInstance The source entity instance
330 1
     * @param object      $data                 the New data for the entity instance
331 1
     *
332
     * @returns object|null                     returns the newly created model if successful,
333
     *                                          or null if model creation failed.
334 1
     */
335
    public function createResourceforResourceSet(
336
        ResourceSet $resourceSet,
337
        $sourceEntityInstance,
338
        $data
339
    ) {
340
        $source = $this->unpackSourceEntity($sourceEntityInstance);
341
342
        $verb = 'create';
343 1
        return $this->createUpdateCoreWrapper($resourceSet, $data, $verb, $source);
344
    }
345
346
    /**
347
     * @param $sourceEntityInstance
348 1
     * @param $data
349 1
     * @param $class
350
     * @param string $verb
351 1
     *
352
     * @throws ODataException
353 1
     * @throws InvalidOperationException
354
     * @return array|mixed
355 1
     */
356
    private function createUpdateDeleteCore($sourceEntityInstance, $data, $class, $verb)
357
    {
358 1
        $raw = App::make('metadataControllers');
359
        $map = $raw->getMetadata();
360
361
        if (!array_key_exists($class, $map)) {
362
            throw new \POData\Common\InvalidOperationException('Controller mapping missing for class ' . $class . '.');
363
        }
364
        $goal = $raw->getMapping($class, $verb);
365
        if (null == $goal) {
366
            throw new \POData\Common\InvalidOperationException(
367
                'Controller mapping missing for ' . $verb . ' verb on class ' . $class . '.'
368
            );
369
        }
370 3
371
        assert(null !== $data, 'Data must not be null');
372 3
        if (is_object($data)) {
373 3
            $arrayData = (array) $data;
374
        } else {
375 3
            $arrayData = $data;
376
        }
377
        if (!is_array($arrayData)) {
378 3
            throw \POData\Common\ODataException::createPreConditionFailedError(
379 3
                'Data not resolvable to key-value array.'
380
            );
381
        }
382
383
        $controlClass = $goal['controller'];
384
        $method = $goal['method'];
385 3
        $paramList = $goal['parameters'];
386
        $controller = App::make($controlClass);
387
        $parms = $this->createUpdateDeleteProcessInput($arrayData, $paramList, $sourceEntityInstance);
388 3
        unset($data);
389 2
390 2
        $result = call_user_func_array(array($controller, $method), $parms);
391 3
392
        return $this->createUpdateDeleteProcessOutput($result);
393
    }
394
395
    /**
396
     * Puts an entity instance to entity set identified by a key.
397 3
     *
398 3
     * @param ResourceSet   $resourceSet   The entity set containing the entity to update
399 3
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to update
400 3
     * @param $data
401 3
     *
402
     * @return bool|null Returns result of executing query
403 3
     */
404 3
    public function putResource(
405 3
        ResourceSet $resourceSet,
406 3
        KeyDescriptor $keyDescriptor,
407 2
        $data
408 2
    ) {
409 2
        // 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...
410 2
        return true;
411 2
    }
412
413
    /**
414
     * @param ResourceSet $sourceResourceSet
415 2
     * @param $data
416
     * @param                            $verb
417
     * @param  Model|null                $source
418 3
     * @throws InvalidOperationException
419 3
     * @throws ODataException
420
     * @return mixed
421 3
     */
422
    private function createUpdateCoreWrapper(ResourceSet $sourceResourceSet, $data, $verb, Model $source = null)
423 3
    {
424
        $lastWord = 'update' == $verb ? 'updated' : 'created';
425
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->getName();
426 3
        if (!$this->auth->canAuth($this->verbMap[$verb], $class, $source)) {
427 3
            throw new ODataException('Access denied', 403);
428 3
        }
429 3
430
        $payload = $this->createUpdateDeleteCore($source, $data, $class, $verb);
431 3
432
        $success = isset($payload['id']);
433
434 3
        if ($success) {
435
            try {
436
                return $class::findOrFail($payload['id']);
437
            } catch (\Exception $e) {
438
                throw new ODataException($e->getMessage(), 500);
439 3
            }
440
        }
441
        throw new ODataException('Target model not successfully ' . $lastWord, 422);
442
    }
443
444
    /**
445
     * @param $data
446
     * @param $paramList
447
     * @param  Model|null $sourceEntityInstance
448
     * @return array
449
     */
450
    private function createUpdateDeleteProcessInput($data, $paramList, Model $sourceEntityInstance = null)
451
    {
452
        $parms = [];
453
454
        foreach ($paramList as $spec) {
455
            $varType = isset($spec['type']) ? $spec['type'] : null;
456
            $varName = $spec['name'];
457
            if (null == $varType) {
458
                $parms[] = ('id' == $varName) ? $sourceEntityInstance->getKey() : $sourceEntityInstance->$varName;
0 ignored issues
show
Bug introduced by
It seems like $sourceEntityInstance is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
459
                continue;
460
            }
461
            // 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...
462
            $var = new $varType();
463
            if ($spec['isRequest']) {
464
                $var->setMethod('POST');
465
                $var->request = new \Symfony\Component\HttpFoundation\ParameterBag($data);
466
            }
467
            $parms[] = $var;
468
        }
469
        return $parms;
470
    }
471
472
    /**
473
     * @param $result
474
     * @throws ODataException
475
     * @return array|mixed
476
     */
477
    private function createUpdateDeleteProcessOutput($result)
478
    {
479
        if (!($result instanceof \Illuminate\Http\JsonResponse)) {
480
            throw ODataException::createInternalServerError('Controller response not well-formed json.');
481
        }
482
        $outData = $result->getData();
483
        if (is_object($outData)) {
484
            $outData = (array) $outData;
485
        }
486
487
        if (!is_array($outData)) {
488
            throw ODataException::createInternalServerError('Controller response does not have an array.');
489
        }
490
        if (!(key_exists('id', $outData) && key_exists('status', $outData) && key_exists('errors', $outData))) {
491
            throw ODataException::createInternalServerError(
492
                'Controller response array missing at least one of id, status and/or errors fields.'
493
            );
494
        }
495
        return $outData;
496
    }
497
498
    /**
499
     * @param $sourceEntityInstance
500
     * @return mixed|null|\object[]
501
     */
502
    private function unpackSourceEntity($sourceEntityInstance)
503
    {
504
        if ($sourceEntityInstance instanceof QueryResult) {
505
            $source = $sourceEntityInstance->results;
506
            $source = (is_array($source)) ? $source[0] : $source;
507
            return $source;
508
        }
509
        return $sourceEntityInstance;
510
    }
511
512
    /**
513
     * Create multiple new resources in a resource set.
514
     *
515
     * @param ResourceSet $sourceResourceSet The entity set containing the entity to fetch
516
     * @param object[]    $data              The new data for the entity instance
517
     *
518
     * @return object[] returns the newly created model if successful, or throws an exception if model creation failed
519
     * @throw  \Exception
520
     */
521
    public function createBulkResourceforResourceSet(
522
        ResourceSet $sourceResourceSet,
523
        array $data
524
    ) {
525
        $verbName = 'bulkCreate';
526
        $mapping = $this->getOptionalVerbMapping($sourceResourceSet, $verbName);
527
528
        $result = [];
529
        try {
530
            DB::beginTransaction();
531
            if (null === $mapping) {
532
                foreach ($data as $newItem) {
533
                    $raw = $this->createResourceforResourceSet($sourceResourceSet, null, $newItem);
534
                    if (null === $raw) {
535
                        throw new \Exception('Bulk model creation failed');
536
                    }
537
                    $result[] = $raw;
538
                }
539
            } else {
540
                $keyDescriptor = null;
541
                $pastVerb = 'created';
542
                $result = $this->processBulkCustom($sourceResourceSet, $data, $mapping, $pastVerb, $keyDescriptor);
543
            }
544
            DB::commit();
545
        } catch (\Exception $e) {
546
            DB::rollBack();
547
            throw $e;
548
        }
549
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $result; (array) is incompatible with the return type declared by the interface POData\Providers\Query\I...kResourceforResourceSet of type object[]|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
550
    }
551
552
    /**
553
     * Updates a group of resources in a resource set.
554
     *
555
     * @param ResourceSet     $sourceResourceSet    The entity set containing the source entity
556
     * @param object          $sourceEntityInstance The source entity instance
557
     * @param KeyDescriptor[] $keyDescriptor        The key identifying the entity to fetch
558
     * @param object[]        $data                 The new data for the entity instances
559
     * @param bool            $shouldUpdate         Should undefined values be updated or reset to default
560
     *
561
     * @return object[] the new resource value if it is assignable, or throw exception for null
562
     * @throw  \Exception
563
     */
564
    public function updateBulkResource(
565
        ResourceSet $sourceResourceSet,
566
        $sourceEntityInstance,
567
        array $keyDescriptor,
568
        array $data,
569
        $shouldUpdate = false
570
    ) {
571
        $numKeys = count($keyDescriptor);
572
        if ($numKeys !== count($data)) {
573
            $msg = 'Key descriptor array and data array must be same length';
574
            throw new \InvalidArgumentException($msg);
575
        }
576
        $result = [];
577
578
        $verbName = 'bulkUpdate';
579
        $mapping = $this->getOptionalVerbMapping($sourceResourceSet, $verbName);
580
581
        try {
582
            DB::beginTransaction();
583
            if (null === $mapping) {
584
                for ($i = 0; $i < $numKeys; $i++) {
585
                    $newItem = $data[$i];
586
                    $newKey = $keyDescriptor[$i];
587
                    $raw = $this->updateResource($sourceResourceSet, $sourceEntityInstance, $newKey, $newItem);
588
                    if (null === $raw) {
589
                        throw new \Exception('Bulk model update failed');
590
                    }
591
                    $result[] = $raw;
592
                }
593
            } else {
594
                $pastVerb = 'updated';
595
                $result = $this->processBulkCustom($sourceResourceSet, $data, $mapping, $pastVerb, $keyDescriptor);
596
            }
597
            DB::commit();
598
        } catch (\Exception $e) {
599
            DB::rollBack();
600
            throw $e;
601
        }
602
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $result; (array) is incompatible with the return type declared by the interface POData\Providers\Query\I...der::updateBulkResource of type object[]|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
603
    }
604
605
    /**
606
     * Attaches child model to parent model.
607
     *
608
     * @param ResourceSet $sourceResourceSet
609
     * @param object      $sourceEntityInstance
610
     * @param ResourceSet $targetResourceSet
611
     * @param object      $targetEntityInstance
612
     * @param $navPropName
613
     *
614
     * @return bool
615
     */
616
    public function hookSingleModel(
617
        ResourceSet $sourceResourceSet,
618
        $sourceEntityInstance,
619
        ResourceSet $targetResourceSet,
620
        $targetEntityInstance,
621
        $navPropName
622
    ) {
623
        $relation = $this->isModelHookInputsOk($sourceEntityInstance, $targetEntityInstance, $navPropName);
624
        assert(
625
            $sourceEntityInstance instanceof Model && $targetEntityInstance instanceof Model,
626
            'Both input entities must be Eloquent models'
627
        );
628
        // in case the fake 'PrimaryKey' attribute got set inbound for a polymorphic-affected model, flatten it now
629
        unset($targetEntityInstance->PrimaryKey);
630
631
        if ($relation instanceof BelongsTo) {
632
            $relation->associate($targetEntityInstance);
633
        } elseif ($relation instanceof BelongsToMany) {
634
            $relation->attach($targetEntityInstance);
635
        } elseif ($relation instanceof HasOneOrMany) {
636
            $relation->save($targetEntityInstance);
637
        }
638
        return true;
639
    }
640
641
    /**
642
     * Removes child model from parent model.
643
     *
644
     * @param ResourceSet $sourceResourceSet
645
     * @param object      $sourceEntityInstance
646
     * @param ResourceSet $targetResourceSet
647
     * @param object      $targetEntityInstance
648
     * @param $navPropName
649
     *
650
     * @return bool
651
     */
652
    public function unhookSingleModel(
653
        ResourceSet $sourceResourceSet,
654
        $sourceEntityInstance,
655
        ResourceSet $targetResourceSet,
656
        $targetEntityInstance,
657
        $navPropName
658
    ) {
659
        $relation = $this->isModelHookInputsOk($sourceEntityInstance, $targetEntityInstance, $navPropName);
660
        assert(
661
            $sourceEntityInstance instanceof Model && $targetEntityInstance instanceof Model,
662
            'Both input entities must be Eloquent models'
663
        );
664
        // in case the fake 'PrimaryKey' attribute got set inbound for a polymorphic-affected model, flatten it now
665
        unset($targetEntityInstance->PrimaryKey);
666
667
        if ($relation instanceof BelongsTo) {
668
            $relation->dissociate();
669
        } elseif ($relation instanceof BelongsToMany) {
670
            $relation->detach($targetEntityInstance);
671
        } elseif ($relation instanceof HasOneOrMany) {
672
            // dig up inverse property name, so we can pass it to unhookSingleModel with source and target elements
673
            // swapped
674
            $otherPropName = $this->getMetadataProvider()
675
                ->resolveReverseProperty($sourceEntityInstance, $targetEntityInstance, $navPropName);
676
            if (null === $otherPropName) {
677
                $srcClass = get_class($sourceEntityInstance);
678
                $msg = 'Bad navigation property, ' . $navPropName . ', on source model ' . $srcClass;
679
                throw new \InvalidArgumentException($msg);
680
            }
681
            $this->unhookSingleModel(
682
                $targetResourceSet,
683
                $targetEntityInstance,
684
                $sourceResourceSet,
685
                $sourceEntityInstance,
686
                $otherPropName
687
            );
688
        }
689
        return true;
690
    }
691
692
    /**
693
     * @param $sourceEntityInstance
694
     * @param $targetEntityInstance
695
     * @param $navPropName
696
     * @throws \InvalidArgumentException
697
     * @return Relation
698
     */
699
    protected function isModelHookInputsOk($sourceEntityInstance, $targetEntityInstance, $navPropName)
700
    {
701
        if (!$sourceEntityInstance instanceof Model || !$targetEntityInstance instanceof Model) {
702
            $msg = 'Both source and target must be Eloquent models';
703
            throw new \InvalidArgumentException($msg);
704
        }
705
        $relation = $sourceEntityInstance->$navPropName();
706
        if (!$relation instanceof Relation) {
707
            $msg = 'Navigation property must be an Eloquent relation';
708
            throw new \InvalidArgumentException($msg);
709
        }
710
        $targType = $relation->getRelated();
711
        if (!$targetEntityInstance instanceof $targType) {
712
            $msg = 'Target instance must be of type compatible with relation declared in method ' . $navPropName;
713
            throw new \InvalidArgumentException($msg);
714
        }
715
        return $relation;
716
    }
717
718
    /**
719
     * @param ResourceSet $sourceResourceSet
720
     * @param $verbName
721
     * @return array|null
722
     */
723
    protected function getOptionalVerbMapping(ResourceSet $sourceResourceSet, $verbName)
724
    {
725
        // dig up target class name
726
        $type = $sourceResourceSet->getResourceType()->getInstanceType();
727
        assert($type instanceof \ReflectionClass, get_class($type));
728
        $modelName = $type->getName();
729
        return $this->getControllerContainer()->getMapping($modelName, $verbName);
730
    }
731
732
    /**
733
     * Prepare bulk request from supplied data.  If $keyDescriptors is not null, its elements are assumed to
734
     * correspond 1-1 to those in $data.
735
     *
736
     * @param $paramList
737
     * @param array                $data
738
     * @param KeyDescriptor[]|null $keyDescriptors
739
     */
740
    protected function prepareBulkRequestInput($paramList, array $data, array $keyDescriptors = null)
741
    {
742
        $parms = [];
743
        $isCreate = null === $keyDescriptors;
744
745
        // for moment, we're only processing parameters of type Request
746
        foreach ($paramList as $spec) {
747
            $varType = isset($spec['type']) ? $spec['type'] : null;
748
            if (null !== $varType) {
749
                $var = new $varType();
750
                if ($spec['isRequest']) {
751
                    $var->setMethod($isCreate ? 'POST' : 'PUT');
752
                    $bulkData = ['data' => $data];
753
                    if (null !== $keyDescriptors) {
754
                        $keys = [];
755
                        foreach ($keyDescriptors as $desc) {
756
                            assert($desc instanceof KeyDescriptor, get_class($desc));
757
                            $rawPayload = $desc->getNamedValues();
758
                            $keyPayload = [];
759
                            foreach ($rawPayload as $keyName => $keyVal) {
760
                                $keyPayload[$keyName] = $keyVal[0];
761
                            }
762
                            $keys[] = $keyPayload;
763
                        }
764
                        $bulkData['keys'] = $keys;
765
                    }
766
                    $var->request = new \Symfony\Component\HttpFoundation\ParameterBag($bulkData);
767
                }
768
                $parms[] = $var;
769
            }
770
        }
771
        return $parms;
772
    }
773
774
    /**
775
     * @param ResourceSet $sourceResourceSet
776
     * @param array       $data
777
     * @param $mapping
778
     * @param $pastVerb
779
     * @param  KeyDescriptor[]|null $keyDescriptor
780
     * @throws ODataException
781
     * @return array
782
     */
783
    protected function processBulkCustom(
784
        ResourceSet $sourceResourceSet,
785
        array $data,
786
        $mapping,
787
        $pastVerb,
788
        array $keyDescriptor = null
789
    ) {
790
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->getName();
791
        $controlClass = $mapping['controller'];
792
        $method = $mapping['method'];
793
        $paramList = $mapping['parameters'];
794
        $controller = App::make($controlClass);
795
        $parms = $this->prepareBulkRequestInput($paramList, $data, $keyDescriptor);
796
797
        $callResult = call_user_func_array(array($controller, $method), $parms);
798
        $payload = $this->createUpdateDeleteProcessOutput($callResult);
799
        $success = isset($payload['id']) && is_array($payload['id']);
800
801
        if ($success) {
802
            try {
803
                // return array of Model objects underlying collection returned by findMany
804
                $result = $class::findMany($payload['id'])->flatten()->all();
805
                return $result;
806
            } catch (\Exception $e) {
807
                throw new ODataException($e->getMessage(), 500);
808
            }
809
        } else {
810
            $msg = 'Target models not successfully ' . $pastVerb;
811
            throw new ODataException($msg, 422);
812
        }
813
    }
814
815
    /**
816
     * Start database transaction.
817
     */
818
    public function startTransaction()
819
    {
820
        DB::beginTransaction();
821
    }
822
823
    /**
824
     * Commit database transaction.
825
     */
826
    public function commitTransaction()
827
    {
828
        DB::commit();
829
    }
830
831
    /**
832
     * Abort database transaction.
833
     */
834
    public function rollBackTransaction()
835
    {
836
        DB::rollBack();
837
    }
838
}
839