Completed
Push — develop ( b4fc62...5b69cf )
by Nate
03:24
created

Integrations   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 651
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 15

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 47
lcom 2
cbo 15
dl 0
loc 651
ccs 0
cts 319
cp 0
rs 8.589
c 0
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A rules() 0 27 1
A getSearchKeywords() 0 11 2
A getInputHtml() 0 11 1
A hasContentColumn() 0 4 1
A defaultSelectionLabel() 0 4 1
A tableAlias() 0 6 1
A getObjectLabel() 0 4 1
A getElementValidationRules() 0 23 3
B inputHtmlVariables() 0 42 5
A getAvailableActions() 0 16 1
A getActions() 0 17 1
A getAvailableItemActions() 0 16 1
A getItemActions() 0 17 1
A resolveActions() 0 15 4
A getActionHtml() 0 16 2
A getItemActionHtml() 0 16 2
C afterElementSave() 0 74 14
A getTableAttributeHtml() 0 13 2
A getSettingsHtml() 0 7 1
A settingsHtmlVariables() 0 9 1
A settingsAttributes() 0 16 1

How to fix   Complexity   

Complex Class

Complex classes like Integrations often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Integrations, and based on these observations, apply Extract Interface, too.

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\ElementInterface;
13
use craft\base\Field;
14
use craft\helpers\Component as ComponentHelper;
15
use craft\helpers\StringHelper;
16
use flipbox\craft\ember\helpers\ModelHelper;
17
use flipbox\craft\ember\records\ActiveRecord;
18
use flipbox\craft\ember\validators\MinMaxValidator;
19
use flipbox\craft\integration\events\RegisterIntegrationFieldActionsEvent;
20
use flipbox\craft\integration\fields\actions\IntegrationActionInterface;
21
use flipbox\craft\integration\fields\actions\IntegrationItemActionInterface;
22
use flipbox\craft\integration\queries\IntegrationAssociationQuery;
23
use flipbox\craft\integration\records\IntegrationAssociation;
24
use flipbox\craft\integration\web\assets\integrations\Integrations as IntegrationsAsset;
25
use yii\base\Exception;
26
use yii\helpers\ArrayHelper;
27
28
/**
29
 * @author Flipbox Factory <[email protected]>
30
 * @since 2.0.0
31
 */
