1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* This file is part of the fangface/yii2-concord package |
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
|
|
|
* Based on; |
16
|
|
|
* |
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
|
|
|
namespace fangface\behaviors; |
22
|
|
|
|
23
|
|
|
use fangface\Tools; |
24
|
|
|
use fangface\db\ActiveRecord; |
25
|
|
|
use yii\base\Behavior; |
26
|
|
|
use yii\base\ModelEvent; |
27
|
|
|
use yii\db\ActiveQuery; |
28
|
|
|
use yii\db\Exception; |
29
|
|
|
use yii\db\Expression; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Nested Set behavior for attaching to ActiveRecord |
33
|
|
|
* CREATE TABLE `example_nest` ( |
34
|
|
|
* `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
35
|
|
|
* `name` char(100) NOT NULL DEFAULT '', |
36
|
|
|
* `path` varchar(255) NOT NULL DEFAULT '', |
37
|
|
|
* `lft` bigint(20) unsigned NOT NULL DEFAULT '0', |
38
|
|
|
* `rgt` bigint(20) unsigned NOT NULL DEFAULT '0', |
39
|
|
|
* `level` smallint(5) unsigned NOT NULL DEFAULT '0', |
40
|
|
|
* `created_at` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', |
41
|
|
|
* `created_by` bigint(20) unsigned NOT NULL DEFAULT '0', |
42
|
|
|
* `modified_at` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', |
43
|
|
|
* `modified_by` bigint(20) unsigned NOT NULL DEFAULT '0', |
44
|
|
|
* PRIMARY KEY (`id`), |
45
|
|
|
* KEY `lft` (`lft`), |
46
|
|
|
* KEY `rgt` (`rgt`), |
47
|
|
|
* KEY `level` (`level`,`lft`) USING BTREE |
48
|
|
|
* ) ENGINE=InnoDB DEFAULT CHARSET=utf8; |
49
|
|
|
* |
50
|
|
|
* @author Alexander Kochetov <[email protected]> |
51
|
|
|
* @author Fangface <[email protected]> |
52
|
|
|
*/ |
53
|
|
|
class NestedSetBehavior extends Behavior |
54
|
|
|
{ |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* |
58
|
|
|
* @var ActiveRecord the owner of this behavior. |
59
|
|
|
*/ |
60
|
|
|
public $owner; |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* |
64
|
|
|
* @var boolean |
65
|
|
|
*/ |
66
|
|
|
public $hasManyRoots = false; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* |
70
|
|
|
* @var boolean |
71
|
|
|
*/ |
72
|
|
|
public $hasPaths = false; |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* |
76
|
|
|
* @var boolean |
77
|
|
|
*/ |
78
|
|
|
public $hasAction = false; |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* |
82
|
|
|
* @var boolean should deletes be performed on each active record individually |
83
|
|
|
*/ |
84
|
|
|
public $deleteIndividual = false; |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* |
88
|
|
|
* @var string |
89
|
|
|
*/ |
90
|
|
|
public $rootAttribute = 'root'; |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* |
94
|
|
|
* @var string |
95
|
|
|
*/ |
96
|
|
|
public $leftAttribute = 'lft'; |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* |
100
|
|
|
* @var string |
101
|
|
|
*/ |
102
|
|
|
public $rightAttribute = 'rgt'; |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* |
106
|
|
|
* @var string |
107
|
|
|
*/ |
108
|
|
|
public $levelAttribute = 'level'; |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* |
112
|
|
|
* @var string |
113
|
|
|
*/ |
114
|
|
|
public $nameAttribute = 'name'; |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* |
118
|
|
|
* @var string |
119
|
|
|
*/ |
120
|
|
|
public $pathAttribute = 'path'; |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* |
124
|
|
|
* @var boolean |
125
|
|
|
*/ |
126
|
|
|
private $_ignoreEvent = false; |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* |
130
|
|
|
* @var boolean |
131
|
|
|
*/ |
132
|
|
|
private $_deleted = false; |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* |
136
|
|
|
* @var string |
137
|
|
|
*/ |
138
|
|
|
private $_previousPath = ''; |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* |
142
|
|
|
* @var integer |
143
|
|
|
*/ |
144
|
|
|
private $_id; |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* |
148
|
|
|
* @var array |
149
|
|
|
*/ |
150
|
|
|
private static $_cached; |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* |
154
|
|
|
* @var integer |
155
|
|
|
*/ |
156
|
|
|
private static $_c = 0; |
157
|
|
|
|
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* @inheritdoc |
161
|
|
|
*/ |
162
|
|
|
public function events() |
163
|
|
|
{ |
164
|
|
|
return [ |
165
|
|
|
ActiveRecord::EVENT_AFTER_FIND => 'afterFind', |
166
|
|
|
ActiveRecord::EVENT_BEFORE_DELETE => 'beforeDelete', |
167
|
|
|
ActiveRecord::EVENT_BEFORE_INSERT => 'beforeInsert', |
168
|
|
|
ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeUpdate', |
169
|
|
|
ActiveRecord::EVENT_BEFORE_SAVE_ALL => 'beforeSaveAll', |
170
|
|
|
ActiveRecord::EVENT_BEFORE_DELETE_FULL => 'beforeDeleteFull' |
171
|
|
|
]; |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* @inheritdoc |
177
|
|
|
*/ |
178
|
|
|
public function attach($owner) |
179
|
|
|
{ |
180
|
|
|
parent::attach($owner); |
181
|
|
|
self::$_cached[get_class($this->owner)][$this->_id = self::$_c++] = $this->owner; |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* Gets descendants for node |
187
|
|
|
* |
188
|
|
|
* @param integer $depth |
189
|
|
|
* the depth |
190
|
|
|
* @param ActiveRecord $object |
191
|
|
|
* [optional] defaults to $this->owner |
192
|
|
|
* @param integer $limit |
193
|
|
|
* [optional] limit results (typically used when only after limited number of immediate children) |
194
|
|
|
* @return ActiveQuery|integer |
195
|
|
|
*/ |
196
|
|
|
public function descendants($depth = null, $object = null, $limit = 0) |
197
|
|
|
{ |
198
|
|
|
$object = (!is_null($object) ? $object : $this->owner); |
199
|
|
|
$query = $object->find()->orderBy([ |
200
|
|
|
$this->levelAttribute => SORT_ASC, |
201
|
|
|
$this->leftAttribute => SORT_ASC |
202
|
|
|
]); |
203
|
|
|
$db = $object->getDb(); |
204
|
|
|
$query->andWhere($db->quoteColumnName($this->leftAttribute) . '>' . $object->getAttribute($this->leftAttribute)); |
205
|
|
|
$query->andWhere($db->quoteColumnName($this->rightAttribute) . '<' . $object->getAttribute($this->rightAttribute)); |
206
|
|
|
$query->addOrderBy($db->quoteColumnName($this->leftAttribute)); |
207
|
|
|
|
208
|
|
|
if ($depth !== null) { |
209
|
|
|
$query->andWhere($db->quoteColumnName($this->levelAttribute) . '<=' . ($object->getAttribute($this->levelAttribute) + $depth)); |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
if ($this->hasManyRoots) { |
213
|
|
|
$query->andWhere($db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute, [ |
|
|
|
|
214
|
|
|
':' . $this->rootAttribute => $object->getAttribute($this->rootAttribute) |
215
|
|
|
]); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
if ($limit) { |
219
|
|
|
$query->limit($limit); |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
return $query; |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
|
226
|
|
|
/** |
227
|
|
|
* Gets children for node (direct descendants only) |
228
|
|
|
* |
229
|
|
|
* @param ActiveRecord $object |
230
|
|
|
* [optional] defaults to $this->owner |
231
|
|
|
* @param integer $limit |
232
|
|
|
* [optional] limit results (typically used when only after limited number of immediate children) |
233
|
|
|
* @return ActiveQuery|integer |
234
|
|
|
*/ |
235
|
|
|
public function children($object = null, $limit = 0) |
236
|
|
|
{ |
237
|
|
|
return $this->descendants(1, $object, $limit); |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
|
241
|
|
|
/** |
242
|
|
|
* Gets one child for node (first direct descendant only). |
243
|
|
|
* |
244
|
|
|
* @param ActiveRecord $object |
245
|
|
|
* [optional] defaults to $this->owner |
246
|
|
|
* @return ActiveQuery |
247
|
|
|
*/ |
248
|
|
|
public function oneChild($object = null) |
249
|
|
|
{ |
250
|
|
|
return $this->children($object, 1); |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
|
254
|
|
|
/** |
255
|
|
|
* Gets ancestors for node |
256
|
|
|
* |
257
|
|
|
* @param integer $depth |
258
|
|
|
* the depth |
259
|
|
|
* @param ActiveRecord $object |
260
|
|
|
* [optional] defaults to $this->owner |
261
|
|
|
* @param boolean $reverse |
262
|
|
|
* Should the result be in reverse order i.e. root first |
263
|
|
|
* @param boolean $idOnly |
264
|
|
|
* Should an array of IDs be returned only |
265
|
|
|
* @return ActiveQuery |
266
|
|
|
*/ |
267
|
|
|
public function ancestors($depth = null, $object = null, $reverse = false, $idOnly = false) |
268
|
|
|
{ |
269
|
|
|
$object = (!is_null($object) ? $object : $this->owner); |
270
|
|
|
$query = $object->find(); |
271
|
|
|
|
272
|
|
|
if ($idOnly) { |
273
|
|
|
$query->select($object->primaryKey()); |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
if ($reverse) { |
277
|
|
|
$query->orderBy([ |
278
|
|
|
$this->levelAttribute => SORT_ASC, |
279
|
|
|
$this->leftAttribute => SORT_ASC |
280
|
|
|
]); |
281
|
|
|
; |
282
|
|
|
} else { |
283
|
|
|
$query->orderBy([ |
284
|
|
|
$this->levelAttribute => SORT_DESC, |
285
|
|
|
$this->leftAttribute => SORT_ASC |
286
|
|
|
]); |
287
|
|
|
; |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
$db = $object->getDb(); |
291
|
|
|
|
292
|
|
|
$query->andWhere($db->quoteColumnName($this->leftAttribute) . '<' . $object->getAttribute($this->leftAttribute)); |
293
|
|
|
$query->andWhere($db->quoteColumnName($this->rightAttribute) . '>' . $object->getAttribute($this->rightAttribute)); |
294
|
|
|
$query->addOrderBy($db->quoteColumnName($this->leftAttribute)); |
295
|
|
|
|
296
|
|
|
if ($depth !== null) { |
297
|
|
|
$query->andWhere($db->quoteColumnName($this->levelAttribute) . '>=' . ($object->getAttribute($this->levelAttribute) - $depth)); |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
if ($this->hasManyRoots) { |
301
|
|
|
$query->andWhere($db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute, [ |
|
|
|
|
302
|
|
|
':' . $this->rootAttribute => $object->getAttribute($this->rootAttribute) |
303
|
|
|
]); |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
return $query; |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* Gets parent of node |
312
|
|
|
* |
313
|
|
|
* @param ActiveRecord $object |
314
|
|
|
* [optional] defaults to $this->owner |
315
|
|
|
* @param boolean $idOnly |
316
|
|
|
* Should only the id be returned |
317
|
|
|
* @return ActiveQuery |
318
|
|
|
*/ |
319
|
|
|
public function parentOnly($object = null, $idOnly = false) |
320
|
|
|
{ |
321
|
|
|
return $this->ancestors(1, $object, false, $idOnly); |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
|
325
|
|
|
/** |
326
|
|
|
* Gets entries at the same level of node (including self) |
327
|
|
|
* |
328
|
|
|
* @param ActiveRecord $object |
329
|
|
|
* [optional] defaults to $this->owner |
330
|
|
|
* @param integer $limit |
331
|
|
|
* [optional] limit results (typically used when only after limited number of immediate children) |
332
|
|
|
* @return ActiveQuery|integer |
333
|
|
|
*/ |
334
|
|
|
public function level($object = null, $limit = 0) |
335
|
|
|
{ |
336
|
|
|
$parent = $this->parentOnly($object)->one(); |
337
|
|
|
return $this->children($parent, $limit); |
|
|
|
|
338
|
|
|
} |
339
|
|
|
|
340
|
|
|
|
341
|
|
|
/** |
342
|
|
|
* Gets a count of entries at the same level of node (including self) |
343
|
|
|
* |
344
|
|
|
* @param ActiveRecord $object |
345
|
|
|
* [optional] defaults to $this->owner |
346
|
|
|
* @return integer |
347
|
|
|
*/ |
348
|
|
|
public function levelCount($object = null) |
349
|
|
|
{ |
350
|
|
|
return $this->level($object, true)->count(); |
|
|
|
|
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
|
354
|
|
|
/** |
355
|
|
|
* Gets previous sibling of node |
356
|
|
|
* |
357
|
|
|
* @param ActiveRecord $object |
358
|
|
|
* [optional] defaults to $this->owner |
359
|
|
|
* @return ActiveQuery |
360
|
|
|
*/ |
361
|
|
|
public function prev($object = null) |
362
|
|
|
{ |
363
|
|
|
$object = (!is_null($object) ? $object : $this->owner); |
364
|
|
|
$query = $object->find(); |
365
|
|
|
$db = $object->getDb(); |
366
|
|
|
$query->andWhere($db->quoteColumnName($this->rightAttribute) . '=' . ($object->getAttribute($this->leftAttribute) - 1)); |
367
|
|
|
|
368
|
|
|
if ($this->hasManyRoots) { |
369
|
|
|
$query->andWhere($db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute, [ |
|
|
|
|
370
|
|
|
':' . $this->rootAttribute => $object->getAttribute($this->rootAttribute) |
371
|
|
|
]); |
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
return $query; |
375
|
|
|
} |
376
|
|
|
|
377
|
|
|
|
378
|
|
|
/** |
379
|
|
|
* Gets next sibling of node |
380
|
|
|
* |
381
|
|
|
* @param ActiveRecord $object |
382
|
|
|
* [optional] defaults to $this->owner |
383
|
|
|
* @return ActiveQuery |
384
|
|
|
*/ |
385
|
|
|
public function next($object = null) |
386
|
|
|
{ |
387
|
|
|
$object = (!is_null($object) ? $object : $this->owner); |
388
|
|
|
$query = $object->find(); |
389
|
|
|
$db = $object->getDb(); |
390
|
|
|
$query->andWhere($db->quoteColumnName($this->leftAttribute) . '=' . ($object->getAttribute($this->rightAttribute) + 1)); |
391
|
|
|
|
392
|
|
|
if ($this->hasManyRoots) { |
393
|
|
|
$query->andWhere($db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute, [ |
|
|
|
|
394
|
|
|
':' . $this->rootAttribute => $object->getAttribute($this->rootAttribute) |
395
|
|
|
]); |
396
|
|
|
} |
397
|
|
|
|
398
|
|
|
return $query; |
399
|
|
|
} |
400
|
|
|
|
401
|
|
|
|
402
|
|
|
/** |
403
|
|
|
* Create root node if multiple-root tree mode. |
404
|
|
|
* Update node if it's not new |
405
|
|
|
* |
406
|
|
|
* @param boolean $runValidation |
407
|
|
|
* should validations be executed on all models before allowing save() |
408
|
|
|
* @param array $attributes |
409
|
|
|
* which attributes should be saved (default null means all changed attributes) |
410
|
|
|
* @param boolean $hasParentModel |
411
|
|
|
* whether this method was called from the top level or by a parent |
412
|
|
|
* If false, it means the method was called at the top level |
413
|
|
|
* @param boolean $fromSaveAll |
414
|
|
|
* has the save() call come from saveAll() or not |
415
|
|
|
* @return boolean did save() successfully process |
416
|
|
|
*/ |
417
|
|
|
private function save($runValidation = true, $attributes = null, $hasParentModel = false, $fromSaveAll = false) |
418
|
|
|
{ |
419
|
|
|
if ($this->owner->getReadOnly() && !$hasParentModel) { |
420
|
|
|
|
421
|
|
|
// return failure if we are at the top of the tree and should not be asking to saveAll |
422
|
|
|
// not allowed to amend or delete |
423
|
|
|
$message = 'Attempting to save on ' . Tools::getClassName($this->owner) . ' readOnly model'; |
424
|
|
|
//$this->addActionError($message); |
425
|
|
|
throw new \fangface\db\Exception($message); |
426
|
|
|
} elseif ($this->owner->getReadOnly() && $hasParentModel) { |
427
|
|
|
|
428
|
|
|
$message = 'Skipping save on ' . Tools::getClassName($this->owner) . ' readOnly model'; |
429
|
|
|
$this->addActionWarning($message); |
|
|
|
|
430
|
|
|
return true; |
431
|
|
|
} else { |
432
|
|
|
|
433
|
|
|
if ($runValidation && !$this->owner->validate($attributes)) { |
434
|
|
|
return false; |
435
|
|
|
} |
436
|
|
|
|
437
|
|
|
if ($this->owner->getIsNewRecord()) { |
438
|
|
|
return $this->makeRoot($attributes); |
|
|
|
|
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
$updateChildPaths = false; |
442
|
|
|
if ($this->hasPaths && !$this->owner->getIsNewRecord()) { |
443
|
|
|
if ($this->owner->hasAttribute($this->pathAttribute)) { |
444
|
|
|
if ($this->owner->hasChanged($this->pathAttribute)) { |
445
|
|
|
$updateChildPaths = true; |
446
|
|
|
if ($this->_previousPath == '') { |
447
|
|
|
$this->_previousPath = $this->owner->getOldAttribute($this->pathAttribute); |
448
|
|
|
} |
449
|
|
|
} |
450
|
|
|
} |
451
|
|
|
if (!$updateChildPaths && $this->owner->hasAttribute($this->nameAttribute)) { |
452
|
|
|
if ($this->owner->hasChanged($this->nameAttribute)) { |
453
|
|
|
$this->_previousPath = $this->owner->getAttribute($this->pathAttribute); |
454
|
|
|
$this->checkAndSetPath($this->owner); |
455
|
|
|
if ($this->_previousPath != $this->owner->getAttribute($this->pathAttribute)) { |
456
|
|
|
$updateChildPaths = true; |
457
|
|
|
} |
458
|
|
|
} |
459
|
|
|
} |
460
|
|
|
} |
461
|
|
|
|
462
|
|
|
$nameChanged = false; |
463
|
|
|
if ($this->owner->hasAttribute($this->nameAttribute) && $this->owner->hasChanged($this->nameAttribute)) { |
464
|
|
|
$nameChanged = true; |
465
|
|
|
if (!$this->beforeRenameNode($this->_previousPath)) { |
466
|
|
|
return false; |
467
|
|
|
} |
468
|
|
|
} |
469
|
|
|
|
470
|
|
|
$result = false; |
471
|
|
|
$db = $this->owner->getDb(); |
472
|
|
|
|
473
|
|
|
if ($db->getTransaction() === null) { |
474
|
|
|
$transaction = $db->beginTransaction(); |
475
|
|
|
} |
476
|
|
|
|
477
|
|
|
try { |
478
|
|
|
|
479
|
|
|
$this->_ignoreEvent = true; |
480
|
|
|
//$result = $this->owner->update(false, $attributes); |
481
|
|
|
if (false && method_exists($this->owner, 'saveAll')) { |
482
|
|
|
$result = $this->owner->saveAll(false, $hasParentModel, false, $attributes); |
483
|
|
|
} else { |
484
|
|
|
$result = $this->owner->save(false, $attributes, $hasParentModel, $fromSaveAll); |
485
|
|
|
} |
486
|
|
|
$this->_ignoreEvent = false; |
487
|
|
|
|
488
|
|
|
if ($result && $updateChildPaths) { |
489
|
|
|
// only if we have children |
490
|
|
|
if ($this->owner->getAttribute($this->rightAttribute) > $this->owner->getAttribute($this->leftAttribute) + 1) { |
491
|
|
|
$condition = $db->quoteColumnName($this->leftAttribute) . '>' . $this->owner->getAttribute($this->leftAttribute) . ' AND ' . $db->quoteColumnName($this->rightAttribute) . '<' . $this->owner->getAttribute($this->rightAttribute); |
492
|
|
|
$params = []; |
493
|
|
|
if ($this->hasManyRoots) { |
494
|
|
|
$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute; |
495
|
|
|
$params[':' . $this->rootAttribute] = $this->owner->getAttribute($this->rootAttribute); |
496
|
|
|
} |
497
|
|
|
|
498
|
|
|
$updateColumns = []; |
499
|
|
|
$pathLength = Tools::strlen($this->_previousPath) + 1; |
500
|
|
|
// SQL Server: SUBSTRING() rather than SUBSTR |
501
|
|
|
// SQL Server: + instead of CONCAT |
502
|
|
|
if ($db->getDriverName() == 'mssql') { |
503
|
|
|
$updateColumns[$this->pathAttribute] = new Expression($db->quoteValue($this->owner->getAttribute($this->pathAttribute)) . ' + SUBSTRING(' . $db->quoteColumnName($this->pathAttribute) . ', ' . $pathLength . '))'); |
504
|
|
|
} else { |
505
|
|
|
$updateColumns[$this->pathAttribute] = new Expression('CONCAT(' . $db->quoteValue($this->owner->getAttribute($this->pathAttribute)) . ', SUBSTR(' . $db->quoteColumnName($this->pathAttribute) . ', ' . $pathLength . '))'); |
506
|
|
|
} |
507
|
|
|
$result = $this->owner->updateAll($updateColumns, $condition, $params); |
508
|
|
|
} |
509
|
|
|
} |
510
|
|
|
|
511
|
|
|
if ($result && $nameChanged) { |
512
|
|
|
$result = $this->afterRenameNode($this->_previousPath); |
513
|
|
|
} |
514
|
|
|
} catch (\Exception $e) { |
515
|
|
|
if (isset($transaction)) { |
516
|
|
|
$transaction->rollback(); |
517
|
|
|
} |
518
|
|
|
throw $e; |
519
|
|
|
} |
520
|
|
|
|
521
|
|
|
if (isset($transaction)) { |
522
|
|
|
if (!$result) { |
523
|
|
|
$transaction->rollback(); |
524
|
|
|
} else { |
525
|
|
|
$transaction->commit(); |
526
|
|
|
} |
527
|
|
|
} |
528
|
|
|
|
529
|
|
|
$this->_previousPath = ''; |
530
|
|
|
} |
531
|
|
|
|
532
|
|
|
return $result; |
533
|
|
|
} |
534
|
|
|
|
535
|
|
|
|
536
|
|
|
/** |
537
|
|
|
* Create root node if multiple-root tree mode. |
538
|
|
|
* Update node if it's not new |
539
|
|
|
* |
540
|
|
|
* @param boolean $runValidation |
541
|
|
|
* whether to perform validation |
542
|
|
|
* @param array $attributes |
543
|
|
|
* list of attributes |
544
|
|
|
* @return boolean whether the saving succeeds |
545
|
|
|
*/ |
546
|
|
|
public function saveNode($runValidation = true, $attributes = null) |
547
|
|
|
{ |
548
|
|
|
return $this->save($runValidation, $attributes); |
549
|
|
|
} |
550
|
|
|
|
551
|
|
|
|
552
|
|
|
/** |
553
|
|
|
* Deletes node and it's descendants |
554
|
|
|
* |
555
|
|
|
* @throws Exception. |
556
|
|
|
* @throws \Exception. |
557
|
|
|
* @param boolean $hasParentModel |
558
|
|
|
* whether this method was called from the top level or by a parent |
559
|
|
|
* If false, it means the method was called at the top level |
560
|
|
|
* @param boolean $fromDeleteFull |
561
|
|
|
* has the delete() call come from deleteFull() or not |
562
|
|
|
* @return boolean did delete() successfully process |
563
|
|
|
*/ |
564
|
|
|
private function delete($hasParentModel = false, $fromDeleteFull = false) |
|
|
|
|
565
|
|
|
{ |
566
|
|
|
if ($this->owner->getIsNewRecord()) { |
567
|
|
|
throw new Exception('The node can\'t be deleted because it is new.'); |
568
|
|
|
} |
569
|
|
|
|
570
|
|
|
if ($this->getIsDeletedRecord()) { |
571
|
|
|
throw new Exception('The node can\'t be deleted because it is already deleted.'); |
572
|
|
|
} |
573
|
|
|
|
574
|
|
|
if (!$this->beforeDeleteNode()) { |
575
|
|
|
return false; |
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
$db = $this->owner->getDb(); |
579
|
|
|
|
580
|
|
|
if ($db->getTransaction() === null) { |
581
|
|
|
$transaction = $db->beginTransaction(); |
582
|
|
|
} |
583
|
|
|
|
584
|
|
|
if ($this->owner->hasAttribute($this->pathAttribute) && $this->owner->hasAttribute($this->nameAttribute)) { |
585
|
|
|
$this->_previousPath = $this->owner->getAttribute($this->pathAttribute); |
586
|
|
|
} |
587
|
|
|
|
588
|
|
|
try { |
589
|
|
|
|
590
|
|
|
$result = true; |
591
|
|
|
if (!$this->owner->isLeaf()) { |
|
|
|
|
592
|
|
|
|
593
|
|
|
$condition = $db->quoteColumnName($this->leftAttribute) . '>=' . $this->owner->getAttribute($this->leftAttribute) . ' AND ' . $db->quoteColumnName($this->rightAttribute) . '<=' . $this->owner->getAttribute($this->rightAttribute); |
594
|
|
|
|
595
|
|
|
if ($this->hasManyRoots) { |
596
|
|
|
$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=' . $this->owner->getAttribute($this->rootAttribute); |
597
|
|
|
} |
598
|
|
|
|
599
|
|
|
if (!$this->deleteIndividual) { |
600
|
|
|
|
601
|
|
|
$result = $this->owner->deleteAll($condition) > 0; |
602
|
|
|
|
603
|
|
|
} else { |
604
|
|
|
|
605
|
|
|
$nodes = $this->owner->descendants()->all(); |
|
|
|
|
606
|
|
|
foreach ($nodes as $node) { |
607
|
|
|
|
608
|
|
|
$node->setIgnoreEvents(true); |
609
|
|
|
if (method_exists($node, 'deleteFull')) { |
610
|
|
|
$result = $node->deleteFull($hasParentModel); |
611
|
|
|
} else { |
612
|
|
|
$result = $node->delete(); |
613
|
|
|
} |
614
|
|
|
$node->setIgnoreEvents(false); |
615
|
|
|
|
616
|
|
|
if (method_exists($node, 'hasActionErrors')) { |
617
|
|
|
if ($node->hasActionErrors()) { |
618
|
|
|
$this->owner->mergeActionErrors($node->getActionErrors()); |
619
|
|
|
} |
620
|
|
|
} |
621
|
|
|
|
622
|
|
|
if (method_exists($node, 'hasActionWarnings')) { |
623
|
|
|
if ($node->hasActionWarnings()) { |
624
|
|
|
$this->owner->mergeActionWarnings($node->getActionWarnings()); |
625
|
|
|
} |
626
|
|
|
} |
627
|
|
|
if (!$result) { |
628
|
|
|
break; |
629
|
|
|
} |
630
|
|
|
} |
631
|
|
|
} |
632
|
|
|
} |
633
|
|
|
|
634
|
|
|
if ($result) { |
635
|
|
|
|
636
|
|
|
$this->shiftLeftRight($this->owner->getAttribute($this->rightAttribute) + 1, $this->owner->getAttribute($this->leftAttribute) - $this->owner->getAttribute($this->rightAttribute) - 1); |
637
|
|
|
|
638
|
|
|
$left = $this->owner->getAttribute($this->leftAttribute); |
639
|
|
|
$right = $this->owner->getAttribute($this->rightAttribute); |
640
|
|
|
|
641
|
|
|
$this->_ignoreEvent = true; |
642
|
|
|
if (method_exists($this->owner, 'deleteFull')) { |
643
|
|
|
$result = $this->owner->deleteFull($hasParentModel); |
644
|
|
|
} else { |
645
|
|
|
$result = $this->owner->delete(); |
646
|
|
|
} |
647
|
|
|
$this->_ignoreEvent = false; |
648
|
|
|
|
649
|
|
|
$this->correctCachedOnDelete($left, $right); |
650
|
|
|
} |
651
|
|
|
|
652
|
|
|
if ($result) { |
653
|
|
|
$result = $this->afterDeleteNode($this->_previousPath); |
654
|
|
|
} |
655
|
|
|
|
656
|
|
|
$this->_previousPath = ''; |
657
|
|
|
|
658
|
|
|
if (!$result) { |
659
|
|
|
if (isset($transaction)) { |
660
|
|
|
$transaction->rollback(); |
661
|
|
|
} |
662
|
|
|
return false; |
663
|
|
|
} |
664
|
|
|
|
665
|
|
|
if (isset($transaction)) { |
666
|
|
|
$transaction->commit(); |
667
|
|
|
} |
668
|
|
|
} catch (\Exception $e) { |
669
|
|
|
if (isset($transaction)) { |
670
|
|
|
$transaction->rollback(); |
671
|
|
|
} |
672
|
|
|
|
673
|
|
|
throw $e; |
674
|
|
|
} |
675
|
|
|
$this->_previousPath = ''; |
676
|
|
|
return true; |
677
|
|
|
} |
678
|
|
|
|
679
|
|
|
|
680
|
|
|
/** |
681
|
|
|
* Deletes node and it's descendants. |
682
|
|
|
* |
683
|
|
|
* @param boolean $hasParentModel |
684
|
|
|
* whether this method was called from the top level or by a parent |
685
|
|
|
* If false, it means the method was called at the top level |
686
|
|
|
* @param boolean $fromDeleteFull |
687
|
|
|
* has the delete() call come from deleteFull() or not |
688
|
|
|
* @return boolean did deleteNode() successfully process |
689
|
|
|
*/ |
690
|
|
|
public function deleteNode($hasParentModel = false, $fromDeleteFull = false) |
691
|
|
|
{ |
692
|
|
|
return $this->delete($hasParentModel, $fromDeleteFull); |
693
|
|
|
} |
694
|
|
|
|
695
|
|
|
|
696
|
|
|
/** |
697
|
|
|
* Prepends node to target as first child |
698
|
|
|
* |
699
|
|
|
* @param ActiveRecord $target |
700
|
|
|
* the target |
701
|
|
|
* @param boolean $runValidation |
702
|
|
|
* [optional] whether to perform validation |
703
|
|
|
* @param array $attributes |
704
|
|
|
* [optional] list of attributes |
705
|
|
|
* @return boolean whether the prepending succeeds |
706
|
|
|
*/ |
707
|
|
|
public function prependTo($target, $runValidation = true, $attributes = null) |
708
|
|
|
{ |
709
|
|
|
if ($runValidation) { |
710
|
|
|
if (!$this->owner->validate($attributes)) { |
711
|
|
|
return false; |
712
|
|
|
} |
713
|
|
|
$runValidation = false; |
714
|
|
|
} |
715
|
|
|
$this->checkAndSetPath($target, true); |
716
|
|
|
return $this->addNode($target, $target->getAttribute($this->leftAttribute) + 1, 1, $runValidation, $attributes); |
|
|
|
|
717
|
|
|
} |
718
|
|
|
|
719
|
|
|
|
720
|
|
|
/** |
721
|
|
|
* Prepends target to node as first child |
722
|
|
|
* |
723
|
|
|
* @param ActiveRecord $target |
724
|
|
|
* the target |
725
|
|
|
* @param boolean $runValidation |
726
|
|
|
* [optional] whether to perform validation |
727
|
|
|
* @param array $attributes |
728
|
|
|
* [optional] list of attributes |
729
|
|
|
* @return boolean whether the prepending succeeds |
730
|
|
|
*/ |
731
|
|
|
public function prepend($target, $runValidation = true, $attributes = null) |
732
|
|
|
{ |
733
|
|
|
return $target->prependTo($this->owner, $runValidation, $attributes); |
|
|
|
|
734
|
|
|
} |
735
|
|
|
|
736
|
|
|
|
737
|
|
|
/** |
738
|
|
|
* Appends node to target as last child |
739
|
|
|
* |
740
|
|
|
* @param ActiveRecord $target |
741
|
|
|
* the target |
742
|
|
|
* @param boolean $runValidation |
743
|
|
|
* [optional] whether to perform validation |
744
|
|
|
* @param array $attributes |
745
|
|
|
* [optional] list of attributes |
746
|
|
|
* @return boolean whether the appending succeeds |
747
|
|
|
*/ |
748
|
|
|
public function appendTo($target, $runValidation = true, $attributes = null) |
749
|
|
|
{ |
750
|
|
|
if ($runValidation) { |
751
|
|
|
if (!$this->owner->validate($attributes)) { |
752
|
|
|
return false; |
753
|
|
|
} |
754
|
|
|
$runValidation = false; |
755
|
|
|
} |
756
|
|
|
$this->checkAndSetPath($target, true); |
757
|
|
|
return $this->addNode($target, $target->getAttribute($this->rightAttribute), 1, $runValidation, $attributes); |
|
|
|
|
758
|
|
|
} |
759
|
|
|
|
760
|
|
|
|
761
|
|
|
/** |
762
|
|
|
* Appends target to node as last child |
763
|
|
|
* |
764
|
|
|
* @param ActiveRecord $target |
765
|
|
|
* the target |
766
|
|
|
* @param boolean $runValidation |
767
|
|
|
* [optional] whether to perform validation |
768
|
|
|
* @param array $attributes |
769
|
|
|
* [optional] list of attributes |
770
|
|
|
* @return boolean whether the appending succeeds |
771
|
|
|
*/ |
772
|
|
|
public function append($target, $runValidation = true, $attributes = null) |
773
|
|
|
{ |
774
|
|
|
return $target->appendTo($this->owner, $runValidation, $attributes); |
|
|
|
|
775
|
|
|
} |
776
|
|
|
|
777
|
|
|
|
778
|
|
|
/** |
779
|
|
|
* Inserts node as previous sibling of target. |
780
|
|
|
* |
781
|
|
|
* @param ActiveRecord $target |
782
|
|
|
* the target. |
783
|
|
|
* @param boolean $runValidation |
784
|
|
|
* [optional] whether to perform validation |
785
|
|
|
* @param array $attributes |
786
|
|
|
* [optional] list of attributes |
787
|
|
|
* @param ActiveRecord $parent |
788
|
|
|
* [optional] parent node if already known |
789
|
|
|
* @return boolean whether the inserting succeeds. |
790
|
|
|
*/ |
791
|
|
|
public function insertBefore($target, $runValidation = true, $attributes = null, $parent = null) |
792
|
|
|
{ |
793
|
|
|
if ($runValidation) { |
794
|
|
|
if (!$this->owner->validate($attributes)) { |
795
|
|
|
return false; |
796
|
|
|
} |
797
|
|
|
$runValidation = false; |
798
|
|
|
} |
799
|
|
|
$this->checkAndSetPath($target, false, false, $parent); |
800
|
|
|
return $this->addNode($target, $target->getAttribute($this->leftAttribute), 0, $runValidation, $attributes); |
|
|
|
|
801
|
|
|
} |
802
|
|
|
|
803
|
|
|
|
804
|
|
|
/** |
805
|
|
|
* Inserts node as next sibling of target |
806
|
|
|
* |
807
|
|
|
* @param ActiveRecord $target |
808
|
|
|
* the target |
809
|
|
|
* @param boolean $runValidation |
810
|
|
|
* [optional] whether to perform validation |
811
|
|
|
* @param array $attributes |
812
|
|
|
* [optional] list of attributes |
813
|
|
|
* @param ActiveRecord $parent |
814
|
|
|
* [optional] parent node if already known |
815
|
|
|
* @return boolean whether the inserting succeeds |
816
|
|
|
*/ |
817
|
|
|
public function insertAfter($target, $runValidation = true, $attributes = null, $parent = null) |
818
|
|
|
{ |
819
|
|
|
if ($runValidation) { |
820
|
|
|
if (!$this->owner->validate($attributes)) { |
821
|
|
|
return false; |
822
|
|
|
} |
823
|
|
|
$runValidation = false; |
824
|
|
|
} |
825
|
|
|
$this->checkAndSetPath($target, false, false, $parent); |
826
|
|
|
return $this->addNode($target, $target->getAttribute($this->rightAttribute) + 1, 0, $runValidation, $attributes); |
|
|
|
|
827
|
|
|
} |
828
|
|
|
|
829
|
|
|
|
830
|
|
|
/** |
831
|
|
|
* Move node as previous sibling of target |
832
|
|
|
* |
833
|
|
|
* @param ActiveRecord $target |
834
|
|
|
* the target |
835
|
|
|
* @param ActiveRecord $parent |
836
|
|
|
* [optional] parent node if already known |
837
|
|
|
* @return boolean whether the moving succeeds |
838
|
|
|
*/ |
839
|
|
|
public function moveBefore($target, $parent = null) |
840
|
|
|
{ |
841
|
|
|
$this->checkAndSetPath($target, false, true, $parent); |
842
|
|
|
return $this->moveNode($target, $target->getAttribute($this->leftAttribute), 0); |
843
|
|
|
} |
844
|
|
|
|
845
|
|
|
|
846
|
|
|
/** |
847
|
|
|
* Move node as next sibling of target |
848
|
|
|
* |
849
|
|
|
* @param ActiveRecord $target |
850
|
|
|
* the target |
851
|
|
|
* @param ActiveRecord $parent |
852
|
|
|
* [optional] parent node if already known |
853
|
|
|
* @return boolean whether the moving succeeds |
854
|
|
|
*/ |
855
|
|
|
public function moveAfter($target, $parent = null) |
856
|
|
|
{ |
857
|
|
|
$this->checkAndSetPath($target, false, true, $parent); |
858
|
|
|
return $this->moveNode($target, $target->getAttribute($this->rightAttribute) + 1, 0); |
859
|
|
|
} |
860
|
|
|
|
861
|
|
|
|
862
|
|
|
/** |
863
|
|
|
* Move node as first child of target |
864
|
|
|
* |
865
|
|
|
* @param ActiveRecord $target |
866
|
|
|
* the target |
867
|
|
|
* @param ActiveRecord $parent |
868
|
|
|
* [optional] parent node if already known |
869
|
|
|
* @return boolean whether the moving succeeds |
870
|
|
|
*/ |
871
|
|
|
public function moveAsFirst($target, $parent = null) |
872
|
|
|
{ |
873
|
|
|
$this->checkAndSetPath($target, true, true, $parent); |
874
|
|
|
return $this->moveNode($target, $target->getAttribute($this->leftAttribute) + 1, 1); |
875
|
|
|
} |
876
|
|
|
|
877
|
|
|
|
878
|
|
|
/** |
879
|
|
|
* Move node as last child of target |
880
|
|
|
* |
881
|
|
|
* @param ActiveRecord $target |
882
|
|
|
* the target |
883
|
|
|
* @param ActiveRecord $parent |
884
|
|
|
* [optional] parent node if already known |
885
|
|
|
* @return boolean whether the moving succeeds |
886
|
|
|
*/ |
887
|
|
|
public function moveAsLast($target, $parent = null) |
888
|
|
|
{ |
889
|
|
|
$this->checkAndSetPath($target, true, true, $parent); |
890
|
|
|
return $this->moveNode($target, $target->getAttribute($this->rightAttribute), 1); |
891
|
|
|
} |
892
|
|
|
|
893
|
|
|
|
894
|
|
|
/** |
895
|
|
|
* Move node as new root |
896
|
|
|
* |
897
|
|
|
* @throws Exception |
898
|
|
|
* @return boolean whether the moving succeeds |
899
|
|
|
*/ |
900
|
|
|
public function moveAsRoot() |
901
|
|
|
{ |
902
|
|
|
if (!$this->hasManyRoots) { |
903
|
|
|
throw new Exception('Many roots mode is off.'); |
904
|
|
|
} |
905
|
|
|
|
906
|
|
|
if ($this->owner->getIsNewRecord()) { |
907
|
|
|
throw new Exception('The node should not be new record.'); |
908
|
|
|
} |
909
|
|
|
|
910
|
|
|
if ($this->getIsDeletedRecord()) { |
911
|
|
|
throw new Exception('The node should not be deleted.'); |
912
|
|
|
} |
913
|
|
|
|
914
|
|
|
if ($this->owner->isRoot()) { |
|
|
|
|
915
|
|
|
throw new Exception('The node already is root node.'); |
916
|
|
|
} |
917
|
|
|
|
918
|
|
|
if ($this->hasPaths) { |
919
|
|
|
throw new Exception('Paths not yet supported for moveAsRoot.'); |
920
|
|
|
} |
921
|
|
|
|
922
|
|
|
$db = $this->owner->getDb(); |
923
|
|
|
|
924
|
|
|
if ($db->getTransaction() === null) { |
925
|
|
|
$transaction = $db->beginTransaction(); |
926
|
|
|
} |
927
|
|
|
|
928
|
|
|
try { |
929
|
|
|
$left = $this->owner->getAttribute($this->leftAttribute); |
930
|
|
|
$right = $this->owner->getAttribute($this->rightAttribute); |
931
|
|
|
$levelDelta = 1 - $this->owner->getAttribute($this->levelAttribute); |
932
|
|
|
$delta = 1 - $left; |
933
|
|
|
$this->owner->updateAll([ |
934
|
|
|
$this->leftAttribute => new Expression($db->quoteColumnName($this->leftAttribute) . sprintf('%+d', $delta)), |
935
|
|
|
$this->rightAttribute => new Expression($db->quoteColumnName($this->rightAttribute) . sprintf('%+d', $delta)), |
936
|
|
|
$this->levelAttribute => new Expression($db->quoteColumnName($this->levelAttribute) . sprintf('%+d', $levelDelta)), |
937
|
|
|
$this->rootAttribute => $this->owner->getPrimaryKey() |
938
|
|
|
], $db->quoteColumnName($this->leftAttribute) . '>=' . $left . ' AND ' . $db->quoteColumnName($this->rightAttribute) . '<=' . $right . ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute, [ |
939
|
|
|
':' . $this->rootAttribute => $this->owner->getAttribute($this->rootAttribute) |
940
|
|
|
]); |
941
|
|
|
$this->shiftLeftRight($right + 1, $left - $right - 1); |
942
|
|
|
|
943
|
|
|
if (isset($transaction)) { |
944
|
|
|
$transaction->commit(); |
945
|
|
|
} |
946
|
|
|
|
947
|
|
|
$this->correctCachedOnMoveBetweenTrees(1, $levelDelta, $this->owner->getPrimaryKey()); |
948
|
|
|
} catch (\Exception $e) { |
949
|
|
|
if (isset($transaction)) { |
950
|
|
|
$transaction->rollback(); |
951
|
|
|
} |
952
|
|
|
|
953
|
|
|
throw $e; |
954
|
|
|
} |
955
|
|
|
|
956
|
|
|
return true; |
957
|
|
|
} |
958
|
|
|
|
959
|
|
|
|
960
|
|
|
/** |
961
|
|
|
* Check to see if this nested set supports path or not |
962
|
|
|
* |
963
|
|
|
* @param ActiveRecord $target |
964
|
|
|
* the target |
965
|
|
|
* @param boolean $isParent |
966
|
|
|
* is target the parent node |
967
|
|
|
* @param boolean $isMove |
968
|
|
|
* is this relating to a node move |
969
|
|
|
* @param boolean $isParent |
970
|
|
|
* is $target the parent of $this->owner |
971
|
|
|
* @param ActiveRecord $parent |
972
|
|
|
* [optional] parent node if already known |
973
|
|
|
*/ |
974
|
|
|
public function checkAndSetPath($target, $isParent = false, $isMove = false, $parent = null) |
|
|
|
|
975
|
|
|
{ |
976
|
|
|
if ($this->hasPaths) { |
977
|
|
|
if ($this->owner->hasAttribute($this->pathAttribute) && $this->owner->hasAttribute($this->nameAttribute)) { |
978
|
|
|
$this->_previousPath = $this->owner->getAttribute($this->pathAttribute); |
979
|
|
|
$this->owner->setAttribute($this->pathAttribute, $this->calculatePath($target, $isParent, $parent)); |
980
|
|
|
} |
981
|
|
|
} |
982
|
|
|
} |
983
|
|
|
|
984
|
|
|
|
985
|
|
|
/** |
986
|
|
|
* Calculate path based on name and target |
987
|
|
|
* |
988
|
|
|
* @param ActiveRecord $target |
989
|
|
|
* the target |
990
|
|
|
* @param boolean $isParent |
991
|
|
|
* is target the parent node |
992
|
|
|
* @param ActiveRecord $parent |
993
|
|
|
* [optional] parent node if already known |
994
|
|
|
* @return string |
995
|
|
|
*/ |
996
|
|
|
public function calculatePath($target, $isParent = false, $parent = null) |
997
|
|
|
{ |
998
|
|
|
$uniqueNames = false; |
999
|
|
|
if (method_exists($this->owner, 'getIsUniqueNames')) { |
1000
|
|
|
$uniqueNames = $this->owner->getIsUniqueNames(); |
|
|
|
|
1001
|
|
|
} |
1002
|
|
|
|
1003
|
|
|
if ($this->hasPaths || $uniqueNames) { |
1004
|
|
|
if (!$isParent && $parent) { |
1005
|
|
|
$target = $parent; |
1006
|
|
|
} elseif (!$isParent) { |
1007
|
|
|
$target = $target->parentOnly()->one(); |
|
|
|
|
1008
|
|
|
} |
1009
|
|
|
} |
1010
|
|
|
if ($this->hasPaths) { |
1011
|
|
|
if ($target->getAttribute($this->pathAttribute) == '/') { |
1012
|
|
|
$path = '/' . $this->owner->getAttribute($this->nameAttribute); |
1013
|
|
|
} else { |
1014
|
|
|
$path = $target->getAttribute($this->pathAttribute) . '/' . $this->owner->getAttribute($this->nameAttribute); |
1015
|
|
|
} |
1016
|
|
|
} else { |
1017
|
|
|
$path = ''; |
1018
|
|
|
} |
1019
|
|
|
if ($uniqueNames) { |
1020
|
|
|
$matches = $this->children($target)->andWhere([ |
1021
|
|
|
$this->nameAttribute => $this->owner->getAttribute($this->nameAttribute) |
1022
|
|
|
]); |
1023
|
|
|
if (!$this->owner->getIsNewRecord()) { |
1024
|
|
|
$matches->andWhere('id != ' . $this->owner->id); |
|
|
|
|
1025
|
|
|
} |
1026
|
|
|
if ($matches->count()) { |
1027
|
|
|
$path = '__DUPLICATE__'; |
1028
|
|
|
} |
1029
|
|
|
} |
1030
|
|
|
return $path; |
1031
|
|
|
} |
1032
|
|
|
|
1033
|
|
|
|
1034
|
|
|
/** |
1035
|
|
|
* Determines if node is descendant of subject node |
1036
|
|
|
* |
1037
|
|
|
* @param ActiveRecord $subj |
1038
|
|
|
* the subject node |
1039
|
|
|
* @param ActiveRecord $object |
1040
|
|
|
* [optional] defaults to $this->owner |
1041
|
|
|
* @return boolean whether the node is descendant of subject node |
1042
|
|
|
*/ |
1043
|
|
|
public function isDescendantOf($subj, $object = null) |
1044
|
|
|
{ |
1045
|
|
|
$object = (!is_null($object) ? $object : $this->owner); |
1046
|
|
|
$result = ($object->getAttribute($this->leftAttribute) > $subj->getAttribute($this->leftAttribute)) && ($object->getAttribute($this->rightAttribute) < $subj->getAttribute($this->rightAttribute)); |
1047
|
|
|
|
1048
|
|
|
if ($this->hasManyRoots) { |
1049
|
|
|
$result = $result && ($object->getAttribute($this->rootAttribute) === $subj->getAttribute($this->rootAttribute)); |
1050
|
|
|
} |
1051
|
|
|
|
1052
|
|
|
return $result; |
1053
|
|
|
} |
1054
|
|
|
|
1055
|
|
|
|
1056
|
|
|
/** |
1057
|
|
|
* Determines if node is leaf |
1058
|
|
|
* |
1059
|
|
|
* @param ActiveRecord $object |
1060
|
|
|
* [optional] defaults to $this->owner |
1061
|
|
|
* @return boolean whether the node is leaf |
1062
|
|
|
*/ |
1063
|
|
|
public function isLeaf($object = null) |
1064
|
|
|
{ |
1065
|
|
|
$object = (!is_null($object) ? $object : $this->owner); |
1066
|
|
|
return $object->getAttribute($this->rightAttribute) - $object->getAttribute($this->leftAttribute) === 1; |
1067
|
|
|
} |
1068
|
|
|
|
1069
|
|
|
|
1070
|
|
|
/** |
1071
|
|
|
* Determines if node is root |
1072
|
|
|
* |
1073
|
|
|
* @param ActiveRecord $object |
1074
|
|
|
* [optional] defaults to $this->owner |
1075
|
|
|
* @return boolean whether the node is root |
1076
|
|
|
*/ |
1077
|
|
|
public function isRoot($object = null) |
1078
|
|
|
{ |
1079
|
|
|
$object = (!is_null($object) ? $object : $this->owner); |
1080
|
|
|
return $object->getAttribute($this->leftAttribute) == 1; |
1081
|
|
|
} |
1082
|
|
|
|
1083
|
|
|
|
1084
|
|
|
/** |
1085
|
|
|
* Returns if the current node is deleted |
1086
|
|
|
* |
1087
|
|
|
* @return boolean whether the node is deleted |
1088
|
|
|
*/ |
1089
|
|
|
public function getIsDeletedRecord() |
1090
|
|
|
{ |
1091
|
|
|
return $this->_deleted; |
1092
|
|
|
} |
1093
|
|
|
|
1094
|
|
|
|
1095
|
|
|
/** |
1096
|
|
|
* Sets if the current node is deleted |
1097
|
|
|
* |
1098
|
|
|
* @param boolean $value |
1099
|
|
|
* whether the node is deleted |
1100
|
|
|
*/ |
1101
|
|
|
public function setIsDeletedRecord($value) |
1102
|
|
|
{ |
1103
|
|
|
$this->_deleted = $value; |
1104
|
|
|
} |
1105
|
|
|
|
1106
|
|
|
|
1107
|
|
|
/** |
1108
|
|
|
* Handle 'afterFind' event of the owner |
1109
|
|
|
* |
1110
|
|
|
* @param ModelEvent $event |
1111
|
|
|
* event parameter |
1112
|
|
|
*/ |
1113
|
|
|
public function afterFind($event) |
|
|
|
|
1114
|
|
|
{ |
1115
|
|
|
self::$_cached[get_class($this->owner)][$this->_id = self::$_c++] = $this->owner; |
1116
|
|
|
} |
1117
|
|
|
|
1118
|
|
|
|
1119
|
|
|
/** |
1120
|
|
|
* Handle 'beforeInsert' event of the owner |
1121
|
|
|
* |
1122
|
|
|
* @param ModelEvent $event |
1123
|
|
|
* event parameter |
1124
|
|
|
* @throws Exception |
1125
|
|
|
* @return boolean |
1126
|
|
|
*/ |
1127
|
|
|
public function beforeInsert($event) |
|
|
|
|
1128
|
|
|
{ |
1129
|
|
|
if ($this->_ignoreEvent) { |
1130
|
|
|
return true; |
1131
|
|
|
} else { |
1132
|
|
|
throw new Exception('You should not use ActiveRecord::save() or ActiveRecord::insert() methods when NestedSetBehavior attached.'); |
1133
|
|
|
} |
1134
|
|
|
} |
1135
|
|
|
|
1136
|
|
|
|
1137
|
|
|
/** |
1138
|
|
|
* Handle 'beforeUpdate' event of the owner |
1139
|
|
|
* |
1140
|
|
|
* @param ModelEvent $event |
1141
|
|
|
* event parameter |
1142
|
|
|
* @throws Exception |
1143
|
|
|
* @return boolean |
1144
|
|
|
*/ |
1145
|
|
|
public function beforeUpdate($event) |
|
|
|
|
1146
|
|
|
{ |
1147
|
|
|
if ($this->_ignoreEvent) { |
1148
|
|
|
return true; |
1149
|
|
|
} else { |
1150
|
|
|
throw new Exception('You should not use ActiveRecord::save() or ActiveRecord::update() methods when NestedSetBehavior attached.'); |
1151
|
|
|
} |
1152
|
|
|
} |
1153
|
|
|
|
1154
|
|
|
|
1155
|
|
|
/** |
1156
|
|
|
* Handle 'beforeDelete' event of the owner |
1157
|
|
|
* |
1158
|
|
|
* @param ModelEvent $event |
1159
|
|
|
* event parameter |
1160
|
|
|
* @throws Exception |
1161
|
|
|
* @return boolean |
1162
|
|
|
*/ |
1163
|
|
|
public function beforeDelete($event) |
|
|
|
|
1164
|
|
|
{ |
1165
|
|
|
if ($this->_ignoreEvent) { |
1166
|
|
|
return true; |
1167
|
|
|
} else { |
1168
|
|
|
throw new Exception('You should not use ActiveRecord::delete() method when NestedSetBehavior behavior attached.'); |
1169
|
|
|
} |
1170
|
|
|
} |
1171
|
|
|
|
1172
|
|
|
|
1173
|
|
|
/** |
1174
|
|
|
* Handle 'beforeSaveAll' event of the owner |
1175
|
|
|
* |
1176
|
|
|
* @param ModelEvent $event |
1177
|
|
|
* event parameter |
1178
|
|
|
* @throws Exception |
1179
|
|
|
* @return boolean |
1180
|
|
|
*/ |
1181
|
|
|
public function beforeSaveAll($event) |
|
|
|
|
1182
|
|
|
{ |
1183
|
|
|
if ($this->_ignoreEvent) { |
1184
|
|
|
return true; |
1185
|
|
|
} elseif ($this->owner->getIsNewRecord()) { |
1186
|
|
|
throw new Exception('You should not use ActiveRecord::saveAll() on new records when NestedSetBehavior attached.'); |
1187
|
|
|
} |
1188
|
|
|
} |
1189
|
|
|
|
1190
|
|
|
|
1191
|
|
|
/** |
1192
|
|
|
* Handle 'beforeDeleteFull' event of the owner |
1193
|
|
|
* |
1194
|
|
|
* @param ModelEvent $event |
1195
|
|
|
* event parameter |
1196
|
|
|
* @throws Exception |
1197
|
|
|
* @return boolean |
1198
|
|
|
*/ |
1199
|
|
|
public function beforeDeleteFull($event) |
|
|
|
|
1200
|
|
|
{ |
1201
|
|
|
if ($this->_ignoreEvent) { |
1202
|
|
|
return true; |
1203
|
|
|
} else { |
1204
|
|
|
throw new Exception('You should not use ActiveRecord::beforeDeleteFull() method when NestedSetBehavior attached.'); |
1205
|
|
|
} |
1206
|
|
|
} |
1207
|
|
|
|
1208
|
|
|
|
1209
|
|
|
/** |
1210
|
|
|
* |
1211
|
|
|
* @param integer $key. |
|
|
|
|
1212
|
|
|
* @param integer $delta. |
|
|
|
|
1213
|
|
|
*/ |
1214
|
|
|
private function shiftLeftRight($key, $delta) |
1215
|
|
|
{ |
1216
|
|
|
$db = $this->owner->getDb(); |
1217
|
|
|
|
1218
|
|
|
foreach ([ |
1219
|
|
|
$this->leftAttribute, |
1220
|
|
|
$this->rightAttribute |
1221
|
|
|
] as $attribute) { |
1222
|
|
|
$condition = $db->quoteColumnName($attribute) . '>=' . $key; |
1223
|
|
|
$params = []; |
1224
|
|
|
|
1225
|
|
|
if ($this->hasManyRoots) { |
1226
|
|
|
$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute; |
1227
|
|
|
$params[':' . $this->rootAttribute] = $this->owner->getAttribute($this->rootAttribute); |
1228
|
|
|
} |
1229
|
|
|
|
1230
|
|
|
$this->owner->updateAll([ |
1231
|
|
|
$attribute => new Expression($db->quoteColumnName($attribute) . sprintf('%+d', $delta)) |
1232
|
|
|
], $condition, $params); |
1233
|
|
|
} |
1234
|
|
|
} |
1235
|
|
|
|
1236
|
|
|
|
1237
|
|
|
/** |
1238
|
|
|
* |
1239
|
|
|
* @param ActiveRecord $target |
1240
|
|
|
* @param int $key |
1241
|
|
|
* @param int $levelUp |
1242
|
|
|
* @param boolean $runValidation |
1243
|
|
|
* @param array $attributes |
1244
|
|
|
* @throws Exception |
1245
|
|
|
* @return boolean |
1246
|
|
|
*/ |
1247
|
|
|
private function addNode($target, $key, $levelUp, $runValidation, $attributes) |
1248
|
|
|
{ |
1249
|
|
|
if (!$this->owner->getIsNewRecord()) { |
1250
|
|
|
throw new Exception('The node can\'t be inserted because it is not new.'); |
1251
|
|
|
} |
1252
|
|
|
|
1253
|
|
|
if ($this->getIsDeletedRecord()) { |
1254
|
|
|
throw new Exception('The node can\'t be inserted because it is deleted.'); |
1255
|
|
|
} |
1256
|
|
|
|
1257
|
|
|
if ($target->getIsDeletedRecord()) { |
|
|
|
|
1258
|
|
|
throw new Exception('The node can\'t be inserted because target node is deleted.'); |
1259
|
|
|
} |
1260
|
|
|
|
1261
|
|
|
if ($this->owner->equals($target)) { |
1262
|
|
|
throw new Exception('The target node should not be self.'); |
1263
|
|
|
} |
1264
|
|
|
|
1265
|
|
|
if (!$levelUp && $target->isRoot()) { |
|
|
|
|
1266
|
|
|
throw new Exception('The target node should not be root.'); |
1267
|
|
|
} |
1268
|
|
|
|
1269
|
|
|
if ($this->hasPaths && $this->owner->getAttribute($this->pathAttribute) == '__DUPLICATE__') { |
1270
|
|
|
throw new Exception('New node has duplicate path.'); |
1271
|
|
|
} |
1272
|
|
|
|
1273
|
|
|
if ($runValidation && !$this->owner->validate($attributes)) { |
1274
|
|
|
return false; |
1275
|
|
|
} |
1276
|
|
|
|
1277
|
|
|
if (!$this->beforeAddNode()) { |
1278
|
|
|
return false; |
1279
|
|
|
} |
1280
|
|
|
|
1281
|
|
|
if ($this->hasManyRoots) { |
1282
|
|
|
$this->owner->setAttribute($this->rootAttribute, $target->getAttribute($this->rootAttribute)); |
1283
|
|
|
} |
1284
|
|
|
|
1285
|
|
|
$db = $this->owner->getDb(); |
1286
|
|
|
|
1287
|
|
|
if ($db->getTransaction() === null) { |
1288
|
|
|
$transaction = $db->beginTransaction(); |
1289
|
|
|
} |
1290
|
|
|
|
1291
|
|
|
try { |
1292
|
|
|
$this->shiftLeftRight($key, 2); |
1293
|
|
|
$this->owner->setAttribute($this->leftAttribute, $key); |
1294
|
|
|
$this->owner->setAttribute($this->rightAttribute, $key + 1); |
1295
|
|
|
$this->owner->setAttribute($this->levelAttribute, $target->getAttribute($this->levelAttribute) + $levelUp); |
1296
|
|
|
$this->_ignoreEvent = true; |
1297
|
|
|
//$result = $this->owner->insert(false, $attributes); |
1298
|
|
|
if (method_exists($this->owner, 'saveAll')) { |
1299
|
|
|
$result = $this->owner->saveAll(false, false, false, $attributes); |
1300
|
|
|
} else { |
1301
|
|
|
$result = $this->owner->save(false, $attributes); |
1302
|
|
|
} |
1303
|
|
|
$this->_ignoreEvent = false; |
1304
|
|
|
|
1305
|
|
|
if ($result) { |
1306
|
|
|
$result = $this->afterAddNode(); |
1307
|
|
|
} |
1308
|
|
|
|
1309
|
|
|
if (!$result) { |
1310
|
|
|
if (isset($transaction)) { |
1311
|
|
|
$transaction->rollback(); |
1312
|
|
|
} |
1313
|
|
|
return false; |
1314
|
|
|
} |
1315
|
|
|
|
1316
|
|
|
$this->owner->setIsNewRecord(false); |
1317
|
|
|
|
1318
|
|
|
if (isset($transaction)) { |
1319
|
|
|
$transaction->commit(); |
1320
|
|
|
} |
1321
|
|
|
|
1322
|
|
|
$this->correctCachedOnAddNode($key); |
1323
|
|
|
} catch (\Exception $e) { |
1324
|
|
|
if (isset($transaction)) { |
1325
|
|
|
$transaction->rollback(); |
1326
|
|
|
} |
1327
|
|
|
throw $e; |
1328
|
|
|
} |
1329
|
|
|
|
1330
|
|
|
return true; |
1331
|
|
|
} |
1332
|
|
|
|
1333
|
|
|
|
1334
|
|
|
/** |
1335
|
|
|
* |
1336
|
|
|
* @param array $attributes |
1337
|
|
|
* @throws Exception |
1338
|
|
|
* @return boolean |
1339
|
|
|
*/ |
1340
|
|
|
private function makeRoot($attributes) |
1341
|
|
|
{ |
1342
|
|
|
$this->owner->setAttribute($this->leftAttribute, 1); |
1343
|
|
|
$this->owner->setAttribute($this->rightAttribute, 2); |
1344
|
|
|
$this->owner->setAttribute($this->levelAttribute, 1); |
1345
|
|
|
if ($this->hasPaths && $this->owner->hasAttribute($this->pathAttribute) && $this->owner->getAttribute($this->pathAttribute) == '') { |
1346
|
|
|
$this->owner->setAttribute($this->pathAttribute, '/'); |
1347
|
|
|
} |
1348
|
|
|
|
1349
|
|
|
if ($this->hasManyRoots) { |
1350
|
|
|
$db = $this->owner->getDb(); |
1351
|
|
|
|
1352
|
|
|
if ($db->getTransaction() === null) { |
1353
|
|
|
$transaction = $db->beginTransaction(); |
1354
|
|
|
} |
1355
|
|
|
|
1356
|
|
|
try { |
1357
|
|
|
$this->_ignoreEvent = true; |
1358
|
|
|
//$result = $this->owner->insert(false, $attributes); |
1359
|
|
|
if (method_exists($this->owner, 'saveAll')) { |
1360
|
|
|
$result = $this->owner->saveAll(false, false, false, $attributes); |
1361
|
|
|
} else { |
1362
|
|
|
$result = $this->owner->save(false, $attributes); |
1363
|
|
|
} |
1364
|
|
|
$this->_ignoreEvent = false; |
1365
|
|
|
|
1366
|
|
|
if (!$result) { |
1367
|
|
|
if (isset($transaction)) { |
1368
|
|
|
$transaction->rollback(); |
1369
|
|
|
} |
1370
|
|
|
|
1371
|
|
|
return false; |
1372
|
|
|
} |
1373
|
|
|
|
1374
|
|
|
$this->owner->setIsNewRecord(false); |
1375
|
|
|
|
1376
|
|
|
$this->owner->setAttribute($this->rootAttribute, $this->owner->getPrimaryKey()); |
1377
|
|
|
$primaryKey = $this->owner->primaryKey(); |
1378
|
|
|
|
1379
|
|
|
if (!isset($primaryKey[0])) { |
1380
|
|
|
throw new Exception(get_class($this->owner) . ' must have a primary key.'); |
1381
|
|
|
} |
1382
|
|
|
|
1383
|
|
|
$this->owner->updateAll([ |
1384
|
|
|
$this->rootAttribute => $this->owner->getAttribute($this->rootAttribute) |
1385
|
|
|
], [ |
1386
|
|
|
$primaryKey[0] => $this->owner->getAttribute($this->rootAttribute) |
1387
|
|
|
]); |
1388
|
|
|
|
1389
|
|
|
if (isset($transaction)) { |
1390
|
|
|
$transaction->commit(); |
1391
|
|
|
} |
1392
|
|
|
} catch (\Exception $e) { |
1393
|
|
|
if (isset($transaction)) { |
1394
|
|
|
$transaction->rollback(); |
1395
|
|
|
} |
1396
|
|
|
|
1397
|
|
|
throw $e; |
1398
|
|
|
} |
1399
|
|
|
} else { |
1400
|
|
|
if ($this->owner->find() |
|
|
|
|
1401
|
|
|
->roots() |
1402
|
|
|
->exists()) { |
1403
|
|
|
throw new Exception('Can\'t create more than one root in single root mode.'); |
1404
|
|
|
} |
1405
|
|
|
|
1406
|
|
|
$this->_ignoreEvent = true; |
1407
|
|
|
//$result = $this->owner->insert(false, $attributes); |
1408
|
|
|
if (method_exists($this->owner, 'saveAll')) { |
1409
|
|
|
$result = $this->owner->saveAll(false, false, false, $attributes); |
1410
|
|
|
} else { |
1411
|
|
|
$result = $this->owner->save(false, $attributes); |
1412
|
|
|
} |
1413
|
|
|
$this->_ignoreEvent = false; |
1414
|
|
|
|
1415
|
|
|
if (!$result) { |
1416
|
|
|
return false; |
1417
|
|
|
} |
1418
|
|
|
|
1419
|
|
|
$this->owner->setIsNewRecord(false); |
1420
|
|
|
} |
1421
|
|
|
|
1422
|
|
|
return true; |
1423
|
|
|
} |
1424
|
|
|
|
1425
|
|
|
|
1426
|
|
|
/** |
1427
|
|
|
* |
1428
|
|
|
* @param ActiveRecord $target |
1429
|
|
|
* @param int $key |
1430
|
|
|
* @param int $levelUp |
1431
|
|
|
* @throws Exception |
1432
|
|
|
* @return boolean |
1433
|
|
|
*/ |
1434
|
|
|
private function moveNode($target, $key, $levelUp) |
1435
|
|
|
{ |
1436
|
|
|
if ($this->owner->getIsNewRecord()) { |
1437
|
|
|
throw new Exception('The node should not be new record.'); |
1438
|
|
|
} |
1439
|
|
|
|
1440
|
|
|
if ($this->getIsDeletedRecord()) { |
1441
|
|
|
throw new Exception('The node should not be deleted.'); |
1442
|
|
|
} |
1443
|
|
|
|
1444
|
|
|
if ($target->getIsDeletedRecord()) { |
|
|
|
|
1445
|
|
|
throw new Exception('The target node should not be deleted.'); |
1446
|
|
|
} |
1447
|
|
|
|
1448
|
|
|
if ($this->owner->equals($target)) { |
1449
|
|
|
throw new Exception('The target node should not be self.'); |
1450
|
|
|
} |
1451
|
|
|
|
1452
|
|
|
if ($target->isDescendantOf($this->owner)) { |
|
|
|
|
1453
|
|
|
throw new Exception('The target node should not be descendant.'); |
1454
|
|
|
} |
1455
|
|
|
|
1456
|
|
|
if (!$levelUp && $target->isRoot()) { |
|
|
|
|
1457
|
|
|
throw new Exception('The target node should not be root.'); |
1458
|
|
|
} |
1459
|
|
|
|
1460
|
|
|
if (!$this->beforeMoveNode($this->_previousPath)) { |
1461
|
|
|
return false; |
1462
|
|
|
} |
1463
|
|
|
|
1464
|
|
|
$db = $this->owner->getDb(); |
1465
|
|
|
|
1466
|
|
|
if ($db->getTransaction() === null) { |
1467
|
|
|
$transaction = $db->beginTransaction(); |
1468
|
|
|
} |
1469
|
|
|
|
1470
|
|
|
try { |
1471
|
|
|
$left = $this->owner->getAttribute($this->leftAttribute); |
1472
|
|
|
$right = $this->owner->getAttribute($this->rightAttribute); |
1473
|
|
|
$levelDelta = $target->getAttribute($this->levelAttribute) - $this->owner->getAttribute($this->levelAttribute) + $levelUp; |
1474
|
|
|
|
1475
|
|
|
if ($this->hasManyRoots && $this->owner->getAttribute($this->rootAttribute) !== $target->getAttribute($this->rootAttribute)) { |
1476
|
|
|
|
1477
|
|
|
foreach ([ |
1478
|
|
|
$this->leftAttribute, |
1479
|
|
|
$this->rightAttribute |
1480
|
|
|
] as $attribute) { |
1481
|
|
|
$this->owner->updateAll([ |
1482
|
|
|
$attribute => new Expression($db->quoteColumnName($attribute) . sprintf('%+d', $right - $left + 1)) |
1483
|
|
|
], $db->quoteColumnName($attribute) . '>=' . $key . ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute, [ |
1484
|
|
|
':' . $this->rootAttribute => $target->getAttribute($this->rootAttribute) |
1485
|
|
|
]); |
1486
|
|
|
} |
1487
|
|
|
|
1488
|
|
|
$delta = $key - $left; |
1489
|
|
|
$this->owner->updateAll([ |
1490
|
|
|
$this->leftAttribute => new Expression($db->quoteColumnName($this->leftAttribute) . sprintf('%+d', $delta)), |
1491
|
|
|
$this->rightAttribute => new Expression($db->quoteColumnName($this->rightAttribute) . sprintf('%+d', $delta)), |
1492
|
|
|
$this->levelAttribute => new Expression($db->quoteColumnName($this->levelAttribute) . sprintf('%+d', $levelDelta)), |
1493
|
|
|
$this->rootAttribute => $target->getAttribute($this->rootAttribute) |
1494
|
|
|
], $db->quoteColumnName($this->leftAttribute) . '>=' . $left . ' AND ' . $db->quoteColumnName($this->rightAttribute) . '<=' . $right . ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute, [ |
1495
|
|
|
':' . $this->rootAttribute => $this->owner->getAttribute($this->rootAttribute) |
1496
|
|
|
]); |
1497
|
|
|
$this->shiftLeftRight($right + 1, $left - $right - 1); |
1498
|
|
|
|
1499
|
|
|
if (isset($transaction)) { |
1500
|
|
|
$transaction->commit(); |
1501
|
|
|
} |
1502
|
|
|
|
1503
|
|
|
$this->correctCachedOnMoveBetweenTrees($key, $levelDelta, $target->getAttribute($this->rootAttribute)); |
1504
|
|
|
} else { |
1505
|
|
|
$delta = $right - $left + 1; |
1506
|
|
|
$this->shiftLeftRight($key, $delta); |
1507
|
|
|
|
1508
|
|
|
if ($left >= $key) { |
1509
|
|
|
$left += $delta; |
1510
|
|
|
$right += $delta; |
1511
|
|
|
} |
1512
|
|
|
|
1513
|
|
|
$condition = $db->quoteColumnName($this->leftAttribute) . '>=' . $left . ' AND ' . $db->quoteColumnName($this->rightAttribute) . '<=' . $right; |
1514
|
|
|
$params = []; |
1515
|
|
|
|
1516
|
|
|
if ($this->hasManyRoots) { |
1517
|
|
|
$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute; |
1518
|
|
|
$params[':' . $this->rootAttribute] = $this->owner->getAttribute($this->rootAttribute); |
1519
|
|
|
} |
1520
|
|
|
|
1521
|
|
|
$updateColumns = []; |
1522
|
|
|
$updateColumns[$this->levelAttribute] = new Expression($db->quoteColumnName($this->levelAttribute) . sprintf('%+d', $levelDelta)); |
1523
|
|
|
|
1524
|
|
|
if ($this->hasPaths && $this->owner->hasAttribute($this->pathAttribute)) { |
1525
|
|
|
$pathLength = Tools::strlen($this->_previousPath) + 1; |
1526
|
|
|
// SQL Server: SUBSTRING() rather than SUBSTR |
1527
|
|
|
// SQL Server: + instead of CONCAT |
1528
|
|
|
if ($db->getDriverName() == 'mssql') { |
1529
|
|
|
$updateColumns[$this->pathAttribute] = new Expression($db->quoteValue($this->owner->getAttribute($this->pathAttribute)) . ' + SUBSTRING(' . $db->quoteColumnName($this->pathAttribute) . ', ' . $pathLength . '))'); |
1530
|
|
|
} else { |
1531
|
|
|
$updateColumns[$this->pathAttribute] = new Expression('CONCAT(' . $db->quoteValue($this->owner->getAttribute($this->pathAttribute)) . ', SUBSTR(' . $db->quoteColumnName($this->pathAttribute) . ', ' . $pathLength . '))'); |
1532
|
|
|
} |
1533
|
|
|
} |
1534
|
|
|
|
1535
|
|
|
$this->owner->updateAll($updateColumns, $condition, $params); |
1536
|
|
|
|
1537
|
|
|
foreach ([ |
1538
|
|
|
$this->leftAttribute, |
1539
|
|
|
$this->rightAttribute |
1540
|
|
|
] as $attribute) { |
1541
|
|
|
$condition = $db->quoteColumnName($attribute) . '>=' . $left . ' AND ' . $db->quoteColumnName($attribute) . '<=' . $right; |
1542
|
|
|
$params = []; |
1543
|
|
|
|
1544
|
|
|
if ($this->hasManyRoots) { |
1545
|
|
|
$condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute; |
1546
|
|
|
$params[':' . $this->rootAttribute] = $this->owner->getAttribute($this->rootAttribute); |
1547
|
|
|
} |
1548
|
|
|
|
1549
|
|
|
$this->owner->updateAll([ |
1550
|
|
|
$attribute => new Expression($db->quoteColumnName($attribute) . sprintf('%+d', $key - $left)) |
1551
|
|
|
], $condition, $params); |
1552
|
|
|
} |
1553
|
|
|
|
1554
|
|
|
$this->shiftLeftRight($right + 1, -$delta); |
1555
|
|
|
|
1556
|
|
|
$result = $this->afterMoveNode($this->_previousPath); |
1557
|
|
|
|
1558
|
|
|
if (isset($transaction)) { |
1559
|
|
|
if ($result) { |
1560
|
|
|
$transaction->commit(); |
1561
|
|
|
} else { |
1562
|
|
|
$transaction->rollback(); |
1563
|
|
|
$this->_previousPath = ''; |
1564
|
|
|
return false; |
1565
|
|
|
} |
1566
|
|
|
} |
1567
|
|
|
|
1568
|
|
|
$this->correctCachedOnMoveNode($key, $levelDelta); |
1569
|
|
|
} |
1570
|
|
|
} catch (\Exception $e) { |
1571
|
|
|
if (isset($transaction)) { |
1572
|
|
|
$transaction->rollback(); |
1573
|
|
|
} |
1574
|
|
|
|
1575
|
|
|
throw $e; |
1576
|
|
|
} |
1577
|
|
|
|
1578
|
|
|
$this->_previousPath = ''; |
1579
|
|
|
|
1580
|
|
|
return true; |
1581
|
|
|
} |
1582
|
|
|
|
1583
|
|
|
|
1584
|
|
|
/** |
1585
|
|
|
* Correct cache for [[delete()]] and [[deleteNode()]]. |
1586
|
|
|
* |
1587
|
|
|
* @param integer $left |
1588
|
|
|
* @param integer $right |
1589
|
|
|
*/ |
1590
|
|
|
private function correctCachedOnDelete($left, $right) |
1591
|
|
|
{ |
1592
|
|
|
$key = $right + 1; |
1593
|
|
|
$delta = $left - $right - 1; |
1594
|
|
|
foreach (self::$_cached[get_class($this->owner)] as $node) { |
1595
|
|
|
/** @var $node ActiveRecord */ |
1596
|
|
|
if ($node->getIsNewRecord() || $node->getIsDeletedRecord()) { |
|
|
|
|
1597
|
|
|
continue; |
1598
|
|
|
} |
1599
|
|
|
|
1600
|
|
|
if ($this->hasManyRoots && $this->owner->getAttribute($this->rootAttribute) !== $node->getAttribute($this->rootAttribute)) { |
1601
|
|
|
continue; |
1602
|
|
|
} |
1603
|
|
|
|
1604
|
|
|
if ($node->getAttribute($this->leftAttribute) >= $left && $node->getAttribute($this->rightAttribute) <= $right) { |
1605
|
|
|
$node->setIsDeletedRecord(true); |
|
|
|
|
1606
|
|
|
} else { |
1607
|
|
|
if ($node->getAttribute($this->leftAttribute) >= $key) { |
1608
|
|
|
$node->setAttribute($this->leftAttribute, $node->getAttribute($this->leftAttribute) + $delta); |
1609
|
|
|
} |
1610
|
|
|
|
1611
|
|
|
if ($node->getAttribute($this->rightAttribute) >= $key) { |
1612
|
|
|
$node->setAttribute($this->rightAttribute, $node->getAttribute($this->rightAttribute) + $delta); |
1613
|
|
|
} |
1614
|
|
|
} |
1615
|
|
|
} |
1616
|
|
|
} |
1617
|
|
|
|
1618
|
|
|
|
1619
|
|
|
/** |
1620
|
|
|
* Correct cache for [[addNode()]] |
1621
|
|
|
* |
1622
|
|
|
* @param int $key |
1623
|
|
|
*/ |
1624
|
|
|
private function correctCachedOnAddNode($key) |
1625
|
|
|
{ |
1626
|
|
|
foreach (self::$_cached[get_class($this->owner)] as $node) { |
1627
|
|
|
/** @var $node ActiveRecord */ |
1628
|
|
|
if ($node->getIsNewRecord() || $node->getIsDeletedRecord()) { |
|
|
|
|
1629
|
|
|
continue; |
1630
|
|
|
} |
1631
|
|
|
|
1632
|
|
|
if ($this->hasManyRoots && $this->owner->getAttribute($this->rootAttribute) !== $node->getAttribute($this->rootAttribute)) { |
1633
|
|
|
continue; |
1634
|
|
|
} |
1635
|
|
|
|
1636
|
|
|
if ($this->owner === $node) { |
1637
|
|
|
continue; |
1638
|
|
|
} |
1639
|
|
|
|
1640
|
|
|
if ($node->getAttribute($this->leftAttribute) >= $key) { |
1641
|
|
|
$node->setAttribute($this->leftAttribute, $node->getAttribute($this->leftAttribute) + 2); |
1642
|
|
|
} |
1643
|
|
|
|
1644
|
|
|
if ($node->getAttribute($this->rightAttribute) >= $key) { |
1645
|
|
|
$node->setAttribute($this->rightAttribute, $node->getAttribute($this->rightAttribute) + 2); |
1646
|
|
|
} |
1647
|
|
|
} |
1648
|
|
|
} |
1649
|
|
|
|
1650
|
|
|
|
1651
|
|
|
/** |
1652
|
|
|
* Correct cache for [[moveNode()]] |
1653
|
|
|
* |
1654
|
|
|
* @param int $key |
1655
|
|
|
* @param int $levelDelta |
1656
|
|
|
*/ |
1657
|
|
|
private function correctCachedOnMoveNode($key, $levelDelta) |
1658
|
|
|
{ |
1659
|
|
|
$left = $this->owner->getAttribute($this->leftAttribute); |
1660
|
|
|
$right = $this->owner->getAttribute($this->rightAttribute); |
1661
|
|
|
$delta = $right - $left + 1; |
1662
|
|
|
|
1663
|
|
|
if ($left >= $key) { |
1664
|
|
|
$left += $delta; |
1665
|
|
|
$right += $delta; |
1666
|
|
|
} |
1667
|
|
|
|
1668
|
|
|
$delta2 = $key - $left; |
1669
|
|
|
|
1670
|
|
|
foreach (self::$_cached[get_class($this->owner)] as $node) { |
1671
|
|
|
/** @var $node ActiveRecord */ |
1672
|
|
|
if ($node->getIsNewRecord() || $node->getIsDeletedRecord()) { |
|
|
|
|
1673
|
|
|
continue; |
1674
|
|
|
} |
1675
|
|
|
|
1676
|
|
|
if ($this->hasManyRoots && $this->owner->getAttribute($this->rootAttribute) !== $node->getAttribute($this->rootAttribute)) { |
1677
|
|
|
continue; |
1678
|
|
|
} |
1679
|
|
|
|
1680
|
|
|
if ($node->getAttribute($this->leftAttribute) >= $key) { |
1681
|
|
|
$node->setAttribute($this->leftAttribute, $node->getAttribute($this->leftAttribute) + $delta); |
1682
|
|
|
} |
1683
|
|
|
|
1684
|
|
|
if ($node->getAttribute($this->rightAttribute) >= $key) { |
1685
|
|
|
$node->setAttribute($this->rightAttribute, $node->getAttribute($this->rightAttribute) + $delta); |
1686
|
|
|
} |
1687
|
|
|
|
1688
|
|
|
if ($node->getAttribute($this->leftAttribute) >= $left && $node->getAttribute($this->rightAttribute) <= $right) { |
1689
|
|
|
$node->setAttribute($this->levelAttribute, $node->getAttribute($this->levelAttribute) + $levelDelta); |
1690
|
|
|
} |
1691
|
|
|
|
1692
|
|
|
if ($node->getAttribute($this->leftAttribute) >= $left && $node->getAttribute($this->leftAttribute) <= $right) { |
1693
|
|
|
$node->setAttribute($this->leftAttribute, $node->getAttribute($this->leftAttribute) + $delta2); |
1694
|
|
|
} |
1695
|
|
|
|
1696
|
|
|
if ($node->getAttribute($this->rightAttribute) >= $left && $node->getAttribute($this->rightAttribute) <= $right) { |
1697
|
|
|
$node->setAttribute($this->rightAttribute, $node->getAttribute($this->rightAttribute) + $delta2); |
1698
|
|
|
} |
1699
|
|
|
|
1700
|
|
|
if ($node->getAttribute($this->leftAttribute) >= $right + 1) { |
1701
|
|
|
$node->setAttribute($this->leftAttribute, $node->getAttribute($this->leftAttribute) - $delta); |
1702
|
|
|
} |
1703
|
|
|
|
1704
|
|
|
if ($node->getAttribute($this->rightAttribute) >= $right + 1) { |
1705
|
|
|
$node->setAttribute($this->rightAttribute, $node->getAttribute($this->rightAttribute) - $delta); |
1706
|
|
|
} |
1707
|
|
|
} |
1708
|
|
|
} |
1709
|
|
|
|
1710
|
|
|
|
1711
|
|
|
/** |
1712
|
|
|
* Correct cache for [[moveNode()]] |
1713
|
|
|
* |
1714
|
|
|
* @param int $key |
1715
|
|
|
* @param int $levelDelta |
1716
|
|
|
* @param int $root |
1717
|
|
|
*/ |
1718
|
|
|
private function correctCachedOnMoveBetweenTrees($key, $levelDelta, $root) |
1719
|
|
|
{ |
1720
|
|
|
$left = $this->owner->getAttribute($this->leftAttribute); |
1721
|
|
|
$right = $this->owner->getAttribute($this->rightAttribute); |
1722
|
|
|
$delta = $right - $left + 1; |
1723
|
|
|
$delta2 = $key - $left; |
1724
|
|
|
$delta3 = $left - $right - 1; |
1725
|
|
|
|
1726
|
|
|
foreach (self::$_cached[get_class($this->owner)] as $node) { |
1727
|
|
|
/** @var $node ActiveRecord */ |
1728
|
|
|
if ($node->getIsNewRecord() || $node->getIsDeletedRecord()) { |
|
|
|
|
1729
|
|
|
continue; |
1730
|
|
|
} |
1731
|
|
|
|
1732
|
|
|
if ($node->getAttribute($this->rootAttribute) === $root) { |
1733
|
|
|
if ($node->getAttribute($this->leftAttribute) >= $key) { |
1734
|
|
|
$node->setAttribute($this->leftAttribute, $node->getAttribute($this->leftAttribute) + $delta); |
1735
|
|
|
} |
1736
|
|
|
|
1737
|
|
|
if ($node->getAttribute($this->rightAttribute) >= $key) { |
1738
|
|
|
$node->setAttribute($this->rightAttribute, $node->getAttribute($this->rightAttribute) + $delta); |
1739
|
|
|
} |
1740
|
|
|
} elseif ($node->getAttribute($this->rootAttribute) === $this->owner->getAttribute($this->rootAttribute)) { |
1741
|
|
|
if ($node->getAttribute($this->leftAttribute) >= $left && $node->getAttribute($this->rightAttribute) <= $right) { |
1742
|
|
|
$node->setAttribute($this->leftAttribute, $node->getAttribute($this->leftAttribute) + $delta2); |
1743
|
|
|
$node->setAttribute($this->rightAttribute, $node->getAttribute($this->rightAttribute) + $delta2); |
1744
|
|
|
$node->setAttribute($this->levelAttribute, $node->getAttribute($this->levelAttribute) + $levelDelta); |
1745
|
|
|
$node->setAttribute($this->rootAttribute, $root); |
1746
|
|
|
} else { |
1747
|
|
|
if ($node->getAttribute($this->leftAttribute) >= $right + 1) { |
1748
|
|
|
$node->setAttribute($this->leftAttribute, $node->getAttribute($this->leftAttribute) + $delta3); |
1749
|
|
|
} |
1750
|
|
|
|
1751
|
|
|
if ($node->getAttribute($this->rightAttribute) >= $right + 1) { |
1752
|
|
|
$node->setAttribute($this->rightAttribute, $node->getAttribute($this->rightAttribute) + $delta3); |
1753
|
|
|
} |
1754
|
|
|
} |
1755
|
|
|
} |
1756
|
|
|
} |
1757
|
|
|
} |
1758
|
|
|
|
1759
|
|
|
|
1760
|
|
|
/** |
1761
|
|
|
* Optionally perform actions/checks before addNode is processed |
1762
|
|
|
* |
1763
|
|
|
* @return boolean success |
1764
|
|
|
*/ |
1765
|
|
|
protected function beforeAddNode() |
1766
|
|
|
{ |
1767
|
|
|
if (method_exists($this->owner, 'beforeAddNode')) { |
1768
|
|
|
return $this->owner->beforeAddNode(); |
|
|
|
|
1769
|
|
|
} |
1770
|
|
|
return true; |
1771
|
|
|
} |
1772
|
|
|
|
1773
|
|
|
|
1774
|
|
|
/** |
1775
|
|
|
* Optionally perform actions/checks after addNode has processed |
1776
|
|
|
* |
1777
|
|
|
* @return boolean success |
1778
|
|
|
*/ |
1779
|
|
|
protected function afterAddNode() |
1780
|
|
|
{ |
1781
|
|
|
if (method_exists($this->owner, 'afterAddNode')) { |
1782
|
|
|
return $this->owner->afterAddNode(); |
|
|
|
|
1783
|
|
|
} |
1784
|
|
|
return true; |
1785
|
|
|
} |
1786
|
|
|
|
1787
|
|
|
|
1788
|
|
|
/** |
1789
|
|
|
* Optionally perform actions/checks before a node name is changed |
1790
|
|
|
* |
1791
|
|
|
* @param string $old |
1792
|
|
|
* old folder path |
1793
|
|
|
* @return boolean success |
1794
|
|
|
*/ |
1795
|
|
|
protected function beforeRenameNode($old) |
1796
|
|
|
{ |
1797
|
|
|
if (method_exists($this->owner, 'beforeRenameNode')) { |
1798
|
|
|
return $this->owner->beforeRenameNode($old); |
|
|
|
|
1799
|
|
|
} |
1800
|
|
|
return true; |
1801
|
|
|
} |
1802
|
|
|
|
1803
|
|
|
|
1804
|
|
|
/** |
1805
|
|
|
* Optionally perform actions/checks after the node name has been changed |
1806
|
|
|
* |
1807
|
|
|
* @param string $old |
1808
|
|
|
* old folder path |
1809
|
|
|
* @return boolean success |
1810
|
|
|
*/ |
1811
|
|
|
protected function afterRenameNode($old) |
1812
|
|
|
{ |
1813
|
|
|
if (method_exists($this->owner, 'afterRenameNode')) { |
1814
|
|
|
return $this->owner->afterRenameNode($old); |
|
|
|
|
1815
|
|
|
} |
1816
|
|
|
return true; |
1817
|
|
|
} |
1818
|
|
|
|
1819
|
|
|
|
1820
|
|
|
/** |
1821
|
|
|
* Optionally perform actions/checks before a node is moved |
1822
|
|
|
* |
1823
|
|
|
* @param string $old |
1824
|
|
|
* old folder path |
1825
|
|
|
* @return boolean success |
1826
|
|
|
*/ |
1827
|
|
|
protected function beforeMoveNode($old) |
1828
|
|
|
{ |
1829
|
|
|
if (method_exists($this->owner, 'beforeMoveNode')) { |
1830
|
|
|
return $this->owner->beforeMoveNode($old); |
|
|
|
|
1831
|
|
|
} |
1832
|
|
|
return true; |
1833
|
|
|
} |
1834
|
|
|
|
1835
|
|
|
|
1836
|
|
|
/** |
1837
|
|
|
* Optionally perform actions/checks after the node is moved |
1838
|
|
|
* |
1839
|
|
|
* @param string $old |
1840
|
|
|
* old folder path |
1841
|
|
|
* @return boolean success |
1842
|
|
|
*/ |
1843
|
|
|
protected function afterMoveNode($old) |
1844
|
|
|
{ |
1845
|
|
|
if (method_exists($this->owner, 'afterMoveNode')) { |
1846
|
|
|
return $this->owner->afterMoveNode($old); |
|
|
|
|
1847
|
|
|
} |
1848
|
|
|
return true; |
1849
|
|
|
} |
1850
|
|
|
|
1851
|
|
|
|
1852
|
|
|
/** |
1853
|
|
|
* Optionally perform actions/checks before a node is deleted |
1854
|
|
|
* |
1855
|
|
|
* @return boolean success |
1856
|
|
|
*/ |
1857
|
|
|
protected function beforeDeleteNode() |
1858
|
|
|
{ |
1859
|
|
|
if (method_exists($this->owner, 'beforeDeleteNode')) { |
1860
|
|
|
return $this->owner->beforeDeleteNode(); |
|
|
|
|
1861
|
|
|
} |
1862
|
|
|
return true; |
1863
|
|
|
} |
1864
|
|
|
|
1865
|
|
|
|
1866
|
|
|
/** |
1867
|
|
|
* Optionally perform actions/checks after the node has been deleted |
1868
|
|
|
* |
1869
|
|
|
* @param string $path |
1870
|
|
|
* @return boolean success |
1871
|
|
|
*/ |
1872
|
|
|
protected function afterDeleteNode($path) |
1873
|
|
|
{ |
1874
|
|
|
if (method_exists($this->owner, 'afterDeleteNode')) { |
1875
|
|
|
return $this->owner->afterDeleteNode($path); |
|
|
|
|
1876
|
|
|
} |
1877
|
|
|
return true; |
1878
|
|
|
} |
1879
|
|
|
|
1880
|
|
|
|
1881
|
|
|
/** |
1882
|
|
|
* Override ignore events flag |
1883
|
|
|
* |
1884
|
|
|
* @param boolean $value |
1885
|
|
|
*/ |
1886
|
|
|
public function setIgnoreEvents($value) |
1887
|
|
|
{ |
1888
|
|
|
$this->_ignoreEvent = $value; |
1889
|
|
|
} |
1890
|
|
|
|
1891
|
|
|
|
1892
|
|
|
/** |
1893
|
|
|
* Set previous path (sometimes useful to avoid looking up parent multiple times) |
1894
|
|
|
* |
1895
|
|
|
* @param string $path |
1896
|
|
|
*/ |
1897
|
|
|
public function setPreviousPath($path) |
1898
|
|
|
{ |
1899
|
|
|
$this->_previousPath = $path; |
1900
|
|
|
} |
1901
|
|
|
|
1902
|
|
|
|
1903
|
|
|
/** |
1904
|
|
|
* Destructor |
1905
|
|
|
*/ |
1906
|
|
|
public function __destruct() |
1907
|
|
|
{ |
1908
|
|
|
unset(self::$_cached[get_class($this->owner)][$this->_id]); |
1909
|
|
|
} |
1910
|
|
|
|
1911
|
|
|
} |
1912
|
|
|
|
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.