Completed
Push — develop ( 0ff0ed...a13565 )
by Nate
12:12
created

Integrations::normalizeQueryEmptyValue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 6
c 0
b 0
f 0
ccs 0
cts 5
cp 0
rs 10
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-integration/blob/master/LICENSE
6
 * @link       https://github.com/flipboxfactory/craft-integration/
7
 */
8
9
namespace flipbox\craft\integration\fields;
10
11
use Craft;
12
use craft\base\Element;
13
use craft\base\ElementInterface;
14
use craft\base\Field;
15
use craft\elements\db\ElementQuery;
16
use craft\elements\db\ElementQueryInterface;
17
use craft\helpers\Component as ComponentHelper;
18
use craft\helpers\Db;
19
use craft\helpers\StringHelper;
20
use flipbox\craft\ember\helpers\ModelHelper;
21
use flipbox\craft\ember\records\ActiveRecord;
22
use flipbox\craft\ember\validators\MinMaxValidator;
23
use flipbox\craft\integration\events\RegisterIntegrationFieldActionsEvent;
24
use flipbox\craft\integration\fields\actions\IntegrationActionInterface;
25
use flipbox\craft\integration\fields\actions\IntegrationItemActionInterface;
26
use flipbox\craft\integration\queries\IntegrationAssociationQuery;
27
use flipbox\craft\integration\records\IntegrationAssociation;
28
use flipbox\craft\integration\web\assets\integrations\Integrations as IntegrationsAsset;
29
use yii\base\Exception;
30
use yii\helpers\ArrayHelper;
31
32
/**
33
 * @author Flipbox Factory <[email protected]>
34
 * @since 1.0.0
35
 */
