Completed
Push — master ( 04ac75...81967b )
by Nate
17:06
created

Provider::isLocked()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 4
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
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 flipbox\ember\helpers\ModelHelper;
16
use flipbox\ember\helpers\ObjectHelper;
17
use flipbox\ember\helpers\QueryHelper;
18
use flipbox\ember\records\ActiveRecordWithId;
19
use flipbox\ember\records\traits\StateAttribute;
20
use flipbox\ember\traits\HandleRules;
21
use flipbox\patron\db\ProviderActiveQuery;
22
use flipbox\patron\helpers\ProviderHelper;
23
use flipbox\patron\Patron;
24
use flipbox\patron\validators\ProviderValidator;
25
use yii\helpers\ArrayHelper;
26
27
/**
28
 * @author Flipbox Factory <[email protected]>
29
 * @since 1.0.0
30
 *
31
 * @property string $class
32
 * @property ProviderLock[] $locks
33
 * @property Token[] $tokens
34
 * @property ProviderInstance[] $instances
35
 * @property ProviderEnvironment[] $environments
36
 */
37
class Provider extends ActiveRecordWithId
38
{
39
    use HandleRules,
40
        StateAttribute;
41
42
    /**
43
     * The table alias
44
     */
45
    const TABLE_ALIAS = 'patron_providers';
46
47
    /**
48
     * @deprecated
49
     */
50
    const CLIENT_ID_LENGTH = ProviderInstance::CLIENT_ID_LENGTH;
51
52
    /**
53
     * @deprecated
54
     */
55
    const CLIENT_SECRET_LENGTH = ProviderInstance::CLIENT_SECRET_LENGTH;
56
57
    /**
58
     * @var bool
59
     */
60
    public $autoSaveInstances = false;
61
62
    /**
63
     * Environments that are temporarily set during the save process
64
     *
65
     * @var null|array
66
     */
67
    private $insertInstances;
68
69
    /**
70
     * @return string|null
71
     */
72
    public function getIcon()
73
    {
74
        if ($this->class === null) {
75
            return null;
76
        }
77
78
        return Patron::getInstance()->getCp()->getProviderIcon(
79
            $this->class
80
        );
81
    }
82
83
    /**
84
     * @inheritdoc
85
     * @return ProviderActiveQuery
86
     * @throws \yii\base\InvalidConfigException
87
     */
88
    public static function find()
89
    {
90
        /** @noinspection PhpIncompatibleReturnTypeInspection */
91
        return Craft::createObject(ProviderActiveQuery::class, [get_called_class()]);
92
    }
93
94
    /**
95
     * @inheritdoc
96
     */
97
    public function rules()
98
    {
99
        return array_merge(
100
            parent::rules(),
101
            $this->handleRules(),
102
            $this->stateRules(),
103
            [
104
                [
105
                    [
106
                        'class'
107
                    ],
108
                    ProviderValidator::class
109
                ],
110
                [
111
                    [
112
                        'class'
113
                    ],
114
                    'required'
115
                ],
116
                [
117
                    [
118
                        'class',
119
                        'settings'
120
                    ],
121
                    'safe',
122
                    'on' => [
123
                        ModelHelper::SCENARIO_DEFAULT
124
                    ]
125
                ]
126
            ]
127
        );
128
    }
129
130
    /**
131
     * Get all of the associated tokens.
132
     *
133
     * @param array $config
134
     * @return \yii\db\ActiveQuery
135
     */
136
    public function getTokens(array $config = [])
137
    {
138
        $query = $this->hasMany(
139
            Token::class,
140
            ['providerId' => 'id']
141
        );
142
143
        if (!empty($config)) {
144
            QueryHelper::configure(
145
                $query,
146
                $config
147
            );
148
        }
149
150
        return $query;
151
    }
152
153
    /**
154
     * Get all of the associated tokens.
155
     *
156
     * @param array $config
157
     * @return \yii\db\ActiveQuery
158
     */
159
    public function getLocks(array $config = [])
160
    {
161
        $query = $this->hasMany(
162
            ProviderLock::class,
163
            ['providerId' => 'id']
164
        );
165
166
        if (!empty($config)) {
167
            QueryHelper::configure(
168
                $query,
169
                $config
170
            );
171
        }
172
173
        return $query;
174
    }
175
176
    /**
177
     * Get all of the associated instances.
178
     *
179
     * @param array $config
180
     * @return \yii\db\ActiveQuery
181
     */
182
    public function getInstances(array $config = [])
183
    {
184
        $query = $this->hasMany(
185
            ProviderInstance::class,
186
            ['providerId' => 'id']
187
        )
188
            ->indexBy('id');
189
190
        if (!empty($config)) {
191
            QueryHelper::configure(
192
                $query,
193
                $config
194
            );
195
        }
196
197
        return $query;
198
    }
199
200
    /**
201
     * @param array $instances
202
     * @return $this
203
     */
204
    public function setInstances(array $instances = [])
205
    {
206
        $records = [];
207
        foreach (array_filter($instances) as $environment) {
208
            $records[] = $this->resolveInstance($environment);
209
        }
210
211
        $this->populateRelation('instances', $records);
212
        return $this;
213
    }
214
215
    /**
216
     * @param $instance
217
     * @return ProviderInstance
218
     */
219
    protected function resolveInstance($instance): ProviderInstance
220
    {
221
        if ($instance instanceof ProviderInstance) {
222
            return $instance;
223
        }
224
225
        $record = new ProviderInstance();
226
227
        /** @noinspection PhpIncompatibleReturnTypeInspection */
228
        return ObjectHelper::populate(
229
            $record,
230
            $instance
231
        );
232
    }
233
234
    /**
235
     * Get all of the associated environments.
236
     *
237
     * @param array $config
238
     * @return \yii\db\ActiveQuery
239
     */
240
    public function getEnvironments(array $config = [])
241
    {
242
        $query = $this->hasMany(
243
            ProviderEnvironment::class,
244
            ['instanceId' => 'id']
245
        )
246
            ->via('instances')
247
            ->indexBy('environment');
248
249
        if (!empty($config)) {
250
            QueryHelper::configure(
251
                $query,
252
                $config
253
            );
254
        }
255
256
        return $query;
257
    }
258
259
    /*******************************************
260
     * SAVE
261
     *******************************************/
262
263
    /**
264
     * @param PluginInterface $plugin
265
     * @param bool $runValidation
266
     * @param null $attributeNames
267
     * @return bool
268
     * @throws \Throwable
269
     * @throws \yii\db\StaleObjectException
270
     */
271
    public function saveAndLock(PluginInterface $plugin, $runValidation = true, $attributeNames = null): bool
272
    {
273
        if (!$this->save($runValidation, $attributeNames)) {
274
            return false;
275
        }
276
277
        return $this->addLock($plugin);
278
    }
279
280
281
    /*******************************************
282
     * LOCK
283
     *******************************************/
284
285
    /**
286
     * @param PluginInterface $plugin
287
     * @return bool
288
     * @throws \Throwable
289
     * @throws \yii\db\StaleObjectException
290
     */
291
    public function addLock(PluginInterface $plugin): bool
292
    {
293
        if (null === ($pluginId = $this->getPluginId($plugin))) {
294
            return false;
295
        }
296
297
        return Patron::getInstance()->getProviderLocks()->associateByIds(
298
            $this->getId() ?: 0,
299
            $pluginId
300
        );
301
    }
302
303
    /**
304
     * @param PluginInterface $plugin
305
     * @return bool
306
     * @throws \Throwable
307
     */
308
    public function removeLock(PluginInterface $plugin): bool
309
    {
310
        if (null === ($pluginId = $this->getPluginId($plugin))) {
311
            return false;
312
        }
313
314
        return Patron::getInstance()->getProviderLocks()->dissociateByIds(
315
            $this->getId() ?: 0,
316
            $pluginId
317
        );
318
    }
319
320
    /**
321
     * @return bool
322
     */
323
    public function isLocked(): bool
324
    {
325
        return !empty($this->locks);
326
    }
327
328
    /**
329
     * @param PluginInterface $plugin
330
     * @return int|null
331
     */
332
    protected function getPluginId(PluginInterface $plugin)
333
    {
334
        $id = (new Query())
335
            ->select([
336
                'id',
337
            ])
338
            ->from(['{{%plugins}}'])
339
            ->where([
340
                'handle' => $plugin->getHandle()
341
            ])
342
            ->scalar();
343
344
        return $id ? (int)$id : null;
345
    }
346
347
    /**
348
     * @param PluginInterface $plugin
349
     * @return int|null
350
     */
351
    protected function getPluginName(PluginInterface $plugin)
352
    {
353
        $id = (new Query())
354
            ->select([
355
                'id',
356
            ])
357
            ->from(['{{%plugins}}'])
358
            ->where([
359
                'handle' => $plugin->getHandle()
360
            ])
361
            ->scalar();
362
363
        return $id ? (int)$id : null;
364
    }
365
366
367
    /*******************************************
368
     * DELETE
369
     *******************************************/
370
371
    /**
372
     * @param PluginInterface|null $plugin
373
     * @return bool|false|int
374
     * @throws \Throwable
375
     * @throws \yii\db\StaleObjectException
376
     */
377
    public function delete(PluginInterface $plugin = null)
378
    {
379
        return $this->canDelete($plugin) ? parent::delete() : false;
380
    }
381
382
    /**
383
     * @param PluginInterface|null $plugin
384
     * @return bool
385
     * @throws \craft\errors\InvalidPluginException
386
     */
387
    protected function canDelete(PluginInterface $plugin = null)
388
    {
389
        // If a plugin is locking this, prevent deletion
390
        $lockQuery = $this->getLocks();
391
        if (null !== $plugin) {
392
            $lockQuery->andWhere(
393
                ['<>', 'pluginId', $this->getPluginId($plugin)]
394
            );
395
        }
396
397
        $locks = $lockQuery->all();
398
399
        if (count($locks) > 0) {
400
            $handles = (new Query())
401
                ->select([
402
                    'handle',
403
                ])
404
                ->from(['{{%plugins}}'])
405
                ->where([
406
                    'id' => ArrayHelper::getColumn($locks, 'pluginId'),
407
                ])
408
                ->column();
409
410
            $names = [];
411
            foreach ($handles as $handle) {
412
                $plugin = Craft::$app->getPlugins()->getPluginInfo($handle);
413
                $names[] = $plugin['name'] ?? 'Unknown Plugin';
414
            }
415
416
            $this->addError(
417
                'locks',
418
                Craft::t(
419
                    'patron',
420
                    'The provider is locked by the following plugins: {plugins}',
421
                    [
422
                        'plugins' => StringHelper::toString($names, ', ')
423
                    ]
424
                )
425
            );
426
            return false;
427
        }
428
429
        return true;
430
    }
431
432
    /*******************************************
433
     * EVENTS
434
     *******************************************/
435
436
    /**
437
     * @inheritdoc
438
     */
439
    public function beforeSave($insert): bool
440
    {
441
        if (!parent::beforeSave($insert)) {
442
            return false;
443
        }
444
445
        if ($insert !== true ||
446
            $this->isRelationPopulated('instances') !== true ||
447
            $this->autoSaveInstances !== true
448
        ) {
449
            return true;
450
        }
451
452
        $this->insertInstances = $this->instances;
453
454
        return true;
455
    }
456
457
    /*******************************************
458
     * UPDATE / INSERT
459
     *******************************************/
460
461
    /**
462
     * We're extracting the environments that may have been explicitly set on the record.  When the 'id'
463
     * attribute is updated, it removes any associated relationships.
464
     *
465
     * @inheritdoc
466
     * @throws \Throwable
467
     */
468
    protected function insertInternal($attributes = null)
469
    {
470
        if (!parent::insertInternal($attributes)) {
471
            return false;
472
        }
473
474
        if (null === $this->insertInstances) {
475
            return true;
476
        }
477
478
        $this->setInstances($this->insertInstances);
479
        $this->insertInstances = null;
480
481
        return $this->upsertInternal($attributes);
482
    }
483
484
    /**
485
     * @inheritdoc
486
     * @throws \Throwable
487
     */
488
    protected function updateInternal($attributes = null)
489
    {
490
        if (false === ($response = parent::updateInternal($attributes))) {
491
            return false;
492
        }
493
494
        return $this->upsertInternal($attributes) ? $response : false;
495
    }
496
497
    /**
498
     * @param null $attributes
499
     * @return bool
500
     * @throws \Throwable
501
     * @throws \yii\db\StaleObjectException
502
     */
503
    protected function upsertInternal($attributes = null): bool
504
    {
505
        if (empty($attributes)) {
506
            return $this->saveInstances();
507
        }
508
509
        if (array_key_exists('instances', $attributes)) {
510
            return $this->saveInstances(true);
511
        }
512
513
        return true;
514
    }
515
516
    /**
517
     * @param bool $force
518
     * @return bool
519
     * @throws \Throwable
520
     * @throws \yii\db\StaleObjectException
521
     */
522
    protected function saveInstances(bool $force = false): bool
523
    {
524
        if ($force === false && $this->autoSaveInstances !== true) {
525
            return true;
526
        }
527
528
        $successful = true;
529
530
        /** @var ProviderInstance[] $allRecords */
531
        $allRecords = $this->getInstances()
532
            ->all();
533
534
        ArrayHelper::index($allRecords, 'providerId');
535
536
        foreach ($this->instances as $model) {
537
            ArrayHelper::remove($allRecords, $this->getId());
538
            $model->providerId = $this->getId();
539
540
            if (!$model->save()) {
541
                $successful = false;
542
                // Log the errors
543
                $error = Craft::t(
544
                    'patron',
545
                    "Couldn't save instance due to validation errors:"
546
                );
547
                foreach ($model->getFirstErrors() as $attributeError) {
548
                    $error .= "\n- " . Craft::t('patron', $attributeError);
549
                }
550
551
                $this->addError('instances', $error);
552
            }
553
        }
554
555
        // Delete old records
556
        foreach ($allRecords as $record) {
557
            $record->delete();
558
        }
559
560
        return $successful;
561
    }
562
563
    /**
564
     * @return string
565
     */
566
    public function getDisplayName(): string
567
    {
568
        return ProviderHelper::displayName(
569
            $this->class
570
        );
571
    }
572
}
573