silverstripe-framework/core/model/Hierarchy.php
Ingo Schommer 3469e4d22a Merging in refactored Translatable architecture from trunk, including related/required changesets like enhancements to Object static handling (see details below)
------------------------------------------------------------------------
r68900 | sminnee | 2008-12-15 14:30:41 +1300 (Mon, 15 Dec 2008) | 1 line

Static caching merges from dnc branch
------------------------------------------------------------------------
r68917 | sminnee | 2008-12-15 14:49:06 +1300 (Mon, 15 Dec 2008) | 1 line

Merged Requirements fix from nestedurls branch
------------------------------------------------------------------------
r70033 | aoneil | 2009-01-13 14:03:41 +1300 (Tue, 13 Jan 2009) | 2 lines

Add translation migration task

------------------------------------------------------------------------
r70072 | ischommer | 2009-01-13 17:34:27 +1300 (Tue, 13 Jan 2009) | 5 lines

API CHANGE Removed obsolete internal Translatable methods: hasOwnTranslatableFields(), allFieldsInTable()
ENHANCEMENT Removed $create flag in Translatable::getTranslation() and replaced with explit action createTranslation()
ENHANCEMENT Sorting return array of Translatable::getTranslatedLangs()
ENHANCEMENT Added a note about saving a page before creating a translation
MINOR Added phpdoc to Translatable
------------------------------------------------------------------------
r70073 | ischommer | 2009-01-13 17:34:45 +1300 (Tue, 13 Jan 2009) | 1 line

ENHANCEMENT Added basic unit tests to new Translatable API
------------------------------------------------------------------------
r70080 | aoneil | 2009-01-13 18:04:21 +1300 (Tue, 13 Jan 2009) | 3 lines

BUGFIX: Fix translatable migration regenerating URLSegments when it shouldn't
BUGFIX: Fix translatable migration not writing records to Live properly

------------------------------------------------------------------------
r70118 | ischommer | 2009-01-14 11:28:24 +1300 (Wed, 14 Jan 2009) | 3 lines

API CHANGE Removed obsolete Translatable::table_exists()
ENHANCEMENT Made Translatable constructor arguments optional, as by default all database fields are marked translatable
MINOR More unit tests for Translatable
------------------------------------------------------------------------
r70138 | ischommer | 2009-01-14 17:00:30 +1300 (Wed, 14 Jan 2009) | 1 line

BUGFIX Disabled assumption that SQLQuery->filtersOnID() should only kick in when exactly one WHERE clause is given - this is very fragile and hard to test. It would return TRUE on $where = "SiteTree.ID = 5", but not on $where = array("Lang = 'de'", "SiteTree.ID = 5")
------------------------------------------------------------------------
r70214 | ischommer | 2009-01-15 18:56:25 +1300 (Thu, 15 Jan 2009) | 3 lines

BUGFIX Falling back to Translatable::current_lang() if no $context object is given, in augmentAllChildrenIncludingDeleted() and AllChildrenIncludingDeleted()
MINOR phpdoc for Translatable
MINOR Added more Translatable unit tests
------------------------------------------------------------------------
r70306 | ischommer | 2009-01-16 17:14:34 +1300 (Fri, 16 Jan 2009) | 9 lines

ENHANCEMENT Recursively creating translations for parent pages to ensure that a translated page is still accessible by traversing the tree, e.g. in "cms translation mode" (in Translatable->onBeforeWrite())
ENHANCEMENT Simplified AllChildrenIncludingDeleted() to not require a special augmentAllChildrenIncludingDeleted() implementation: We don't combine untranslated/translated children any longer (which was used in CMS tree view), but rather just show translated records
ENHANCEMENT Ensuring uniqueness of URL segments by appending "-<langcode>" to new translations (in Translatable->onBeforeWrite())
ENHANCEMENT Added Translatable->alternateGetByUrl() as a hook into SiteTree::get_by_url()
ENHANCEMENT Adding link back to original page in CMS editform for translations
BUGFIX Excluding HiddenField instances from Translatable->updateCMSFields()
BUGFIX Don't require a record to be written (through exists()) when checking Translatable->isTranslation() or Translatable->hasTranslation()
MINOR Don't use createMethod() shortcut for Translatable->AllChildrenIncludingDeleted()
MINOR Added Translatable unit tests
------------------------------------------------------------------------
r70318 | ischommer | 2009-01-19 11:46:16 +1300 (Mon, 19 Jan 2009) | 1 line

