NestedSet::checkAndSetPath()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.2
c 0
b 0
f 0
cc 4
eloc 5
nc 3
nop 4
1
<?php
2
/**
3
 * This file is part of the fangface/yii2-concord package
4
 *
5
 * For the full copyright and license information, please view
6
 * the file LICENSE.md that was distributed with this source code.
7
 *
8
 * @package fangface/yii2-concord
9
 * @author Fangface <[email protected]>
10
 * @copyright Copyright (c) 2014 Fangface <[email protected]>
11
 * @license https://github.com/fangface/yii2-concord/blob/master/LICENSE.md MIT License
12
 *
13
 */
14
15
/**
16
 * Based on;
17
 * @link https://github.com/creocoder/yii2-nested-set-behavior
18
 * @copyright Copyright (c) 2013 Alexander Kochetov
19
 * @license http://opensource.org/licenses/BSD-3-Clause
20
 */
21
22
namespace fangface\behaviors;
23
24
use fangface\Tools;
25
use fangface\db\ActiveRecord;
26
use yii\base\Behavior;
27
use yii\base\ModelEvent;
28
use yii\db\ActiveQuery;
29
use yii\db\Exception;
30
use yii\db\Expression;
31
32
/**
33
 * Nested Set behavior for attaching to ActiveRecord
34
 * CREATE TABLE `example_nest` (
35
 *   `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
36
 *   `name` char(100) NOT NULL DEFAULT '',
37
 *   `path` varchar(255) NOT NULL DEFAULT '',
38
 *   `lft` bigint(20) unsigned NOT NULL DEFAULT '0',
39
 *   `rgt` bigint(20) unsigned NOT NULL DEFAULT '0',
40
 *   `level` smallint(5) unsigned NOT NULL DEFAULT '0',
41
 *   `created_at` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
42
 *   `created_by` bigint(20) unsigned NOT NULL DEFAULT '0',
43
 *   `modified_at` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
44
 *   `modified_by` bigint(20) unsigned NOT NULL DEFAULT '0',
45
 *   PRIMARY KEY (`id`),
46
 *   KEY `lft` (`lft`),
47
 *   KEY `rgt` (`rgt`),
48
 *   KEY `level` (`level`,`lft`) USING BTREE
49
 * ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
50
 *
51
 * @author Alexander Kochetov <[email protected]>
52
 * @author Fangface <[email protected]>
53
 */