36
abstract class Integrations extends Field
37
{
38
    /**
39
     * The Plugin's translation category
40
     */
41
    const TRANSLATION_CATEGORY = '';
42
43
    /**
44
     * The action path to preform field actions
45
     */
46
    const ACTION_PREFORM_ACTION_PATH = '';
47
48
    /**
49
     * The action path to preform field actions
50
     */
51
    const ACTION_PREFORM_ITEM_ACTION_PATH = '';
52
53
    /**
54
     * The action path to associate an item
55
     */
56
    const ACTION_ASSOCIATION_ITEM_PATH = '';
57
58
    /**
59
     * The action path to dissociate an item
60
     */
61
    const ACTION_DISSOCIATION_ITEM_PATH = '';
62
63
    /**
64
     * The action path to create an integration object
65
     */
66
    const ACTION_CREATE_ITEM_PATH = '';
67
68
    /**
69
     * The action event name
70
     */
71
    const EVENT_REGISTER_ACTIONS = 'registerActions';
72
73
    /**
74
     * The action event name
75
     */
76
    const EVENT_REGISTER_AVAILABLE_ACTIONS = 'registerAvailableActions';
77
78
    /**
79
     * The item action event name
80
     */
81
    const EVENT_REGISTER_ITEM_ACTIONS = 'registerItemActions';
82
83
    /**
84
     * The item action event name
85
     */
86
    const EVENT_REGISTER_AVAILABLE_ITEM_ACTIONS = 'registerAvailableItemActions';
87
88
    /**
89
     * The input template path
90
     */
91
    const INPUT_TEMPLATE_PATH = '';
92
93
    /**
94
     * The input template path
95
     */
96
    const INPUT_ITEM_TEMPLATE_PATH = '_inputItem';
97
98
    /**
99
     * The input template path
100
     */
101
    const SETTINGS_TEMPLATE_PATH = '';
102
103
    /**
104
     * @var string
105
     */
106
    public $object;
107
108
    /**
109
     * @var int|null
110
     */
111
    public $min;
112
113
    /**
114
     * @var int|null
115
     */
116
    public $max;
117
118
    /**
119
     * @var string
120
     */
121
    public $viewUrl = '';
122
123
    /**
124
     * @var string
125
     */
126
    public $listUrl = '';
127
128
    /**
129
     * @var IntegrationActionInterface[]
130
     */
131
    public $selectedActions = [];
132
133
    /**
134
     * @var IntegrationItemActionInterface[]
135
     */
136
    public $selectedItemActions = [];
137
138
    /**
139
     * @var string|null
140
     */
141
    public $selectionLabel;
142
143
    /**
144
     * @inheritdoc
145
     */
146
    protected $defaultAvailableActions = [];
147
148
    /**
149
     * @inheritdoc
150
     */
151
    protected $defaultAvailableItemActions = [];
152
153
    /**
154
     * @return string
155
     */
156
    abstract public static function recordClass(): string;
157
158
    /**
159
     * @inheritdoc
160
     */
161
    public static function hasContentColumn(): bool
162
    {
163
        return false;
164
    }
165
166
    /**
167
     * @return string
168
     */
169
    public static function defaultSelectionLabel(): string
170
    {
171
        return Craft::t(static::TRANSLATION_CATEGORY, 'Add an Object');
172
    }
173
174
    /**
175
     * @return string
176
     */
177
    protected static function tableAlias(): string
178
    {
179
        /** @var ActiveRecord $recordClass */
180
        $recordClass = static::recordClass();
181
        return $recordClass::tableAlias();
182
    }
183
184
    /*******************************************
185
     * OBJECT
186
     *******************************************/
187
188
    /**
189
     * @return string
190
     */
191
    public function getObjectLabel(): string
192
    {
193
        return StringHelper::titleize($this->object);
194
    }
195
196
    /*******************************************
197
     * VALIDATION
198
     *******************************************/
199
200
    /**
201
     * @inheritdoc
202
     */
203
    public function getElementValidationRules(): array
204
    {
205
        return [
206
            [
207
                MinMaxValidator::class,
208
                'min' => $this->min ? (int)$this->min : null,
209
                'max' => $this->max ? (int)$this->max : null,
210
                'tooFew' => Craft::t(
211
                    static::TRANSLATION_CATEGORY,
212
                    '{attribute} should contain at least {min, number} {min, plural, one{domain} other{domains}}.'
213
                ),
214
                'tooMany' => Craft::t(
215
                    static::TRANSLATION_CATEGORY,
216
                    '{attribute} should contain at most {max, number} {max, plural, one{domain} other{domains}}.'
217
                ),
218
                'skipOnEmpty' => false
219
            ]
220
        ];
221
    }
222
223
    /*******************************************
224
     * NORMALIZE VALUE
225
     *******************************************/
226
227
    /**
228
     * @param $value
229
     * @param ElementInterface|null $element
230
     * @return IntegrationAssociationQuery
231
     */
232
    public function normalizeValue(
233
        $value,
234
        ElementInterface $element = null
235
    ) {
236
    
237
        if ($value instanceof IntegrationAssociationQuery) {
238
            return $value;
239
        }
240
241
        $query = $this->getQuery($element);
242
        $this->normalizeQueryValue($query, $value, $element);
243
        return $query;
244
    }
245
246
    /**
247
     * @param ElementInterface|null $element
248
     * @return IntegrationAssociationQuery
249
     */
250
    protected function getQuery(ElementInterface $element = null): IntegrationAssociationQuery
251
    {
252
        /** @var IntegrationAssociation $recordClass */
253
        $recordClass = static::recordClass();
254
255
        $query = $recordClass::find();
256
257
        if ($this->max !== null) {
258
            $query->limit($this->max);
259
        }
260
261
        $query->field($this)
262
            ->siteId($this->targetSiteId($element))
263
            ->elementId(($element === null || $element->getId() === null
264
            ) ? false : $element->getId());
265
266
        return $query;
267
    }
268
269
    /**
270
     * @param IntegrationAssociationQuery $query
271
     * @param $value
272
     * @param ElementInterface|null $element
273
     */
274
    protected function normalizeQueryValue(
275
        IntegrationAssociationQuery $query,
276
        $value,
277
        ElementInterface $element = null
278
    ) {
279
    
280
        if (is_array($value)) {
281
            $this->normalizeQueryInputValues($query, $value, $element);
282
            return;
283
        }
284
285
        if ($value === '') {
286
            $this->normalizeQueryEmptyValue($query);
287
            return;
288
        }
289
    }
290
291
    /**
292
     * @param IntegrationAssociationQuery $query
293
     * @param array $value
294
     * @param ElementInterface|null $element
295
     */
296
    protected function normalizeQueryInputValues(
297
        IntegrationAssociationQuery $query,
298
        array $value,
299
        ElementInterface $element = null
300
    ) {
301
    
302
        $models = [];
303
        $sortOrder = 1;
304
        foreach ($value as $val) {
305
            $models[] = $this->normalizeQueryInputValue($val, $sortOrder, $element);
306
        }
307
        $query->setCachedResult($models);
308
    }
309
310
    /**
311
     * @param $value
312
     * @param int $sortOrder
313
     * @param ElementInterface|null $element
314
     * @return IntegrationAssociation
315
     */
316
    protected function normalizeQueryInputValue(
317
        $value,
318
        int &$sortOrder,
319
        ElementInterface $element = null
320
    ): IntegrationAssociation {
321
    
322
        if (is_array($value)) {
323
            $value = StringHelper::toString($value);
324
        }
325
326
        /** @var IntegrationAssociation $recordClass */
327
        $recordClass = static::recordClass();
328
329
        /** @var IntegrationAssociation $association */
330
        $association = new $recordClass();
331
        $association->setField($this)
332
            ->setElement($element)
333
            ->setSiteId($this->targetSiteId($element));
334
335
        $association->sortOrder = $sortOrder++;
336
        $association->objectId = $value;
337
338
        return $association;
339
    }
340
341
    /**
342
     * @param IntegrationAssociationQuery $query
343
     */
344
    protected function normalizeQueryEmptyValue(
345
        IntegrationAssociationQuery $query
346
    ) {
347
    
348
        $query->setCachedResult([]);
349
    }
350
351
    /**
352
     * Returns the site ID that target elements should have.
353
     *
354
     * @param ElementInterface|Element|null $element
355
     *
356
     * @return int
357
     */
358
    protected function targetSiteId(ElementInterface $element = null): int
359
    {
360
        /** @var Element $element */
361
        if (Craft::$app->getIsMultiSite() === true && $element !== null) {
362
            return $element->siteId;
363
        }
364
365
        return Craft::$app->getSites()->currentSite->id;
366
    }
367
368
369
    /*******************************************
370
     * MODIFY ELEMENT QUERY
371
     *******************************************/
372
373
    /**
374
     * @inheritdoc
375
     */
376
    public function modifyElementsQuery(ElementQueryInterface $query, $value)
377
    {
378
        if ($value === null || !$query instanceof ElementQuery) {
379
            return null;
380
        }
381
382
        if ($value === false) {
383
            return false;
384
        }
385
386
        if (is_string($value)) {
387
            $this->modifyElementsQueryForStringValue($query, $value);
388
            return null;
389
        }
390
391
        $this->modifyElementsQueryForTargetValue($query, $value);
392
        return null;
393
    }
394
395
    /**
396
     * @param ElementQuery $query
397
     * @param string $value
398
     */
399
    protected function modifyElementsQueryForStringValue(
400
        ElementQuery $query,
401
        string $value
402
    ) {
403
    
404
        if ($value === 'not :empty:') {
405
            $value = ':notempty:';
406
        }
407
408
        if ($value === ':notempty:' || $value === ':empty:') {
409
            $this->modifyElementsQueryForEmptyValue($query, $value);
410
            return;
411
        }
412
413
        $this->modifyElementsQueryForTargetValue($query, $value);
414
    }
415
416
    /**
417
     * @param ElementQuery $query
418
     * @param $value
419
     */
420
    protected function modifyElementsQueryForTargetValue(
421
        ElementQuery $query,
422
        $value
423
    ) {
424
    
425
        $alias = $this->tableAlias();
426
        $name = '{{%' . $this->tableAlias() . '}}';
427
428
        $joinTable = "{$name} {$alias}";
429
        $query->query->innerJoin($joinTable, "[[{$alias}.elementId]] = [[subquery.elementsId]]");
430
        $query->subQuery->innerJoin($joinTable, "[[{$alias}.elementId]] = [[elements.id]]");
431
432
        $query->subQuery->andWhere(
433
            Db::parseParam($alias . '.fieldId', $this->id)
1 ignored issue
show
Bug introduced by
It seems like \craft\helpers\Db::parse... '.fieldId', $this->id) targeting craft\helpers\Db::parseParam() can also be of type string; however, craft\db\Query::andWhere() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
434
        );
435
436
        $query->subQuery->andWhere(
437
            Db::parseParam($alias . '.objectId', $value)
1 ignored issue
show
Bug introduced by
It seems like \craft\helpers\Db::parse... . '.objectId', $value) targeting craft\helpers\Db::parseParam() can also be of type string; however, craft\db\Query::andWhere() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
438
        );
