Completed
Branch master (fafb06)
by Nate
04:30
created

DomainAssociations::deleteRecord()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 38
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 38
ccs 0
cts 36
cp 0
rs 8.8571
c 0
b 0
f 0
cc 2
eloc 28
nc 2
nop 4
crap 6
1
<?php
2
3
/**
4
 * @copyright  Copyright (c) Flipbox Digital Limited
5
 * @license    https://flipboxfactory.com/software/domains/license
6
 * @link       https://www.flipboxfactory.com/software/domains/
7
 */
8
9
namespace flipbox\domains\services;
10
11
use Craft;
12
use craft\base\Element;
13
use craft\base\ElementInterface;
14
use craft\db\Query;
15
use flipbox\domains\db\DomainsQuery;
16
use flipbox\domains\Domains as DomainsPlugin;
17
use flipbox\domains\events\DomainAssociationEvent;
18
use flipbox\domains\fields\Domains;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, flipbox\domains\services\Domains.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
19
use flipbox\domains\models\Domain;
20
use flipbox\ember\helpers\ArrayHelper;
21
use yii\base\Component;
22
23
/**
24
 * @author Flipbox Factory <[email protected]>
25
 * @since 1.0.0
26
 */
27
class DomainAssociations extends Component
28
{
29
    /**
30
     * @event DomainAssociationEvent The event that is triggered before a domain association.
31
     *
32
     * You may set [[DomainAssociationEvent::isValid]] to `false` to prevent the associate action.
33
     */
34
    const EVENT_BEFORE_ASSOCIATE = 'beforeAssociate';
35
36
    /**
37
     * @event DomainAssociationEvent The event that is triggered after a domain association.
38
     */
39
    const EVENT_AFTER_ASSOCIATE = 'afterAssociate';
40
41
    /**
42
     * @event DomainAssociationEvent The event that is triggered before a domain dissociation.
43
     *
44
     * You may set [[DomainAssociationEvent::isValid]] to `false` to prevent the dissociate action.
45
     */
46
    const EVENT_BEFORE_DISSOCIATE = 'beforeDissociate';
47
48
    /**
49
     * @event DomainAssociationEvent The event that is triggered after a domain dissociation.
50
     */
51
    const EVENT_AFTER_DISSOCIATE = 'afterDissociate';
52
53
    /**
54
     * @param Domains $field
55
     * @param DomainsQuery $query
56
     * @param ElementInterface|Element $element
57
     * @return bool
58
     * @throws \Exception
59
     */
60
    public function save(
61
        Domains $field,
62
        DomainsQuery $query,
63
        ElementInterface $element
64
    ) {
65
        // Nothing to save
66
        if (null === ($models = $query->getCachedResult())) {
67
            return true;
68
        }
69
70
        $transaction = Craft::$app->getDb()->beginTransaction();
71
        try {
72
            $currentModels = $this->getCurrentDomainAssociations($field, $element);
73
74
            $newOrder = [];
75
            if (!$this->associateAll($element, $models, $currentModels, $newOrder)) {
0 ignored issues
show
Bug introduced by
It seems like $currentModels defined by $this->getCurrentDomainA...tions($field, $element) on line 72 can also be of type null; however, flipbox\domains\services...iations::associateAll() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
76
                $transaction->rollBack();
77
                return false;
78
            }
79
80
            if (!$this->dissociateAll($element, $currentModels)) {
81
                $transaction->rollBack();
82
                return false;
83
            }
84
85
            if (!$this->reOrderIfChanged(
86
                DomainsPlugin::getInstance()->getField()->getTableName($field),
87
                $newOrder,
88
                $element
89
            )) {
90
                $transaction->rollBack();
91
                return false;
92
            }
93
        } catch (\Exception $e) {
94
            $transaction->rollBack();
95
            throw $e;
96
        }
97
98
        $transaction->commit();
99
        return true;
100
    }
101
102
    /*******************************************
103
     * ASSOCIATE
104
     *******************************************/
105
106
    /**
107
     * @param Domain $model
108
     * @return bool
109
     * @throws \Exception
110
     */
111
    public function associate(Domain $model): bool
112
    {
113
114
        if ($model->sortOrder === null) {
115
            $model->sortOrder = $this->nextSortOrder($model);
116
        }
117
118
        if ($this->associationExists($model)) {
119
            return $this->applySortOrder($model);
120
        }
121
122
        $event = new DomainAssociationEvent([
123
            'domain' => $model
124
        ]);
125
126
        $this->trigger(
127
            static::EVENT_BEFORE_ASSOCIATE,
128
            $event
129
        );
130
131
        // Green light?
132
        if (!$event->isValid) {
133
            DomainsPlugin::info(
134
                "Event aborted association.",
135
                __METHOD__
136
            );
137
138
            return false;
139
        }
140
141
        $transaction = Craft::$app->getDb()->beginTransaction();
142
        try {
143
            if (!$this->insertRecord(
144
                DomainsPlugin::getInstance()->getField()->getTableName($model->getField()),
145
                $model->getElementId(),
146
                $model->domain,
147
                $model->status,
148
                $model->sortOrder,
149
                $model->getSiteId()
150
            )) {
151
                $transaction->rollBack();
152
                return false;
153
            }
154
155
            $this->trigger(
156
                static::EVENT_AFTER_ASSOCIATE,
157
                $event
158
            );
159
        } catch (\Exception $e) {
160
            $transaction->rollBack();
161
            throw $e;
162
        }
163
164
        $transaction->commit();
165
        return true;
166
    }
167
168
    /*******************************************
169
     * DISSOCIATE
170
     *******************************************/
171
172
    /**
173
     * @param Domain $model
174
     * @param bool $autoReorder
175
     * @return bool
176
     * @throws \Exception
177
     */
178
    public function dissociate(
179
        Domain $model,
180
        bool $autoReorder = true
181
    ) {
182
        if (!$this->associationExists($model)) {
183
            return true;
184
        }
185
186
        $event = new DomainAssociationEvent([
187
            'domain' => $model
188
        ]);
189
190
        // Trigger event
191
        $this->trigger(
192
            static::EVENT_BEFORE_DISSOCIATE,
193
            $event
194
        );
195
196
        // Green light?
197
        if (!$event->isValid) {
198
            DomainsPlugin::info(
199
                "Event aborted dissociation.",
200
                __METHOD__
201
            );
202
            return false;
203
        }
204
205
        $tableName = DomainsPlugin::getInstance()->getField()->getTableName($model->getField());
206
207
        $transaction = Craft::$app->getDb()->beginTransaction();
208
        try {
209
            $this->deleteRecord(
210
                $tableName,
211
                $model->getElementId(),
212
                $model->domain,
213
                $model->getSiteId()
214
            );
215
216
            $this->trigger(
217
                static::EVENT_AFTER_DISSOCIATE,
218
                $event
219
            );
220
221
            // Reorder?
222
            if ($autoReorder === true &&
223
                !$this->autoReorder($tableName, $model->getElementId(), $model->getSiteId())
224
            ) {
225
                $transaction->rollBack();
226
                return false;
227
            }
228
        } catch (\Exception $e) {
229
            $transaction->rollBack();
230
            throw $e;
231
        }
232
233
        $transaction->commit();
234
        return true;
235
    }
236
237
    /*******************************************
238
     * ORDER
239
     *******************************************/
240
241
    /**
242
     * @param string $tableName
243
     * @param int $elementId
244
     * @param int $siteId
245
     * @param array $sortOrder
246
     * @return bool
247
     * @throws \yii\db\Exception
248
     */
249
    public function updateOrder(
250
        string $tableName,
251
        int $elementId,
252
        int $siteId,
253
        array $sortOrder
254
    ): bool {
255
        $db = Craft::$app->getDb();
256
257
        foreach ($sortOrder as $domain => $order) {
258
            $db->createCommand()
259
                ->update(
260
                    $tableName,
261
                    ['sortOrder' => $order],
262
                    [
263
                        'domain' => $domain,
264
                        'elementId' => $elementId,
265
                        'siteId' => $siteId
266
                    ]
267
                )
268
                ->execute();
269
        }
270
271
        return true;
272
    }
273
274
    /*******************************************
275
     * INSERT / DELETE
276
     *******************************************/
277
278
    /**
279
     * @param string $tableName
280
     * @param int $elementId
281
     * @param string $domain
282
     * @param string $status
283
     * @param int $sortOrder
284
     * @param int $siteId
285
     * @return bool
286
     * @throws \yii\db\Exception
287
     */
288
    private function insertRecord(
289
        string $tableName,
290
        int $elementId,
291
        string $domain,
292
        string $status,
293
        int $sortOrder,
294
        int $siteId
295
    ): bool {
296
        if (!$successful = (bool)Craft::$app->getDb()->createCommand()->insert(
297
            $tableName,
298
            [
299
                'elementId' => $elementId,
300
                'domain' => $domain,
301
                'status' => $status,
302
                'sortOrder' => $sortOrder,
303
                'siteId' => $siteId
304
            ]
305
        )->execute()) {
306
            DomainsPlugin::trace(
307
                sprintf(
308
                    "Failed to associate domain '%s' to element '%s' for site '%s'.",
309
                    (string)$domain,
310
                    (string)$elementId,
311
                    (string)$siteId
312
                ),
313
                __METHOD__
314
            );
315
            return false;
316
        }
317
318
        DomainsPlugin::trace(
319
            sprintf(
320
                "Successfully associated domain '%s' to element '%s' for site '%s'.",
321
                (string)$domain,
322
                (string)$elementId,
323
                (string)$siteId
324
            ),
325
            __METHOD__
326
        );
327
328
        return true;
329
    }
330
331
    /**
332
     * @param string $tableName
333
     * @param int $elementId
334
     * @param string $domain
335
     * @param int $siteId
336
     * @return bool
337
     * @throws \yii\db\Exception
338
     */
339
    private function deleteRecord(
340
        string $tableName,
341
        int $elementId,
342
        string $domain,
343
        int $siteId
344
    ): bool {
345
        if (!$successful = (bool)Craft::$app->getDb()->createCommand()->delete(
346
            $tableName,
347
            [
348
                'elementId' => $elementId,
349
                'domain' => $domain,
350
                'siteId' => $siteId
351
            ]
352
        )->execute()) {
353
            DomainsPlugin::trace(
354
                sprintf(
355
                    "Failed to dissociate domain '%s' with element '%s' for site '%s'.",
356
                    (string)$domain,
357
                    (string)$elementId,
358
                    (string)$siteId
359
                ),
360
                __METHOD__
361
            );
362
            return false;
363
        }
364
365
        DomainsPlugin::trace(
366
            sprintf(
367
                "Successfully dissociated domain '%s' with element '%s' for site '%s'",
368
                (string)$domain,
369
                (string)$elementId,
370
                (string)$siteId
371
            ),
372
            __METHOD__
373
        );
374
375
        return true;
376
    }
377
378
379
    /*******************************************
380
     * ASSOCIATE / DISSOCIATE MANY
381
     *******************************************/
382
383
    /**
384
     * @param ElementInterface|Element $element
385
     * @param array $models
386
     * @param array $currentModels
387
     * @param array $newOrder
388
     * @return bool
389
     * @throws \Exception
390
     */
391
    private function associateAll(
392
        ElementInterface $element,
393
        array $models,
394
        array &$currentModels,
395
        array &$newOrder
396
    ): bool {
397
        $ct = 1;
398
        foreach ($models as $model) {
399
            $model->setElement($element);
400
            $model->setSiteId($element->siteId);
1 ignored issue
show
Bug introduced by
Accessing siteId on the interface craft\base\ElementInterface 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...
401
            $model->sortOrder = $ct;
402
403
            $newOrder[$model->domain] = $ct++;
404
405
            if (null !== ArrayHelper::remove($currentModels, $model->domain)) {
406
                continue;
407
            }
408
409
            if (!$this->associate($model)) {
410
                return false;
411
            }
412
        }
413
414
        return true;
415
    }
416
417
    /**
418
     * @param ElementInterface|Element $element
419
     * @param array $models
420
     * @return bool
421
     * @throws \Exception
422
     */
423
    private function dissociateAll(
424
        ElementInterface $element,
425
        array $models
426
    ): bool {
427
        foreach ($models as $model) {
428
            $model->setElement($element);
429
            $model->setSiteId($element->siteId);
1 ignored issue
show
Bug introduced by
Accessing siteId on the interface craft\base\ElementInterface 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...
430
431
            if (!$this->dissociate($model)) {
432
                return false;
433
            }
434
        }
435
436
        return true;
437
    }
438
439
    /*******************************************
440
     * SORT ORDER
441
     *******************************************/
442
443
    /**
444
     * @param Domain $model
445
     * @return bool
446
     * @throws \Exception
447
     * @throws \yii\db\Exception
448
     */
449
    private function applySortOrder(
450
        Domain $model
451
    ): bool {
452
        $tableName = DomainsPlugin::getInstance()->getField()->getTableName($model->getField());
453
454
        $currentSortOrder = $this->currentSortOrder($tableName, $model->getElementId(), $model->getSiteId());
455
456
        if (count($currentSortOrder) < $model->sortOrder) {
457
            $model->sortOrder = count($currentSortOrder);
458
        }
459
460
        $order = ArrayHelper::insertSequential($currentSortOrder, $model->domain, $model->sortOrder);
461
462
        if ($order === false) {
463
            return $this->associate($model);
464
        }
465
466
        if ($order === true) {
467
            return true;
468
        }
469
470
        return $this->updateOrder(
471
            $tableName,
472
            $model->getElementId(),
473
            $model->getSiteId(),
474
            $order
0 ignored issues
show
Bug introduced by
It seems like $order defined by \flipbox\ember\helpers\A...ain, $model->sortOrder) on line 460 can also be of type boolean; however, flipbox\domains\services...ciations::updateOrder() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
475
        );
476
    }
