Test Failed
Pull Request — master (#100)
by Alex
04:07 queued 33s
created

LaravelQuery::unhookSingleModel()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 33
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
511
                if (null === $raw) {
512
                    throw new \Exception('Bulk model creation failed');
513
                }
514
                $result[] = $raw;
515
            }
516
            DB::commit();
517
        } catch (\Exception $e) {
518
            DB::rollBack();
519
            throw $e;
520
        }
521
        return $result;
522
    }
523
524
    /**
525
     * Updates a group of resources in a resource set.
526
     *
527
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
528
     * @param object $sourceEntityInstance The source entity instance
529
     * @param KeyDescriptor[] $keyDescriptor The key identifying the entity to fetch
530
     * @param object[] $data The new data for the entity instances
531
     * @param bool $shouldUpdate Should undefined values be updated or reset to default
532
     *
533
     * @return object[] the new resource value if it is assignable, or throw exception for null
534
     * @throw \Exception
535
     */
536
    public function updateBulkResource(
537
        ResourceSet $sourceResourceSet,
538
        $sourceEntityInstance,
539
        array $keyDescriptor,
540
        array $data,
541
        $shouldUpdate = false
542
    ) {
543
        $numKeys = count($keyDescriptor);
544
        if ($numKeys !== count($data)) {
545
            $msg = 'Key descriptor array and data array must be same length';
546
            throw new \InvalidArgumentException($msg);
547
        }
548
        $result = [];
549
550
        try {
551
            DB::beginTransaction();
552
            for ($i = 0; $i < $numKeys; $i++) {
553
                $newItem = $data[$i];
554
                $newKey = $keyDescriptor[$i];
555
                $raw = $this->updateResource($sourceResourceSet, $sourceEntityInstance, $newKey, $newItem);
556
                if (null === $raw) {
557
                    throw new \Exception('Bulk model update failed');
558
                }
559
                $result[] = $raw;
560
            }
561
            DB::commit();
562
        } catch (\Exception $e) {
563
            DB::rollBack();
564
            throw $e;
565
        }
566
        return $result;
567
    }
568
569
    /**
570
     * Attaches child model to parent model
571
     *
572
     * @param ResourceSet $sourceResourceSet
573
     * @param object $sourceEntityInstance
574
     * @param ResourceSet $targetResourceSet
575
     * @param object $targetEntityInstance
576
     * @param $navPropName
577
     *
578
     * @return bool
579
     */
580
    public function hookSingleModel(
581
        ResourceSet $sourceResourceSet,
582
        $sourceEntityInstance,
583
        ResourceSet $targetResourceSet,
584
        $targetEntityInstance,
585
        $navPropName
586
    ) {
587
        $relation = $this->isModelHookInputsOk($sourceEntityInstance, $targetEntityInstance, $navPropName);
588
        assert($sourceEntityInstance instanceof Model && $targetEntityInstance instanceof Model);
589
590
        if ($relation instanceof BelongsTo) {
591
            $relation->associate($targetEntityInstance);
592
        } elseif ($relation instanceof BelongsToMany) {
593
            $relation->attach($targetEntityInstance);
594
        } elseif ($relation instanceof HasOneOrMany) {
595
            $relation->save($targetEntityInstance);
596
        }
597
        return true;
598
    }
599
600
    /**
601
     * Removes child model from parent model
602
     *
603
     * @param ResourceSet $sourceResourceSet
604
     * @param object $sourceEntityInstance
605
     * @param ResourceSet $targetResourceSet
606
     * @param object $targetEntityInstance
607
     * @param $navPropName
608
     *
609
     * @return bool
610
     */
611
    public function unhookSingleModel(
612
        ResourceSet $sourceResourceSet,
613
        $sourceEntityInstance,
614
        ResourceSet $targetResourceSet,
615
        $targetEntityInstance,
616
        $navPropName
617
    ) {
618
        $relation = $this->isModelHookInputsOk($sourceEntityInstance, $targetEntityInstance, $navPropName);
619
        assert($sourceEntityInstance instanceof Model && $targetEntityInstance instanceof Model);
620
621
        if ($relation instanceof BelongsTo) {
622
            $relation->dissociate();
623
        } elseif ($relation instanceof BelongsToMany) {
624
            $relation->detach($targetEntityInstance);
625
        } elseif ($relation instanceof HasOneOrMany) {
626
            // dig up inverse property name, so we can pass it to unhookSingleModel with source and target elements
627
            // swapped
628
            $otherPropName = $this->getMetadataProvider()
629
                ->resolveReverseProperty($sourceEntityInstance, $targetEntityInstance, $navPropName);
630
            if (null === $otherPropName) {
631
                $msg = 'Bad navigation property, '.$navPropName.', on source model '.get_class($sourceEntityInstance);
632
                throw new \InvalidArgumentException($msg);
633
            }
634
            $this->unhookSingleModel(
635
                $targetResourceSet,
636
                $targetEntityInstance,
637
                $sourceResourceSet,
638
                $sourceEntityInstance,
639
                $otherPropName
640
            );
641
        }
642
        return true;
643
    }
644
645
    /**
646
     * @param $sourceEntityInstance
647
     * @param $targetEntityInstance
648
     * @param $navPropName
649
     * @return Relation
650
     * @throws \InvalidArgumentException
651
     */
652
    protected function isModelHookInputsOk($sourceEntityInstance, $targetEntityInstance, $navPropName)
653
    {
654
        if (!$sourceEntityInstance instanceof Model || !$targetEntityInstance instanceof Model) {
655
            $msg = 'Both source and target must be Eloquent models';
656
            throw new \InvalidArgumentException($msg);
657
        }
658
        $relation = $sourceEntityInstance->$navPropName();
659
        if (!$relation instanceof Relation) {
660
            $msg = 'Navigation property must be an Eloquent relation';
661
            throw new \InvalidArgumentException($msg);
662
        }
663
        $targType = $relation->getRelated();
664
        if (!$targetEntityInstance instanceof $targType) {
665
            $msg = 'Target instance must be of type compatible with relation declared in method '.$navPropName;
666
            throw new \InvalidArgumentException($msg);
667
        }
668
        return $relation;
669
    }
670
}
671