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

Token::saveEnvironments()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

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