Completed
Push — develop ( 2f50d2...a740b5 )
by Nate
04:32
created

Relationship::removeFromCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 0
cts 6
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
1
<?php
2
3
/**
4
 * @copyright  Copyright (c) Flipbox Digital Limited
5
 * @license    https://github.com/flipboxfactory/craft-element-lists/LICENSE
6
 * @link       https://github.com/flipboxfactory/craft-element-lists/
7
 */
8
9
namespace flipbox\craft\element\lists\relationships;
10
11
use Craft;
12
use craft\base\ElementInterface;
13
use craft\elements\db\ElementQuery;
14
use craft\helpers\ArrayHelper;
15
use craft\helpers\Json;
16
use flipbox\craft\element\lists\ElementList;
17
use flipbox\craft\element\lists\fields\RelationalInterface;
18
use flipbox\craft\element\lists\queries\AssociationQuery;
19
use flipbox\craft\element\lists\records\Association;
20
use flipbox\craft\ember\helpers\QueryHelper;
21
use flipbox\organizations\records\UserAssociation;
22
use Tightenco\Collect\Support\Collection;
23
use yii\base\BaseObject;
24
use yii\base\Exception;
25
use yii\base\UnknownPropertyException;
26
use yii\db\QueryInterface;
27
28
/**
29
 * Manages User Types associated to Organization/User associations
30
 *
31
 * @author Flipbox Factory <[email protected]>
32
 * @since 3.0.0
33
 */
