Completed
Push — master ( fa2880...7af5aa )
by Nate
01:12
created

Integrations::settingsHtmlVariables()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 9
c 0
b 0
f 0
ccs 0
cts 9
cp 0
rs 9.9666
cc 1
nc 1
nop 0
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\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 1.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 {min, number} {min, plural, one{domain} other{domains}}.'
212
                ),
213
                'tooMany' => Craft::t(
214
                    static::TRANSLATION_CATEGORY,
215
                    '{attribute} should contain at most {max, number} {max, plural, one{domain} other{domains}}.'
216
                ),
217
                'skipOnEmpty' => false
218
            ]
219
        ];
220
    }
221
222
    /*******************************************
223
     * RULES
224
     *******************************************/
225
226
    /**
227
     * @inheritdoc
228
     */
229
    public function rules()
230
    {
231
        return array_merge(
232
            parent::rules(),
233
            [
234
                [
235
                    'object',
236
                    'required',
237
                    'message' => Craft::t(static::TRANSLATION_CATEGORY, 'Object cannot be empty.')
238
                ],
239
                [
240
                    [
241
                        'object',
242
                        'min',
243
                        'max',
244
                        'viewUrl',
245
                        'listUrl',
246
                        'selectionLabel'
247
                    ],
248
                    'safe',
249
                    'on' => [
250
                        ModelHelper::SCENARIO_DEFAULT
251
                    ]
252
                ]
253
            ]
254
        );
255
    }
256
257
258
    /*******************************************
259
     * SEARCH
260
     *******************************************/
261
262
    /**
263
     * @param IntegrationAssociationQuery $value
264
     * @inheritdoc
265
     */
266
    public function getSearchKeywords($value, ElementInterface $element): string
267
    {
268
        $objects = [];
269
270
        /** @var IntegrationAssociation $association */
271
        foreach ($value->all() as $association) {
272
            array_push($objects, $association->objectId);
273
        }
274
275
        return parent::getSearchKeywords($objects, $element);
276
    }
277
278
279
    /*******************************************
280
     * VIEWS
281
     *******************************************/
282
283
    /**
284
     * @inheritdoc
285
     * @param IntegrationAssociationQuery $value
286
     * @throws \Twig_Error_Loader
287
     * @throws \yii\base\Exception
288
     */
289
    public function getInputHtml($value, ElementInterface $element = null): string
290
    {
291
        $value->limit(null);
292
293
        Craft::$app->getView()->registerAssetBundle(IntegrationsAsset::class);
294
295
        return Craft::$app->getView()->renderTemplate(
296
            static::INPUT_TEMPLATE_PATH,
297
            $this->inputHtmlVariables($value, $element)
298
        );
299
    }
300
301
    /**
302
     * @param IntegrationAssociationQuery $query
303
     * @param ElementInterface|null $element
304
     * @param bool $static
305
     * @return array
306
     * @throws \craft\errors\MissingComponentException
307
     * @throws \yii\base\InvalidConfigException
308
     */
309
    protected function inputHtmlVariables(
310
        IntegrationAssociationQuery $query,
311
        ElementInterface $element = null,
312
        bool $static = false
313
    ): array {
314
    
315
316
        return [
317
            'field' => $this,
318
            'element' => $element,
319
            'value' => $query,
320
            'objectLabel' => $this->getObjectLabel(),
321
            'static' => $static,
322
            'itemTemplate' => static::INPUT_ITEM_TEMPLATE_PATH,
323
            'settings' => [
324
                'translationCategory' => static::TRANSLATION_CATEGORY,
325
                'limit' => $this->max ? $this->max : null,
326
                'data' => [
327
                    'field' => $this->id,
328
                    'element' => $element ? $element->getId() : null
329
                ],
330
                'actions' => $this->getActionHtml($element),
331
                'actionAction' => static::ACTION_PREFORM_ACTION_PATH,
332
                'createItemAction' => static::ACTION_CREATE_ITEM_PATH,
333
                'itemData' => [
334
                    'field' => $this->id,
335
                    'element' => $element ? $element->getId() : null
336
                ],
337
                'itemSettings' => [
338
                    'translationCategory' => static::TRANSLATION_CATEGORY,
339
                    'actionAction' => static::ACTION_PREFORM_ITEM_ACTION_PATH,
340
                    'associateAction' => static::ACTION_ASSOCIATION_ITEM_PATH,
341
                    'dissociateAction' => static::ACTION_DISSOCIATION_ITEM_PATH,
342
                    'data' => [
343
                        'field' => $this->id,
344
                        'element' => $element ? $element->getId() : null
345
                    ],
346
                    'actions' => $this->getItemActionHtml($element),
347
                ]
348
            ]
349
        ];
350
    }
