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

LaravelQuery::updateResource()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 9

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 12
ccs 0
cts 4
cp 0
rs 9.4285
cc 1
eloc 9
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 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[]|null returns the newly created model if successful, or null if model creation failed
0 ignored issues
show
Documentation introduced by
Should the return type not be array? Also, consider making the array more specific, something like array<String>, or String[].

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.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
500
     */
501
    public function createBulkResourceforResourceSet(
502
        ResourceSet $sourceResourceSet,
503
        array $data
504
    ) {
505
        $result = [];
506
        try {
507
            DB::beginTransaction();
508
            foreach ($data as $newItem) {
509
                $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...
510
                if (null === $raw) {
511
                    throw new \Exception('Bulk model creation failed');
512
                }
513
                $result[] = $raw;
514
            }
515
            DB::commit();
516
        } catch (\Exception $e) {
517
            DB::rollBack();
518
            throw $e;
519
        }
520
        return $result;
521
    }
522
523
    /**
524
     * Updates a group of resources in a resource set.
525
     *
526
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
527
     * @param object $sourceEntityInstance The source entity instance
528
     * @param KeyDescriptor[] $keyDescriptor The key identifying the entity to fetch
529
     * @param object[] $data The new data for the entity instances
530
     * @param bool $shouldUpdate Should undefined values be updated or reset to default
531
     *
532
     * @return object[]|null the new resource value if it is assignable, or throw exception for null
0 ignored issues
show
Documentation introduced by
Should the return type not be array? Also, consider making the array more specific, something like array<String>, or String[].

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.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
533
     */
534
    public function updateBulkResource(
535
        ResourceSet $sourceResourceSet,
536
        $sourceEntityInstance,
537
        array $keyDescriptor,
538
        array $data,
539
        $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...
540
    ) {
541
        $numKeys = count($keyDescriptor);
542
        if ($numKeys !== count($data)) {
543
            $msg = 'Key descriptor array and data array must be same length';
544
            throw new \InvalidArgumentException($msg);
545
        }
546
        $result = [];
547
548
        try {
549
            DB::beginTransaction();
550
            for ($i = 0; $i < $numKeys; $i++) {
551
                $newItem = $data[$i];
552
                $newKey = $keyDescriptor[$i];
553
                $raw = $this->updateResource($sourceResourceSet, $sourceEntityInstance, $newKey, $newItem);
554
                if (null === $raw) {
555
                    throw new \Exception('Bulk model update failed');
556
                }
557
                $result[] = $raw;
558
            }
559
            DB::commit();
560
        } catch (\Exception $e) {
561
            DB::rollBack();
562
            throw $e;
563
        }
564
        return $result;
565
    }
566
567
    /**
568
     * Attaches child model to parent model
569
     *
570
     * @param ResourceSet $sourceResourceSet
571
     * @param object $sourceEntityInstance
572
     * @param ResourceSet $targetResourceSet
573
     * @param object $targetEntityInstance
574
     * @param $navPropName
575
     *
576
     * @return bool
577
     */
578
    public function hookSingleModel(
1 ignored issue
show
Coding Style introduced by
function hookSingleModel() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
579
        ResourceSet $sourceResourceSet,
580
        $sourceEntityInstance,
581
        ResourceSet $targetResourceSet,
582
        $targetEntityInstance,
583
        $navPropName
584
    ) {
585
        $relation = $this->isModelHookInputsOk($sourceEntityInstance, $targetEntityInstance, $navPropName);
586
        assert($sourceEntityInstance instanceof Model && $targetEntityInstance instanceof Model);
587
588
        if ($relation instanceof BelongsTo) {
589
            $relation->associate($targetEntityInstance);
590
        } elseif ($relation instanceof BelongsToMany) {
591
            $relation->attach($targetEntityInstance);
592
        } elseif ($relation instanceof HasOneOrMany) {
593
            $relation->save($targetEntityInstance);
594
        }
595
        return true;
596
    }
597
598
    /**
599
     * Removes child model from parent model
600
     *
601
     * @param ResourceSet $sourceResourceSet
602
     * @param object $sourceEntityInstance
603
     * @param ResourceSet $targetResourceSet
604
     * @param object $targetEntityInstance
605
     * @param $navPropName
606
     *
607
     * @return bool
608
     */
609
    public function unhookSingleModel(
1 ignored issue
show
Coding Style introduced by
function unhookSingleModel() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
610
        ResourceSet $sourceResourceSet,
611
        $sourceEntityInstance,
612
        ResourceSet $targetResourceSet,
613
        $targetEntityInstance,
614
        $navPropName
615
    ) {
616
        $relation = $this->isModelHookInputsOk($sourceEntityInstance, $targetEntityInstance, $navPropName);
617
        assert($sourceEntityInstance instanceof Model && $targetEntityInstance instanceof Model);
618
619
        if ($relation instanceof BelongsTo) {
620
            $relation->dissociate();
621
        } elseif ($relation instanceof BelongsToMany) {
622
            $relation->detach($targetEntityInstance);
623
        } elseif ($relation instanceof HasOneOrMany) {
624
            // dig up inverse property name, so we can pass it to unhookSingleModel with source and target elements
625
            // swapped
626
            $otherPropName = $this->getMetadataProvider()
627
                ->resolveReverseProperty($sourceEntityInstance, $targetEntityInstance, $navPropName);
628
            if (null === $otherPropName) {
629
                $msg = 'Bad navigation property, '.$navPropName.', on source model '.get_class($sourceEntityInstance);
630
                throw new \InvalidArgumentException($msg);
631
            }
632
            $this->unhookSingleModel(
633
                $targetResourceSet,
634
                $targetEntityInstance,
635
                $sourceResourceSet,
636
                $sourceEntityInstance,
637
                $otherPropName
638
            );
639
        }
640
        return true;
641
    }
642
643
    /**
644
     * @param $sourceEntityInstance
645
     * @param $targetEntityInstance
646
     * @param $navPropName
647
     * @return Relation
648
     * @throws \InvalidArgumentException
649
     */
650
    protected function isModelHookInputsOk($sourceEntityInstance, $targetEntityInstance, $navPropName)
651
    {
652
        if (!$sourceEntityInstance instanceof Model || !$targetEntityInstance instanceof Model) {
653
            $msg = 'Both source and target must be Eloquent models';
654
            throw new \InvalidArgumentException($msg);
655
        }
656
        $relation = $sourceEntityInstance->$navPropName();
657
        if (!$relation instanceof Relation) {
658
            $msg = 'Navigation property must be an Eloquent relation';
659
            throw new \InvalidArgumentException($msg);
660
        }
661
        $targType = $relation->getRelated();
662
        if (!$targetEntityInstance instanceof $targType) {
663
            $msg = 'Target instance must be of type compatible with relation declared in method '.$navPropName;
664
            throw new \InvalidArgumentException($msg);
665
        }
666
        return $relation;
667
    }
668
}
669