439
440
        $query->query->distinct(true);
441
    }
442
443
    /**
444
     * @param ElementQuery $query
445
     * @param string $value
446
     */
447
    protected function modifyElementsQueryForEmptyValue(
448
        ElementQuery $query,
449
        string $value
450
    ) {
451
    
452
        $operator = ($value === ':notempty:' ? '!=' : '=');
453
        $query->subQuery->andWhere(
454
            $this->emptyValueSubSelect(
0 ignored issues
show
Documentation introduced by
$this->emptyValueSubSele...as() . '}}', $operator) is of type string, but the function expects a array.

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...
455
                $this->tableAlias(),
456
                '{{%' . $this->tableAlias() . '}}',
457
                $operator
458
            )
459
        );
460
    }
461
462
    /**
463
     * @param string $alias
464
     * @param string $name
465
     * @param string $operator
466
     * @return string
467
     */
468
    protected function emptyValueSubSelect(
469
        string $alias,
470
        string $name,
471
        string $operator
472
    ): string {
473
    
474
        return "(select count([[{$alias}.elementId]]) from " .
475
            $name .
476
            " {{{$alias}}} where [[{$alias}.elementId" .
477
            "]] = [[elements.id]] and [[{$alias}.fieldId]] = {$this->id}) {$operator} 0";
478
    }
