| Total Complexity | 55 | 
| Total Lines | 419 | 
| Duplicated Lines | 0 % | 
| Changes | 0 | ||
Complex classes like Hierarchy often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Hierarchy, and based on these observations, apply Extract Interface, too.
| 1 | <?php | ||
| 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() | ||
| 188 | } | ||
| 189 | |||
| 190 | /** | ||
| 191 | * Return all children, including those 'not in menus'. | ||
| 192 | * | ||
| 193 | * @return DataList | ||
| 194 | */ | ||
| 195 | public function AllChildren() | ||
| 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 | /** @var DataObject|Hierarchy|Versioned $owner */ | ||
| 212 | $owner = $this->owner; | ||
| 213 | $stageChildren = $owner->stageChildren(true); | ||
| 214 | |||
| 215 | // Add live site content that doesn't exist on the stage site, if required. | ||
| 216 |         if ($owner->hasExtension(Versioned::class) && $owner->hasStages()) { | ||
| 217 | // Next, go through the live children. Only some of these will be listed | ||
| 218 | $liveChildren = $owner->liveChildren(true, true); | ||
| 219 |             if ($liveChildren) { | ||
| 220 | $merged = new ArrayList(); | ||
| 221 | $merged->merge($stageChildren); | ||
| 222 | $merged->merge($liveChildren); | ||
| 223 | $stageChildren = $merged; | ||
| 224 | } | ||
| 225 | } | ||
| 226 |         $owner->extend("augmentAllChildrenIncludingDeleted", $stageChildren); | ||
| 227 | return $stageChildren; | ||
| 228 | } | ||
| 229 | |||
| 230 | /** | ||
| 231 | * Return all the children that this page had, including pages that were deleted from both stage & live. | ||
| 232 | * | ||
| 233 | * @return DataList | ||
| 234 | * @throws Exception | ||
| 235 | */ | ||
| 236 | public function AllHistoricalChildren() | ||
| 237 |     { | ||
| 238 | /** @var DataObject|Versioned|Hierarchy $owner */ | ||
| 239 | $owner = $this->owner; | ||
| 240 |         if (!$owner->hasExtension(Versioned::class) || !$owner->hasStages()) { | ||
| 241 | throw new Exception( | ||
| 242 | 'Hierarchy->AllHistoricalChildren() only works with Versioned extension applied with staging' | ||
| 243 | ); | ||
| 244 | } | ||
| 245 | |||
| 246 | $baseTable = $owner->baseTable(); | ||
| 247 | $parentIDColumn = $owner->getSchema()->sqlColumnForField($owner, 'ParentID'); | ||
| 248 | return Versioned::get_including_deleted( | ||
| 249 | $owner->baseClass(), | ||
| 250 | [ $parentIDColumn => $owner->ID ], | ||
| 251 |             "\"{$baseTable}\".\"ID\" ASC" | ||
| 252 | ); | ||
| 253 | } | ||
| 254 | |||
| 255 | /** | ||
| 256 | * Return the number of children that this page ever had, including pages that were deleted. | ||
| 257 | * | ||
| 258 | * @return int | ||
| 259 | */ | ||
| 260 | public function numHistoricalChildren() | ||
| 261 |     { | ||
| 262 | return $this->AllHistoricalChildren()->count(); | ||
| 263 | } | ||
| 264 | |||
| 265 | /** | ||
| 266 | * Return the number of direct children. By default, values are cached after the first invocation. Can be | ||
| 267 |      * augumented by {@link augmentNumChildrenCountQuery()}. | ||
| 268 | * | ||
| 269 | * @param bool $cache Whether to retrieve values from cache | ||
| 270 | * @return int | ||
| 271 | */ | ||
| 272 | public function numChildren($cache = true) | ||
| 273 |     { | ||
| 274 | // Load if caching | ||
| 275 |         if ($cache) { | ||
| 276 | $numChildren = $this->owner->_cache_numChildren; | ||
| 277 |             if (isset($numChildren)) { | ||
| 278 | return $numChildren; | ||
| 279 | } | ||
| 280 | } | ||
| 281 | |||
| 282 | // We call stageChildren(), because Children() has canView() filtering | ||
| 283 | $numChildren = (int)$this->owner->stageChildren(true)->Count(); | ||
| 284 | |||
| 285 | // Save if caching | ||
| 286 |         if ($cache) { | ||
| 287 | $this->owner->_cache_numChildren = $numChildren; | ||
| 288 | } | ||
| 289 | return $numChildren; | ||
| 290 | } | ||
| 291 | |||
| 292 | /** | ||
| 293 | * Checks if we're on a controller where we should filter. ie. Are we loading the SiteTree? | ||
| 294 | * | ||
| 295 | * @return bool | ||
| 296 | */ | ||
| 297 | public function showingCMSTree() | ||
| 298 |     { | ||
| 299 |         if (!Controller::has_curr() || !class_exists(LeftAndMain::class)) { | ||
| 300 | return false; | ||
| 301 | } | ||
| 302 | $controller = Controller::curr(); | ||
| 303 | return $controller instanceof LeftAndMain | ||
| 304 |             && in_array($controller->getAction(), array("treeview", "listview", "getsubtree")); | ||
| 305 | } | ||
| 306 | |||
| 307 | /** | ||
| 308 | * Return children in the stage site. | ||
| 309 | * | ||
| 310 | * @param bool $showAll Include all of the elements, even those not shown in the menus. Only applicable when | ||
| 311 |      *                      extension is applied to {@link SiteTree}. | ||
| 312 | * @return DataList | ||
| 313 | */ | ||
| 314 | public function stageChildren($showAll = false) | ||
| 315 |     { | ||
| 316 | $hideFromHierarchy = $this->owner->config()->hide_from_hierarchy; | ||
| 317 | $hideFromCMSTree = $this->owner->config()->hide_from_cms_tree; | ||
| 318 | $baseClass = $this->owner->baseClass(); | ||
| 319 | $staged = DataObject::get($baseClass) | ||
| 320 |                 ->filter('ParentID', (int)$this->owner->ID) | ||
| 321 |                 ->exclude('ID', (int)$this->owner->ID); | ||
| 322 |         if ($hideFromHierarchy) { | ||
| 323 |             $staged = $staged->exclude('ClassName', $hideFromHierarchy); | ||
| 324 | } | ||
| 325 |         if ($hideFromCMSTree && $this->showingCMSTree()) { | ||
| 326 |             $staged = $staged->exclude('ClassName', $hideFromCMSTree); | ||
| 327 | } | ||
| 328 |         if (!$showAll && DataObject::getSchema()->fieldSpec($this->owner, 'ShowInMenus')) { | ||
| 329 |             $staged = $staged->filter('ShowInMenus', 1); | ||
| 330 | } | ||
| 331 |         $this->owner->extend("augmentStageChildren", $staged, $showAll); | ||
| 332 | return $staged; | ||
| 333 | } | ||
| 334 | |||
| 335 | /** | ||
| 336 | * Return children in the live site, if it exists. | ||
| 337 | * | ||
| 338 | * @param bool $showAll Include all of the elements, even those not shown in the menus. Only | ||
| 339 |      *                                   applicable when extension is applied to {@link SiteTree}. | ||
| 340 | * @param bool $onlyDeletedFromStage Only return items that have been deleted from stage | ||
| 341 | * @return DataList | ||
| 342 | * @throws Exception | ||
| 343 | */ | ||
| 344 | public function liveChildren($showAll = false, $onlyDeletedFromStage = false) | ||
| 345 |     { | ||
| 346 | /** @var Versioned|DataObject|Hierarchy $owner */ | ||
| 347 | $owner = $this->owner; | ||
| 348 |         if (!$owner->hasExtension(Versioned::class) || !$owner->hasStages()) { | ||
| 349 |             throw new Exception('Hierarchy->liveChildren() only works with Versioned extension applied with staging'); | ||
| 350 | } | ||
| 351 | |||
| 352 | $hideFromHierarchy = $owner->config()->hide_from_hierarchy; | ||
| 353 | $hideFromCMSTree = $owner->config()->hide_from_cms_tree; | ||
| 354 | $children = DataObject::get($owner->baseClass()) | ||
| 355 |             ->filter('ParentID', (int)$owner->ID) | ||
| 356 |             ->exclude('ID', (int)$owner->ID) | ||
| 357 | ->setDataQueryParam(array( | ||
| 358 | 'Versioned.mode' => $onlyDeletedFromStage ? 'stage_unique' : 'stage', | ||
| 359 | 'Versioned.stage' => 'Live' | ||
| 360 | )); | ||
| 361 |         if ($hideFromHierarchy) { | ||
| 362 |             $children = $children->exclude('ClassName', $hideFromHierarchy); | ||
| 363 | } | ||
| 364 |         if ($hideFromCMSTree && $this->showingCMSTree()) { | ||
| 365 |             $children = $children->exclude('ClassName', $hideFromCMSTree); | ||
| 366 | } | ||
| 367 |         if (!$showAll && DataObject::getSchema()->fieldSpec($owner, 'ShowInMenus')) { | ||
| 368 |             $children = $children->filter('ShowInMenus', 1); | ||
| 369 | } | ||
| 370 | |||
| 371 | return $children; | ||
| 372 | } | ||
| 373 | |||
| 374 | /** | ||
| 375 | * Get this object's parent, optionally filtered by an SQL clause. If the clause doesn't match the parent, nothing | ||
| 376 | * is returned. | ||
| 377 | * | ||
| 378 | * @param string $filter | ||
| 379 | * @return DataObject | ||
| 380 | */ | ||
| 381 | public function getParent($filter = null) | ||
| 392 | ]); | ||
| 393 | } | ||
| 394 | |||
| 395 | /** | ||
| 396 | * Return all the parents of this class in a set ordered from the closest to furtherest parent. | ||
| 397 | * | ||
| 398 | * @param bool $includeSelf | ||
| 399 | * @return ArrayList | ||
| 400 | */ | ||
| 401 | public function getAncestors($includeSelf = false) | ||
| 402 |     { | ||
| 403 | $ancestors = new ArrayList(); | ||
| 404 | $object = $this->owner; | ||
| 405 | |||
| 406 |         if ($includeSelf) { | ||
| 407 | $ancestors->push($object); | ||
| 408 | } | ||
| 409 |         while ($object = $object->getParent()) { | ||
| 410 | $ancestors->push($object); | ||
| 411 | } | ||
| 412 | |||
| 413 | return $ancestors; | ||
| 414 | } | ||
| 415 | |||
| 416 | /** | ||
| 417 |      * Returns a human-readable, flattened representation of the path to the object, using its {@link Title} attribute. | ||
| 418 | * | ||
| 419 | * @param string $separator | ||
| 420 | * @return string | ||
| 421 | */ | ||
| 422 | public function getBreadcrumbs($separator = ' » ') | ||
| 432 | } | ||
| 433 | |||
| 434 | /** | ||
| 435 | * Flush all Hierarchy caches: | ||
| 436 | * - Children (instance) | ||
| 437 | * - NumChildren (instance) | ||
| 438 | */ | ||
| 439 | public function flushCache() | ||
| 443 | } | ||
| 444 | } | ||
| 445 | 
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths