1
|
|
|
<?php namespace Arcanedev\LaravelNestedSet\Eloquent; |
2
|
|
|
|
3
|
|
|
use Arcanedev\LaravelNestedSet\Utilities\NestedSet; |
4
|
|
|
use Arcanedev\LaravelNestedSet\Traits\NodeTrait; |
5
|
|
|
use Arcanedev\LaravelNestedSet\Utilities\TreeHelper; |
6
|
|
|
use Illuminate\Database\Eloquent\Builder; |
7
|
|
|
use Illuminate\Database\Eloquent\Model; |
8
|
|
|
use Illuminate\Database\Eloquent\ModelNotFoundException; |
9
|
|
|
use Illuminate\Database\Query\Builder as Query; |
10
|
|
|
use Illuminate\Database\Query\Expression; |
11
|
|
|
use LogicException; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* Class QueryBuilder |
15
|
|
|
* |
16
|
|
|
* @package Arcanedev\LaravelNestedSet\Eloquent |
17
|
|
|
* @author ARCANEDEV <[email protected]> |
18
|
|
|
*/ |
19
|
|
|
class QueryBuilder extends Builder |
20
|
|
|
{ |
21
|
|
|
/* ------------------------------------------------------------------------------------------------ |
22
|
|
|
| Properties |
23
|
|
|
| ------------------------------------------------------------------------------------------------ |
24
|
|
|
*/ |
25
|
|
|
/** |
26
|
|
|
* @var \Arcanedev\LaravelNestedSet\Traits\NodeTrait|\Illuminate\Database\Eloquent\Model |
27
|
|
|
*/ |
28
|
|
|
protected $model; |
29
|
|
|
|
30
|
|
|
/* ------------------------------------------------------------------------------------------------ |
31
|
|
|
| Main Functions |
32
|
|
|
| ------------------------------------------------------------------------------------------------ |
33
|
|
|
*/ |
34
|
|
|
/** |
35
|
|
|
* Get node's `lft` and `rgt` values. |
36
|
|
|
* |
37
|
|
|
* @param mixed $id |
38
|
|
|
* @param bool $required |
39
|
|
|
* |
40
|
|
|
* @return array |
41
|
|
|
*/ |
42
|
73 |
|
public function getNodeData($id, $required = false) |
43
|
|
|
{ |
44
|
73 |
|
$query = $this->toBase(); |
45
|
|
|
|
46
|
73 |
|
$query->where($this->model->getKeyName(), '=', $id); |
47
|
|
|
|
48
|
73 |
|
$data = $query->first([ |
49
|
73 |
|
$this->model->getLftName(), |
50
|
73 |
|
$this->model->getRgtName(), |
51
|
57 |
|
]); |
52
|
|
|
|
53
|
73 |
|
if ( ! $data && $required) { |
54
|
4 |
|
throw new ModelNotFoundException; |
55
|
|
|
} |
56
|
|
|
|
57
|
69 |
|
return (array) $data; |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* Get plain node data. |
62
|
|
|
* |
63
|
|
|
* @param mixed $id |
64
|
|
|
* @param bool $required |
65
|
|
|
* |
66
|
|
|
* @return array |
67
|
|
|
*/ |
68
|
45 |
|
public function getPlainNodeData($id, $required = false) |
69
|
|
|
{ |
70
|
45 |
|
return array_values($this->getNodeData($id, $required)); |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Scope limits query to select just root node. |
75
|
|
|
* |
76
|
|
|
* @return self |
77
|
|
|
*/ |
78
|
20 |
|
public function whereIsRoot() |
79
|
|
|
{ |
80
|
20 |
|
$this->query->whereNull($this->model->getParentIdName()); |
81
|
|
|
|
82
|
20 |
|
return $this; |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* Limit results to ancestors of specified node. |
87
|
|
|
* |
88
|
|
|
* @param mixed $id |
89
|
|
|
* |
90
|
|
|
* @return self |
91
|
|
|
*/ |
92
|
20 |
|
public function whereAncestorOf($id) |
93
|
|
|
{ |
94
|
20 |
|
$keyName = $this->model->getKeyName(); |
95
|
|
|
|
96
|
20 |
|
if (NestedSet::isNode($id)) { |
97
|
16 |
|
$value = '?'; |
98
|
|
|
|
99
|
16 |
|
$this->query->addBinding($id->getLft()); |
100
|
|
|
|
101
|
16 |
|
$id = $id->getKey(); |
102
|
12 |
|
} else { |
103
|
4 |
|
$valueQuery = $this->model |
104
|
4 |
|
->newQuery() |
105
|
4 |
|
->toBase() |
106
|
4 |
|
->select("_.".$this->model->getLftName()) |
107
|
4 |
|
->from($this->model->getTable().' as _') |
108
|
4 |
|
->where($keyName, '=', $id) |
109
|
4 |
|
->limit(1); |
110
|
|
|
|
111
|
4 |
|
$this->query->mergeBindings($valueQuery); |
112
|
|
|
|
113
|
4 |
|
$value = '(' . $valueQuery->toSql() . ')'; |
114
|
|
|
} |
115
|
|
|
|
116
|
20 |
|
list($lft, $rgt) = $this->wrappedColumns(); |
117
|
|
|
|
118
|
20 |
|
$this->query->whereRaw("{$value} between {$lft} and {$rgt}"); |
119
|
|
|
|
120
|
|
|
// Exclude the node |
121
|
20 |
|
$this->where($keyName, '<>', $id); |
122
|
|
|
|
123
|
20 |
|
return $this; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* Get ancestors of specified node. |
128
|
|
|
* |
129
|
|
|
* @param mixed $id |
130
|
|
|
* @param array $columns |
131
|
|
|
* |
132
|
|
|
* @return self |
133
|
|
|
*/ |
134
|
12 |
|
public function ancestorsOf($id, array $columns = ['*']) |
135
|
|
|
{ |
136
|
12 |
|
return $this->whereAncestorOf($id)->get($columns); |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* Add node selection statement between specified range. |
141
|
|
|
* |
142
|
|
|
* @param array $values |
143
|
|
|
* @param string $boolean |
144
|
|
|
* @param bool $not |
145
|
|
|
* |
146
|
|
|
* @return self |
147
|
|
|
*/ |
148
|
43 |
|
public function whereNodeBetween($values, $boolean = 'and', $not = false) |
149
|
|
|
{ |
150
|
43 |
|
$this->query->whereBetween($this->model->getLftName(), $values, $boolean, $not); |
151
|
|
|
|
152
|
43 |
|
return $this; |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* Add node selection statement between specified range joined with `or` operator. |
157
|
|
|
* |
158
|
|
|
* @param array $values |
159
|
|
|
* |
160
|
|
|
* @return self |
161
|
|
|
*/ |
162
|
|
|
public function orWhereNodeBetween($values) |
163
|
|
|
{ |
164
|
|
|
return $this->whereNodeBetween($values, 'or'); |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* Add constraint statement to descendants of specified node. |
169
|
|
|
* |
170
|
|
|
* @param mixed $id |
171
|
|
|
* @param string $boolean |
172
|
|
|
* @param bool $not |
173
|
|
|
* |
174
|
|
|
* @return self |
175
|
|
|
*/ |
176
|
47 |
|
public function whereDescendantOf($id, $boolean = 'and', $not = false) |
177
|
|
|
{ |
178
|
47 |
|
$data = NestedSet::isNode($id) |
179
|
46 |
|
? $id->getBounds() |
180
|
47 |
|
: $this->model->newNestedSetQuery()->getPlainNodeData($id, true); |
181
|
|
|
|
182
|
|
|
// Don't include the node |
183
|
43 |
|
++$data[0]; |
184
|
|
|
|
185
|
43 |
|
return $this->whereNodeBetween($data, $boolean, $not); |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* @param mixed $id |
190
|
|
|
* |
191
|
|
|
* @return self |
192
|
|
|
*/ |
193
|
|
|
public function whereNotDescendantOf($id) |
194
|
|
|
{ |
195
|
|
|
return $this->whereDescendantOf($id, 'and', true); |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
/** |
199
|
|
|
* @param mixed $id |
200
|
|
|
* |
201
|
|
|
* @return self |
202
|
|
|
*/ |
203
|
4 |
|
public function orWhereDescendantOf($id) |
204
|
|
|
{ |
205
|
4 |
|
return $this->whereDescendantOf($id, 'or'); |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
/** |
209
|
|
|
* @param mixed $id |
210
|
|
|
* |
211
|
|
|
* @return self |
212
|
|
|
*/ |
213
|
|
|
public function orWhereNotDescendantOf($id) |
214
|
|
|
{ |
215
|
|
|
return $this->whereDescendantOf($id, 'or', true); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* Get descendants of specified node. |
220
|
|
|
* |
221
|
|
|
* @param mixed $id |
222
|
|
|
* @param array $columns |
223
|
|
|
* |
224
|
|
|
* @return \Arcanedev\LaravelNestedSet\Eloquent\Collection |
225
|
|
|
*/ |
226
|
|
|
public function descendantsOf($id, array $columns = ['*']) |
227
|
|
|
{ |
228
|
|
|
try { |
229
|
|
|
return $this->whereDescendantOf($id)->get($columns); |
230
|
|
|
} |
231
|
|
|
catch (ModelNotFoundException $e) { |
|
|
|
|
232
|
|
|
return $this->model->newCollection(); |
233
|
|
|
} |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
/** |
237
|
|
|
* @param mixed $id |
238
|
|
|
* @param string $operator |
239
|
|
|
* @param string $boolean |
240
|
|
|
* |
241
|
|
|
* @return self |
242
|
|
|
*/ |
243
|
|
|
protected function whereIsBeforeOrAfter($id, $operator, $boolean) |
244
|
|
|
{ |
245
|
|
|
if (NestedSet::isNode($id)) { |
246
|
|
|
$value = '?'; |
247
|
|
|
|
248
|
|
|
$this->query->addBinding($id->getLft()); |
249
|
|
|
} else { |
250
|
|
|
$valueQuery = $this->model |
251
|
|
|
->newQuery() |
252
|
|
|
->toBase() |
253
|
|
|
->select('_n.'.$this->model->getLftName()) |
254
|
|
|
->from($this->model->getTable().' as _n') |
255
|
|
|
->where('_n.'.$this->model->getKeyName(), '=', $id); |
256
|
|
|
|
257
|
|
|
$this->query->mergeBindings($valueQuery); |
258
|
|
|
|
259
|
|
|
$value = '('.$valueQuery->toSql().')'; |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
list($lft,) = $this->wrappedColumns(); |
263
|
|
|
|
264
|
|
|
$this->query->whereRaw("{$lft} {$operator} {$value}", [ ], $boolean); |
265
|
|
|
|
266
|
|
|
return $this; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
/** |
270
|
|
|
* Constraint nodes to those that are after specified node. |
271
|
|
|
* |
272
|
|
|
* @param mixed $id |
273
|
|
|
* @param string $boolean |
274
|
|
|
* |
275
|
|
|
* @return self |
276
|
|
|
*/ |
277
|
|
|
public function whereIsAfter($id, $boolean = 'and') |
278
|
|
|
{ |
279
|
|
|
return $this->whereIsBeforeOrAfter($id, '>', $boolean); |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
/** |
283
|
|
|
* Constraint nodes to those that are before specified node. |
284
|
|
|
* |
285
|
|
|
* @param mixed $id |
286
|
|
|
* @param string $boolean |
287
|
|
|
* |
288
|
|
|
* @return self |
289
|
|
|
*/ |
290
|
|
|
public function whereIsBefore($id, $boolean = 'and') |
291
|
|
|
{ |
292
|
|
|
return $this->whereIsBeforeOrAfter($id, '<', $boolean); |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
/** |
296
|
|
|
* Include depth level into the result. |
297
|
|
|
* |
298
|
|
|
* @param string $as |
299
|
|
|
* |
300
|
|
|
* @return self |
301
|
|
|
*/ |
302
|
16 |
|
public function withDepth($as = 'depth') |
303
|
|
|
{ |
304
|
16 |
|
if ($this->query->columns === null) { |
305
|
16 |
|
$this->query->columns = ['*']; |
306
|
12 |
|
} |
307
|
|
|
|
308
|
16 |
|
$table = $this->wrappedTable(); |
309
|
|
|
|
310
|
16 |
|
list($lft, $rgt) = $this->wrappedColumns(); |
311
|
|
|
|
312
|
16 |
|
$query = $this->model |
313
|
16 |
|
->newScopedQuery('_d') |
314
|
16 |
|
->toBase() |
315
|
16 |
|
->selectRaw('count(1) - 1') |
316
|
16 |
|
->from($this->model->getTable().' as _d') |
317
|
16 |
|
->whereRaw("{$table}.{$lft} between _d.{$lft} and _d.{$rgt}"); |
318
|
|
|
|
319
|
16 |
|
$this->query->selectSub($query, $as); |
320
|
|
|
|
321
|
16 |
|
return $this; |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
/** |
325
|
|
|
* Get wrapped `lft` and `rgt` column names. |
326
|
|
|
* |
327
|
|
|
* @return array |
328
|
|
|
*/ |
329
|
48 |
|
protected function wrappedColumns() |
330
|
|
|
{ |
331
|
48 |
|
$grammar = $this->query->getGrammar(); |
332
|
|
|
|
333
|
|
|
return [ |
334
|
48 |
|
$grammar->wrap($this->model->getLftName()), |
335
|
48 |
|
$grammar->wrap($this->model->getRgtName()), |
336
|
36 |
|
]; |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
/** |
340
|
|
|
* Get a wrapped table name. |
341
|
|
|
* |
342
|
|
|
* @return string |
343
|
|
|
*/ |
344
|
28 |
|
protected function wrappedTable() |
345
|
|
|
{ |
346
|
28 |
|
return $this->query->getGrammar()->wrapTable($this->getQuery()->from); |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
/** |
350
|
|
|
* Wrap model's key name. |
351
|
|
|
* |
352
|
|
|
* @return string |
353
|
|
|
*/ |
354
|
12 |
|
protected function wrappedKey() |
355
|
|
|
{ |
356
|
12 |
|
return $this->query->getGrammar()->wrap($this->model->getKeyName()); |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
/** |
360
|
|
|
* Exclude root node from the result. |
361
|
|
|
* |
362
|
|
|
* @return self |
363
|
|
|
*/ |
364
|
8 |
|
public function withoutRoot() |
365
|
|
|
{ |
366
|
8 |
|
$this->query->whereNotNull($this->model->getParentIdName()); |
367
|
|
|
|
368
|
8 |
|
return $this; |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
/** |
372
|
|
|
* Order by node position. |
373
|
|
|
* |
374
|
|
|
* @param string $dir |
375
|
|
|
* |
376
|
|
|
* @return self |
377
|
|
|
*/ |
378
|
52 |
|
public function defaultOrder($dir = 'asc') |
379
|
|
|
{ |
380
|
52 |
|
$this->query->orders = null; |
381
|
|
|
|
382
|
52 |
|
$this->query->orderBy($this->model->getLftName(), $dir); |
383
|
|
|
|
384
|
52 |
|
return $this; |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
/** |
388
|
|
|
* Order by reversed node position. |
389
|
|
|
* |
390
|
|
|
* @return $this |
391
|
|
|
*/ |
392
|
4 |
|
public function reversed() |
393
|
|
|
{ |
394
|
4 |
|
return $this->defaultOrder('desc'); |
395
|
|
|
} |
396
|
|
|
|
397
|
|
|
/** |
398
|
|
|
* Move a node to the new position. |
399
|
|
|
* |
400
|
|
|
* @param mixed $key |
401
|
|
|
* @param int $position |
402
|
|
|
* |
403
|
|
|
* @return int |
404
|
|
|
*/ |
405
|
37 |
|
public function moveNode($key, $position) |
406
|
|
|
{ |
407
|
37 |
|
list($lft, $rgt) = $this->model->newNestedSetQuery() |
408
|
37 |
|
->getPlainNodeData($key, true); |
409
|
|
|
|
410
|
37 |
|
if ($lft < $position && $position <= $rgt) { |
411
|
|
|
throw new LogicException('Cannot move node into itself.'); |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
// Get boundaries of nodes that should be moved to new position |
415
|
37 |
|
$from = min($lft, $position); |
416
|
37 |
|
$to = max($rgt, $position - 1); |
417
|
|
|
|
418
|
|
|
// The height of node that is being moved |
419
|
37 |
|
$height = $rgt - $lft + 1; |
420
|
|
|
|
421
|
|
|
// The distance that our node will travel to reach it's destination |
422
|
37 |
|
$distance = $to - $from + 1 - $height; |
423
|
|
|
|
424
|
|
|
// If no distance to travel, just return |
425
|
37 |
|
if ($distance === 0) { |
426
|
|
|
return 0; |
427
|
|
|
} |
428
|
|
|
|
429
|
37 |
|
if ($position > $lft) { |
430
|
29 |
|
$height *= -1; |
431
|
24 |
|
} else { |
432
|
8 |
|
$distance *= -1; |
433
|
|
|
} |
434
|
|
|
|
435
|
37 |
|
$boundary = [$from, $to]; |
436
|
|
|
$query = $this->toBase()->where(function (Query $inner) use ($boundary) { |
437
|
37 |
|
$inner->whereBetween($this->model->getLftName(), $boundary); |
438
|
37 |
|
$inner->orWhereBetween($this->model->getRgtName(), $boundary); |
439
|
37 |
|
}); |
440
|
|
|
|
441
|
37 |
|
return $query->update($this->patch( |
442
|
37 |
|
compact('lft', 'rgt', 'from', 'to', 'height', 'distance') |
443
|
30 |
|
)); |
444
|
|
|
} |
445
|
|
|
|
446
|
|
|
/** |
447
|
|
|
* Make or remove gap in the tree. Negative height will remove gap. |
448
|
|
|
* |
449
|
|
|
* @param int $cut |
450
|
|
|
* @param int $height |
451
|
|
|
* |
452
|
|
|
* @return int |
453
|
|
|
*/ |
454
|
47 |
|
public function makeGap($cut, $height) |
455
|
|
|
{ |
456
|
|
|
$query = $this->toBase()->whereNested(function (Query $inner) use ($cut) { |
457
|
47 |
|
$inner->where($this->model->getLftName(), '>=', $cut); |
458
|
47 |
|
$inner->orWhere($this->model->getRgtName(), '>=', $cut); |
459
|
47 |
|
}); |
460
|
|
|
|
461
|
47 |
|
return $query->update($this->patch( |
462
|
47 |
|
compact('cut', 'height') |
463
|
36 |
|
)); |
464
|
|
|
} |
465
|
|
|
|
466
|
|
|
/** |
467
|
|
|
* Get patch for columns. |
468
|
|
|
* |
469
|
|
|
* @param array $params |
470
|
|
|
* |
471
|
|
|
* @return array |
472
|
|
|
*/ |
473
|
80 |
|
protected function patch(array $params) |
474
|
|
|
{ |
475
|
80 |
|
$grammar = $this->query->getGrammar(); |
476
|
80 |
|
$columns = []; |
477
|
|
|
|
478
|
80 |
|
foreach ([$this->model->getLftName(), $this->model->getRgtName()] as $col) { |
479
|
80 |
|
$columns[$col] = $this->columnPatch($grammar->wrap($col), $params); |
480
|
63 |
|
} |
481
|
|
|
|
482
|
80 |
|
return $columns; |
483
|
|
|
} |
484
|
|
|
|
485
|
|
|
/** |
486
|
|
|
* Get patch for single column. |
487
|
|
|
* |
488
|
|
|
* @param string $col |
489
|
|
|
* @param array $params |
490
|
|
|
* |
491
|
|
|
* @return string |
492
|
|
|
*/ |
493
|
80 |
|
protected function columnPatch($col, array $params) |
494
|
|
|
{ |
495
|
|
|
/** |
496
|
|
|
* @var int $height |
497
|
|
|
* @var int $distance |
498
|
|
|
* @var int $lft |
499
|
|
|
* @var int $rgt |
500
|
|
|
* @var int $from |
501
|
|
|
* @var int $to |
502
|
|
|
*/ |
503
|
80 |
|
extract($params); |
504
|
|
|
|
505
|
80 |
|
if ($height > 0) $height = '+'.$height; |
506
|
|
|
|
507
|
80 |
|
if (isset($cut)) { |
508
|
47 |
|
return new Expression("case when {$col} >= {$cut} then {$col}{$height} else {$col} end"); |
509
|
|
|
} |
510
|
|
|
|
511
|
37 |
|
if ($distance > 0) { |
512
|
29 |
|
$distance = '+'.$distance; |
513
|
24 |
|
} |
514
|
|
|
|
515
|
37 |
|
return new Expression( |
516
|
|
|
"case ". |
517
|
37 |
|
"when {$col} between {$lft} and {$rgt} then {$col}{$distance} ". // Move the node |
518
|
37 |
|
"when {$col} between {$from} and {$to} then {$col}{$height} ". // Move other nodes |
519
|
37 |
|
"else {$col} end" |
520
|
30 |
|
); |
521
|
|
|
} |
522
|
|
|
|
523
|
|
|
/** |
524
|
|
|
* Get statistics of errors of the tree. |
525
|
|
|
* |
526
|
|
|
* @return array |
527
|
|
|
*/ |
528
|
12 |
|
public function countErrors() |
529
|
|
|
{ |
530
|
|
|
$checks = [ |
531
|
12 |
|
'oddness' => $this->getOddnessQuery(), // Check if lft and rgt values are ok |
532
|
12 |
|
'duplicates' => $this->getDuplicatesQuery(), // Check if lft and rgt values are unique |
533
|
12 |
|
'wrong_parent' => $this->getWrongParentQuery(), // Check if parent_id is set correctly |
534
|
12 |
|
'missing_parent' => $this->getMissingParentQuery() // Check for nodes that have missing parent |
535
|
9 |
|
]; |
536
|
|
|
|
537
|
12 |
|
$query = $this->query->newQuery(); |
538
|
|
|
|
539
|
12 |
|
foreach ($checks as $key => $inner) { |
540
|
|
|
/** @var \Illuminate\Database\Query\Builder $inner */ |
541
|
12 |
|
$inner->selectRaw('count(1)'); |
542
|
|
|
|
543
|
12 |
|
$query->selectSub($inner, $key); |
544
|
9 |
|
} |
545
|
|
|
|
546
|
12 |
|
return (array) $query->first(); |
547
|
|
|
} |
548
|
|
|
|
549
|
|
|
/** |
550
|
|
|
* @return \Illuminate\Database\Query\Builder |
551
|
|
|
*/ |
552
|
12 |
|
protected function getOddnessQuery() |
553
|
|
|
{ |
554
|
12 |
|
return $this->model |
555
|
12 |
|
->newNestedSetQuery() |
556
|
12 |
|
->toBase() |
557
|
|
|
->whereNested(function (Query $inner) { |
558
|
12 |
|
list($lft, $rgt) = $this->wrappedColumns(); |
559
|
|
|
|
560
|
12 |
|
$inner->whereRaw("{$lft} >= {$rgt}") |
561
|
12 |
|
->orWhereRaw("({$rgt} - {$lft}) % 2 = 0"); |
562
|
12 |
|
}); |
563
|
|
|
} |
564
|
|
|
|
565
|
|
|
/** |
566
|
|
|
* @return \Illuminate\Database\Query\Builder |
567
|
|
|
*/ |
568
|
12 |
|
protected function getDuplicatesQuery() |
569
|
|
|
{ |
570
|
12 |
|
$table = $this->wrappedTable(); |
571
|
|
|
|
572
|
12 |
|
$query = $this->model |
573
|
12 |
|
->newNestedSetQuery('c1') |
574
|
12 |
|
->toBase() |
575
|
12 |
|
->from($this->query->raw("{$table} c1, {$table} c2")) |
576
|
12 |
|
->whereRaw("c1.id < c2.id") |
577
|
|
|
->whereNested(function (Query $inner) { |
578
|
12 |
|
list($lft, $rgt) = $this->wrappedColumns(); |
579
|
|
|
|
580
|
12 |
|
$inner->orWhereRaw("c1.{$lft}=c2.{$lft}") |
581
|
12 |
|
->orWhereRaw("c1.{$rgt}=c2.{$rgt}") |
582
|
12 |
|
->orWhereRaw("c1.{$lft}=c2.{$rgt}") |
583
|
12 |
|
->orWhereRaw("c1.{$rgt}=c2.{$lft}"); |
584
|
12 |
|
}); |
585
|
|
|
|
586
|
12 |
|
return $this->model->applyNestedSetScope($query, 'c2'); |
587
|
|
|
} |
588
|
|
|
|
589
|
|
|
/** |
590
|
|
|
* @return \Illuminate\Database\Query\Builder |
591
|
|
|
*/ |
592
|
12 |
|
protected function getWrongParentQuery() |
593
|
|
|
{ |
594
|
12 |
|
$table = $this->wrappedTable(); |
595
|
12 |
|
$keyName = $this->wrappedKey(); |
596
|
12 |
|
$parentIdName = $this->query->raw($this->model->getParentIdName()); |
597
|
|
|
|
598
|
12 |
|
$query = $this->model |
599
|
12 |
|
->newNestedSetQuery('c') |
600
|
12 |
|
->toBase() |
601
|
12 |
|
->from($this->query->raw("{$table} c, {$table} p, $table m")) |
602
|
12 |
|
->whereRaw("c.{$parentIdName}=p.{$keyName}") |
603
|
12 |
|
->whereRaw("m.{$keyName} <> p.{$keyName}") |
604
|
12 |
|
->whereRaw("m.{$keyName} <> c.{$keyName}") |
605
|
|
|
->whereNested(function (Query $inner) { |
606
|
12 |
|
list($lft, $rgt) = $this->wrappedColumns(); |
607
|
|
|
|
608
|
12 |
|
$inner->whereRaw("c.{$lft} not between p.{$lft} and p.{$rgt}") |
609
|
12 |
|
->orWhereRaw("c.{$lft} between m.{$lft} and m.{$rgt}") |
610
|
12 |
|
->whereRaw("m.{$lft} between p.{$lft} and p.{$rgt}"); |
611
|
12 |
|
}); |
612
|
|
|
|
613
|
12 |
|
$this->model->applyNestedSetScope($query, 'p'); |
614
|
12 |
|
$this->model->applyNestedSetScope($query, 'm'); |
615
|
|
|
|
616
|
12 |
|
return $query; |
617
|
|
|
} |
618
|
|
|
|
619
|
|
|
/** |
620
|
|
|
* @return \Illuminate\Database\Query\Builder |
621
|
|
|
*/ |
622
|
12 |
|
protected function getMissingParentQuery() |
623
|
|
|
{ |
624
|
12 |
|
return $this->model |
625
|
12 |
|
->newNestedSetQuery() |
626
|
12 |
|
->toBase() |
627
|
12 |
|
->whereNested(function (Query $inner) { |
628
|
12 |
|
$table = $this->wrappedTable(); |
629
|
12 |
|
$keyName = $this->wrappedKey(); |
630
|
12 |
|
$parentIdName = $this->query->raw($this->model->getParentIdName()); |
631
|
|
|
|
632
|
12 |
|
$existsCheck = $this->model |
633
|
12 |
|
->newNestedSetQuery() |
634
|
12 |
|
->toBase() |
635
|
12 |
|
->selectRaw('1') |
636
|
12 |
|
->from($this->query->raw("{$table} p")) |
637
|
12 |
|
->whereRaw("{$table}.{$parentIdName} = p.{$keyName}") |
638
|
12 |
|
->limit(1); |
639
|
|
|
|
640
|
12 |
|
$this->model->applyNestedSetScope($existsCheck, 'p'); |
641
|
|
|
|
642
|
12 |
|
$inner->whereRaw("{$parentIdName} is not null") |
643
|
12 |
|
->addWhereExistsQuery($existsCheck, 'and', true); |
644
|
12 |
|
}); |
645
|
|
|
} |
646
|
|
|
|
647
|
|
|
/** |
648
|
|
|
* Get the number of total errors of the tree. |
649
|
|
|
* |
650
|
|
|
* @return int |
651
|
|
|
*/ |
652
|
8 |
|
public function getTotalErrors() |
653
|
|
|
{ |
654
|
8 |
|
return array_sum($this->countErrors()); |
655
|
|
|
} |
656
|
|
|
|
657
|
|
|
/** |
658
|
|
|
* Get whether the tree is broken. |
659
|
|
|
* |
660
|
|
|
* @return bool |
661
|
|
|
*/ |
662
|
8 |
|
public function isBroken() |
663
|
|
|
{ |
664
|
8 |
|
return $this->getTotalErrors() > 0; |
665
|
|
|
} |
666
|
|
|
|
667
|
|
|
/** |
668
|
|
|
* Fixes the tree based on parentage info. |
669
|
|
|
* Nodes with invalid parent are saved as roots. |
670
|
|
|
* |
671
|
|
|
* @return int The number of fixed nodes |
672
|
|
|
*/ |
673
|
4 |
|
public function fixTree() |
674
|
|
|
{ |
675
|
|
|
$columns = [ |
676
|
4 |
|
$this->model->getKeyName(), |
677
|
4 |
|
$this->model->getParentIdName(), |
678
|
4 |
|
$this->model->getLftName(), |
679
|
4 |
|
$this->model->getRgtName(), |
680
|
3 |
|
]; |
681
|
|
|
|
682
|
4 |
|
$dictionary = $this->defaultOrder() |
683
|
4 |
|
->get($columns) |
684
|
4 |
|
->groupBy($this->model->getParentIdName()) |
685
|
4 |
|
->all(); |
686
|
|
|
|
687
|
4 |
|
return TreeHelper::fixNodes($dictionary); |
688
|
|
|
} |
689
|
|
|
|
690
|
|
|
/** |
691
|
|
|
* Rebuild the tree based on raw data. |
692
|
|
|
* If item data does not contain primary key, new node will be created. |
693
|
|
|
* |
694
|
|
|
* @param array $data |
695
|
|
|
* @param bool $delete Whether to delete nodes that exists but not in the data array |
696
|
|
|
* |
697
|
|
|
* @return int |
698
|
|
|
*/ |
699
|
8 |
|
public function rebuildTree(array $data, $delete = false) |
700
|
|
|
{ |
701
|
8 |
|
$existing = $this->get()->getDictionary(); |
702
|
8 |
|
$dictionary = []; |
703
|
8 |
|
$this->buildRebuildDictionary($dictionary, $data, $existing); |
704
|
|
|
|
705
|
4 |
|
if ( ! empty($existing)) { |
706
|
4 |
|
if ($delete) { |
707
|
4 |
|
$this->model |
708
|
4 |
|
->newScopedQuery() |
709
|
4 |
|
->whereIn($this->model->getKeyName(), array_keys($existing)) |
710
|
4 |
|
->forceDelete(); |
711
|
3 |
|
} else { |
712
|
|
|
/** @var NodeTrait $model */ |
713
|
|
|
foreach ($existing as $model) { |
714
|
|
|
$dictionary[$model->getParentId()][] = $model; |
715
|
|
|
} |
716
|
|
|
} |
717
|
3 |
|
} |
718
|
|
|
|
719
|
4 |
|
return TreeHelper::fixNodes($dictionary); |
720
|
|
|
} |
721
|
|
|
|
722
|
|
|
/** |
723
|
|
|
* @param array $dictionary |
724
|
|
|
* @param array $data |
725
|
|
|
* @param array $existing |
726
|
|
|
* @param mixed $parentId |
727
|
|
|
*/ |
728
|
8 |
|
protected function buildRebuildDictionary( |
729
|
|
|
array &$dictionary, |
730
|
|
|
array $data, |
731
|
|
|
array &$existing, |
732
|
|
|
$parentId = null |
733
|
|
|
) { |
734
|
8 |
|
$keyName = $this->model->getKeyName(); |
735
|
|
|
|
736
|
8 |
|
foreach ($data as $itemData) { |
737
|
8 |
|
if ( ! isset($itemData[$keyName])) { |
738
|
4 |
|
$model = $this->model->newInstance(); |
|
|
|
|
739
|
|
|
// We will save it as raw node since tree will be fixed |
740
|
4 |
|
$model->rawNode(0, 0, $parentId); |
741
|
3 |
|
} else { |
742
|
4 |
|
if ( ! isset($existing[$key = $itemData[$keyName]])) { |
743
|
4 |
|
throw new ModelNotFoundException; |
744
|
|
|
} |
745
|
|
|
$model = $existing[$key]; |
746
|
|
|
unset($existing[$key]); |
747
|
|
|
} |
748
|
|
|
|
749
|
4 |
|
$model->fill($itemData)->save(); |
750
|
4 |
|
$dictionary[$parentId][] = $model; |
751
|
|
|
|
752
|
4 |
|
if ( ! isset($itemData['children'])) { |
753
|
4 |
|
continue; |
754
|
|
|
} |
755
|
|
|
|
756
|
|
|
$this->buildRebuildDictionary( |
757
|
|
|
$dictionary, |
758
|
|
|
$itemData['children'], |
759
|
|
|
$existing, |
760
|
|
|
$model->getKey() |
761
|
|
|
); |
762
|
3 |
|
} |
763
|
4 |
|
} |
764
|
|
|
|
765
|
|
|
/** |
766
|
|
|
* @param string|null $table |
767
|
|
|
* |
768
|
|
|
* @return self |
769
|
|
|
*/ |
770
|
|
|
public function applyNestedSetScope($table = null) |
771
|
|
|
{ |
772
|
|
|
return $this->model->applyNestedSetScope($this, $table); |
773
|
|
|
} |
774
|
|
|
|
775
|
|
|
/** |
776
|
|
|
* Get the root node. |
777
|
|
|
* |
778
|
|
|
* @param array $columns |
779
|
|
|
* |
780
|
|
|
* @return self |
781
|
|
|
*/ |
782
|
16 |
|
public function root(array $columns = ['*']) |
783
|
|
|
{ |
784
|
16 |
|
return $this->whereIsRoot()->first($columns); |
785
|
|
|
} |
786
|
|
|
} |
787
|
|
|
|
Scrutinizer analyzes your
composer.json
/composer.lock
file if available to determine the classes, and functions that are defined by your dependencies.It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.