Completed
Push — develop ( 856b14...45ab37 )
by Nate
17:44
created

Provider   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 536
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 14

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 56
lcom 3
cbo 14
dl 0
loc 536
ccs 0
cts 301
cp 0
rs 5.5199
c 0
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A getIcon() 0 10 2
A rules() 0 32 1
A getTokens() 0 16 2
A getLocks() 0 16 2
A getInstances() 0 17 2
A setInstances() 0 10 2
A resolveInstance() 0 14 2
A getEnvironments() 0 18 2
A saveAndLock() 0 8 2
A addLock() 0 11 3
A removeLock() 0 11 3
A isLocked() 0 4 1
A getPluginId() 0 14 2
A getPluginName() 0 14 2
A delete() 0 4 2
A canDelete() 0 44 4
A beforeSave() 0 17 5
A insertInternal() 0 15 3
A updateInternal() 0 8 2
A upsertInternal() 0 12 3
B saveInstances() 0 40 7

How to fix   Complexity   

Complex Class

Complex classes like Provider 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 Provider, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * @copyright  Copyright (c) Flipbox Digital Limited
5
 * @license    https://flipboxfactory.com/software/patron/license
6
 * @link       https://www.flipboxfactory.com/software/patron/
7
 */
8
9
namespace flipbox\patron\records;
10
11
use Craft;
12
use craft\base\PluginInterface;
13
use craft\db\Query;
14
use craft\helpers\StringHelper;
15
use craft\helpers\Template;
16
use flipbox\ember\helpers\ModelHelper;
17
use flipbox\ember\helpers\ObjectHelper;
18
use flipbox\ember\helpers\QueryHelper;
19
use flipbox\ember\records\ActiveRecordWithId;
20
use flipbox\ember\records\traits\StateAttribute;
21
use flipbox\ember\traits\HandleRules;
22
use flipbox\patron\db\ProviderActiveQuery;
23
use flipbox\patron\helpers\ProviderHelper;
24
use flipbox\patron\Patron;
25
use flipbox\patron\providers\SettingsInterface;
26
use flipbox\patron\validators\ProviderValidator;
27
use Twig_Markup;
28
use yii\base\InvalidArgumentException;
29
use yii\helpers\ArrayHelper;
30
31
/**
32
 * @author Flipbox Factory <[email protected]>
33
 * @since 1.0.0
34
 *
35
 * @property string $class
36
 * @property ProviderLock[] $locks
37
 * @property Token[] $tokens
38
 * @property ProviderInstance[] $instances
39
 * @property ProviderEnvironment[] $environments
40
 */
