Last active
December 9, 2015 02:56
-
-
Save nyeholt/918714a48bb17c8b85c1 to your computer and use it in GitHub Desktop.
Fake Plastic Trees
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Capture / cache some commonly used data elements for each page | |
* | |
* @author marcus | |
*/ | |
class SiteDataService { | |
protected $items = array(); | |
protected $mapped = array(); | |
/** | |
* | |
* @var string | |
*/ | |
public $itemClass = 'MenuItem'; | |
/** | |
* Additional fields to be queried from the SiteTree/Page tables | |
* | |
* @var array | |
*/ | |
public $additionalFields = array(); | |
/** | |
* Needed to create the menu item objects | |
* | |
* @var Injector | |
*/ | |
public $injector; | |
public function __construct() { | |
} | |
public function getItem($id) { | |
$this->getItems(); | |
return isset($this->items[$id]) ? $this->items[$id] : null; | |
} | |
protected function getItems() { | |
if (!$this->items) { | |
$this->generateMenuItems(); | |
} | |
return $this->items; | |
} | |
public function generateMenuItems() { | |
$all = array(); | |
$allids = array( | |
); | |
if (class_exists('Multisites')) { | |
$site = Multisites::inst()->getCurrentSite(); | |
$all[] = array( | |
'ID' => $site->ID, | |
'ClassName' => 'Site', | |
'Title' => $site->Title, | |
'ParentID' => 0, | |
'MenuTitle' => $site->Title, | |
'URLSegment' => '', | |
'CanViewType' => $site->CanViewType, | |
); | |
$allids[$site->ID] = true; | |
} | |
$public = $this->getPublicNodes(); | |
foreach ($public as $row) { | |
$allids[$row['ID']] = true; | |
$all[] = $row; | |
} | |
// and private nodes | |
$private = $this->getPrivateNodes(); | |
foreach ($private as $row) { | |
$allids[$row['ID']] = true; | |
$all[] = $row; | |
} | |
$others = $this->getAdditionalNodes(); | |
foreach ($others as $row) { | |
$allids[$row['ID']] = true; | |
$all[] = $row; | |
} | |
$deferred = array(); | |
$final = array(); | |
$hierarchy = array(); | |
$counter = 0; | |
$this->loopstuff($final, $all, $allids, 0); | |
$ordered = ArrayList::create(); | |
// start at 0 | |
if (isset($final[0]['kids'])) { | |
foreach ($final[0]['kids'] as $id) { | |
$node = $final[$id]; | |
$this->buildLinks($node, null, $ordered, $final); | |
} | |
} | |
} | |
protected function queryFields() { | |
$fields = array( | |
'"Page"."ID" AS ID', 'ClassName', 'Title', 'MenuTitle', 'URLSegment', 'ParentID', 'CanViewType', 'Sort', 'ShowInMenus', | |
); | |
foreach ($this->additionalFields as $field) { | |
$fields[] = $field; | |
} | |
return $fields; | |
} | |
protected function getPublicNodes() { | |
$fields = $this->queryFields(); | |
$query = new SQLQuery($fields, 'SiteTree'); | |
$query = $query->addInnerJoin('Page', '"SiteTree"."ID" = "Page"."ID"'); | |
$query = $query->setOrderBy('ParentID', 'ASC'); | |
// if the user is logged in, we only exclude nodes that have a specific permission set on them | |
if (Member::currentUserID()) { | |
$query->addWhere('"CanViewType" NOT IN (\'OnlyTheseUsers\')'); | |
} else { | |
$query->addWhere('"CanViewType" NOT IN (\'LoggedInUsers\', \'OnlyTheseUsers\')'); | |
} | |
$this->adjustPublicNodeQuery($query); | |
$this->adjustForVersioned($query); | |
$results = $query->execute(); | |
return $results; | |
} | |
protected function adjustPublicNodeQuery(SQLQuery $query) { | |
} | |
/** | |
* Get private nodes, assuming SilverStripe's default perm structure | |
* @return SS_Query | |
*/ | |
protected function getPrivateNodes() { | |
if (!Member::currentUserID()) { | |
return array(); | |
} | |
$groups = Member::currentUser()->Groups()->column(); | |
if (!count($groups)) { | |
return $groups; | |
} | |
$fields = $this->queryFields(); | |
$query = new SQLQuery($fields, 'SiteTree'); | |
$query = $query->addInnerJoin('Page', '"SiteTree"."ID" = "Page"."ID"'); | |
$query = $query->setOrderBy('ParentID', 'ASC'); | |
$query->addWhere('"CanViewType" IN (\'OnlyTheseUsers\')'); | |
if (Permission::check('ADMIN')) { | |
// don't need to restrict the canView by anything | |
} else { | |
$query->addInnerJoin('SiteTree_ViewerGroups', '"SiteTree_ViewerGroups"."SiteTreeID" = "SiteTree"."ID"'); | |
$query->addWhere('"SiteTree_ViewerGroups"."GroupID" IN (' . implode(',', $groups).')'); | |
} | |
$this->adjustPrivateNodeQuery($query); | |
$this->adjustForVersioned($query); | |
$sql = $query->sql(); | |
$results = $query->execute(); | |
return $results; | |
} | |
protected function adjustForVersioned(SQLQuery $query) { | |
$ownerClass = 'Page'; | |
$stage = Versioned::current_stage(); | |
if($stage && ($stage != 'Stage')) { | |
foreach($query->getFrom() as $table => $dummy) { | |
// Only rewrite table names that are actually part of the subclass tree | |
// This helps prevent rewriting of other tables that get joined in, in | |
// particular, many_many tables | |
if(class_exists($table) && ($table == $ownerClass | |
|| is_subclass_of($table, $ownerClass) | |
|| is_subclass_of($ownerClass, $table))) { | |
$query->renameTable($table, $table . '_' . $stage); | |
} | |
} | |
} | |
} | |
protected function adjustPrivateNodeQuery(SQLQuery $query) { | |
} | |
protected function getAdditionalNodes() { | |
return array(); | |
} | |
protected function buildLinks($node, $parent, $out, $nodemap) { | |
$kids = isset($node['kids']) ? $node['kids'] : array(); | |
$node = $this->createMenuNode($node); | |
$out->push($node); | |
$node->Link = ltrim($parent ? $parent->Link . '/' . $node->URLSegment : $node->URLSegment, '/'); | |
foreach ($kids as $id) { | |
$n = $nodemap[$id]; | |
$this->buildLinks($n, $node, $out, $nodemap); | |
} | |
} | |
/** | |
* Creates a menu item from an array of data | |
* | |
* @param array $data | |
* @returns MenuItem | |
*/ | |
public function createMenuNode($data) { | |
$cls = $this->itemClass; | |
$node = $cls::create($data, $this); | |
$this->items[$node->ID] = $node; | |
return $node; | |
} | |
protected function loopstuff (&$final, $remaining, $ids, $lastcount) { | |
$deferred = array(); | |
foreach ($remaining as $row) { | |
// orphan | |
if ($row['ParentID'] && !isset($ids[$row['ParentID']])) { | |
continue; | |
} | |
if (!isset($final[$row['ID']])) { | |
$final[$row['ID']] = $row; | |
} | |
if ($row['ParentID'] && !isset($final[$row['ParentID']])) { | |
$deferred[$row['ID']] = $row; | |
} else { | |
// add to the hierarchy of things | |
$existing = isset($final[$row['ParentID']]['kids']) ? $final[$row['ParentID']]['kids'] : array(); | |
$existing[] = $row['ID']; | |
$final[$row['ParentID']]['kids'] = $existing; | |
} | |
} | |
if (count($deferred) == $lastcount) { | |
return; | |
} | |
$lastcount = count($deferred); | |
if (count($deferred)) { | |
$this->loopstuff($final, $deferred, $ids, $lastcount); | |
} | |
} | |
} | |
/** | |
* Subclass of ArrayData that has some site specific functionality | |
* | |
* @author marcus | |
*/ | |
class MenuItem extends ArrayData { | |
/** | |
* @var SiteDataService | |
*/ | |
protected $siteData; | |
public function __construct($value, SiteDataService $siteData) { | |
if (!isset($value['MenuTitle']) || strlen($value['MenuTitle']) == 0 && strlen($value['MenuTitle'])) { | |
if (isset($value['Title'])) { | |
$value['MenuTitle'] = $value['Title']; | |
} | |
} | |
parent::__construct($value); | |
$this->siteData = $siteData; | |
} | |
public function Children() { | |
$kids = ArrayList::create(); | |
if (isset($this->array['kids'])) { | |
foreach ($this->array['kids'] as $id) { | |
$kid = $this->siteData->getItem($id); | |
if ($kid && $kid->ShowInMenus) { | |
$kids->push($kid); | |
} | |
} | |
} | |
$kids = $kids->sort('Sort ASC'); | |
return $kids; | |
} | |
public function getAncestors() { | |
$ancestors = new ArrayList(); | |
$object = $this; | |
while($object = $object->getParent()) { | |
$ancestors->push($object); | |
} | |
return $ancestors; | |
} | |
public function getParent() { | |
return $this->ParentID ? $this->siteData->getItem($this->ParentID) : null; | |
} | |
/** | |
* Returns true if this is the currently active page being used to handle this request. | |
* | |
* @return bool | |
*/ | |
public function isCurrent() { | |
return $this->ID ? $this->ID == Director::get_current_page()->ID : false; | |
} | |
/** | |
* Check if this page is in the currently active section (e.g. it is either current or one of its children is | |
* currently being viewed). | |
* | |
* @return bool | |
*/ | |
public function isSection() { | |
if ($this->isCurrent()) { | |
return true; | |
} | |
$ancestors = $this->getAncestors(); | |
if (Director::get_current_page() instanceof Page) { | |
$node = Director::get_current_page()->asMenuItem(); | |
if ($node) { | |
$ancestors = $node->getAncestors(); | |
return $ancestors && in_array($this->ID, $node->getAncestors()->column()); | |
} | |
} | |
return false; | |
} | |
/** | |
* Check if the parent of this page has been removed (or made otherwise unavailable), and is still referenced by | |
* this child. Any such orphaned page may still require access via the CMS, but should not be shown as accessible | |
* to external users. | |
* | |
* @return bool | |
*/ | |
public function isOrphaned() { | |
// Always false for root pages | |
if(empty($this->ParentID)) return false; | |
// Parent must exist and not be an orphan itself | |
$parent = $this->getParent(); | |
return !$parent || !$parent->ID || $parent->isOrphaned(); | |
} | |
/** | |
* Return "link" or "current" depending on if this is the {@link SiteTree::isCurrent()} current page. | |
* | |
* @return string | |
*/ | |
public function LinkOrCurrent() { | |
return $this->isCurrent() ? 'current' : 'link'; | |
} | |
/** | |
* Return "link" or "section" depending on if this is the {@link SiteTree::isSeciton()} current section. | |
* | |
* @return string | |
*/ | |
public function LinkOrSection() { | |
return $this->isSection() ? 'section' : 'link'; | |
} | |
/** | |
* Return "link", "current" or "section" depending on if this page is the current page, or not on the current page | |
* but in the current section. | |
* | |
* @return string | |
*/ | |
public function LinkingMode() { | |
if($this->isCurrent()) { | |
return 'current'; | |
} elseif($this->isSection()) { | |
return 'section'; | |
} else { | |
return 'link'; | |
} | |
} | |
/** | |
* Check if this page is in the given current section. | |
* | |
* @param string $sectionName Name of the section to check | |
* @return bool True if we are in the given section | |
*/ | |
public function InSection($sectionName) { | |
$page = Director::get_current_page(); | |
while($page) { | |
if($sectionName == $page->URLSegment) | |
return true; | |
$page = $page->Parent; | |
} | |
return false; | |
} | |
} | |
// then in Page | |
class Page extends SiteTree { | |
/** | |
* Return a breadcrumb trail to this page. Excludes "hidden" pages (with ShowInMenus=0) by default. | |
* | |
* @param int $maxDepth The maximum depth to traverse. | |
* @param bool $unlinked Do not make page names links | |
* @param bool|string $stopAtPageType ClassName of a page to stop the upwards traversal. | |
* @param bool $showHidden Include pages marked with the attribute ShowInMenus = 0 | |
* @return HTMLText The breadcrumb trail. | |
*/ | |
public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false) { | |
$page = $this->asMenuItem(); | |
$pages = array(); | |
while( | |
$page | |
&& (!$maxDepth || count($pages) < $maxDepth) | |
&& (!$stopAtPageType || $page->ClassName != $stopAtPageType) | |
) { | |
if($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) { | |
$pages[] = $page; | |
} | |
$page = $page->getParent(); | |
} | |
$template = new SSViewer('BreadcrumbsTemplate'); | |
return $template->process($this->customise(new ArrayData(array( | |
'Pages' => new ArrayList(array_reverse($pages)) | |
)))); | |
} | |
/** | |
* Returns the page in the current page stack of the given level. Level(1) will return the main menu item that | |
* we're currently inside, etc. | |
* | |
* @param int $level | |
* @return SiteTree | |
*/ | |
public function Level($level) { | |
$menuNode = $this->asMenuItem(); | |
$stack = array(); | |
if ($menuNode) { | |
$parent = $menuNode; | |
$stack[] = $parent; | |
while($parent = $parent->getParent()) { | |
array_unshift($stack, $parent); | |
} | |
} | |
return isset($stack[$level-1]) ? $stack[$level-1] : null; | |
} | |
public function asMenuItem() { | |
$item = singleton('SiteDataService')->getItem($this->ID); | |
if (!$item) { | |
// need to create a new item | |
$item = singleton('SiteDataService')->createMenuNode($this->toMap()); | |
} | |
return $item; | |
} | |
} | |
// and page controller | |
class Page_Controller extends ContentController { | |
/** | |
* Returns a fixed navigation menu of the given level. | |
* @param int $level Menu level to return. | |
* @return ArrayList | |
*/ | |
public function getMenu($level = 1) { | |
$result = array(); | |
$page = $this->data(); | |
$siteData = $page->siteData ? $page->siteData : singleton('SiteDataService'); | |
if($level == 1) { | |
$root = $page->siteData->getItem(Multisites::inst()->getCurrentSiteId()); | |
if ($root) { | |
$result = $root->Children(); | |
} | |
} else { | |
$parent = $page->asMenuItem(); | |
$stack = array($parent); | |
if($parent) { | |
while($parent = $parent->getParent()) { | |
array_unshift($stack, $parent); | |
} | |
} | |
if(isset($stack[$level-2])) { | |
$result = $stack[$level-2]->Children(); | |
} | |
} | |
return $result; | |
} | |
public function getSiteMenu($level = 1) { | |
$site = Multisites::inst()->getCurrentSite(); | |
$page = $this->data(); | |
$siteData = $page->siteData ? $page->siteData : singleton('SiteDataService'); | |
$result = new ArrayList(); | |
if($level == 1) { | |
$root = $siteData->getItem($site->ID); | |
$pages = $root->Children(); | |
} else { | |
$parent = $page->asMenuItem(); | |
$stack = array($parent); | |
while(($parent = $parent->getParent()) && $parent->ID > 0 && $parent->ClassName != 'Site') { | |
array_unshift($stack, $parent); | |
} | |
if(!isset($stack[$level - 2])) { | |
return; | |
} | |
$pages = $stack[$level - 2]->Children(); | |
} | |
return $pages; | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment