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

ProviderInstance::rules()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 47
ccs 0
cts 11
cp 0
rs 9.1563
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\helpers\ArrayHelper;
13
use flipbox\ember\helpers\ModelHelper;
14
use flipbox\ember\helpers\ObjectHelper;
15
use flipbox\ember\helpers\QueryHelper;
16
use flipbox\ember\records\ActiveRecordWithId;
17
use flipbox\patron\helpers\ProviderHelper;
18
use flipbox\patron\Patron;
19
use flipbox\patron\providers\SettingsInterface;
20
use flipbox\patron\validators\ProviderSettings as ProviderSettingsValidator;
21
use yii\base\InvalidArgumentException;
22
23
/**
24
 * @author Flipbox Factory <[email protected]>
25
 * @since 1.0.0
26
 *
27
 * @property string $clientId
28
 * @property string $clientSecret
29
 * @property array $settings
30
 * @property int $providerId
31
 * @property Provider $provider
32
 * @property ProviderEnvironment[] $environments
33
 */
34
class ProviderInstance extends ActiveRecordWithId
35
{
36
    use traits\ProviderAttribute;
37
38
    /**
39
     * The table alias
40
     */
41
    const TABLE_ALIAS = 'patron_provider_instances';
42
43
    /**
44
     * The length of the identifier
45
     */
46
    const CLIENT_ID_LENGTH = 100;
47
48
    /**
49
     * The length of the secret
50
     */
51
    const CLIENT_SECRET_LENGTH = 255;
52
53
    /**
54
     * @var bool
55
     */
56
    public $autoSaveEnvironments = true;
57
58
    /**
59
     * Environments that are temporarily set during the save process
60
     *
61
     * @var null|array
62
     */
63
    private $insertEnvironments;
64
65
    /**
66
     * @var SettingsInterface
67
     */
68
    private $providerSettings;
69
70
    /**
71
     * @inheritdoc
72
     */
73
    protected $getterPriorityAttributes = [
74
        'providerId'
75
    ];
76
77
    /**
78
     * @inheritdoc
79
     */
80
    public function rules()
81
    {
82
        return array_merge(
83
            parent::rules(),
84
            $this->providerRules(),
85
            [
86
                [
87
                    [
88
                        'clientId'
89
                    ],
90
                    'string',
91
                    'max' => static::CLIENT_ID_LENGTH
92
                ],
93
                [
94
                    [
95
                        'clientSecret'
96
                    ],
97
                    'string',
98
                    'max' => static::CLIENT_SECRET_LENGTH
99
                ],
100
                [
101
                    [
102
                        'providerId',
103
                        'clientId'
104
                    ],
105
                    'required'
106
                ],
107
                [
108
                    [
109
                        'settings'
110
                    ],
111
                    ProviderSettingsValidator::class
112
                ],
113
                [
114
                    [
115
                        'clientId',
116
                        'clientSecret',
117
                        'settings'
118
                    ],
119
                    'safe',
120
                    'on' => [
121
                        ModelHelper::SCENARIO_DEFAULT
122
                    ]
123
                ]
124
            ]
125
        );
126
    }
127
128
129
    /**
130
     * Get all of the associated environments.
131
     *
132
     * @param array $config
133
     * @return \yii\db\ActiveQueryInterface
134
     */
135
    public function getEnvironments(array $config = [])
136
    {
137
        $query = $this->hasMany(
138
            ProviderEnvironment::class,
139
            ['instanceId' => 'id']
140
        )
141
            ->indexBy('environment');
142
143
        if (!empty($config)) {
144
            QueryHelper::configure(
145
                $query,
146
                $config
147
            );
148
        }
149
150
        return $query;
151
    }
152
153
    /**
154
     * @param array $environments
155
     * @return $this
156
     */
157
    public function setEnvironments(array $environments = [])
158
    {
159
        $environments = array_filter($environments);
160
161
        // Do nothing
162
        if (empty($environments) && !$this->isRelationPopulated('environments')) {
163
            return $this;
164
        }
165
166
        $records = [];
167
        foreach (array_filter($environments) as $key => $environment) {
168
            $records[$key] = $this->resolveEnvironment($key, $environment);
169
        }
170
171
        $this->populateRelation('environments', $records);
172
        return $this;
173
    }
174
175
    /**
176
     * @param string $key
177
     * @param $environment
178
     * @return ProviderEnvironment
179
     */
180
    protected function resolveEnvironment(string $key, $environment): ProviderEnvironment
181
    {
182
        if ($environment instanceof ProviderEnvironment) {
183
            return $environment;
184
        }
185
186
        if (!$record = $this->environments[$key] ?? null) {
187
            $record = new ProviderEnvironment();
188
        }
189
190
        if (!is_array($environment)) {
191
            $environment = ['environment' => $environment];
192
        }
193
194
        /** @noinspection PhpIncompatibleReturnTypeInspection */
195
        return ObjectHelper::populate(
196
            $record,
197
            $environment
198
        );
199
    }
200
201
202
    /*******************************************
203
     * EVENTS
204
     *******************************************/
205
206
    /**
207
     * @inheritdoc
208
     */
209
    public function afterFind()
210
    {
211
        if ($this->clientSecret) {
212
            $this->clientSecret = ProviderHelper::decryptClientSecret($this->clientSecret);
213
        }
214
215
        parent::afterFind();
216
    }
217
218
    /**
219
     * @inheritdoc
220
     */
221
    public function beforeSave($insert)
222
    {
223
        if ($this->clientSecret) {
224
            $this->clientSecret = ProviderHelper::encryptClientSecret($this->clientSecret);
225
        }
226
227
        if (!parent::beforeSave($insert)) {
228
            return false;
229
        }
230
231
        if ($insert !== true ||
232
            $this->isRelationPopulated('environments') !== true ||
233
            $this->autoSaveEnvironments !== true
234
        ) {
235
            return true;
236
        }
237
238
        $this->insertEnvironments = $this->environments;
239
240
        return true;
241
    }
242
243
    /**
244
     * @inheritdoc
245
     * @throws \Throwable
246
     */
247
    public function afterSave($insert, $changedAttributes)
248
    {
249
        if ($this->clientSecret) {
250
            $this->clientSecret = ProviderHelper::decryptClientSecret($this->clientSecret);
251
        }
252
253
        parent::afterSave($insert, $changedAttributes);
254
    }
255
256
257
    /*******************************************
258
     * UPDATE / INSERT
259
     *******************************************/
260
261
    /**
262
     * We're extracting the environments that may have been explicitly set on the record.  When the 'id'
263
     * attribute is updated, it removes any associated relationships.
264
     *
265
     * @inheritdoc
266
     * @throws \Throwable
267
     */
268
    protected function insertInternal($attributes = null)
269
    {
270
        if (!parent::insertInternal($attributes)) {
271
            return false;
272
        }
273
274
        if (null === $this->insertEnvironments) {
275
            return true;
276
        }
277
278
        $this->setEnvironments($this->insertEnvironments);
279
        $this->insertEnvironments = null;
280
281
        return $this->upsertInternal($attributes);
282
    }
283
284
    /**
285
     * @inheritdoc
286
     * @throws \Throwable
287
     */
288
    protected function updateInternal($attributes = null)
289
    {
290
        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...
291
            return false;
292
        }
293
294
        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...
295
    }
