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); |
|
|
|
|
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]); |
|
|
|
|
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
/** |
251
|
|
|
* Set parent. |
252
|
|
|
* Don't forget save model after setting it. |
253
|
|
|
* @param static $parent |
|
|
|
|
254
|
|
|
* @return false|string |
255
|
|
|
*/ |
256
|
1 |
|
public function setParent($parent) |
257
|
|
|
{ |
258
|
1 |
|
if (empty($parent) || $this->guid == $parent->guid || $parent->hasAncestor($this)) { |
|
|
|
|
259
|
|
|
return false; |
260
|
|
|
} |
261
|
1 |
|
unset($this->parent); |
262
|
1 |
|
$this->trigger(static::$eventParentChanged); |
|
|
|
|
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 |
|
|
|
|
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 |
|
|
|
|
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 |
|
|
|
|
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'); |
|
|
|
|
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(); |
|
|
|
|
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(); |
|
|
|
|
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()) { |
|
|
|
|
414
|
|
|
throw new \yii\db\IntegrityException('Update failed:' . $child->errors); |
|
|
|
|
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(); |
|
|
|
|
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']); |
|
|
|
|
483
|
38 |
|
$this->on(static::EVENT_BEFORE_DELETE, [$this, 'onDeleteChildren']); |
|
|
|
|
484
|
38 |
|
} |
485
|
|
|
} |
486
|
|
|
|
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.