477
478
    /**
479
     * @param Domain $model
480
     * @return int
481
     */
482
    private function nextSortOrder(
483
        Domain $model
484
    ): int {
485
        $maxSortOrder = $this->baseAssociationQuery(
486
            DomainsPlugin::getInstance()->getField()->getTableName($model->getField()),
487
            $model->getElementId(),
488
            $model->getSiteId()
489
        )->max('[[sortOrder]]');
490
491
        $maxSortOrder++;
492
493
        return $maxSortOrder;
494
    }
495
496
    /**
497
     * Re-orders so they are in sequential order
498
     *
499
     * @param string $tableName
500
     * @param int $elementId
501
     * @param int $siteId
502
     * @return bool
503
     * @throws \yii\db\Exception
504
     */
505
    protected function autoReorder(string $tableName, int $elementId, int $siteId): bool
506
    {
507
        $currentSortOrder = $this->currentSortOrder($tableName, $elementId, $siteId);
508
509
        if (empty($currentSortOrder)) {
510
            return true;
511
        }
512
513
        return $this->updateOrder(
514
            $tableName,
515
            $elementId,
516
            $siteId,
517
            array_combine(
518
                range(1, count($currentSortOrder)),
519
                array_keys($currentSortOrder)
520
            )
521
        );
522
    }