41
class Provider extends ActiveRecordWithId
42
{
43
    use HandleRules,
44
        StateAttribute;
45
46
    /**
47
     * The table alias
48
     */
49
    const TABLE_ALIAS = 'patron_providers';
50
51
    /**
52
     * @deprecated
53
     */
54
    const CLIENT_ID_LENGTH = ProviderInstance::CLIENT_ID_LENGTH;
55
56
    /**
57
     * @deprecated
58
     */
59
    const CLIENT_SECRET_LENGTH = ProviderInstance::CLIENT_SECRET_LENGTH;
60
61
    /**
62
     * @var bool
63
     */
64
    public $autoSaveInstances = false;
65
66
    /**
67
     * Environments that are temporarily set during the save process
68
     *
69
     * @var null|array
70
     */
71
    private $insertInstances;
72
73
    /**
74
     * @return string|null
75
     */
76
    public function getIcon()
77
    {
78
        if ($this->class === null) {
79
            return null;
80
        }
81
82
        return Patron::getInstance()->getCp()->getProviderIcon(
83
            $this->class
84
        );
85
    }
86
87
    /**
88
     * @inheritdoc
89
     * @return ProviderActiveQuery
90
     * @throws \yii\base\InvalidConfigException
91
     */
92
    public static function find()
93
    {
94
        /** @noinspection PhpIncompatibleReturnTypeInspection */
95
        return Craft::createObject(ProviderActiveQuery::class, [get_called_class()]);
96
    }
97
98
    /**
99
     * @inheritdoc
100
     */
101
    public function rules()
102
    {
103
        return array_merge(
104
            parent::rules(),
105
            $this->handleRules(),
106
            $this->stateRules(),
107
            [
108
                [
109
                    [
110
                        'class'
111
                    ],
112
                    ProviderValidator::class
113
                ],
114
                [
115
                    [
116
                        'class'
117
                    ],
118
                    'required'
119
                ],
120
                [
121
                    [
122
                        'class',
123
                        'settings'
124
                    ],
125
                    'safe',
126
                    'on' => [
127
                        ModelHelper::SCENARIO_DEFAULT
128
                    ]
129
                ]
130
            ]
131
        );
132
    }
133
134
    /**
135
     * Get all of the associated tokens.
136
     *
137
     * @param array $config
138
     * @return \yii\db\ActiveQuery
139
     */
140
    public function getTokens(array $config = [])
141
    {
142
        $query = $this->hasMany(
143
            Token::class,
144
            ['providerId' => 'id']
145
        );
146
147
        if (!empty($config)) {
148
            QueryHelper::configure(
149
                $query,
150
                $config
151
            );
152
        }
153
154
        return $query;
155
    }
156
157
    /**
158
     * Get all of the associated tokens.
159
     *
160
     * @param array $config
161
     * @return \yii\db\ActiveQuery
162
     */
163
    public function getLocks(array $config = [])
164
    {
165
        $query = $this->hasMany(
166
            ProviderLock::class,
167
            ['providerId' => 'id']
168
        );
169
170
        if (!empty($config)) {
171
            QueryHelper::configure(
172
                $query,
173
                $config
174
            );
175
        }
176
177
        return $query;
178
    }
179
180
    /**
181
     * Get all of the associated instances.
182
     *
183
     * @param array $config
184
     * @return \yii\db\ActiveQuery
185
     */
186
    public function getInstances(array $config = [])
187
    {
188
        $query = $this->hasMany(
189
            ProviderInstance::class,
190
            ['providerId' => 'id']
191
        )
192
            ->indexBy('id');
193
194
        if (!empty($config)) {
195
            QueryHelper::configure(
196
                $query,
197
                $config
198
            );
199
        }
200
201
        return $query;
202
    }
203
204
    /**
205
     * @param array $instances
206
     * @return $this
207
     */
208
    public function setInstances(array $instances = [])
209
    {
210
        $records = [];
211
        foreach (array_filter($instances) as $environment) {
212
            $records[] = $this->resolveInstance($environment);
213
        }
214
215
        $this->populateRelation('instances', $records);
216
        return $this;
217
    }
218
219
    /**
220
     * @param $instance
221
     * @return ProviderInstance
222
     */
223
    protected function resolveInstance($instance): ProviderInstance
224
    {
225
        if ($instance instanceof ProviderInstance) {
226
            return $instance;
227
        }
228
229
        $record = new ProviderInstance();
230
231
        /** @noinspection PhpIncompatibleReturnTypeInspection */
232
        return ObjectHelper::populate(
233
            $record,
234
            $instance
235
        );
236
    }
237
238
    /**
239
     * Get all of the associated environments.
240
     *
241
     * @param array $config
242
     * @return \yii\db\ActiveQuery
243
     */
244
    public function getEnvironments(array $config = [])
245
    {
246
        $query = $this->hasMany(
247
            ProviderEnvironment::class,
248
            ['instanceId' => 'id']
249
        )
250
            ->via('instances')
251
            ->indexBy('environment');
252
253
        if (!empty($config)) {
254
            QueryHelper::configure(
255
                $query,
256
                $config
257
            );
258
        }
259
260
        return $query;
261
    }
262
263
    /*******************************************
264
     * SAVE
265
     *******************************************/
266
267
    /**
268
     * @param PluginInterface $plugin
269
     * @param bool $runValidation
270
     * @param null $attributeNames
271
     * @return bool
272
     * @throws \Throwable
273
     * @throws \yii\db\StaleObjectException
274
     */
275
    public function saveAndLock(PluginInterface $plugin, $runValidation = true, $attributeNames = null): bool
276
    {
277
        if (!$this->save($runValidation, $attributeNames)) {
278
            return false;
279
        }
280
281
        return $this->addLock($plugin);
282
    }
283
284
285
    /*******************************************
286
     * LOCK
287
     *******************************************/
288
289
    /**
290
     * @param PluginInterface $plugin
291
     * @return bool
292
     * @throws \Throwable
293
     * @throws \yii\db\StaleObjectException
294
     */
295
    public function addLock(PluginInterface $plugin): bool
296
    {
297
        if (null === ($pluginId = $this->getPluginId($plugin))) {
298
            return false;
299
        }
300
301
        return Patron::getInstance()->getProviderLocks()->associateByIds(
302
            $this->getId() ?: 0,
303
            $pluginId
304
        );
305
    }
306
307
    /**
308
     * @param PluginInterface $plugin
309
     * @return bool
310
     * @throws \Throwable
311
     */
312
    public function removeLock(PluginInterface $plugin): bool
313
    {
314
        if (null === ($pluginId = $this->getPluginId($plugin))) {
315
            return false;
316
        }
317
318
        return Patron::getInstance()->getProviderLocks()->dissociateByIds(
319
            $this->getId() ?: 0,
320
            $pluginId
321
        );
322
    }
323
324
    /**
325
     * @return bool
326
     */
327
    public function isLocked(): bool
328
    {
329
        return !empty($this->locks);
330
    }
331
332
    /**
333
     * @param PluginInterface $plugin
334
     * @return int|null
335
     */
336
    protected function getPluginId(PluginInterface $plugin)
337
    {
338
        $id = (new Query())
339
            ->select([
340
                'id',
341
            ])
342
            ->from(['{{%plugins}}'])
343
            ->where([
344
                'handle' => $plugin->getHandle()
345
            ])
346
            ->scalar();
347
348
        return $id ? (int)$id : null;
349
    }
350
351
    /**
352
     * @param PluginInterface $plugin
353
     * @return int|null
354
     */
355
    protected function getPluginName(PluginInterface $plugin)
356
    {
357
        $id = (new Query())
358
            ->select([
359
                'id',
360
            ])
361
            ->from(['{{%plugins}}'])
362
            ->where([
363
                'handle' => $plugin->getHandle()
364
            ])
365
            ->scalar();
366
367
        return $id ? (int)$id : null;
368
    }
369
370
371
    /*******************************************
372
     * DELETE
373
     *******************************************/
374
375
    /**
376
     * @param PluginInterface|null $plugin
377
     * @return bool|false|int
378
     * @throws \Throwable
379
     * @throws \yii\db\StaleObjectException
380
     */
381
    public function delete(PluginInterface $plugin = null)
382
    {
383
        return $this->canDelete($plugin) ? parent::delete() : false;
384
    }
385
386
    /**
387
     * @param PluginInterface|null $plugin
388
     * @return bool
389
     * @throws \craft\errors\InvalidPluginException
390
     */
391
    protected function canDelete(PluginInterface $plugin = null)
392
    {
393
        // If a plugin is locking this, prevent deletion
394
        $lockQuery = $this->getLocks();
395
        if (null !== $plugin) {
396
            $lockQuery->andWhere(
397
                ['<>', 'pluginId', $this->getPluginId($plugin)]
398
            );
399
        }
400
401
        $locks = $lockQuery->all();
402
403
        if (count($locks) > 0) {
404
            $handles = (new Query())
405
                ->select([
406
                    'handle',
407
                ])
408
                ->from(['{{%plugins}}'])
409
                ->where([
410
                    'id' => ArrayHelper::getColumn($locks, ['pluginId']),
0 ignored issues
show
Documentation introduced by
array('pluginId') is of type array<integer,string,{"0":"string"}>, but the function expects a string|object<Closure>.

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...
411
                ])
412
                ->column();
413
414
            $names = [];
415
            foreach ($handles as $handle) {
416
                $plugin = Craft::$app->getPlugins()->getPluginInfo($handle);
417
                $names[] = $plugin['name'] ?? 'Unknown Plugin';
418
            }
419
420
            $this->addError(
421
                'locks',
422
                Craft::t(
423
                    'patron',
424
                    'The provider is locked by the following plugins: {plugins}',
425
                    [
426
                        'plugins' => StringHelper::toString($names, ', ')
427
                    ]
428
                )
429
            );
430
            return false;
431
        }
432
433
        return true;
434
    }
435
436
    /*******************************************
437
     * EVENTS
438
     *******************************************/
439
440
    /**
441
     * @inheritdoc
442
     */
443
    public function beforeSave($insert): bool
444
    {
445
        if (!parent::beforeSave($insert)) {
446
            return false;
447
        }
448
449
        if ($insert !== true ||
450
            $this->isRelationPopulated('instances') !== true ||
451
            $this->autoSaveInstances !== true
452
        ) {
453
            return true;
454
        }
455
456
        $this->insertInstances = $this->instances;
457
458
        return true;
459
    }
460
461
    /*******************************************
462
     * UPDATE / INSERT
463
     *******************************************/
464
465
    /**
466
     * We're extracting the environments that may have been explicitly set on the record.  When the 'id'
467
     * attribute is updated, it removes any associated relationships.
468
     *
469
     * @inheritdoc
470
     * @throws \Throwable
471
     */
472
    protected function insertInternal($attributes = null)
473
    {
474
        if (!parent::insertInternal($attributes)) {
475
            return false;
476
        }
477
478
        if (null === $this->insertInstances) {
479
            return true;
480
        }
481
482
        $this->setInstances($this->insertInstances);
483
        $this->insertInstances = null;
484
485
        return $this->upsertInternal($attributes);
486
    }
487
488
    /**
489
     * @inheritdoc
490
     * @throws \Throwable
491
     */
492
    protected function updateInternal($attributes = null)
493
    {
494
        if (!parent::updateInternal($attributes)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression parent::updateInternal($attributes) of type false|integer is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
495
            return false;
496
        }
497
498
        return $this->upsertInternal($attributes);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->upsertInternal($attributes); (boolean) is incompatible with the return type of the parent method yii\db\BaseActiveRecord::updateInternal of type false|integer.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
499
    }
500
501
    /**
502
     * @param null $attributes
503
     * @return bool
504
     * @throws \Throwable
505
     * @throws \yii\db\StaleObjectException
506
     */
507
    protected function upsertInternal($attributes = null): bool
508
    {
509
        if (empty($attributes)) {
510
            return $this->saveInstances();
511
        }
512
513
        if (array_key_exists('instances', $attributes)) {
514
            return $this->saveInstances(true);
515
        }
516
517
        return true;
518
    }
519
520
    /**
521
     * @param bool $force
522
     * @return bool
523
     * @throws \Throwable
524
     * @throws \yii\db\StaleObjectException
525
     */
526
    protected function saveInstances(bool $force = false): bool
527
    {
528
        if ($force === false && $this->autoSaveInstances !== true) {
529
            return true;
530
        }
531
532
        $successful = true;
533
534
        /** @var ProviderInstance[] $allRecords */
535
        $allRecords = $this->getInstances()
536
            ->all();
537
538
        ArrayHelper::index($allRecords, 'providerId');
539
540
        foreach ($this->instances as $model) {
541
            ArrayHelper::remove($allRecords, $this->getId());
542
            $model->providerId = $this->getId();
543
544
            if (!$model->save()) {
545
                $successful = false;
546
                // Log the errors
547
                $error = Craft::t(
548
                    'patron',
549
                    "Couldn't save instance due to validation errors:"
550
                );
551
                foreach ($model->getFirstErrors() as $attributeError) {
552
                    $error .= "\n- " . Craft::t('patron', $attributeError);
553
                }
554
555
                $this->addError('instances', $error);
556
            }
557
        }
558
559
        // Delete old records
560
        foreach ($allRecords as $record) {
561
            $record->delete();
562
        }
563
564
        return $successful;
565
    }
566
567
    /**
568
     * @return string
569
     */
570
    public function getDisplayName(): string
571
    {
572
        return ProviderHelper::displayName(
573
            $this->class
574
        );
575
    }
576
}
577