1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Encima\Albero; |
4
|
|
|
|
5
|
|
|
use Illuminate\Database\Eloquent\Model; |
6
|
|
|
|
7
|
|
|
class SetValidator |
8
|
|
|
{ |
9
|
|
|
/** @var \Illuminate\Database\Eloquent\Model */ |
10
|
|
|
protected $node = null; |
11
|
|
|
|
12
|
|
|
public function __construct(Model $node) |
13
|
|
|
{ |
14
|
|
|
$this->node = $node; |
15
|
|
|
} |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* Determine if the validation passes. |
19
|
|
|
* |
20
|
|
|
* @return boolean |
21
|
|
|
*/ |
22
|
|
|
public function passes(): bool |
23
|
|
|
{ |
24
|
|
|
return |
25
|
|
|
$this->validateBounds() && |
26
|
|
|
$this->validateDuplicates() && |
27
|
|
|
$this->validateRoots(); |
28
|
|
|
} |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* Determine if validation fails. |
32
|
|
|
* |
33
|
|
|
* @return boolean |
34
|
|
|
*/ |
35
|
|
|
public function fails(): bool |
36
|
|
|
{ |
37
|
|
|
return !$this->passes(); |
38
|
|
|
} |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Validates bounds of the nested tree structure. It will perform checks on |
42
|
|
|
* the `lft`, `rgt` and `parent_id` columns. Mainly that they're not null, |
43
|
|
|
* rights greater than lefts, and that they're within the bounds of the parent. |
44
|
|
|
* |
45
|
|
|
* @return boolean |
46
|
|
|
*/ |
47
|
|
|
protected function validateBounds(): bool |
48
|
|
|
{ |
49
|
|
|
$connection = $this->node->getConnection(); |
50
|
|
|
$grammar = $connection->getQueryGrammar(); |
51
|
|
|
|
52
|
|
|
$tableName = $this->node->getTable(); |
53
|
|
|
$primaryKeyName = $this->node->getKeyName(); |
54
|
|
|
$parentColumn = $this->node->getQualifiedParentColumnName(); |
55
|
|
|
|
56
|
|
|
$lftCol = $grammar->wrap($this->node->getLeftColumnName()); |
|
|
|
|
57
|
|
|
$rgtCol = $grammar->wrap($this->node->getRightColumnName()); |
58
|
|
|
|
59
|
|
|
$qualifiedLftCol = $grammar->wrap($this->node->getQualifiedLeftColumnName()); |
60
|
|
|
$qualifiedRgtCol = $grammar->wrap($this->node->getQualifiedRightColumnName()); |
61
|
|
|
$qualifiedParentCol = $grammar->wrap($this->node->getQualifiedParentColumnName()); |
62
|
|
|
|
63
|
|
|
$whereStm = "(${qualifiedLftCol} IS NULL OR |
64
|
|
|
${qualifiedRgtCol} IS NULL OR |
65
|
|
|
${qualifiedLftCol} >= ${qualifiedRgtCol} OR |
66
|
|
|
(${qualifiedParentCol} IS NOT NULL AND |
67
|
|
|
(${qualifiedLftCol} <= parent.${lftCol} OR |
68
|
|
|
${qualifiedRgtCol} >= parent.${rgtCol})))"; |
69
|
|
|
|
70
|
|
|
$query = $this->node->newQuery() |
71
|
|
|
->join( |
72
|
|
|
$connection->raw($grammar->wrapTable($tableName).' AS parent'), |
73
|
|
|
$parentColumn, |
74
|
|
|
'=', |
75
|
|
|
$connection->raw('parent.'.$grammar->wrap($primaryKeyName)), |
76
|
|
|
'left outer' |
77
|
|
|
) |
78
|
|
|
->whereRaw($whereStm); |
79
|
|
|
|
80
|
|
|
return ($query->count() == 0); |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* Checks that there are no duplicates for the `lft` and `rgt` columns. |
85
|
|
|
* |
86
|
|
|
* @return boolean |
87
|
|
|
*/ |
88
|
|
|
protected function validateDuplicates(): bool |
89
|
|
|
{ |
90
|
|
|
return ( |
91
|
|
|
!$this->duplicatesExistForColumn($this->node->getQualifiedLeftColumnName()) && |
|
|
|
|
92
|
|
|
!$this->duplicatesExistForColumn($this->node->getQualifiedRightColumnName()) |
93
|
|
|
); |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* For each root of the whole nested set tree structure, checks that their |
98
|
|
|
* `lft` and `rgt` bounds are properly set. |
99
|
|
|
* |
100
|
|
|
* @return boolean |
101
|
|
|
*/ |
102
|
|
|
protected function validateRoots(): bool |
103
|
|
|
{ |
104
|
|
|
$roots = forward_static_call([get_class($this->node), 'roots'])->get(); |
105
|
|
|
|
106
|
|
|
// If a scope is defined in the model we should check that the roots are |
107
|
|
|
// valid *for each* value in the scope columns. |
108
|
|
|
if ($this->node->isScoped()) { |
109
|
|
|
return $this->validateRootsByScope($roots); |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
return $this->isEachRootValid($roots); |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
/** |
116
|
|
|
* Checks if duplicate values for the column specified exist. Takes |
117
|
|
|
* the Nested Set scope columns into account (if appropiate). |
118
|
|
|
* |
119
|
|
|
* @param string $column |
120
|
|
|
* @return boolean |
121
|
|
|
*/ |
122
|
|
|
protected function duplicatesExistForColumn(string $column): bool |
123
|
|
|
{ |
124
|
|
|
$connection = $this->node->getConnection(); |
125
|
|
|
$grammar = $connection->getQueryGrammar(); |
126
|
|
|
|
127
|
|
|
$columns = array_merge($this->node->getQualifiedScopedColumns(), [$column]); |
|
|
|
|
128
|
|
|
|
129
|
|
|
$columnsForSelect = implode(', ', array_map(function ($col) use ($grammar) { |
130
|
|
|
return $grammar->wrap($col); |
131
|
|
|
}, $columns)); |
132
|
|
|
|
133
|
|
|
$wrappedColumn = $grammar->wrap($column); |
134
|
|
|
|
135
|
|
|
$query = $this->node->newQuery() |
136
|
|
|
->select($connection->raw("${columnsForSelect}, COUNT(${wrappedColumn})")) |
137
|
|
|
->havingRaw("COUNT(${wrappedColumn}) > 1"); |
138
|
|
|
|
139
|
|
|
foreach ($columns as $col) { |
140
|
|
|
$query->groupBy($col); |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
$result = $query->first(); |
144
|
|
|
|
145
|
|
|
return !is_null($result); |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* Check that each root node in the list supplied satisfies that its bounds |
150
|
|
|
* values (lft, rgt indexes) are less than the next. |
151
|
|
|
* |
152
|
|
|
* @param mixed $roots |
153
|
|
|
* @return boolean |
154
|
|
|
*/ |
155
|
|
|
protected function isEachRootValid($roots): bool |
156
|
|
|
{ |
157
|
|
|
$left = $right = 0; |
158
|
|
|
|
159
|
|
|
foreach ($roots as $root) { |
160
|
|
|
$rootLeft = $root->getLeft(); |
161
|
|
|
$rootRight = $root->getRight(); |
162
|
|
|
|
163
|
|
|
if (!($rootLeft > $left && $rootRight > $right)) { |
164
|
|
|
return false; |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
$left = $rootLeft; |
168
|
|
|
$right = $rootRight; |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
return true; |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
/** |
175
|
|
|
* Check that each root node in the list supplied satisfies that its bounds |
176
|
|
|
* values (lft, rgt indexes) are less than the next *within each scope*. |
177
|
|
|
* |
178
|
|
|
* @param mixed $roots |
179
|
|
|
* @return boolean |
180
|
|
|
*/ |
181
|
|
|
protected function validateRootsByScope($roots): bool |
182
|
|
|
{ |
183
|
|
|
foreach ($this->groupRootsByScope($roots) as $scope => $groupedRoots) { |
184
|
|
|
$valid = $this->isEachRootValid($groupedRoots); |
185
|
|
|
|
186
|
|
|
if (!$valid) { |
187
|
|
|
return false; |
188
|
|
|
} |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
return true; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* Given a list of root nodes, it returns an array in which the keys are the |
196
|
|
|
* array of the actual scope column values and the values are the root nodes |
197
|
|
|
* inside that scope themselves |
198
|
|
|
* |
199
|
|
|
* @param mixed $roots |
200
|
|
|
* @return array |
201
|
|
|
*/ |
202
|
|
|
protected function groupRootsByScope($roots): array |
203
|
|
|
{ |
204
|
|
|
$rootsGroupedByScope = []; |
205
|
|
|
|
206
|
|
|
foreach ($roots as $root) { |
207
|
|
|
$key = $this->keyForScope($root); |
208
|
|
|
|
209
|
|
|
if (!isset($rootsGroupedByScope[$key])) { |
210
|
|
|
$rootsGroupedByScope[$key] = []; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
$rootsGroupedByScope[$key][] = $root; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
return $rootsGroupedByScope; |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
/** |
220
|
|
|
* Builds a single string for the given scope columns values. Useful for |
221
|
|
|
* making array keys for grouping. |
222
|
|
|
* |
223
|
|
|
* @param Baum\Node $node |
|
|
|
|
224
|
|
|
* @return string |
225
|
|
|
*/ |
226
|
|
|
protected function keyForScope(Model $node): string |
227
|
|
|
{ |
228
|
|
|
return implode('-', array_map(function ($column) use ($node) { |
229
|
|
|
$value = $node->getAttribute($column); |
230
|
|
|
|
231
|
|
|
if (is_null($value)) { |
232
|
|
|
return 'NULL'; |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
return $value; |
236
|
|
|
}, $node->getScopedColumns())); |
|
|
|
|
237
|
|
|
} |
238
|
|
|
} |
239
|
|
|
|