Completed
Push — develop ( bb979b...2f50d2 )
by Nate
09:37
created

Relationship::objectArray()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 0
cts 10
cp 0
rs 9.5222
c 0
b 0
f 0
cc 5
nc 4
nop 1
crap 30
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\ElementQueryInterface;
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\UnknownPropertyException;
25
use yii\db\QueryInterface;
26
27
/**
28
 * Manages User Types associated to Organization/User associations
29
 *
30
 * @author Flipbox Factory <[email protected]>
31
 * @since 3.0.0
32
 */
33
class Relationship extends BaseObject implements RelationshipInterface
34
{
35
    /**
36
     * The element the relations are related to
37
     *
38
     * @var ElementInterface|null
39
     */
40
    private $element;
41
42
    /**
43
     * The field which accesses the relations
44
     *
45
     * @var RelationalInterface
46
     */
47
    private $field;
48
49
    /**
50
     * The association records
51
     *
52
     * @var Collection|null
53
     */
54
    private $collection;
55
56
    /**
57
     * @var bool
58
     */
59
    protected $mutated = false;
60
61
    /**
62
     * @param ElementInterface|null $element
63
     * @param RelationalInterface $field
64
     * @param array $config
65
     */
66
    public function __construct(RelationalInterface $field, ElementInterface $element = null, array $config = [])
67
    {
68
        $this->element = $element;
69
        $this->field = $field;
70
71
        parent::__construct($config);
72
    }
73
74
    /**
75
     * @inheritDoc
76
     */
77
    public function isMutated(): bool
78
    {
79
        return $this->mutated;
80
    }
81
82
    /**
83
     * @inheritDoc
84
     */
85
    public function exists($object): bool
86
    {
87
        return null !== $this->findKey($object);
88
    }
89
90
    /************************************************************
91
     * QUERY
92
     ************************************************************/
93
94
    /**
95
     * @inheritDoc
96
     */
97
    public function getQuery(): ElementQueryInterface
98
    {
99
        return $this->field->getQuery($this->element);
100
    }
101
102
103
    /************************************************************
104
     * COLLECTIONS
105
     ************************************************************/
106
107
    /**
108
     * @return Collection
109
     */
110
    public function getElements(): Collection
111
    {
112
        if (null === $this->collection) {
113
            return Collection::make(
114
                $this->getQuery()->all()
115
            );
116
        };
117
118
        return Collection::make(
119
            $this->getQuery()
120
                ->id(
121
                    $this->collection
122
                        ->sortBy('sortOrder')
123
                        ->pluck('targetId')
124
                        ->all()
125
                )
126
                ->fixedOrder(true)
127
                ->limit(null)
128
                ->all()
129
        );
130
    }
131
132
    /**
133
     * @inheritDoc
134
     */
135
    public function getCollection(): Collection
136
    {
137
        if (null === $this->collection) {
138
            $this->collection = new Collection(
139
                $this->query()->all()
140
            );
141
        }
142
143
        return $this->collection;
144
    }
145
146
147
    /************************************************************
148
     * ADD / REMOVE
149
     ************************************************************/
150
151
    /**
152
     * @inheritDoc
153
     */
154
    public function add($objects, array $attributes = []): RelationshipInterface
155
    {
156
        foreach ($this->objectArray($objects) as $object) {
157
            if (null === ($association = $this->findOne($object))) {
158
                $association = $this->create($object);
159
                $this->addToCache($association);
160
            }
161
162
            if (!empty($attributes)) {
163
                Craft::configure(
164
                    $association,
165
                    $attributes
166
                );
167
168
                $this->mutated = true;
169
            }
170
        }
171
172
        return $this;
173
    }
174
175
    /**
176
     * @inheritDoc
177
     */
178
    public function remove($objects): RelationshipInterface
179
    {
180
        foreach ($this->objectArray($objects) as $object) {
181
            if (null !== ($key = $this->findKey($object))) {
182
                $this->removeFromCache($key);
183
            }
184
        }
185
186
        return $this;
187
    }
188
189
190
    /*******************************************
191
     * COMMIT
192
     *******************************************/
193
194
    /**
195
     * @return bool
196
     */
197
    public function save(): bool
198
    {
199
        // No changes?
200
        if (!$this->isMutated()) {
201
            return true;
202
        }
203
204
        $success = true;
205
206
        list($newAssociations, $existingAssociations) = $this->delta();
207
208
        // Delete those removed
209
        foreach ($existingAssociations as $existingAssociation) {
210
            if (!$existingAssociation->delete()) {
211
                $success = false;
212
            }
213
        }
214
215
        foreach ($newAssociations as $newAssociation) {
216
            if (!$newAssociation->save()) {
217
                $success = false;
218
            }
219
        }
220
221
        $this->setCache($newAssociations);
222
        $this->mutated = false;
223
224
        if (!$success && $this->element) {
225
            $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...
226
        }
227
228
        return $success;
229
    }
230
231
232
    /**
233
     * @inheritDoc
234
     */
235
    public function reset(): RelationshipInterface
236
    {
237
        $this->collection = null;
238
        $this->mutated = false;
239
        return $this;
240
    }
241
242
243
    /**
244
     * @inheritDoc
245
     */
246
    protected function delta(): array
247
    {
248
        $existingAssociations = $this->query()
249
            ->indexBy('targetId')
250
            ->all();
251
252
        $associations = [];
253
        $order = 1;
254
        /** @var Association $newAssociation */
255
        foreach ($this->getCollection()->sortBy('sortOrder') as $newAssociation) {
256
            if (null === ($association = ArrayHelper::remove(
257
                    $existingAssociations,
258
                    $newAssociation->targetId
259
                ))
260
            ) {
261
                $association = $newAssociation;
262
            }
263
264
            $association->sourceId = $newAssociation->sourceId;
265
            $association->targetId = $newAssociation->targetId;
266
            $association->fieldId = $newAssociation->fieldId;
267
            $association->sourceSiteId = $newAssociation->sourceSiteId;
268
            $association->sortOrder = $order++;
269
270
            $associations[] = $association;
271
        }
272
273
        return [$associations, $existingAssociations];
274
    }
275
276
277
    /**
278
     * Ensure we're working with an array of objects, not configs, etc
279
     * @param array|QueryInterface|Collection|ElementInterface|Association $objects
280
     * @return array
281
     */
282
    protected function objectArray($objects): array
283
    {
284
        if ($objects instanceof QueryInterface || $objects instanceof Collection) {
285
            $objects = $objects->all();
286
        }
287
288
        // proper array
289
        if (!is_array($objects) || ArrayHelper::isAssociative($objects)) {
290
            $objects = [$objects];
291
        }
292
293
        return array_filter($objects);
294
    }
295
296
297
    /*******************************************
298
     * CACHE
299
     *******************************************/
300
301
    /**
302
     * @param Association[] $associations
303
     * @param bool $mutated
304
     * @return static
305
     */
306
    protected function setCache(array $associations, bool $mutated = true): self
307
    {
308
        $this->collection = Collection::make($associations);
309
        $this->mutated = $mutated;
310
311
        return $this;
312
    }
313
314
    /**
315
     * @param Association $association
316
     * @return static
317
     */
318
    protected function addToCache(Association $association): self
319
    {
320
        if (null === $this->collection) {
321
            return $this->setCache([$association], true);
322
        }
323
324
        $this->collection->push($association);
325
        $this->mutated = true;
326
327
        return $this;
328
    }
329
330
    /**
331
     * @param int $key
332
     * @return static
333
     */
334
    protected function removeFromCache(int $key): self
335
    {
336
        $this->collection->forget($key);
337
        $this->mutated = true;
338
339
        return $this;
340
    }
341
342
    /**
343
     * @param array $criteria
344
     * @return AssociationQuery
345
     */
346
    protected function query(array $criteria = []): AssociationQuery
347
    {
348
        /** @noinspection PhpUndefinedMethodInspection */
349
        $query = Association::find()
350
            ->setSource($this->element->getId() ?: false)
351
            ->orderBy([
352
                'sortOrder' => SORT_ASC
353
            ]);
354
355
        if (!empty($criteria)) {
356
            QueryHelper::configure(
357
                $query,
358
                $criteria
359
            );
360
        }
361
362
        return $query;
363
    }
364
365
    /**
366
     * Create a new relationship object
367
     *
368
     * @param $object
369
     * @return Association
370
     */
371
    protected function create($object): Association
372
    {
373
        $element = $this->resolveElement($object);
374
375
        return new Association([
376
            '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...
377
            'sourceId' => $this->element ? $this->element->getId() : null,
378
            'targetId' => $element->getId()
379
        ]);
380
    }
381
382
    /**
383
     * @param Association|ElementInterface|int|string|null $object
384
     * @return Association|null
385
     */
386
    protected function findOne($object = null)
387
    {
388
        if (null === ($key = $this->findKey($object))) {
389
            return null;
390
        }
391
392
        return $this->getCollection()->get($key);
393
    }
394
395
    /**
396
     * @param UserAssociation|int|array|null $object
397
     * @return int|null
398
     */
399
    protected function findKey($object = null)
400
    {
401
        if (null === ($element = $this->resolveElement($object))) {
402
            ElementList::info(sprintf(
403
                "Unable to resolve relationship: %s",
404
                (string)Json::encode($object)
405
            ));
406
            return null;
407
        }
408
409
        // Todo - perform this lookup via Collection method
410
        foreach ($this->getCollection() as $key => $association) {
411
            if ($association->targetId == $element->getId()) {
412
                return $key;
413
            }
414
        }
415
416
        return null;
417
    }
418
419
    /**
420
     * @param ElementInterface|Association|int|array|null $element
421
     * @return ElementInterface|null
422
     */
423
    protected function resolveElement($element = null)
424
    {
425
        if (null === $element) {
426
            return null;
427
        }
428
429
        if ($element instanceof ElementInterface) {
430
            return $element;
431
        }
432
433
        if ($element instanceof Association) {
434
            $element = $element->targetId;
435
        }
436
437
        if (is_array($element) &&
438
            null !== ($id = ArrayHelper::getValue($element, 'id'))
439
        ) {
440
            $element = $id;
441
        }
442
443
        return $this->field->resolveElement($element);
444
    }
445
446
447
    /*******************************************
448
     * MAGIC
449
     *******************************************/
450
451
    /**
452
     * @param string $name
453
     * @return mixed
454
     */
455
    public function __get($name)
456
    {
457
        try {
458
            return parent::__get($name);
459
        } catch (UnknownPropertyException $e) {
460
            return $this->getQuery()->{$name};
461
        }
462
    }
463
464
    /**
465
     * @param string $name
466
     * @param mixed $value
467
     */
468
    public function __set($name, $value)
469
    {
470
        try {
471
            return parent::__set($name, $value);
472
        } catch (UnknownPropertyException $e) {
473
            return $this->getQuery()->{$name}($value);
474
        }
475
    }
476
477
    /**
478
     * @param string $name
479
     * @param array $params
480
     * @return mixed
481
     */
482
    public function __call($name, $params)
483
    {
484
        if ($this->getQuery()->hasMethod($name)) {
485
            return call_user_func_array([$this->getQuery(), $name], $params);
486
        }
487
488
        return parent::__call($name, $params);
489
    }
490
}
491