Completed
Push — master ( 93d695...69007e )
by vistart
05:29
created

MultipleBlameableTrait   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 336
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 81.08%

Importance

Changes 13
Bugs 0 Features 0
Metric Value
wmc 56
c 13
b 0
f 0
lcom 1
cbo 8
dl 0
loc 336
ccs 120
cts 148
cp 0.8108
rs 6.5957

18 Methods

Rating   Name   Duplication   Size   Complexity  
A getMultipleBlameableAttributeRules() 0 8 2
B addBlame() 0 23 6
A createBlame() 0 9 2
C addOrCreateBlame() 0 27 8
B removeBlame() 0 19 5
A removeAllBlames() 0 4 1
A getBlamesCount() 0 4 1
A getBlameGuids() 0 13 3
A onBlamesChanged() 0 5 1
A unsetInvalidBlames() 0 14 3
A setBlameGuids() 0 12 4
A getBlame() 0 9 4
A getOrCreateBlame() 0 13 4
A getBlameds() 0 10 2
A getAllBlames() 0 11 4
A getNonBlameds() 0 5 1
A onInitBlamesLimit() 0 7 4
A getEmptyBlamesJson() 0 4 1

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
/**
4
 *  _   __ __ _____ _____ ___  ____  _____
5
 * | | / // // ___//_  _//   ||  __||_   _|
6
 * | |/ // /(__  )  / / / /| || |     | |
7
 * |___//_//____/  /_/ /_/ |_||_|     |_|
8
 * @link http://vistart.name/
9
 * @copyright Copyright (c) 2016 vistart
10
 * @license http://vistart.name/license/
11
 */
12
13
namespace vistart\Models\traits;
14
15
use vistart\Helpers\Number;
16
use vistart\Models\events\MultipleBlameableEvent;
17
use yii\web\JsonParser;
18
19
/**
20
 * 一个模型的某个属性可能对应多个责任者,该 trait 用于处理此种情况。此种情况违反
21
 * 了关系型数据库第一范式,因此此 trait 只适用于责任者属性修改不频繁的场景,在开
22
 * 发时必须严格测试数据一致性,并同时考量性能。
23
 * Basic Principles:
24
 * <ol>
25
 * <li>when adding blame, it will check whether each of blames including to be
26
 * added is valid.
27
 * </li>
28
 * <li>when removing blame, as well as counting, getting or setting list of them,
29
 * it will also check whether each of blames is valid.
30
 * </li>
31
 * <li>By default, once blame was deleted, the guid of it is not removed from
32
 * list of blames immediately. It will check blame if valid when adding, removing,
33
 * counting, getting and setting it. You can define a blame model and attach it
34
 * events triggered when inserting, updating and deleting a blame, then disable
35
 * checking the validity of blames.
36
 * </li>
37
 * </ol>
38
 * Notice:
39
 * <ol>
40
 * <li>You must specify two properties: $multiBlamesClass and $multiBlamesAttribute.
41
 * <ul>
42
 * <li>$multiBlamesClass specify the class name of blame.</li>
43
 * <li>$multiBlamesAttribute specify the field name of blames.</li>
44
 * </ul>
45
 * </li>
46
 * <li>You should rename each name of following methods to be needed optionally.</li>
47
 * </ol>
48
 * @property-read array $multiBlamesAttributeRules
49
 * @property array $blameGuids
50
 * @property-read array $allBlames
51
 * @property-read array $nonBlameds
52
 * @property-read integer $blamesCount
53
 * @version 2.0
54
 * @author vistart <[email protected]>
55
 */
