This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * @link https://github.com/paulzi/yii2-materialized-path |
||
4 | * @copyright Copyright (c) 2015 PaulZi <[email protected]> |
||
5 | * @license MIT (https://github.com/paulzi/yii2-materialized-path/blob/master/LICENSE) |
||
6 | */ |
||
7 | |||
8 | namespace paulzi\materializedPath; |
||
9 | |||
10 | use paulzi\sortable\SortableBehavior; |
||
11 | use Yii; |
||
12 | use yii\base\Behavior; |
||
13 | use yii\base\Exception; |
||
14 | use yii\base\NotSupportedException; |
||
15 | use yii\db\ActiveRecord; |
||
16 | use yii\db\Expression; |
||
17 | |||
18 | /** |
||
19 | * Materialized Path Behavior for Yii2 |
||
20 | * @author PaulZi <[email protected]> |
||
21 | * |
||
22 | * @property ActiveRecord $owner |
||
23 | */ |
||
24 | class MaterializedPathBehavior extends Behavior |
||
25 | { |
||
26 | const OPERATION_MAKE_ROOT = 1; |
||
27 | const OPERATION_PREPEND_TO = 2; |
||
28 | const OPERATION_APPEND_TO = 3; |
||
29 | const OPERATION_INSERT_BEFORE = 4; |
||
30 | const OPERATION_INSERT_AFTER = 5; |
||
31 | const OPERATION_DELETE_ALL = 6; |
||
32 | |||
33 | |||
34 | /** |
||
35 | * @var string |
||
36 | */ |
||
37 | public $pathAttribute = 'path'; |
||
38 | |||
39 | /** |
||
40 | * @var string |
||
41 | */ |
||
42 | public $depthAttribute = 'depth'; |
||
43 | |||
44 | /** |
||
45 | * @var string |
||
46 | */ |
||
47 | public $itemAttribute; |
||
48 | |||
49 | /** |
||
50 | * @var string|null |
||
51 | */ |
||
52 | public $treeAttribute; |
||
53 | |||
54 | /** |
||
55 | * @var array|false SortableBehavior config |
||
56 | */ |
||
57 | public $sortable = []; |
||
58 | |||
59 | /** |
||
60 | * @var string |
||
61 | */ |
||
62 | public $delimiter = '/'; |
||
63 | |||
64 | /** |
||
65 | * @var int Value of $depthAttribute for root node. |
||
66 | */ |
||
67 | public $rootDepthValue = 0; |
||
68 | |||
69 | /** |
||
70 | * @var int|null |
||
71 | */ |
||
72 | protected $operation; |
||
73 | |||
74 | /** |
||
75 | * @var ActiveRecord|self|null |
||
76 | */ |
||
77 | protected $node; |
||
78 | |||
79 | /** |
||
80 | * @var SortableBehavior |
||
81 | */ |
||
82 | protected $behavior; |
||
83 | |||
84 | /** |
||
85 | * @var bool |
||
86 | */ |
||
87 | protected $primaryKeyMode = false; |
||
88 | |||
89 | |||
90 | /** |
||
91 | * @inheritdoc |
||
92 | */ |
||
93 | 201 | public function events() |
|
94 | { |
||
95 | return [ |
||
96 | 201 | ActiveRecord::EVENT_BEFORE_INSERT => 'beforeSave', |
|
97 | ActiveRecord::EVENT_AFTER_INSERT => 'afterInsert', |
||
98 | ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeSave', |
||
99 | ActiveRecord::EVENT_AFTER_UPDATE => 'afterUpdate', |
||
100 | ActiveRecord::EVENT_BEFORE_DELETE => 'beforeDelete', |
||
101 | ActiveRecord::EVENT_AFTER_DELETE => 'afterDelete', |
||
102 | ]; |
||
103 | } |
||
104 | |||
105 | /** |
||
106 | * @param ActiveRecord $owner |
||
107 | * @throws Exception |
||
108 | */ |
||
109 | 201 | public function attach($owner) |
|
110 | { |
||
111 | 201 | parent::attach($owner); |
|
112 | 201 | if ($this->itemAttribute === null) { |
|
113 | 201 | $primaryKey = $owner->primaryKey(); |
|
114 | 201 | if (!isset($primaryKey[0])) { |
|
115 | throw new Exception('"' . $owner->className() . '" must have a primary key.'); |
||
116 | } |
||
117 | 201 | $this->itemAttribute = $primaryKey[0]; |
|
118 | 201 | $this->primaryKeyMode = true; |
|
119 | } |
||
120 | 201 | if ($this->sortable !== false) { |
|
121 | 201 | $this->behavior = Yii::createObject(array_merge( |
|
122 | [ |
||
123 | 201 | 'class' => SortableBehavior::className(), |
|
124 | 'query' => function () { |
||
125 | 87 | return $this->getSortableQuery(); |
|
126 | 201 | }, |
|
127 | ], |
||
128 | 201 | $this->sortable |
|
129 | )); |
||
130 | 201 | $owner->attachBehavior('materialized-path-sortable', $this->behavior); |
|
131 | } |
||
132 | 201 | } |
|
133 | |||
134 | /** |
||
135 | * @param int|null $depth |
||
136 | * @return \yii\db\ActiveQuery |
||
137 | */ |
||
138 | 9 | public function getParents($depth = null) |
|
139 | { |
||
140 | 9 | $path = $this->getParentPath(); |
|
141 | 9 | if ($path !== null) { |
|
142 | 9 | $paths = explode($this->delimiter, $path); |
|
143 | 9 | if (!$this->primaryKeyMode) { |
|
144 | 9 | $path = null; |
|
145 | 9 | $paths = array_map( |
|
146 | 9 | function ($value) use (&$path) { |
|
147 | 9 | return $path = ($path !== null ? $path . $this->delimiter : '') . $value; |
|
148 | 9 | }, |
|
149 | 9 | $paths |
|
150 | ); |
||
151 | } |
||
152 | 9 | if ($depth !== null) { |
|
153 | 9 | $paths = array_slice($paths, -$depth); |
|
154 | } |
||
155 | } else { |
||
156 | 3 | $paths = []; |
|
157 | } |
||
158 | |||
159 | 9 | $tableName = $this->owner->tableName(); |
|
160 | 9 | $condition = ['and']; |
|
161 | 9 | if ($this->primaryKeyMode) { |
|
162 | 6 | $condition[] = ["{$tableName}.[[{$this->itemAttribute}]]" => $paths]; |
|
163 | } else { |
||
164 | 9 | $condition[] = ["{$tableName}.[[{$this->pathAttribute}]]" => $paths]; |
|
165 | } |
||
166 | |||
167 | 9 | $query = $this->owner->find() |
|
168 | 9 | ->andWhere($condition) |
|
169 | 9 | ->andWhere($this->treeCondition()) |
|
170 | 9 | ->addOrderBy(["{$tableName}.[[{$this->pathAttribute}]]" => SORT_ASC]); |
|
171 | 9 | $query->multiple = true; |
|
172 | |||
173 | 9 | return $query; |
|
174 | } |
||
175 | |||
176 | /** |
||
177 | * @return \yii\db\ActiveQuery |
||
178 | */ |
||
179 | 6 | public function getParent() |
|
180 | { |
||
181 | 6 | $query = $this->getParents(1)->limit(1); |
|
182 | 6 | $query->multiple = false; |
|
183 | 6 | return $query; |
|
184 | } |
||
185 | |||
186 | /** |
||
187 | * @return \yii\db\ActiveQuery |
||
188 | */ |
||
189 | 3 | public function getRoot() |
|
190 | { |
||
191 | 3 | $path = explode($this->delimiter, $this->owner->getAttribute($this->pathAttribute)); |
|
192 | 3 | $path = array_shift($path); |
|
193 | 3 | $tableName = $this->owner->tableName(); |
|
194 | 3 | $query = $this->owner->find() |
|
195 | 3 | ->andWhere(["{$tableName}.[[{$this->pathAttribute}]]" => $path]) |
|
196 | 3 | ->andWhere($this->treeCondition()) |
|
197 | 3 | ->limit(1); |
|
198 | 3 | $query->multiple = false; |
|
199 | 3 | return $query; |
|
200 | } |
||
201 | |||
202 | /** |
||
203 | * @param int|null $depth |
||
204 | * @param bool $andSelf |
||
205 | * @return \yii\db\ActiveQuery |
||
206 | */ |
||
207 | 27 | public function getDescendants($depth = null, $andSelf = false) |
|
208 | { |
||
209 | 27 | $tableName = $this->owner->tableName(); |
|
210 | 27 | $path = $this->owner->getAttribute($this->pathAttribute); |
|
211 | 27 | $query = $this->owner->find() |
|
212 | 27 | ->andWhere(['like', "{$tableName}.[[{$this->pathAttribute}]]", $this->getLike($path), false]); |
|
213 | |||
214 | 27 | if ($andSelf) { |
|
215 | 9 | $query->orWhere(["{$tableName}.[[{$this->pathAttribute}]]" => $path]); |
|
216 | } |
||
217 | |||
218 | 27 | if ($depth !== null) { |
|
219 | 21 | $query->andWhere(['<=', "{$tableName}.[[{$this->depthAttribute}]]", $this->owner->getAttribute($this->depthAttribute) + $depth]); |
|
220 | } |
||
221 | |||
222 | 27 | $orderBy = []; |
|
223 | 27 | $orderBy["{$tableName}.[[{$this->depthAttribute}]]"] = SORT_ASC; |
|
224 | 27 | View Code Duplication | if ($this->sortable !== false) { |
225 | 27 | $orderBy["{$tableName}.[[{$this->behavior->sortAttribute}]]"] = SORT_ASC; |
|
226 | } |
||
227 | 27 | $orderBy["{$tableName}.[[{$this->itemAttribute}]]"] = SORT_ASC; |
|
228 | |||
229 | $query |
||
230 | 27 | ->andWhere($this->treeCondition()) |
|
231 | 27 | ->addOrderBy($orderBy); |
|
232 | 27 | $query->multiple = true; |
|
233 | |||
234 | 27 | return $query; |
|
235 | } |
||
236 | |||
237 | /** |
||
238 | * @return \yii\db\ActiveQuery |
||
239 | */ |
||
240 | 12 | public function getChildren() |
|
241 | { |
||
242 | 12 | return $this->getDescendants(1); |
|
243 | } |
||
244 | |||
245 | /** |
||
246 | * @param int|null $depth |
||
247 | * @return \yii\db\ActiveQuery |
||
248 | */ |
||
249 | 3 | public function getLeaves($depth = null) |
|
250 | { |
||
251 | 3 | $tableName = $this->owner->tableName(); |
|
252 | $condition = [ |
||
253 | 3 | 'and', |
|
254 | 3 | ['like', "leaves.[[{$this->pathAttribute}]]", new Expression($this->concatExpression(["{$tableName}.[[{$this->pathAttribute}]]", ':delimiter']), [':delimiter' => $this->delimiter . '%'])], |
|
255 | ]; |
||
256 | |||
257 | 3 | if ($this->treeAttribute !== null) { |
|
258 | 3 | $condition[] = ["leaves.[[{$this->treeAttribute}]]" => new Expression("{$tableName}.[[{$this->treeAttribute}]]")]; |
|
259 | } |
||
260 | |||
261 | 3 | $query = $this->getDescendants($depth) |
|
262 | 3 | ->leftJoin("{$tableName} leaves", $condition) |
|
263 | 3 | ->andWhere(["leaves.[[{$this->pathAttribute}]]" => null]); |
|
264 | 3 | $query->multiple = true; |
|
265 | 3 | return $query; |
|
266 | } |
||
267 | |||
268 | /** |
||
269 | * @return \yii\db\ActiveQuery |
||
270 | * @throws NotSupportedException |
||
271 | */ |
||
272 | 3 | View Code Duplication | public function getPrev() |
273 | { |
||
274 | 3 | if ($this->sortable === false) { |
|
275 | throw new NotSupportedException('prev() not allow if not set sortable'); |
||
276 | } |
||
277 | 3 | $tableName = $this->owner->tableName(); |
|
278 | 3 | $query = $this->owner->find() |
|
279 | 3 | ->andWhere(['like', "{$tableName}.[[{$this->pathAttribute}]]", $this->getLike($this->getParentPath()), false]) |
|
0 ignored issues
–
show
|
|||
280 | 3 | ->andWhere(["{$tableName}.[[{$this->depthAttribute}]]" => $this->owner->getAttribute($this->depthAttribute)]) |
|
281 | 3 | ->andWhere(['<', "{$tableName}.[[{$this->behavior->sortAttribute}]]", $this->owner->getSortablePosition()]) |
|
282 | 3 | ->andWhere($this->treeCondition()) |
|
283 | 3 | ->orderBy(["{$tableName}.[[{$this->behavior->sortAttribute}]]" => SORT_DESC]) |
|
284 | 3 | ->limit(1); |
|
285 | 3 | $query->multiple = false; |
|
286 | 3 | return $query; |
|
287 | } |
||
288 | |||
289 | /** |
||
290 | * @return \yii\db\ActiveQuery |
||
291 | * @throws NotSupportedException |
||
292 | */ |
||
293 | 6 | View Code Duplication | public function getNext() |
294 | { |
||
295 | 6 | if ($this->sortable === false) { |
|
296 | throw new NotSupportedException('prev() not allow if not set sortable'); |
||
297 | } |
||
298 | 6 | $tableName = $this->owner->tableName(); |
|
299 | 6 | $query = $this->owner->find() |
|
300 | 6 | ->andWhere(['like', "{$tableName}.[[{$this->pathAttribute}]]", $this->getLike($this->getParentPath()), false]) |
|
0 ignored issues
–
show
It seems like
$this->getParentPath() targeting paulzi\materializedPath\...havior::getParentPath() can also be of type array or null ; however, paulzi\materializedPath\...PathBehavior::getLike() does only seem to accept string , maybe add an additional type check?
This check looks at variables that are passed out again to other methods. If the outgoing method call has stricter type requirements than the method itself, an issue is raised. An additional type check may prevent trouble. ![]() |
|||
301 | 6 | ->andWhere(["{$tableName}.[[{$this->depthAttribute}]]" => $this->owner->getAttribute($this->depthAttribute)]) |
|
302 | 6 | ->andWhere(['>', "{$tableName}.[[{$this->behavior->sortAttribute}]]", $this->owner->getSortablePosition()]) |
|
303 | 6 | ->andWhere($this->treeCondition()) |
|
304 | 6 | ->orderBy(["{$tableName}.[[{$this->behavior->sortAttribute}]]" => SORT_ASC]) |
|
305 | 6 | ->limit(1); |
|
306 | 6 | $query->multiple = false; |
|
307 | 6 | return $query; |
|
308 | } |
||
309 | |||
310 | /** |
||
311 | * Returns all sibilings of node. |
||
312 | * |
||
313 | * @param bool $andSelf = false Include self node into result. |
||
314 | * @return \yii\db\ActiveQuery |
||
315 | */ |
||
316 | public function getSiblings($andSelf = false) |
||
317 | { |
||
318 | $tableName = $this->owner->tableName(); |
||
319 | $path = $this->getParentPath(); |
||
320 | $like = strtr($path . $this->delimiter, ['%' => '\%', '_' => '\_', '\\' => '\\\\']); |
||
321 | |||
322 | $query = $this->owner->find() |
||
323 | ->andWhere(['like', "{$tableName}.[[{$this->pathAttribute}]]", $like . '%', false]) |
||
324 | ->andWhere(['<=', "{$tableName}.[[{$this->depthAttribute}]]", $this->owner->{$this->depthAttribute}]); |
||
325 | |||
326 | if (!$andSelf) { |
||
327 | $query->andWhere(["!=", "{$tableName}.[[{$this->itemAttribute}]]", $this->owner->{$this->itemAttribute}]); |
||
328 | } |
||
329 | |||
330 | $orderBy = []; |
||
331 | $orderBy["{$tableName}.[[{$this->depthAttribute}]]"] = SORT_ASC; |
||
332 | View Code Duplication | if ($this->sortable !== false) { |
|
333 | $orderBy["{$tableName}.[[{$this->behavior->sortAttribute}]]"] = SORT_ASC; |
||
334 | } |
||
335 | $orderBy["{$tableName}.[[{$this->itemAttribute}]]"] = SORT_ASC; |
||
336 | |||
337 | $query |
||
338 | ->andWhere($this->treeCondition()) |
||
339 | ->addOrderBy($orderBy); |
||
340 | $query->multiple = true; |
||
341 | return $query; |
||
342 | } |
||
343 | |||
344 | /** |
||
345 | * @param bool $asArray = false |
||
346 | * @return null|string|array |
||
347 | */ |
||
348 | 66 | public function getParentPath($asArray = false) |
|
349 | { |
||
350 | 66 | return static::getParentPathInternal($this->owner->getAttribute($this->pathAttribute), $this->delimiter, $asArray); |
|
351 | } |
||
352 | |||
353 | /** |
||
354 | * Populate children relations for self and all descendants |
||
355 | * |
||
356 | * @param int $depth = null |
||
357 | * @param string|array $with = null |
||
358 | * @return static |
||
359 | */ |
||
360 | 3 | public function populateTree($depth = null, $with = null) |
|
361 | { |
||
362 | /** @var ActiveRecord[]|static[] $nodes */ |
||
363 | 3 | $query = $this->getDescendants($depth); |
|
364 | 3 | if ($with) { |
|
365 | $query->with($with); |
||
366 | } |
||
367 | 3 | $nodes = $query->all(); |
|
368 | |||
369 | 3 | $relates = []; |
|
370 | 3 | foreach ($nodes as $node) { |
|
371 | 3 | $path = $node->getParentPath(true); |
|
372 | 3 | $key = array_pop($path); |
|
373 | 3 | if (!isset($relates[$key])) { |
|
374 | 3 | $relates[$key] = []; |
|
375 | } |
||
376 | 3 | $relates[$key][] = $node; |
|
377 | } |
||
378 | |||
379 | 3 | $ownerDepth = $this->owner->getAttribute($this->depthAttribute); |
|
380 | 3 | $nodes[] = $this->owner; |
|
381 | 3 | foreach ($nodes as $node) { |
|
382 | 3 | $key = $node->getAttribute($this->itemAttribute); |
|
383 | 3 | if (isset($relates[$key])) { |
|
384 | 3 | $node->populateRelation('children', $relates[$key]); |
|
385 | 3 | } elseif ($depth === null || $ownerDepth + $depth > $node->getAttribute($this->depthAttribute)) { |
|
386 | 3 | $node->populateRelation('children', []); |
|
387 | } |
||
388 | } |
||
389 | |||
390 | 3 | return $this->owner; |
|
391 | } |
||
392 | |||
393 | /** |
||
394 | * @return bool |
||
395 | */ |
||
396 | 72 | public function isRoot() |
|
397 | { |
||
398 | 72 | return count(explode($this->delimiter, $this->owner->getAttribute($this->pathAttribute))) === 1; |
|
399 | } |
||
400 | |||
401 | /** |
||
402 | * @param ActiveRecord $node |
||
403 | * @return bool |
||
404 | */ |
||
405 | 96 | public function isChildOf($node) |
|
406 | { |
||
407 | 96 | if ($node->getIsNewRecord()) { |
|
408 | 30 | return false; |
|
409 | } |
||
410 | 66 | $nodePath = $node->getAttribute($this->pathAttribute) . $this->delimiter; |
|
411 | 66 | $result = substr($this->owner->getAttribute($this->pathAttribute), 0, strlen($nodePath)) === $nodePath; |
|
412 | |||
413 | 66 | if ($result && $this->treeAttribute !== null) { |
|
414 | 9 | $result = $this->owner->getAttribute($this->treeAttribute) === $node->getAttribute($this->treeAttribute); |
|
415 | } |
||
416 | |||
417 | 66 | return $result; |
|
418 | } |
||
419 | |||
420 | /** |
||
421 | * @return bool |
||
422 | */ |
||
423 | 3 | public function isLeaf() |
|
424 | { |
||
425 | 3 | return count($this->owner->children) === 0; |
|
426 | } |
||
427 | |||
428 | /** |
||
429 | * @return ActiveRecord |
||
430 | */ |
||
431 | 6 | public function makeRoot() |
|
432 | { |
||
433 | 6 | $this->operation = self::OPERATION_MAKE_ROOT; |
|
434 | 6 | return $this->owner; |
|
435 | } |
||
436 | |||
437 | /** |
||
438 | * @param ActiveRecord $node |
||
439 | * @return ActiveRecord |
||
440 | */ |
||
441 | 33 | public function prependTo($node) |
|
442 | { |
||
443 | 33 | $this->operation = self::OPERATION_PREPEND_TO; |
|
444 | 33 | $this->node = $node; |
|
445 | 33 | return $this->owner; |
|
446 | } |
||
447 | |||
448 | /** |
||
449 | * @param ActiveRecord $node |
||
450 | * @return ActiveRecord |
||
451 | */ |
||
452 | 33 | public function appendTo($node) |
|
453 | { |
||
454 | 33 | $this->operation = self::OPERATION_APPEND_TO; |
|
455 | 33 | $this->node = $node; |
|
456 | 33 | return $this->owner; |
|
457 | } |
||
458 | |||
459 | /** |
||
460 | * @param ActiveRecord $node |
||
461 | * @return ActiveRecord |
||
462 | */ |
||
463 | 30 | public function insertBefore($node) |
|
464 | { |
||
465 | 30 | $this->operation = self::OPERATION_INSERT_BEFORE; |
|
466 | 30 | $this->node = $node; |
|
467 | 30 | return $this->owner; |
|
468 | } |
||
469 | |||
470 | /** |
||
471 | * @param ActiveRecord $node |
||
472 | * @return ActiveRecord |
||
473 | */ |
||
474 | 30 | public function insertAfter($node) |
|
475 | { |
||
476 | 30 | $this->operation = self::OPERATION_INSERT_AFTER; |
|
477 | 30 | $this->node = $node; |
|
478 | 30 | return $this->owner; |
|
479 | } |
||
480 | |||
481 | /** |
||
482 | * Need for paulzi/auto-tree |
||
483 | */ |
||
484 | public function preDeleteWithChildren() |
||
485 | { |
||
486 | $this->operation = self::OPERATION_DELETE_ALL; |
||
487 | } |
||
488 | |||
489 | /** |
||
490 | * @return bool|int |
||
491 | * @throws \Exception |
||
492 | * @throws \yii\db\Exception |
||
493 | */ |
||
494 | 9 | public function deleteWithChildren() |
|
495 | { |
||
496 | 9 | $this->operation = self::OPERATION_DELETE_ALL; |
|
497 | 9 | if (!$this->owner->isTransactional(ActiveRecord::OP_DELETE)) { |
|
498 | $transaction = $this->owner->getDb()->beginTransaction(); |
||
499 | try { |
||
500 | $result = $this->deleteWithChildrenInternal(); |
||
501 | if ($result === false) { |
||
502 | $transaction->rollBack(); |
||
503 | } else { |
||
504 | $transaction->commit(); |
||
505 | } |
||
506 | return $result; |
||
507 | } catch (\Exception $e) { |
||
508 | $transaction->rollBack(); |
||
509 | throw $e; |
||
510 | } |
||
511 | } else { |
||
512 | 9 | $result = $this->deleteWithChildrenInternal(); |
|
513 | } |
||
514 | 6 | return $result; |
|
515 | } |
||
516 | |||
517 | /** |
||
518 | * @param bool $middle |
||
519 | * @return int |
||
520 | */ |
||
521 | 3 | public function reorderChildren($middle = true) |
|
522 | { |
||
523 | /** @var ActiveRecord|SortableBehavior $item */ |
||
524 | 3 | $item = count($this->owner->children) > 0 ? $this->owner->children[0] : null; |
|
525 | 3 | if ($item) { |
|
526 | 3 | return $item->reorder($middle); |
|
527 | } else { |
||
528 | return 0; |
||
529 | } |
||
530 | } |
||
531 | |||
532 | /** |
||
533 | * @throws Exception |
||
534 | * @throws NotSupportedException |
||
535 | */ |
||
536 | 138 | public function beforeSave() |
|
537 | { |
||
538 | 138 | if ($this->node !== null && !$this->node->getIsNewRecord()) { |
|
539 | 108 | $this->node->refresh(); |
|
540 | } |
||
541 | |||
542 | 138 | switch ($this->operation) { |
|
543 | 138 | case self::OPERATION_MAKE_ROOT: |
|
544 | 6 | $this->makeRootInternal(); |
|
545 | |||
546 | 6 | break; |
|
547 | 132 | case self::OPERATION_PREPEND_TO: |
|
548 | 33 | $this->insertIntoInternal(false); |
|
549 | |||
550 | 21 | break; |
|
551 | 99 | case self::OPERATION_APPEND_TO: |
|
552 | 33 | $this->insertIntoInternal(true); |
|
553 | |||
554 | 21 | break; |
|
555 | 66 | case self::OPERATION_INSERT_BEFORE: |
|
556 | 30 | $this->insertNearInternal(false); |
|
557 | |||
558 | 21 | break; |
|
559 | |||
560 | 36 | case self::OPERATION_INSERT_AFTER: |
|
561 | 30 | $this->insertNearInternal(true); |
|
562 | |||
563 | 18 | break; |
|
564 | |||
565 | default: |
||
566 | 6 | if ($this->owner->getIsNewRecord()) { |
|
567 | 3 | throw new NotSupportedException('Method "' . $this->owner->className() . '::insert" is not supported for inserting new nodes.'); |
|
568 | } |
||
569 | |||
570 | 3 | $item = $this->owner->getAttribute($this->itemAttribute); |
|
571 | 3 | $path = $this->getParentPath(); |
|
572 | 3 | $this->owner->setAttribute($this->pathAttribute, ($path !== null ? $path . $this->delimiter : null) . $item); |
|
573 | } |
||
574 | 90 | } |
|
575 | |||
576 | /** |
||
577 | * @throws Exception |
||
578 | */ |
||
579 | 33 | public function afterInsert() |
|
580 | { |
||
581 | 33 | if ($this->operation === self::OPERATION_MAKE_ROOT && $this->treeAttribute !== null && $this->owner->getAttribute($this->treeAttribute) === null) { |
|
582 | 3 | $id = $this->getPrimaryKeyValue(); |
|
583 | 3 | $this->owner->setAttribute($this->treeAttribute, $id); |
|
584 | |||
585 | 3 | $primaryKey = $this->owner->primaryKey(); |
|
586 | 3 | View Code Duplication | if (!isset($primaryKey[0])) { |
587 | throw new Exception('"' . $this->owner->className() . '" must have a primary key.'); |
||
588 | } |
||
589 | |||
590 | 3 | $this->owner->updateAll([$this->treeAttribute => $id], [$primaryKey[0] => $id]); |
|
591 | } |
||
592 | 33 | if ($this->owner->getAttribute($this->pathAttribute) === null) { |
|
593 | 33 | $primaryKey = $this->owner->primaryKey(); |
|
594 | 33 | View Code Duplication | if (!isset($primaryKey[0])) { |
595 | throw new Exception('"' . $this->owner->className() . '" must have a primary key.'); |
||
596 | } |
||
597 | 33 | $id = $this->getPrimaryKeyValue(); |
|
598 | 33 | if ($this->operation === self::OPERATION_MAKE_ROOT) { |
|
599 | 3 | $path = $id; |
|
600 | } else { |
||
601 | 30 | if ($this->operation === self::OPERATION_INSERT_BEFORE || $this->operation === self::OPERATION_INSERT_AFTER) { |
|
602 | 18 | $path = $this->node->getParentPath(); |
|
603 | } else { |
||
604 | 12 | $path = $this->node->getAttribute($this->pathAttribute); |
|
605 | } |
||
606 | 30 | $path = $path . $this->delimiter . $id; |
|
607 | } |
||
608 | 33 | $this->owner->setAttribute($this->pathAttribute, $path); |
|
609 | 33 | $this->owner->updateAll([$this->pathAttribute => $path], [$primaryKey[0] => $id]); |
|
610 | } |
||
611 | 33 | $this->operation = null; |
|
612 | 33 | $this->node = null; |
|
613 | 33 | } |
|
614 | |||
615 | /** |
||
616 | * @param \yii\db\AfterSaveEvent $event |
||
617 | */ |
||
618 | 57 | public function afterUpdate($event) |
|
619 | { |
||
620 | 57 | $this->moveNode($event->changedAttributes); |
|
621 | 57 | $this->operation = null; |
|
622 | 57 | $this->node = null; |
|
623 | 57 | } |
|
624 | |||
625 | /** |
||
626 | * @param \yii\base\ModelEvent $event |
||
627 | * @throws Exception |
||
628 | */ |
||
629 | 18 | public function beforeDelete($event) |
|
630 | { |
||
631 | 18 | if ($this->owner->getIsNewRecord()) { |
|
632 | 6 | throw new Exception('Can not delete a node when it is new record.'); |
|
633 | } |
||
634 | 12 | if ($this->isRoot() && $this->operation !== self::OPERATION_DELETE_ALL) { |
|
635 | 3 | throw new Exception('Method "'. $this->owner->className() . '::delete" is not supported for deleting root nodes.'); |
|
636 | } |
||
637 | 9 | $this->owner->refresh(); |
|
638 | 9 | if ($this->operation !== static::OPERATION_DELETE_ALL && !$this->primaryKeyMode) { |
|
639 | /** @var self $parent */ |
||
640 | 3 | $parent =$this->getParent()->one(); |
|
641 | 3 | $slugs1 = $parent->getChildren() |
|
642 | 3 | ->andWhere(['<>', $this->itemAttribute, $this->owner->getAttribute($this->itemAttribute)]) |
|
643 | 3 | ->select([$this->itemAttribute]) |
|
644 | 3 | ->column(); |
|
645 | 3 | $slugs2 = $this->getChildren() |
|
646 | 3 | ->select([$this->itemAttribute]) |
|
647 | 3 | ->column(); |
|
648 | 3 | if (array_intersect($slugs1, $slugs2)) { |
|
649 | $event->isValid = false; |
||
650 | } |
||
651 | } |
||
652 | 9 | } |
|
653 | |||
654 | /** |
||
655 | * |
||
656 | */ |
||
657 | 9 | public function afterDelete() |
|
658 | { |
||
659 | 9 | if ($this->operation !== static::OPERATION_DELETE_ALL) { |
|
660 | 3 | foreach ($this->owner->children as $child) { |
|
661 | /** @var self $child */ |
||
662 | 3 | if ($this->owner->next === null) { |
|
663 | $child->appendTo($this->owner->parent)->save(); |
||
664 | } else { |
||
665 | 3 | $child->insertBefore($this->owner->next)->save(); |
|
666 | } |
||
667 | } |
||
668 | } |
||
669 | 9 | $this->operation = null; |
|
670 | 9 | $this->node = null; |
|
671 | 9 | } |
|
672 | |||
673 | |||
674 | /** |
||
675 | * @return string |
||
676 | */ |
||
677 | 36 | protected function getPrimaryKeyValue() |
|
678 | { |
||
679 | 36 | $result = $this->owner->getPrimaryKey(true); |
|
680 | 36 | return reset($result); |
|
681 | } |
||
682 | |||
683 | /** |
||
684 | * @param bool $forInsertNear |
||
685 | * @throws Exception |
||
686 | */ |
||
687 | 126 | protected function checkNode($forInsertNear = false) |
|
688 | { |
||
689 | 126 | if ($forInsertNear && $this->node->isRoot()) { |
|
690 | 9 | throw new Exception('Can not move a node before/after root.'); |
|
691 | } |
||
692 | 117 | if ($this->node->getIsNewRecord()) { |
|
693 | 12 | throw new Exception('Can not move a node when the target node is new record.'); |
|
694 | } |
||
695 | |||
696 | 105 | if ($this->owner->equals($this->node)) { |
|
0 ignored issues
–
show
It seems like
$this->node can also be of type null or object<paulzi\materializ...terializedPathBehavior> ; however, yii\db\ActiveRecord::equals() does only seem to accept object<yii\db\ActiveRecord> , maybe add an additional type check?
If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check: /**
* @return array|string
*/
function returnsDifferentValues($x) {
if ($x) {
return 'foo';
}
return array();
}
$x = returnsDifferentValues($y);
if (is_array($x)) {
// $x is an array.
}
If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue. ![]() |
|||
697 | 12 | throw new Exception('Can not move a node when the target node is same.'); |
|
698 | } |
||
699 | |||
700 | 93 | if ($this->node->isChildOf($this->owner)) { |
|
701 | 12 | throw new Exception('Can not move a node when the target node is child.'); |
|
702 | } |
||
703 | 81 | } |
|
704 | |||
705 | /** |
||
706 | * Make root operation internal handler |
||
707 | */ |
||
708 | 6 | protected function makeRootInternal() |
|
709 | { |
||
710 | 6 | $item = $this->owner->getAttribute($this->itemAttribute); |
|
711 | |||
712 | 6 | if ($item !== null) { |
|
713 | 6 | $this->owner->setAttribute($this->pathAttribute, $item); |
|
714 | } |
||
715 | |||
716 | 6 | if ($this->sortable !== false) { |
|
717 | 6 | $this->owner->setAttribute($this->behavior->sortAttribute, 0); |
|
718 | } |
||
719 | |||
720 | 6 | if ($this->treeAttribute !== null && !$this->owner->getDirtyAttributes([$this->treeAttribute]) && !$this->owner->getIsNewRecord()) { |
|
721 | 3 | $this->owner->setAttribute($this->treeAttribute, $this->getPrimaryKeyValue()); |
|
722 | } |
||
723 | |||
724 | 6 | $this->owner->setAttribute($this->depthAttribute, $this->rootDepthValue); |
|
725 | 6 | } |
|
726 | |||
727 | /** |
||
728 | * Append to operation internal handler |
||
729 | * @param bool $append |
||
730 | * @throws Exception |
||
731 | */ |
||
732 | 66 | View Code Duplication | protected function insertIntoInternal($append) |
733 | { |
||
734 | 66 | $this->checkNode(false); |
|
735 | 42 | $item = $this->owner->getAttribute($this->itemAttribute); |
|
736 | |||
737 | 42 | if ($item !== null) { |
|
738 | 42 | $path = $this->node->getAttribute($this->pathAttribute); |
|
739 | 42 | $this->owner->setAttribute($this->pathAttribute, $path . $this->delimiter . $item); |
|
740 | } |
||
741 | |||
742 | 42 | $this->owner->setAttribute($this->depthAttribute, $this->node->getAttribute($this->depthAttribute) + 1); |
|
743 | |||
744 | 42 | if ($this->treeAttribute !== null) { |
|
745 | 42 | $this->owner->setAttribute($this->treeAttribute, $this->node->getAttribute($this->treeAttribute)); |
|
746 | } |
||
747 | |||
748 | 42 | if ($this->sortable !== false) { |
|
749 | 42 | if ($append) { |
|
750 | 21 | $this->behavior->moveLast(); |
|
751 | } else { |
||
752 | 21 | $this->behavior->moveFirst(); |
|
753 | } |
||
754 | } |
||
755 | 42 | } |
|
756 | |||
757 | /** |
||
758 | * Insert operation internal handler |
||
759 | * @param bool $forward |
||
760 | * @throws Exception |
||
761 | */ |
||
762 | 60 | View Code Duplication | protected function insertNearInternal($forward) |
763 | { |
||
764 | 60 | $this->checkNode(true); |
|
765 | 39 | $item = $this->owner->getAttribute($this->itemAttribute); |
|
766 | |||
767 | 39 | if ($item !== null) { |
|
768 | 39 | $path = $this->node->getParentPath(); |
|
769 | 39 | $this->owner->setAttribute($this->pathAttribute, $path . $this->delimiter . $item); |
|
770 | } |
||
771 | |||
772 | 39 | $this->owner->setAttribute($this->depthAttribute, $this->node->getAttribute($this->depthAttribute)); |
|
773 | |||
774 | 39 | if ($this->treeAttribute !== null) { |
|
775 | 39 | $this->owner->setAttribute($this->treeAttribute, $this->node->getAttribute($this->treeAttribute)); |
|
776 | } |
||
777 | |||
778 | 39 | if ($this->sortable !== false) { |
|
779 | 39 | if ($forward) { |
|
780 | 18 | $this->behavior->moveAfter($this->node); |
|
0 ignored issues
–
show
It seems like
$this->node can also be of type null or object<paulzi\materializ...terializedPathBehavior> ; however, paulzi\sortable\SortableBehavior::moveAfter() does only seem to accept object<yii\db\ActiveRecord> , maybe add an additional type check?
If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check: /**
* @return array|string
*/
function returnsDifferentValues($x) {
if ($x) {
return 'foo';
}
return array();
}
$x = returnsDifferentValues($y);
if (is_array($x)) {
// $x is an array.
}
If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue. ![]() |
|||
781 | } else { |
||
782 | 21 | $this->behavior->moveBefore($this->node); |
|
0 ignored issues
–
show
It seems like
$this->node can also be of type null or object<paulzi\materializ...terializedPathBehavior> ; however, paulzi\sortable\SortableBehavior::moveBefore() does only seem to accept object<yii\db\ActiveRecord> , maybe add an additional type check?
If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check: /**
* @return array|string
*/
function returnsDifferentValues($x) {
if ($x) {
return 'foo';
}
return array();
}
$x = returnsDifferentValues($y);
if (is_array($x)) {
// $x is an array.
}
If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue. ![]() |
|||
783 | } |
||
784 | } |
||
785 | 39 | } |
|
786 | |||
787 | /** |
||
788 | * @return int |
||
789 | */ |
||
790 | 9 | protected function deleteWithChildrenInternal() |
|
791 | { |
||
792 | 9 | if (!$this->owner->beforeDelete()) { |
|
793 | return false; |
||
794 | } |
||
795 | 6 | $result = $this->owner->deleteAll($this->getDescendants(null, true)->where); |
|
796 | 6 | $this->owner->setOldAttributes(null); |
|
797 | 6 | $this->owner->afterDelete(); |
|
798 | 6 | return $result; |
|
799 | } |
||
800 | |||
801 | /** |
||
802 | * @param array $changedAttributes |
||
803 | * @throws Exception |
||
804 | */ |
||
805 | 57 | protected function moveNode($changedAttributes) |
|
806 | { |
||
807 | 57 | $path = isset($changedAttributes[$this->pathAttribute]) ? $changedAttributes[$this->pathAttribute] : $this->owner->getAttribute($this->pathAttribute); |
|
808 | 57 | $update = []; |
|
809 | $condition = [ |
||
810 | 57 | 'and', |
|
811 | 57 | ['like', "[[{$this->pathAttribute}]]", $this->getLike($path), false], |
|
812 | ]; |
||
813 | 57 | if ($this->treeAttribute !== null) { |
|
814 | 54 | $tree = isset($changedAttributes[$this->treeAttribute]) ? $changedAttributes[$this->treeAttribute] : $this->owner->getAttribute($this->treeAttribute); |
|
815 | 54 | $condition[] = [$this->treeAttribute => $tree]; |
|
816 | } |
||
817 | 57 | $params = []; |
|
818 | |||
819 | 57 | if (isset($changedAttributes[$this->pathAttribute])) { |
|
820 | 30 | $substringExpr = $this->substringExpression( |
|
821 | 30 | "[[{$this->pathAttribute}]]", |
|
822 | 30 | 'LENGTH(:pathOld) + 1', |
|
823 | 30 | "LENGTH([[{$this->pathAttribute}]]) - LENGTH(:pathOld)" |
|
824 | ); |
||
825 | 30 | $update[$this->pathAttribute] = new Expression($this->concatExpression([':pathNew', $substringExpr])); |
|
826 | 30 | $params[':pathOld'] = $path; |
|
827 | 30 | $params[':pathNew'] = $this->owner->getAttribute($this->pathAttribute); |
|
828 | } |
||
829 | |||
830 | 57 | if ($this->treeAttribute !== null && isset($changedAttributes[$this->treeAttribute])) { |
|
831 | 15 | $update[$this->treeAttribute] = $this->owner->getAttribute($this->treeAttribute); |
|
832 | } |
||
833 | |||
834 | 57 | if ($this->depthAttribute !== null && isset($changedAttributes[$this->depthAttribute])) { |
|
835 | 30 | $delta = $this->owner->getAttribute($this->depthAttribute) - $changedAttributes[$this->depthAttribute]; |
|
836 | 30 | $update[$this->depthAttribute] = new Expression("[[{$this->depthAttribute}]]" . sprintf('%+d', $delta)); |
|
837 | } |
||
838 | |||
839 | 57 | if (!empty($update)) { |
|
840 | 30 | $this->owner->updateAll($update, $condition, $params); |
|
841 | } |
||
842 | 57 | } |
|
843 | |||
844 | /** |
||
845 | * @param string $path |
||
846 | * @param string $delimiter |
||
847 | * @param bool $asArray = false |
||
848 | * @return null|string|array |
||
849 | */ |
||
850 | 66 | protected static function getParentPathInternal($path, $delimiter, $asArray = false) |
|
851 | { |
||
852 | 66 | $path = explode($delimiter, $path); |
|
853 | 66 | array_pop($path); |
|
854 | 66 | if ($asArray) { |
|
855 | 6 | return $path; |
|
856 | } |
||
857 | 63 | return count($path) > 0 ? implode($delimiter, $path) : null; |
|
858 | } |
||
859 | |||
860 | /** |
||
861 | * @return array |
||
862 | */ |
||
863 | 123 | protected function treeCondition() |
|
864 | { |
||
865 | 123 | $tableName = $this->owner->tableName(); |
|
866 | 123 | if ($this->treeAttribute === null) { |
|
867 | 123 | return []; |
|
868 | } else { |
||
869 | 123 | return ["{$tableName}.[[{$this->treeAttribute}]]" => $this->owner->getAttribute($this->treeAttribute)]; |
|
870 | } |
||
871 | } |
||
872 | |||
873 | /** |
||
874 | * @return \yii\db\ActiveQuery |
||
875 | */ |
||
876 | 87 | protected function getSortableQuery() |
|
877 | { |
||
878 | 87 | switch ($this->operation) { |
|
879 | 87 | case self::OPERATION_PREPEND_TO: |
|
880 | 66 | case self::OPERATION_APPEND_TO: |
|
881 | 42 | $path = $this->node->getAttribute($this->pathAttribute); |
|
882 | 42 | $depth = $this->node->getAttribute($this->depthAttribute) + 1; |
|
883 | 42 | break; |
|
884 | |||
885 | 45 | case self::OPERATION_INSERT_BEFORE: |
|
886 | 24 | case self::OPERATION_INSERT_AFTER: |
|
887 | 39 | $path = $this->node->getParentPath(); |
|
888 | 39 | $depth = $this->node->getAttribute($this->depthAttribute); |
|
889 | 39 | break; |
|
890 | |||
891 | default: |
||
892 | 6 | $path = $this->getParentPath(); |
|
893 | 6 | $depth = $this->owner->getAttribute($this->depthAttribute); |
|
894 | } |
||
895 | 87 | $tableName = $this->owner->tableName(); |
|
896 | |||
897 | 87 | return $this->owner->find() |
|
898 | 87 | ->andWhere($this->treeCondition()) |
|
899 | 87 | ->andWhere($path !== null ? ['like', "{$tableName}.[[{$this->pathAttribute}]]", $this->getLike($path), false] : '1=0') |
|
900 | 87 | ->andWhere(["{$tableName}.[[{$this->depthAttribute}]]" => $depth]); |
|
901 | } |
||
902 | |||
903 | /** |
||
904 | * @param string $path |
||
905 | * @return string |
||
906 | */ |
||
907 | 117 | protected function getLike($path) |
|
908 | { |
||
909 | 117 | return strtr($path . $this->delimiter, ['%' => '\%', '_' => '\_', '\\' => '\\\\']) . '%'; |
|
910 | } |
||
911 | |||
912 | /** |
||
913 | * @param array $items |
||
914 | * @return string |
||
915 | */ |
||
916 | 33 | protected function concatExpression($items) |
|
917 | { |
||
918 | 33 | if ($this->owner->getDb()->driverName === 'sqlite' || $this->owner->getDb()->driverName === 'pgsql') { |
|
919 | 22 | return implode(' || ', $items); |
|
920 | } |
||
921 | 11 | return 'CONCAT(' . implode(',', $items) . ')'; |
|
922 | } |
||
923 | |||
924 | 30 | protected function substringExpression($string, $from, $length) |
|
925 | { |
||
926 | 30 | if ($this->owner->getDb()->driverName === 'sqlite') { |
|
927 | 10 | return "SUBSTR({$string}, {$from}, {$length})"; |
|
928 | } |
||
929 | 20 | return "SUBSTRING({$string}, {$from}, {$length})"; |
|
930 | } |
||
931 | } |
||
932 |
This check looks at variables that are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.