Test Failed
Push — master ( 1d3103...bce4af )
by Alex
01:28
created

LaravelQuery::getRelatedResourceSet()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 26
Code Lines 23

Duplication

Lines 26
Ratio 100 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 26
loc 26
ccs 0
cts 6
cp 0
rs 8.8571
cc 1
eloc 23
nc 1
nop 10
crap 2

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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