479
480
481
    /*******************************************
482
     * RULES
483
     *******************************************/
484
485
    /**
486
     * @inheritdoc
487
     */
488
    public function rules()
489
    {
490
        return array_merge(
491
            parent::rules(),
492
            [
493
                [
494
                    'object',
495
                    'required',
496
                    'message' => Craft::t(static::TRANSLATION_CATEGORY, 'Object cannot be empty.')
497
                ],
498
                [
499
                    [
500
                        'object',
501
                        'min',
502
                        'max',
503
                        'viewUrl',
504
                        'listUrl',
505
                        'selectionLabel'
506
                    ],
507
                    'safe',
508
                    'on' => [
509
                        ModelHelper::SCENARIO_DEFAULT
510
                    ]
511
                ]
512
            ]
513
        );
514
    }
515
516
517
    /*******************************************
518
     * SEARCH
519
     *******************************************/
520
521
    /**
522
     * @param IntegrationAssociationQuery $value
523
     * @inheritdoc
524
     */
525
    public function getSearchKeywords($value, ElementInterface $element): string
526
    {
527
        $objects = [];
528
529
        /** @var IntegrationAssociation $association */
530
        foreach ($value->all() as $association) {
531
            array_push($objects, $association->objectId);
532
        }
533
534
        return parent::getSearchKeywords($objects, $element);
535
    }
