Test Failed
Push — master ( 402d2b...dc29cf )
by Alex
03:08
created

LaravelQuery::rollBackTransaction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
ccs 0
cts 0
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 2
1
<?php
2
3
namespace AlgoWeb\PODataLaravel\Query;
4
5
use AlgoWeb\PODataLaravel\Controllers\MetadataControllerContainer;
6
use AlgoWeb\PODataLaravel\Enums\ActionVerb;
7
use AlgoWeb\PODataLaravel\Models\TestModel;
8
use AlgoWeb\PODataLaravel\Providers\MetadataProvider;
9
use Illuminate\Database\Eloquent\Relations\BelongsTo;
10
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
11
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
12
use Illuminate\Database\Eloquent\Relations\MorphOneOrMany;
13
use Illuminate\Database\Eloquent\Relations\MorphTo;
14
use Illuminate\Database\Eloquent\Relations\MorphToMany;
15
use Illuminate\Database\Eloquent\Relations\Relation;
16
use Illuminate\Support\Facades\DB;
17
use POData\Common\InvalidOperationException;
18
use POData\Providers\Metadata\ResourceProperty;
19
use POData\Providers\Metadata\ResourceSet;
20
use POData\UriProcessor\QueryProcessor\ExpressionParser\FilterInfo;
21
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
22
use POData\UriProcessor\QueryProcessor\SkipTokenParser\SkipTokenInfo;
23
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
24
use POData\Providers\Query\IQueryProvider;
25
use POData\Providers\Expression\MySQLExpressionProvider;
26 5
use POData\Providers\Query\QueryType;
27
use POData\Providers\Query\QueryResult;
28
use POData\Providers\Expression\PHPExpressionProvider;
29 5
use \POData\Common\ODataException;
30 5
use AlgoWeb\PODataLaravel\Interfaces\AuthInterface;
31 5
use AlgoWeb\PODataLaravel\Auth\NullAuthProvider;
32 5
use Illuminate\Support\Facades\App;
33
use Illuminate\Database\Eloquent\Model;
34
use Symfony\Component\Process\Exception\InvalidArgumentException;
35
36
class LaravelQuery implements IQueryProvider
37
{
38
    protected $expression;
39
    protected $auth;
40
    protected $reader;
41
    public $queryProviderClassName;
42
    private $verbMap = [];
43
    protected $metadataProvider;
44
    protected $controllerContainer;
45
46
    public function __construct(AuthInterface $auth = null)
47
    {
48
        /* MySQLExpressionProvider();*/
49
        $this->expression = new LaravelExpressionProvider(); //PHPExpressionProvider('expression');
50
        $this->queryProviderClassName = get_class($this);
51
        $this->auth = isset($auth) ? $auth : new NullAuthProvider();
52
        $this->reader = new LaravelReadQuery($this->auth);
53
        $this->verbMap['create'] = ActionVerb::CREATE();
54
        $this->verbMap['update'] = ActionVerb::UPDATE();
55
        $this->verbMap['delete'] = ActionVerb::DELETE();
56
        $this->metadataProvider = new MetadataProvider(App::make('app'));
57
        $this->controllerContainer = App::make('metadataControllers');
58
    }
59
60
    /**
61
     * Indicates if the QueryProvider can handle ordered paging, this means respecting order, skip, and top parameters
62
     * If the query provider can not handle ordered paging, it must return the entire result set and POData will
63
     * perform the ordering and paging
64
     *
65
     * @return Boolean True if the query provider can handle ordered paging, false if POData should perform the paging
66
     */
67
    public function handlesOrderedPaging()
68
    {
69
        return true;
70 3
    }
71
72
    /**
73
     * Gets the expression provider used by to compile OData expressions into expression used by this query provider.
74
     *
75
     * @return \POData\Providers\Expression\IExpressionProvider
76
     */
77
    public function getExpressionProvider()
78
    {
79 3
        return $this->expression;
80
    }
81
82 3
    /**
83 1
     * Gets the LaravelReadQuery instance used to handle read queries (repetitious, nyet?)
84 1
     *
85
     * @return LaravelReadQuery
86 3
     */
87 3
    public function getReader()
88 3
    {
89
        return $this->reader;
90 3
    }
91
92
    /**
93
     * Dig out local copy of POData-Laravel metadata provider
94
     *
95
     * @return MetadataProvider
96
     */
97 1
    public function getMetadataProvider()
98
    {
99
        return $this->metadataProvider;
100 3
    }
101 1
102 1
    /**
103 3
     * Dig out local copy of controller metadata mapping
104 1
     *
105 1
     * @return MetadataControllerContainer
106
     */
107 3
    public function getControllerContainer()
108
    {
109 3
        assert(null !== $this->controllerContainer, get_class($this->controllerContainer));
110
        return $this->controllerContainer;
111
    }
112
113
    /**
114
     * Gets collection of entities belongs to an entity set
115
     * IE: http://host/EntitySet
116
     *  http://host/EntitySet?$skip=10&$top=5&filter=Prop gt Value
117 3
     *
118 1
     * @param QueryType                $queryType   Is this is a query for a count, entities, or entities-with-count
119 1
     * @param ResourceSet              $resourceSet The entity set containing the entities to fetch
120 1
     * @param FilterInfo|null          $filterInfo  The $filter parameter of the OData query.  NULL if none specified
121 1
     * @param null|InternalOrderByInfo $orderBy     sorted order if we want to get the data in some specific order
122 1
     * @param integer|null             $top         number of records which need to be retrieved
123 3
     * @param integer|null             $skip        number of records which need to be skipped
124 3
     * @param SkipTokenInfo|null       $skipToken   value indicating what records to skip
125
     * @param Model|Relation|null       $sourceEntityInstance Starting point of query
126
     *
127 3
     * @return QueryResult
128 3
     */
129 3 View Code Duplication
    public function getResourceSet(
130
        QueryType $queryType,
131
        ResourceSet $resourceSet,
132
        $filterInfo = null,
133
        $orderBy = null,
134
        $top = null,
135
        $skip = null,
136
        $skipToken = null,
137
        $sourceEntityInstance = null
138
    ) {
139
        $source = $this->unpackSourceEntity($sourceEntityInstance);
140
        return $this->getReader()->getResourceSet(
141
            $queryType,
142
            $resourceSet,
143
            $filterInfo,
144
            $orderBy,
145
            $top,
146
            $skip,
147
            $skipToken,
148
            $source
149
        );
150
    }
151
    /**
152
     * Gets an entity instance from an entity set identified by a key
153
     * IE: http://host/EntitySet(1L)
154
     * http://host/EntitySet(KeyA=2L,KeyB='someValue')
155
     *
156
     * @param ResourceSet           $resourceSet    The entity set containing the entity to fetch
157
     * @param KeyDescriptor|null    $keyDescriptor  The key identifying the entity to fetch
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
    ) {
165
        return $this->getReader()->getResourceFromResourceSet($resourceSet, $keyDescriptor);
166
    }
167
168
    /**
169
     * Get related resource set for a resource
170
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection
171
     * http://host/EntitySet?$expand=NavigationPropertyToCollection
172
     *
173
     * @param QueryType          $queryType            Is this is a query for a count, entities, or entities-with-count
174
     * @param ResourceSet        $sourceResourceSet    The entity set containing the source entity
175
     * @param object             $sourceEntityInstance The source entity instance
176
     * @param ResourceSet        $targetResourceSet    The resource set pointed to by the navigation property
177
     * @param ResourceProperty   $targetProperty       The navigation property to retrieve
178
     * @param FilterInfo|null    $filter               The $filter parameter of the OData query.  NULL if none specified
179
     * @param mixed|null         $orderBy              sorted order if we want to get the data in some specific order
180
     * @param integer|null       $top                  number of records which need to be retrieved
181
     * @param integer|null       $skip                 number of records which need to be skipped
182
     * @param SkipTokenInfo|null $skipToken            value indicating what records to skip
183
     *
184
     * @return QueryResult
185
     *
186
     */
187 View Code Duplication
    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 navigation property
251
     * @param ResourceProperty $targetProperty The navigation property to fetch
252
     *
253
     * @return object|null The related resource if found else null
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
     * @param ResourceSet      $sourceResourceSet
298
     * @param object           $sourceEntityInstance
299 1
     *
300 1
     * return bool true if resources sucessfully deteled, otherwise false.
301
     */
302 1
    public function deleteResource(
303
        ResourceSet $sourceResourceSet,
304 1
        $sourceEntityInstance
305
    ) {
306 1
        $source = $this->unpackSourceEntity($sourceEntityInstance);
307
308
        $verb = 'delete';
309 1
        if (!($source instanceof Model)) {
310
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
311
        }
312
313
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->getName();
314
        $id = $source->getKey();
315
        $name = $source->getKeyName();
316
        $data = [$name => $id];
317
318 1
        $data = $this->createUpdateDeleteCore($source, $data, $class, $verb);
319
320
        $success = isset($data['id']);
321
        if ($success) {
322 1
            return true;
323 1
        }
324 1
        throw new ODataException('Target model not successfully deleted', 422);
325 1
    }
326 1
    /**
327
     * @param ResourceSet      $resourceSet   The entity set containing the entity to fetch
328 1
     * @param object           $sourceEntityInstance The source entity instance
329
     * @param object           $data                 The New data for the entity instance.
330 1
     *
331 1
     * returns object|null returns the newly created model if sucessful or null if model creation failed.
332
     */
333
    public function createResourceforResourceSet(
334 1
        ResourceSet $resourceSet,
335
        $sourceEntityInstance,
336
        $data
337
    ) {
338
        $source = $this->unpackSourceEntity($sourceEntityInstance);
339
340
        $verb = 'create';
341
        return $this->createUpdateCoreWrapper($resourceSet, $data, $verb, $source);
342
    }
343 1
344
    /**
345
     * @param $sourceEntityInstance
346
     * @param $data
347
     * @param $class
348 1
     * @param string $verb
349 1
     * @return array|mixed
350
     * @throws ODataException
351 1
     * @throws InvalidOperationException
352
     */
353 1
    private function createUpdateDeleteCore($sourceEntityInstance, $data, $class, $verb)
354
    {
355 1
        $raw = App::make('metadataControllers');
356
        $map = $raw->getMetadata();
357
358 1
        if (!array_key_exists($class, $map)) {
359
            throw new \POData\Common\InvalidOperationException('Controller mapping missing for class '.$class.'.');
360
        }
361
        $goal = $raw->getMapping($class, $verb);
362
        if (null == $goal) {
363
            throw new \POData\Common\InvalidOperationException(
364
                'Controller mapping missing for '.$verb.' verb on class '.$class.'.'
365
            );
366
        }
367
368
        assert(null !== $data, 'Data must not be null');
369
        if (is_object($data)) {
370 3
            $arrayData = (array) $data;
371
        } else {
372 3
            $arrayData = $data;
373 3
        }
374
        if (!is_array($arrayData)) {
375 3
            throw \POData\Common\ODataException::createPreConditionFailedError(
376
                'Data not resolvable to key-value array.'
377
            );
378 3
        }
379 3
380
        $controlClass = $goal['controller'];
381
        $method = $goal['method'];
382
        $paramList = $goal['parameters'];
383
        $controller = App::make($controlClass);
384
        $parms = $this->createUpdateDeleteProcessInput($arrayData, $paramList, $sourceEntityInstance);
385 3
        unset($data);
386
387
        $result = call_user_func_array(array($controller, $method), $parms);
388 3
389 2
        return $this->createUpdateDeleteProcessOutput($result);
390 2
    }
391 3
392
    /**
393
     * Puts an entity instance to entity set identified by a key.
394
     *
395
     * @param ResourceSet $resourceSet The entity set containing the entity to update
396
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to update
397 3
     * @param $data
398 3
     *
399 3
     * @return bool|null Returns result of executing query
400 3
     */
401 3
    public function putResource(
402
        ResourceSet $resourceSet,
403 3
        KeyDescriptor $keyDescriptor,
404 3
        $data
405 3
    ) {
406 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...
407 2
        return true;
408 2
    }
409 2
410 2
    /**
411 2
     * @param ResourceSet $sourceResourceSet
412
     * @param $data
413
     * @param $verb
414
     * @param Model|null $source
415 2
     * @return mixed
416
     * @throws InvalidOperationException
417
     * @throws ODataException
418 3
     */
419 3
    private function createUpdateCoreWrapper(ResourceSet $sourceResourceSet, $data, $verb, Model $source = null)
420
    {
421 3
        $lastWord = 'update' == $verb ? 'updated' : 'created';
422
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->getName();
423 3
        if (!$this->auth->canAuth($this->verbMap[$verb], $class, $source)) {
424
            throw new ODataException('Access denied', 403);
425
        }
426 3
427 3
        $payload = $this->createUpdateDeleteCore($source, $data, $class, $verb);
428 3
429 3
        $success = isset($payload['id']);
430
431 3
        if ($success) {
432
            try {
433
                return $class::findOrFail($payload['id']);
434 3
            } catch (\Exception $e) {
435
                throw new ODataException($e->getMessage(), 500);
436
            }
437
        }
438
        throw new ODataException('Target model not successfully '.$lastWord, 422);
439 3
    }
440
441
    /**
442
     * @param $data
443
     * @param $paramList
444
     * @param Model|null $sourceEntityInstance
445
     * @return array
446
     */
447
    private function createUpdateDeleteProcessInput($data, $paramList, Model $sourceEntityInstance = null)
448
    {
449
        $parms = [];
450
451
        foreach ($paramList as $spec) {
452
            $varType = isset($spec['type']) ? $spec['type'] : null;
453
            $varName = $spec['name'];
454
            if (null == $varType) {
455
                $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...
456
                continue;
457
            }
458
            // 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...
459
            $var = new $varType();
460
            if ($spec['isRequest']) {
461
                $var->setMethod('POST');
462
                $var->request = new \Symfony\Component\HttpFoundation\ParameterBag($data);
463
            }
464
            $parms[] = $var;
465
        }
466
        return $parms;
467
    }
468
469
    /**
470
     * @param $result
471
     * @return array|mixed
472
     * @throws ODataException
473
     */
474
    private function createUpdateDeleteProcessOutput($result)
475
    {
476
        if (!($result instanceof \Illuminate\Http\JsonResponse)) {
477
            throw ODataException::createInternalServerError('Controller response not well-formed json.');
478
        }
479
        $outData = $result->getData();
480
        if (is_object($outData)) {
481
            $outData = (array)$outData;
482
        }
483
484
        if (!is_array($outData)) {
485
            throw ODataException::createInternalServerError('Controller response does not have an array.');
486
        }
487
        if (!(key_exists('id', $outData) && key_exists('status', $outData) && key_exists('errors', $outData))) {
488
            throw ODataException::createInternalServerError(
489
                'Controller response array missing at least one of id, status and/or errors fields.'
490
            );
491
        }
492
        return $outData;
493
    }
494
495
    /**
496
     * @param $sourceEntityInstance
497
     * @return mixed|null|\object[]
498
     */
499
    private function unpackSourceEntity($sourceEntityInstance)
500
    {
501
        if ($sourceEntityInstance instanceof QueryResult) {
502
            $source = $sourceEntityInstance->results;
503
            $source = (is_array($source)) ? $source[0] : $source;
504
            return $source;
505
        }
506
        return $sourceEntityInstance;
507
    }
508
509
    /**
510
     * Create multiple new resources in a resource set.
511
     * @param ResourceSet $sourceResourceSet The entity set containing the entity to fetch
512
     * @param object[] $data The new data for the entity instance
513
     *
514
     * @return object[] returns the newly created model if successful, or throws an exception if model creation failed
515
     * @throw \Exception
516
     */
517
    public function createBulkResourceforResourceSet(
518
        ResourceSet $sourceResourceSet,
519
        array $data
520
    ) {
521
        $verbName = 'bulkCreate';
522
        $mapping = $this->getOptionalVerbMapping($sourceResourceSet, $verbName);
523
524
        $result = [];
525
        try {
526
            DB::beginTransaction();
527
            if (null === $mapping) {
528
                foreach ($data as $newItem) {
529
                    $raw = $this->createResourceforResourceSet($sourceResourceSet, null, $newItem);
530
                    if (null === $raw) {
531
                        throw new \Exception('Bulk model creation failed');
532
                    }
533
                    $result[] = $raw;
534
                }
535
            } else {
536
                $keyDescriptor = null;
537
                $pastVerb = 'created';
538
                $result = $this->processBulkCustom($sourceResourceSet, $data, $mapping, $pastVerb, $keyDescriptor);
539
            }
540
            DB::commit();
541
        } catch (\Exception $e) {
542
            DB::rollBack();
543
            throw $e;
544
        }
545
        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...
546
    }
547
548
    /**
549
     * Updates a group of resources in a resource set.
550
     *
551
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
552
     * @param object $sourceEntityInstance The source entity instance
553
     * @param KeyDescriptor[] $keyDescriptor The key identifying the entity to fetch
554
     * @param object[] $data The new data for the entity instances
555
     * @param bool $shouldUpdate Should undefined values be updated or reset to default
556
     *
557
     * @return object[] the new resource value if it is assignable, or throw exception for null
558
     * @throw \Exception
559
     */
560
    public function updateBulkResource(
561
        ResourceSet $sourceResourceSet,
562
        $sourceEntityInstance,
563
        array $keyDescriptor,
564
        array $data,
565
        $shouldUpdate = false
566
    ) {
567
        $numKeys = count($keyDescriptor);
568
        if ($numKeys !== count($data)) {
569
            $msg = 'Key descriptor array and data array must be same length';
570
            throw new \InvalidArgumentException($msg);
571
        }
572
        $result = [];
573
574
        $verbName = 'bulkUpdate';
575
        $mapping = $this->getOptionalVerbMapping($sourceResourceSet, $verbName);
576
577
        try {
578
            DB::beginTransaction();
579
            if (null === $mapping) {
580
                for ($i = 0; $i < $numKeys; $i++) {
581
                    $newItem = $data[$i];
582
                    $newKey = $keyDescriptor[$i];
583
                    $raw = $this->updateResource($sourceResourceSet, $sourceEntityInstance, $newKey, $newItem);
584
                    if (null === $raw) {
585
                        throw new \Exception('Bulk model update failed');
586
                    }
587
                    $result[] = $raw;
588
                }
589
            } else {
590
                $pastVerb = 'updated';
591
                $result = $this->processBulkCustom($sourceResourceSet, $data, $mapping, $pastVerb, $keyDescriptor);
592
            }
593
            DB::commit();
594
        } catch (\Exception $e) {
595
            DB::rollBack();
596
            throw $e;
597
        }
598
        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...
599
    }
600
601
    /**
602
     * Attaches child model to parent model
603
     *
604
     * @param ResourceSet $sourceResourceSet
605
     * @param object $sourceEntityInstance
606
     * @param ResourceSet $targetResourceSet
607
     * @param object $targetEntityInstance
608
     * @param $navPropName
609
     *
610
     * @return bool
611
     */
612
    public function hookSingleModel(
613
        ResourceSet $sourceResourceSet,
614
        $sourceEntityInstance,
615
        ResourceSet $targetResourceSet,
616
        $targetEntityInstance,
617
        $navPropName
618
    ) {
619
        $relation = $this->isModelHookInputsOk($sourceEntityInstance, $targetEntityInstance, $navPropName);
620
        assert(
621
            $sourceEntityInstance instanceof Model && $targetEntityInstance instanceof Model,
622
            'Both input entities must be Eloquent models'
623
        );
624
        // in case the fake 'PrimaryKey' attribute got set inbound for a polymorphic-affected model, flatten it now
625
        unset($targetEntityInstance->PrimaryKey);
626
627
        if ($relation instanceof BelongsTo) {
628
            $relation->associate($targetEntityInstance);
629
        } elseif ($relation instanceof BelongsToMany) {
630
            $relation->attach($targetEntityInstance);
631
        } elseif ($relation instanceof HasOneOrMany) {
632
            $relation->save($targetEntityInstance);
633
        }
634
        return true;
635
    }
636
637
    /**
638
     * Removes child model from parent model
639
     *
640
     * @param ResourceSet $sourceResourceSet
641
     * @param object $sourceEntityInstance
642
     * @param ResourceSet $targetResourceSet
643
     * @param object $targetEntityInstance
644
     * @param $navPropName
645
     *
646
     * @return bool
647
     */
648
    public function unhookSingleModel(
649
        ResourceSet $sourceResourceSet,
650
        $sourceEntityInstance,
651
        ResourceSet $targetResourceSet,
652
        $targetEntityInstance,
653
        $navPropName
654
    ) {
655
        $relation = $this->isModelHookInputsOk($sourceEntityInstance, $targetEntityInstance, $navPropName);
656
        assert(
657
            $sourceEntityInstance instanceof Model && $targetEntityInstance instanceof Model,
658
            'Both input entities must be Eloquent models'
659
        );
660
        // in case the fake 'PrimaryKey' attribute got set inbound for a polymorphic-affected model, flatten it now
661
        unset($targetEntityInstance->PrimaryKey);
662
663
        if ($relation instanceof BelongsTo) {
664
            $relation->dissociate();
665
        } elseif ($relation instanceof BelongsToMany) {
666
            $relation->detach($targetEntityInstance);
667
        } elseif ($relation instanceof HasOneOrMany) {
668
            // dig up inverse property name, so we can pass it to unhookSingleModel with source and target elements
669
            // swapped
670
            $otherPropName = $this->getMetadataProvider()
671
                ->resolveReverseProperty($sourceEntityInstance, $targetEntityInstance, $navPropName);
672
            if (null === $otherPropName) {
673
                $msg = 'Bad navigation property, '.$navPropName.', on source model '.get_class($sourceEntityInstance);
674
                throw new \InvalidArgumentException($msg);
675
            }
676
            $this->unhookSingleModel(
677
                $targetResourceSet,
678
                $targetEntityInstance,
679
                $sourceResourceSet,
680
                $sourceEntityInstance,
681
                $otherPropName
682
            );
683
        }
684
        return true;
685
    }
686
687
    /**
688
     * @param $sourceEntityInstance
689
     * @param $targetEntityInstance
690
     * @param $navPropName
691
     * @return Relation
692
     * @throws \InvalidArgumentException
693
     */
694
    protected function isModelHookInputsOk($sourceEntityInstance, $targetEntityInstance, $navPropName)
695
    {
696
        if (!$sourceEntityInstance instanceof Model || !$targetEntityInstance instanceof Model) {
697
            $msg = 'Both source and target must be Eloquent models';
698
            throw new \InvalidArgumentException($msg);
699
        }
700
        $relation = $sourceEntityInstance->$navPropName();
701
        if (!$relation instanceof Relation) {
702
            $msg = 'Navigation property must be an Eloquent relation';
703
            throw new \InvalidArgumentException($msg);
704
        }
705
        $targType = $relation->getRelated();
706
        if (!$targetEntityInstance instanceof $targType) {
707
            $msg = 'Target instance must be of type compatible with relation declared in method '.$navPropName;
708
            throw new \InvalidArgumentException($msg);
709
        }
710
        return $relation;
711
    }
712
713
    /**
714
     * @param ResourceSet $sourceResourceSet
715
     * @return array|null
716
     * @param $verbName
717
     */
718
    protected function getOptionalVerbMapping(ResourceSet $sourceResourceSet, $verbName)
719
    {
720
        // dig up target class name
721
        $type = $sourceResourceSet->getResourceType()->getInstanceType();
722
        assert($type instanceof \ReflectionClass, get_class($type));
723
        $modelName = $type->getName();
724
        return $this->getControllerContainer()->getMapping($modelName, $verbName);
725
    }
726
727
    /**
728
     * Prepare bulk request from supplied data.  If $keyDescriptors is not null, its elements are assumed to
729
     * correspond 1-1 to those in $data.
730
     *
731
     * @param $paramList
732
     * @param array $data
733
     * @param KeyDescriptor[]|null $keyDescriptors
734
     */
735
    protected function prepareBulkRequestInput($paramList, array $data, array $keyDescriptors = null)
736
    {
737
        $parms = [];
738
        $isCreate = null === $keyDescriptors;
739
740
        // for moment, we're only processing parameters of type Request
741
        foreach ($paramList as $spec) {
742
            $varType = isset($spec['type']) ? $spec['type'] : null;
743
            if (null !== $varType) {
744
                $var = new $varType();
745
                if ($spec['isRequest']) {
746
                    $var->setMethod($isCreate ? 'POST' : 'PUT');
747
                    $bulkData = [ 'data' => $data];
748
                    if (null !== $keyDescriptors) {
749
                        $keys = [];
750
                        foreach ($keyDescriptors as $desc) {
751
                            assert($desc instanceof KeyDescriptor, get_class($desc));
752
                            $rawPayload = $desc->getNamedValues();
753
                            $keyPayload = [];
754
                            foreach ($rawPayload as $keyName => $keyVal) {
755
                                $keyPayload[$keyName] = $keyVal[0];
756
                            }
757
                            $keys[] = $keyPayload;
758
                        }
759
                        $bulkData['keys'] = $keys;
760
                    }
761
                    $var->request = new \Symfony\Component\HttpFoundation\ParameterBag($bulkData);
762
                }
763
                $parms[] = $var;
764
            }
765
        }
766
        return $parms;
767
    }
768
769
    /**
770
     * @param ResourceSet $sourceResourceSet
771
     * @param array $data
772
     * @param $mapping
773
     * @param $pastVerb
774
     * @param KeyDescriptor[]|null $keyDescriptor
775
     * @return array
776
     * @throws ODataException
777
     */
778
    protected function processBulkCustom(
779
        ResourceSet $sourceResourceSet,
780
        array $data,
781
        $mapping,
782
        $pastVerb,
783
        array $keyDescriptor = null
784
    ) {
785
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->getName();
786
        $controlClass = $mapping['controller'];
787
        $method = $mapping['method'];
788
        $paramList = $mapping['parameters'];
789
        $controller = App::make($controlClass);
790
        $parms = $this->prepareBulkRequestInput($paramList, $data, $keyDescriptor);
791
792
        $callResult = call_user_func_array(array($controller, $method), $parms);
793
        $payload = $this->createUpdateDeleteProcessOutput($callResult);
794
        $success = isset($payload['id']) && is_array($payload['id']);
795
796
        if ($success) {
797
            try {
798
                $result = $class::findMany($payload['id'])->toArray()[0];
799
                return $result;
800
            } catch (\Exception $e) {
801
                throw new ODataException($e->getMessage(), 500);
802
            }
803
        } else {
804
            throw new ODataException('Target models not successfully ' . $pastVerb, 422);
805
        }
806
    }
807
808
    /**
809
     * Start database transaction
810
     */
811
    public function startTransaction()
812
    {
813
        DB::beginTransaction();
814
    }
815
816
    /**
817
     * Commit database transaction
818
     */
819
    public function commitTransaction()
820
    {
821
        DB::commit();
822
    }
823
824
    /**
825
     * Abort database transaction
826
     */
827
    public function rollBackTransaction()
828
    {
829
        DB::rollBack();
830
    }
831
}
832