SelfBlameableTrait::getAncestorChain()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 2
Bugs 0 Features 2
Metric Value
c 2
b 0
f 2
dl 0
loc 11
ccs 7
cts 7
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 https://vistart.me/
9
 * @copyright Copyright (c) 2016 - 2017 vistart
10
 * @license https://vistart.me/license/
11
 */
12
13
namespace rhosocial\base\models\traits;
14
15
use Yii;
16
use yii\base\ModelEvent;
17
use yii\base\InvalidConfigException;
18
use yii\base\InvalidParamException;
19
use yii\db\ActiveQuery;
20
use yii\db\IntegrityException;
21
22
/**
23
 * This trait is designed for the model who contains parent.
24
 *
25
 * The BlameableTrait use this trait by default. If you want to use this trait
26
 * into seperate model, please call the `initSelfBlameableEvents()` method in
27
 * `init()` method, like following:
28
 * ```php
29
 * public function init()
30
 * {
31
 *     $this->initSelfBlameableEvents();  // put it before parent call.
32
 *     parent::init();
33
 * }
34
 * ```
35
 *
36
 * The default reference ID attribute is `guid`. You can specify another attribute
37
 * in [[__construct()]] method.
38
 *
39
 * We strongly recommend you to set ancestor limit and children limit, and they
40
 * should not be too large.
41
 * The ancestor limit is preferably no more than 256, and children limit is no
42
 * more than 1024.
43
 * Too large number may seriously slow down the database response speed, especially
44
 * in updating operation.
45
 *
46
 * The data consistency between reference ID attribute and parent attribute can
47
 * only be ensured by my own. And update and delete operations should be placed
48
 * in the transaction to avoid data inconsistencies.
49
 * Even so, we cannot fully guarantee data consistency. Therefore, we provide a
50
 * method [[clearInvalidParent()]] for clearing non-existing parent node.
51
 *
52
 * @property static $parent
53
 * @property-read static[] $ancestors
54
 * @property-read string[] $ancestorChain
55
 * @property-read array $ancestorModels
56
 * @property-read static $commonAncestor
57
 * @property-read static[] $children
58
 * @property-read static[] $oldChildren
59
 * @property array $selfBlameableRules
60
 * @version 1.0
61
 * @author vistart <[email protected]>
62
 */
