Merge pull request #2048 from open-sausages/pulls/4.0/sitetree-can-create-cache

ENHANCEMENT: Cache canCreate in CMSMain
This commit is contained in:
Damian Mooyman 2017-12-13 16:55:35 +13:00 committed by GitHub
commit 6b0863d362
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 367 additions and 100 deletions

View File

@ -8,3 +8,8 @@ SilverStripe\Core\Injector\Injector:
factory: SilverStripe\Core\Cache\CacheFactory factory: SilverStripe\Core\Cache\CacheFactory
constructor: constructor:
namespace: "CMSMain_SiteTreeHints" namespace: "CMSMain_SiteTreeHints"
Psr\SimpleCache\CacheInterface.SiteTree_CreatableChildren:
factory: SilverStripe\Core\Cache\CacheFactory
constructor:
namespace: "SiteTree_CreatableChildren"

View File

@ -16,3 +16,5 @@ SilverStripe\Core\Injector\Injector:
properties: properties:
Services: Services:
- '%$SilverStripe\Security\PermissionChecker.sitetree' - '%$SilverStripe\Security\PermissionChecker.sitetree'
- '%$SilverStripe\CMS\Controllers\CMSMain'
- '%$SilverStripe\CMS\Model\SiteTree'

View File

@ -25,8 +25,6 @@ use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Core\Environment; use SilverStripe\Core\Environment;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Manifest\ModuleResource;
use SilverStripe\Core\Manifest\ModuleResourceLoader;
use SilverStripe\Forms\DateField; use SilverStripe\Forms\DateField;
use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\FieldGroup; use SilverStripe\Forms\FieldGroup;
@ -53,7 +51,6 @@ use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\ORM\HiddenClass; use SilverStripe\ORM\HiddenClass;
use SilverStripe\ORM\Hierarchy;
use SilverStripe\ORM\Hierarchy\MarkedSet; use SilverStripe\ORM\Hierarchy\MarkedSet;
use SilverStripe\ORM\SS_List; use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\ValidationResult; use SilverStripe\ORM\ValidationResult;
@ -69,6 +66,8 @@ use SilverStripe\Versioned\ChangeSetItem;
use SilverStripe\Versioned\Versioned; use SilverStripe\Versioned\Versioned;
use SilverStripe\View\ArrayData; use SilverStripe\View\ArrayData;
use SilverStripe\View\Requirements; use SilverStripe\View\Requirements;
use SilverStripe\Core\Flushable;
use SilverStripe\Core\Cache\MemberCacheFlusher;
use Translatable; use Translatable;
/** /**
@ -81,7 +80,7 @@ use Translatable;
* *
* @mixin LeftAndMainPageIconsExtension * @mixin LeftAndMainPageIconsExtension
*/ */
class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionProvider class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionProvider, Flushable, MemberCacheFlusher
{ {
/** /**
* Unique ID for page icons CSS block * Unique ID for page icons CSS block
@ -162,6 +161,15 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
'SiteTreeAsUL' => 'HTMLFragment', 'SiteTreeAsUL' => 'HTMLFragment',
); );
private static $dependencies = [
'HintsCache' => '%$' . CacheInterface::class . '.CMSMain_SiteTreeHints',
];
/**
* @var CacheInterface
*/
protected $hintsCache;
protected function init() protected function init()
{ {
// set reading lang // set reading lang
@ -381,6 +389,33 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return 'edit'; return 'edit';
} }
/**
* @param CacheInterface $cache
* @return $this
*/
public function setHintsCache(CacheInterface $cache)
{
$this->hintsCache = $cache;
return $this;
}
/**
* @return CacheInterface $cache
*/
public function getHintsCache()
{
return $this->hintsCache;
}
/**
* Clears all dependent cache backends
*/
public function clearCache()
{
$this->getHintsCache()->clear();
}
public function LinkWithSearch($link) public function LinkWithSearch($link)
{ {
// Whitelist to avoid side effects // Whitelist to avoid side effects
@ -977,26 +1012,26 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
public function SiteTreeHints() public function SiteTreeHints()
{ {
$classes = SiteTree::page_type_classes(); $classes = SiteTree::page_type_classes();
$memberID = Security::getCurrentUser() ? Security::getCurrentUser()->ID : 0;
$cacheCanCreate = array(); $cache = $this->getHintsCache();
foreach ($classes as $class) { $cacheKey = $this->generateHintsCacheKey($memberID);
$cacheCanCreate[$class] = singleton($class)->canCreate();
}
// Generate basic cache key. Too complex to encompass all variations
$cache = Injector::inst()->get(CacheInterface::class . '.CMSMain_SiteTreeHints');
$cacheKey = md5(implode('_', array(Security::getCurrentUser()->ID, implode(',', $cacheCanCreate), implode(',', $classes))));
if ($this->getRequest()->getVar('flush')) {
$cache->clear();
}
$json = $cache->get($cacheKey); $json = $cache->get($cacheKey);
if (!$json) {
$def['Root'] = array(); if ($json) {
$def['Root']['disallowedChildren'] = array(); return $json;
}
$canCreate = [];
foreach ($classes as $class) {
$canCreate[$class] = singleton($class)->canCreate();
}
$def['Root'] = [];
$def['Root']['disallowedChildren'] = [];
// Contains all possible classes to support UI controls listing them all, // Contains all possible classes to support UI controls listing them all,
// such as the "add page here" context menu. // such as the "add page here" context menu.
$def['All'] = array(); $def['All'] = [];
// Identify disallows and set globals // Identify disallows and set globals
foreach ($classes as $class) { foreach ($classes as $class) {
@ -1006,21 +1041,21 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
} }
// Name item // Name item
$def['All'][$class] = array( $def['All'][$class] = [
'title' => $obj->i18n_singular_name() 'title' => $obj->i18n_singular_name()
); ];
// Check if can be created at the root // Check if can be created at the root
$needsPerm = $obj->config()->get('need_permission'); $needsPerm = $obj->config()->get('need_permission');
if (!$obj->config()->get('can_be_root') if (!$obj->config()->get('can_be_root')
|| (!array_key_exists($class, $cacheCanCreate) || !$cacheCanCreate[$class]) || (!array_key_exists($class, $canCreate) || !$canCreate[$class])
|| ($needsPerm && !$this->can($needsPerm)) || ($needsPerm && !$this->can($needsPerm))
) { ) {
$def['Root']['disallowedChildren'][] = $class; $def['Root']['disallowedChildren'][] = $class;
} }
// Hint data specific to the class // Hint data specific to the class
$def[$class] = array(); $def[$class] = [];
$defaultChild = $obj->defaultChild(); $defaultChild = $obj->defaultChild();
if ($defaultChild !== 'Page' && $defaultChild !== null) { if ($defaultChild !== 'Page' && $defaultChild !== null) {
@ -1037,7 +1072,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$json = Convert::raw2json($def); $json = Convert::raw2json($def);
$cache->set($cacheKey, $json); $cache->set($cacheKey, $json);
}
return $json; return $json;
} }
@ -1477,7 +1512,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$gridField = new GridField('Page', 'Pages', $list, $gridFieldConfig); $gridField = new GridField('Page', 'Pages', $list, $gridFieldConfig);
$gridField->setAttribute('cms-loading-ignore-url-params', true); $gridField->setAttribute('cms-loading-ignore-url-params', true);
/** @var GridFieldDataColumns $columns */ /** @var GridFieldDataColumns $columns */
$columns = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns'); $columns = $gridField->getConfig()->getComponentByType(GridFieldDataColumns::class);
// Don't allow navigating into children nodes on filtered lists // Don't allow navigating into children nodes on filtered lists
$fields = array( $fields = array(
@ -1486,7 +1521,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
'LastEdited' => _t('SilverStripe\\CMS\\Model\\SiteTree.LASTUPDATED', 'Last Updated'), 'LastEdited' => _t('SilverStripe\\CMS\\Model\\SiteTree.LASTUPDATED', 'Last Updated'),
); );
/** @var GridFieldSortableHeader $sortableHeader */ /** @var GridFieldSortableHeader $sortableHeader */
$sortableHeader = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldSortableHeader'); $sortableHeader = $gridField->getConfig()->getComponentByType(GridFieldSortableHeader::class);
$sortableHeader->setFieldSorting(array('getTreeTitle' => 'Title')); $sortableHeader->setFieldSorting(array('getTreeTitle' => 'Title'));
$gridField->getState()->ParentID = $parentID; $gridField->getState()->ParentID = $parentID;
@ -1630,13 +1665,13 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
if ($doPublish) { if ($doPublish) {
$record->publishRecursive(); $record->publishRecursive();
$message = _t( $message = _t(
'SilverStripe\\CMS\\Controllers\\CMSMain.PUBLISHED', __CLASS__ . '.PUBLISHED',
"Published '{title}' successfully.", "Published '{title}' successfully.",
['title' => $record->Title] ['title' => $record->Title]
); );
} else { } else {
$message = _t( $message = _t(
'SilverStripe\\CMS\\Controllers\\CMSMain.SAVED', __CLASS__ . '.SAVED',
"Saved '{title}' successfully.", "Saved '{title}' successfully.",
['title' => $record->Title] ['title' => $record->Title]
); );
@ -1670,7 +1705,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
/** @var SiteTree $newItem */ /** @var SiteTree $newItem */
$newItem = Injector::inst()->create($className); $newItem = Injector::inst()->create($className);
$newItem->Title = _t( $newItem->Title = _t(
'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE', __CLASS__ . '.NEWPAGE',
"New {pagetype}", "New {pagetype}",
'followed by a page type title', 'followed by a page type title',
array('pagetype' => singleton($className)->i18n_singular_name()) array('pagetype' => singleton($className)->i18n_singular_name())
@ -1756,7 +1791,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$this->getResponse()->addHeader( $this->getResponse()->addHeader(
'X-Status', 'X-Status',
rawurlencode(_t( rawurlencode(_t(
'SilverStripe\\CMS\\Controllers\\CMSMain.RESTORED', __CLASS__ . '.RESTORED',
"Restored '{title}' successfully", "Restored '{title}' successfully",
'Param {title} is a title', 'Param {title} is a title',
array('title' => $record->Title) array('title' => $record->Title)
@ -1863,7 +1898,11 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$this->getResponse()->addHeader( $this->getResponse()->addHeader(
'X-Status', 'X-Status',
rawurlencode(_t('SilverStripe\\CMS\\Controllers\\CMSMain.REMOVEDPAGE', "Removed '{title}' from the published site", array('title' => $record->Title))) rawurlencode(_t(
__CLASS__ . '.REMOVEDPAGE',
"Removed '{title}' from the published site",
['title' => $record->Title]
))
); );
return $this->getResponseNegotiator()->respond($this->getRequest()); return $this->getResponseNegotiator()->respond($this->getRequest());
@ -2006,7 +2045,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
break; break;
} }
} }
$response .= _t('SilverStripe\\CMS\\Controllers\\CMSMain.PUBPAGES', "Done: Published {count} pages", array('count' => $count)); $response .= _t(__CLASS__ . '.PUBPAGES', "Done: Published {count} pages", array('count' => $count));
} else { } else {
$token = SecurityToken::inst(); $token = SecurityToken::inst();
$fields = new FieldList(); $fields = new FieldList();
@ -2014,16 +2053,16 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$tokenField = $fields->first(); $tokenField = $fields->first();
$tokenHtml = ($tokenField) ? $tokenField->FieldHolder() : ''; $tokenHtml = ($tokenField) ? $tokenField->FieldHolder() : '';
$publishAllDescription = _t( $publishAllDescription = _t(
'SilverStripe\\CMS\\Controllers\\CMSMain.PUBALLFUN2', __CLASS__ . '.PUBALLFUN2',
'Pressing this button will do the equivalent of going to every page and pressing "publish". ' 'Pressing this button will do the equivalent of going to every page and pressing "publish". '
. 'It\'s intended to be used after there have been massive edits of the content, such as when ' . 'It\'s intended to be used after there have been massive edits of the content, such as when '
. 'the site was first built.' . 'the site was first built.'
); );
$response .= '<h1>' . _t('SilverStripe\\CMS\\Controllers\\CMSMain.PUBALLFUN', '"Publish All" functionality') . '</h1> $response .= '<h1>' . _t(__CLASS__ . '.PUBALLFUN', '"Publish All" functionality') . '</h1>
<p>' . $publishAllDescription . '</p> <p>' . $publishAllDescription . '</p>
<form method="post" action="publishall"> <form method="post" action="publishall">
<input type="submit" name="confirm" value="' <input type="submit" name="confirm" value="'
. _t('SilverStripe\\CMS\\Controllers\\CMSMain.PUBALLCONFIRM', "Please publish every page in the site, copying content stage to live", 'Confirmation button') .'" />' . _t(__CLASS__ . '.PUBALLCONFIRM', "Please publish every page in the site, copying content stage to live", 'Confirmation button') .'" />'
. $tokenHtml . . $tokenHtml .
'</form>'; '</form>';
} }
@ -2056,7 +2095,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$this->getResponse()->addHeader( $this->getResponse()->addHeader(
'X-Status', 'X-Status',
rawurlencode(_t( rawurlencode(_t(
'SilverStripe\\CMS\\Controllers\\CMSMain.RESTORED', __CLASS__ . '.RESTORED',
"Restored '{title}' successfully", "Restored '{title}' successfully",
array('title' => $restoredPage->Title) array('title' => $restoredPage->Title)
)) ))
@ -2093,7 +2132,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$this->getResponse()->addHeader( $this->getResponse()->addHeader(
'X-Status', 'X-Status',
rawurlencode(_t( rawurlencode(_t(
'SilverStripe\\CMS\\Controllers\\CMSMain.DUPLICATED', __CLASS__ . '.DUPLICATED',
"Duplicated '{title}' successfully", "Duplicated '{title}' successfully",
array('title' => $newPage->Title) array('title' => $newPage->Title)
)) ))
@ -2131,7 +2170,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$this->getResponse()->addHeader( $this->getResponse()->addHeader(
'X-Status', 'X-Status',
rawurlencode(_t( rawurlencode(_t(
'SilverStripe\\CMS\\Controllers\\CMSMain.DUPLICATEDWITHCHILDREN', __CLASS__ . '.DUPLICATEDWITHCHILDREN',
"Duplicated '{title}' and children successfully", "Duplicated '{title}' and children successfully",
array('title' => $newPage->Title) array('title' => $newPage->Title)
)) ))
@ -2152,10 +2191,10 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$title = CMSPagesController::menu_title(); $title = CMSPagesController::menu_title();
return array( return array(
"CMS_ACCESS_CMSMain" => array( "CMS_ACCESS_CMSMain" => array(
'name' => _t('SilverStripe\\CMS\\Controllers\\CMSMain.ACCESS', "Access to '{title}' section", array('title' => $title)), 'name' => _t(__CLASS__ . '.ACCESS', "Access to '{title}' section", array('title' => $title)),
'category' => _t('SilverStripe\\Security\\Permission.CMS_ACCESS_CATEGORY', 'CMS Access'), 'category' => _t('SilverStripe\\Security\\Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
'help' => _t( 'help' => _t(
'SilverStripe\\CMS\\Controllers\\CMSMain.ACCESS_HELP', __CLASS__ . '.ACCESS_HELP',
'Allow viewing of the section containing page tree and content. View and edit permissions can be handled through page specific dropdowns, as well as the separate "Content permissions".' 'Allow viewing of the section containing page tree and content. View and edit permissions can be handled through page specific dropdowns, as well as the separate "Content permissions".'
), ),
'sort' => -99 // below "CMS_ACCESS_LeftAndMain", but above everything else 'sort' => -99 // below "CMS_ACCESS_LeftAndMain", but above everything else
@ -2174,4 +2213,43 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$this->extend('updateCMSTreeTitle', $rootTitle); $this->extend('updateCMSTreeTitle', $rootTitle);
return $rootTitle; return $rootTitle;
} }
/**
* Cache key for SiteTreeHints() method
*
* @param $memberID
* @return string
*/
protected function generateHintsCacheKey($memberID)
{
return md5($memberID . '_' . __CLASS__);
}
/**
* Clear the cache on ?flush
*/
public static function flush()
{
CMSMain::singleton()->clearCache();
}
/**
* Flush the hints cache for a specific member
*
* @param array $memberIDs
*/
public function flushMemberCache($memberIDs = null)
{
$cache = $this->getHintsCache();
if (!$memberIDs) {
$cache->clear();
return;
}
foreach ($memberIDs as $memberID) {
$key = $this->generateHintsCacheKey($memberID);
$cache->delete($key);
}
}
} }

View File

@ -3,6 +3,7 @@
namespace SilverStripe\CMS\Model; namespace SilverStripe\CMS\Model;
use Page; use Page;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\CampaignAdmin\AddToCampaignHandler_FormAction; use SilverStripe\CampaignAdmin\AddToCampaignHandler_FormAction;
use SilverStripe\CMS\Controllers\CMSPageEditController; use SilverStripe\CMS\Controllers\CMSPageEditController;
use SilverStripe\CMS\Controllers\ContentController; use SilverStripe\CMS\Controllers\ContentController;
@ -16,6 +17,7 @@ use SilverStripe\Control\RequestHandler;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Core\Flushable;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Manifest\ModuleResource; use SilverStripe\Core\Manifest\ModuleResource;
use SilverStripe\Core\Manifest\ModuleResourceLoader; use SilverStripe\Core\Manifest\ModuleResourceLoader;
@ -30,7 +32,6 @@ use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\GridField\GridField; use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldDataColumns; use SilverStripe\Forms\GridField\GridFieldDataColumns;
use SilverStripe\Forms\HTMLEditor\HTMLEditorField; use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
use SilverStripe\Forms\ListboxField;
use SilverStripe\Forms\LiteralField; use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\OptionsetField; use SilverStripe\Forms\OptionsetField;
use SilverStripe\Forms\Tab; use SilverStripe\Forms\Tab;
@ -66,6 +67,7 @@ use SilverStripe\View\HTML;
use SilverStripe\View\Parsers\ShortcodeParser; use SilverStripe\View\Parsers\ShortcodeParser;
use SilverStripe\View\Parsers\URLSegmentFilter; use SilverStripe\View\Parsers\URLSegmentFilter;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
use SilverStripe\Core\Cache\MemberCacheFlusher;
use Subsite; use Subsite;
/** /**
@ -100,7 +102,7 @@ use Subsite;
* @mixin SiteTreeLinkTracking * @mixin SiteTreeLinkTracking
* @mixin InheritedPermissionsExtension * @mixin InheritedPermissionsExtension
*/ */
class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvider, CMSPreviewable, Resettable class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvider, CMSPreviewable, Resettable, Flushable, MemberCacheFlusher
{ {
/** /**
@ -343,6 +345,18 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
*/ */
private static $base_description = 'Generic content page'; private static $base_description = 'Generic content page';
/**
* @var array
*/
private static $dependencies = [
'creatableChildrenCache' => '%$' . CacheInterface::class . '.SiteTree_CreatableChildren'
];
/**
* @var CacheInterface
*/
protected $creatableChildrenCache;
/** /**
* Fetches the {@link SiteTree} object that maps to a link. * Fetches the {@link SiteTree} object that maps to a link.
* *
@ -904,6 +918,25 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
return null; return null;
} }
/**
* @param CacheInterface $cache
* @return $this
*/
public function setCreatableChildrenCache(CacheInterface $cache)
{
$this->creatableChildrenCache = $cache;
return $this;
}
/**
* @return CacheInterface $cache
*/
public function getCreatableChildrenCache()
{
return $this->creatableChildrenCache;
}
/** /**
* Return a string of the form "parent - page" or "grandparent - parent - page" using page titles * Return a string of the form "parent - page" or "grandparent - parent - page" using page titles
* *
@ -1508,6 +1541,26 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
$this->_cache_statusFlags = null; $this->_cache_statusFlags = null;
} }
/**
* Flushes the member specific cache for creatable children
*
* @param array $memberIDs
*/
public function flushMemberCache($memberIDs = null)
{
$cache = SiteTree::singleton()->getCreatableChildrenCache();
if (!$memberIDs) {
$cache->clear();
return;
}
foreach ($memberIDs as $memberID) {
$key = $this->generateChildrenCacheKey($memberID);
$cache->delete($key);
}
}
public function validate() public function validate()
{ {
$result = parent::validate(); $result = parent::validate();
@ -2528,6 +2581,32 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
return $allowedChildren; return $allowedChildren;
} }
/**
* Gets a list of the page types that can be created under this specific page
*
* @return array
*/
public function creatableChildren()
{
// Build the list of candidate children
$cache = SiteTree::singleton()->getCreatableChildrenCache();
$cacheKey = $this->generateChildrenCacheKey(Security::getCurrentUser() ? Security::getCurrentUser()->ID : 0);
$children = $cache->get($cacheKey, []);
if (!$children || !isset($children[$this->ID])) {
$children[$this->ID] = [];
$candidates = static::page_type_classes();
foreach ($candidates as $childClass) {
$child = singleton($childClass);
if ($child->canCreate(null, ['Parent' => $this])) {
$children[$this->ID][$childClass] = $child->i18n_singular_name();
}
}
$cache->set($cacheKey, $children);
}
return $children[$this->ID];
}
/** /**
* Returns the class name of the default class for children of this page. * Returns the class name of the default class for children of this page.
* *
@ -2644,18 +2723,7 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
*/ */
public function getTreeTitle() public function getTreeTitle()
{ {
// Build the list of candidate children $children = $this->creatableChildren();
$children = array();
$candidates = static::page_type_classes();
foreach ($this->allowedChildren() as $childClass) {
if (!in_array($childClass, $candidates)) {
continue;
}
$child = singleton($childClass);
if ($child->canCreate(null, array('Parent' => $this))) {
$children[$childClass] = $child->i18n_singular_name();
}
}
$flags = $this->getStatusFlags(); $flags = $this->getStatusFlags();
$treeTitle = sprintf( $treeTitle = sprintf(
"<span class=\"jstree-pageicon page-icon class-%s\"></span><span class=\"item\" data-allowedchildren=\"%s\">%s</span>", "<span class=\"jstree-pageicon page-icon class-%s\"></span><span class=\"item\" data-allowedchildren=\"%s\">%s</span>",
@ -2956,6 +3024,15 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
} }
} }
/**
* Clear the creatableChildren cache on flush
*/
public static function flush()
{
Injector::inst()->get(CacheInterface::class . '.SiteTree_CreatableChildren')
->clear();
}
/** /**
* Update dependant pages * Update dependant pages
*/ */
@ -2973,4 +3050,15 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
} }
} }
} }
/**
* Cache key for creatableChildren() method
*
* @param int $memberID
* @return string
*/
protected function generateChildrenCacheKey($memberID)
{
return md5($memberID . '_' . __CLASS__);
}
} }

View File

@ -147,17 +147,6 @@ class CMSMainTest extends FunctionalTest
$this->assertEquals(1, $dsCount, "Published page has no duplicate version records: it has " . $dsCount . " for version " . $latestID); $this->assertEquals(1, $dsCount, "Published page has no duplicate version records: it has " . $dsCount . " for version " . $latestID);
$this->session()->clear('loggedInAs'); $this->session()->clear('loggedInAs');
//$this->assertRegexp('/Done: Published 4 pages/', $response->getBody())
/*
$response = Director::test("admin/pages/publishitems", array(
'ID' => ''
'Title' => ''
'action_publish' => 'Save and publish',
), $session);
$this->assertRegexp('/Done: Published 4 pages/', $response->getBody())
*/
} }
/** /**
@ -577,4 +566,56 @@ class CMSMainTest extends FunctionalTest
$this->assertEquals(CMSMainTest_ClassB::class, $newPage->ClassName); $this->assertEquals(CMSMainTest_ClassB::class, $newPage->ClassName);
$this->assertEquals('Class A', $newPage->Title); $this->assertEquals('Class A', $newPage->Title);
} }
public function testSiteTreeHintsCache()
{
$cms = CMSMain::create();
/** @var Member $user */
$user = $this->objFromFixture(Member::class, 'rootedituser');
Security::setCurrentUser($user);
$pageClass = array_values(SiteTree::page_type_classes())[0];
$mockPageMissesCache = $this->getMockBuilder($pageClass)
->setMethods(['canCreate'])
->getMock();
$mockPageMissesCache
->expects($this->exactly(3))
->method('canCreate');
$mockPageHitsCache = $this->getMockBuilder($pageClass)
->setMethods(['canCreate'])
->getMock();
$mockPageHitsCache
->expects($this->never())
->method('canCreate');
// Initially, cache misses (1)
Injector::inst()->registerService($mockPageMissesCache, $pageClass);
$hints = $cms->SiteTreeHints();
$this->assertNotNull($hints);
// Now it hits
Injector::inst()->registerService($mockPageHitsCache, $pageClass);
$hints = $cms->SiteTreeHints();
$this->assertNotNull($hints);
// Mutating member record invalidates cache. Misses (2)
$user->FirstName = 'changed';
$user->write();
Injector::inst()->registerService($mockPageMissesCache, $pageClass);
$hints = $cms->SiteTreeHints();
$this->assertNotNull($hints);
// Now it hits again
Injector::inst()->registerService($mockPageHitsCache, $pageClass);
$hints = $cms->SiteTreeHints();
$this->assertNotNull($hints);
// Different user. Misses. (3)
$user = $this->objFromFixture(Member::class, 'allcmssectionsuser');
Security::setCurrentUser($user);
Injector::inst()->registerService($mockPageMissesCache, $pageClass);
$hints = $cms->SiteTreeHints();
$this->assertNotNull($hints);
}
} }

View File

@ -26,6 +26,7 @@ use SilverStripe\Versioned\Versioned;
use SilverStripe\View\Parsers\Diff; use SilverStripe\View\Parsers\Diff;
use SilverStripe\View\Parsers\ShortcodeParser; use SilverStripe\View\Parsers\ShortcodeParser;
use SilverStripe\View\Parsers\URLSegmentFilter; use SilverStripe\View\Parsers\URLSegmentFilter;
use SilverStripe\Core\Injector\Injector;
use LogicException; use LogicException;
class SiteTreeTest extends SapphireTest class SiteTreeTest extends SapphireTest
@ -1506,4 +1507,56 @@ class SiteTreeTest extends SapphireTest
$class = new SiteTreeTest_LegacyControllerName; $class = new SiteTreeTest_LegacyControllerName;
$this->assertEquals(SiteTreeTest_LegacyControllerName_Controller::class, $class->getControllerName()); $this->assertEquals(SiteTreeTest_LegacyControllerName_Controller::class, $class->getControllerName());
} }
public function testTreeTitleCache()
{
$siteTree = SiteTree::create();
$user = $this->objFromFixture(Member::class, 'allsections');
Security::setCurrentUser($user);
$pageClass = array_values(SiteTree::page_type_classes())[0];
$mockPageMissesCache = $this->getMockBuilder($pageClass)
->setMethods(['canCreate'])
->getMock();
$mockPageMissesCache
->expects($this->exactly(3))
->method('canCreate');
$mockPageHitsCache = $this->getMockBuilder($pageClass)
->setMethods(['canCreate'])
->getMock();
$mockPageHitsCache
->expects($this->never())
->method('canCreate');
// Initially, cache misses (1)
Injector::inst()->registerService($mockPageMissesCache, $pageClass);
$title = $siteTree->getTreeTitle();
$this->assertNotNull($title);
// Now it hits
Injector::inst()->registerService($mockPageHitsCache, $pageClass);
$title = $siteTree->getTreeTitle();
$this->assertNotNull($title);
// Mutating member record invalidates cache. Misses (2)
$user->FirstName = 'changed';
$user->write();
Injector::inst()->registerService($mockPageMissesCache, $pageClass);
$title = $siteTree->getTreeTitle();
$this->assertNotNull($title);
// Now it hits again
Injector::inst()->registerService($mockPageHitsCache, $pageClass);
$title = $siteTree->getTreeTitle();
$this->assertNotNull($title);
// Different user. Misses. (3)
$user = $this->objFromFixture(Member::class, 'editor');
Security::setCurrentUser($user);
Injector::inst()->registerService($mockPageMissesCache, $pageClass);
$title = $siteTree->getTreeTitle();
$this->assertNotNull($title);
}
} }