351
352
353
    /*******************************************
354
     * ACTIONS
355
     *******************************************/
356
357
    /**
358
     * @return IntegrationActionInterface[]
359
     * @throws \craft\errors\MissingComponentException
360
     * @throws \yii\base\InvalidConfigException
361
     */
362
    public function getAvailableActions(): array
363
    {
364
        $event = new RegisterIntegrationFieldActionsEvent([
365
            'actions' => $this->defaultAvailableActions
366
        ]);
367
368
        $this->trigger(
369
            static::EVENT_REGISTER_AVAILABLE_ACTIONS,
370
            $event
371
        );
372
373
        return $this->resolveActions(
374
            array_filter((array)$event->actions),
375
            IntegrationActionInterface::class
376
        );
377
    }
378
379
    /**
380
     * @param ElementInterface|null $element
381
     * @return IntegrationActionInterface[]
382
     * @throws \craft\errors\MissingComponentException
383
     * @throws \yii\base\InvalidConfigException
384
     */
385
    public function getActions(ElementInterface $element = null): array
386
    {
387
        $event = new RegisterIntegrationFieldActionsEvent([
388
            'actions' => $this->selectedActions,
389
            'element' => $element
390
        ]);
391
392
        $this->trigger(
393
            static::EVENT_REGISTER_ACTIONS,
394
            $event
395
        );
396
397
        return $this->resolveActions(
398
            array_filter((array)$event->actions),
399
            IntegrationActionInterface::class
400
        );
401
    }
402
403
    /**
404
     * @return IntegrationActionInterface[]
405
     * @throws \craft\errors\MissingComponentException
406
     * @throws \yii\base\InvalidConfigException
407
     */
408
    public function getAvailableItemActions(): array
409
    {
410
        $event = new RegisterIntegrationFieldActionsEvent([
411
            'actions' => $this->defaultAvailableItemActions
412
        ]);
413
414
        $this->trigger(
415
            static::EVENT_REGISTER_AVAILABLE_ITEM_ACTIONS,
416
            $event
417
        );
418
419
        return $this->resolveActions(
420
            array_filter((array)$event->actions),
421
            IntegrationItemActionInterface::class
422
        );
423
    }
424
425
    /**
426
     * @param ElementInterface|null $element
427
     * @return IntegrationItemActionInterface[]
428
     * @throws \craft\errors\MissingComponentException
429
     * @throws \yii\base\InvalidConfigException
430
     */
431
    public function getItemActions(ElementInterface $element = null): array
432
    {
433
        $event = new RegisterIntegrationFieldActionsEvent([
434
            'actions' => $this->selectedItemActions,
435
            'element' => $element
436
        ]);
437
438
        $this->trigger(
439
            static::EVENT_REGISTER_ITEM_ACTIONS,
440
            $event
441
        );
442
443
        return $this->resolveActions(
444
            array_filter((array)$event->actions),
445
            IntegrationItemActionInterface::class
446
        );
447
    }
448
449
    /**
450
     * @param array $actions
451
     * @param string $instance
452
     * @return array
453
     * @throws \craft\errors\MissingComponentException
454
     * @throws \yii\base\InvalidConfigException
455
     */
456
    protected function resolveActions(array $actions, string $instance)
457
    {
458
        foreach ($actions as $i => $action) {
459
            // $action could be a string or config array
460
            if (!$action instanceof $instance) {
461
                $actions[$i] = $action = ComponentHelper::createComponent($action, $instance);
462
463
                if ($actions[$i] === null) {
464
                    unset($actions[$i]);
465
                }
466
            }
467
        }
468
469
        return array_values($actions);
470
    }
471
472
    /**
473
     * @param ElementInterface|null $element
474
     * @return array
475
     * @throws \craft\errors\MissingComponentException
476
     * @throws \yii\base\InvalidConfigException
477
     */
478
    protected function getActionHtml(ElementInterface $element = null): array
479
    {
480
        $actionData = [];
481
482
        foreach ($this->getActions($element) as $action) {
483
            $actionData[] = [
484
                'type' => get_class($action),
485
                'destructive' => $action->isDestructive(),
486
                'name' => $action->getTriggerLabel(),
487
                'trigger' => $action->getTriggerHtml(),
488
                'confirm' => $action->getConfirmationMessage(),
489
            ];
490
        }
491
492
        return $actionData;
493
    }
