Skip to content

Instantly share code, notes, and snippets.

@nyeholt
Last active December 9, 2015 02:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nyeholt/918714a48bb17c8b85c1 to your computer and use it in GitHub Desktop.
Save nyeholt/918714a48bb17c8b85c1 to your computer and use it in GitHub Desktop.
Fake Plastic Trees
<?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