34
class Relationship extends BaseObject implements RelationshipInterface
35
{
36
    /**
37
     * The element the relations are related to
38
     *
39
     * @var ElementInterface|null
40
     */
41
    private $element;
42
43
    /**
44
     * The field which accesses the relations
45
     *
46
     * @var RelationalInterface
47
     */
48
    private $field;
49
50
    /**
51
     * The association records
52
     *
53
     * @var Collection|null
54
     */
55
    private $relations;
56
57
    /**
58
     * @var bool
59
     */
60
    protected $mutated = false;
61
62
    /**
63
     * @param ElementInterface|null $element
64
     * @param RelationalInterface $field
65
     * @param array $config
66
     */
67
    public function __construct(RelationalInterface $field, ElementInterface $element = null, array $config = [])
68
    {
69
        $this->element = $element;
70
        $this->field = $field;
71
72
        parent::__construct($config);
73
    }
74
75
76
    /************************************************************
77
     * QUERY
78
     ************************************************************/
79
80
    /**
81
     * @param Association|ElementInterface|int|string $object
82
     * @return Association
83
     */
84
    public function findOrCreate($object): Association
85
    {
86
        if (null === ($association = $this->findOne($object))) {
87
            $association = $this->create($object);
88
        }
89
90
        return $association;
91
    }
92
93
    /**
94
     * @param Association|ElementInterface|int|string $object
95
     * @return Association
96
     * @throws Exception
97
     */
98
    public function findOrFail($object): Association
99
    {
100
        if (null === ($association = $this->findOne($object))) {
101
            throw new Exception("Relationship could not be found.");
102
        }
103
104
        return $association;
105
    }
106
107
108
    /************************************************************
109
     * COLLECTIONS
110
     ************************************************************/
111
112
    /**
113
     * @return Collection
114
     */
115
    public function getCollection(): Collection
116
    {
117
        if (null === $this->relations) {
118
            return Collection::make(
119
                $this->field->getQuery($this->element)->all()
120
            );
121
        };
122
123
        return Collection::make(
124
            $this->field->getQuery($this->element)
125
                ->id(
126
                    $this->relations
127
                        ->sortBy('sortOrder')
128
                        ->pluck('targetId')
129
                        ->all()
130
                )
131
                ->fixedOrder(true)
132
                ->limit(null)
133
                ->all()
134
        );
135
    }
136
137
    /**
138
     * @inheritDoc
139
     */
140
    public function getRelationships(): Collection
141
    {
142
        if (null === $this->relations) {
143
            $this->newRelations($this->query()->all(), false);
144
        }
145
146
        return $this->relations;
147
    }
148
149
150
    /************************************************************
151
     * ADD / REMOVE
152
     ************************************************************/
153
154
    /**
155
     * @inheritDoc
156
     */
157
    public function add($objects, array $attributes = []): RelationshipInterface
158
    {
159
        foreach ($this->objectArray($objects) as $object) {
160
            if (null === ($association = $this->findOne($object))) {
161
                $association = $this->create($object);
162
                $this->addToRelations($association);
163
            }
164
165
            if (!empty($attributes)) {
166
                Craft::configure(
167
                    $association,
168
                    $attributes
169
                );
170
171
                $this->mutated = true;
172
            }
173
        }
174
175
        return $this;
176
    }
177
178
    /**
179
     * @inheritDoc
180
     */
181
    public function remove($objects): RelationshipInterface
182
    {
183
        foreach ($this->objectArray($objects) as $object) {
184
            if (null !== ($key = $this->findKey($object))) {
185
                $this->removeFromRelations($key);
186
            }
187
        }
188
189
        return $this;
190
    }
191
192
193
    /*******************************************
194
     * COMMIT
195
     *******************************************/
196
197
    /**
198
     * @return bool
199
     */
200
    public function save(): bool
201
    {
202
        // No changes?
203
        if (!$this->isMutated()) {
204
            return true;
205
        }
206
207
        $success = true;
208
209
        list($newAssociations, $existingAssociations) = $this->delta();
210
211
        // Delete those removed
212
        foreach ($existingAssociations as $existingAssociation) {
213
            if (!$existingAssociation->delete()) {
214
                $success = false;
215
            }
216
        }
217
218
        foreach ($newAssociations as $newAssociation) {
219
            if (!$newAssociation->save()) {
220
                $success = false;
221
            }
222
        }
223
224
        $this->newRelations($newAssociations);
225
        $this->mutated = false;
226
227
        if (!$success && $this->element) {
228
            $this->element->addError($this->field->handle, 'Unable to save relationship.');
0 ignored issues
show
Bug introduced by
Accessing handle on the interface flipbox\craft\element\li...lds\RelationalInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
229
        }
230
231
        return $success;
232
    }
233
234
    /**
235
     * @inheritDoc
236
     */
237
    public function clear(): RelationshipInterface
238
    {
239
        $this->newRelations([]);
240
        return $this;
241
    }
242
243
244
    /**
245
     * @inheritDoc
246
     */
247
    public function reset(): RelationshipInterface
248
    {
249
        $this->relations = null;
250
        $this->mutated = false;
251
        return $this;
252
    }
253
254
    /**
255
     * @inheritDoc
256
     */
257
    public function isMutated(): bool
258
    {
259
        return $this->mutated;
260
    }
261
262
    /**
263
     * @inheritDoc
264
     */
265
    public function exists($object): bool
266
    {
267
        return null !== $this->findKey($object);
268
    }
269
270
    /**
271
     * @inheritDoc
272
     */
273
    public function count()
274
    {
275
        return $this->getCollection()->count();
276
    }
277
278
    /**
279
     * @inheritDoc
280
     */
281
    protected function delta(): array
282
    {
283
        $existingAssociations = $this->query()
284
            ->indexBy('targetId')
285
            ->all();
286
287
        $associations = [];
288
        $order = 1;
289
        /** @var Association $newAssociation */
290
        foreach ($this->getRelationships()->sortBy('sortOrder') as $newAssociation) {
291
            if (null === ($association = ArrayHelper::remove(
292
                $existingAssociations,
293
                $newAssociation->targetId
294
            ))
295
            ) {
296
                $association = $newAssociation;
297
            }
298
299
            $association->sourceId = $newAssociation->sourceId;
300
            $association->targetId = $newAssociation->targetId;
301
            $association->fieldId = $newAssociation->fieldId;
302
            $association->sourceSiteId = $newAssociation->sourceSiteId;
303
            $association->sortOrder = $order++;
304
305
            $associations[] = $association;
306
        }
307
308
        return [$associations, $existingAssociations];
309
    }
310
311
312
    /**
313
     * Ensure we're working with an array of objects, not configs, etc
314
     *
315
     * @param array|QueryInterface|Collection|ElementInterface|Association $objects
316
     * @return array
317
     */
318
    protected function objectArray($objects): array
319
    {
320
        if ($objects instanceof QueryInterface || $objects instanceof Collection) {
321
            $objects = $objects->all();
322
        }
323
324
        // proper array
325
        if (!is_array($objects) || ArrayHelper::isAssociative($objects)) {
326
            $objects = [$objects];
327
        }
328
329
        return array_filter($objects);
330
    }
331
332
333
    /*******************************************
334
     * CACHE
335
     *******************************************/
336
337
    /**
338
     * @param Association[] $associations
339
     * @param bool $mutated
340
     * @return static
341
     */
342
    protected function newRelations(array $associations, bool $mutated = true): self
343
    {
344
        $this->relations = Collection::make($associations);
345
        $this->mutated = $mutated;
346
347
        return $this;
348
    }
349
350
    /**
351
     * @param Association $association
352
     * @return static
353
     */
354
    protected function addToRelations(Association $association): self
355
    {
356
        if (null === $this->relations) {
357
            return $this->newRelations([$association], true);
358
        }
359
360
        $this->relations->push($association);
361
        $this->mutated = true;
362
363
        return $this;
364
    }
365
366
    /**
367
     * @param int $key
368
     * @return static
369
     */
370
    protected function removeFromRelations(int $key): self
371
    {
372
        $this->relations->forget($key);
373
        $this->mutated = true;
374
375
        return $this;
376
    }
377
378
    /**
379
     * @param array $criteria
380
     * @return AssociationQuery
381
     */
382
    protected function query(array $criteria = []): AssociationQuery
383
    {
384
        /** @noinspection PhpUndefinedMethodInspection */
385
        $query = Association::find()
386
            ->setSource($this->element->getId() ?: false)
387
            ->orderBy([
388
                'sortOrder' => SORT_ASC
389
            ]);
390
391
        if (!empty($criteria)) {
392
            QueryHelper::configure(
393
                $query,
394
                $criteria
395
            );
396
        }
397
398
        return $query;
399
    }
400
401
    /**
402
     * Create a new relationship object
403
     *
404
     * @param $object
405
     * @return Association
406
     */
407
    protected function create($object): Association
408
    {
409
        $element = $this->resolveElement($object);
410
411
        return new Association([
412
            'field' => $this->field->id,
0 ignored issues
show
Bug introduced by
Accessing id on the interface flipbox\craft\element\li...lds\RelationalInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
413
            'sourceId' => $this->element ? $this->element->getId() : null,
414
            'targetId' => $element->getId()
415
        ]);
416
    }
417
418
    /**
419
     * @param Association|ElementInterface|int|string|null $object
420
     * @return Association|null
421
     */
422
    public function findOne($object = null)
423
    {
424
        if (null === ($key = $this->findKey($object))) {
425
            return null;
426
        }
427
428
        return $this->getRelationships()->get($key);
429
    }
430
431
    /**
432
     * @param UserAssociation|int|array|null $object
433
     * @return int|null
434
     */
435
    protected function findKey($object = null)
436
    {
437
        if (null === ($element = $this->resolveElement($object))) {
438
            ElementList::info(sprintf(
439
                "Unable to resolve relationship: %s",
440
                (string)Json::encode($object)
441
            ));
442
            return null;
443
        }
444
445
        // Todo - perform this lookup via Collection method
446
        foreach ($this->getRelationships() as $key => $association) {
447
            if ($association->targetId == $element->getId()) {
448
                return $key;
449
            }
450
        }
451
452
        return null;
453
    }
454
455
    /**
456
     * @param ElementInterface|Association|int|array|null $element
457
     * @return ElementInterface|null
458
     */
459
    protected function resolveElement($element = null)
460
    {
461
        if (null === $element) {
462
            return null;
463
        }
464
465
        if ($element instanceof ElementInterface) {
466
            return $element;
467
        }
468
469
        if ($element instanceof Association) {
470
            $element = $element->targetId;
471
        }
472
473
        if (is_array($element) &&
474
            null !== ($id = ArrayHelper::getValue($element, 'id'))
475
        ) {
476
            $element = $id;
477
        }
478
479
        return $this->field->resolveElement($element);
480
    }
481
482
483
    /*******************************************
484
     * MAGIC (pass calls onto query)
485
     *******************************************/
486
487
    /**
488
     * @param string $name
489
     * @return mixed
490
     */
491
    public function __get($name)
492
    {
493
        try {
494
            return parent::__get($name);
495
        } catch (UnknownPropertyException $e) {
496
            return $this->field->getQuery($this->element)->{$name};
497
        }
498
    }
499
500
    /**
501
     * @param string $name
502
     * @param mixed $value
503
     */
504
    public function __set($name, $value)
505
    {
506
        try {
507
            return parent::__set($name, $value);
508
        } catch (UnknownPropertyException $e) {
509
            return $this->field->getQuery($this->element)->{$name}($value);
510
        }
511
    }
512
513
    /**
514
     * @param string $name
515
     * @param array $params
516
     * @return mixed
517
     */
518
    public function __call($name, $params)
519
    {
520
        /** @var ElementQuery $query */
521
        $query = $this->field->getQuery($this->element);
522
        if ($query->hasMethod($name)) {
523
            return call_user_func_array([$query, $name], $params);
524
        }
525
526
        return parent::__call($name, $params);
527
    }
528
}
529