54
class NestedSet extends Behavior
55
{
56
    /**
57
	 * @var ActiveRecord the owner of this behavior.
58
	 */
59
	public $owner;
60
	/**
61
	 * @var boolean
62
	 */
63
	public $hasManyRoots = false;
64
	/**
65
	 * @var boolean
66
	 */
67
	public $hasPaths = false;
68
	/**
69
	 * @var boolean
70
	 */
71
	public $hasAction = false;
72
	/**
73
	 * @var boolean should deletes be performed on each active record individually
74
	 */
75
	public $deleteIndividual = false;
76
	/**
77
	 * @var string
78
	 */
79
	public $rootAttribute = 'root';
80
	/**
81
	 * @var string
82
	 */
83
	public $leftAttribute = 'lft';
84
	/**
85
	 * @var string
86
	 */
87
	public $rightAttribute = 'rgt';
88
	/**
89
	 * @var string
90
	 */
91
	public $levelAttribute = 'level';
92
	/**
93
	 * @var string
94
	 */
95
	public $nameAttribute = 'name';
96
	/**
97
	 * @var string
98
	 */
99
	public $pathAttribute = 'path';
100
	/**
101
	 * @var boolean
102
	 */
103
	private $_ignoreEvent = false;
104
	/**
105
	 * @var boolean
106
	 */
107
	private $_deleted = false;
108
	/**
109
	 * @var string
110
	 */
111
	private $_previousPath = '';
112
	/**
113
	 * @var integer
114
	 */
115
	private $_id;
116
	/**
117
	 * @var array
118
	 */
119
	private static $_cached;
120
	/**
121
	 * @var integer
122
	 */
123
	private static $_c = 0;
124
125
	/**
126
	 * @inheritdoc
127
	 */
128
	public function events()
129
	{
130
		return [
131
			ActiveRecord::EVENT_AFTER_FIND => 'afterFind',
132
			ActiveRecord::EVENT_BEFORE_DELETE => 'beforeDelete',
133
			ActiveRecord::EVENT_BEFORE_INSERT => 'beforeInsert',
134
			ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeUpdate',
135
		    ActiveRecord::EVENT_BEFORE_SAVE_ALL => 'beforeSaveAll',
136
		    ActiveRecord::EVENT_BEFORE_DELETE_FULL => 'beforeDeleteFull',
137
		];
138
	}
139
140
	/**
141
	 * @inheritdoc
142
	 */
143
	public function attach($owner)
144
	{
145
		parent::attach($owner);
146
		self::$_cached[get_class($this->owner)][$this->_id = self::$_c++] = $this->owner;
147
	}
148
149
	/**
150
	 * Gets descendants for node
151
	 * @param integer $depth the depth
152
	 * @param ActiveRecord $object [optional] defaults to $this->owner
153
	 * @param integer $limit [optional] limit results (typically used when only after limited number of immediate children)
154
	 * @return ActiveQuery|integer
155
	 */
156
	public function descendants($depth = null, $object = null, $limit = 0)
157
	{
158
	    $object = (!is_null($object) ? $object : $this->owner);
159
	    $query = $object->find()->orderBy([$this->levelAttribute => SORT_ASC, $this->leftAttribute => SORT_ASC]);
160
		$db = $object->getDb();
161
		$query->andWhere($db->quoteColumnName($this->leftAttribute) . '>'
162
			. $object->getAttribute($this->leftAttribute));
163
		$query->andWhere($db->quoteColumnName($this->rightAttribute) . '<'
164
			. $object->getAttribute($this->rightAttribute));
165
		$query->addOrderBy($db->quoteColumnName($this->leftAttribute));
166
167
		if ($depth !== null) {
168
			$query->andWhere($db->quoteColumnName($this->levelAttribute) . '<='
169
				. ($object->getAttribute($this->levelAttribute) + $depth));
170
		}
171
172
		if ($this->hasManyRoots) {
173
			$query->andWhere(
174
				$db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute,
175
				[':' . $this->rootAttribute => $object->getAttribute($this->rootAttribute)]
0 ignored issues
show
Unused Code introduced by
The call to ActiveQueryInterface::andWhere() has too many arguments starting with array(':' . $this->rootA...($this->rootAttribute)).

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...
176
			);
177
		}
178
179
		if ($limit) {
180
		    $query->limit($limit);
181
		}
182
183
		return $query;
184
	}
185
186
	/**
187
	 * Gets children for node (direct descendants only)
188
	 * @param ActiveRecord $object [optional] defaults to $this->owner
189
	 * @param integer $limit [optional] limit results (typically used when only after limited number of immediate children)
190
	 * @return ActiveQuery|integer
191
	 */
192
	public function children($object = null, $limit = 0)
193
	{
194
       return $this->descendants(1, $object, $limit);
195
	}
196
197
	/**
198
	 * Gets one child for node (first direct descendant only).
199
	 * @param ActiveRecord $object [optional] defaults to $this->owner
200
	 * @return ActiveQuery
201
	 */
202
	public function oneChild($object = null)
203
	{
204
	    return $this->children($object, 1);
205
	}
206
207
	/**
208
	 * Gets ancestors for node
209
	 * @param integer $depth the depth
210
	 * @param ActiveRecord $object [optional] defaults to $this->owner
211
	 * @param boolean $reverse Should the result be in reverse order i.e. root first
212
	 * @param boolean $idOnly Should an array of IDs be returned only
213
	 * @return ActiveQuery
214
	 */
215
	public function ancestors($depth = null, $object = null, $reverse = false, $idOnly = false)
216
	{
217
	    $object = (!is_null($object) ? $object : $this->owner);
218
	    $query = $object->find();
219
220
	    if ($idOnly) {
221
	        $query->select($object->primaryKey());
222
	    }
223
224
        if ($reverse) {
225
            $query->orderBy([$this->levelAttribute => SORT_ASC, $this->leftAttribute => SORT_ASC]);;
226
        } else {
227
            $query->orderBy([$this->levelAttribute => SORT_DESC, $this->leftAttribute => SORT_ASC]);;
228
        }
229
230
        $db = $object->getDb();
231
232
		$query->andWhere($db->quoteColumnName($this->leftAttribute) . '<'
233
			. $object->getAttribute($this->leftAttribute));
234
		$query->andWhere($db->quoteColumnName($this->rightAttribute) . '>'
235
			. $object->getAttribute($this->rightAttribute));
236
		$query->addOrderBy($db->quoteColumnName($this->leftAttribute));
237
238
		if ($depth !== null) {
239
			$query->andWhere($db->quoteColumnName($this->levelAttribute) . '>='
240
				. ($object->getAttribute($this->levelAttribute) - $depth));
241
		}
242
243
		if ($this->hasManyRoots) {
244
			$query->andWhere(
245
				$db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute,
246
				[':' . $this->rootAttribute => $object->getAttribute($this->rootAttribute)]
0 ignored issues
show
Unused Code introduced by
The call to ActiveQueryInterface::andWhere() has too many arguments starting with array(':' . $this->rootA...($this->rootAttribute)).

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...
247
			);
248
		}
249
250
        return $query;
251
	}
252
253
	/**
254
	 * Gets parent of node
255
	 * @param ActiveRecord $object [optional] defaults to $this->owner
256
	 * @param boolean $idOnly Should only the id be returned
257
	 * @return ActiveQuery
258
	 */
259
	public function parentOnly($object = null, $idOnly = false)
260
	{
261
		return $this->ancestors(1, $object, false, $idOnly);
262
	}
263
264
    /**
265
	 * Gets entries at the same level of node (including self)
266
	 * @param ActiveRecord $object [optional] defaults to $this->owner
267
	 * @param integer $limit [optional] limit results (typically used when only after limited number of immediate children)
268
	 * @return ActiveQuery|integer
269
	 */
270
	public function level($object = null, $limit = 0)
271
	{
272
	    $parent = $this->parentOnly($object)->one();
273
	    return $this->children($parent, $limit);
0 ignored issues
show
Bug introduced by
It seems like $parent defined by $this->parentOnly($object)->one() on line 272 can also be of type array or object<yii\db\ActiveRecord>; however, fangface\behaviors\NestedSet::children() does only seem to accept object<fangface\db\ActiveRecord>|null, 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.

Loading history...
274
	}
275
276
    /**
277
	 * Gets a count of entries at the same level of node (including self)
278
	 * @param ActiveRecord $object [optional] defaults to $this->owner
279
	 * @return integer
280
	 */
281
	public function levelCount($object = null)
282
	{
283
	    return $this->level($object, true)->count();
284
	}
285
286
	/**
287
	 * Gets previous sibling of node
288
	 * @param ActiveRecord $object [optional] defaults to $this->owner
289
	 * @return ActiveQuery
290
	 */
291
	public function prev($object = null)
292
	{
293
	    $object = (!is_null($object) ? $object : $this->owner);
294
	    $query = $object->find();
295
		$db = $object->getDb();
296
		$query->andWhere($db->quoteColumnName($this->rightAttribute) . '='
297
			. ($object->getAttribute($this->leftAttribute) - 1));
298
299
		if ($this->hasManyRoots) {
300
			$query->andWhere(
301
				$db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute,
302
				[':' . $this->rootAttribute => $object->getAttribute($this->rootAttribute)]
0 ignored issues
show
Unused Code introduced by
The call to ActiveQueryInterface::andWhere() has too many arguments starting with array(':' . $this->rootA...($this->rootAttribute)).

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...
303
			);
304
		}
305
306
		return $query;
307
	}
308
309
	/**
310
	 * Gets next sibling of node
311
	 * @param ActiveRecord $object [optional] defaults to $this->owner
312
	 * @return ActiveQuery
313
	 */
314
	public function next($object = null)
315
	{
316
	    $object = (!is_null($object) ? $object : $this->owner);
317
	    $query = $object->find();
318
		$db = $object->getDb();
319
		$query->andWhere($db->quoteColumnName($this->leftAttribute) . '='
320
			. ($object->getAttribute($this->rightAttribute) + 1));
321
322
		if ($this->hasManyRoots) {
323
			$query->andWhere(
324
				$db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute,
325
				[':' . $this->rootAttribute => $object->getAttribute($this->rootAttribute)]
0 ignored issues
show
Unused Code introduced by
The call to ActiveQueryInterface::andWhere() has too many arguments starting with array(':' . $this->rootA...($this->rootAttribute)).

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...
326
			);
327
		}
328
329
		return $query;
330
	}
331
332
	/**
333
	 * Create root node if multiple-root tree mode. Update node if it's not new
334
	 *
335
     * @param boolean $runValidation
336
     *        should validations be executed on all models before allowing save()
337
     * @param array $attributes
338
     *        which attributes should be saved (default null means all changed attributes)
339
     * @param boolean $hasParentModel
340
     *        whether this method was called from the top level or by a parent
341
     *        If false, it means the method was called at the top level
342
     * @param boolean $fromSaveAll
343
     *        has the save() call come from saveAll() or not
344
     * @return boolean
345
     *        did save() successfully process
346
	 */
347
	private function save($runValidation = true, $attributes = null, $hasParentModel = false, $fromSaveAll = false)
348
	{
349
350
	    if ($this->owner->getReadOnly() && !$hasParentModel) {
351
352
	        // return failure if we are at the top of the tree and should not be asking to saveAll
353
	        // not allowed to amend or delete
354
	        $message = 'Attempting to save on ' . Tools::getClassName($this->owner) . ' readOnly model';
355
	        //$this->addActionError($message);
356
	        throw new \fangface\db\Exception($message);
357
358
	    } elseif ($this->owner->getReadOnly() && $hasParentModel) {
359
360
	        $message = 'Skipping save on ' . Tools::getClassName($this->owner) . ' readOnly model';
361
	        $this->addActionWarning($message);
362
	        return true;
363
364
	    } else {
365
366
	        if ($runValidation && !$this->owner->validate($attributes)) {
367
                return false;
368
            }
369
370
    		if ($this->owner->getIsNewRecord()) {
371
    			return $this->makeRoot($attributes);
0 ignored issues
show
Bug introduced by
It seems like $attributes defined by parameter $attributes on line 347 can also be of type null; however, fangface\behaviors\NestedSet::makeRoot() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and 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.

Loading history...
372
    		}
373
374
    		$updateChildPaths = false;
375
            if ($this->hasPaths && !$this->owner->getIsNewRecord()) {
376
                if ($this->owner->hasAttribute($this->pathAttribute)) {
377
                    if ($this->owner->hasChanged($this->pathAttribute)) {
378
                        $updateChildPaths = true;
379
                        if ($this->_previousPath == '') {
380
                            $this->_previousPath = $this->owner->getOldAttribute($this->pathAttribute);
381
                        }
382
                    }
383
                }
384
                if (!$updateChildPaths && $this->owner->hasAttribute($this->nameAttribute)) {
385
                    if ($this->owner->hasChanged($this->nameAttribute)) {
386
                        $this->_previousPath = $this->owner->getAttribute($this->pathAttribute);
387
                        $this->checkAndSetPath($this->owner);
388
                        if ($this->_previousPath != $this->owner->getAttribute($this->pathAttribute)) {
389
                            $updateChildPaths = true;
390
                        }
391
                    }
392
                }
393
            }
394
395
            $nameChanged = false;
396
            if ($this->owner->hasAttribute($this->nameAttribute) && $this->owner->hasChanged($this->nameAttribute)) {
397
                $nameChanged = true;
398
                if (!$this->beforeRenameNode($this->_previousPath)) {
399
                    return false;
400
                }
401
            }
402
403
            $result = false;
404
            $db = $this->owner->getDb();
405
406
    		if ($db->getTransaction() === null) {
407
    			$transaction = $db->beginTransaction();
408
    		}
409
410
    		try {
411
412
                $this->_ignoreEvent = true;
413
    			//$result = $this->owner->update(false, $attributes);
414
                if (false && method_exists($this->owner, 'saveAll')) {
415
                    $result = $this->owner->saveAll(false, $hasParentModel, false, $attributes);
416
                } else {
417
                    $result = $this->owner->save(false, $attributes, $hasParentModel, $fromSaveAll);
418
                }
419
        		$this->_ignoreEvent = false;
420
421
                if ($result && $updateChildPaths) {
422
                    // only if we have children
423
                    if ($this->owner->getAttribute($this->rightAttribute) > $this->owner->getAttribute($this->leftAttribute) + 1) {
424
                        $condition = $db->quoteColumnName($this->leftAttribute) . '>' . $this->owner->getAttribute($this->leftAttribute) . ' AND '
425
        					. $db->quoteColumnName($this->rightAttribute) . '<' . $this->owner->getAttribute($this->rightAttribute);
426
    				    $params = [];
427
    				    if ($this->hasManyRoots) {
428
    					   $condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute;
429
    					   $params[':' . $this->rootAttribute] = $this->owner->getAttribute($this->rootAttribute);
430
    				    }
431
432
                        $updateColumns = [];
433
                        $pathLength = Tools::strlen($this->_previousPath) + 1;
434
                        // SQL Server: SUBSTRING() rather than SUBSTR
435
                        // SQL Server: + instead of CONCAT
436
                        if ($db->getDriverName() == 'mssql') {
437
                            $updateColumns[$this->pathAttribute] = new Expression($db->quoteValue($this->owner->getAttribute($this->pathAttribute)) . ' + SUBSTRING(' . $db->quoteColumnName($this->pathAttribute) . ', ' . $pathLength . '))');
438
                        } else {
439
                            $updateColumns[$this->pathAttribute] = new Expression('CONCAT(' . $db->quoteValue($this->owner->getAttribute($this->pathAttribute)) . ', SUBSTR(' . $db->quoteColumnName($this->pathAttribute) . ', ' . $pathLength . '))');
440
                        }
441
    				    $result = $this->owner->updateAll(
442
    					   $updateColumns,
443
    					   $condition,
444
    					   $params
445
    				    );
446
                    }
447
                }
448
449
	            if ($result && $nameChanged) {
450
                    $result = $this->afterRenameNode($this->_previousPath);
451
	            }
452
453
    		} catch (\Exception $e) {
454
    			if (isset($transaction)) {
455
    				$transaction->rollback();
456
    			}
457
    			throw $e;
458
    		}
459
460
            if (isset($transaction)) {
461
                if (!$result) {
462
                    $transaction->rollback();
463
                } else {
464
                    $transaction->commit();
465
                }
466
			}
467
468
    		$this->_previousPath = '';
469
470
	    }
471
472
		return $result;
473
	}
474
475
	/**
476
	 * Create root node if multiple-root tree mode. Update node if it's not new
477
	 * @param boolean $runValidation whether to perform validation
478
	 * @param array $attributes list of attributes
479
	 * @return boolean whether the saving succeeds
480
	 */
481
	public function saveNode($runValidation = true, $attributes = null)
482
	{
483
		return $this->save($runValidation, $attributes);
484
	}
485
486
	/**
487
	 * Deletes node and it's descendants
488
	 * @throws Exception.
489
	 * @throws \Exception.
490
     * @param boolean $hasParentModel
491
     *        whether this method was called from the top level or by a parent
492
     *        If false, it means the method was called at the top level
493
     * @param boolean $fromDeleteFull
494
     *        has the delete() call come from deleteFull() or not
495
     * @return boolean
496
     *        did delete() successfully process
497
	 */
498
	private function delete($hasParentModel = false, $fromDeleteFull = false)
499
	{
500
		if ($this->owner->getIsNewRecord()) {
501
			throw new Exception('The node can\'t be deleted because it is new.');
502
		}
503
504
		if ($this->getIsDeletedRecord()) {
505
			throw new Exception('The node can\'t be deleted because it is already deleted.');
506
		}
507
508
        if (!$this->beforeDeleteNode()) {
509
            return false;
510
        }
511
512
		$db = $this->owner->getDb();
513
514
		if ($db->getTransaction() === null) {
515
			$transaction = $db->beginTransaction();
516
		}
517
518
        if ($this->owner->hasAttribute($this->pathAttribute) && $this->owner->hasAttribute($this->nameAttribute)) {
519
            $this->_previousPath = $this->owner->getAttribute($this->pathAttribute);
520
        }
521
522
		try {
523
524
		    $result = true;
525
		    if (!$this->owner->isLeaf()) {
526
527
		        $condition = $db->quoteColumnName($this->leftAttribute) . '>='
528
		            . $this->owner->getAttribute($this->leftAttribute) . ' AND '
529
	                . $db->quoteColumnName($this->rightAttribute) . '<='
530
                    . $this->owner->getAttribute($this->rightAttribute);
531
532
		        if ($this->hasManyRoots) {
533
		            $condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=' . $this->owner->getAttribute($this->rootAttribute);
534
		        }
535
536
                if (!$this->deleteIndividual) {
537
538
                    $result = $this->owner->deleteAll($condition) > 0;
539
540
                } else {
541
542
                    $nodes = $this->owner->descendants()->all();
543
    		        foreach ($nodes as $node) {
544
545
    		            $node->setIgnoreEvents(true);
546
        		        if (method_exists($node, 'deleteFull')) {
547
                    	    $result = $node->deleteFull($hasParentModel);
548
                    	} else {
549
                    	    $result = $node->delete();
550
                    	}
551
    		            $node->setIgnoreEvents(false);
552
553
    		            if (method_exists($node, 'hasActionErrors')) {
554
    		                if ($node->hasActionErrors()) {
555
    		                    $this->owner->mergeActionErrors($node->getActionErrors());
556
    		                }
557
    		            }
558
559
    		            if (method_exists($node, 'hasActionWarnings')) {
560
    		                if ($node->hasActionWarnings()) {
561
    		                    $this->owner->mergeActionWarnings($node->getActionWarnings());
562
    		                }
563
    		            }
564
    		            if (!$result) {
565
    		                break;
566
    		            }
567
    		        }
568
                }
569
570
		    }
571
572
		    if ($result) {
573
574
		        $this->shiftLeftRight(
575
                    $this->owner->getAttribute($this->rightAttribute) + 1,
576
                    $this->owner->getAttribute($this->leftAttribute) - $this->owner->getAttribute($this->rightAttribute) - 1
577
                );
578
579
        		$left = $this->owner->getAttribute($this->leftAttribute);
580
                $right = $this->owner->getAttribute($this->rightAttribute);
581
582
            	$this->_ignoreEvent = true;
583
            	if (method_exists($this->owner, 'deleteFull')) {
584
            	    $result = $this->owner->deleteFull($hasParentModel);
585
            	} else {
586
            	    $result = $this->owner->delete();
587
            	}
588
            	$this->_ignoreEvent = false;
589
590
		        $this->correctCachedOnDelete($left, $right);
591
		    }
592
593
            if ($result) {
594
                $result = $this->afterDeleteNode($this->_previousPath);
595
            }
596
597
            $this->_previousPath = '';
598
599
			if (!$result) {
600
				if (isset($transaction)) {
601
					$transaction->rollback();
602
				}
603
				return false;
604
			}
605
606
			if (isset($transaction)) {
607
			    $transaction->commit();
608
			}
609
610
		} catch (\Exception $e) {
611
			if (isset($transaction)) {
612
				$transaction->rollback();
613
			}
614
615
			throw $e;
616
		}
617
        $this->_previousPath = '';
618
		return true;
619
	}
620
621
	/**
622
	 * Deletes node and it's descendants.
623
	 *
624
     * @param boolean $hasParentModel
625
     *        whether this method was called from the top level or by a parent
626
     *        If false, it means the method was called at the top level
627
     * @param boolean $fromDeleteFull
628
     *        has the delete() call come from deleteFull() or not
629
     * @return boolean
630
     *        did deleteNode() successfully process
631
632
	 */
633
	public function deleteNode($hasParentModel = false, $fromDeleteFull = false)
634
	{
635
		return $this->delete($hasParentModel, $fromDeleteFull);
636
	}
637
638
	/**
639
	 * Prepends node to target as first child
640
	 * @param ActiveRecord $target the target
641
	 * @param boolean $runValidation [optional] whether to perform validation
642
	 * @param array $attributes [optional] list of attributes
643
	 * @return boolean whether the prepending succeeds
644
	 */
645
	public function prependTo($target, $runValidation = true, $attributes = null)
646
	{
647
        if ($runValidation) {
648
            if (!$this->owner->validate($attributes)) {
649
                return false;
650
            }
651
            $runValidation = false;
652
        }
653
	    $this->checkAndSetPath($target, true);
654
		return $this->addNode(
655
			$target,
656
			$target->getAttribute($this->leftAttribute) + 1,
657
			1,
658
			$runValidation,
659
			$attributes
0 ignored issues
show
Bug introduced by
It seems like $attributes defined by parameter $attributes on line 645 can also be of type null; however, fangface\behaviors\NestedSet::addNode() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and 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.

Loading history...
660
		);
661
	}
662
663
	/**
664
	 * Prepends target to node as first child
665
	 * @param ActiveRecord $target the target
666
	 * @param boolean $runValidation [optional] whether to perform validation
667
	 * @param array $attributes [optional] list of attributes
668
	 * @return boolean whether the prepending succeeds
669
	 */
670
	public function prepend($target, $runValidation = true, $attributes = null)
671
	{
672
		return $target->prependTo(
673
			$this->owner,
674
			$runValidation,
675
			$attributes
676
		);
677
	}
678
679
	/**
680
	 * Appends node to target as last child
681
	 * @param ActiveRecord $target the target
682
	 * @param boolean $runValidation [optional] whether to perform validation
683
	 * @param array $attributes [optional] list of attributes
684
	 * @return boolean whether the appending succeeds
685
	 */
686
	public function appendTo($target, $runValidation = true, $attributes = null)
687
	{
688
        if ($runValidation) {
689
            if (!$this->owner->validate($attributes)) {
690
                return false;
691
            }
692
            $runValidation = false;
693
        }
694
	    $this->checkAndSetPath($target, true);
695
	    return $this->addNode(
696
			$target,
697
			$target->getAttribute($this->rightAttribute),
698
			1,
699
			$runValidation,
700
			$attributes
0 ignored issues
show
Bug introduced by
It seems like $attributes defined by parameter $attributes on line 686 can also be of type null; however, fangface\behaviors\NestedSet::addNode() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and 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.

Loading history...
701
		);
702
	}
703
704
	/**
705
	 * Appends target to node as last child
706
	 * @param ActiveRecord $target the target
707
	 * @param boolean $runValidation [optional] whether to perform validation
708
	 * @param array $attributes [optional] list of attributes
709
	 * @return boolean whether the appending succeeds
710
	 */
711
	public function append($target, $runValidation = true, $attributes = null)
712
	{
713
		return $target->appendTo(
714
			$this->owner,
715
			$runValidation,
716
			$attributes
717
		);
718
	}
719
720
	/**
721
	 * Inserts node as previous sibling of target.
722
	 * @param ActiveRecord $target the target.
723
	 * @param boolean $runValidation [optional] whether to perform validation
724
	 * @param array $attributes [optional] list of attributes
725
	 * @param ActiveRecord $parent [optional] parent node if already known
726
	 * @return boolean whether the inserting succeeds.
727
	 */
728
	public function insertBefore($target, $runValidation = true, $attributes = null, $parent = null)
729
	{
730
        if ($runValidation) {
731
            if (!$this->owner->validate($attributes)) {
732
                return false;
733
            }
734
            $runValidation = false;
735
        }
736
	    $this->checkAndSetPath($target, false, false, $parent);
737
	    return $this->addNode(
738
			$target,
739
			$target->getAttribute($this->leftAttribute),
740
			0,
741
			$runValidation,
742
			$attributes
0 ignored issues
show
Bug introduced by
It seems like $attributes defined by parameter $attributes on line 728 can also be of type null; however, fangface\behaviors\NestedSet::addNode() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and 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.

Loading history...
743
		);
744
	}
745
746
	/**
747
	 * Inserts node as next sibling of target
748
	 * @param ActiveRecord $target the target
749
	 * @param boolean $runValidation [optional] whether to perform validation
750
	 * @param array $attributes [optional] list of attributes
751
	 * @param ActiveRecord $parent [optional] parent node if already known
752
	 * @return boolean whether the inserting succeeds
753
	 */
754
	public function insertAfter($target, $runValidation = true, $attributes = null, $parent = null)
755
	{
756
        if ($runValidation) {
757
            if (!$this->owner->validate($attributes)) {
758
                return false;
759
            }
760
            $runValidation = false;
761
        }
762
	    $this->checkAndSetPath($target, false, false, $parent);
763
	    return $this->addNode(
764
			$target,
765
			$target->getAttribute($this->rightAttribute) + 1,
766
			0,
767
			$runValidation,
768
			$attributes
0 ignored issues
show
Bug introduced by
It seems like $attributes defined by parameter $attributes on line 754 can also be of type null; however, fangface\behaviors\NestedSet::addNode() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and 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.

Loading history...
769
		);
770
	}
771
772
	/**
773
	 * Move node as previous sibling of target
774
	 * @param ActiveRecord $target the target
775
	 * @param ActiveRecord $parent [optional] parent node if already known
776
	 * @return boolean whether the moving succeeds
777
	 */
778
	public function moveBefore($target, $parent = null)
779
	{
780
        $this->checkAndSetPath($target, false, true, $parent);
781
	    return $this->moveNode(
782
			$target,
783
			$target->getAttribute($this->leftAttribute),
784
			0
785
		);
786
	}
787
788
	/**
789
	 * Move node as next sibling of target
790
	 * @param ActiveRecord $target the target
791
	 * @param ActiveRecord $parent [optional] parent node if already known
792
	 * @return boolean whether the moving succeeds
793
	 */
794
	public function moveAfter($target, $parent = null)
795
	{
796
        $this->checkAndSetPath($target, false, true, $parent);
797
	    return $this->moveNode(
798
			$target,
799
			$target->getAttribute($this->rightAttribute) + 1,
800
			0
801
		);
802
	}
803
804
	/**
805
	 * Move node as first child of target
806
	 * @param ActiveRecord $target the target
807
	 * @param ActiveRecord $parent [optional] parent node if already known
808
	 * @return boolean whether the moving succeeds
809
	 */
810
	public function moveAsFirst($target, $parent = null)
811
	{
812
        $this->checkAndSetPath($target, true, true, $parent);
813
	    return $this->moveNode(
814
			$target,
815
			$target->getAttribute($this->leftAttribute) + 1,
816
			1
817
		);
818
	}
819
820
	/**
821
	 * Move node as last child of target
822
	 * @param ActiveRecord $target the target
823
	 * @param ActiveRecord $parent [optional] parent node if already known
824
	 * @return boolean whether the moving succeeds
825
	 */
826
	public function moveAsLast($target, $parent = null)
827
	{
828
        $this->checkAndSetPath($target, true, true, $parent);
829
	    return $this->moveNode(
830
			$target,
831
			$target->getAttribute($this->rightAttribute),
832
			1
833
		);
834
	}
835
836
	/**
837
	 * Move node as new root
838
	 * @throws Exception
839
	 * @return boolean whether the moving succeeds
840
	 */
841
	public function moveAsRoot()
842
	{
843
		if (!$this->hasManyRoots) {
844
			throw new Exception('Many roots mode is off.');
845
		}
846
847
		if ($this->owner->getIsNewRecord()) {
848
			throw new Exception('The node should not be new record.');
849
		}
850
851
		if ($this->getIsDeletedRecord()) {
852
			throw new Exception('The node should not be deleted.');
853
		}
854
855
		if ($this->owner->isRoot()) {
856
			throw new Exception('The node already is root node.');
857
		}
858
859
		if ($this->hasPaths) {
860
			throw new Exception('Paths not yet supported for moveAsRoot.');
861
		}
862
863
		$db = $this->owner->getDb();
864
865
		if ($db->getTransaction() === null) {
866
			$transaction = $db->beginTransaction();
867
		}
868
869
		try {
870
			$left = $this->owner->getAttribute($this->leftAttribute);
871
			$right = $this->owner->getAttribute($this->rightAttribute);
872
			$levelDelta = 1 - $this->owner->getAttribute($this->levelAttribute);
873
			$delta = 1 - $left;
874
			$this->owner->updateAll(
875
				[
876
					$this->leftAttribute => new Expression($db->quoteColumnName($this->leftAttribute)
877
						. sprintf('%+d', $delta)),
878
					$this->rightAttribute => new Expression($db->quoteColumnName($this->rightAttribute)
879
						. sprintf('%+d', $delta)),
880
					$this->levelAttribute => new Expression($db->quoteColumnName($this->levelAttribute)
881
						. sprintf('%+d', $levelDelta)),
882
					$this->rootAttribute => $this->owner->getPrimaryKey(),
883
				],
884
				$db->quoteColumnName($this->leftAttribute) . '>=' . $left . ' AND '
885
					. $db->quoteColumnName($this->rightAttribute) . '<=' . $right . ' AND '
886
					. $db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute,
887
				[':' . $this->rootAttribute => $this->owner->getAttribute($this->rootAttribute)]
888
			);
889
			$this->shiftLeftRight($right + 1, $left - $right - 1);
890
891
			if (isset($transaction)) {
892
				$transaction->commit();
893
			}
894
895
			$this->correctCachedOnMoveBetweenTrees(1, $levelDelta, $this->owner->getPrimaryKey());
896
		} catch (\Exception $e) {
897
			if (isset($transaction)) {
898
				$transaction->rollback();
899
			}
900
901
			throw $e;
902
		}
903
904
		return true;
905
	}
906
907
    /**
908
     * Check to see if this nested set supports path or not
909
	 * @param ActiveRecord $target the target
910
	 * @param boolean $isParent is target the parent node
911
	 * @param boolean $isMove is this relating to a node move
912
	 * @param boolean $isParent is $target the parent of $this->owner
913
	 * @param ActiveRecord $parent [optional] parent node if already known
914
     */
915
	public function checkAndSetPath($target, $isParent = false, $isMove = false, $parent = null)
916
	{
917
        if ($this->hasPaths) {
918
            if ($this->owner->hasAttribute($this->pathAttribute) && $this->owner->hasAttribute($this->nameAttribute)) {
919
                $this->_previousPath = $this->owner->getAttribute($this->pathAttribute);
920
                $this->owner->setAttribute($this->pathAttribute, $this->calculatePath($target, $isParent, $parent));
921
            }
922
        }
923
	}
924
925
    /**
926
     * Calculate path based on name and target
927
	 * @param ActiveRecord $target the target
928
	 * @param boolean $isParent is target the parent node
929
	 * @param ActiveRecord $parent [optional] parent node if already known
930
	 * @return string
931
     */
932
	public function calculatePath($target, $isParent = false, $parent = null)
933
	{
934
        $uniqueNames = false;
935
	    if (method_exists($this->owner, 'getIsUniqueNames')) {
936
            $uniqueNames = $this->owner->getIsUniqueNames();
937
        }
938
939
	    if ($this->hasPaths || $uniqueNames) {
940
            if (!$isParent && $parent) {
941
                $target = $parent;
942
            } elseif (!$isParent) {
943
                $target = $target->parentOnly()->one();
944
    	    }
945
	    }
946
        if ($this->hasPaths) {
947
            if ($target->getAttribute($this->pathAttribute) == '/') {
948
                $path = '/' . $this->owner->getAttribute($this->nameAttribute);
949
            } else {
950
                $path = $target->getAttribute($this->pathAttribute) . '/' . $this->owner->getAttribute($this->nameAttribute);
951
            }
952
        } else {
953
            $path = '';
954
        }
955
        if ($uniqueNames) {
956
            $matches = $this->children($target)->andWhere([$this->nameAttribute => $this->owner->getAttribute($this->nameAttribute)]);
957
            if (!$this->owner->getIsNewRecord()) {
958
                $matches->andWhere('id != ' . $this->owner->id);
959
            }
960
            if ($matches->count()) {
961
                $path = '__DUPLICATE__';
962
            }
963
        }
964
        return $path;
965
	}
966
967
	/**
968
	 * Determines if node is descendant of subject node
969
	 * @param ActiveRecord $subj the subject node
970
	 * @param ActiveRecord $object [optional] defaults to $this->owner
971
	 * @return boolean whether the node is descendant of subject node
972
	 */
973
	public function isDescendantOf($subj, $object = null)
974
	{
975
	    $object = (!is_null($object) ? $object : $this->owner);
976
		$result = ($object->getAttribute($this->leftAttribute) > $subj->getAttribute($this->leftAttribute))
977
			&& ($object->getAttribute($this->rightAttribute) < $subj->getAttribute($this->rightAttribute));
978
979
		if ($this->hasManyRoots) {
980
			$result = $result && ($object->getAttribute($this->rootAttribute)
981
				=== $subj->getAttribute($this->rootAttribute));
982
		}
983
984
		return $result;
985
	}
986
987
	/**
988
	 * Determines if node is leaf
989
	 * @param ActiveRecord $object [optional] defaults to $this->owner
990
	 * @return boolean whether the node is leaf
991
	 */
992
	public function isLeaf($object = null)
993
	{
994
	    $object = (!is_null($object) ? $object : $this->owner);
995
	    return $object->getAttribute($this->rightAttribute)
996
			- $object->getAttribute($this->leftAttribute) === 1;
997
	}
998
999
	/**
1000
	 * Determines if node is root
1001
	 * @param ActiveRecord $object [optional] defaults to $this->owner
1002
	 * @return boolean whether the node is root
1003
	 */
1004
	public function isRoot($object = null)
1005
	{
1006
	    $object = (!is_null($object) ? $object : $this->owner);
1007
	    return $object->getAttribute($this->leftAttribute) == 1;
1008
	}
1009
1010
	/**
1011
	 * Returns if the current node is deleted
1012
	 * @return boolean whether the node is deleted
1013
	 */
1014
	public function getIsDeletedRecord()
1015
	{
1016
		return $this->_deleted;
1017
	}
1018
1019
	/**
1020
	 * Sets if the current node is deleted
1021
	 * @param boolean $value whether the node is deleted
1022
	 */
1023
	public function setIsDeletedRecord($value)
1024
	{
1025
		$this->_deleted = $value;
1026
	}
1027
1028
	/**
1029
	 * Handle 'afterFind' event of the owner
1030
	 * @param ModelEvent $event event parameter
1031
	 */
1032
	public function afterFind($event)
1033
	{
1034
		self::$_cached[get_class($this->owner)][$this->_id = self::$_c++] = $this->owner;
1035
	}
1036
1037
	/**
1038
	 * Handle 'beforeInsert' event of the owner
1039
	 * @param ModelEvent $event event parameter
1040
	 * @throws Exception
1041
	 * @return boolean
1042
	 */
1043
	public function beforeInsert($event)
1044
	{
1045
		if ($this->_ignoreEvent) {
1046
			return true;
1047
		} else {
1048
			throw new Exception('You should not use ActiveRecord::save() or ActiveRecord::insert() methods when NestedSet behavior attached.');
1049
		}
1050
	}
1051
1052
	/**
1053
	 * Handle 'beforeUpdate' event of the owner
1054
	 * @param ModelEvent $event event parameter
1055
	 * @throws Exception
1056
	 * @return boolean
1057
	 */
1058
	public function beforeUpdate($event)
1059
	{
1060
		if ($this->_ignoreEvent) {
1061
			return true;
1062
		} else {
1063
			throw new Exception('You should not use ActiveRecord::save() or ActiveRecord::update() methods when NestedSet behavior attached.');
1064
		}
1065
	}
1066
1067
	/**
1068
	 * Handle 'beforeDelete' event of the owner
1069
	 * @param ModelEvent $event event parameter
1070
	 * @throws Exception
1071
	 * @return boolean
1072
	 */
1073
	public function beforeDelete($event)
1074
	{
1075
		if ($this->_ignoreEvent) {
1076
			return true;
1077
		} else {
1078
			throw new Exception('You should not use ActiveRecord::delete() method when NestedSet behavior attached.');
1079
		}
1080
	}
1081
1082
	/**
1083
	 * Handle 'beforeSaveAll' event of the owner
1084
	 * @param ModelEvent $event event parameter
1085
	 * @throws Exception
1086
	 * @return boolean
1087
	 */
1088
	public function beforeSaveAll($event)
1089
	{
1090
		if ($this->_ignoreEvent) {
1091
			return true;
1092
		} elseif ($this->owner->getIsNewRecord()) {
1093
			throw new Exception('You should not use ActiveRecord::saveAll() on new records when NestedSet behavior attached.');
1094
		}
1095
	}
1096
1097
	/**
1098
	 * Handle 'beforeDeleteFull' event of the owner
1099
	 * @param ModelEvent $event event parameter
1100
	 * @throws Exception
1101
	 * @return boolean
1102
	 */
1103
	public function beforeDeleteFull($event)
1104
	{
1105
		if ($this->_ignoreEvent) {
1106
			return true;
1107
		} else {
1108
			throw new Exception('You should not use ActiveRecord::beforeDeleteFull() method when NestedSet behavior attached.');
1109
		}
1110
	}
1111
1112
	/**
1113
	 * @param integer $key.
1114
	 * @param integer $delta.
1115
	 */
1116
	private function shiftLeftRight($key, $delta)
1117
	{
1118
		$db = $this->owner->getDb();
1119
1120
		foreach ([$this->leftAttribute, $this->rightAttribute] as $attribute) {
1121
			$condition = $db->quoteColumnName($attribute) . '>=' . $key;
1122
			$params = [];
1123
1124
			if ($this->hasManyRoots) {
1125
				$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute;
1126
				$params[':' . $this->rootAttribute] = $this->owner->getAttribute($this->rootAttribute);
1127
			}
1128
1129
			$this->owner->updateAll(
1130
				[$attribute => new Expression($db->quoteColumnName($attribute) . sprintf('%+d', $delta))],
1131
				$condition,
1132
				$params
1133
			);
1134
		}
1135
	}
1136
1137
	/**
1138
	 * @param ActiveRecord $target
1139
	 * @param int $key
1140
	 * @param int $levelUp
1141
	 * @param boolean $runValidation
1142
	 * @param array $attributes
1143
	 * @throws Exception
1144
	 * @return boolean
1145
	 */
1146
	private function addNode($target, $key, $levelUp, $runValidation, $attributes)
1147
	{
1148
		if (!$this->owner->getIsNewRecord()) {
1149
			throw new Exception('The node can\'t be inserted because it is not new.');
1150
		}
1151
1152
		if ($this->getIsDeletedRecord()) {
1153
			throw new Exception('The node can\'t be inserted because it is deleted.');
1154
		}
1155
1156
		if ($target->getIsDeletedRecord()) {
1157
			throw new Exception('The node can\'t be inserted because target node is deleted.');
1158
		}
1159
1160
		if ($this->owner->equals($target)) {
1161
			throw new Exception('The target node should not be self.');
1162
		}
1163
1164
		if (!$levelUp && $target->isRoot()) {
1165
			throw new Exception('The target node should not be root.');
1166
		}
1167
1168
        if ($this->hasPaths && $this->owner->getAttribute($this->pathAttribute) == '__DUPLICATE__') {
1169
			throw new Exception('New node has duplicate path.');
1170
        }
1171
1172
		if ($runValidation && !$this->owner->validate($attributes)) {
1173
			return false;
1174
		}
1175
1176
        if (!$this->beforeAddNode()) {
1177
            return false;
1178
        }
1179
1180
		if ($this->hasManyRoots) {
1181
			$this->owner->setAttribute($this->rootAttribute, $target->getAttribute($this->rootAttribute));
1182
		}
1183
1184
		$db = $this->owner->getDb();
1185
1186
		if ($db->getTransaction() === null) {
1187
			$transaction = $db->beginTransaction();
1188
		}
1189
1190
		try {
1191
			$this->shiftLeftRight($key, 2);
1192
			$this->owner->setAttribute($this->leftAttribute, $key);
1193
			$this->owner->setAttribute($this->rightAttribute, $key + 1);
1194
			$this->owner->setAttribute($this->levelAttribute, $target->getAttribute($this->levelAttribute) + $levelUp);
1195
			$this->_ignoreEvent = true;
1196
            //$result = $this->owner->insert(false, $attributes);
1197
            if (method_exists($this->owner, 'saveAll')) {
1198
                $result = $this->owner->saveAll(false, false, false, $attributes);
1199
            } else {
1200
                $result = $this->owner->save(false, $attributes);
1201
            }
1202
			$this->_ignoreEvent = false;
1203
1204
            if ($result) {
1205
                $result = $this->afterAddNode();
1206
            }
1207
1208
			if (!$result) {
1209
				if (isset($transaction)) {
1210
					$transaction->rollback();
1211
				}
1212
				return false;
1213
			}
1214
1215
    	    $this->owner->setIsNewRecord(false);
1216
1217
			if (isset($transaction)) {
1218
				$transaction->commit();
1219
			}
1220
1221
			$this->correctCachedOnAddNode($key);
1222
		} catch (\Exception $e) {
1223
			if (isset($transaction)) {
1224
				$transaction->rollback();
1225
			}
1226
			throw $e;
1227
		}
1228
1229
		return true;
1230
	}
1231
1232
	/**
1233
	 * @param array $attributes
1234
	 * @throws Exception
1235
	 * @return boolean
1236
	 */
1237
	private function makeRoot($attributes)
1238
	{
1239
		$this->owner->setAttribute($this->leftAttribute, 1);
1240
		$this->owner->setAttribute($this->rightAttribute, 2);
1241
		$this->owner->setAttribute($this->levelAttribute, 1);
1242
        if ($this->hasPaths && $this->owner->hasAttribute($this->pathAttribute) && $this->owner->getAttribute($this->pathAttribute) == '') {
1243
            $this->owner->setAttribute($this->pathAttribute, '/');
1244
        }
1245
1246
		if ($this->hasManyRoots) {
1247
			$db = $this->owner->getDb();
1248
1249
			if ($db->getTransaction() === null) {
1250
				$transaction = $db->beginTransaction();
1251
			}
1252
1253
			try {
1254
				$this->_ignoreEvent = true;
1255
    			//$result = $this->owner->insert(false, $attributes);
1256
                if (method_exists($this->owner, 'saveAll')) {
1257
                    $result = $this->owner->saveAll(false, false, false, $attributes);
1258
                } else {
1259
                    $result = $this->owner->save(false, $attributes);
1260
                }
1261
				$this->_ignoreEvent = false;
1262
1263
				if (!$result) {
1264
					if (isset($transaction)) {
1265
						$transaction->rollback();
1266
					}
1267
1268
					return false;
1269
				}
1270
1271
				$this->owner->setIsNewRecord(false);
1272
1273
				$this->owner->setAttribute($this->rootAttribute, $this->owner->getPrimaryKey());
1274
				$primaryKey = $this->owner->primaryKey();
1275
1276
				if (!isset($primaryKey[0])) {
1277
					throw new Exception(get_class($this->owner) . ' must have a primary key.');
1278
				}
1279
1280
				$this->owner->updateAll(
1281
					[$this->rootAttribute => $this->owner->getAttribute($this->rootAttribute)],
1282
					[$primaryKey[0] => $this->owner->getAttribute($this->rootAttribute)]
1283
				);
1284
1285
				if (isset($transaction)) {
1286
					$transaction->commit();
1287
				}
1288
			} catch (\Exception $e) {
1289
				if (isset($transaction)) {
1290
					$transaction->rollback();
1291
				}
1292
1293
				throw $e;
1294
			}
1295
		} else {
1296
			if ($this->owner->find()->roots()->exists()) {
0 ignored issues
show
Bug introduced by
The method roots() does not seem to exist on object<yii\db\ActiveQueryInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1297
				throw new Exception('Can\'t create more than one root in single root mode.');
1298
			}
1299
1300
			$this->_ignoreEvent = true;
1301
			//$result = $this->owner->insert(false, $attributes);
1302
            if (method_exists($this->owner, 'saveAll')) {
1303
                $result = $this->owner->saveAll(false, false, false, $attributes);
1304
            } else {
1305
                $result = $this->owner->save(false, $attributes);
1306
            }
1307
			$this->_ignoreEvent = false;
1308
1309
			if (!$result) {
1310
				return false;
1311
			}
1312
1313
			$this->owner->setIsNewRecord(false);
1314
		}
1315
1316
		return true;
1317
	}
1318
1319
	/**
1320
	 * @param ActiveRecord $target
1321
	 * @param int $key
1322
	 * @param int $levelUp
1323
	 * @throws Exception
1324
	 * @return boolean
1325
	 */
1326
	private function moveNode($target, $key, $levelUp)
1327
	{
1328
		if ($this->owner->getIsNewRecord()) {
1329
			throw new Exception('The node should not be new record.');
1330
		}
1331
1332
		if ($this->getIsDeletedRecord()) {
1333
			throw new Exception('The node should not be deleted.');
1334
		}
1335
1336
		if ($target->getIsDeletedRecord()) {
1337
			throw new Exception('The target node should not be deleted.');
1338
		}
1339
1340
		if ($this->owner->equals($target)) {
1341
			throw new Exception('The target node should not be self.');
1342
		}
1343
1344
		if ($target->isDescendantOf($this->owner)) {
1345
			throw new Exception('The target node should not be descendant.');
1346
		}
1347
1348
		if (!$levelUp && $target->isRoot()) {
1349
			throw new Exception('The target node should not be root.');
1350
		}
1351
1352
        if (!$this->beforeMoveNode($this->_previousPath)) {
1353
            return false;
1354
        }
1355
1356
		$db = $this->owner->getDb();
1357
1358
		if ($db->getTransaction() === null) {
1359
			$transaction = $db->beginTransaction();
1360
		}
1361
1362
		try {
1363
			$left = $this->owner->getAttribute($this->leftAttribute);
1364
			$right = $this->owner->getAttribute($this->rightAttribute);
1365
			$levelDelta = $target->getAttribute($this->levelAttribute) - $this->owner->getAttribute($this->levelAttribute)
1366
				+ $levelUp;
1367
1368
			if ($this->hasManyRoots && $this->owner->getAttribute($this->rootAttribute) !==
1369
				$target->getAttribute($this->rootAttribute)) {
1370
1371
				foreach ([$this->leftAttribute, $this->rightAttribute] as $attribute) {
1372
					$this->owner->updateAll(
1373
						[$attribute => new Expression($db->quoteColumnName($attribute)
1374
							. sprintf('%+d', $right - $left + 1))],
1375
						$db->quoteColumnName($attribute) . '>=' . $key . ' AND '
1376
							. $db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute,
1377
						[':' . $this->rootAttribute => $target->getAttribute($this->rootAttribute)]
1378
					);
1379
				}
1380
1381
				$delta = $key - $left;
1382
				$this->owner->updateAll(
1383
					[
1384
						$this->leftAttribute => new Expression($db->quoteColumnName($this->leftAttribute)
1385
							. sprintf('%+d', $delta)),
1386
						$this->rightAttribute => new Expression($db->quoteColumnName($this->rightAttribute)
1387
							. sprintf('%+d', $delta)),
1388
						$this->levelAttribute => new Expression($db->quoteColumnName($this->levelAttribute)
1389
							. sprintf('%+d', $levelDelta)),
1390
						$this->rootAttribute => $target->getAttribute($this->rootAttribute),
1391
					],
1392
					$db->quoteColumnName($this->leftAttribute) . '>=' . $left . ' AND '
1393
						. $db->quoteColumnName($this->rightAttribute) . '<=' . $right . ' AND '
1394
						. $db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute,
1395
					[':' . $this->rootAttribute => $this->owner->getAttribute($this->rootAttribute)]
1396
				);
1397
				$this->shiftLeftRight($right + 1, $left - $right - 1);
1398
1399
				if (isset($transaction)) {
1400
					$transaction->commit();
1401
				}
1402
1403
				$this->correctCachedOnMoveBetweenTrees($key, $levelDelta, $target->getAttribute($this->rootAttribute));
1404
			} else {
1405
				$delta = $right - $left + 1;
1406
				$this->shiftLeftRight($key, $delta);
1407
1408
				if ($left >= $key) {
1409
					$left += $delta;
1410
					$right += $delta;
1411
				}
1412
1413
				$condition = $db->quoteColumnName($this->leftAttribute) . '>=' . $left . ' AND '
1414
					. $db->quoteColumnName($this->rightAttribute) . '<=' . $right;
1415
				$params = [];
1416
1417
				if ($this->hasManyRoots) {
1418
					$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute;
1419
					$params[':' . $this->rootAttribute] = $this->owner->getAttribute($this->rootAttribute);
1420
				}
1421
1422
                $updateColumns = [];
1423
                $updateColumns[$this->levelAttribute] = new Expression($db->quoteColumnName($this->levelAttribute)
1424
					. sprintf('%+d', $levelDelta));
1425
1426
				if ($this->hasPaths && $this->owner->hasAttribute($this->pathAttribute)) {
1427
                    $pathLength = Tools::strlen($this->_previousPath) + 1;
1428
                    // SQL Server: SUBSTRING() rather than SUBSTR
1429
                    // SQL Server: + instead of CONCAT
1430
                    if ($db->getDriverName() == 'mssql') {
1431
                        $updateColumns[$this->pathAttribute] = new Expression($db->quoteValue($this->owner->getAttribute($this->pathAttribute)) . ' + SUBSTRING(' . $db->quoteColumnName($this->pathAttribute) . ', ' . $pathLength . '))');
1432
                    } else {
1433
                        $updateColumns[$this->pathAttribute] = new Expression('CONCAT(' . $db->quoteValue($this->owner->getAttribute($this->pathAttribute)) . ', SUBSTR(' . $db->quoteColumnName($this->pathAttribute) . ', ' . $pathLength . '))');
1434
                    }
1435
				}
1436
1437
				$this->owner->updateAll(
1438
					$updateColumns,
1439
					$condition,
1440
					$params
1441
				);
1442
1443
				foreach ([$this->leftAttribute, $this->rightAttribute] as $attribute) {
1444
					$condition = $db->quoteColumnName($attribute) . '>=' . $left . ' AND '
1445
						. $db->quoteColumnName($attribute) . '<=' . $right;
1446
					$params = [];
1447
1448
					if ($this->hasManyRoots) {
1449
						$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=:'
1450
							. $this->rootAttribute;
1451
						$params[':' . $this->rootAttribute] = $this->owner->getAttribute($this->rootAttribute);
1452
					}
1453
1454
					$this->owner->updateAll(
1455
						[$attribute => new Expression($db->quoteColumnName($attribute)
1456
							. sprintf('%+d', $key - $left))],
1457
						$condition,
1458
						$params
1459
					);
1460
				}
1461
1462
				$this->shiftLeftRight($right + 1, -$delta);
1463
1464
                $result = $this->afterMoveNode($this->_previousPath);
1465
1466
				if (isset($transaction)) {
1467
				    if ($result) {
1468
                        $transaction->commit();
1469
				    } else {
1470
				        $transaction->rollback();
1471
				        $this->_previousPath = '';
1472
				        return false;
1473
				    }
1474
				}
1475
1476
				$this->correctCachedOnMoveNode($key, $levelDelta);
1477
			}
1478
		} catch (\Exception $e) {
1479
			if (isset($transaction)) {
1480
				$transaction->rollback();
1481
			}
1482
1483
			throw $e;
1484
		}
1485
1486
		$this->_previousPath = '';
1487
1488
		return true;
1489
	}
1490
1491
	/**
1492
	 * Correct cache for [[delete()]] and [[deleteNode()]].
1493
	 *
1494
	 * @param integer $left
1495
	 * @param integer $right
1496
	 */
1497
	private function correctCachedOnDelete($left, $right)
1498
	{
1499
		$key = $right + 1;
1500
		$delta = $left - $right - 1;
1501
		foreach (self::$_cached[get_class($this->owner)] as $node) {
1502
			/** @var $node ActiveRecord */
1503
			if ($node->getIsNewRecord() || $node->getIsDeletedRecord()) {
1504
				continue;
1505
			}
1506
1507
			if ($this->hasManyRoots && $this->owner->getAttribute($this->rootAttribute)
1508
				!== $node->getAttribute($this->rootAttribute)) {
1509
				continue;
1510
			}
1511
1512
			if ($node->getAttribute($this->leftAttribute) >= $left
1513
				&& $node->getAttribute($this->rightAttribute) <= $right) {
1514
                    $node->setIsDeletedRecord(true);
1515
			} else {
1516
				if ($node->getAttribute($this->leftAttribute) >= $key) {
1517
					$node->setAttribute(
1518
						$this->leftAttribute,
1519
						$node->getAttribute($this->leftAttribute) + $delta
1520
					);
1521
				}
1522
1523
				if ($node->getAttribute($this->rightAttribute) >= $key) {
1524
					$node->setAttribute(
1525
						$this->rightAttribute,
1526
						$node->getAttribute($this->rightAttribute) + $delta
1527
					);
1528
				}
1529
			}
1530
		}
1531
	}
1532
1533
	/**
1534
	 * Correct cache for [[addNode()]]
1535
	 * @param int $key
1536
	 */
1537
	private function correctCachedOnAddNode($key)
1538
	{
1539
		foreach (self::$_cached[get_class($this->owner)] as $node) {
1540
			/** @var $node ActiveRecord */
1541
			if ($node->getIsNewRecord() || $node->getIsDeletedRecord()) {
1542
				continue;
1543
			}
1544
1545
			if ($this->hasManyRoots && $this->owner->getAttribute($this->rootAttribute)
1546
				!== $node->getAttribute($this->rootAttribute)) {
1547
				continue;
1548
			}
1549
1550
			if ($this->owner === $node) {
1551
				continue;
1552
			}
1553
1554
			if ($node->getAttribute($this->leftAttribute) >= $key) {
1555
				$node->setAttribute(
1556
					$this->leftAttribute,
1557
					$node->getAttribute($this->leftAttribute) + 2
1558
				);
1559
			}
1560
1561
			if ($node->getAttribute($this->rightAttribute) >= $key) {
1562
				$node->setAttribute(
1563
					$this->rightAttribute,
1564
					$node->getAttribute($this->rightAttribute) + 2
1565
				);
1566
			}
1567
		}
1568
	}
1569
1570
	/**
1571
	 * Correct cache for [[moveNode()]]
1572
	 * @param int $key
1573
	 * @param int $levelDelta
1574
	 */
1575
	private function correctCachedOnMoveNode($key, $levelDelta)
1576
	{
1577
		$left = $this->owner->getAttribute($this->leftAttribute);
1578
		$right = $this->owner->getAttribute($this->rightAttribute);
1579
		$delta = $right - $left + 1;
1580
1581
		if ($left >= $key) {
1582
			$left += $delta;
1583
			$right += $delta;
1584
		}
1585
1586
		$delta2 = $key - $left;
1587
1588
		foreach (self::$_cached[get_class($this->owner)] as $node) {
1589
			/** @var $node ActiveRecord */
1590
			if ($node->getIsNewRecord() || $node->getIsDeletedRecord()) {
1591
				continue;
1592
			}
1593
1594
			if ($this->hasManyRoots && $this->owner->getAttribute($this->rootAttribute)
1595
				!== $node->getAttribute($this->rootAttribute)) {
1596
				continue;
1597
			}
1598
1599
			if ($node->getAttribute($this->leftAttribute) >= $key) {
1600
				$node->setAttribute(
1601
					$this->leftAttribute,
1602
					$node->getAttribute($this->leftAttribute) + $delta
1603
				);
1604
			}
1605
1606
			if ($node->getAttribute($this->rightAttribute) >= $key) {
1607
				$node->setAttribute(
1608
					$this->rightAttribute,
1609
					$node->getAttribute($this->rightAttribute) + $delta
1610
				);
1611
			}
1612
1613
			if ($node->getAttribute($this->leftAttribute) >= $left
1614
				&& $node->getAttribute($this->rightAttribute) <= $right) {
1615
				$node->setAttribute(
1616
					$this->levelAttribute,
1617
					$node->getAttribute($this->levelAttribute) + $levelDelta
1618
				);
1619
			}
1620
1621
			if ($node->getAttribute($this->leftAttribute) >= $left
1622
				&& $node->getAttribute($this->leftAttribute) <= $right) {
1623
				$node->setAttribute(
1624
					$this->leftAttribute,
1625
					$node->getAttribute($this->leftAttribute) + $delta2
1626
				);
1627
			}
1628
1629
			if ($node->getAttribute($this->rightAttribute) >= $left
1630
				&& $node->getAttribute($this->rightAttribute) <= $right) {
1631
				$node->setAttribute(
1632
					$this->rightAttribute,
1633
					$node->getAttribute($this->rightAttribute) + $delta2
1634
				);
1635
			}
1636
1637
			if ($node->getAttribute($this->leftAttribute) >= $right + 1) {
1638
				$node->setAttribute(
1639
					$this->leftAttribute,
1640
					$node->getAttribute($this->leftAttribute) - $delta
1641
				);
1642
			}
1643
1644
			if ($node->getAttribute($this->rightAttribute) >= $right + 1) {
1645
				$node->setAttribute(
1646
					$this->rightAttribute,
1647
					$node->getAttribute($this->rightAttribute) - $delta
1648
				);
1649
			}
1650
		}
1651
	}
1652
1653
	/**
1654
	 * Correct cache for [[moveNode()]]
1655
	 * @param int $key
1656
	 * @param int $levelDelta
1657
	 * @param int $root
1658
	 */
1659
	private function correctCachedOnMoveBetweenTrees($key, $levelDelta, $root)
1660
	{
1661
		$left = $this->owner->getAttribute($this->leftAttribute);
1662
		$right = $this->owner->getAttribute($this->rightAttribute);
1663
		$delta = $right - $left + 1;
1664
		$delta2 = $key - $left;
1665
		$delta3 = $left - $right - 1;
1666
1667
		foreach (self::$_cached[get_class($this->owner)] as $node) {
1668
			/** @var $node ActiveRecord */
1669
			if ($node->getIsNewRecord() || $node->getIsDeletedRecord()) {
1670
				continue;
1671
			}
1672
1673
			if ($node->getAttribute($this->rootAttribute) === $root) {
1674
				if ($node->getAttribute($this->leftAttribute) >= $key) {
1675
					$node->setAttribute(
1676
						$this->leftAttribute,
1677
						$node->getAttribute($this->leftAttribute) + $delta
1678
					);
1679
				}
1680
1681
				if ($node->getAttribute($this->rightAttribute) >= $key) {
1682
					$node->setAttribute(
1683
						$this->rightAttribute,
1684
						$node->getAttribute($this->rightAttribute) + $delta
1685
					);
1686
				}
1687
			} elseif ($node->getAttribute($this->rootAttribute)
1688
				=== $this->owner->getAttribute($this->rootAttribute)) {
1689
				if ($node->getAttribute($this->leftAttribute) >= $left
1690
					&& $node->getAttribute($this->rightAttribute) <= $right) {
1691
					$node->setAttribute(
1692
						$this->leftAttribute,
1693
						$node->getAttribute($this->leftAttribute) + $delta2
1694
					);
1695
					$node->setAttribute(
1696
						$this->rightAttribute,
1697
						$node->getAttribute($this->rightAttribute) + $delta2
1698
					);
1699
					$node->setAttribute(
1700
						$this->levelAttribute,
1701
						$node->getAttribute($this->levelAttribute) + $levelDelta
1702
					);
1703
					$node->setAttribute($this->rootAttribute, $root);
1704
				} else {
1705
					if ($node->getAttribute($this->leftAttribute) >= $right + 1) {
1706
						$node->setAttribute(
1707
							$this->leftAttribute,
1708
							$node->getAttribute($this->leftAttribute) + $delta3
1709
						);
1710
					}
1711
1712
					if ($node->getAttribute($this->rightAttribute) >= $right + 1) {
1713
						$node->setAttribute(
1714
							$this->rightAttribute,
1715
							$node->getAttribute($this->rightAttribute) + $delta3
1716
						);
1717
					}
1718
				}
1719
			}
1720
		}
1721
	}
1722
1723
	/**
1724
	 * Optionally perform actions/checks before addNode is processed
1725
	 * @return boolean success
1726
	 */
1727
	protected function beforeAddNode()
1728
	{
1729
        if (method_exists($this->owner, 'beforeAddNode')) {
1730
            return $this->owner->beforeAddNode();
1731
        }
1732
        return true;
1733
	}
1734
1735
	/**
1736
	 * Optionally perform actions/checks after addNode has processed
1737
	 * @return boolean success
1738
	 */
1739
	protected function afterAddNode()
1740
	{
1741
        if (method_exists($this->owner, 'afterAddNode')) {
1742
            return $this->owner->afterAddNode();
1743
        }
1744
        return true;
1745
	}
1746
1747
	/**
1748
	 * Optionally perform actions/checks before a node name is changed
1749
     * @param string $old old folder path
1750
	 * @return boolean success
1751
	 */
1752
	protected function beforeRenameNode($old)
1753
	{
1754
        if (method_exists($this->owner, 'beforeRenameNode')) {
1755
            return $this->owner->beforeRenameNode($old);
1756
        }
1757
        return true;
1758
	}
1759
1760
	/**
1761
	 * Optionally perform actions/checks after the node name has been changed
1762
     * @param string $old old folder path
1763
	 * @return boolean success
1764
	 */
1765
	protected function afterRenameNode($old)
1766
	{
1767
        if (method_exists($this->owner, 'afterRenameNode')) {
1768
            return $this->owner->afterRenameNode($old);
1769
        }
1770
        return true;
1771
	}
1772
1773
	/**
1774
	 * Optionally perform actions/checks before a node is moved
1775
     * @param string $old old folder path
1776
	 * @return boolean success
1777
	 */
1778
	protected function beforeMoveNode($old)
1779
	{
1780
        if (method_exists($this->owner, 'beforeMoveNode')) {
1781
            return $this->owner->beforeMoveNode($old);
1782
        }
1783
        return true;
1784
	}
1785
1786
	/**
1787
	 * Optionally perform actions/checks after the node is moved
1788
     * @param string $old old folder path
1789
	 * @return boolean success
1790
	 */
1791
	protected function afterMoveNode($old)
1792
	{
1793
        if (method_exists($this->owner, 'afterMoveNode')) {
1794
            return $this->owner->afterMoveNode($old);
1795
        }
1796
        return true;
1797
	}
1798
1799
	/**
1800
	 * Optionally perform actions/checks before a node is deleted
1801
	 * @return boolean success
1802
	 */
1803
	protected function beforeDeleteNode()
1804
	{
1805
        if (method_exists($this->owner, 'beforeDeleteNode')) {
1806
            return $this->owner->beforeDeleteNode();
1807
        }
1808
        return true;
1809
	}
1810
1811
	/**
1812
	 * Optionally perform actions/checks after the node has been deleted
1813
     * @param string $path
1814
	 * @return boolean success
1815
	 */
1816
	protected function afterDeleteNode($path)
1817
	{
1818
        if (method_exists($this->owner, 'afterDeleteNode')) {
1819
            return $this->owner->afterDeleteNode($path);
1820
        }
1821
        return true;
1822
	}
1823
1824
	/**
1825
     * Override ignore events flag
1826
     * @param boolean $value
1827
     */
1828
    public function setIgnoreEvents($value)
1829
    {
1830
        $this->_ignoreEvent = $value;
1831
    }
1832
1833
    /**
1834
     * Set previous path (sometimes useful to avoid looking up parent multiple times)
1835
     * @param string $path
1836
     */
1837
    public function setPreviousPath($path)
1838
    {
1839
        $this->_previousPath = $path;
1840
    }
1841
1842
	/**
1843
	 * Destructor
1844
	 */
1845
	public function __destruct()
1846
	{
1847
		unset(self::$_cached[get_class($this->owner)][$this->_id]);
1848
	}
1849
}
1850