56
trait MultipleBlameableTrait
57
{
58
59
    /**
60
     * @var string class name of multiple blameable class.
61
     */
62
    public $multiBlamesClass = '';
63
64
    /**
65
     * @var string name of multiple blameable attribute.
66
     */
67
    public $multiBlamesAttribute = 'blames';
68
69
    /**
70
     * @var integer the limit of blames. it should be greater than or equal 1, and
71
     * less than or equal 10.
72
     */
73
    public $blamesLimit = 10;
74
75
    /**
76
     * @var boolean determines whether blames list has been changed.
77
     */
78
    public $blamesChanged = false;
79
80
    /**
81
     * @var string event name.
82
     */
83
    public static $eventMultipleBlamesChanged = 'multipleBlamesChanged';
84
85
    /**
86
     * Get the rules associated with multiple blameable attribute.
87
     * @return array rules.
88
     */
89 8
    public function getMultipleBlameableAttributeRules()
90
    {
91 8
        return is_string($this->multiBlamesAttribute) ? [
92 8
            [[$this->multiBlamesAttribute], 'required'],
93 8
            [[$this->multiBlamesAttribute], 'string', 'max' => $this->blamesLimit * 39 + 1],
94 8
            [[$this->multiBlamesAttribute], 'default', 'value' => '[]'],
95 8
            ] : [];
96
    }
97
98
    /**
99
     * Add specified blame.
100
     * @param [multiBlamesClass]|string $blame
0 ignored issues
show
Documentation introduced by
The doc-type [multiBlamesClass]|string could not be parsed: Unknown type name "" at position 0. [(view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
101
     * @return false|array
102
     */
103 2
    public function addBlame($blame)
104
    {
105 2
        if (!is_string($this->multiBlamesAttribute)) {
106
            return false;
107
        }
108 2
        $blameGuid = '';
109 2
        if (is_string($blame)) {
110 1
            $blameGuid = $blame;
111 1
        }
112 2
        if ($blame instanceof $this->multiBlamesClass) {
113 1
            $blameGuid = $blame->guid;
114 1
        }
115 2
        $blameGuids = $this->getBlameGuids(true);
116 2
        if (array_search($blameGuid, $blameGuids)) {
117
            throw new \yii\base\InvalidParamException('the blame has existed.');
118
        }
119 2
        if ($this->getBlamesCount() >= $this->blamesLimit) {
120
            throw new \yii\base\InvalidCallException("the limit($this->blamesLimit) of blames has been reached.");
121
        }
122 2
        $blameGuids[] = $blameGuid;
123 2
        $this->setBlameGuids($blameGuids);
124 2
        return $this->getBlameGuids();
125
    }
126
127
    /**
128
     * Create blame.
129
     * @param \vistart\Models\models\BaseUserModel $user who will own this blame.
130
     * @param array $config blame class config array.
131
     */
132 1
    public static function createBlame($user, $config = [])
133
    {
134 1
        if (!($user instanceof \vistart\Models\models\BaseUserModel)) {
135
            throw new \yii\base\InvalidParamException('the type of user instance must be BaseUserModel or its extended class.');
136
        }
137 1
        $mbClass = static::buildNoInitModel();
138 1
        $mb = $mbClass->multiBlamesClass;
139 1
        return $user->create($mb::className(), $config);
140
    }
141
142
    /**
143
     * Add specified blame, or create it before adding if it didn't exist.
144
     * @param [multiBlamesClass]|string|array $blame If this is string or
0 ignored issues
show
Documentation introduced by
The doc-type [multiBlamesClass]|string|array could not be parsed: Unknown type name "" at position 0. [(view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
145
     * [multiBlamesClass] instance, and the it existed, then will add it. If it
146
     * didn't exist, and this is a array, it will be regarded as config array.
147
     * Notice, This parameter passed by reference, so it must be a variable!
148
     * @param \vistart\Models\models\BaseUserModel $user whose blame.
149
     * If null, it will take this blameable model's user.
150
     * @return false|array false if blame created failed or not enable this feature.
151
     * blames guid array if created and added successfully.
152
     * @throws \yii\base\InvalidConfigException
153
     * @throws \yii\base\InvalidParamException
154
     * @see addBlame()
155
     */
156 1
    public function addOrCreateBlame(&$blame = null, $user = null)
157
    {
158 1
        if (!is_string($this->multiBlamesClass)) {
159
            throw new \yii\base\InvalidConfigException('$multiBlamesClass must be specified if you want to use multiple blameable features.');
160
        }
161 1
        if (is_array($blame)) {
162 1
            if ($user == null) {
163 1
                $user = $this->user;
0 ignored issues
show
Bug introduced by
The property user does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
164 1
            }
165 1
            $blame = static::getOrCreateBlame($blame, $user);
0 ignored issues
show
Documentation introduced by
$blame is of type array, but the function expects a object<vistart\Models\traits\type>.

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...
166 1
            if (!$blame->save()) {
167
                return false;
168
            }
169 1
            return $this->addBlame($blame->guid);
170
        }
171
        $blameGuid = '';
172
        if (is_string($blame)) {
173
            $blameGuid = $blame;
174
        }
175
        if ($blame instanceof $this->multiBlamesClass) {
176
            $blameGuid = $blame->guid;
177
        }
178
        if (($mb = static::getBlame($blameGuid)) !== null) {
179
            return $this->addBlame($mb);
180
        }
181
        return false;
182
    }
183
184
    /**
185
     * Remove specified blame.
186
     * @param [multiBlamesClass] $blame
0 ignored issues
show
Documentation introduced by
The doc-type [multiBlamesClass] could not be parsed: Unknown type name "" at position 0. [(view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
187
     * @return false|array all guids in json format.
188
     */
189 1
    public function removeBlame($blame)
190
    {
191 1
        if (!is_string($this->multiBlamesAttribute)) {
192
            return false;
193
        }
194 1
        $blameGuid = '';
195 1
        if (is_string($blame)) {
196 1
            $blameGuid = $blame;
197 1
        }
198 1
        if ($blame instanceof $this->multiBlamesClass) {
199 1
            $blameGuid = $blame->guid;
200 1
        }
201 1
        $blameGuids = $this->getBlameGuids(true);
202 1
        if (($key = array_search($blameGuid, $blameGuids)) !== false) {
203 1
            unset($blameGuids[$key]);
204 1
            $this->setBlameGuids($blameGuids);
205 1
        }
206 1
        return $this->getBlameGuids();
207
    }
208
209
    /**
210
     * Remove all blames.
211
     */
212 8
    public function removeAllBlames()
213
    {
214 8
        $this->setBlameGuids();
215 8
    }
216
217
    /**
218
     * Count the blames.
219
     * @return integer
220
     */
221 2
    public function getBlamesCount()
222
    {
223 2
        return count($this->getBlameGuids(true));
224
    }
225
226
    /**
227
     * Get the guid array of blames. it may check all guids if valid before return.
228
     * @param boolean $checkValid determines whether checking the blame is valid.
229
     * @return array all guids in json format.
230
     */
231 2
    public function getBlameGuids($checkValid = false)
232
    {
233 2
        $multiBlamesAttribute = $this->multiBlamesAttribute;
234 2
        if ($multiBlamesAttribute === false) {
235
            return [];
236
        }
237 2
        $jsonParser = new JsonParser();
238 2
        $guids = $jsonParser->parse($this->$multiBlamesAttribute, true);
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a string.

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...
239 2
        if ($checkValid) {
240 2
            $guids = $this->unsetInvalidBlames($guids);
241 2
        }
242 2
        return $guids;
243
    }
244
245
    /**
246
     * Event triggered when blames list changed.
247
     * @param \vistart\Models\events\MultipleBlameableEvent $event
248
     */
249 8
    public function onBlamesChanged($event)
250
    {
251 8
        $sender = $event->sender;
252 8
        $sender->blamesChanged = $event->blamesChanged;
253 8
    }
254
255
    /**
256
     * Remove invalid blame guid from provided guid array.
257
     * @param array $guids guid array of blames.
258
     * @return array guid array of blames unset invalid.
259
     */
260 8
    protected function unsetInvalidBlames($guids)
261
    {
262 8
        $checkedGuids = Number::unsetInvalidUuids($guids);
263 8
        $multiBlamesClass = $this->multiBlamesClass;
264 8
        foreach ($checkedGuids as $key => $guid) {
265 2
            $blame = $multiBlamesClass::findOne($guid);
266 2
            if (!$blame) {
267 1
                unset($checkedGuids[$key]);
268 1
            }
269 8
        }
270 8
        $diff = array_diff($guids, $checkedGuids);
271 8
        $this->trigger(static::$eventMultipleBlamesChanged, new MultipleBlameableEvent(['blamesChanged' => !empty($diff)]));
0 ignored issues
show
Bug introduced by
It seems like trigger() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
272 8
        return $checkedGuids;
273
    }
274
275
    /**
276
     * Set the guid array of blames, it may check all guids if valid.
277
     * @param array $guids guid array of blames.
278
     * @param boolean $checkValid determines whether checking the blame is valid.
279
     * @return false|array all guids.
280
     */
281 8
    public function setBlameGuids($guids = [], $checkValid = true)
282
    {
283 8
        if (!is_array($guids) || $this->multiBlamesAttribute === false) {
284
            return null;
285
        }
286 8
        if ($checkValid) {
287 8
            $guids = $this->unsetInvalidBlames($guids);
288 8
        }
289 8
        $multiBlamesAttribute = $this->multiBlamesAttribute;
290 8
        $this->$multiBlamesAttribute = json_encode(array_values($guids));
291 8
        return $guids;
292
    }
293
294
    /**
295
     * Get blame.
296
     * @param string $blameGuid
297
     * @return [multiBlamesClass]
0 ignored issues
show
Documentation introduced by
The doc-type [multiBlamesClass] could not be parsed: Unknown type name "" at position 0. [(view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
298
     */
299 2
    public static function getBlame($blameGuid)
300
    {
301 2
        $self = static::buildNoInitModel();
302 2
        if (empty($self->multiBlamesClass) || !is_string($self->multiBlamesClass) || $self->multiBlamesAttribute === false) {
303
            return null;
304
        }
305 2
        $mbClass = $self->multiBlamesClass;
306 2
        return $mbClass::findOne($blameGuid);
307
    }
308
309
    /**
310
     * 
311
     * @param type $blameGuid
312
     * @param type $user
313
     * @return type
314
     */
315 1
    public static function getOrCreateBlame($blameGuid, $user = null)
316
    {
317 1
        if (is_string($blameGuid)) {
318
            $blameGuid = static::getBlame($blameGuid);
319
            if ($blameGuid !== null) {
320
                return $blameGuid;
321
            }
322
        }
323 1
        if (is_array($blameGuid)) {
324 1
            return static::createBlame($user, $blameGuid);
0 ignored issues
show
Bug introduced by
It seems like $user defined by parameter $user on line 315 can also be of type null; however, vistart\Models\traits\Mu...bleTrait::createBlame() does only seem to accept object<vistart\Models\models\BaseUserModel>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
325
        }
326
        return null;
327
    }
328
329
    /**
330
     * Get all ones to be blamed by `$blame`.
331
     * @param [multiBlamesClass] $blame
0 ignored issues
show
Documentation introduced by
The doc-type [multiBlamesClass] could not be parsed: Unknown type name "" at position 0. [(view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
332
     * @return array
333
     */
334 1
    public function getBlameds($blame)
335
    {
336 1
        $blameds = static::getBlame($blame->guid);
337 1
        if (empty($blameds)) {
338 1
            return null;
339
        }
340 1
        $createdByAttribute = $this->createdByAttribute;
0 ignored issues
show
Bug introduced by
The property createdByAttribute does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
341 1
        return static::find()->where([$createdByAttribute => $this->$createdByAttribute])
342 1
                ->andWhere(['like', $this->multiBlamesAttribute, $blame->guid])->all();
343
    }
344
345
    /**
346
     * Get all the blames of record.
347
     * @return array all blames.
348
     */
349 1
    public function getAllBlames()
350
    {
351 1
        if (empty($this->multiBlamesClass) ||
352 1
            !is_string($this->multiBlamesClass) ||
353 1
            $this->multiBlamesAttribute === false) {
354
            return null;
355
        }
356 1
        $multiBlamesClass = $this->multiBlamesClass;
357 1
        $createdByAttribute = $this->createdByAttribute;
358 1
        return $multiBlamesClass::findAll([$createdByAttribute => $this->$createdByAttribute]);
359
    }
360
361
    /**
362
     * Get all records which without any blames.
363
     * @return array all non-blameds.
364
     */
365 1
    public function getNonBlameds()
366
    {
367 1
        $createdByAttribute = $this->createdByAttribute;
368 1
        return static::find()->where([$createdByAttribute => $this->$createdByAttribute, $this->multiBlamesAttribute => static::getEmptyBlamesJson()])->all();
369
    }
370
371
    /**
372
     * Initialize blames limit.
373
     * @param \yii\base\Event $event
374
     */
375 8
    public function onInitBlamesLimit($event)
376
    {
377 8
        $sender = $event->sender;
378 8
        if (!is_int($sender->blamesLimit) || $sender->blamesLimit < 1 || $sender->blamesLimit > 10) {
379
            $sender->blamesLimit = 10;
380
        }
381 8
    }
382
383
    /**
384
     * Get the json of empty blames array.
385
     * @return string
386
     */
387 1
    public static function getEmptyBlamesJson()
388
    {
389 1
        return json_encode([]);
390
    }
391
}
392