1 | <?php |
||
2 | /** |
||
3 | * @link https://github.com/paulzi/yii2-adjacency-list |
||
4 | * @copyright Copyright (c) 2015 PaulZi <[email protected]> |
||
5 | * @license MIT (https://github.com/paulzi/yii2-adjacency-list/blob/master/LICENSE) |
||
6 | */ |
||
7 | |||
8 | namespace paulzi\adjacencyList; |
||
9 | |||
10 | use Yii; |
||
11 | use yii\base\Behavior; |
||
12 | use yii\base\Exception; |
||
13 | use yii\base\NotSupportedException; |
||
14 | use yii\db\ActiveRecord; |
||
15 | use yii\db\Query; |
||
16 | use paulzi\sortable\SortableBehavior; |
||
17 | |||
18 | |||
19 | /** |
||
20 | * Adjacency List Behavior for Yii2 |
||
21 | * @author PaulZi <[email protected]> |
||
22 | * |
||
23 | * @property ActiveRecord $owner |
||
24 | */ |
||
25 | class AdjacencyListBehavior extends Behavior |
||
26 | { |
||
27 | const OPERATION_MAKE_ROOT = 1; |
||
28 | const OPERATION_PREPEND_TO = 2; |
||
29 | const OPERATION_APPEND_TO = 3; |
||
30 | const OPERATION_INSERT_BEFORE = 4; |
||
31 | const OPERATION_INSERT_AFTER = 5; |
||
32 | const OPERATION_DELETE_ALL = 6; |
||
33 | |||
34 | /** |
||
35 | * @var string |
||
36 | */ |
||
37 | public $parentAttribute = 'parent_id'; |
||
38 | |||
39 | /** |
||
40 | * @var array|false SortableBehavior config |
||
41 | */ |
||
42 | public $sortable = []; |
||
43 | |||
44 | /** |
||
45 | * @var bool |
||
46 | */ |
||
47 | public $checkLoop = false; |
||
48 | |||
49 | /** |
||
50 | * @var int |
||
51 | */ |
||
52 | public $parentsJoinLevels = 3; |
||
53 | |||
54 | /** |
||
55 | * @var int |
||
56 | */ |
||
57 | public $childrenJoinLevels = 3; |
||
58 | |||
59 | /** |
||
60 | * @var bool |
||
61 | */ |
||
62 | protected $operation; |
||
63 | |||
64 | /** |
||
65 | * @var ActiveRecord|self|null |
||
66 | */ |
||
67 | protected $node; |
||
68 | |||
69 | /** |
||
70 | * @var SortableBehavior |
||
71 | */ |
||
72 | protected $behavior; |
||
73 | |||
74 | /** |
||
75 | * @var ActiveRecord[] |
||
76 | */ |
||
77 | private $_parentsOrdered; |
||
78 | |||
79 | /** |
||
80 | * @var array |
||
81 | */ |
||
82 | private $_parentsIds; |
||
83 | |||
84 | /** |
||
85 | * @var array |
||
86 | */ |
||
87 | private $_childrenIds; |
||
88 | |||
89 | |||
90 | /** |
||
91 | * @inheritdoc |
||
92 | */ |
||
93 | 225 | public function events() |
|
94 | { |
||
95 | return [ |
||
96 | 225 | ActiveRecord::EVENT_BEFORE_INSERT => 'beforeSave', |
|
97 | ActiveRecord::EVENT_AFTER_INSERT => 'afterSave', |
||
98 | ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeSave', |
||
99 | ActiveRecord::EVENT_AFTER_UPDATE => 'afterSave', |
||
100 | ActiveRecord::EVENT_BEFORE_DELETE => 'beforeDelete', |
||
101 | ActiveRecord::EVENT_AFTER_DELETE => 'afterDelete', |
||
102 | ]; |
||
103 | } |
||
104 | |||
105 | /** |
||
106 | * @param ActiveRecord $owner |
||
107 | */ |
||
108 | 225 | public function attach($owner) |
|
109 | { |
||
110 | 225 | parent::attach($owner); |
|
111 | 225 | if ($this->sortable !== false) { |
|
112 | 225 | $this->behavior = Yii::createObject(array_merge( |
|
113 | [ |
||
114 | 225 | 'class' => SortableBehavior::className(), |
|
115 | 225 | 'query' => [$this->parentAttribute], |
|
116 | ], |
||
117 | 225 | $this->sortable |
|
118 | )); |
||
119 | 225 | $owner->attachBehavior('adjacency-list-sortable', $this->behavior); |
|
120 | } |
||
121 | 225 | } |
|
122 | |||
123 | /** |
||
124 | * @param int|null $depth |
||
125 | * @return \yii\db\ActiveQuery |
||
126 | * @throws Exception |
||
127 | */ |
||
128 | 6 | public function getParents($depth = null) |
|
129 | { |
||
130 | 6 | $tableName = $this->owner->tableName(); |
|
131 | 6 | $ids = $this->getParentsIds($depth); |
|
132 | 6 | $query = $this->owner->find() |
|
133 | 6 | ->andWhere(["{$tableName}.[[" . $this->getPrimaryKey() . "]]" => $ids]); |
|
134 | 6 | $query->multiple = true; |
|
135 | 6 | return $query; |
|
136 | } |
||
137 | |||
138 | /** |
||
139 | * @param int|null $depth |
||
140 | * @return ActiveRecord[] |
||
141 | * @throws Exception |
||
142 | */ |
||
143 | 3 | public function getParentsOrdered($depth = null) |
|
144 | { |
||
145 | 3 | if ($depth === null && $this->_parentsOrdered !== null) { |
|
146 | return $this->_parentsOrdered; |
||
147 | } |
||
148 | 3 | $parents = $this->getParents($depth)->all(); |
|
149 | 3 | $ids = array_flip($this->getParentsIds()); |
|
150 | 3 | $primaryKey = $this->getPrimaryKey(); |
|
151 | View Code Duplication | usort($parents, function($a, $b) use ($ids, $primaryKey) { |
|
152 | 3 | $aIdx = $ids[$a->$primaryKey]; |
|
153 | 3 | $bIdx = $ids[$b->$primaryKey]; |
|
154 | 3 | if ($aIdx == $bIdx) { |
|
155 | return 0; |
||
156 | } else { |
||
157 | 3 | return $aIdx > $bIdx ? -1 : 1; |
|
158 | } |
||
159 | 3 | }); |
|
160 | 3 | if ($depth !== null) { |
|
161 | 3 | $this->_parentsOrdered = $parents; |
|
162 | } |
||
163 | 3 | return $parents; |
|
164 | } |
||
165 | |||
166 | /** |
||
167 | * @return \yii\db\ActiveQuery |
||
168 | * @throws Exception |
||
169 | */ |
||
170 | 3 | public function getParent() |
|
171 | { |
||
172 | 3 | return $this->owner->hasOne($this->owner->className(), [$this->getPrimaryKey() => $this->parentAttribute]); |
|
173 | } |
||
174 | |||
175 | /** |
||
176 | * @return \yii\db\ActiveQuery |
||
177 | */ |
||
178 | 3 | public function getRoot() |
|
179 | { |
||
180 | 3 | $tableName = $this->owner->tableName(); |
|
181 | 3 | $id = $this->getParentsIds(); |
|
182 | 3 | $id = $id ? $id[count($id) - 1] : $this->owner->primaryKey; |
|
183 | 3 | $query = $this->owner->find() |
|
184 | 3 | ->andWhere(["{$tableName}.[[" . $this->getPrimaryKey() . "]]" => $id]); |
|
185 | 3 | $query->multiple = false; |
|
186 | 3 | return $query; |
|
187 | } |
||
188 | |||
189 | /** |
||
190 | * @param int|null $depth |
||
191 | * @param bool $andSelf |
||
192 | * @return \yii\db\ActiveQuery |
||
193 | */ |
||
194 | 12 | public function getDescendants($depth = null, $andSelf = false) |
|
195 | { |
||
196 | 12 | $tableName = $this->owner->tableName(); |
|
197 | 12 | $ids = $this->getDescendantsIds($depth, true); |
|
198 | 12 | if ($andSelf) { |
|
199 | 3 | $ids[] = $this->owner->getPrimaryKey(); |
|
200 | } |
||
201 | 12 | $query = $this->owner->find() |
|
202 | 12 | ->andWhere(["{$tableName}.[[" . $this->getPrimaryKey() . "]]" => $ids]); |
|
203 | 12 | $query->multiple = true; |
|
204 | 12 | return $query; |
|
205 | } |
||
206 | |||
207 | /** |
||
208 | * @param int|null $depth |
||
209 | * @return ActiveRecord[] |
||
210 | * @throws Exception |
||
211 | */ |
||
212 | 3 | public function getDescendantsOrdered($depth = null) |
|
213 | { |
||
214 | 3 | if ($depth === null) { |
|
215 | 3 | $descendants = $this->owner->descendants; |
|
216 | } else { |
||
217 | 3 | $descendants = $this->getDescendants($depth)->all(); |
|
218 | } |
||
219 | 3 | $ids = array_flip($this->getDescendantsIds($depth, true)); |
|
220 | 3 | $primaryKey = $this->getPrimaryKey(); |
|
221 | View Code Duplication | usort($descendants, function($a, $b) use ($ids, $primaryKey) { |
|
222 | 3 | $aIdx = $ids[$a->$primaryKey]; |
|
223 | 3 | $bIdx = $ids[$b->$primaryKey]; |
|
224 | 3 | if ($aIdx == $bIdx) { |
|
225 | return 0; |
||
226 | } else { |
||
227 | 3 | return $aIdx > $bIdx ? 1 : -1; |
|
228 | } |
||
229 | 3 | }); |
|
230 | 3 | return $descendants; |
|
231 | } |
||
232 | |||
233 | /** |
||
234 | * @return \yii\db\ActiveQuery |
||
235 | */ |
||
236 | 12 | public function getChildren() |
|
237 | { |
||
238 | 12 | $result = $this->owner->hasMany($this->owner->className(), [$this->parentAttribute => $this->getPrimaryKey()]); |
|
239 | 12 | if ($this->sortable !== false) { |
|
240 | 12 | $result->orderBy([$this->behavior->sortAttribute => SORT_ASC]); |
|
241 | } |
||
242 | 12 | return $result; |
|
243 | } |
||
244 | |||
245 | /** |
||
246 | * @param int|null $depth |
||
247 | * @return \yii\db\ActiveQuery |
||
248 | */ |
||
249 | 3 | public function getLeaves($depth = null) |
|
250 | { |
||
251 | 3 | $query = $this->getDescendants($depth) |
|
252 | 3 | ->joinWith(['children' => function ($query) { |
|
253 | /** @var \yii\db\ActiveQuery $query */ |
||
254 | 3 | $modelClass = $query->modelClass; |
|
255 | $query |
||
256 | 3 | ->from($modelClass::tableName() . ' children') |
|
257 | 3 | ->orderBy(null); |
|
258 | 3 | }]) |
|
259 | 3 | ->andWhere(["children.[[{$this->parentAttribute}]]" => null]); |
|
260 | 3 | $query->multiple = true; |
|
261 | 3 | return $query; |
|
262 | } |
||
263 | |||
264 | /** |
||
265 | * @return \yii\db\ActiveQuery |
||
266 | * @throws NotSupportedException |
||
267 | */ |
||
268 | 3 | View Code Duplication | public function getPrev() |
269 | { |
||
270 | 3 | if ($this->sortable === false) { |
|
271 | throw new NotSupportedException('prev() not allow if not set sortable'); |
||
272 | } |
||
273 | 3 | $tableName = $this->owner->tableName(); |
|
274 | 3 | $query = $this->owner->find() |
|
275 | 3 | ->andWhere([ |
|
276 | 3 | 'and', |
|
277 | 3 | ["{$tableName}.[[{$this->parentAttribute}]]" => $this->owner->getAttribute($this->parentAttribute)], |
|
278 | 3 | ['<', "{$tableName}.[[{$this->behavior->sortAttribute}]]", $this->owner->getSortablePosition()], |
|
279 | ]) |
||
280 | 3 | ->orderBy(["{$tableName}.[[{$this->behavior->sortAttribute}]]" => SORT_DESC]) |
|
281 | 3 | ->limit(1); |
|
282 | 3 | $query->multiple = false; |
|
283 | 3 | return $query; |
|
284 | } |
||
285 | |||
286 | /** |
||
287 | * @return \yii\db\ActiveQuery |
||
288 | * @throws NotSupportedException |
||
289 | */ |
||
290 | 3 | View Code Duplication | public function getNext() |
291 | { |
||
292 | 3 | if ($this->sortable === false) { |
|
293 | throw new NotSupportedException('next() not allow if not set sortable'); |
||
294 | } |
||
295 | 3 | $tableName = $this->owner->tableName(); |
|
296 | 3 | $query = $this->owner->find() |
|
297 | 3 | ->andWhere([ |
|
298 | 3 | 'and', |
|
299 | 3 | ["{$tableName}.[[{$this->parentAttribute}]]" => $this->owner->getAttribute($this->parentAttribute)], |
|
300 | 3 | ['>', "{$tableName}.[[{$this->behavior->sortAttribute}]]", $this->owner->getSortablePosition()], |
|
301 | ]) |
||
302 | 3 | ->orderBy(["{$tableName}.[[{$this->behavior->sortAttribute}]]" => SORT_ASC]) |
|
303 | 3 | ->limit(1); |
|
304 | 3 | $query->multiple = false; |
|
305 | 3 | return $query; |
|
306 | } |
||
307 | |||
308 | /** |
||
309 | * @param int|null $depth |
||
310 | * @param bool $cache |
||
311 | * @return array |
||
312 | */ |
||
313 | 27 | public function getParentsIds($depth = null, $cache = true) |
|
314 | { |
||
315 | 27 | if ($cache && $this->_parentsIds !== null) { |
|
316 | 3 | return $depth === null ? $this->_parentsIds : array_slice($this->_parentsIds, 0, $depth); |
|
317 | } |
||
318 | |||
319 | 27 | $parentId = $this->owner->getAttribute($this->parentAttribute); |
|
320 | 27 | if ($parentId === null) { |
|
321 | 9 | if ($cache) { |
|
322 | 9 | $this->_parentsIds = []; |
|
323 | } |
||
324 | 9 | return []; |
|
325 | } |
||
326 | 27 | $result = [(string)$parentId]; |
|
327 | 27 | $tableName = $this->owner->tableName(); |
|
328 | 27 | $primaryKey = $this->getPrimaryKey(); |
|
329 | 27 | $depthCur = 1; |
|
330 | 27 | while ($parentId !== null && ($depth === null || $depthCur < $depth)) { |
|
331 | 27 | $query = (new Query()) |
|
332 | 27 | ->select(["lvl0.[[{$this->parentAttribute}]] AS lvl0"]) |
|
333 | 27 | ->from("{$tableName} lvl0") |
|
334 | 27 | ->where(["lvl0.[[{$primaryKey}]]" => $parentId]); |
|
335 | 27 | for ($i = 0; $i < $this->parentsJoinLevels && ($depth === null || $i + $depthCur + 1 < $depth); $i++) { |
|
336 | 15 | $j = $i + 1; |
|
337 | $query |
||
338 | 15 | ->addSelect(["lvl{$j}.[[{$this->parentAttribute}]] as lvl{$j}"]) |
|
339 | 15 | ->leftJoin("{$tableName} lvl{$j}", "lvl{$j}.[[{$primaryKey}]] = lvl{$i}.[[{$this->parentAttribute}]]"); |
|
340 | } |
||
341 | 27 | if ($parentIds = $query->one($this->owner->getDb())) { |
|
342 | 27 | foreach ($parentIds as $parentId) { |
|
343 | 27 | $depthCur++; |
|
344 | 27 | if ($parentId === null) { |
|
345 | 27 | break; |
|
346 | } |
||
347 | 27 | $result[] = $parentId; |
|
348 | } |
||
349 | } else { |
||
350 | $parentId = null; |
||
351 | } |
||
352 | } |
||
353 | 27 | if ($cache && $depth === null) { |
|
354 | 24 | $this->_parentsIds = $result; |
|
355 | } |
||
356 | 27 | return $result; |
|
357 | } |
||
358 | |||
359 | /** |
||
360 | * @param int|null $depth |
||
361 | * @param bool $flat |
||
362 | * @param bool $cache |
||
363 | * @return array |
||
364 | */ |
||
365 | 24 | public function getDescendantsIds($depth = null, $flat = false, $cache = true) |
|
366 | { |
||
367 | 24 | if ($cache && $this->_childrenIds !== null) { |
|
368 | 6 | $result = $depth === null ? $this->_childrenIds : array_slice($this->_childrenIds, 0, $depth); |
|
369 | 6 | return $flat && !empty($result) ? call_user_func_array('array_merge', $result) : $result; |
|
370 | } |
||
371 | |||
372 | 24 | $result = []; |
|
373 | 24 | $tableName = $this->owner->tableName(); |
|
374 | 24 | $primaryKey = $this->getPrimaryKey(); |
|
375 | 24 | $depthCur = 0; |
|
376 | 24 | $lastLevelIds = [$this->owner->primaryKey]; |
|
377 | 24 | while (!empty($lastLevelIds) && ($depth === null || $depthCur < $depth)) { |
|
378 | 24 | $levels = 1; |
|
379 | 24 | $depthCur++; |
|
380 | 24 | $query = (new Query()) |
|
381 | 24 | ->select(["lvl0.[[{$primaryKey}]] AS lvl0"]) |
|
382 | 24 | ->from("{$tableName} lvl0") |
|
383 | 24 | ->where(["lvl0.[[{$this->parentAttribute}]]" => $lastLevelIds]); |
|
384 | 24 | View Code Duplication | if ($this->sortable !== false) { |
385 | 24 | $query->orderBy(["lvl0.[[{$this->behavior->sortAttribute}]]" => SORT_ASC]); |
|
386 | } |
||
387 | 24 | for ($i = 0; $i < $this->childrenJoinLevels && ($depth === null || $i + $depthCur + 1 < $depth); $i++) { |
|
388 | 18 | $depthCur++; |
|
389 | 18 | $levels++; |
|
390 | 18 | $j = $i + 1; |
|
391 | $query |
||
392 | 18 | ->addSelect(["lvl{$j}.[[{$primaryKey}]] as lvl{$j}"]) |
|
393 | 18 | ->leftJoin("{$tableName} lvl{$j}", [ |
|
394 | 18 | 'and', |
|
395 | 18 | "lvl{$j}.[[{$this->parentAttribute}]] = lvl{$i}.[[{$primaryKey}]]", |
|
396 | 18 | ['is not', "lvl{$i}.[[{$primaryKey}]]", null], |
|
397 | ]); |
||
398 | 18 | View Code Duplication | if ($this->sortable !== false) { |
399 | 18 | $query->addOrderBy(["lvl{$j}.[[{$this->behavior->sortAttribute}]]" => SORT_ASC]); |
|
400 | } |
||
401 | } |
||
402 | 24 | if ($this->childrenJoinLevels) { |
|
403 | 21 | $columns = []; |
|
404 | 21 | foreach ($query->all($this->owner->getDb()) as $row) { |
|
405 | 21 | $level = 0; |
|
406 | 21 | foreach ($row as $id) { |
|
407 | 21 | if ($id !== null) { |
|
408 | 21 | $columns[$level][$id] = true; |
|
409 | } |
||
410 | 21 | $level++; |
|
411 | } |
||
412 | } |
||
413 | 21 | for ($i = 0; $i < $levels; $i++) { |
|
414 | 21 | if (isset($columns[$i])) { |
|
415 | 21 | $lastLevelIds = array_keys($columns[$i]); |
|
416 | 21 | $result[] = $lastLevelIds; |
|
417 | } else { |
||
418 | 18 | $lastLevelIds = []; |
|
419 | 18 | break; |
|
420 | } |
||
421 | } |
||
422 | } else { |
||
423 | 21 | $lastLevelIds = $query->column($this->owner->getDb()); |
|
424 | 21 | if ($lastLevelIds) { |
|
425 | 21 | $result[] = $lastLevelIds; |
|
426 | } |
||
427 | } |
||
428 | } |
||
429 | 24 | if ($cache && $depth === null) { |
|
430 | 24 | $this->_childrenIds = $result; |
|
431 | } |
||
432 | 24 | return $flat && !empty($result) ? call_user_func_array('array_merge', $result) : $result; |
|
433 | } |
||
434 | |||
435 | /** |
||
436 | * Populate children relations for self and all descendants |
||
437 | * |
||
438 | * @param int $depth = null |
||
439 | * @param string|array $with = null |
||
440 | * @return static |
||
441 | */ |
||
442 | 3 | public function populateTree($depth = null, $with = null) |
|
443 | { |
||
444 | /** @var ActiveRecord[]|static[] $nodes */ |
||
445 | 3 | $depths = [$this->owner->getPrimaryKey() => 0]; |
|
446 | 3 | $data = $this->getDescendantsIds($depth); |
|
447 | 3 | foreach ($data as $i => $ids) { |
|
448 | 3 | foreach ($ids as $id) { |
|
449 | 3 | $depths[$id] = $i + 1; |
|
450 | } |
||
451 | } |
||
452 | 3 | $query = $this->getDescendants($depth); |
|
453 | 3 | if ($with) { |
|
454 | $query->with($with); |
||
455 | } |
||
456 | $nodes = $query |
||
457 | 3 | ->orderBy($this->sortable !== false ? [$this->behavior->sortAttribute => SORT_ASC] : null) |
|
458 | 3 | ->all(); |
|
459 | |||
460 | 3 | $relates = []; |
|
461 | 3 | foreach ($nodes as $node) { |
|
462 | 3 | $key = $node->getAttribute($this->parentAttribute); |
|
463 | 3 | if (!isset($relates[$key])) { |
|
464 | 3 | $relates[$key] = []; |
|
465 | } |
||
466 | 3 | $relates[$key][] = $node; |
|
467 | } |
||
468 | |||
469 | 3 | $nodes[] = $this->owner; |
|
470 | 3 | foreach ($nodes as $node) { |
|
471 | 3 | $key = $node->getPrimaryKey(); |
|
472 | 3 | if (isset($relates[$key])) { |
|
473 | 3 | $node->populateRelation('children', $relates[$key]); |
|
474 | 3 | } elseif ($depth === null || (isset($depths[$node->getPrimaryKey()]) && $depths[$node->getPrimaryKey()] < $depth)) { |
|
475 | 3 | $node->populateRelation('children', []); |
|
476 | } |
||
477 | } |
||
478 | |||
479 | 3 | return $this->owner; |
|
480 | } |
||
481 | |||
482 | /** |
||
483 | * @return bool |
||
484 | */ |
||
485 | 81 | public function isRoot() |
|
486 | { |
||
487 | 81 | return $this->owner->getAttribute($this->parentAttribute) === null; |
|
488 | } |
||
489 | |||
490 | /** |
||
491 | * @param ActiveRecord $node |
||
492 | * @return bool |
||
493 | */ |
||
494 | 15 | public function isChildOf($node) |
|
495 | { |
||
496 | 15 | $ids = $this->getParentsIds(); |
|
497 | 15 | return in_array($node->getPrimaryKey(), $ids); |
|
498 | } |
||
499 | |||
500 | /** |
||
501 | * @return bool |
||
502 | */ |
||
503 | 3 | public function isLeaf() |
|
504 | { |
||
505 | 3 | return count($this->owner->children) === 0; |
|
506 | } |
||
507 | |||
508 | /** |
||
509 | * @return ActiveRecord |
||
510 | */ |
||
511 | 6 | public function makeRoot() |
|
512 | { |
||
513 | 6 | $this->operation = self::OPERATION_MAKE_ROOT; |
|
514 | 6 | return $this->owner; |
|
515 | } |
||
516 | |||
517 | /** |
||
518 | * @param ActiveRecord $node |
||
519 | * @return ActiveRecord |
||
520 | */ |
||
521 | 36 | public function prependTo($node) |
|
522 | { |
||
523 | 36 | $this->operation = self::OPERATION_PREPEND_TO; |
|
524 | 36 | $this->node = $node; |
|
525 | 36 | return $this->owner; |
|
526 | } |
||
527 | |||
528 | /** |
||
529 | * @param ActiveRecord $node |
||
530 | * @return ActiveRecord |
||
531 | */ |
||
532 | 36 | public function appendTo($node) |
|
533 | { |
||
534 | 36 | $this->operation = self::OPERATION_APPEND_TO; |
|
535 | 36 | $this->node = $node; |
|
536 | 36 | return $this->owner; |
|
537 | } |
||
538 | |||
539 | /** |
||
540 | * @param ActiveRecord $node |
||
541 | * @return ActiveRecord |
||
542 | */ |
||
543 | 30 | public function insertBefore($node) |
|
544 | { |
||
545 | 30 | $this->operation = self::OPERATION_INSERT_BEFORE; |
|
546 | 30 | $this->node = $node; |
|
547 | 30 | return $this->owner; |
|
548 | } |
||
549 | |||
550 | /** |
||
551 | * @param ActiveRecord $node |
||
552 | * @return ActiveRecord |
||
553 | */ |
||
554 | 33 | public function insertAfter($node) |
|
555 | { |
||
556 | 33 | $this->operation = self::OPERATION_INSERT_AFTER; |
|
557 | 33 | $this->node = $node; |
|
558 | 33 | return $this->owner; |
|
559 | } |
||
560 | |||
561 | /** |
||
562 | * Need for paulzi/auto-tree |
||
563 | */ |
||
564 | public function preDeleteWithChildren() |
||
565 | { |
||
566 | $this->operation = self::OPERATION_DELETE_ALL; |
||
567 | } |
||
568 | |||
569 | /** |
||
570 | * @return bool|int |
||
571 | * @throws \Exception |
||
572 | * @throws \yii\db\Exception |
||
573 | */ |
||
574 | 12 | public function deleteWithChildren() |
|
575 | { |
||
576 | 12 | $this->operation = self::OPERATION_DELETE_ALL; |
|
577 | 12 | if (!$this->owner->isTransactional(ActiveRecord::OP_DELETE)) { |
|
578 | $transaction = $this->owner->getDb()->beginTransaction(); |
||
579 | try { |
||
580 | $result = $this->deleteWithChildrenInternal(); |
||
581 | if ($result === false) { |
||
582 | $transaction->rollBack(); |
||
583 | } else { |
||
584 | $transaction->commit(); |
||
585 | } |
||
586 | return $result; |
||
587 | } catch (\Exception $e) { |
||
588 | $transaction->rollBack(); |
||
589 | throw $e; |
||
590 | } |
||
591 | } else { |
||
592 | 12 | $result = $this->deleteWithChildrenInternal(); |
|
593 | } |
||
594 | 9 | return $result; |
|
595 | } |
||
596 | |||
597 | /** |
||
598 | * @param bool $middle |
||
599 | * @return int |
||
600 | */ |
||
601 | 3 | public function reorderChildren($middle = true) |
|
602 | { |
||
603 | /** @var ActiveRecord|SortableBehavior $item */ |
||
604 | 3 | $item = $this->owner->children[0]; |
|
605 | 3 | if ($item) { |
|
606 | 3 | return $item->reorder($middle); |
|
607 | } else { |
||
608 | return 0; |
||
609 | } |
||
610 | } |
||
611 | |||
612 | /** |
||
613 | * @throws Exception |
||
614 | * @throws NotSupportedException |
||
615 | */ |
||
616 | 147 | public function beforeSave() |
|
617 | { |
||
618 | 147 | if ($this->node !== null && !$this->node->getIsNewRecord()) { |
|
619 | 117 | $this->node->refresh(); |
|
620 | } |
||
621 | 147 | switch ($this->operation) { |
|
622 | 147 | case self::OPERATION_MAKE_ROOT: |
|
623 | 6 | $this->owner->setAttribute($this->parentAttribute, null); |
|
624 | 6 | if ($this->sortable !== false) { |
|
625 | 6 | $this->owner->setAttribute($this->behavior->sortAttribute, 0); |
|
626 | } |
||
627 | 6 | break; |
|
628 | |||
629 | 141 | case self::OPERATION_PREPEND_TO: |
|
630 | 36 | $this->insertIntoInternal(false); |
|
631 | 24 | break; |
|
632 | |||
633 | 105 | case self::OPERATION_APPEND_TO: |
|
634 | 36 | $this->insertIntoInternal(true); |
|
635 | 24 | break; |
|
636 | |||
637 | 69 | case self::OPERATION_INSERT_BEFORE: |
|
638 | 30 | $this->insertNearInternal(false); |
|
639 | 21 | break; |
|
640 | |||
641 | 39 | case self::OPERATION_INSERT_AFTER: |
|
642 | 33 | $this->insertNearInternal(true); |
|
643 | 21 | break; |
|
644 | |||
645 | default: |
||
646 | 6 | if ($this->owner->getIsNewRecord()) { |
|
647 | 3 | throw new NotSupportedException('Method "' . $this->owner->className() . '::insert" is not supported for inserting new nodes.'); |
|
648 | } |
||
649 | } |
||
650 | 99 | } |
|
651 | |||
652 | /** |
||
653 | * |
||
654 | */ |
||
655 | 99 | public function afterSave() |
|
656 | { |
||
657 | 99 | $this->operation = null; |
|
658 | 99 | $this->node = null; |
|
659 | 99 | } |
|
660 | |||
661 | /** |
||
662 | * @param \yii\base\ModelEvent $event |
||
663 | * @throws Exception |
||
664 | */ |
||
665 | 21 | public function beforeDelete($event) |
|
666 | { |
||
667 | 21 | if ($this->owner->getIsNewRecord()) { |
|
668 | 6 | throw new Exception('Can not delete a node when it is new record.'); |
|
669 | } |
||
670 | 15 | if ($this->isRoot() && $this->operation !== self::OPERATION_DELETE_ALL) { |
|
671 | 3 | throw new Exception('Method "'. $this->owner->className() . '::delete" is not supported for deleting root nodes.'); |
|
672 | } |
||
673 | 12 | $this->owner->refresh(); |
|
674 | 12 | } |
|
675 | |||
676 | /** |
||
677 | * |
||
678 | */ |
||
679 | 12 | public function afterDelete() |
|
680 | { |
||
681 | 12 | if ($this->operation !== static::OPERATION_DELETE_ALL) { |
|
682 | 3 | $this->owner->updateAll( |
|
683 | 3 | [$this->parentAttribute => $this->owner->getAttribute($this->parentAttribute)], |
|
684 | 3 | [$this->parentAttribute => $this->owner->getPrimaryKey()] |
|
685 | ); |
||
686 | } |
||
687 | 12 | $this->operation = null; |
|
688 | 12 | } |
|
689 | |||
690 | /** |
||
691 | * @return mixed |
||
692 | * @throws Exception |
||
693 | */ |
||
694 | 63 | protected function getPrimaryKey() |
|
695 | { |
||
696 | 63 | $primaryKey = $this->owner->primaryKey(); |
|
697 | 63 | if (!isset($primaryKey[0])) { |
|
698 | throw new Exception('"' . $this->owner->className() . '" must have a primary key.'); |
||
699 | } |
||
700 | 63 | return $primaryKey[0]; |
|
701 | } |
||
702 | |||
703 | /** |
||
704 | * @param bool $forInsertNear |
||
705 | * @throws Exception |
||
706 | */ |
||
707 | 135 | protected function checkNode($forInsertNear = false) |
|
708 | { |
||
709 | 135 | if ($forInsertNear && $this->node->isRoot()) { |
|
710 | 9 | throw new Exception('Can not move a node before/after root.'); |
|
711 | } |
||
712 | 126 | if ($this->node->getIsNewRecord()) { |
|
713 | 12 | throw new Exception('Can not move a node when the target node is new record.'); |
|
714 | } |
||
715 | |||
716 | 114 | if ($this->owner->equals($this->node)) { |
|
717 | 12 | throw new Exception('Can not move a node when the target node is same.'); |
|
718 | } |
||
719 | |||
720 | 102 | if ($this->checkLoop && $this->node->isChildOf($this->owner)) { |
|
721 | 12 | throw new Exception('Can not move a node when the target node is child.'); |
|
722 | } |
||
723 | 90 | } |
|
724 | |||
725 | /** |
||
726 | * Append to operation internal handler |
||
727 | * @param bool $append |
||
728 | * @throws Exception |
||
729 | */ |
||
730 | 72 | protected function insertIntoInternal($append) |
|
731 | { |
||
732 | 72 | $this->checkNode(false); |
|
733 | 48 | $this->owner->setAttribute($this->parentAttribute, $this->node->getPrimaryKey()); |
|
734 | 48 | if ($this->sortable !== false) { |
|
735 | 48 | if ($append) { |
|
736 | 24 | $this->behavior->moveLast(); |
|
737 | } else { |
||
738 | 24 | $this->behavior->moveFirst(); |
|
739 | } |
||
740 | } |
||
741 | 48 | } |
|
742 | |||
743 | /** |
||
744 | * Insert operation internal handler |
||
745 | * @param bool $forward |
||
746 | * @throws Exception |
||
747 | */ |
||
748 | 63 | protected function insertNearInternal($forward) |
|
749 | { |
||
750 | 63 | $this->checkNode(true); |
|
751 | 42 | $this->owner->setAttribute($this->parentAttribute, $this->node->getAttribute($this->parentAttribute)); |
|
752 | 42 | if ($this->sortable !== false) { |
|
753 | 42 | if ($forward) { |
|
754 | 21 | $this->behavior->moveAfter($this->node); |
|
755 | } else { |
||
756 | 21 | $this->behavior->moveBefore($this->node); |
|
757 | } |
||
758 | } |
||
759 | 42 | } |
|
760 | |||
761 | /** |
||
762 | * @return int |
||
763 | */ |
||
764 | 12 | protected function deleteWithChildrenInternal() |
|
765 | { |
||
766 | 12 | if (!$this->owner->beforeDelete()) { |
|
767 | return false; |
||
768 | } |
||
769 | 9 | $ids = $this->getDescendantsIds(null, true); |
|
770 | 9 | $ids[] = $this->owner->primaryKey; |
|
771 | 9 | $result = $this->owner->deleteAll([$this->getPrimaryKey() => $ids]); |
|
772 | 9 | $this->owner->setOldAttributes(null); |
|
773 | 9 | $this->owner->afterDelete(); |
|
774 | 9 | return $result; |
|
775 | } |
||
776 | } |