Completed
Push — master ( 47b819...9b5529 )
by vistart
07:10 queued 44s
created

SelfBlameableTrait::getAncestorModels()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 11
ccs 8
cts 8
cp 1
rs 9.2
cc 4
eloc 7
nc 3
nop 1
crap 4
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 yii\base\ModelEvent;
16
use yii\db\ActiveQuery;
17
use yii\db\IntegrityException;
18
19
/**
20
 * This trait is designed for thw model who contains parent.
21
 *
22
 * @property static $parent
23
 * @property-read static[] $ancestors
24
 * @property-read string[] $ancestorChain
25
 * @property-read static $commonAncestor
26
 * @property-read static[] $children
27
 * @property-read static[] $oldChildren
28
 * @property array $selfBlameableRules
29
 * @version 2.0
30
 * @author vistart <[email protected]>
31
 */
32
trait SelfBlameableTrait
33
{
34
35
    /**
36
     * @var false|string attribute name of which store the parent's guid.
37
     */
38
    public $parentAttribute = false;
39
40
    /**
41
     * @var string|array rule name and parameters of parent attribute, as well
42
     * as self referenced ID attribute.
43
     */
44
    public $parentAttributeRule = ['string', 'max' => 36];
45
46
    /**
47
     * @var string self referenced ID attribute.
48
     */
49
    public $refIdAttribute = 'guid';
50
    public static $parentNone = 0;
51
    public static $parentParent = 1;
52
    public static $parentTypes = [
53
        0 => 'none',
54
        1 => 'parent',
55
    ];
56
    public static $onNoAction = 0;
57
    public static $onRestrict = 1;
58
    public static $onCascade = 2;
59
    public static $onSetNull = 3;
60
    public static $onUpdateTypes = [
61
        0 => 'on action',
62
        1 => 'restrict',
63
        2 => 'cascade',
64
        3 => 'set null',
65
    ];
66
67
    /**
68
     * @var integer indicates the on delete type. default to cascade.
69
     */
70
    public $onDeleteType = 2;
71
72
    /**
73
     * @var integer indicates the on update type. default to cascade.
74
     */
75
    public $onUpdateType = 2;
76
77
    /**
78
     * @var boolean indicates whether throw exception or not when restriction occured on updating or deleting operation.
79
     */
80
    public $throwRestrictException = false;
81
    private $localSelfBlameableRules = [];
82
    public static $eventParentChanged = 'parentChanged';
83
84
    /**
85
     * @var false|integer Set the limit of ancestor level. False is no limit.
86
     */
87
    public $ancestorLimit = false;
88
89
    /**
90
     * @var false|integer Set the limit of children. False is no limit.
91
     */
92
    public $childrenLimit = false;
93
94
    /**
95
     * Get rules associated with self blameable attribute.
96
     * @return array rules.
97
     */
98 13
    public function getSelfBlameableRules()
99
    {
100 13
        if (!is_string($this->parentAttribute)) {
101 12
            return [];
102
        }
103 1
        if (!empty($this->localSelfBlameableRules) && is_array($this->localSelfBlameableRules)) {
104 1
            return $this->localSelfBlameableRules;
105
        }
106 1
        if (is_string($this->parentAttributeRule)) {
107
            $this->parentAttributeRule = [$this->parentAttributeRule];
108
        }
109 1
        $this->localSelfBlameableRules = [
110 1
            array_merge([$this->parentAttribute], $this->parentAttributeRule),
111
        ];
112 1
        return $this->localSelfBlameableRules;
113
    }
114
115
    /**
116
     * Set rules associated with self blameable attribute.
117
     * @param array $rules rules.
118
     */
119 1
    public function setSelfBlameableRules($rules = [])
120
    {
121 1
        $this->localSelfBlameableRules = $rules;
122 1
    }
123
124
    /**
125
     * Check whether this model has reached the ancestor limit.
126
     * If $ancestorLimit is false, it will be regared as no limit(return false).
127
     * If $ancestorLimit is not false and not a number, 256 will be taken.
128
     * @return boolean
129
     */
130 10
    public function hasReachedAncestorLimit()
131
    {
132 10
        if ($this->ancestorLimit === false) {
133 10
            return false;
134
        }
135
        if (!is_numeric($this->ancestorLimit)) {
136
            $this->ancestorLimit = 256;
137
        }
138
        return count($this->getAncestorChain()) >= $this->ancestorLimit;
139
    }
140
141
    /**
142
     * Check whether this model has reached the children limit.
143
     * If $childrenLimit is false, it will be regarded as no limit(return false).
144
     * If $childrenLimist is not false and not a number, 256 will be taken.
145
     * @return boolean
146
     */
147 10
    public function hasReachedChildrenLimit()
148
    {
149 10
        if ($this->childrenLimit === false) {
150 10
            return false;
151
        }
152
        if (!is_numeric($this->childrenLimit)) {
153
            $this->childrenLimit = 256;
154
        }
155
        return ((int) $this->getChildren()->count()) >= $this->childrenLimit;
156
    }
157
158
    /**
159
     * Bear a child.
160
     * @param array $config
161
     * @return static|null Null if reached the ancestor limit or children limit.
162
     */
163 10
    public function bear($config = [])
164
    {
165 10
        if ($this->hasReachedAncestorLimit() || $this->hasReachedChildrenLimit()) {
166
            return null;
167
        }
168 10
        if (isset($config['class'])) {
169 10
            unset($config['class']);
170 10
        }
171 10
        $refIdAttribute = $this->refIdAttribute;
172 10
        $config[$this->parentAttribute] = $this->$refIdAttribute;
173 10
        return new static($config);
0 ignored issues
show
Unused Code introduced by
The call to SelfBlameableTrait::__construct() has too many arguments starting with $config.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
174
    }
175
176
    /**
177
     * Event triggered before deleting itself.
178
     * @param ModelEvent $event
179
     * @return boolean true if parentAttribute not specified.
180
     * @throws IntegrityException throw if $throwRestrictException is true when $onDeleteType is on restrict.
181
     */
182 19
    public function onDeleteChildren($event)
183
    {
184 19
        $sender = $event->sender;
185 19
        if (!is_string($sender->parentAttribute)) {
186 15
            return true;
187
        }
188 4
        switch ($sender->onDeleteType) {
189 4
            case static::$onRestrict:
190 1
                $event->isValid = $sender->children === null;
191 1
                if ($this->throwRestrictException) {
192 1
                    throw new \yii\db\IntegrityException('Delete restricted.');
193
                }
194 1
                break;
195 3
            case static::$onCascade:
196 1
                $event->isValid = $sender->deleteChildren();
197 1
                break;
198 2
            case static::$onSetNull:
199 1
                $event->isValid = $sender->updateChildren(null);
200 1
                break;
201 1
            case static::$onNoAction:
202 1
            default:
203 1
                $event->isValid = true;
204 1
                break;
205 4
        }
206 4
    }
207
208
    /**
209
     * Event triggered before updating itself.
210
     * @param ModelEvent $event
211
     * @return boolean true if parentAttribute not specified.
212
     * @throws IntegrityException throw if $throwRestrictException is true when $onUpdateType is on restrict.
213
     */
214 4
    public function onUpdateChildren($event)
215
    {
216 4
        $sender = $event->sender;
217 4
        if (!is_string($sender->parentAttribute)) {
218
            return true;
219
        }
220 4
        switch ($sender->onUpdateType) {
221 4
            case static::$onRestrict:
222 1
                $event->isValid = $sender->getOldChildren() === null;
223 1
                if ($this->throwRestrictException) {
224 1
                    throw new \yii\db\IntegrityException('Update restricted.');
225
                }
226
                break;
227 3
            case static::$onCascade:
228 1
                $event->isValid = $sender->updateChildren();
229 1
                break;
230 2
            case static::$onSetNull:
231 1
                $event->isValid = $sender->updateChildren(null);
232 1
                break;
233 1
            case static::$onNoAction:
234 1
            default:
235 1
                $event->isValid = true;
236 1
                break;
237 3
        }
238 3
    }
239
240
    /**
241
     * Get parent query.
242
     * Or get parent instance if access by magic property.
243
     * @return ActiveQuery
244
     */
245 3
    public function getParent()
246
    {
247 3
        return $this->hasOne(static::className(), [$this->refIdAttribute => $this->parentAttribute]);
0 ignored issues
show
Bug introduced by
It seems like hasOne() 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...
248
    }
249
250
    /**
251
     * Set parent.
252
     * Don't forget save model after setting it.
253
     * @param static $parent
0 ignored issues
show
introduced by
The type SelfBlameableTrait for parameter $parent is a trait, and thus cannot be used for type-hinting in PHP. Maybe consider adding an interface and use that for type-hinting?
Loading history...
254
     * @return false|string
255
     */
256 1
    public function setParent($parent)
257
    {
258 1
        if (empty($parent) || $this->guid == $parent->guid || $parent->hasAncestor($this)) {
0 ignored issues
show
Bug introduced by
The property guid 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...
259
            return false;
260
        }
261 1
        unset($this->parent);
262 1
        $this->trigger(static::$eventParentChanged);
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...
263 1
        return $this->{$this->parentAttribute} = $parent->guid;
264
    }
265
266
    /**
267
     * Check whether this model has parent.
268
     * @return boolean
269
     */
270 1
    public function hasParent()
271
    {
272 1
        return $this->parent !== null;
273
    }
274
275
    /**
276
     * Check whether if $ancestor is the ancestor of myself.
277
     * Note, Itself will not be regarded as the its ancestor.
278
     * @param static $ancestor
0 ignored issues
show
introduced by
The type SelfBlameableTrait for parameter $ancestor is a trait, and thus cannot be used for type-hinting in PHP. Maybe consider adding an interface and use that for type-hinting?
Loading history...
279
     * @return boolean
280
     */
281 1
    public function hasAncestor($ancestor)
282
    {
283 1
        if (!$this->hasParent()) {
284 1
            return false;
285
        }
286 1
        if ($this->parent->guid == $ancestor->guid) {
287
            return true;
288
        }
289 1
        return $this->parent->hasAncestor($ancestor);
290
    }
291
292
    /**
293
     * Get ancestor chain. (Ancestor GUID Only!)
294
     * If this model has ancestor, the return array consists all the ancestor in order.
295
     * The first element is parent, and the last element is root, otherwise return empty array.
296
     * If you want to get ancestor model, you can simplify instance a query and specify the 
297
     * condition with the return value. But it will not return models under the order of ancestor chain.
298
     * @param string[] $ancestor
299
     * @return string[]
300
     */
301 1
    public function getAncestorChain($ancestor = [])
302
    {
303 1
        if (!is_string($this->parentAttribute)) {
304
            return [];
305
        }
306 1
        if (!$this->hasParent()) {
307 1
            return $ancestor;
308
        }
309 1
        $ancestor[] = $this->parent->guid;
310 1
        return $this->parent->getAncestorChain($ancestor);
311
    }
312
313
    /**
314
     * Get ancestors with specified ancestor chain.
315
     * @param string[] $ancestor Ancestor chain.
316
     * @return static[]|null
317
     */
318 1
    public static function getAncestorModels($ancestor)
319
    {
320 1
        if (empty($ancestor) || !is_array($ancestor)) {
321 1
            return null;
322
        }
323 1
        $models = [];
324 1
        foreach ($ancestor as $self) {
325 1
            $models[] = static::findOne($self);
326 1
        }
327 1
        return $models;
328
    }
329
330
    /**
331
     * Get ancestors.
332
     * @return static[]|null
333
     */
334 1
    public function getAncestors()
335
    {
336 1
        return is_string($this->parentAttribute) ? $this->getAncestorModels($this->getAncestorChain()) : null;
337
    }
338
339
    /**
340
     * Check whether if this model has common ancestor with $model.
341
     * @param static $model
0 ignored issues
show
introduced by
The type SelfBlameableTrait for parameter $model is a trait, and thus cannot be used for type-hinting in PHP. Maybe consider adding an interface and use that for type-hinting?
Loading history...
342
     * @return boolean
343
     */
344 1
    public function hasCommonAncestor($model)
345
    {
346 1
        return is_string($this->parentAttribute) ? $this->getCommonAncestor($model) !== null : false;
347
    }
348
349
    /**
350
     * Get common ancestor. If there isn't common ancestor, null will be given.
351
     * @param static $model
0 ignored issues
show
introduced by
The type SelfBlameableTrait for parameter $model is a trait, and thus cannot be used for type-hinting in PHP. Maybe consider adding an interface and use that for type-hinting?
Loading history...
352
     * @return static
353
     */
354 1
    public function getCommonAncestor($model)
355
    {
356 1
        if (!is_string($this->parentAttribute) || empty($model) || !$model->hasParent()) {
357 1
            return null;
358
        }
359 1
        $ancestor = $this->getAncestorChain();
360 1
        if (in_array($model->parent->guid, $ancestor)) {
361 1
            return $model->parent;
362
        }
363 1
        return $this->getCommonAncestor($model->parent);
364
    }
365
366
    /**
367
     * Get children query.
368
     * Or get children instances if access magic property.
369
     * @return ActiveQuery
370
     */
371 3
    public function getChildren()
372
    {
373 3
        return $this->hasMany(static::className(), [$this->parentAttribute => $this->refIdAttribute])->inverseOf('parent');
0 ignored issues
show
Bug introduced by
It seems like hasMany() 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...
374
    }
375
376
    /**
377
     * Get children which parent attribute point to the my old guid.
378
     * @return static[]
379
     */
380 4
    public function getOldChildren()
381
    {
382 4
        return static::find()->where([$this->parentAttribute => $this->getOldAttribute($this->refIdAttribute)])->all();
0 ignored issues
show
Bug introduced by
It seems like getOldAttribute() 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...
383
    }
384
385
    /**
386
     * Update all children, not grandchildren.
387
     * If onUpdateType is on cascade, the children will be updated automatically.
388
     * @param mixed $value set guid if false, set empty string if empty() return
389
     * true, otherwise set it to $parentAttribute.
390
     * @return IntegrityException|boolean true if all update operations
391
     * succeeded to execute, or false if anyone of them failed. If not production
392
     * environment or enable debug mode, it will return exception.
393
     * @throws IntegrityException throw if anyone update failed.
394
     */
395 3
    public function updateChildren($value = false)
396
    {
397 3
        $children = $this->getOldChildren();
398 3
        if (empty($children)) {
399
            return true;
400
        }
401 3
        $parentAttribute = $this->parentAttribute;
402 3
        $transaction = $this->getDb()->beginTransaction();
0 ignored issues
show
Bug introduced by
It seems like getDb() 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...
403
        try {
404 3
            foreach ($children as $child) {
405 3
                if ($value === false) {
406 1
                    $refIdAttribute = $this->refIdAttribute;
407 1
                    $child->$parentAttribute = $this->$refIdAttribute;
408 3
                } elseif (empty($value)) {
409 2
                    $child->$parentAttribute = '';
410 2
                } else {
411
                    $child->$parentAttribute = $value;
412
                }
413 3
                if (!$child->save()) {
0 ignored issues
show
Bug introduced by
It seems like save() 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...
414
                    throw new \yii\db\IntegrityException('Update failed:' . $child->errors);
0 ignored issues
show
Bug introduced by
The property errors 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...
415
                }
416 3
            }
417 3
            $transaction->commit();
418 3
        } catch (\yii\db\IntegrityException $ex) {
419
            $transaction->rollBack();
420
            if (YII_DEBUG || YII_ENV !== YII_ENV_PROD) {
421
                Yii::error($ex->errorInfo, static::className() . '\update');
422
                return $ex;
423
            }
424
            Yii::warning($ex->errorInfo, static::className() . '\update');
425
            return false;
426
        }
427 3
        return true;
428
    }
429
430
    /**
431
     * Delete all children, not grandchildren.
432
     * If onDeleteType is on cascade, the children will be deleted automatically.
433
     * If onDeleteType is on restrict and contains children, the deletion will
434
     * be restricted.
435
     * @return IntegrityException|boolean true if all delete operations
436
     * succeeded to execute, or false if anyone of them failed. If not production
437
     * environment or enable debug mode, it will return exception.
438
     * @throws IntegrityException throw if anyone delete failed.
439
     */
440 1
    public function deleteChildren()
441
    {
442 1
        $children = $this->children;
443 1
        if (empty($children)) {
444 1
            return true;
445
        }
446 1
        $transaction = $this->getDb()->beginTransaction();
0 ignored issues
show
Bug introduced by
It seems like getDb() 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...
447
        try {
448 1
            foreach ($children as $child) {
449 1
                if (!$child->delete()) {
450
                    throw new \yii\db\IntegrityException('Delete failed:' . $child->errors);
451
                }
452 1
            }
453 1
            $transaction->commit();
454 1
        } catch (\yii\db\IntegrityException $ex) {
455
            $transaction->rollBack();
456
            if (YII_DEBUG || YII_ENV !== YII_ENV_PROD) {
457
                Yii::error($ex->errorInfo, static::className() . '\delete');
458
                return $ex;
459
            }
460
            Yii::warning($ex->errorInfo, static::className() . '\delete');
461
            return false;
462
        }
463 1
        return true;
464
    }
465
466
    /**
467
     * Update children's parent attribute.
468
     * Event triggered before updating.
469
     * @param ModelEvent $event
470
     * @return boolean
471
     */
472 16
    public function onParentRefIdChanged($event)
473
    {
474 16
        $sender = $event->sender;
475 16
        if ($sender->isAttributeChanged($sender->refIdAttribute)) {
476 4
            return $sender->onUpdateChildren($event);
477
        }
478 14
    }
479
480 38
    protected function initSelfBlameableEvents()
481
    {
482 38
        $this->on(static::EVENT_BEFORE_UPDATE, [$this, 'onParentRefIdChanged']);
0 ignored issues
show
Bug introduced by
It seems like on() 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...
483 38
        $this->on(static::EVENT_BEFORE_DELETE, [$this, 'onDeleteChildren']);
0 ignored issues
show
Bug introduced by
It seems like on() 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...
484 38
    }
485
}
486