536
537
538
    /*******************************************
539
     * VIEWS
540
     *******************************************/
541
542
    /**
543
     * @inheritdoc
544
     * @param IntegrationAssociationQuery $value
545
     * @throws \Twig_Error_Loader
546
     * @throws \yii\base\Exception
547
     */
548
    public function getInputHtml($value, ElementInterface $element = null): string
549
    {
550
        $value->limit(null);
551
552
        Craft::$app->getView()->registerAssetBundle(IntegrationsAsset::class);
553
554
        return Craft::$app->getView()->renderTemplate(
555
            static::INPUT_TEMPLATE_PATH,
556
            $this->inputHtmlVariables($value, $element)
557
        );
558
    }
559
560
    /**
561
     * @param IntegrationAssociationQuery $query
562
     * @param ElementInterface|null $element
563
     * @param bool $static
564
     * @return array
565
     * @throws \craft\errors\MissingComponentException
566
     * @throws \yii\base\InvalidConfigException
567
     */
568
    protected function inputHtmlVariables(
569
        IntegrationAssociationQuery $query,
570
        ElementInterface $element = null,
571
        bool $static = false
572
    ): array {
573
    
574
        return [
575
            'field' => $this,
576
            'element' => $element,
577
            'value' => $query,
578
            'objectLabel' => $this->getObjectLabel(),
579
            'static' => $static,
580
            'itemTemplate' => static::INPUT_ITEM_TEMPLATE_PATH,
581
            'settings' => [
582
                'translationCategory' => static::TRANSLATION_CATEGORY,
583
                'limit' => $this->max ? $this->max : null,
584
                'data' => [
585
                    'field' => $this->id,
586
                    'element' => $element ? $element->getId() : null
587
                ],
588
                'actions' => $this->getActionHtml($element),
589
                'actionAction' => static::ACTION_PREFORM_ACTION_PATH,
590
                'createItemAction' => static::ACTION_CREATE_ITEM_PATH,
591
                'itemData' => [
592
                    'field' => $this->id,
593
                    'element' => $element ? $element->getId() : null
594
                ],
595
                'itemSettings' => [
596
                    'translationCategory' => static::TRANSLATION_CATEGORY,
597
                    'actionAction' => static::ACTION_PREFORM_ITEM_ACTION_PATH,
598
                    'associateAction' => static::ACTION_ASSOCIATION_ITEM_PATH,
599
                    'dissociateAction' => static::ACTION_DISSOCIATION_ITEM_PATH,
600
                    'data' => [
601
                        'field' => $this->id,
602
                        'element' => $element ? $element->getId() : null
603
                    ],
604
                    'actions' => $this->getItemActionHtml($element),
605
                ]
606
            ]
607
        ];
608
    }
609
610
611
    /*******************************************
612
     * ACTIONS
613
     *******************************************/
614
615
    /**
616
     * @return IntegrationActionInterface[]
617
     * @throws \craft\errors\MissingComponentException
618
     * @throws \yii\base\InvalidConfigException
619
     */
620
    public function getAvailableActions(): array
621
    {
622
        $event = new RegisterIntegrationFieldActionsEvent([
623
            'actions' => $this->defaultAvailableActions
624
        ]);
625
626
        $this->trigger(
627
            static::EVENT_REGISTER_AVAILABLE_ACTIONS,
628
            $event
629
        );
630
631
        return $this->resolveActions(
632
            array_filter((array)$event->actions),
633
            IntegrationActionInterface::class
634
        );
635
    }
636
637
    /**
638
     * @param ElementInterface|null $element
639
     * @return IntegrationActionInterface[]
640
     * @throws \craft\errors\MissingComponentException
641
     * @throws \yii\base\InvalidConfigException
642
     */
643
    public function getActions(ElementInterface $element = null): array