63
trait SelfBlameableTrait
64
{
65
66
    /**
67
     * @var false|string attribute name of which store the parent's guid.
68
     * If you do not want to use self-blameable features, please set it false.
69
     * Or if you access any features of this trait when this parameter is false,
70
     * exception may be thrown.
71
     */
72
    public $parentAttribute = false;
73
74
    /**
75
     * @var string|array rule name and parameters of parent attribute, as well
76
     * as self referenced ID attribute.
77
     */
78
    public $parentAttributeRule = ['string', 'max' => 16];
79
80
    /**
81
     * @var string self referenced ID attribute.
82
     * If you enable self-blameable features, this parameter should be specified,
83
     * otherwise, exception will be thrown.
84
     */
85
    public $refIdAttribute = 'guid';
86
    public static $parentNone = 0;
87
    public static $parentParent = 1;
88
    public static $parentTypes = [
89
        0 => 'none',
90
        1 => 'parent',
91
    ];
92
    
93
    /**
94
     * @var string The constant determines the null parent.
95
     */
96
    public static $nullParent = '';
97
    public static $onNoAction = 0;
98
    public static $onRestrict = 1;
99
    public static $onCascade = 2;
100
    public static $onSetNull = 3;
101
    public static $onUpdateTypes = [
102
        0 => 'on action',
103
        1 => 'restrict',
104
        2 => 'cascade',
105
        3 => 'set null',
106
    ];
107
108
    /**
109
     * @var integer indicates the on delete type. default to cascade.
110
     */
111
    public $onDeleteType = 2;
112
113
    /**
114
     * @var integer indicates the on update type. default to cascade.
115
     */
116
    public $onUpdateType = 2;
117
118
    /**
119
     * @var boolean indicates whether throw exception or not when restriction occured on updating or deleting operation.
120
     */
121
    public $throwRestrictException = false;
122
    
123
    /**
124
     * @var array store the attribute validation rules.
125
     * If this field is a non-empty array, then it will be given.
126
     */
127
    private $localSelfBlameableRules = [];
128
    public static $eventParentChanged = 'parentChanged';
129
    public static $eventChildAdded = 'childAdded';
130
131
    /**
132
     * @var false|integer Set the limit of ancestor level. False is no limit.
133
     * We strongly recommend you set an unsigned integer which is less than 256.
134
     */
135
    public $ancestorLimit = false;
136
137
    /**
138
     * @var false|integer Set the limit of children (not descendants). False is no limit.
139
     * We strongly recommend you set an unsigned integer which is less than 1024.
140
     */
141
    public $childrenLimit = false;
142
143
    /**
144
     * Get rules associated with self blameable attribute.
145
     * If self-blameable rules has been stored locally, then it will be given,
146
     * or return the parent attribute rule.
147
     * @return array rules.
148
     */
149 199
    public function getSelfBlameableRules()
150
    {
151 199
        if (!is_string($this->parentAttribute)) {
152 199
            return [];
153
        }
154 46
        if (!empty($this->localSelfBlameableRules) && is_array($this->localSelfBlameableRules)) {
155 2
            return $this->localSelfBlameableRules;
156
        }
157 46
        if (is_string($this->parentAttributeRule)) {
158
            $this->parentAttributeRule = [$this->parentAttributeRule];
159
        }
160 46
        $this->localSelfBlameableRules = [
161 46
            array_merge([$this->parentAttribute], $this->parentAttributeRule),
162
        ];
163 46
        return $this->localSelfBlameableRules;
164
    }
165
166
    /**
167
     * Set rules associated with self blameable attribute.
168
     * @param array $rules rules.
169
     */
170 1
    public function setSelfBlameableRules($rules = [])
171
    {
172 1
        $this->localSelfBlameableRules = $rules;
173 1
    }
174
175
    /**
176
     * Check whether this model has reached the ancestor limit.
177
     * If $ancestorLimit is false, it will be regared as no limit(return false).
178
     * If $ancestorLimit is not false and not an unsigned integer, 256 will be taken.
179
     * @return boolean
180
     */
181 61
    public function hasReachedAncestorLimit()
182
    {
183 61
        if ($this->ancestorLimit === false) {
184 61
            return false;
185
        }
186 4
        if (!is_numeric($this->ancestorLimit) || $this->ancestorLimit < 0) {
187 1
            $this->ancestorLimit = 256;
188
        }
189 4
        return count($this->getAncestorChain()) >= $this->ancestorLimit;
190
    }
191
192
    /**
193
     * Check whether this model has reached the children limit.
194
     * If $childrenLimit is false, it will be regarded as no limit(return false).
195
     * If $childrenLimit is not false and not an unsigned integer, 1024 will be taken.
196
     * @return boolean
197
     */
198 8
    public function hasReachedChildrenLimit()
199
    {
200 8
        if ($this->childrenLimit === false) {
201 7
            return false;
202
        }
203 2
        if (!is_numeric($this->childrenLimit) || $this->childrenLimit < 0) {
204 1
            $this->childrenLimit = 1024;
205
        }
206 2
        return ((int) $this->getChildren()->count()) >= $this->childrenLimit;
207
    }
208
209
    /**
210
     * Bear a child.
211
     * The creator of this child is not necessarily the creator of current one.
212
     * For example: Someone commit a comment on another user's comment, these
213
     * two comments are father and son, but do not belong to the same owner.
214
     * Therefore, you need to specify the creator of current model.
215
     * @param array $config
216
     * @return static|null Null if reached the ancestor limit or children limit.
217
     * @throws InvalidConfigException Self reference ID attribute or
218
     * parent attribute not determined.
219
     * @throws InvalidParamException ancestor or children limit reached.
220
     */
221 8
    public function bear($config = [])
222
    {
223 8
        if (!$this->parentAttribute) {
224 1
            throw new InvalidConfigException("Parent Attribute Not Determined.");
225
        }
226 7
        if (!$this->refIdAttribute) {
227
            throw new InvalidConfigException("Self Reference ID Attribute Not Determined.");
228
        }
229 7
        if ($this->hasReachedAncestorLimit()) {
230 1
            throw new InvalidParamException("Reached Ancestor Limit: " . $this->ancestorLimit);
231
        }
232 7
        if ($this->hasReachedChildrenLimit()) {
233 1
            throw new InvalidParamException("Reached Children Limit: ". $this->childrenLimit);
234
        }
235 6
        if (isset($config['class'])) {
236 1
            unset($config['class']);
237
        }
238 6
        $model = 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...
239 6
        if ($this->addChild($model) === false) {
240
            return false;
241
        }
242 6
        return $model;
243
    }
244
245
    /**
246
     * Add a child.
247
     * But if children limit reached, false will be given.
248
     * @param static $child
0 ignored issues
show
introduced by
The type SelfBlameableTrait for parameter $child 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...
249
     * @return boolean Whether adding child succeeded or not.
250
     */
251 6
    public function addChild($child)
252
    {
253 6
        return $this->hasReachedChildrenLimit() ? false : $child->setParent($this);
254
    }
255
256
    /**
257
     * Event triggered before deleting itself.
258
     * Note: DO NOT call it directly unless you know the consequences.
259
     * @param ModelEvent $event
260
     * @return boolean true if parentAttribute not specified.
261
     * @throws IntegrityException throw if $throwRestrictException is true when $onDeleteType is on restrict.
262
     */
263 98
    public function onDeleteChildren($event)
264
    {
265 98
        $sender = $event->sender;
266
        /* @var $sender static */
267 98
        if (empty($sender->parentAttribute) || !is_string($sender->parentAttribute)) {
268 98
            return true;
269
        }
270 30
        switch ($sender->onDeleteType) {
271 30
            case static::$onRestrict:
272 1
                $event->isValid = $sender->children === null;
273 1
                if ($sender->throwRestrictException) {
274 1
                    throw new IntegrityException('Delete restricted.');
275
                }
276 1
                break;
277 30
            case static::$onCascade:
278 30
                $event->isValid = $sender->deleteChildren();
0 ignored issues
show
Documentation Bug introduced by
It seems like $sender->deleteChildren() can also be of type object<yii\db\IntegrityException>. However, the property $isValid is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
279 30
                break;
280 2
            case static::$onSetNull:
281 1
                $event->isValid = $sender->updateChildren(null);
0 ignored issues
show
Documentation Bug introduced by
It seems like $sender->updateChildren(null) can also be of type object<yii\db\IntegrityException>. However, the property $isValid is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
282 1
                break;
283 1
            case static::$onNoAction:
284
            default:
285 1
                $event->isValid = true;
286 1
                break;
287
        }
288 30
    }
289
290
    /**
291
     * Event triggered before updating itself.
292
     * Note: DO NOT call it directly unless you know the consequences.
293
     * @param ModelEvent $event
294
     * @return boolean true if parentAttribute not specified.
295
     * @throws IntegrityException throw if $throwRestrictException is true when $onUpdateType is on restrict.
296
     */
297 4
    public function onUpdateChildren($event)
298
    {
299 4
        $sender = $event->sender;
300
        /* @var $sender static */
301 4
        if (empty($sender->parentAttribute) || !is_string($sender->parentAttribute)) {
302
            return true;
303
        }
304 4
        switch ($sender->onUpdateType) {
305 4
            case static::$onRestrict:
306 1
                $event->isValid = $sender->getOldChildren() === null;
307 1
                if ($sender->throwRestrictException) {
308 1
                    throw new IntegrityException('Update restricted.');
309
                }
310 1
                break;
311 3
            case static::$onCascade:
312 1
                $event->isValid = $sender->updateChildren();
0 ignored issues
show
Documentation Bug introduced by
It seems like $sender->updateChildren() can also be of type object<yii\db\IntegrityException>. However, the property $isValid is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
313 1
                break;
314 2
            case static::$onSetNull:
315 1
                $event->isValid = $sender->updateChildren(null);
0 ignored issues
show
Documentation Bug introduced by
It seems like $sender->updateChildren(null) can also be of type object<yii\db\IntegrityException>. However, the property $isValid is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
316 1
                break;
317 1
            case static::$onNoAction:
318
            default:
319 1
                $event->isValid = true;
320 1
                break;
321
        }
322 4
    }
323
324
    /**
325
     * Get parent query.
326
     * Or get parent instance if access by magic property.
327
     * @return ActiveQuery
328
     */
329 61
    public function getParent()
330
    {
331 61
        return $this->hasOne(static::class, [$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...
332
    }
333
    
334
    /**
335
     * Get parent ID.
336
     * @return string|null null if parent attribute isn't enabled.
337
     */
338 4
    public function getParentId()
339
    {
340 4
        return (is_string($this->parentAttribute) && !empty($this->parentAttribute)) ?
341 4
        $this->{$this->parentAttribute} : null;
342
    }
343
    
344
    /**
345
     * Set parent ID.
346
     * @param string $parentId
347
     * @return string|null null if parent attribute isn't enabled.
348
     */
349 4
    public function setParentId($parentId)
350
    {
351 4
        return (is_string($this->parentAttribute) && !empty($this->parentAttribute)) ?
352 4
        $this->{$this->parentAttribute} = $parentId : null;
353
    }
354
    
355
    /**
356
     * Get reference ID.
357
     * @return string
358
     */
359 61
    public function getRefId()
360
    {
361 61
        if ($this->refIdAttribute == $this->guidAttribute) {
0 ignored issues
show
Bug introduced by
The property guidAttribute 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...
362 61
            return $this->getGUID();
0 ignored issues
show
Bug introduced by
It seems like getGUID() 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...
363
        }
364
        if ($this->refIdAttribute == $this->idAttribute) {
0 ignored issues
show
Bug introduced by
The property idAttribute does not seem to exist. Did you mean refIdAttribute?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
365
            return $this->getID();
0 ignored issues
show
Bug introduced by
It seems like getID() 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...
366
        }
367
        return $this->{$this->refIdAttribute};
368
    }
369
    
370
    /**
371
     * Set reference ID.
372
     * @param string $refId
373
     * @return string
374
     */
375
    public function setRefId($refId)
376
    {
377
        if ($this->refIdAttribute == $this->guidAttribute) {
378
            return $this->setGUID($refId);
0 ignored issues
show
Bug introduced by
It seems like setGUID() 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...
379
        }
380
        if ($this->refIdAttribute == $this->idAttribute) {
0 ignored issues
show
Bug introduced by
The property idAttribute does not seem to exist. Did you mean refIdAttribute?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
381
            return $this->setID($refId);
0 ignored issues
show
Bug introduced by
It seems like setID() 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...
382
        }
383
        return $this->{$this->refIdAttribute} = $refId;
384
    }
385
386
    /**
387
     * Set parent.
388
     * Don't forget save model after setting it.
389
     * @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...
390
     * @return false|string False if restriction reached. Otherwise parent's GUID given.
391
     */
392 61
    public function setParent($parent)
393
    {
394 61
        if (empty($parent) || $this->getRefId() == $parent->getRefId() ||
395 61
            $parent->hasAncestor($this) || $this->hasReachedAncestorLimit()) {
396 1
            return false;
397
        }
398 61
        unset($this->parent);
399 61
        unset($parent->children);
400 61
        $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...
401 61
        $parent->trigger(static::$eventChildAdded);
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...
402 61
        return $this->{$this->parentAttribute} = $parent->getRefId();
403
    }
404
    
405
    /**
406
     * Set null parent.
407
     * This method would unset the lazy loading records before setting it.
408
     * Don't forget save model after setting it.
409
     */
410 4
    public function setNullParent()
411
    {
412 4
        if ($this->hasParent()) {
413 2
            unset($this->parent->children);
414
        }
415 4
        unset($this->parent);
416 4
        $this->setParentId(static::$nullParent);
417 4
    }
418
419
    /**
420
     * Check whether this model has parent.
421
     * @return boolean
422
     */
423 61
    public function hasParent()
424
    {
425 61
        return $this->parent !== null;
426
    }
427
428
    /**
429
     * Check whether if $ancestor is the ancestor of myself.
430
     * Note: Itself will not be regarded as the its ancestor.
431
     * @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...
432
     * @return boolean
433
     */
434 61
    public function hasAncestor($ancestor)
435
    {
436 61
        if (!$this->hasParent()) {
437 61
            return false;
438
        }
439 9
        if ($this->parent->getRefId() == $ancestor->getRefId()) {
440 2
            return true;
441
        }
442 9
        return $this->parent->hasAncestor($ancestor);
443
    }
444
445
    /**
446
     * Get ancestor chain. (Ancestors' GUID Only!)
447
     * If this model has ancestor, the return array consists all the ancestor in order.
448
     * The first element is parent, and the last element is root, otherwise return empty array.
449
     * If you want to get ancestor model, you can simplify instance a query and specify the
450
     * condition with the return value. But it will not return models under the order of ancestor chain.
451
     * @param string[] $ancestor
452
     * @return string[]
453
     */
454 9
    public function getAncestorChain($ancestor = [])
455
    {
456 9
        if (!is_string($this->parentAttribute) || empty($this->parentAttribute)) {
457 1
            return [];
458
        }
459 8
        if (!$this->hasParent()) {
460 8
            return $ancestor;
461
        }
462 8
        $ancestor[] = $this->parent->getRefId();
463 8
        return $this->parent->getAncestorChain($ancestor);
464
    }
465
466
    /**
467
     * Get ancestors with specified ancestor chain.
468
     * @param string[] $ancestor Ancestor chain.
469
     * @return static[]|null
470
     */
471 2
    public static function getAncestorModels($ancestor)
472
    {
473 2
        if (empty($ancestor) || !is_array($ancestor)) {
474 1
            return [];
475
        }
476 2
        $models = [];
477 2
        foreach ($ancestor as $self) {
478 2
            $models[] = static::findOne($self);
479
        }
480 2
        return $models;
481
    }
482
483
    /**
484
     * Get ancestors.
485
     * @return static[]
486
     */
487 1
    public function getAncestors()
488
    {
489 1
        return static::getAncestorModels($this->getAncestorChain());
490
    }
491
492
    /**
493
     * Check whether if this model has common ancestor with $model.
494
     * @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...
495
     * @return boolean
496
     */
497 2
    public function hasCommonAncestor($model)
498
    {
499 2
        return $this->getCommonAncestor($model) !== null;
500
    }
501
502
    /**
503
     * Get common ancestor. If there isn't common ancestor, null will be given.
504
     * @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...
505
     * @return static
506
     */
507 3
    public function getCommonAncestor($model)
508
    {
509 3
        if (empty($this->parentAttribute) || !is_string($this->parentAttribute) ||
510 2
            empty($model) || !$model->hasParent()) {
511 1
            return null;
512
        }
513 2
        $ancestors = $this->getAncestorChain();
514 2
        if (in_array($model->parent->getRefId(), $ancestors)) {
515 2
            return $model->parent;
516
        }
517 1
        return $this->getCommonAncestor($model->parent);
518
    }
519
520
    /**
521
     * Get children query.
522
     * Or get children instances if access magic property.
523
     * @return ActiveQuery
524
     */
525 61
    public function getChildren()
526
    {
527 61
        return $this->hasMany(static::class, [$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...
528
    }
529
530
    /**
531
     * Get children which parent attribute point to old guid.
532
     * @return static[]
533
     */
534 4
    public function getOldChildren()
535
    {
536 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...
537
    }
538
539
    /**
540
     * Update all children, not grandchildren (descendants).
541
     * If onUpdateType is on cascade, the children will be updated automatically.
542
     * @param mixed $value set guid if false, set empty string if empty() return
543
     * true, otherwise set it to $parentAttribute.
544
     * @return IntegrityException|boolean true if all update operations
545
     * succeeded to execute, or false if anyone of them failed. If not production
546
     * environment or enable debug mode, it will return exception.
547
     * @throws IntegrityException throw if anyone update failed.
548
     * The exception message only contains the first error.
549
     */
550 3
    public function updateChildren($value = false)
551
    {
552 3
        $children = $this->getOldChildren();
553 3
        if (empty($children)) {
554 1
            return true;
555
        }
556 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...
557
        try {
558 3
            foreach ($children as $child) {
559
                /* @var $child static */
560 3
                if ($value === false) {
561 1
                    $child->setParent($this);
562
                } elseif (empty($value)) {
563 2
                    $child->setNullParent();
564
                } else {
565
                    $child->setParentId($value);
566
                }
567 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...
568
                    throw new IntegrityException('Update failed:', $child->getErrors());
0 ignored issues
show
Bug introduced by
It seems like getErrors() 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...
569
                }
570
            }
571 3
            $transaction->commit();
572
        } catch (IntegrityException $ex) {
573
            $transaction->rollBack();
574
            if (YII_DEBUG || YII_ENV !== YII_ENV_PROD) {
575
                Yii::error($ex->getMessage(), __METHOD__);
576
                return $ex;
577
            }
578
            Yii::warning($ex->getMessage(), __METHOD__);
579
            return false;
580
        }
581 3
        return true;
582
    }
583
584
    /**
585
     * Delete all children, not grandchildren (descendants).
586
     * If onDeleteType is `on cascade`, the children will be deleted automatically.
587
     * If onDeleteType is `on restrict` and contains children, the deletion will
588
     * be restricted.
589
     * @return IntegrityException|boolean true if all delete operations
590
     * succeeded to execute, or false if anyone of them failed. If not production
591
     * environment or enable debug mode, it will return exception.
592
     * @throws IntegrityException throw if anyone delete failed.
593
     * The exception message only contains the first error.
594
     */
595 30
    public function deleteChildren()
596
    {
597 30
        $children = $this->children;
598 30
        if (empty($children)) {
599 30
            return true;
600
        }
601 30
        $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...
602
        try {
603 30
            foreach ($children as $child) {
604
                /* @var $child static */
605 30
                if (!$child->delete()) {
0 ignored issues
show
Bug introduced by
The method delete() does not exist on rhosocial\base\models\traits\SelfBlameableTrait. Did you maybe mean onDeleteChildren()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
606 9
                    throw new IntegrityException('Delete failed:', $child->getErrors());
0 ignored issues
show
Bug introduced by
It seems like getErrors() 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...
607
                }
608
            }
609 30
            $transaction->commit();
610 9
        } catch (IntegrityException $ex) {
611 9
            $transaction->rollBack();
612 9
            if (YII_DEBUG || YII_ENV !== YII_ENV_PROD) {
613 9
                Yii::error($ex->getMessage(), __METHOD__);
614 9
                return $ex;
615
            }
616
            Yii::warning($ex->getMessage(), __METHOD__);
617
            return false;
618
        }
619 30
        return true;
620
    }
621
622
    /**
623
     * Update children's parent attribute.
624
     * Event triggered before updating.
625
     * @param ModelEvent $event
626
     * @return boolean
627
     */
628 62
    public function onParentRefIdChanged($event)
629
    {
630 62
        $sender = $event->sender;
631
        /* @var $sender static */
632 62
        if ($sender->isAttributeChanged($sender->refIdAttribute)) {
0 ignored issues
show
Bug introduced by
It seems like isAttributeChanged() 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...
633 4
            return $sender->onUpdateChildren($event);
634
        }
635 61
    }
636
637
    /**
638
     * Attach events associated with self blameable attribute.
639
     */
640 209
    protected function initSelfBlameableEvents()
641
    {
642 209
        $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...
643 209
        $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...
644 209
    }
645
    
646
    /**
647
     * Clear invalid parent.
648
     * The invalid state depends on which if parent id exists but it's corresponding
649
     * parent cannot be found.
650
     * @return boolean True if parent attribute is set null, False if parent valid.
651
     */
652 4
    public function clearInvalidParent()
653
    {
654 4
        if ($this->getParentId() !== static::$nullParent && !$this->hasParent()) {
655 2
            $this->setNullParent();
656 2
            return true;
657
        }
658 2
        return false;
659
    }
660
}
661