494
495
    /**
496
     * @param ElementInterface|null $element
497
     * @return array
498
     * @throws \craft\errors\MissingComponentException
499
     * @throws \yii\base\InvalidConfigException
500
     */
501
    protected function getItemActionHtml(ElementInterface $element = null): array
502
    {
503
        $actionData = [];
504
505
        foreach ($this->getItemActions($element) as $action) {
506
            $actionData[] = [
507
                'type' => get_class($action),
508
                'destructive' => $action->isDestructive(),
509
                'name' => $action->getTriggerLabel(),
510
                'trigger' => $action->getTriggerHtml(),
511
                'confirm' => $action->getConfirmationMessage(),
512
            ];
513
        }
514
515
        return $actionData;
516
    }
517
518
519
520
    /*******************************************
521
     * EVENTS
522
     *******************************************/
523
524
    /**
525
     * @param ElementInterface $element
526
     * @param bool $isNew
527
     * @return bool|void
528
     * @throws \Throwable
529
     * @throws \yii\db\StaleObjectException
530
     */
531
    public function afterElementSave(ElementInterface $element, bool $isNew)
532
    {
533
        /** @var IntegrationAssociationQuery $query */
534
        $query = $element->getFieldValue($this->handle);
535
536
        $currentAssociations = [];
537
538
        if (!$isNew) {
539
            /** @var ActiveRecord $recordClass */
540
            $recordClass = static::recordClass();
541
542
            /** @var IntegrationAssociationQuery $existingQuery */
543
            $existingQuery = $recordClass::find();
544
            $existingQuery->element = $query->element;
545
            $existingQuery->field = $query->field;
546
            $existingQuery->site = $query->site;
547
            $existingQuery->indexBy = 'objectId';
548
549
            $currentAssociations = $existingQuery->all();
550
        }
551
552
        $success = true;
553
554
        if (null === ($records = $query->getCachedResult())) {
555
            foreach ($currentAssociations as $currentAssociation) {
556
                if (!$currentAssociation->delete()) {
557
                    $success = false;
558
                }
559
            }
560
561
            if (!$success) {
562
                $this->addError('types', 'Unable to dissociate object.');
563
                throw new Exception('Unable to dissociate object.');
564
            }
565
566
            parent::afterElementSave($element, $isNew);
567
        } else {
568
            $associations = [];
569
            $order = 1;
570
            foreach ($records as $record) {
571
                if (null === ($association = ArrayHelper::remove($currentAssociations, $record->objectId))) {
572
                    $association = $record;
573
                }
574
                $association->sortOrder = $order++;
575
                $associations[] = $association;
576
            }
577
578
            // DeleteOrganization those removed
579
            foreach ($currentAssociations as $currentAssociation) {
580
                if (!$currentAssociation->delete()) {
581
                    $success = false;
582
                }
583
            }
584
585
            foreach ($associations as $association) {
586
                if (!$association->save()) {
587
                    $success = false;
588
                }
589
            }
590
591
            if (!$success) {
592
                $this->addError('users', 'Unable to associate objects.');
593
                throw new Exception('Unable to associate objects.');
594
            }
595
596
            parent::afterElementSave($element, $isNew);
597
        }
598
    }
599
600
601
    /*******************************************
602
     * SETTINGS
603
     *******************************************/
604
605
    /**
606
     * @inheritdoc
607
     * @throws \Twig_Error_Loader
608
     * @throws \yii\base\Exception
609
     */
610
    public function getSettingsHtml()
611
    {
612
        return Craft::$app->getView()->renderTemplate(
613
            static::SETTINGS_TEMPLATE_PATH,
614
            $this->settingsHtmlVariables()
615
        );
616
    }
617
618
    /**
619
     * @return array
620
     * @throws \craft\errors\MissingComponentException
621
     * @throws \yii\base\InvalidConfigException
622
     */
623
    protected function settingsHtmlVariables(): array
624
    {
625
        return [
626
            'field' => $this,
627
            'availableActions' => $this->getAvailableActions(),
628
            'availableItemActions' => $this->getAvailableItemActions(),
629
            'translationCategory' => static::TRANSLATION_CATEGORY,
630
        ];
631
    }
632
633
    /**
634
     * @inheritdoc
635
     */
636
    public function settingsAttributes(): array
637
    {
638
        return array_merge(
639
            [
640
                'object',
641
                'min',
642
                'max',
643
                'viewUrl',
644
                'listUrl',
645
                'selectedActions',
646
                'selectedItemActions',
647
                'selectionLabel'
648
            ],
649
            parent::settingsAttributes()
650
        );
651
    }
652
}
653