644
    {
645
        $event = new RegisterIntegrationFieldActionsEvent([
646
            'actions' => $this->selectedActions,
647
            'element' => $element
648
        ]);
649
650
        $this->trigger(
651
            static::EVENT_REGISTER_ACTIONS,
652
            $event
653
        );
654
655
        return $this->resolveActions(
656
            array_filter((array)$event->actions),
657
            IntegrationActionInterface::class
658
        );
659
    }
660
661
    /**
662
     * @return IntegrationActionInterface[]
663
     * @throws \craft\errors\MissingComponentException
664
     * @throws \yii\base\InvalidConfigException
665
     */
666
    public function getAvailableItemActions(): array
667
    {
668
        $event = new RegisterIntegrationFieldActionsEvent([
669
            'actions' => $this->defaultAvailableItemActions
670
        ]);
671
672
        $this->trigger(
673
            static::EVENT_REGISTER_AVAILABLE_ITEM_ACTIONS,
674
            $event
675
        );
676
677
        return $this->resolveActions(
678
            array_filter((array)$event->actions),
679
            IntegrationItemActionInterface::class
680
        );
681
    }
682
683
    /**
684
     * @param ElementInterface|null $element
685
     * @return IntegrationItemActionInterface[]
686
     * @throws \craft\errors\MissingComponentException
687
     * @throws \yii\base\InvalidConfigException
688
     */
689
    public function getItemActions(ElementInterface $element = null): array
690
    {
691
        $event = new RegisterIntegrationFieldActionsEvent([
692
            'actions' => $this->selectedItemActions,
693
            'element' => $element
694
        ]);
695
696
        $this->trigger(
697
            static::EVENT_REGISTER_ITEM_ACTIONS,
698
            $event
699
        );
700
701
        return $this->resolveActions(
702
            array_filter((array)$event->actions),
703
            IntegrationItemActionInterface::class
704
        );
705
    }
706
707
    /**
708
     * @param array $actions
709
     * @param string $instance
710
     * @return array
711
     * @throws \craft\errors\MissingComponentException
712
     * @throws \yii\base\InvalidConfigException
713
     */
714
    protected function resolveActions(array $actions, string $instance)
715
    {
716
        foreach ($actions as $i => $action) {
717
            // $action could be a string or config array
718
            if (!$action instanceof $instance) {
719
                $actions[$i] = $action = ComponentHelper::createComponent($action, $instance);
720
721
                if ($actions[$i] === null) {
722
                    unset($actions[$i]);
723
                }
724
            }
725
        }
726
727
        return array_values($actions);
728
    }
729
730
    /**
731
     * @param ElementInterface|null $element
732
     * @return array
733
     * @throws \craft\errors\MissingComponentException
734
     * @throws \yii\base\InvalidConfigException
735
     */
736
    protected function getActionHtml(ElementInterface $element = null): array
737
    {
738
        $actionData = [];
739
740
        foreach ($this->getActions($element) as $action) {
741
            $actionData[] = [
742
                'type' => get_class($action),
743
                'destructive' => $action->isDestructive(),
744
                'name' => $action->getTriggerLabel(),
745
                'trigger' => $action->getTriggerHtml(),
746
                'confirm' => $action->getConfirmationMessage(),
747
            ];
748
        }
749
750
        return $actionData;
751
    }
752
753
    /**
754
     * @param ElementInterface|null $element
755
     * @return array
756
     * @throws \craft\errors\MissingComponentException
757
     * @throws \yii\base\InvalidConfigException
758
     */
759
    protected function getItemActionHtml(ElementInterface $element = null): array
760
    {
761
        $actionData = [];
762
763
        foreach ($this->getItemActions($element) as $action) {
764
            $actionData[] = [
765
                'type' => get_class($action),
766
                'destructive' => $action->isDestructive(),
767
                'name' => $action->getTriggerLabel(),
768
                'trigger' => $action->getTriggerHtml(),
769
                'confirm' => $action->getConfirmationMessage(),
770
            ];
771
        }
772
773
        return $actionData;
774
    }
