Version: current

      1 <?php
      2
      3 /**
      4  * DataObjects that use the Hierachy decorator can be be organised as a hierachy, with children and parents.
      5  * The most obvious example of this is SiteTree.
      6  * @package sapphire
      7  * @subpackage model
      8  */
      9 class Hierarchy extends DataObjectDecorator {
     10
     11     protected $markedNodes;
     12     protected $markingFilter;
     13     /**
     14      * @var Int
     15      */
     16     protected $_cache_numChildren;
     17
     18     function augmentSQL(SQLQuery &$query) {
     19
     20     }
     21
     22     function augmentDatabase() {
     23
     24     }
     25
     26     function augmentWrite(&$manipulation) {
     27
     28     }
     29
     30     /**
     31      * Returns the children of this DataObject as an XHTML UL. This will be called recursively on each child,
     32      * so if they have children they will be displayed as a UL inside a LI.
     33      * @param string $attributes Attributes to add to the UL.
     34      * @param string $titleEval PHP code to evaluate to start each child - this should include '<li>'
     35      * @param string $extraArg Extra arguments that will be passed on to children, for if they overload this function.
     36      * @param boolean $limitToMarked Display only marked children.
     37      * @param string $childrenMethod The name of the method used to get children from each object
     38      * @param boolean $rootCall Set to true for this first call, and then to false for calls inside the recursion. You should not change this.
     39      * @param int $minNodeCount
     40      * @return string
     41      */
     42     public function getChildrenAsUL($attributes = "", $titleEval = '"<li>" . $child->Title', $extraArg = null, $limitToMarked = false, $childrenMethod = "AllChildrenIncludingDeleted", $numChildrenMethod = "numChildren", $rootCall = true, $minNodeCount = 30) {
     43         if ($limitToMarked && $rootCall) {
     44             $this->markingFinished($numChildrenMethod);
     45         }
     46
     47         if ($this->owner->hasMethod($childrenMethod)) {
     48             $children = $this->owner->$childrenMethod($extraArg);
     49         } else {
     50             user_error(sprintf("Can't find the method '%s' on class '%s' for getting tree children",
     51                             $childrenMethod, get_class($this->owner)), E_USER_ERROR);
     52         }
     53
     54         if ($children) {
     55             if ($attributes) {
     56                 $attributes = " $attributes";
     57             }
     58
     59             $output = "<ul$attributes>\n";
     60
     61             foreach ($children as $child) {
     62                 if (!$limitToMarked || $child->isMarked()) {
     63                     $foundAChild = true;
     64                     $output .= eval("return $titleEval;") . "\n" .
     65                             $child->getChildrenAsUL("", $titleEval, $extraArg, $limitToMarked, $childrenMethod, $numChildrenMethod, false, $minNodeCount) . "</li>\n";
     66                 }
     67             }
     68
     69             $output .= "</ul>\n";
     70         }
     71
     72         if (isset($foundAChild) && $foundAChild) {
     73             return $output;
     74         }
     75     }
     76
     77     /**
     78      * Mark a segment of the tree, by calling mark().
     79      * The method performs a breadth-first traversal until the number of nodes is more than minCount.
     80      * This is used to get a limited number of tree nodes to show in the CMS initially.
     81      *
     82      * This method returns the number of nodes marked.  After this method is called other methods
     83      * can check isExpanded() and isMarked() on individual nodes.
     84      *
     85      * @param int $minNodeCount The minimum amount of nodes to mark.
     86      * @return int The actual number of nodes marked.
     87      */
     88     public function markPartialTree($minNodeCount = 30, $context = null, $childrenMethod = "AllChildrenIncludingDeleted", $numChildrenMethod = "numChildren") {
     89         if (!is_numeric($minNodeCount))
     90             $minNodeCount = 30;
     91
     92         $this->markedNodes = array($this->owner->ID => $this->owner);
     93         $this->owner->markUnexpanded();
     94
     95         // foreach can't handle an ever-growing $nodes list
     96         while (list($id, $node) = each($this->markedNodes)) {
     97             $this->markChildren($node, $context, $childrenMethod, $numChildrenMethod);
     98             if ($minNodeCount && sizeof($this->markedNodes) >= $minNodeCount) {
     99                 break;
    100             }
    101         }
    102         return sizeof($this->markedNodes);
    103     }
    104
    105     /**
    106      * Filter the marking to only those object with $node->$parameterName = $parameterValue
    107      * @param string $parameterName The parameter on each node to check when marking.
    108      * @param mixed $parameterValue The value the parameter must be to be marked.
    109      */
    110     public function setMarkingFilter($parameterName, $parameterValue) {
    111         $this->markingFilter = array(
    112             "parameter" => $parameterName,
    113             "value" => $parameterValue
    114         );
    115     }
    116
    117     /**
    118      * Filter the marking to only those where the function returns true.
    119      * The node in question will be passed to the function.
    120      * @param string $funcName The function name.
    121      */
    122     public function setMarkingFilterFunction($funcName) {
    123         $this->markingFilter = array(
    124             "func" => $funcName,
    125         );
    126     }
    127
    128     /**
    129      * Returns true if the marking filter matches on the given node.
    130      * @param DataObject $node Node to check.
    131      * @return boolean
    132      */
    133     public function markingFilterMatches($node) {
    134         if (!$this->markingFilter) {
    135             return true;
    136         }
    137
    138         if (isset($this->markingFilter['parameter']) && $parameterName = $this->markingFilter['parameter']) {
    139             if (is_array($this->markingFilter['value'])) {
    140                 $ret = false;
    141                 foreach ($this->markingFilter['value'] as $value) {
    142                     $ret = $ret || $node->$parameterName == $value;
    143                     if ($ret == true) {
    144                         break;
    145                     }
    146                 }
    147                 return $ret;
    148             } else {
    149                 return ($node->$parameterName == $this->markingFilter['value']);
    150             }
    151         } else if ($func = $this->markingFilter['func']) {
    152             return call_user_func($func, $node);
    153         }
    154     }
    155
    156     /**
    157      * Mark all children of the given node that match the marking filter.
    158      * @param DataObject $node Parent node.
    159      */
    160     public function markChildren($node, $context = null, $childrenMethod = "AllChildrenIncludingDeleted", $numChildrenMethod = "numChildren") {
    161         if ($node->hasMethod($childrenMethod)) {
    162             $children = $node->$childrenMethod($context);
    163         } else {
    164             user_error(sprintf("Can't find the method '%s' on class '%s' for getting tree children",
    165                             $childrenMethod, get_class($this->owner)), E_USER_ERROR);
    166         }
    167
    168         $node->markExpanded();
    169         if ($children) {
    170             foreach ($children as $child) {
    171                 if (!$this->markingFilter || $this->markingFilterMatches($child)) {
    172                     if ($child->$numChildrenMethod()) {
    173                         $child->markUnexpanded();
    174                     } else {
    175                         $child->markExpanded();
    176                     }
    177                     $this->markedNodes[$child->ID] = $child;
    178                 }
    179             }
    180         }
    181     }
    182
    183     /**
    184      * Ensure marked nodes that have children are also marked expanded.
    185      * Call this after marking but before iterating over the tree.
    186      */
    187     protected function markingFinished($numChildrenMethod = "numChildren") {
    188         // Mark childless nodes as expanded.
    189         if ($this->markedNodes) {
    190             foreach ($this->markedNodes as $id => $node) {
    191                 if (!$node->isExpanded() && !$node->$numChildrenMethod()) {
    192                     $node->markExpanded();
    193                 }
    194             }
    195         }
    196     }
    197
    198     /**
    199      * Return CSS classes of 'unexpanded', 'closed', both, or neither, depending on
    200      * the marking of this DataObject.
    201      */
    202     public function markingClasses() {
    203         $classes = '';
    204         if (!$this->isExpanded()) {
    205             $classes .= " unexpanded";
    206         }
    207         if (!$this->isTreeOpened()) {
    208             $classes .= " closed";
    209         }
    210         return $classes;
    211     }
    212
    213     /**
    214      * Mark the children of the DataObject with the given ID.
    215      * @param int $id ID of parent node.
    216      * @param boolean $open If this is true, mark the parent node as opened.
    217      */
    218     public function markById($id, $open = false) {
    219         if (isset($this->markedNodes[$id])) {
    220             $this->markChildren($this->markedNodes[$id]);
    221             if ($open) {
    222                 $this->markedNodes[$id]->markOpened();
    223             }
    224             return true;
    225         } else {
    226             return false;
    227         }
    228     }
    229
    230     /**
    231      * Expose the given object in the tree, by marking this page and all it ancestors.
    232      * @param DataObject $childObj
    233      */
    234     public function markToExpose($childObj) {
    235         if (is_object($childObj)) {
    236             $stack = array_reverse($childObj->parentStack());
    237             foreach ($stack as $stackItem) {
    238                 $this->markById($stackItem->ID, true);
    239             }
    240         }
    241     }
    242
    243     /**
    244      * Return the IDs of all the marked nodes
    245      */
    246     public function markedNodeIDs() {
    247         return array_keys($this->markedNodes);
    248     }
    249
    250     /**
    251      * Return an array of this page and its ancestors, ordered item -> root.
    252      * @return array
    253      */
    254     public function parentStack() {
    255         $p = $this->owner;
    256
    257         while ($p) {
    258             $stack[] = $p;
    259             $p = $p->ParentID ? $p->Parent() : null;
    260         }
    261
    262         return $stack;
    263     }
    264
    265     /**
    266      * True if this DataObject is marked.
    267      * @var boolean
    268      */
    269     protected static $marked = array();
    270     /**
    271      * True if this DataObject is expanded.
    272      * @var boolean
    273      */
    274     protected static $expanded = array();
    275     /**
    276      * True if this DataObject is opened.
    277      * @var boolean
    278      */
    279     protected static $treeOpened = array();
    280
    281     /**
    282      * Mark this DataObject as expanded.
    283      */
    284     public function markExpanded() {
    285         self::$marked[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
    286         self::$expanded[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
    287     }
    288
    289     /**
    290      * Mark this DataObject as unexpanded.
    291      */
    292     public function markUnexpanded() {
    293         self::$marked[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
    294         self::$expanded[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = false;
    295     }
    296
    297     /**
    298      * Mark this DataObject's tree as opened.
    299      */
    300     public function markOpened() {
    301         self::$marked[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
    302         self::$treeOpened[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
    303     }
    304
    305     /**
    306      * Check if this DataObject is marked.
    307      * @return boolean
    308      */
    309     public function isMarked() {
    310         $baseClass = ClassInfo::baseDataClass($this->owner->class);
    311         $id = $this->owner->ID;
    312         return isset(self::$marked[$baseClass][$id]) ? self::$marked[$baseClass][$id] : false;
    313     }
    314
    315     /**
    316      * Check if this DataObject is expanded.
    317      * @return boolean
    318      */
    319     public function isExpanded() {
    320         $baseClass = ClassInfo::baseDataClass($this->owner->class);
    321         $id = $this->owner->ID;
    322         return isset(self::$expanded[$baseClass][$id]) ? self::$expanded[$baseClass][$id] : false;
    323     }
    324
    325     /**
    326      * Check if this DataObject's tree is opened.
    327      */
    328     public function isTreeOpened() {
    329         $baseClass = ClassInfo::baseDataClass($this->owner->class);
    330         $id = $this->owner->ID;
    331         return isset(self::$treeOpened[$baseClass][$id]) ? self::$treeOpened[$baseClass][$id] : false;
    332     }
    333
    334     /**
    335      * Return a partial tree as an HTML UL.
    336      */
    337     public function partialTreeAsUL($minCount = 50) {
    338         $children = $this->owner->AllChildren();
    339         if ($children) {
    340             if ($attributes)
    341                 $attributes = " $attributes";
    342             $output = "<ul$attributes>\n";
    343
    344             foreach ($children as $child) {
    345                 $output .= eval("return $titleEval;") . "\n" .
    346                         $child->getChildrenAsUL("", $titleEval, $extraArg) . "</li>\n";
    347             }
    348             $output .= "</ul>\n";
    349         }
    350         return $output;
    351     }
    352
    353     /**
    354      * Get a list of this DataObject's and all it's descendants IDs.
    355      * @return int
    356      */
    357     public function getDescendantIDList() {
    358         $idList = array();
    359         $this->loadDescendantIDListInto($idList);
    360         return $idList;
    361     }
    362
    363     /**
    364      * Get a list of this DataObject's and all it's descendants ID, and put it in $idList.
    365      * @var array $idList Array to put results in.
    366      */
    367     public function loadDescendantIDListInto(&$idList) {
    368         if ($children = $this->AllChildren()) {
    369             foreach ($children as $child) {
    370                 if (in_array($child->ID, $idList)) {
    371                     continue;
    372                 }
    373                 $idList[] = $child->ID;
    374                 $ext = $child->getExtensionInstance('Hierarchy');
    375                 $ext->setOwner($child);
    376                 $ext->loadDescendantIDListInto($idList);
    377                 $ext->clearOwner();
    378             }
    379         }
    380     }
    381
    382     /**
    383      * Get the children for this DataObject.
    384      * @return DataObjectSet
    385      */
    386     public function Children() {
    387         if (!(isset($this->_cache_children) && $this->_cache_children)) {
    388             $result = $this->owner->stageChildren(false);
    389             if (isset($result)) {
    390                 $this->_cache_children = new DataObjectSet();
    391                 foreach ($result as $child) {
    392                     if ($child->canView()) {
    393                         $this->_cache_children->push($child);
    394                     }
    395                 }
    396             }
    397         }
    398         return $this->_cache_children;
    399     }
    400
    401     /**
    402      * Return all children, including those 'not in menus'.
    403      * @return DataObjectSet
    404      */
    405     public function AllChildren($st=1) {
    406         return $this->owner->stageChildren($st);
    407     }
    408
    409     /**
    410      * Return all children, including those that have been deleted but are still in live.
    411      * Deleted children will be marked as "DeletedFromStage"
    412      * Added children will be marked as "AddedToStage"
    413      * Modified children will be marked as "ModifiedOnStage"
    414      * Everything else has "SameOnStage" set, as an indicator that this information has been looked up.
    415      * @return DataObjectSet
    416      */
    417     public function AllChildrenIncludingDeleted($context = null) {
    418         return $this->doAllChildrenIncludingDeleted($context);
    419     }
    420
    421     /**
    422      * @see AllChildrenIncludingDeleted
    423      *
    424      * @param unknown_type $context
    425      * @return DataObjectSet
    426      */
    427     public function doAllChildrenIncludingDeleted($context = null) {
    428         if (!$this->owner)
    429             user_error('Hierarchy::doAllChildrenIncludingDeleted() called without $this->owner');
    430
    431         $idxStageChildren = array();
    432         $idxLiveChildren = array();
    433
    434         $baseClass = ClassInfo::baseDataClass($this->owner->class);
    435         if ($baseClass) {
    436             $stageChildren = $this->owner->stageChildren(true);
    437
    438             // Add live site content that doesn't exist on the stage site, if required.
    439             if ($this->owner->hasExtension('Versioned')) {
    440                 // Next, go through the live children.  Only some of these will be listed
    441                 $liveChildren = $this->owner->liveChildren(true, true);
    442                 if ($liveChildren) {
    443                     foreach ($liveChildren as $child) {
    444                         $stageChildren->push($child);
    445                     }
    446                 }
    447             }
    448
    449             $this->owner->extend("augmentAllChildrenIncludingDeleted", $stageChildren, $context);
    450         } else {
    451             user_error("Hierarchy::AllChildren() Couldn't determine base class for '{$this->owner->class}'", E_USER_ERROR);
    452         }
    453
    454         return $stageChildren;
    455     }
    456
    457     /**
    458      * Return all the children that this page had, including pages that were deleted
    459      * from both stage & live.
    460      */
    461     public function AllHistoricalChildren() {
    462         $baseClass = ClassInfo::baseDataClass($this->owner->class);
    463         return Versioned::get_including_deleted($baseClass,
    464                 "\"ParentID\" = " . (int) $this->owner->ID, "\"$baseClass\".\"ID\" ASC");
    465     }
    466
    467     /**
    468      * Return the number of children that this page ever had, including pages that were deleted
    469      */
    470     public function numHistoricalChildren() {
    471         $query = Versioned::get_including_deleted_query(ClassInfo::baseDataClass($this->owner->class),
    472                         "\"ParentID\" = " . (int) $this->owner->ID);
    473
    474         return $query->unlimitedRowCount();
    475     }
    476
    477     /**
    478      * Return the number of direct children.
    479      * By default, values are cached after the first invocation.
    480      * Can be augumented by {@link augmentNumChildrenCountQuery()}.
    481      *
    482      * @param Boolean $cache
    483      * @return int
    484      */
    485     public function numChildren($cache = true) {
    486         $baseClass = ClassInfo::baseDataClass($this->owner->class);
    487
    488         // Build the cache for this class if it doesn't exist.
    489         if (!$cache || !is_numeric($this->_cache_numChildren)) {
    490             // We build the query in an extension-friendly way.
    491             $query = new SQLQuery(
    492                             "COUNT(*)",
    493                             "\"$baseClass\"",
    494                             sprintf('"ParentID" = %d', $this->owner->ID)
    495             );
    496             $this->owner->extend('augmentSQL', $query);
    497             $this->owner->extend('augmentNumChildrenCountQuery', $query);
    498
    499             $this->_cache_numChildren = (int) $query->execute()->value();
    500         }
    501
    502         // If theres no value in the cache, it just means that it doesn't have any children.
    503         return $this->_cache_numChildren;
    504     }
    505
    506     /**
    507      * Return children from the stage site
    508      *
    509      * @param showAll Inlcude all of the elements, even those not shown in the menus.
    510      *   (only applicable when extension is applied to {@link SiteTree}).
    511      * @return DataObjectSet
    512      */
    513     public function stageChildren($showAll = false) {
    514         if ($this->owner->db('ShowInMenus')) {
    515             $extraFilter = ($showAll) ? '' : " AND \"ShowInMenus\"=1";
    516         } else {
    517             $extraFilter = '';
    518         }
    519         if (!isset($_REQUEST['filter']) || $_REQUEST['filter'] != 'CMSSiteTreeFilter_Search') {
    520             if ((($showAll === false) || ($showAll == 1)) && $this->owner->db('ShowOnlyInTab')) {
    521                 $extraFilter .= " AND \"ShowOnlyInTab\"=0";
    522             }
    523         }
    524
    525         $baseClass = ClassInfo::baseDataClass($this->owner->class);
    526         // hide files and photos from import (Hidden = 1)
    527         if (is_a($this->owner, 'File') && $this->owner->hasDatabaseField('Hidden')) {
    528             $extraFilter .= " AND \"Hidden\"=0";
    529         }
    530
    531         $limit = '';
    532         $sort = '';
    533
    534         // check if the request comes from the cms-admin section, allow for content and getsubtree only
    535         $cms = preg_match("/admin\/?$/", $_SERVER['REQUEST_URI']) || preg_match("/admin\/getsubtree/", $_SERVER['REQUEST_URI']);
    536
    537         // if searched, make $cms filter false
    538         if (preg_match("/admin\/filterSiteTree/", $_SERVER['REQUEST_URI']))
    539             $cms = false;
    540
    541         // get limit
    542         $limit = $this->owner->NumberCMSChildren;
    543
    544         if ($cms && $limit)
    545             $sort = "Created DESC";
    546         else
    547             $limit = '';
    548
    549         $staged = DataObject::get($baseClass, "\"{$baseClass}\".\"ParentID\" = "
    550                         . (int) $this->owner->ID . " AND \"{$baseClass}\".\"ID\" != " . (int) $this->owner->ID
    551                         . $extraFilter, $sort, "", $limit);
    552
    553         if (!$staged)
    554             $staged = new DataObjectSet();
    555         $this->owner->extend("augmentStageChildren", $staged, $showAll);
    556         return $staged;
    557     }
    558
    559     /**
    560      * Return children from the live site, if it exists.
    561      *
    562      * @param boolean $showAll Include all of the elements, even those not shown in the menus.
    563      *   (only applicable when extension is applied to {@link SiteTree}).
    564      * @param boolean $onlyDeletedFromStage Only return items that have been deleted from stage
    565      * @return DataObjectSet
    566      */
    567     public function liveChildren($showAll = false, $onlyDeletedFromStage = false) {
    568         if ($this->owner->db('ShowInMenus')) {
    569             $extraFilter = ($showAll) ? '' : " AND \"ShowInMenus\"=1";
    570         } else {
    571             $extraFilter = '';
    572         }
    573         if (!isset($_REQUEST['filter']) || $_REQUEST['filter'] != 'CMSSiteTreeFilter_Search') {
    574             if ($this->owner->db('ShowOnlyInTab')) {
    575                 $extraFilter .= " AND \"ShowOnlyInTab\"=0";
    576             }
    577         }
    578         $join = "";
    579
    580         $baseClass = ClassInfo::baseDataClass($this->owner->class);
    581
    582         $filter = "\"{$baseClass}\".\"ParentID\" = " . (int) $this->owner->ID
    583                 . " AND \"{$baseClass}\".\"ID\" != " . (int) $this->owner->ID
    584                 . $extraFilter;
    585
    586         if ($onlyDeletedFromStage) {
    587             // Note that the lack of double-quotes around $baseClass are the only thing preventing
    588             // it from being rewritten to {$baseClass}_Live.  This is brittle and a little clumsy
    589             $join = "LEFT JOIN {$baseClass} ON {$baseClass}.\"ID\" = \"{$baseClass}\".\"ID\"";
    590             $filter .= " AND {$baseClass}.\"ID\" IS NULL";
    591         }
    592
    593         $oldStage = Versioned::current_stage();
    594         Versioned::reading_stage('Live');
    595
    596         // Singleton is necessary and not $this->owner so as not to muck with Translatable's
    597         // behaviour.
    598         $query = singleton($baseClass)->extendedSQL($filter, null, null, $join);
    599
    600         // Since we didn't include double quotes in the join & filter, we need to add them into the
    601         // SQL now, after Versioned has done is query rewriting
    602         $correctedSQL = str_replace(array("LEFT JOIN {$baseClass}", "{$baseClass}.\"ID\""),
    603                         array("LEFT JOIN \"{$baseClass}\"", "\"{$baseClass}\".\"ID\""), $query->sql());
    604
    605         $result = $this->owner->buildDataObjectSet(DB::query($correctedSQL));
    606
    607         Versioned::reading_stage($oldStage);
    608
    609         return $result;
    610     }
    611
    612     /**
    613      * Get the parent of this class.
    614      * @return DataObject
    615      */
    616     public function getParent($filter = '') {
    617         if ($p = $this->owner->__get("ParentID")) {
    618             $tableClasses = ClassInfo::dataClassesFor($this->owner->class);
    619             $baseClass = array_shift($tableClasses);
    620             $filter .= ( $filter) ? " AND " : "" . "\"$baseClass\".\"ID\" = $p";
    621             return DataObject::get_one($this->owner->class, $filter);
    622         }
    623     }
    624
    625     /**
    626      * Return all the parents of this class in a set ordered from the lowest to highest parent.
    627      *
    628      * @return DataObjectSet
    629      */
    630     public function getAncestors() {
    631         $ancestors = new DataObjectSet();
    632         $object = $this->owner;
    633
    634         while ($object = $object->getParent()) {
    635             $ancestors->push($object);
    636         }
    637
    638         return $ancestors;
    639     }
    640
    641     /**
    642      * Get the next node in the tree of the type. If there is no instance of the className descended from this node,
    643      * then search the parents.
    644      *
    645      * @todo Write!
    646      */
    647     public function naturalPrev($className, $afterNode = null) {
    648         return null;
    649     }
    650
    651     /**
    652      * Get the next node in the tree of the type. If there is no instance of the className descended from this node,
    653      * then search the parents.
    654      * @param string $className Class name of the node to find.
    655      * @param string|int $root ID/ClassName of the node to limit the search to
    656      * @param DataObject afterNode Used for recursive calls to this function
    657      * @return DataObject
    658      */
    659     public function naturalNext($className = null, $root = 0, $afterNode = null) {
    660         // If this node is not the node we are searching from, then we can possibly return this
    661         // node as a solution
    662         if ($afterNode && $afterNode->ID != $this->owner->ID) {
    663             if (!$className || ($className && $this->owner->class == $className)) {
    664                 return $this->owner;
    665             }
    666         }
    667
    668         $nextNode = null;
    669         $baseClass = ClassInfo::baseDataClass($this->owner->class);
    670
    671         $children = DataObject::get(ClassInfo::baseDataClass($this->owner->class), "\"$baseClass\".\"ParentID\"={$this->owner->ID}" . ( ( $afterNode ) ? " AND \"Sort\" > " . sprintf('%d', $afterNode->Sort) : "" ), '"Sort" ASC');
    672
    673         // Try all the siblings of this node after the given node
    674         /* if( $siblings = DataObject::get( ClassInfo::baseDataClass($this->owner->class), "\"ParentID\"={$this->owner->ParentID}" . ( $afterNode ) ? "\"Sort\" > {$afterNode->Sort}" : "" , '\"Sort\" ASC' ) )
    675           $searchNodes->merge( $siblings ); */
    676
    677         if ($children) {
    678             foreach ($children as $node) {
    679                 if ($nextNode = $node->naturalNext($className, $node->ID, $this->owner)) {
    680                     break;
    681                 }
    682             }
    683
    684             if ($nextNode) {
    685                 return $nextNode;
    686             }
    687         }
    688
    689         // if this is not an instance of the root class or has the root id, search the parent
    690         if (!(is_numeric($root) && $root == $this->owner->ID || $root == $this->owner->class) && ($parent = $this->owner->Parent())) {
    691             return $parent->naturalNext($className, $root, $this->owner);
    692         }
    693
    694         return null;
    695     }
    696
    697     function flushCache() {
    698         $this->_cache_children = null;
    699         $this->_cache_numChildren = null;
    700     }
    701
    702 }
    703 ?>
    704

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -