1 | <?php |
||||||
2 | |||||||
3 | namespace SilverStripe\ORM\Hierarchy; |
||||||
4 | |||||||
5 | use SilverStripe\Admin\LeftAndMain; |
||||||
6 | use SilverStripe\Control\Controller; |
||||||
7 | use SilverStripe\ORM\DataList; |
||||||
8 | use SilverStripe\ORM\SS_List; |
||||||
9 | use SilverStripe\ORM\ValidationResult; |
||||||
10 | use SilverStripe\ORM\ArrayList; |
||||||
11 | use SilverStripe\ORM\DataObject; |
||||||
12 | use SilverStripe\ORM\DataExtension; |
||||||
13 | use SilverStripe\Versioned\Versioned; |
||||||
14 | use Exception; |
||||||
15 | |||||||
16 | /** |
||||||
17 | * DataObjects that use the Hierarchy extension can be be organised as a hierarchy, with children and parents. The most |
||||||
18 | * obvious example of this is SiteTree. |
||||||
19 | * |
||||||
20 | * @property int $ParentID |
||||||
21 | * @property DataObject|Hierarchy $owner |
||||||
22 | * @method DataObject Parent() |
||||||
23 | */ |
||||||
24 | class Hierarchy extends DataExtension |
||||||
25 | { |
||||||
26 | /** |
||||||
27 | * The lower bounds for the amount of nodes to mark. If set, the logic will expand nodes until it reaches at least |
||||||
28 | * this number, and then stops. Root nodes will always show regardless of this settting. Further nodes can be |
||||||
29 | * lazy-loaded via ajax. This isn't a hard limit. Example: On a value of 10, with 20 root nodes, each having 30 |
||||||
30 | * children, the actual node count will be 50 (all root nodes plus first expanded child). |
||||||
31 | * |
||||||
32 | * @config |
||||||
33 | * @var int |
||||||
34 | */ |
||||||
35 | private static $node_threshold_total = 50; |
||||||
36 | |||||||
37 | /** |
||||||
38 | * Limit on the maximum children a specific node can display. Serves as a hard limit to avoid exceeding available |
||||||
39 | * server resources in generating the tree, and browser resources in rendering it. Nodes with children exceeding |
||||||
40 | * this value typically won't display any children, although this is configurable through the $nodeCountCallback |
||||||
41 | * parameter in {@link getChildrenAsUL()}. "Root" nodes will always show all children, regardless of this setting. |
||||||
42 | * |
||||||
43 | * @config |
||||||
44 | * @var int |
||||||
45 | */ |
||||||
46 | private static $node_threshold_leaf = 250; |
||||||
47 | |||||||
48 | /** |
||||||
49 | * A list of classnames to exclude from display in both the CMS and front end |
||||||
50 | * displays. ->Children() and ->AllChildren affected. |
||||||
51 | * Especially useful for big sets of pages like listings |
||||||
52 | * If you use this, and still need the classes to be editable |
||||||
53 | * then add a model admin for the class |
||||||
54 | * Note: Does not filter subclasses (non-inheriting) |
||||||
55 | * |
||||||
56 | * @var array |
||||||
57 | * @config |
||||||
58 | */ |
||||||
59 | private static $hide_from_hierarchy = array(); |
||||||
60 | |||||||
61 | /** |
||||||
62 | * A list of classnames to exclude from display in the page tree views of the CMS, |
||||||
63 | * unlike $hide_from_hierarchy above which effects both CMS and front end. |
||||||
64 | * Especially useful for big sets of pages like listings |
||||||
65 | * If you use this, and still need the classes to be editable |
||||||
66 | * then add a model admin for the class |
||||||
67 | * Note: Does not filter subclasses (non-inheriting) |
||||||
68 | * |
||||||
69 | * @var array |
||||||
70 | * @config |
||||||
71 | */ |
||||||
72 | private static $hide_from_cms_tree = array(); |
||||||
73 | |||||||
74 | /** |
||||||
75 | * Prevent virtual page virtualising these fields |
||||||
76 | * |
||||||
77 | * @config |
||||||
78 | * @var array |
||||||
79 | */ |
||||||
80 | private static $non_virtual_fields = [ |
||||||
81 | '_cache_children', |
||||||
82 | '_cache_numChildren', |
||||||
83 | ]; |
||||||
84 | |||||||
85 | public static function get_extra_config($class, $extension, $args) |
||||||
86 | { |
||||||
87 | return array( |
||||||
88 | 'has_one' => array('Parent' => $class) |
||||||
89 | ); |
||||||
90 | } |
||||||
91 | |||||||
92 | /** |
||||||
93 | * Validate the owner object - check for existence of infinite loops. |
||||||
94 | * |
||||||
95 | * @param ValidationResult $validationResult |
||||||
96 | */ |
||||||
97 | public function validate(ValidationResult $validationResult) |
||||||
98 | { |
||||||
99 | // The object is new, won't be looping. |
||||||
100 | /** @var DataObject|Hierarchy $owner */ |
||||||
101 | $owner = $this->owner; |
||||||
102 | if (!$owner->ID) { |
||||||
103 | return; |
||||||
104 | } |
||||||
105 | // The object has no parent, won't be looping. |
||||||
106 | if (!$owner->ParentID) { |
||||||
107 | return; |
||||||
108 | } |
||||||
109 | // The parent has not changed, skip the check for performance reasons. |
||||||
110 | if (!$owner->isChanged('ParentID')) { |
||||||
111 | return; |
||||||
112 | } |
||||||
113 | |||||||
114 | // Walk the hierarchy upwards until we reach the top, or until we reach the originating node again. |
||||||
115 | $node = $owner; |
||||||
116 | while ($node && $node->ParentID) { |
||||||
117 | if ((int)$node->ParentID === (int)$owner->ID) { |
||||||
118 | // Hierarchy is looping. |
||||||
119 | $validationResult->addError( |
||||||
120 | _t( |
||||||
121 | __CLASS__ . '.InfiniteLoopNotAllowed', |
||||||
122 | 'Infinite loop found within the "{type}" hierarchy. Please change the parent to resolve this', |
||||||
123 | 'First argument is the class that makes up the hierarchy.', |
||||||
124 | array('type' => get_class($owner)) |
||||||
125 | ), |
||||||
126 | 'bad', |
||||||
127 | 'INFINITE_LOOP' |
||||||
128 | ); |
||||||
129 | break; |
||||||
130 | } |
||||||
131 | $node = $node->Parent(); |
||||||
132 | } |
||||||
133 | } |
||||||
134 | |||||||
135 | |||||||
136 | /** |
||||||
137 | * Get a list of this DataObject's and all it's descendants IDs. |
||||||
138 | * |
||||||
139 | * @return int[] |
||||||
140 | */ |
||||||
141 | public function getDescendantIDList() |
||||||
142 | { |
||||||
143 | $idList = array(); |
||||||
144 | $this->loadDescendantIDListInto($idList); |
||||||
145 | return $idList; |
||||||
146 | } |
||||||
147 | |||||||
148 | /** |
||||||
149 | * Get a list of this DataObject's and all it's descendants ID, and put them in $idList. |
||||||
150 | * |
||||||
151 | * @param array $idList Array to put results in. |
||||||
152 | * @param DataObject|Hierarchy $node |
||||||
153 | */ |
||||||
154 | protected function loadDescendantIDListInto(&$idList, $node = null) |
||||||
155 | { |
||||||
156 | if (!$node) { |
||||||
157 | $node = $this->owner; |
||||||
158 | } |
||||||
159 | $children = $node->AllChildren(); |
||||||
160 | foreach ($children as $child) { |
||||||
161 | if (!in_array($child->ID, $idList)) { |
||||||
162 | $idList[] = $child->ID; |
||||||
163 | $this->loadDescendantIDListInto($idList, $child); |
||||||
164 | } |
||||||
165 | } |
||||||
166 | } |
||||||
167 | |||||||
168 | /** |
||||||
169 | * Get the children for this DataObject filtered by canView() |
||||||
170 | * |
||||||
171 | * @return SS_List |
||||||
172 | */ |
||||||
173 | public function Children() |
||||||
174 | { |
||||||
175 | $children = $this->owner->_cache_children; |
||||||
176 | if ($children) { |
||||||
177 | return $children; |
||||||
178 | } |
||||||
179 | |||||||
180 | $children = $this |
||||||
181 | ->owner |
||||||
182 | ->stageChildren(false) |
||||||
183 | ->filterByCallback(function (DataObject $record) { |
||||||
184 | return $record->canView(); |
||||||
185 | }); |
||||||
186 | $this->owner->_cache_children = $children; |
||||||
187 | return $children; |
||||||
188 | } |
||||||
189 | |||||||
190 | /** |
||||||
191 | * Return all children, including those 'not in menus'. |
||||||
192 | * |
||||||
193 | * @return DataList |
||||||
194 | */ |
||||||
195 | public function AllChildren() |
||||||
196 | { |
||||||
197 | return $this->owner->stageChildren(true); |
||||||
198 | } |
||||||
199 | |||||||
200 | /** |
||||||
201 | * Return all children, including those that have been deleted but are still in live. |
||||||
202 | * - Deleted children will be marked as "DeletedFromStage" |
||||||
203 | * - Added children will be marked as "AddedToStage" |
||||||
204 | * - Modified children will be marked as "ModifiedOnStage" |
||||||
205 | * - Everything else has "SameOnStage" set, as an indicator that this information has been looked up. |
||||||
206 | * |
||||||
207 | * @return ArrayList |
||||||
208 | */ |
||||||
209 | public function AllChildrenIncludingDeleted() |
||||||
210 | { |
||||||
211 | $stageChildren = $this->owner->stageChildren(true); |
||||||
212 | |||||||
213 | // Add live site content that doesn't exist on the stage site, if required. |
||||||
214 | if ($this->owner->hasExtension(Versioned::class)) { |
||||||
0 ignored issues
–
show
|
|||||||
215 | // Next, go through the live children. Only some of these will be listed |
||||||
216 | $liveChildren = $this->owner->liveChildren(true, true); |
||||||
217 | if ($liveChildren) { |
||||||
218 | $merged = new ArrayList(); |
||||||
219 | $merged->merge($stageChildren); |
||||||
220 | $merged->merge($liveChildren); |
||||||
221 | $stageChildren = $merged; |
||||||
222 | } |
||||||
223 | } |
||||||
224 | $this->owner->extend("augmentAllChildrenIncludingDeleted", $stageChildren); |
||||||
225 | return $stageChildren; |
||||||
226 | } |
||||||
227 | |||||||
228 | /** |
||||||
229 | * Return all the children that this page had, including pages that were deleted from both stage & live. |
||||||
230 | * |
||||||
231 | * @return DataList |
||||||
232 | * @throws Exception |
||||||
233 | */ |
||||||
234 | public function AllHistoricalChildren() |
||||||
235 | { |
||||||
236 | if (!$this->owner->hasExtension(Versioned::class)) { |
||||||
237 | throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied'); |
||||||
238 | } |
||||||
239 | |||||||
240 | $baseTable = $this->owner->baseTable(); |
||||||
0 ignored issues
–
show
The method
baseTable() does not exist on SilverStripe\ORM\Hierarchy\Hierarchy .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed.
Loading history...
|
|||||||
241 | $parentIDColumn = $this->owner->getSchema()->sqlColumnForField($this->owner, 'ParentID'); |
||||||
0 ignored issues
–
show
It seems like
$this->owner can also be of type SilverStripe\ORM\Hierarchy\Hierarchy ; however, parameter $class of SilverStripe\ORM\DataObj...ma::sqlColumnForField() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
The method
getSchema() does not exist on SilverStripe\ORM\Hierarchy\Hierarchy .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed.
Loading history...
|
|||||||
242 | return Versioned::get_including_deleted( |
||||||
243 | $this->owner->baseClass(), |
||||||
244 | [ $parentIDColumn => $this->owner->ID ], |
||||||
0 ignored issues
–
show
array($parentIDColumn => $this->owner->ID) of type array<string,integer> is incompatible with the type string expected by parameter $filter of SilverStripe\Versioned\V...get_including_deleted() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||
245 | "\"{$baseTable}\".\"ID\" ASC" |
||||||
246 | ); |
||||||
247 | } |
||||||
248 | |||||||
249 | /** |
||||||
250 | * Return the number of children that this page ever had, including pages that were deleted. |
||||||
251 | * |
||||||
252 | * @return int |
||||||
253 | */ |
||||||
254 | public function numHistoricalChildren() |
||||||
255 | { |
||||||
256 | return $this->AllHistoricalChildren()->count(); |
||||||
257 | } |
||||||
258 | |||||||
259 | /** |
||||||
260 | * Return the number of direct children. By default, values are cached after the first invocation. Can be |
||||||
261 | * augumented by {@link augmentNumChildrenCountQuery()}. |
||||||
262 | * |
||||||
263 | * @param bool $cache Whether to retrieve values from cache |
||||||
264 | * @return int |
||||||
265 | */ |
||||||
266 | public function numChildren($cache = true) |
||||||
267 | { |
||||||
268 | // Load if caching |
||||||
269 | if ($cache) { |
||||||
270 | $numChildren = $this->owner->_cache_numChildren; |
||||||
271 | if (isset($numChildren)) { |
||||||
272 | return $numChildren; |
||||||
273 | } |
||||||
274 | } |
||||||
275 | |||||||
276 | // We call stageChildren(), because Children() has canView() filtering |
||||||
277 | $numChildren = (int)$this->owner->stageChildren(true)->Count(); |
||||||
278 | |||||||
279 | // Save if caching |
||||||
280 | if ($cache) { |
||||||
281 | $this->owner->_cache_numChildren = $numChildren; |
||||||
282 | } |
||||||
283 | return $numChildren; |
||||||
284 | } |
||||||
285 | |||||||
286 | /** |
||||||
287 | * Checks if we're on a controller where we should filter. ie. Are we loading the SiteTree? |
||||||
288 | * |
||||||
289 | * @return bool |
||||||
290 | */ |
||||||
291 | public function showingCMSTree() |
||||||
292 | { |
||||||
293 | if (!Controller::has_curr() || !class_exists(LeftAndMain::class)) { |
||||||
294 | return false; |
||||||
295 | } |
||||||
296 | $controller = Controller::curr(); |
||||||
297 | return $controller instanceof LeftAndMain |
||||||
298 | && in_array($controller->getAction(), array("treeview", "listview", "getsubtree")); |
||||||
299 | } |
||||||
300 | |||||||
301 | /** |
||||||
302 | * Return children in the stage site. |
||||||
303 | * |
||||||
304 | * @param bool $showAll Include all of the elements, even those not shown in the menus. Only applicable when |
||||||
305 | * extension is applied to {@link SiteTree}. |
||||||
306 | * @return DataList |
||||||
307 | */ |
||||||
308 | public function stageChildren($showAll = false) |
||||||
309 | { |
||||||
310 | $hideFromHierarchy = $this->owner->config()->hide_from_hierarchy; |
||||||
311 | $hideFromCMSTree = $this->owner->config()->hide_from_cms_tree; |
||||||
312 | $baseClass = $this->owner->baseClass(); |
||||||
313 | $staged = DataObject::get($baseClass) |
||||||
314 | ->filter('ParentID', (int)$this->owner->ID) |
||||||
315 | ->exclude('ID', (int)$this->owner->ID); |
||||||
316 | if ($hideFromHierarchy) { |
||||||
317 | $staged = $staged->exclude('ClassName', $hideFromHierarchy); |
||||||
318 | } |
||||||
319 | if ($hideFromCMSTree && $this->showingCMSTree()) { |
||||||
320 | $staged = $staged->exclude('ClassName', $hideFromCMSTree); |
||||||
321 | } |
||||||
322 | if (!$showAll && DataObject::getSchema()->fieldSpec($this->owner, 'ShowInMenus')) { |
||||||
323 | $staged = $staged->filter('ShowInMenus', 1); |
||||||
324 | } |
||||||
325 | $this->owner->extend("augmentStageChildren", $staged, $showAll); |
||||||
326 | return $staged; |
||||||
327 | } |
||||||
328 | |||||||
329 | /** |
||||||
330 | * Return children in the live site, if it exists. |
||||||
331 | * |
||||||
332 | * @param bool $showAll Include all of the elements, even those not shown in the menus. Only |
||||||
333 | * applicable when extension is applied to {@link SiteTree}. |
||||||
334 | * @param bool $onlyDeletedFromStage Only return items that have been deleted from stage |
||||||
335 | * @return DataList |
||||||
336 | * @throws Exception |
||||||
337 | */ |
||||||
338 | public function liveChildren($showAll = false, $onlyDeletedFromStage = false) |
||||||
339 | { |
||||||
340 | if (!$this->owner->hasExtension(Versioned::class)) { |
||||||
341 | throw new Exception('Hierarchy->liveChildren() only works with Versioned extension applied'); |
||||||
342 | } |
||||||
343 | |||||||
344 | $hideFromHierarchy = $this->owner->config()->hide_from_hierarchy; |
||||||
345 | $hideFromCMSTree = $this->owner->config()->hide_from_cms_tree; |
||||||
346 | $children = DataObject::get($this->owner->baseClass()) |
||||||
347 | ->filter('ParentID', (int)$this->owner->ID) |
||||||
0 ignored issues
–
show
|
|||||||
348 | ->exclude('ID', (int)$this->owner->ID) |
||||||
349 | ->setDataQueryParam(array( |
||||||
350 | 'Versioned.mode' => $onlyDeletedFromStage ? 'stage_unique' : 'stage', |
||||||
351 | 'Versioned.stage' => 'Live' |
||||||
352 | )); |
||||||
353 | if ($hideFromHierarchy) { |
||||||
354 | $children = $children->exclude('ClassName', $hideFromHierarchy); |
||||||
355 | } |
||||||
356 | if ($hideFromCMSTree && $this->showingCMSTree()) { |
||||||
357 | $children = $children->exclude('ClassName', $hideFromCMSTree); |
||||||
358 | } |
||||||
359 | if (!$showAll && DataObject::getSchema()->fieldSpec($this->owner, 'ShowInMenus')) { |
||||||
0 ignored issues
–
show
It seems like
$this->owner can also be of type SilverStripe\ORM\Hierarchy\Hierarchy ; however, parameter $classOrInstance of SilverStripe\ORM\DataObjectSchema::fieldSpec() does only seem to accept SilverStripe\ORM\DataObject|string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||
360 | $children = $children->filter('ShowInMenus', 1); |
||||||
361 | } |
||||||
362 | |||||||
363 | return $children; |
||||||
364 | } |
||||||
365 | |||||||
366 | /** |
||||||
367 | * Get this object's parent, optionally filtered by an SQL clause. If the clause doesn't match the parent, nothing |
||||||
368 | * is returned. |
||||||
369 | * |
||||||
370 | * @param string $filter |
||||||
371 | * @return DataObject |
||||||
372 | */ |
||||||
373 | public function getParent($filter = null) |
||||||
374 | { |
||||||
375 | $parentID = $this->owner->ParentID; |
||||||
376 | if (empty($parentID)) { |
||||||
377 | return null; |
||||||
378 | } |
||||||
379 | $baseClass = $this->owner->baseClass(); |
||||||
380 | $idSQL = $this->owner->getSchema()->sqlColumnForField($baseClass, 'ID'); |
||||||
381 | return DataObject::get_one($baseClass, [ |
||||||
382 | [$idSQL => $parentID], |
||||||
383 | $filter |
||||||
384 | ]); |
||||||
385 | } |
||||||
386 | |||||||
387 | /** |
||||||
388 | * Return all the parents of this class in a set ordered from the closest to furtherest parent. |
||||||
389 | * |
||||||
390 | * @param bool $includeSelf |
||||||
391 | * @return ArrayList |
||||||
392 | */ |
||||||
393 | public function getAncestors($includeSelf = false) |
||||||
394 | { |
||||||
395 | $ancestors = new ArrayList(); |
||||||
396 | $object = $this->owner; |
||||||
397 | |||||||
398 | if ($includeSelf) { |
||||||
399 | $ancestors->push($object); |
||||||
400 | } |
||||||
401 | while ($object = $object->getParent()) { |
||||||
402 | $ancestors->push($object); |
||||||
403 | } |
||||||
404 | |||||||
405 | return $ancestors; |
||||||
406 | } |
||||||
407 | |||||||
408 | /** |
||||||
409 | * Returns a human-readable, flattened representation of the path to the object, using its {@link Title} attribute. |
||||||
410 | * |
||||||
411 | * @param string $separator |
||||||
412 | * @return string |
||||||
413 | */ |
||||||
414 | public function getBreadcrumbs($separator = ' » ') |
||||||
415 | { |
||||||
416 | $crumbs = array(); |
||||||
417 | $ancestors = array_reverse($this->owner->getAncestors()->toArray()); |
||||||
418 | /** @var DataObject $ancestor */ |
||||||
419 | foreach ($ancestors as $ancestor) { |
||||||
420 | $crumbs[] = $ancestor->getTitle(); |
||||||
421 | } |
||||||
422 | $crumbs[] = $this->owner->getTitle(); |
||||||
423 | return implode($separator, $crumbs); |
||||||
424 | } |
||||||
425 | |||||||
426 | /** |
||||||
427 | * Flush all Hierarchy caches: |
||||||
428 | * - Children (instance) |
||||||
429 | * - NumChildren (instance) |
||||||
430 | */ |
||||||
431 | public function flushCache() |
||||||
432 | { |
||||||
433 | $this->owner->_cache_children = null; |
||||||
434 | $this->owner->_cache_numChildren = null; |
||||||
435 | } |
||||||
436 | } |
||||||
437 |
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.
This is most likely a typographical error or the method has been renamed.