775
776
777
778
    /*******************************************
779
     * EVENTS
780
     *******************************************/
781
782
    /**
783
     * @param ElementInterface $element
784
     * @param bool $isNew
785
     * @return bool|void
786
     * @throws \Throwable
787
     * @throws \yii\db\StaleObjectException
788
     */
789
    public function afterElementSave(ElementInterface $element, bool $isNew)
790
    {
791
        /** @var IntegrationAssociationQuery $query */
792
        $query = $element->getFieldValue($this->handle);
793
794
        $currentAssociations = [];
795
796
        if (!$isNew) {
797
            /** @var ActiveRecord $recordClass */
798
            $recordClass = static::recordClass();
799
800
            /** @var IntegrationAssociationQuery $existingQuery */
801
            $existingQuery = $recordClass::find();
802
            $existingQuery->element = $query->element;
803
            $existingQuery->field = $query->field;
804
            $existingQuery->site = $query->site;
805
            $existingQuery->indexBy = 'objectId';
806
807
            $currentAssociations = $existingQuery->all();
808
        }
809
810
        $success = true;
811
812
        if (null === ($records = $query->getCachedResult())) {
813
            foreach ($currentAssociations as $currentAssociation) {
814
                if (!$currentAssociation->delete()) {
815
                    $success = false;
816
                }
817
            }
818
819
            if (!$success) {
820
                $this->addError('types', 'Unable to dissociate object.');
821
                throw new Exception('Unable to dissociate object.');
822
            }
823
824
            parent::afterElementSave($element, $isNew);
825
        } else {
826
            $associations = [];
827
            $order = 1;
828
            foreach ($records as $record) {
829
                if (null === ($association = ArrayHelper::remove($currentAssociations, $record->objectId))) {
830
                    $association = $record;
831
                }
832
                $association->sortOrder = $order++;
833
                $associations[] = $association;
834
            }
835
836
            // DeleteOrganization those removed
837
            foreach ($currentAssociations as $currentAssociation) {
838
                if (!$currentAssociation->delete()) {
839
                    $success = false;
840
                }
841
            }
842
843
            foreach ($associations as $association) {
844
                if (!$association->save()) {
845
                    $success = false;
846
                }
847
            }
848
849
            if (!$success) {
850
                $this->addError('users', 'Unable to associate objects.');
851
                throw new Exception('Unable to associate objects.');
852
            }
853
854
            parent::afterElementSave($element, $isNew);
855
        }
856
    }
857
858
859
    /*******************************************
860
     * SETTINGS
861
     *******************************************/
862
863
    /**
864
     * @inheritdoc
865
     * @throws \Twig_Error_Loader
866
     * @throws \yii\base\Exception
867
     */
868
    public function getSettingsHtml()
869
    {
870
        return Craft::$app->getView()->renderTemplate(
871
            static::SETTINGS_TEMPLATE_PATH,
872
            $this->settingsHtmlVariables()
873
        );
874
    }
875
876
    /**
877
     * @return array
878
     * @throws \craft\errors\MissingComponentException
879
     * @throws \yii\base\InvalidConfigException
880
     */
881
    protected function settingsHtmlVariables(): array
882
    {
883
        return [
884
            'field' => $this,
885
            'availableActions' => $this->getAvailableActions(),
886
            'availableItemActions' => $this->getAvailableItemActions(),
887
            'translationCategory' => static::TRANSLATION_CATEGORY,
888
        ];
889
    }
890
891
    /**
892
     * @inheritdoc
893
     */
894
    public function settingsAttributes(): array
895
    {
896
        return array_merge(
897
            [
898
                'object',
899
                'min',
900
                'max',
901
                'viewUrl',
902
                'listUrl',
903
                'selectedActions',
904
                'selectedItemActions',
905
                'selectionLabel'
906
            ],
907
            parent::settingsAttributes()
908
        );
909
    }
910
}
911