296
297
    /**
298
     * @param null $attributes
299
     * @return bool
300
     * @throws \Throwable
301
     * @throws \yii\db\StaleObjectException
302
     */
303
    protected function upsertInternal($attributes = null): bool
304
    {
305
        if (empty($attributes)) {
306
            return $this->saveEnvironments();
307
        }
308
309
        if (array_key_exists('environments', $attributes)) {
310
            return $this->saveEnvironments(true);
311
        }
312
313
        return true;
314
    }
315
316
317
    /*******************************************
318
     * ENVIRONMENTS
319
     *******************************************/
320
321
    /**
322
     * @param bool $force
323
     * @return bool
324
     * @throws \Throwable
325
     * @throws \yii\db\StaleObjectException
326
     */
327
    protected function saveEnvironments(bool $force = false): bool
328
    {
329
        if ($force === false && $this->autoSaveEnvironments !== true) {
330
            return true;
331
        }
332
333
        $successful = true;
334
335
        /** @var ProviderEnvironment[] $allRecords */
336
        $allRecords = $this->getEnvironments()
337
            ->indexBy('environment')
338
            ->all();
339
340
341
        foreach ($this->environments as $model) {
342
            ArrayHelper::remove($allRecords, $model->environment);
343
            $model->instanceId = $this->getId();
344
345
            if (!$model->save()) {
346
                $successful = false;
347
348
                $error = Craft::t(
349
                    'patron',
350
                    "Couldn't save environment due to validation errors:"
351
                );
352
353
                foreach ($model->getFirstErrors() as $attributeError) {
354
                    $error .= "\n- " . Craft::t('patron', $attributeError);
355
                }
356
357
                $this->addError('environments', $error);
358
            }
359
        }
360
361
        // Delete old records
362
        foreach ($allRecords as $record) {
363
            $record->delete();
364
        }
365
366
        return $successful;
367
    }
368
369
370
    /**
371
     * @return string
372
     * @throws \yii\base\InvalidConfigException
373
     */
374
    public function getHtml(): string
375
    {
376
        return $this->getProviderSettings()->inputHtml();
377
    }
378
379
    /**
380
     * @return SettingsInterface
381
     * @throws \yii\base\InvalidConfigException
382
     */
383
    public function getProviderSettings(): SettingsInterface
384
    {
385
        if (!$this->providerSettings instanceof SettingsInterface) {
386
            $this->providerSettings = Patron::getInstance()->getProviderSettings()->resolveSettings(
387
                $this->getProvider(),
388
                $this->settings
389
            );
390
        }
391
392
        return $this->providerSettings;
393
    }
394
}
395