BUGFIX Reverted special cases for Translatable in Versioned->canBeVersioned() (originally committed in r42119) - was checking for existence of underscores in table names as an indication of the "_lang" suffix, which is no longer needed. It was also a flawed assumption which tripped over classes like TranslatableTest_TestPage
------------------------------------------------------------------------
r70319 | ischommer | 2009-01-19 11:47:02 +1300 (Mon, 19 Jan 2009) | 1 line

ENHANCEMENT Disabled Translatab-e>augmentWrite() - was only needed for the blacklist fields implementation which is inactive for the moment
------------------------------------------------------------------------
r70326 | ischommer | 2009-01-19 14:25:23 +1300 (Mon, 19 Jan 2009) | 2 lines

ENHANCEMENT Making ErrorPage static HTML files translatable (#2233)
ENHANCEMENT Added ErrorPage::$static_filepath to flexibly set location of static error pages (defaults to /assets)
------------------------------------------------------------------------
r70327 | ischommer | 2009-01-19 15:18:41 +1300 (Mon, 19 Jan 2009) | 1 line

FEATURE Enabled specifying a language through a hidden field in SearchForm which limits the search to pages in this language (incl. unit tests)
------------------------------------------------------------------------
r71258 | sharvey | 2009-02-03 15:49:34 +1300 (Tue, 03 Feb 2009) | 2 lines

BUGFIX: Fix translatable being enabled when it shouldn't be

------------------------------------------------------------------------
r71340 | ischommer | 2009-02-04 14:36:12 +1300 (Wed, 04 Feb 2009) | 1 line

BUGFIX Including Hierarchy->children in flushCache() and renamed to _cache_children. This caused problems in TranslatableTest when re-using the same SiteTree->Children() method with different languages on the same object (even with calling flushCache() inbetween the calls)
------------------------------------------------------------------------
r71567 | gmunn | 2009-02-10 13:49:16 +1300 (Tue, 10 Feb 2009) | 1 line

'URLSegment' on line 484 and 494 now escaped
------------------------------------------------------------------------
r72054 | ischommer | 2009-02-23 10:30:41 +1300 (Mon, 23 Feb 2009) | 3 lines

BUGFIX Fixed finding a translated homepage without an explicit URLSegment (e.g. http://mysite.com/?lang=de) - see #3540
ENHANCEMENT Added Translatable::get_homepage_urlsegment_by_language()
ENHANCEMENT Added RootURLController::get_default_homepage_urlsegment()
------------------------------------------------------------------------
r72367 | ischommer | 2009-03-03 11:13:30 +1300 (Tue, 03 Mar 2009) | 2 lines

ENHANCEMENT Added i18n::get_lang_from_locale() and i18n::convert_rfc1766()
ENHANCEMENT Using IETF/HTTP compatible "long" language code in SiteTree->MetaTags(). This means the default <meta type="content-language..."> value will be "en-US" instead of "en". The locale can be either set through the Translatable content language, or through i18n::set_locale()
------------------------------------------------------------------------
r73036 | sminnee | 2009-03-14 13:16:32 +1300 (Sat, 14 Mar 2009) | 1 line

ENHANCEMENT #3032 ajshort: Use static methods for accessing static data
------------------------------------------------------------------------
r73059 | sminnee | 2009-03-15 14:09:59 +1300 (Sun, 15 Mar 2009) | 2 lines

ENHANCEMENT: Added Object::clearCache() to clear a cache
BUGFIX: Make object cache testing more robust
------------------------------------------------------------------------
r73338 | ischommer | 2009-03-19 05:13:40 +1300 (Thu, 19 Mar 2009) | 9 lines

API CHANGE Added concept of "translation groups" to Translatable- every page can belong to a group of related translations, rather than having an explicit "original", meaning you can have pages in "non-default" languages which have no representation in other language trees. This group is recorded in a new table "<classname>_translationgroups". Translatable->createTranslation() and Translatable->onBeforeWrite() will automatically associate records in this groups. Added Translatable->addTranslationGroup(), Translatable->removeTranslationGroup(), Translatable->getTranslationGroup()
API CHANGE Removed Translatable->isTranslation() - after the new "translation group" model, every page is potentially a translation
API CHANGE Translatable->findOriginalIDs(), Translatable->setOriginalPage(), Translatable->getOriginalPage()
ENHANCEMENT Translatable->getCMSFields() will now always show the "create translation" option, not only on default languages - meaning you can create translations based on other translations
ENHANCEMENT Translatable language dropdown in CMS will always show all available languages, rather than filtering by already existing translations
ENHANCEMENT Added check for an existing record in Translatable->createTranslation()
BUGFIX Removed Translatable->getLang() which overloaded the $db property - it was causing side effects during creation of SiteTree default records.
BUGFIX Added check in Translatable->augmentSQL() to avoid reapplying "Lang = ..." filter twice
BUGFIX Removed bypass in Translatable->AllChildrenIncludingDeleted()
------------------------------------------------------------------------
r73339 | ischommer | 2009-03-19 05:15:46 +1300 (Thu, 19 Mar 2009) | 1 line

BUGFIX Disabled "untranslated" CSS class for SiteTree elements - doesn't apply any longer with the new "translation groups" concept
------------------------------------------------------------------------
r73341 | ischommer | 2009-03-19 06:01:51 +1300 (Thu, 19 Mar 2009) | 1 line

BUGFIX Disabled auto-excluding of default language from the "available languages" array in LanguageDropdownField - due to the new "translation groups" its possible to have a translation from another language into the default language
------------------------------------------------------------------------
r73342 | ischommer | 2009-03-19 06:13:23 +1300 (Thu, 19 Mar 2009) | 4 lines

BUGFIX Setting ParentID of translated record if recursively creating parents in Translatable::onBeforeWrite()
BUGFIX Fixing inline form action for "create translation"
BUGFIX Removed link to "original page" for a translation - no longer valid
MINOR documentation for Translatable
------------------------------------------------------------------------
r73464 | ischommer | 2009-03-20 20:51:00 +1300 (Fri, 20 Mar 2009) | 1 line

MINOR documentation
------------------------------------------------------------------------
r73465 | ischommer | 2009-03-20 20:58:52 +1300 (Fri, 20 Mar 2009) | 1 line

BUGFIX Fixed Hierarchy->Children() testing in TranslatableTest - with the new datamodel you can't call Children() in a different language regardless of Translatable::set_reading_lang(), the Children() call has to be made from a parent in the same language
------------------------------------------------------------------------
r73466 | ischommer | 2009-03-20 21:36:40 +1300 (Fri, 20 Mar 2009) | 2 lines

ENHANCEMENT Added Translatable::get_locale_from_lang(), Translatable::get_common_locales(), $common_locales and $likely_subtags in preparation to switch Translatable from using short "lang" codes to proper long locales
API CHANGE Deprecated Translatable::set_default_lang(), Translatable::default_lang()
------------------------------------------------------------------------
r73467 | ischommer | 2009-03-20 21:38:57 +1300 (Fri, 20 Mar 2009) | 1 line

ENHANCEMENT Supporting "Locale-English" and "Locale-Native" as listing arguments in LanguageDropdownField
------------------------------------------------------------------------
r73468 | ischommer | 2009-03-20 21:47:06 +1300 (Fri, 20 Mar 2009) | 7 lines

ENHANCEMENT Adjusted SearchForm, Debug, ErrorPage, SiteTree to using locales instead of lang codes
API CHANGE Changed Translatable datamodel to use locales ("en_US") instead of lang values ("en).
API CHANGE Changed Translatable::$default_lang to $default_locale, Translatable::$reading_lang to $reading_locale
API CHANGE Using "locale" instead of "lang" in Translatable::choose_site_lang() to auto-detect language from cookies or GET parameters
API CHANGE Deprecated Translatable::is_default_lang(), set_default_lang(), get_default_lang(), current_lang(), set_reading_lang(), get_reading_lang(), get_by_lang(), get_one_by_lang()
API CHANGE Removed Translatable::get_original() - with the new "translation groups" concept there no longer is an original for a translation
BUGFIX Updated MigrateTranslatableTask to new Locale based datamodel
------------------------------------------------------------------------
r73470 | ischommer | 2009-03-20 21:56:57 +1300 (Fri, 20 Mar 2009) | 1 line

MINOR fixed typo
------------------------------------------------------------------------
r73472 | sminnee | 2009-03-21 17:30:04 +1300 (Sat, 21 Mar 2009) | 1 line

BUGFIX: Fixed translatable test execution by making protected methods public
------------------------------------------------------------------------
r73473 | sminnee | 2009-03-21 18:10:05 +1300 (Sat, 21 Mar 2009) | 1 line

ENHANCEMENT: Added Object::combined_static(), which gets all values of a static property from each class in the hierarchy
------------------------------------------------------------------------
r73883 | ischommer | 2009-04-01 08:32:19 +1300 (Wed, 01 Apr 2009) | 1 line

BUGFIX Making $_SINGLETONS a global instead of a static in Core.php so it can be re-used in other places
------------------------------------------------------------------------
r73951 | ischommer | 2009-04-02 05:35:32 +1300 (Thu, 02 Apr 2009) | 3 lines

API CHANGE Deprecated Translatable::enable() and i18n::enable()- use Object::add_extension('SiteTree','Translatable'), Deprecated Translatable::disable() and i18n::disable() - use Object::remove_extension('SiteTree','Translatable'), Deprecated Translatable::enabled() - use $myPage->hasExtension('Translatable')
API CHANGE Removed Translatable::creating_from() - doesn't apply any longer
ENHANCEMENT Translatable extension is no longer hooked up to SiteTree by default, which should improve performance and memory usage for sites not using Translatable. Please use Object::add_extension('SiteTree','Translatable') in your _config.php instead. Adjusted several classes (Image, ErrorPage, RootURLController) to the new behaviour.
------------------------------------------------------------------------
r73882 | ischommer | 2009-04-01 08:31:21 +1300 (Wed, 01 Apr 2009) | 1 line

ENHANCEMENT Added DataObjectDecorator->setOwner()
------------------------------------------------------------------------
r73884 | ischommer | 2009-04-01 08:32:51 +1300 (Wed, 01 Apr 2009) | 1 line

ENHANCEMENT Added Extension::get_classname_without_arguments()
------------------------------------------------------------------------
r73900 | ischommer | 2009-04-01 11:27:53 +1300 (Wed, 01 Apr 2009) | 7 lines

API CHANGE Deprecated Object->extInstance(), use getExtensionInstance() instead
ENHANCEMENT Added Object->getExtensionInstances()
ENHANCEMENT Added Object::get_extensions()
ENHANCEMENT Unsetting class caches when using Object::add_extension() to avoid problems with defineMethods etc.
BUGFIX Fixed extension comparison with case sensitivity and stripping arguments in Object::has_extension()
BUGFIX Unsetting all cached singletons in Object::remove_extension() to avoid outdated extension_instances
MINOR Documentation in Object
------------------------------------------------------------------------
r74017 | ischommer | 2009-04-03 10:49:40 +1300 (Fri, 03 Apr 2009) | 1 line

ENHANCEMENT Improved deprecated fallbacks in Translatable by auto-converting short language codes to long locales and vice versa through i18n::get_lang_from_locale()/i18n::get_locale_from_lang()
------------------------------------------------------------------------
r74030 | ischommer | 2009-04-03 11:41:26 +1300 (Fri, 03 Apr 2009) | 1 line

MINOR Re-added Translatable::default_lang() for more graceful fallback to Translatable::default_locale()
------------------------------------------------------------------------
r74065 | ischommer | 2009-04-04 05:38:51 +1300 (Sat, 04 Apr 2009) | 1 line

BUGFIX Re-added Translatable->isTranslation() for more friendly deprecation (originally removed in r73338)
------------------------------------------------------------------------
r74069 | ischommer | 2009-04-04 09:43:01 +1300 (Sat, 04 Apr 2009) | 1 line

BUGFIX Fixed legacy handling of Translatable::enable(),Translatable::disable() and Translatable::is_enabled() - applying extension to SiteTree instead of Page to avoid datamodel clashes
------------------------------------------------------------------------
r74070 | ischommer | 2009-04-04 10:23:51 +1300 (Sat, 04 Apr 2009) | 1 line

API CHANGE Deprecated Translatable::choose_site_lang(), use choose_site_locale()
------------------------------------------------------------------------
r74941 | ischommer | 2009-04-22 15:22:09 +1200 (Wed, 22 Apr 2009) | 2 lines

ENHANCEMENT Adding SapphireTest::set_up_once() and SapphireTest::tear_down_once() for better test performance with state that just needs to be initialized once per test case (not per test method). Added new SapphireTestSuite to support this through PHPUnit.
ENHANCEMENT Using set_up_once() in TranslatableTest and TranslatableSearchFormTest for better test run performance
------------------------------------------------------------------------
r74942 | ischommer | 2009-04-22 15:24:50 +1200 (Wed, 22 Apr 2009) | 1 line

BUGFIX Fixed TranslatableSearchFormTest->setUp() method
------------------------------------------------------------------------
r73509 | ischommer | 2009-03-23 11:59:14 +1300 (Mon, 23 Mar 2009) | 1 line

MINOR phpdoc documentation
------------------------------------------------------------------------


git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/branches/2.3@74986 467b73ca-7a2a-4603-9d3b-597d59a354a9
2011-02-02 14:27:16 +13:00

606 lines
18 KiB
PHP

<?php
/**
* DataObjects that use the Hierachy decorator can be be organised as a hierachy, with children and parents.
* The most obvious example of this is SiteTree.
* @package sapphire
* @subpackage model
*/
class Hierarchy extends DataObjectDecorator {
protected $markedNodes;
protected $markingFilter;
function augmentSQL(SQLQuery &$query) {
}
function augmentDatabase() {
}
function augmentWrite(&$manipulation) {
}
/**
* Returns the children of this DataObject as an XHTML UL. This will be called recursively on each child,
* so if they have children they will be displayed as a UL inside a LI.
* @param string $attributes Attributes to add to the UL.
* @param string $titleEval PHP code to evaluate to start each child - this should include '<li>'
* @param string $extraArg Extra arguments that will be passed on to children, for if they overload this function.
* @param boolean $limitToMarked Display only marked children.
* @param boolean $rootCall Set to true for this first call, and then to false for calls inside the recursion. You should not change this.
* @return string
*/
public function getChildrenAsUL($attributes = "", $titleEval = '"<li>" . $child->Title', $extraArg = null, $limitToMarked = false, $rootCall = true) {
if($limitToMarked && $rootCall) {
$this->markingFinished();
}
$children = $this->owner->AllChildrenIncludingDeleted($extraArg);
if($children) {
if($attributes) {
$attributes = " $attributes";
}
$output = "<ul$attributes>\n";
foreach($children as $child) {
if(!$limitToMarked || $child->isMarked()) {
$foundAChild = true;
$output .= eval("return $titleEval;") . "\n" .
$child->getChildrenAsUL("", $titleEval, $extraArg, $limitToMarked, false) . "</li>\n";
}
}
$output .= "</ul>\n";
}
if(isset($foundAChild) && $foundAChild) {
return $output;
}
}
/**
* Mark a segment of the tree, by calling mark().
* The method performs a breadth-first traversal until the number of nodes is more than minCount.
* This is used to get a limited number of tree nodes to show in the CMS initially.
*
* This method returns the number of nodes marked. After this method is called other methods
* can check isExpanded() and isMarked() on individual nodes.
*
* @param int $minCount The minimum amount of nodes to mark.
* @return int The actual number of nodes marked.
*/
public function markPartialTree($minCount = 30, $context = null) {
$this->markedNodes = array($this->owner->ID => $this->owner);
$this->owner->markUnexpanded();
// foreach can't handle an ever-growing $nodes list
while(list($id, $node) = each($this->markedNodes)) {
$this->markChildren($node, $context);
if($minCount && sizeof($this->markedNodes) >= $minCount) {
break;
}
}
return sizeof($this->markedNodes);
}
/**
* Filter the marking to only those object with $node->$parameterName = $parameterValue
* @param string $parameterName The parameter on each node to check when marking.
* @param mixed $parameterValue The value the parameter must be to be marked.
*/
public function setMarkingFilter($parameterName, $parameterValue) {
$this->markingFilter = array(
"parameter" => $parameterName,
"value" => $parameterValue
);
}
/**
* Filter the marking to only those where the function returns true.
* The node in question will be passed to the function.
* @param string $funcName The function name.
*/
public function setMarkingFilterFunction($funcName) {
$this->markingFilter = array(
"func" => $funcName,
);
}
/**
* Returns true if the marking filter matches on the given node.
* @param DataObject $node Node to check.
* @return boolean
*/
public function markingFilterMatches($node) {
if(!$this->markingFilter) {
return true;
}
if(isset($this->markingFilter['parameter']) && $parameterName = $this->markingFilter['parameter']) {
if(is_array($this->markingFilter['value'])){
$ret = false;
foreach($this->markingFilter['value'] as $value) {
$ret = $ret||$node->$parameterName==$value;
if($ret == true) {
break;
}
}
return $ret;
} else {
return ($node->$parameterName == $this->markingFilter['value']);
}
} else if ($funcName = $this->markingFilter['func']) {
return $funcName($node);
}
}
/**
* Mark all children of the given node that match the marking filter.
* @param DataObject $node Parent node.
*/
public function markChildren($node, $context = null) {
$children = $node->AllChildrenIncludingDeleted($context);
$node->markExpanded();
if($children) {
foreach($children as $child) {
if(!$this->markingFilter || $this->markingFilterMatches($child)) {
$child->markUnexpanded();
$this->markedNodes[$child->ID] = $child;
}
}
}
}
/**
* Ensure marked nodes that have children are also marked expanded.
* Call this after marking but before iterating over the tree.
*/
protected function markingFinished() {
// Mark childless nodes as expanded.
foreach($this->markedNodes as $id => $node) {
if(!$node->isExpanded() && !$node->numChildren()) {
$node->markExpanded();
}
}
}
/**
* Return CSS classes of 'unexpanded', 'closed', both, or neither, depending on
* the marking of this DataObject.
*/
public function markingClasses() {
$classes = '';
if(!$this->expanded) {
$classes .= " unexpanded";
}
if(!$this->treeOpened) {
$classes .= " closed";
}
return $classes;
}
/**
* Mark the children of the DataObject with the given ID.
* @param int $id ID of parent node.
* @param boolean $open If this is true, mark the parent node as opened.
*/
public function markById($id, $open = false) {
if(isset($this->markedNodes[$id])) {
$this->markChildren($this->markedNodes[$id]);
if($open) {
$this->markedNodes[$id]->markOpened();
}
return true;
} else {
return false;
}
}
/**
* Expose the given object in the tree, by marking this page and all it ancestors.
* @param DataObject $childObj
*/
public function markToExpose($childObj) {
if(is_object($childObj)){
$stack = array_reverse($childObj->parentStack());
foreach($stack as $stackItem) {
$this->markById($stackItem->ID, true);
}
}
}
/**
* Return an array of this page and its ancestors, ordered item -> root.
* @return array
*/
public function parentStack() {
$p = $this->owner;
while($p) {
$stack[] = $p;
$p = $p->ParentID ? $p->Parent() : null;
}
return $stack;
}
/**
* True if this DataObject is marked.
* @var boolean
*/
protected $marked = false;
/**
* True if this DataObject is expanded.
* @var boolean
*/
protected $expanded = false;
/**
* True if this DataObject is opened.
* @var boolean
*/
protected $treeOpened = false;
/**
* Mark this DataObject as expanded.
*/
public function markExpanded() {
$this->marked = true;
$this->expanded = true;
}
/**
* Mark this DataObject as unexpanded.
*/
public function markUnexpanded() {
$this->marked = true;
$this->expanded = false;
}
/**
* Mark this DataObject's tree as opened.
*/
public function markOpened() {
$this->marked = true;
$this->treeOpened = true;
}
/**
* Check if this DataObject is marked.
* @return boolean
*/
public function isMarked() {
return $this->marked;
}
/**
* Check if this DataObject is expanded.
* @return boolean
*/
public function isExpanded() {
return $this->expanded;
}
/**
* Check if this DataObject's tree is opened.
*/
public function isTreeOpened() {
return $this->treeOpened;
}
/**
* Return a partial tree as an HTML UL.
*/
public function partialTreeAsUL($minCount = 50) {
$children = $this->owner->AllChildren();
if($children) {
if($attributes) $attributes = " $attributes";
$output = "<ul$attributes>\n";
foreach($children as $child) {
$output .= eval("return $titleEval;") . "\n" .
$child->getChildrenAsUL("", $titleEval, $extraArg) . "</li>\n";
}
$output .= "</ul>\n";
}
return $output;
}
/**
* Get a list of this DataObject's and all it's descendants IDs.
* @return int
*/
public function getDescendantIDList() {
$idList = array();
$this->loadDescendantIDListInto($idList);
return $idList;
}
/**
* Get a list of this DataObject's and all it's descendants ID, and put it in $idList.
* @var array $idList Array to put results in.
*/
public function loadDescendantIDListInto(&$idList) {
if($children = $this->AllChildren()) {
foreach($children as $child) {
if(in_array($child->ID, $idList)) {
continue;
}
$idList[] = $child->ID;
$child->loadDescendantIDListInto($idList);
}
}
}
/**
* Cached result for AllChildren().
* @var DataObjectSet
*/
protected $allChildren;
/**
* Cached result for AllChildrenIncludingDeleted().
* @var DataObjectSet
*/
protected $allChildrenIncludingDeleted;
/**
* Cached result for Children().
*/
protected $children;
/**
* Get the children for this DataObject.
* @return DataObjectSet
*/
public function Children() {
if(!(isset($this->_cache_children) && $this->_cache_children)) {
$result = $this->owner->stageChildren(false);
if(isset($result)) {
$this->_cache_children = new DataObjectSet();
foreach($result as $child) {
if($child->canView()) {
$this->_cache_children->push($child);
}
}
}
}
return $this->_cache_children;
}
/**
* Return all children, including those 'not in menus'.
* @return DataObjectSet
*/
public function AllChildren() {
// Cache the allChildren data, so that future requests will return the references to the same
// object. This allows the mark..() system to work appropriately.
if(!$this->allChildren) {
$this->allChildren = $this->owner->stageChildren(true);
}
return $this->allChildren;
}
/**
* Return all children, including those that have been deleted but are still in live.
* Deleted children will be marked as "DeletedFromStage"
* Added children will be marked as "AddedToStage"
* Modified children will be marked as "ModifiedOnStage"
* Everything else has "SameOnStage" set, as an indicator that this information has been looked up.
* @return DataObjectSet
*/
public function AllChildrenIncludingDeleted($context = null) {
return $this->doAllChildrenIncludingDeleted($context);
}
/**
* @see AllChildrenIncludingDeleted
*
* @param unknown_type $context
* @return DataObjectSet
*/
public function doAllChildrenIncludingDeleted($context = null) {
// Cache the allChildren data, so that future requests will return the references to the same
// object. This allows the mark..() system to work appropriately.
if(!$this->allChildrenIncludingDeleted) {
$baseClass = ClassInfo::baseDataClass($this->owner->class);
if($baseClass) {
$stageChildren = $this->owner->stageChildren(true);
$this->allChildrenIncludingDeleted = $stageChildren;
$this->owner->extend("augmentAllChildrenIncludingDeleted", $stageChildren, $context);
// Add live site content, if required.
if($this->owner->hasExtension('Versioned')) {
// Get all the requisite data, and index it
$liveChildren = $this->owner->liveChildren(true);
if(isset($stageChildren)) {
foreach($stageChildren as $child) {
$idxStageChildren[$child->ID] = $child;
}
}
if(isset($liveChildren)) {
foreach($liveChildren as $child) {
$idxLiveChildren[$child->ID] = $child;
}
}
DataObject::disable_subclass_access();
if(isset($idxStageChildren)) {
$foundInLive = Versioned::get_by_stage( $baseClass, 'Live', "`{$baseClass}`.`ID` IN (" . implode(",", array_keys($idxStageChildren)) . ")", "" );
}
if(isset($idxLiveChildren)) {
$foundInStage = Versioned::get_by_stage( $baseClass, 'Stage', "`{$baseClass}`.`ID` IN (" . implode(",", array_keys($idxLiveChildren)) . ")", "" );
}
DataObject::enable_subclass_access();
if(isset($foundInLive)) {
foreach($foundInLive as $child) {
$idxFoundInLive[$child->ID] = $child;
}
}
if(isset($foundInStage)) {
foreach($foundInStage as $child) {
$idxFoundInStage[$child->ID] = $child;
}
}
$this->allChildrenIncludingDeleted = new DataObjectSet();
// First, go through the stage children. They will all be listed but may be different colours
if($stageChildren) {
foreach($stageChildren as $child) {
$this->allChildrenIncludingDeleted->push($child);
}
}
// Next, go through the live children. Only some of these will be listed
if($liveChildren) {
foreach($liveChildren as $child) {
// Not found on stage = deleted page. Anything else is ignored
if(!isset($idxFoundInStage[$child->ID])) {
$this->allChildrenIncludingDeleted->push($child);
}
}
}
}
} else {
user_error("Hierarchy::AllChildren() Couldn't determine base class for '{$this->owner->class}'", E_USER_ERROR);
}
}
return $this->allChildrenIncludingDeleted;
}
/**
* Return the number of children
* @return int
*/
public function numChildren() {
$baseClass = ClassInfo::baseDataClass($this->owner->class);
// We build the query in an extension-friendly way.
$query = new SQLQuery("COUNT(*)","`$baseClass`","ParentID = " . (int)$this->owner->ID);
$this->owner->extend('augmentSQL', $query);
$this->owner->extend('augmentNumChildrenCountQuery', $query);
return $query->execute()->value();
}
/**
* Return children from the stage site
* @param showAll Inlcude all of the elements, even those not shown in the menus.
* @return DataObjectSet
*/
public function stageChildren($showAll = false) {
$extraFilter = $showAll ? '' : " AND ShowInMenus = 1";
$baseClass = ClassInfo::baseDataClass($this->owner->class);
$staged = DataObject::get($baseClass, "`{$baseClass}`.`ParentID` = " . (int)$this->owner->ID . " AND `{$baseClass}`.ID != " . (int)$this->owner->ID . $extraFilter, "");
if(!$staged) $staged = new DataObjectSet();
$this->owner->extend("augmentStageChildren", $staged, $showAll);
return $staged;
}
/**
* Return children from the live site, if it exists.
* @param boolean $showAll Include all of the elements, even those not shown in the menus.
* @return DataObjectSet
*/
public function liveChildren($showAll = false) {
$extraFilter = $showAll ? '' : " AND ShowInMenus = 1";
$baseClass = ClassInfo::baseDataClass($this->owner->class);
return Versioned::get_by_stage($baseClass, "Live", "`{$baseClass}`.`ParentID` = " . (int)$this->owner->ID . " AND `{$baseClass}`.ID != " . (int)$this->owner->ID. $extraFilter, "");
}
/**
* Get the parent of this class.
* @return DataObject
*/
public function getParent($filter = '') {
if($p = $this->owner->__get("ParentID")) {
$tableClasses = ClassInfo::dataClassesFor($this->owner->class);
$baseClass = array_shift($tableClasses);
$filter .= $filter?" AND ":""."`$baseClass`.ID = $p";
return DataObject::get_one($this->owner->class, $filter);
}
}
/**
* Get the next node in the tree of the type. If there is no instance of the className descended from this node,
* then search the parents.
*
* @todo Write!
*/
public function naturalPrev( $className, $afterNode = null ) {
return null;
}
/**
* Get the next node in the tree of the type. If there is no instance of the className descended from this node,
* then search the parents.
* @param string $className Class name of the node to find.
* @param string|int $root ID/ClassName of the node to limit the search to
* @param DataObject afterNode Used for recursive calls to this function
* @return DataObject
*/
public function naturalNext( $className = null, $root = 0, $afterNode = null ) {
// If this node is not the node we are searching from, then we can possibly return this
// node as a solution
if($afterNode && $afterNode->ID != $this->owner->ID) {
if(!$className || ($className && $this->owner->class == $className)) {
return $this->owner;
}
}
$nextNode = null;
$baseClass = ClassInfo::baseDataClass($this->owner->class);
// Try searching for the next node of the given class in each of the children, but treat each
// child as the root of the search. This will stop the recursive call from searching backwards.
// If afterNode is given, then only search for the nodes after
if(!$afterNode || $afterNode->ParentID != $this->owner->ID) {
$children = $this->AllChildren();
} else {
$children = DataObject::get(ClassInfo::baseDataClass($this->owner->class), "`$baseClass`.`ParentID`={$this->owner->ID}" . ( ( $afterNode ) ? " AND `Sort` > " . sprintf( '%d', $afterNode->Sort ) : "" ), '`Sort` ASC');
}
// Try all the siblings of this node after the given node
/*if( $siblings = DataObject::get( ClassInfo::baseDataClass($this->owner->class), "`ParentID`={$this->owner->ParentID}" . ( $afterNode ) ? "`Sort` > {$afterNode->Sort}" : "" , '`Sort` ASC' ) )
$searchNodes->merge( $siblings );*/
if($children) {
foreach($children as $node) {
if($nextNode = $node->naturalNext($className, $node->ID, $this->owner)) {
break;
}
}
if($nextNode) {
return $nextNode;
}
}
// if this is not an instance of the root class or has the root id, search the parent
if(!(is_numeric($root) && $root == $this->owner->ID || $root == $this->owner->class) && ($parent = $this->owner->Parent())) {
return $parent->naturalNext( $className, $root, $this->owner );
}
return null;
}
function flushCache() {
$this->_cache_children = null;
$this->_cache_allChildrenIncludingDeleted = null;
$this->_cache_allChildren = null;
}
}
?>