523
524
    /**
525
     * @param Domains $field
526
     * @param ElementInterface|Element $element
527
     * @return array|null
528
     */
529
    protected function getCurrentDomainAssociations(Domains $field, ElementInterface $element)
530
    {
531
        return (new DomainsQuery($field))
532
            ->siteId($element->siteId)
1 ignored issue
show
Bug introduced by
Accessing siteId on the interface craft\base\ElementInterface 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...
533
            ->elementId($element->getId())
534
            ->indexBy('domain')
535
            ->all();
536
    }
537
538
    /**
539
     * @param string $tableName
540
     * @param int $elementId
541
     * @param int $siteId
542
     * @return array
543
     */
544
    private function currentSortOrder(string $tableName, int $elementId, int $siteId): array
545
    {
546
        return $this->baseAssociationQuery($tableName, $elementId, $siteId)
547
            ->indexBy('domain')
548
            ->select(['sortOrder'])
549
            ->column();
550
    }
551
552
    /**
553
     * @param string $tableName
554
     * @param int $elementId
555
     * @param int $siteId
556
     * @return Query
557
     */
558
    private function baseAssociationQuery(string $tableName, int $elementId, int $siteId): Query
559
    {
560
        return (new Query())
561
            ->from($tableName)
562
            ->where([
563
                'elementId' => $elementId,
564
                'siteId' => $siteId
565
            ])
566
            ->orderBy(['sortOrder' => SORT_ASC]);
567
    }
568
569
    /*******************************************
570
     * UTILITIES
571
     *******************************************/
572
573
    /**
574
     * @param Domain $model
575
     * @return bool
576
     */
577
    private function associationExists(
578
        Domain $model
579
    ): bool {
580
        $tableName = DomainsPlugin::getInstance()->getField()->getTableName($model->getField());
581
582
        $condition = [
583
            'domain' => $model->domain,
584
        ];
585
586
        if ($model->sortOrder !== null) {
587
            $condition['sortOrder'] = $model->sortOrder;
588
        }
589
590
        return $this->baseAssociationQuery($tableName, $model->getElementId(), $model->getSiteId())
591
            ->andWhere($condition)
592
            ->exists();
593
    }
594
595
    /**
596
     * @param string $tableName
597
     * @param array $newOrder
598
     * @param ElementInterface|Element $element
599
     * @return bool
600
     * @throws \yii\db\Exception
601
     */
602
    private function reOrderIfChanged(string $tableName, array $newOrder, ElementInterface $element)
603
    {
604
        $currentOrder = $this->currentSortOrder($tableName, $element->getId(), $element->siteId);
1 ignored issue
show
Bug introduced by
Accessing siteId on the interface craft\base\ElementInterface 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...
605
606
        if ((empty($currentOrder) && empty($newOrder)) || $currentOrder == $newOrder) {
607
            return true;
608
        }
609
610
        if (!$this->updateOrder($tableName, $element->getId(), $element->siteId, $newOrder)) {
1 ignored issue
show
Bug introduced by
Accessing siteId on the interface craft\base\ElementInterface 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...
611
            return false;
612
        }
613
614
        return true;
615
    }
616
}
617