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

LaravelQuery::updateBulkResource()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

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 9
ccs 0
cts 0
cp 0
rs 9.6666
cc 1
eloc 6
nc 1
nop 5
crap 2
1
<?php
2
3
namespace AlgoWeb\PODataLaravel\Query;
4
5
use AlgoWeb\PODataLaravel\Enums\ActionVerb;
6
use AlgoWeb\PODataLaravel\Providers\MetadataProvider;
7
use Illuminate\Database\Eloquent\Relations\BelongsTo;
8
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
9
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
10
use Illuminate\Database\Eloquent\Relations\MorphOneOrMany;
11
use Illuminate\Database\Eloquent\Relations\MorphTo;
12
use Illuminate\Database\Eloquent\Relations\MorphToMany;
13
use Illuminate\Database\Eloquent\Relations\Relation;
14
use POData\Common\InvalidOperationException;
15
use POData\Providers\Metadata\ResourceProperty;
16
use POData\Providers\Metadata\ResourceSet;
17
use POData\UriProcessor\QueryProcessor\ExpressionParser\FilterInfo;
18
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
19
use POData\UriProcessor\QueryProcessor\SkipTokenParser\SkipTokenInfo;
20
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
21
use POData\Providers\Query\IQueryProvider;
22
use POData\Providers\Expression\MySQLExpressionProvider;
23
use POData\Providers\Query\QueryType;
24
use POData\Providers\Query\QueryResult;
25
use POData\Providers\Expression\PHPExpressionProvider;
26 5
use \POData\Common\ODataException;
27
use AlgoWeb\PODataLaravel\Interfaces\AuthInterface;
28
use AlgoWeb\PODataLaravel\Auth\NullAuthProvider;
29 5
use Illuminate\Support\Facades\App;
30 5
use Illuminate\Database\Eloquent\Model;
31 5
use Symfony\Component\Process\Exception\InvalidArgumentException;
32 5
33
class LaravelQuery implements IQueryProvider
34
{
35
    protected $expression;
36
    protected $auth;
37
    protected $reader;
38
    public $queryProviderClassName;
39
    private $verbMap = [];
40
    protected $metadataProvider;
41
42
    public function __construct(AuthInterface $auth = null)
43
    {
44
        /* MySQLExpressionProvider();*/
45
        $this->expression = new LaravelExpressionProvider(); //PHPExpressionProvider('expression');
46
        $this->queryProviderClassName = get_class($this);
47
        $this->auth = isset($auth) ? $auth : new NullAuthProvider();
48
        $this->reader = new LaravelReadQuery($this->auth);
49
        $this->verbMap['create'] = ActionVerb::CREATE();
50
        $this->verbMap['update'] = ActionVerb::UPDATE();
51
        $this->verbMap['delete'] = ActionVerb::DELETE();
52
        $this->metadataProvider = new MetadataProvider(App::make('app'));
53
    }
54
55
    /**
56
     * Indicates if the QueryProvider can handle ordered paging, this means respecting order, skip, and top parameters
57
     * If the query provider can not handle ordered paging, it must return the entire result set and POData will
58
     * perform the ordering and paging
59
     *
60
     * @return Boolean True if the query provider can handle ordered paging, false if POData should perform the paging
61
     */
62
    public function handlesOrderedPaging()
63
    {
64
        return true;
65
    }
66
67
    /**
68
     * Gets the expression provider used by to compile OData expressions into expression used by this query provider.
69
     *
70 3
     * @return \POData\Providers\Expression\IExpressionProvider
71
     */
72
    public function getExpressionProvider()
73
    {
74
        return $this->expression;
75
    }
76
77
    /**
78
     * Gets the LaravelReadQuery instance used to handle read queries (repetitious, nyet?)
79 3
     *
80
     * @return LaravelReadQuery
81
     */
82 3
    public function getReader()
83 1
    {
84 1
        return $this->reader;
85
    }
86 3
87 3
    /**
88 3
     * Dig out local copy of POData-Laravel metadata provider
89
     *
90 3
     * @return MetadataProvider
91
     */
92
    public function getMetadataProvider()
93
    {
94
        return $this->metadataProvider;
95
    }
96
97 1
    /**
98
     * Gets collection of entities belongs to an entity set
99
     * IE: http://host/EntitySet
100 3
     *  http://host/EntitySet?$skip=10&$top=5&filter=Prop gt Value
101 1
     *
102 1
     * @param QueryType                $queryType   Is this is a query for a count, entities, or entities-with-count
103 3
     * @param ResourceSet              $resourceSet The entity set containing the entities to fetch
104 1
     * @param FilterInfo|null          $filterInfo  The $filter parameter of the OData query.  NULL if none specified
105 1
     * @param null|InternalOrderByInfo $orderBy     sorted order if we want to get the data in some specific order
106
     * @param integer|null             $top         number of records which need to be retrieved
107 3
     * @param integer|null             $skip        number of records which need to be skipped
108
     * @param SkipTokenInfo|null       $skipToken   value indicating what records to skip
109 3
     * @param Model|Relation|null       $sourceEntityInstance Starting point of query
110
     *
111
     * @return QueryResult
112
     */
113 View Code Duplication
    public function getResourceSet(
114
        QueryType $queryType,
115
        ResourceSet $resourceSet,
116
        $filterInfo = null,
117 3
        $orderBy = null,
118 1
        $top = null,
119 1
        $skip = null,
120 1
        $skipToken = null,
121 1
        $sourceEntityInstance = null
122 1
    ) {
123 3
        $source = $this->unpackSourceEntity($sourceEntityInstance);
124 3
        return $this->getReader()->getResourceSet(
125
            $queryType,
126
            $resourceSet,
127 3
            $filterInfo,
128 3
            $orderBy,
129 3
            $top,
130
            $skip,
131
            $skipToken,
132
            $source
133
        );
134
    }
135
    /**
136
     * Gets an entity instance from an entity set identified by a key
137
     * IE: http://host/EntitySet(1L)
138
     * http://host/EntitySet(KeyA=2L,KeyB='someValue')
139
     *
140
     * @param ResourceSet           $resourceSet    The entity set containing the entity to fetch
141
     * @param KeyDescriptor|null    $keyDescriptor  The key identifying the entity to fetch
142
     *
143
     * @return Model|null Returns entity instance if found else null
144
     */
145
    public function getResourceFromResourceSet(
146
        ResourceSet $resourceSet,
147
        KeyDescriptor $keyDescriptor = null
148
    ) {
149
        return $this->getReader()->getResourceFromResourceSet($resourceSet, $keyDescriptor);
150
    }
151
152
    /**
153
     * Get related resource set for a resource
154
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection
155
     * http://host/EntitySet?$expand=NavigationPropertyToCollection
156
     *
157
     * @param QueryType          $queryType            Is this is a query for a count, entities, or entities-with-count
158
     * @param ResourceSet        $sourceResourceSet    The entity set containing the source entity
159
     * @param object             $sourceEntityInstance The source entity instance
160
     * @param ResourceSet        $targetResourceSet    The resource set pointed to by the navigation property
161
     * @param ResourceProperty   $targetProperty       The navigation property to retrieve
162
     * @param FilterInfo|null    $filter               The $filter parameter of the OData query.  NULL if none specified
163
     * @param mixed|null         $orderBy              sorted order if we want to get the data in some specific order
164
     * @param integer|null       $top                  number of records which need to be retrieved
165
     * @param integer|null       $skip                 number of records which need to be skipped
166
     * @param SkipTokenInfo|null $skipToken            value indicating what records to skip
167
     *
168
     * @return QueryResult
169
     *
170
     */
171 View Code Duplication
    public function getRelatedResourceSet(
172
        QueryType $queryType,
173
        ResourceSet $sourceResourceSet,
174
        $sourceEntityInstance,
175
        ResourceSet $targetResourceSet,
176
        ResourceProperty $targetProperty,
177
        FilterInfo $filter = null,
178
        $orderBy = null,
179
        $top = null,
180
        $skip = null,
181
        $skipToken = null
182
    ) {
183
        $source = $this->unpackSourceEntity($sourceEntityInstance);
184
        return $this->getReader()->getRelatedResourceSet(
185
            $queryType,
186
            $sourceResourceSet,
187
            $source,
188
            $targetResourceSet,
189
            $targetProperty,
190
            $filter,
191
            $orderBy,
192
            $top,
193
            $skip,
194
            $skipToken
195
        );
196
    }
197
198
    /**
199 3
     * Gets a related entity instance from an entity set identified by a key
200
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33)
201
     *
202
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
203
     * @param object $sourceEntityInstance The source entity instance.
204
     * @param ResourceSet $targetResourceSet The entity set containing the entity to fetch
205
     * @param ResourceProperty $targetProperty The metadata of the target property.
206
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch
207
     *
208
     * @return Model|null Returns entity instance if found else null
209
     */
210 3
    public function getResourceFromRelatedResourceSet(
211 3
        ResourceSet $sourceResourceSet,
212
        $sourceEntityInstance,
213 3
        ResourceSet $targetResourceSet,
214 3
        ResourceProperty $targetProperty,
215 3
        KeyDescriptor $keyDescriptor
216 3
    ) {
217 3
        $source = $this->unpackSourceEntity($sourceEntityInstance);
218 3
        return $this->getReader()->getResourceFromRelatedResourceSet(
219 3
            $sourceResourceSet,
220
            $source,
221 3
            $targetResourceSet,
222
            $targetProperty,
223
            $keyDescriptor
224
        );
225
    }
226
227
    /**
228
     * Get related resource for a resource
229
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
230
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity
231
     *
232
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
233
     * @param object $sourceEntityInstance The source entity instance.
234
     * @param ResourceSet $targetResourceSet The entity set containing the entity pointed to by the navigation property
235
     * @param ResourceProperty $targetProperty The navigation property to fetch
236
     *
237
     * @return object|null The related resource if found else null
238
     */
239
    public function getRelatedResourceReference(
240
        ResourceSet $sourceResourceSet,
241
        $sourceEntityInstance,
242
        ResourceSet $targetResourceSet,
243
        ResourceProperty $targetProperty
244
    ) {
245
        $source = $this->unpackSourceEntity($sourceEntityInstance);
246
247
        $result = $this->getReader()->getRelatedResourceReference(
248
            $sourceResourceSet,
249
            $source,
250
            $targetResourceSet,
251
            $targetProperty
252
        );
253
        return $result;
254
    }
255
256
    /**
257
     * Updates a resource
258
     *
259
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
260
     * @param object           $sourceEntityInstance The source entity instance
261
     * @param KeyDescriptor    $keyDescriptor        The key identifying the entity to fetch
262
     * @param object           $data                 The New data for the entity instance.
263
     * @param bool             $shouldUpdate        Should undefined values be updated or reset to default
264
     *
265
     * @return object|null The new resource value if it is assignable or throw exception for null.
266
     */
267
    public function updateResource(
268
        ResourceSet $sourceResourceSet,
269
        $sourceEntityInstance,
270
        KeyDescriptor $keyDescriptor,
271
        $data,
272
        $shouldUpdate = false
273
    ) {
274
        $source = $this->unpackSourceEntity($sourceEntityInstance);
275
276
        $verb = 'update';
277
        return $this->createUpdateCoreWrapper($sourceResourceSet, $data, $verb, $source);
278
    }
279
    /**
280
     * Delete resource from a resource set.
281
     * @param ResourceSet      $sourceResourceSet
282
     * @param object           $sourceEntityInstance
283
     *
284
     * return bool true if resources sucessfully deteled, otherwise false.
285
     */
286
    public function deleteResource(
287
        ResourceSet $sourceResourceSet,
288
        $sourceEntityInstance
289
    ) {
290
        $source = $this->unpackSourceEntity($sourceEntityInstance);
291
292 1
        $verb = 'delete';
293
        if (!($source instanceof Model)) {
294
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
295
        }
296
297
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->getName();
298
        $id = $source->getKey();
299 1
        $name = $source->getKeyName();
300 1
        $data = [$name => $id];
301
302 1
        $data = $this->createUpdateDeleteCore($source, $data, $class, $verb);
303
304 1
        $success = isset($data['id']);
305
        if ($success) {
306 1
            return true;
307
        }
308
        throw new ODataException('Target model not successfully deleted', 422);
309 1
    }
310
    /**
311
     * @param ResourceSet      $resourceSet   The entity set containing the entity to fetch
312
     * @param object           $sourceEntityInstance The source entity instance
313
     * @param object           $data                 The New data for the entity instance.
314
     *
315
     * returns object|null returns the newly created model if sucessful or null if model creation failed.
316
     */
317
    public function createResourceforResourceSet(
318 1
        ResourceSet $resourceSet,
319
        $sourceEntityInstance,
320
        $data
321
    ) {
322 1
        $source = $this->unpackSourceEntity($sourceEntityInstance);
323 1
324 1
        $verb = 'create';
325 1
        return $this->createUpdateCoreWrapper($resourceSet, $data, $verb, $source);
326 1
    }
327
328 1
    /**
329
     * @param $sourceEntityInstance
330 1
     * @param $data
331 1
     * @param $class
332
     * @param string $verb
333
     * @return array|mixed
334 1
     * @throws ODataException
335
     * @throws InvalidOperationException
336
     */
337
    private function createUpdateDeleteCore($sourceEntityInstance, $data, $class, $verb)
338
    {
339
        $raw = App::make('metadataControllers');
340
        $map = $raw->getMetadata();
341
342
        if (!array_key_exists($class, $map)) {
343 1
            throw new \POData\Common\InvalidOperationException('Controller mapping missing for class '.$class.'.');
344
        }
345
        $goal = $raw->getMapping($class, $verb);
346
        if (null == $goal) {
347
            throw new \POData\Common\InvalidOperationException(
348 1
                'Controller mapping missing for '.$verb.' verb on class '.$class.'.'
349 1
            );
350
        }
351 1
352
        assert(null !== $data, 'Data must not be null');
353 1
        if (is_object($data)) {
354
            $arrayData = (array) $data;
355 1
        } else {
356
            $arrayData = $data;
357
        }
358 1
        if (!is_array($arrayData)) {
359
            throw \POData\Common\ODataException::createPreConditionFailedError(
360
                'Data not resolvable to key-value array.'
361
            );
362
        }
363
364
        $controlClass = $goal['controller'];
365
        $method = $goal['method'];
366
        $paramList = $goal['parameters'];
367
        $controller = App::make($controlClass);
368
        $parms = $this->createUpdateDeleteProcessInput($arrayData, $paramList, $sourceEntityInstance);
369
        unset($data);
370 3
371
        $result = call_user_func_array(array($controller, $method), $parms);
372 3
373 3
        return $this->createUpdateDeleteProcessOutput($result);
374
    }
375 3
376
    /**
377
     * Puts an entity instance to entity set identified by a key.
378 3
     *
379 3
     * @param ResourceSet $resourceSet The entity set containing the entity to update
380
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to update
381
     * @param $data
382
     *
383
     * @return bool|null Returns result of executing query
384
     */
385 3
    public function putResource(
386
        ResourceSet $resourceSet,
387
        KeyDescriptor $keyDescriptor,
388 3
        $data
389 2
    ) {
390 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...
391 3
        return true;
392
    }
393
394
    /**
395
     * @param ResourceSet $sourceResourceSet
396
     * @param $data
397 3
     * @param $verb
398 3
     * @param Model|null $source
399 3
     * @return mixed
400 3
     * @throws InvalidOperationException
401 3
     * @throws ODataException
402
     */
403 3
    private function createUpdateCoreWrapper(ResourceSet $sourceResourceSet, $data, $verb, Model $source = null)
404 3
    {
405 3
        $lastWord = 'update' == $verb ? 'updated' : 'created';
406 3
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->getName();
407 2
        if (!$this->auth->canAuth($this->verbMap[$verb], $class, $source)) {
408 2
            throw new ODataException('Access denied', 403);
409 2
        }
410 2
411 2
        $payload = $this->createUpdateDeleteCore($source, $data, $class, $verb);
412
413
        $success = isset($payload['id']);
414
415 2
        if ($success) {
416
            try {
417
                return $class::findOrFail($payload['id']);
418 3
            } catch (\Exception $e) {
419 3
                throw new ODataException($e->getMessage(), 500);
420
            }
421 3
        }
422
        throw new ODataException('Target model not successfully '.$lastWord, 422);
423 3
    }
424
425
    /**
426 3
     * @param $data
427 3
     * @param $paramList
428 3
     * @param Model|null $sourceEntityInstance
429 3
     * @return array
430
     */
431 3
    private function createUpdateDeleteProcessInput($data, $paramList, Model $sourceEntityInstance = null)
432
    {
433
        $parms = [];
434 3
435
        foreach ($paramList as $spec) {
436
            $varType = isset($spec['type']) ? $spec['type'] : null;
437
            $varName = $spec['name'];
438
            if (null == $varType) {
439 3
                $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...
440
                continue;
441
            }
442
            // 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...
443
            $var = new $varType();
444
            if ($spec['isRequest']) {
445
                $var->setMethod('POST');
446
                $var->request = new \Symfony\Component\HttpFoundation\ParameterBag($data);
447
            }
448
            $parms[] = $var;
449
        }
450
        return $parms;
451
    }
452
453
    /**
454
     * @param $result
455
     * @return array|mixed
456
     * @throws ODataException
457
     */
458
    private function createUpdateDeleteProcessOutput($result)
459
    {
460
        if (!($result instanceof \Illuminate\Http\JsonResponse)) {
461
            throw ODataException::createInternalServerError('Controller response not well-formed json.');
462
        }
463
        $outData = $result->getData();
464
        if (is_object($outData)) {
465
            $outData = (array)$outData;
466
        }
467
468
        if (!is_array($outData)) {
469
            throw ODataException::createInternalServerError('Controller response does not have an array.');
470
        }
471
        if (!(key_exists('id', $outData) && key_exists('status', $outData) && key_exists('errors', $outData))) {
472
            throw ODataException::createInternalServerError(
473
                'Controller response array missing at least one of id, status and/or errors fields.'
474
            );
475
        }
476
        return $outData;
477
    }
478
479
    /**
480
     * @param $sourceEntityInstance
481
     * @return mixed|null|\object[]
482
     */
483
    private function unpackSourceEntity($sourceEntityInstance)
484
    {
485
        if ($sourceEntityInstance instanceof QueryResult) {
486
            $source = $sourceEntityInstance->results;
487
            $source = (is_array($source)) ? $source[0] : $source;
488
            return $source;
489
        }
490
        return $sourceEntityInstance;
491
    }
492
493
    /**
494
     * Create multiple new resources in a resource set.
495
     * @param ResourceSet $sourceResourceSet The entity set containing the entity to fetch
496
     * @param object[] $data The new data for the entity instance
497
     *
498
     * @return object[]|null returns the newly created model if successful, or null if model creation failed
499
     */
500
    public function createBulkResourceforResourceSet(
501
        ResourceSet $sourceResourceSet,
0 ignored issues
show
Unused Code introduced by
The parameter $sourceResourceSet is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
502
        array $data
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
503
    ) {
504
        // TODO: Implement createBulkResourceforResourceSet() 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...
505
    }
506
507
    /**
508
     * Updates a group of resources in a resource set.
509
     *
510
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
511
     * @param object $sourceEntityInstance The source entity instance
512
     * @param KeyDescriptor[] $keyDescriptor The key identifying the entity to fetch
513
     * @param object[] $data The new data for the entity instances
514
     * @param bool $shouldUpdate Should undefined values be updated or reset to default
515
     *
516
     * @return object[]|null the new resource value if it is assignable, or throw exception for null
517
     */
518
    public function updateBulkResource(
519
        ResourceSet $sourceResourceSet,
520
        $sourceEntityInstance,
0 ignored issues
show
Unused Code introduced by
The parameter $sourceEntityInstance is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
521
        array $keyDescriptor,
0 ignored issues
show
Unused Code introduced by
The parameter $keyDescriptor is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
522
        array $data,
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
523
        $shouldUpdate = false
1 ignored issue
show
Unused Code introduced by
The parameter $shouldUpdate is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
524
    ) {
525
        // TODO: Implement updateBulkResource() 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...
526
    }
527
528
    /**
529
     * Attaches child model to parent model
530
     *
531
     * @param ResourceSet $sourceResourceSet
532
     * @param object $sourceEntityInstance
533
     * @param ResourceSet $targetResourceSet
534
     * @param object $targetEntityInstance
535
     * @param $navPropName
536
     *
537
     * @return bool
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
538
     */
539
    public function hookSingleModel(
540
        ResourceSet $sourceResourceSet,
541
        $sourceEntityInstance,
542
        ResourceSet $targetResourceSet,
543
        $targetEntityInstance,
544
        $navPropName
545
    ) {
546
        $relation = $this->isModelHookInputsOk($sourceEntityInstance, $targetEntityInstance, $navPropName);
547
        assert($sourceEntityInstance instanceof Model && $targetEntityInstance instanceof Model);
548
549
        if ($relation instanceof BelongsTo) {
550
            $relation->associate($targetEntityInstance);
551
        } elseif ($relation instanceof BelongsToMany) {
552
            $relation->attach($targetEntityInstance);
553
        } elseif ($relation instanceof HasOneOrMany) {
554
            $relation->save($targetEntityInstance);
555
        }
556
    }
557
558
    /**
559
     * Removes child model from parent model
560
     *
561
     * @param ResourceSet $sourceResourceSet
562
     * @param object $sourceEntityInstance
563
     * @param ResourceSet $targetResourceSet
564
     * @param object $targetEntityInstance
565
     * @param $navPropName
566
     *
567
     * @return bool
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
568
     */
569
    public function unhookSingleModel(
570
        ResourceSet $sourceResourceSet,
571
        $sourceEntityInstance,
572
        ResourceSet $targetResourceSet,
573
        $targetEntityInstance,
574
        $navPropName
575
    ) {
576
        $relation = $this->isModelHookInputsOk($sourceEntityInstance, $targetEntityInstance, $navPropName);
577
        assert($sourceEntityInstance instanceof Model && $targetEntityInstance instanceof Model);
578
579
        if ($relation instanceof BelongsTo) {
580
            $relation->dissociate();
581
        } elseif ($relation instanceof BelongsToMany) {
582
            $relation->detach($targetEntityInstance);
583
        } elseif ($relation instanceof HasOneOrMany) {
584
            // dig up inverse property name, so we can pass it to unhookSingleModel with source and target elements
585
            // swapped
586
            $otherPropName = $this->getMetadataProvider()
587
                ->resolveReverseProperty($sourceEntityInstance, $targetEntityInstance, $navPropName);
588
            if (null === $otherPropName) {
589
                $msg = 'Bad navigation property, '.$navPropName.', on source model '.get_class($sourceEntityInstance);
590
                throw new \InvalidArgumentException($msg);
591
            }
592
            $this->unhookSingleModel(
593
                $targetResourceSet,
594
                $targetEntityInstance,
595
                $sourceResourceSet,
596
                $sourceEntityInstance,
597
                $otherPropName
598
            );
599
        }
600
    }
601
602
    /**
603
     * @param $sourceEntityInstance
604
     * @param $targetEntityInstance
605
     * @param $navPropName
606
     * @return mixed
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use Relation.

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...
607
     */
608
    protected function isModelHookInputsOk($sourceEntityInstance, $targetEntityInstance, $navPropName)
609
    {
610
        if (!$sourceEntityInstance instanceof Model || !$targetEntityInstance instanceof Model) {
611
            $msg = 'Both source and target must be Eloquent models';
612
            throw new \InvalidArgumentException($msg);
613
        }
614
        $relation = $sourceEntityInstance->$navPropName();
615
        if (!$relation instanceof Relation) {
616
            $msg = 'Navigation property must be an Eloquent relation';
617
            throw new \InvalidArgumentException($msg);
618
        }
619
        $targType = $relation->getRelated();
620
        if (!$targetEntityInstance instanceof $targType) {
621
            $msg = 'Target instance must be of type compatible with relation declared in method '.$navPropName;
622
            throw new \InvalidArgumentException($msg);
623
        }
624
        return $relation;
625
    }
626
}
627