32
abstract class Integrations extends Field
33
{
34
    use ModifyElementQueryTrait,
35
        NormalizeValueTrait;
36
37
    /**
38
     * The Plugin's translation category
39
     */
40
    const TRANSLATION_CATEGORY = '';
41
42
    /**
43
     * The action path to preform field actions
44
     */
45
    const ACTION_PREFORM_ACTION_PATH = '';
46
47
    /**
48
     * The action path to preform field actions
49
     */
50
    const ACTION_PREFORM_ITEM_ACTION_PATH = '';
51
52
    /**
53
     * The action path to associate an item
54
     */
55
    const ACTION_ASSOCIATION_ITEM_PATH = '';
56
57
    /**
58
     * The action path to dissociate an item
59
     */
60
    const ACTION_DISSOCIATION_ITEM_PATH = '';
61
62
    /**
63
     * The action path to create an integration object
64
     */
65
    const ACTION_CREATE_ITEM_PATH = '';
66
67
    /**
68
     * The action event name
69
     */
70
    const EVENT_REGISTER_ACTIONS = 'registerActions';
71
72
    /**
73
     * The action event name
74
     */
75
    const EVENT_REGISTER_AVAILABLE_ACTIONS = 'registerAvailableActions';
76
77
    /**
78
     * The item action event name
79
     */
80
    const EVENT_REGISTER_ITEM_ACTIONS = 'registerItemActions';
81
82
    /**
83
     * The item action event name
84
     */
85
    const EVENT_REGISTER_AVAILABLE_ITEM_ACTIONS = 'registerAvailableItemActions';
86
87
    /**
88
     * The input template path
89
     */
90
    const INPUT_TEMPLATE_PATH = '';
91
92
    /**
93
     * The input template path
94
     */
95
    const INPUT_ITEM_TEMPLATE_PATH = '_inputItem';
96
97
    /**
98
     * The input template path
99
     */
100
    const SETTINGS_TEMPLATE_PATH = '';
101
102
    /**
103
     * @var string
104
     */
105
    public $object;
106
107
    /**
108
     * @var int|null
109
     */
110
    public $min;
111
112
    /**
113
     * @var int|null
114
     */
115
    public $max;
116
117
    /**
118
     * @var string
119
     */
120
    public $viewUrl = '';
121
122
    /**
123
     * @var string
124
     */
125
    public $listUrl = '';
126
127
    /**
128
     * @var IntegrationActionInterface[]
129
     */
130
    public $selectedActions = [];
131
132
    /**
133
     * @var IntegrationItemActionInterface[]
134
     */
135
    public $selectedItemActions = [];
136
137
    /**
138
     * @var string|null
139
     */
140
    public $selectionLabel;
141
142
    /**
143
     * @inheritdoc
144
     */
145
    protected $defaultAvailableActions = [];
146
147
    /**
148
     * @inheritdoc
149
     */
150
    protected $defaultAvailableItemActions = [];
151
152
    /**
153
     * @return string
154
     */
155
    abstract public static function recordClass(): string;
156
157
    /**
158
     * @inheritdoc
159
     */
160
    public static function hasContentColumn(): bool
161
    {
162
        return false;
163
    }
164
165
    /**
166
     * @return string
167
     */
168
    public static function defaultSelectionLabel(): string
169
    {
170
        return Craft::t(static::TRANSLATION_CATEGORY, 'Add an Object');
171
    }
172
173
    /**
174
     * @return string
175
     */
176
    protected static function tableAlias(): string
177
    {
178
        /** @var ActiveRecord $recordClass */
179
        $recordClass = static::recordClass();
180
        return $recordClass::tableAlias();
181
    }
182
183
    /*******************************************
184
     * OBJECT
185
     *******************************************/
186
187
    /**
188
     * @return string
189
     */
190
    public function getObjectLabel(): string
191
    {
192
        return StringHelper::titleize($this->object);
193
    }
194
195
    /*******************************************
196
     * VALIDATION
197
     *******************************************/
198
199
    /**
200
     * @inheritdoc
201
     */
202
    public function getElementValidationRules(): array
203
    {
204
        return [
205
            [
206
                MinMaxValidator::class,
207
                'min' => $this->min ? (int)$this->min : null,
208
                'max' => $this->max ? (int)$this->max : null,
209
                'tooFew' => Craft::t(
210
                    static::TRANSLATION_CATEGORY,
211
                    '{attribute} should contain at least ' .
212
                    '{min, number} ' .
213
                    '{min, plural, one{association} other{associations}}.'
214
                ),
215
                'tooMany' => Craft::t(
216
                    static::TRANSLATION_CATEGORY,
217
                    '{attribute} should contain at most ' .
218
                    '{max, number} ' .
219
                    '{max, plural, one{association} other{associations}}.'
220
                ),
221
                'skipOnEmpty' => false
222
            ]
223
        ];
224
    }
225
226
    /*******************************************
227
     * RULES
228
     *******************************************/
229
230
    /**
231
     * @inheritdoc
232
     */
233
    public function rules()
234
    {
235
        return array_merge(
236
            parent::rules(),
237
            [
238
                [
239
                    'object',
240
                    'required',
241
                    'message' => Craft::t(static::TRANSLATION_CATEGORY, 'Object cannot be empty.')
242
                ],
243
                [
244
                    [
245
                        'object',
246
                        'min',
247
                        'max',
248
                        'viewUrl',
249
                        'listUrl',
250
                        'selectionLabel'
251
                    ],
252
                    'safe',
253
                    'on' => [
254
                        ModelHelper::SCENARIO_DEFAULT
255
                    ]
256
                ]
257
            ]
258
        );
259
    }
260
261
262
    /*******************************************
263
     * SEARCH
264
     *******************************************/
265
266
    /**
267
     * @param IntegrationAssociationQuery $value
268
     * @inheritdoc
269
     */
270
    public function getSearchKeywords($value, ElementInterface $element): string
271
    {
272
        $objects = [];
273
274
        /** @var IntegrationAssociation $association */
275
        foreach ($value->all() as $association) {
276
            array_push($objects, $association->objectId);
277
        }
278
279
        return parent::getSearchKeywords($objects, $element);
280
    }
281
282
283
    /*******************************************
284
     * VIEWS
285
     *******************************************/
286
287
    /**
288
     * @inheritdoc
289
     * @param IntegrationAssociationQuery $value
290
     * @throws \Twig_Error_Loader
291
     * @throws \yii\base\Exception
292
     */
293
    public function getInputHtml($value, ElementInterface $element = null): string
294
    {
295
        $value->limit(null);
296
297
        Craft::$app->getView()->registerAssetBundle(IntegrationsAsset::class);
298
299
        return Craft::$app->getView()->renderTemplate(
300
            static::INPUT_TEMPLATE_PATH,
301
            $this->inputHtmlVariables($value, $element)
302
        );
303
    }
304
305
    /**
306
     * @param IntegrationAssociationQuery $query
307
     * @param ElementInterface|null $element
308
     * @param bool $static
309
     * @return array
310
     * @throws \craft\errors\MissingComponentException
311
     * @throws \yii\base\InvalidConfigException
312
     */
313
    protected function inputHtmlVariables(
314
        IntegrationAssociationQuery $query,
315
        ElementInterface $element = null,
316
        bool $static = false
317
    ): array {
318
    
319
320
        return [
321
            'field' => $this,
322
            'element' => $element,
323
            'value' => $query,
324
            'objectLabel' => $this->getObjectLabel(),
325
            'static' => $static,
326
            'itemTemplate' => static::INPUT_ITEM_TEMPLATE_PATH,
327
            'settings' => [
328
                'translationCategory' => static::TRANSLATION_CATEGORY,
329
                'limit' => $this->max ? $this->max : null,
330
                'data' => [
331
                    'field' => $this->id,
332
                    'element' => $element ? $element->getId() : null
333
                ],
334
                'actions' => $this->getActionHtml($element),
335
                'actionAction' => static::ACTION_PREFORM_ACTION_PATH,
336
                'createItemAction' => static::ACTION_CREATE_ITEM_PATH,
337
                'itemData' => [
338
                    'field' => $this->id,
339
                    'element' => $element ? $element->getId() : null
340
                ],
341
                'itemSettings' => [
342
                    'translationCategory' => static::TRANSLATION_CATEGORY,
343
                    'actionAction' => static::ACTION_PREFORM_ITEM_ACTION_PATH,
344
                    'associateAction' => static::ACTION_ASSOCIATION_ITEM_PATH,
345
                    'dissociateAction' => static::ACTION_DISSOCIATION_ITEM_PATH,
346
                    'data' => [
347
                        'field' => $this->id,
348
                        'element' => $element ? $element->getId() : null
349
                    ],
350
                    'actions' => $this->getItemActionHtml($element),
351
                ]
352
            ]
353
        ];
354
    }
355
356
357
    /*******************************************
358
     * ACTIONS
359
     *******************************************/
360
361
    /**
362
     * @return IntegrationActionInterface[]
363
     * @throws \craft\errors\MissingComponentException
364
     * @throws \yii\base\InvalidConfigException
365
     */
366
    public function getAvailableActions(): array
367
    {
368
        $event = new RegisterIntegrationFieldActionsEvent([
369
            'actions' => $this->defaultAvailableActions
370
        ]);
371
372
        $this->trigger(
373
            static::EVENT_REGISTER_AVAILABLE_ACTIONS,
374
            $event
375
        );
376
377
        return $this->resolveActions(
378
            array_filter((array)$event->actions),
379
            IntegrationActionInterface::class
380
        );
381
    }
382
383
    /**
384
     * @param ElementInterface|null $element
385
     * @return IntegrationActionInterface[]
386
     * @throws \craft\errors\MissingComponentException
387
     * @throws \yii\base\InvalidConfigException
388
     */
389
    public function getActions(ElementInterface $element = null): array
390
    {
391
        $event = new RegisterIntegrationFieldActionsEvent([
392
            'actions' => $this->selectedActions,
393
            'element' => $element
394
        ]);
395
396
        $this->trigger(
397
            static::EVENT_REGISTER_ACTIONS,
398
            $event
399
        );
400
401
        return $this->resolveActions(
402
            array_filter((array)$event->actions),
403
            IntegrationActionInterface::class
404
        );
405
    }
406
407
    /**
408
     * @return IntegrationActionInterface[]
409
     * @throws \craft\errors\MissingComponentException
410
     * @throws \yii\base\InvalidConfigException
411
     */
412
    public function getAvailableItemActions(): array
413
    {
414
        $event = new RegisterIntegrationFieldActionsEvent([
415
            'actions' => $this->defaultAvailableItemActions
416
        ]);
417
418
        $this->trigger(
419
            static::EVENT_REGISTER_AVAILABLE_ITEM_ACTIONS,
420
            $event
421
        );
422
423
        return $this->resolveActions(
424
            array_filter((array)$event->actions),
425
            IntegrationItemActionInterface::class
426
        );
427
    }
428
429
    /**
430
     * @param ElementInterface|null $element
431
     * @return IntegrationItemActionInterface[]
432
     * @throws \craft\errors\MissingComponentException
433
     * @throws \yii\base\InvalidConfigException
434
     */
435
    public function getItemActions(ElementInterface $element = null): array
436
    {
437
        $event = new RegisterIntegrationFieldActionsEvent([
438
            'actions' => $this->selectedItemActions,
439
            'element' => $element
440
        ]);
441
442
        $this->trigger(
443
            static::EVENT_REGISTER_ITEM_ACTIONS,
444
            $event
445
        );
446
447
        return $this->resolveActions(
448
            array_filter((array)$event->actions),
449
            IntegrationItemActionInterface::class
450
        );
451
    }
452
453
    /**
454
     * @param array $actions
455
     * @param string $instance
456
     * @return array
457
     * @throws \craft\errors\MissingComponentException
458
     * @throws \yii\base\InvalidConfigException
459
     */
460
    protected function resolveActions(array $actions, string $instance)
461
    {
462
        foreach ($actions as $i => $action) {
463
            // $action could be a string or config array
464
            if (!$action instanceof $instance) {
465
                $actions[$i] = $action = ComponentHelper::createComponent($action, $instance);
466
467
                if ($actions[$i] === null) {
468
                    unset($actions[$i]);
469
                }
470
            }
471
        }
472
473
        return array_values($actions);
474
    }
475
476
    /**
477
     * @param ElementInterface|null $element
478
     * @return array
479
     * @throws \craft\errors\MissingComponentException
480
     * @throws \yii\base\InvalidConfigException
481
     */
482
    protected function getActionHtml(ElementInterface $element = null): array
483
    {
484
        $actionData = [];
485
486
        foreach ($this->getActions($element) as $action) {
487
            $actionData[] = [
488
                'type' => get_class($action),
489
                'destructive' => $action->isDestructive(),
490
                'name' => $action->getTriggerLabel(),
491
                'trigger' => $action->getTriggerHtml(),
492
                'confirm' => $action->getConfirmationMessage(),
493
            ];
494
        }
495
496
        return $actionData;
497
    }
498
499
    /**
500
     * @param ElementInterface|null $element
501
     * @return array
502
     * @throws \craft\errors\MissingComponentException
503
     * @throws \yii\base\InvalidConfigException
504
     */
505
    protected function getItemActionHtml(ElementInterface $element = null): array
506
    {
507
        $actionData = [];
508
509
        foreach ($this->getItemActions($element) as $action) {
510
            $actionData[] = [
511
                'type' => get_class($action),
512
                'destructive' => $action->isDestructive(),
513
                'name' => $action->getTriggerLabel(),
514
                'trigger' => $action->getTriggerHtml(),
515
                'confirm' => $action->getConfirmationMessage(),
516
            ];
517
        }
518
519
        return $actionData;
520
    }
521
522
    /*******************************************
523
     * EVENTS
524
     *******************************************/
525
526
    /**
527
     * @param ElementInterface $element
528
     * @param bool $isNew
529
     * @return bool|void
530
     * @throws \Throwable
531
     * @throws \yii\db\StaleObjectException
532
     */
533
    public function afterElementSave(ElementInterface $element, bool $isNew)
534
    {
535
        /** @var IntegrationAssociationQuery $query */
536
        $query = $element->getFieldValue($this->handle);
537
538
        // Cached results
539
        if (null === ($records = $query->getCachedResult())) {
540
            parent::afterElementSave($element, $isNew);
541
            return;
542
        }
543
544
        $currentAssociations = [];
545
546
        if ($isNew === false) {
547
            /** @var IntegrationAssociation $recordClass */
548
            $recordClass = static::recordClass();
549
550
            /** @var IntegrationAssociationQuery $existingQuery */
551
            $existingQuery = $recordClass::find();
552
            $existingQuery->element = $query->element;
553
            $existingQuery->field = $query->field;
554
            $existingQuery->site = $query->site;
555
            $existingQuery->indexBy = 'objectId';
556
557
            $currentAssociations = $existingQuery->all();
558
        }
559
560
        $success = true;
561
562
        if (empty($records)) {
563
            foreach ($currentAssociations as $currentAssociation) {
564
                if (!$currentAssociation->delete()) {
565
                    $success = false;
566
                }
567
            }
568
569
            if (!$success) {
570
                $this->addError('types', 'Unable to dissociate object.');
571
                throw new Exception('Unable to dissociate object.');
572
            }
573
574
            parent::afterElementSave($element, $isNew);
575
            return;
576
        }
577
578
        $associations = [];
579
        $order = 1;
580
581
        foreach ($records as $record) {
582
            if (null === ($association = ArrayHelper::remove($currentAssociations, $record->objectId))) {
583
                $association = $record;
584
            }
585
            $association->sortOrder = $order++;
586
            $associations[] = $association;
587
        }
588
589
        // DeleteOrganization those removed
590
        foreach ($currentAssociations as $currentAssociation) {
591
            if (!$currentAssociation->delete()) {
592
                $success = false;
593
            }
594
        }
595
596
        foreach ($associations as $association) {
597
            if (!$association->save()) {
598
                $success = false;
599
            }
600
        }
601
602
        if (!$success) {
603
            $this->addError('users', 'Unable to associate objects.');
604
            throw new Exception('Unable to associate objects.');
605
        }
606
    }
607
608
609
    /*******************************************
610
     * INDEX
611
     *******************************************/
612
613
    /**
614
     * @inheritdoc
615
     */
616
    public function getTableAttributeHtml($value, ElementInterface $element): string
617
    {
618
        if ($value instanceof IntegrationAssociationQuery) {
619
            $ids = (clone $value)
620
                ->select(['objectId'])
621
                ->column();
622
623
            return StringHelper::toString($ids, ', ');
624
625
        }
626
627
        return '';
628
    }
629
630
631
    /*******************************************
632
     * SETTINGS
633
     *******************************************/
634
635
    /**
636
     * @inheritdoc
637
     * @throws \Twig_Error_Loader
638
     * @throws \yii\base\Exception
639
     */
640
    public function getSettingsHtml()
641
    {
642
        return Craft::$app->getView()->renderTemplate(
643
            static::SETTINGS_TEMPLATE_PATH,
644
            $this->settingsHtmlVariables()
645
        );
646
    }
647
648
    /**
649
     * @return array
650
     * @throws \craft\errors\MissingComponentException
651
     * @throws \yii\base\InvalidConfigException
652
     */
653
    protected function settingsHtmlVariables(): array
654
    {
655
        return [
656
            'field' => $this,
657
            'availableActions' => $this->getAvailableActions(),
658
            'availableItemActions' => $this->getAvailableItemActions(),
659
            'translationCategory' => static::TRANSLATION_CATEGORY,
660
        ];
661
    }
662
663
    /**
664
     * @inheritdoc
665
     */
666
    public function settingsAttributes(): array
667
    {
668
        return array_merge(
669
            [
670
                'object',
671
                'min',
672
                'max',
673
                'viewUrl',
674
                'listUrl',
675
                'selectedActions',
676
                'selectedItemActions',
677
                'selectionLabel'
678
            ],
679
            parent::settingsAttributes()
680
        );
681
    }
682
}
683