Merge pull request #5804 from open-sausages/feature/themestack

API Theme stacking
This commit is contained in:
Sam Minnée 2016-07-15 17:12:54 +12:00 committed by GitHub
commit 5c98d331a3
68 changed files with 503 additions and 862 deletions

View File

@ -1509,7 +1509,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
)
);
$dateFormatField->setValue($self->DateFormat);
$dateFormatField->setDescriptionTemplate('MemberDatetimeOptionsetField_description_date');
$dateFormatField->setDescriptionTemplate('forms/MemberDatetimeOptionsetField_description_date');
$defaultTimeFormat = Zend_Locale_Format::getTimeFormat(new Zend_Locale($self->Locale));
$timeFormatMap = array(
@ -1526,7 +1526,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
)
);
$timeFormatField->setValue($self->TimeFormat);
$timeFormatField->setDescriptionTemplate('MemberDatetimeOptionsetField_description_time');
$timeFormatField->setDescriptionTemplate('forms/MemberDatetimeOptionsetField_description_time');
});
return parent::getCMSFields();

View File

@ -783,7 +783,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
return $controller->renderWith($controller->getTemplatesWithSuffix('_Content'));
},
'Breadcrumbs' => function() use (&$controller) {
return $controller->renderWith('CMSBreadcrumbs');
return $controller->renderWith('Includes/CMSBreadcrumbs');
},
'default' => function() use(&$controller) {
return $controller->renderWith($controller->getViewer('show'));

View File

@ -366,7 +366,7 @@ abstract class ModelAdmin extends LeftAndMain {
'ModelName' => Convert::raw2att($modelName),
'Fields' => $specFields,
'Relations' => $specRelations,
))->renderWith('ModelAdmin_ImportSpec');
))->renderWith('Includes/ModelAdmin_ImportSpec');
$fields->push(new LiteralField("SpecFor{$modelName}", $specHTML));
$fields->push(

View File

@ -77,8 +77,7 @@ require_once 'core/manifest/ConfigManifest.php';
require_once 'core/manifest/ConfigStaticManifest.php';
require_once 'core/manifest/ClassManifest.php';
require_once 'core/manifest/ManifestFileFinder.php';
require_once 'core/manifest/TemplateLoader.php';
require_once 'core/manifest/TemplateManifest.php';
require_once 'view/TemplateLoader.php';
require_once 'core/manifest/TokenisedRegularExpression.php';
require_once 'control/injector/Injector.php';
@ -113,7 +112,7 @@ $configManifest = new SS_ConfigManifest(BASE_PATH, false, $flush);
Config::inst()->pushConfigYamlManifest($configManifest);
// Load template manifest
SS_TemplateLoader::instance()->pushManifest(new SS_TemplateManifest(
SilverStripe\View\TemplateLoader::instance()->addSet('$default', new SilverStripe\View\ThemeManifest(
BASE_PATH, project(), false, $flush
));

View File

@ -1,87 +0,0 @@
<?php
/**
* Handles finding templates from a stack of template manifest objects.
*
* @package framework
* @subpackage manifest
*/
class SS_TemplateLoader {
/**
* @var SS_TemplateLoader
*/
private static $instance;
/**
* @var SS_TemplateManifest[]
*/
protected $manifests = array();
/**
* @return SS_TemplateLoader
*/
public static function instance() {
return self::$instance ? self::$instance : self::$instance = new self();
}
/**
* Returns the currently active template manifest instance.
*
* @return SS_TemplateManifest
*/
public function getManifest() {
return $this->manifests[count($this->manifests) - 1];
}
/**
* @param SS_TemplateManifest $manifest
*/
public function pushManifest(SS_TemplateManifest $manifest) {
$this->manifests[] = $manifest;
}
/**
* @return SS_TemplateManifest
*/
public function popManifest() {
return array_pop($this->manifests);
}
/**
* Attempts to find possible candidate templates from a set of template
* names from modules, current theme directory and finally the application
* folder.
*
* The template names can be passed in as plain strings, or be in the
* format "type/name", where type is the type of template to search for
* (e.g. Includes, Layout).
*
* @param string|array $templates
* @param string $theme
*
* @return array
*/
public function findTemplates($templates, $theme = null) {
$result = array();
foreach ((array) $templates as $template) {
if (strpos($template, '/')) {
list($type, $template) = explode('/', $template, 2);
} else {
$type = null;
}
if ($found = $this->getManifest()->getCandidateTemplate($template, $theme)) {
if ($type && isset($found[$type])) {
$found = array(
'main' => $found[$type]
);
}
$result = array_merge($found, $result);
}
}
return $result;
}
}

View File

@ -1,271 +0,0 @@
<?php
/**
* A class which builds a manifest of all templates present in a directory,
* in both modules and themes.
*
* @package framework
* @subpackage manifest
*/
class SS_TemplateManifest {
const TEMPLATES_DIR = 'templates';
protected $base;
protected $tests;
protected $cache;
protected $cacheKey;
protected $project;
protected $inited;
protected $templates = array();
/**
* Constructs a new template manifest. The manifest is not actually built
* or loaded from cache until needed.
*
* @param string $base The base path.
* @param string $project Path to application code
*
* @param bool $includeTests Include tests in the manifest.
* @param bool $forceRegen Force the manifest to be regenerated.
*/
public function __construct($base, $project, $includeTests = false, $forceRegen = false) {
$this->base = $base;
$this->tests = $includeTests;
$this->project = $project;
$cacheClass = defined('SS_MANIFESTCACHE') ? SS_MANIFESTCACHE : 'ManifestCache_File';
$this->cache = new $cacheClass('templatemanifest'.($includeTests ? '_tests' : ''));
$this->cacheKey = $this->getCacheKey($includeTests);
if ($forceRegen) {
$this->regenerate();
}
}
/**
* @return string
*/
public function getBase() {
return $this->base;
}
/**
* Generate a unique cache key to avoid manifest cache collisions.
* We compartmentalise based on the base path, the given project, and whether
* or not we intend to include tests.
* @param boolean $includeTests
* @return string
*/
public function getCacheKey($includeTests = false) {
return sha1(sprintf(
"manifest-%s-%s-%s",
$this->base,
$this->project,
(int) $includeTests // cast true to 1, false to 0
)
);
}
/**
* Returns a map of all template information. The map is in the following
* format:
*
* <code>
* array(
* 'moduletemplate' => array(
* 'main' => '/path/to/module/templates/Main.ss'
* ),
* 'include' => array(
* 'include' => '/path/to/module/templates/Includes/Include.ss'
* ),
* 'page' => array(
* 'themes' => array(
* 'simple' => array(
* 'main' => '/path/to/theme/Page.ss'
* 'Layout' => '/path/to/theme/Layout/Page.ss'
* )
* )
* )
* )
* </code>
*
* @return array
*/
public function getTemplates() {
if (!$this->inited) {
$this->init();
}
return $this->templates;
}
/**
* Returns a set of possible candidate templates that match a certain
* template name.
*
* This is the same as extracting an individual array element from
* {@link SS_TemplateManifest::getTemplates()}.
*
* @param string $name
* @return array
*/
public function getTemplate($name) {
if (!$this->inited) {
$this->init();
}
$name = strtolower($name);
if (array_key_exists($name, $this->templates)) {
return $this->templates[$name];
} else {
return array();
}
}
/**
* Returns the correct candidate template. In order of importance, application
* project code, current theme and finally modules.
*
* @param string $name
* @param string $theme - theme name
*
* @return array
*/
public function getCandidateTemplate($name, $theme = null) {
$found = array();
$candidates = $this->getTemplate($name);
// theme overrides modules
if ($theme && isset($candidates['themes'][$theme])) {
$found = array_merge($candidates, $candidates['themes'][$theme]);
}
// project overrides theme
if ($this->project && isset($candidates[$this->project])) {
$found = array_merge($found, $candidates[$this->project]);
}
$found = ($found) ? $found : $candidates;
if (isset($found['themes'])) unset($found['themes']);
if (isset($found[$this->project])) unset($found[$this->project]);
return $found;
}
/**
* Regenerates the manifest by scanning the base path.
*
* @param bool $cache
*/
public function regenerate($cache = true) {
$finder = new ManifestFileFinder();
$finder->setOptions(array(
'name_regex' => '/\.ss$/',
'include_themes' => true,
'ignore_tests' => !$this->tests,
'file_callback' => array($this, 'handleFile')
));
$finder->find($this->base);
if ($cache) {
$this->cache->save($this->templates, $this->cacheKey);
}
$this->inited = true;
}
public function handleFile($basename, $pathname, $depth)
{
$projectFile = false;
$theme = null;
// Template in theme
if (preg_match(
'#'.preg_quote($this->base.'/'.THEMES_DIR).'/([^/_]+)(_[^/]+)?/(.*)$#',
$pathname,
$matches
)) {
$theme = $matches[1];
$relPath = $matches[3];
// Template in project
} elseif (preg_match(
'#'.preg_quote($this->base.'/'.$this->project).'/(.*)$#',
$pathname,
$matches
)) {
$projectFile = true;
$relPath = $matches[1];
// Template in module
} elseif (preg_match(
'#'.preg_quote($this->base).'/([^/]+)/(.*)$#',
$pathname,
$matches
)) {
$relPath = $matches[2];
} else {
throw new \LogicException("Can't determine meaning of path: $pathname");
}
// If a templates subfolder is used, ignore that
if (preg_match('#'.preg_quote(self::TEMPLATES_DIR).'/(.*)$#', $relPath, $matches)) {
$relPath = $matches[1];
}
// Layout and Content folders have special meaning
if (preg_match('#^(.*/)?(Layout|Content|Includes)/([^/]+)$#', $relPath, $matches)) {
$type = $matches[2];
$relPath = "$matches[1]$matches[3]";
} else {
$type = "main";
}
$name = strtolower(substr($relPath, 0, -3));
$name = str_replace('/', '\\', $name);
if ($theme) {
$this->templates[$name]['themes'][$theme][$type] = $pathname;
} else if ($projectFile) {
$this->templates[$name][$this->project][$type] = $pathname;
} else {
$this->templates[$name][$type] = $pathname;
}
// If we've found a template in a subdirectory, then allow its use for a non-namespaced class
// as well. This was a common SilverStripe 3 approach, where templates were placed into
// subfolders to suit the whim of the developer.
if (strpos($name, '\\') !== false) {
$name2 = substr($name, strrpos($name, '\\') + 1);
// In of these cases, the template will only be provided if it isn't already set. This
// matches SilverStripe 3 prioritisation.
if ($theme) {
if (!isset($this->templates[$name2]['themes'][$theme][$type])) {
$this->templates[$name2]['themes'][$theme][$type] = $pathname;
}
} else if ($projectFile) {
if (!isset($this->templates[$name2][$this->project][$type])) {
$this->templates[$name2][$this->project][$type] = $pathname;
}
} else {
if (!isset($this->templates[$name2][$type])) {
$this->templates[$name2][$type] = $pathname;
}
}
}
}
protected function init() {
if ($data = $this->cache->load($this->cacheKey)) {
$this->templates = $data;
$this->inited = true;
} else {
$this->regenerate();
}
}
}

View File

@ -13,8 +13,8 @@ use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
use SilverStripe\Security\Group;
use SilverStripe\Security\Permission;
use SilverStripe\View\TemplateLoader;
use SilverStripe\View\ThemeManifest;
/**
* Test case class for the Sapphire framework.
@ -848,7 +848,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
SS_ClassLoader::instance()->pushManifest($classManifest, false);
SapphireTest::set_test_class_manifest($classManifest);
SS_TemplateLoader::instance()->pushManifest(new SS_TemplateManifest(
TemplateLoader::instance()->addSet('$default', new ThemeManifest(
BASE_PATH, project(), true, $flush
));
@ -1054,22 +1054,15 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
*/
protected function useTestTheme($themeBaseDir, $theme, $callback) {
Config::nest();
global $project;
$manifest = new SS_TemplateManifest($themeBaseDir, $project, true, true);
SS_TemplateLoader::instance()->pushManifest($manifest);
Config::inst()->update('SSViewer', 'theme', $theme);
if (strpos($themeBaseDir, BASE_PATH) === 0) $themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH));
SSViewer::set_themes([$themeBaseDir.'/themes/'.$theme, '$default']);
$e = null;
try { $callback(); }
catch (Exception $e) { /* NOP for now, just save $e */ }
// Remove all the test themes we created
SS_TemplateLoader::instance()->popManifest();
Config::unnest();
if ($e) throw $e;

View File

@ -397,6 +397,14 @@ all changed project files.
This will resolve the majority of upgrading work, but for specific changes that will
require manual intervention, please see the below upgrading notes.
### Make sure templates are in correct locations
Templates are now much more strict about their locations. You can no longer put a template in an arbitrary
folder and have it be found. Case is now also checked on case-sensitive filesystems.
Either include the folder in the template name (`renderWith('Field.ss')` => `renderWith('forms/Field.ss')`), move the
template into the correct directory, or both.
### Update code that uses SQLQuery
Where your code once used SQLQuery you should now use SQLSelect in all cases, as this has been removed.

View File

@ -373,7 +373,7 @@ class Email extends ViewableData {
// Requery data so that updated versions of To, From, Subject, etc are included
$data = $this->templateData();
$template = new SSViewer($this->ss_template);
$template = new SSViewer('email/'.$this->ss_template);
if($template->exists()) {
$fullBody = $template->process($data);

View File

@ -16,10 +16,10 @@ class ProtectedAssetAdapter extends AssetAdapter implements ProtectedAdapter {
private static $server_configuration = array(
'apache' => array(
'.htaccess' => "Protected_HTAccess"
'.htaccess' => "filesystem/Protected_HTAccess"
),
'microsoft-iis' => array(
'web.config' => "Protected_WebConfig"
'web.config' => "filesystem/Protected_WebConfig"
)
);

View File

@ -21,10 +21,10 @@ class PublicAssetAdapter extends AssetAdapter implements PublicAdapter {
*/
private static $server_configuration = array(
'apache' => array(
'.htaccess' => "Assets_HTAccess"
'.htaccess' => "filesystem/Assets_HTAccess"
),
'microsoft-iis' => array(
'web.config' => "Assets_WebConfig"
'web.config' => "filesystem/Assets_WebConfig"
)
);

View File

@ -48,7 +48,7 @@ class AssetField extends FileField {
*
* @var string
*/
protected $templateFileButtons = 'AssetField_FileButtons';
protected $templateFileButtons = 'Includes/AssetField_FileButtons';
/**
* Parent data record. Will be infered from parent form or controller if blank. The destination

View File

@ -1632,7 +1632,7 @@ class Form extends RequestHandler {
public function forTemplate() {
$return = $this->renderWith(array_merge(
(array)$this->getTemplate(),
array('Form')
array('Includes/Form')
));
// Now that we're rendered, clear message

View File

@ -1052,7 +1052,7 @@ class FormField extends RequestHandler {
$matches = array();
foreach(array_reverse(ClassInfo::ancestry($this)) as $className) {
$matches[] = $className . $customTemplateSuffix;
$matches[] = 'forms/'. $className . $customTemplateSuffix;
if($className == "FormField") {
break;
@ -1060,7 +1060,7 @@ class FormField extends RequestHandler {
}
if($customTemplate) {
array_unshift($matches, $customTemplate);
array_unshift($matches, 'forms/'.$customTemplate);
}
return $matches;

View File

@ -44,6 +44,7 @@ class MemberDatetimeOptionsetField extends OptionsetField {
'Options' => new ArrayList($options)
));
return $this->customise($properties)->renderWith(
$this->getTemplates()
);

View File

@ -65,7 +65,7 @@ class UploadField extends FileField {
*
* @var string
*/
protected $templateFileButtons = 'UploadField_FileButtons';
protected $templateFileButtons = 'Includes/UploadField_FileButtons';
/**
* Template to use for the edit form

View File

@ -131,7 +131,7 @@ class GridFieldAddExistingAutocompleter
}
return array(
$this->targetFragment => $forTemplate->renderWith($this->itemClass)
$this->targetFragment => $forTemplate->renderWith('Includes/'.$this->itemClass)
);
}

View File

@ -44,7 +44,7 @@ class GridFieldAddNewButton implements GridField_HTMLProvider {
));
return array(
$this->targetFragment => $data->renderWith('GridFieldAddNewbutton'),
$this->targetFragment => $data->renderWith('Includes/GridFieldAddNewButton'),
);
}

View File

@ -27,7 +27,7 @@ class GridFieldButtonRow implements GridField_HTMLProvider {
));
return array(
$this->targetFragment => $data->renderWith('GridFieldButtonRow')
$this->targetFragment => $data->renderWith('Includes/GridFieldButtonRow')
);
}
}

View File

@ -441,7 +441,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
// Always show with base template (full width, no other panels),
// regardless of overloaded CMS controller templates.
// TODO Allow customization, e.g. to display an edit form alongside a search form from the CMS controller
$form->setTemplate('LeftAndMain_EditForm');
$form->setTemplate('Includes/LeftAndMain_EditForm');
$form->addExtraClass('cms-content cms-edit-form center');
$form->setAttribute('data-pjax-fragment', 'CurrentForm Content');
if($form->Fields()->hasTabset()) {

View File

@ -86,7 +86,7 @@ class GridFieldEditButton implements GridField_ColumnProvider {
'Link' => Controller::join_links($gridField->Link('item'), $record->ID, 'edit')
));
return $data->renderWith('GridFieldEditButton');
return $data->renderWith('Includes/GridFieldEditButton');
}
/**

View File

@ -162,7 +162,7 @@ class GridFieldFilterHeader implements GridField_HTMLProvider, GridField_DataMan
}
return array(
'header' => $forTemplate->renderWith('GridFieldFilterHeader_Row'),
'header' => $forTemplate->renderWith('Includes/GridFieldFilterHeader_Row'),
);
}
}

View File

@ -48,7 +48,7 @@ class GridFieldFooter implements GridField_HTMLProvider {
return array(
'footer' => $forTemplate->renderWith(
'GridFieldFooter',
'Includes/GridFieldFooter',
array(
'Colspan' => count($gridField->getColumns())
)

View File

@ -66,7 +66,7 @@ class GridFieldLevelup extends Object implements GridField_HTMLProvider {
));
return array(
'before' => $forTemplate->renderWith('GridFieldLevelup'),
'before' => $forTemplate->renderWith('Includes/GridFieldLevelup'),
);
}
}

View File

@ -67,7 +67,7 @@ class GridFieldPageCount implements GridField_HTMLProvider {
$paginator = $this->getPaginator($gridField);
if ($paginator && ($forTemplate = $paginator->getTemplateParameters($gridField))) {
return array(
$this->targetFragment => $forTemplate->renderWith($this->itemClass)
$this->targetFragment => $forTemplate->renderWith('Includes/'.$this->itemClass)
);
}
}

View File

@ -259,7 +259,7 @@ class GridFieldPaginator implements GridField_HTMLProvider, GridField_DataManipu
$forTemplate = $this->getTemplateParameters($gridField);
if($forTemplate) {
return array(
'footer' => $forTemplate->renderWith($this->itemClass,
'footer' => $forTemplate->renderWith('Includes/'.$this->itemClass,
array('Colspan'=>count($gridField->getColumns())))
);
}

View File

@ -164,7 +164,7 @@ class GridFieldSortableHeader implements GridField_HTMLProvider, GridField_DataM
}
return array(
'header' => $forTemplate->renderWith('GridFieldSortableHeader_Row'),
'header' => $forTemplate->renderWith('Includes/GridFieldSortableHeader_Row'),
);
}

View File

@ -13,7 +13,7 @@ class GridFieldToolbarHeader implements GridField_HTMLProvider {
public function getHTMLFragments( $gridField) {
return array(
'header' => $gridField->renderWith('GridFieldToolbarHeader')
'header' => $gridField->renderWith('Includes/GridFieldToolbarHeader')
);
}
}

View File

@ -22,7 +22,7 @@ class GridFieldViewButton implements GridField_ColumnProvider {
$data = new ArrayData(array(
'Link' => Controller::join_links($field->Link('item'), $record->ID, 'view')
));
return $data->renderWith('GridFieldViewButton');
return $data->renderWith('Includes/GridFieldViewButton');
}
}

View File

@ -181,7 +181,7 @@ class HTMLEditorField_Toolbar extends RequestHandler {
/**
* @var string
*/
protected $templateViewFile = 'HTMLEditorField_viewfile';
protected $templateViewFile = 'Includes/HTMLEditorField_viewfile';
protected $controller, $name;

View File

@ -1,5 +1,7 @@
<?php
use SilverStripe\View\TemplateLoader;
/**
* Default configuration for HtmlEditor specific to tinymce
*/
@ -419,13 +421,18 @@ class TinyMCEConfig extends HTMLEditorConfig {
Director::absoluteBaseURL(),
FRAMEWORK_ADMIN_DIR . '/client/dist/styles/editor.css'
);
if($theme = SSViewer::get_theme_folder()) {
$editorDir = $theme . '/css/editor.css';
foreach(SSViewer::get_themes() as $theme) {
$path = TemplateLoader::instance()->getPath($theme);
$editorDir = $path . '/css/editor.css';;
if(file_exists(BASE_PATH . '/' . $editorDir)) {
$editor[] = Controller::join_links(
Director::absoluteBaseURL(),
$editorDir
);
break;
}
}
return $editor;

View File

@ -1,6 +1,10 @@
<?php
use SilverStripe\View\TemplateLoader;
use SilverStripe\View\ThemeManifest;
/**
* Tests for the {@link SS_TemplateLoader} class.
* Tests for the {@link TemplateLoader} class.
*
* @package framework
* @subpackage tests
@ -16,162 +20,82 @@ class TemplateLoaderTest extends SapphireTest {
*/
public function setUp() {
parent::setUp();
// Fake project root
$this->base = dirname(__FILE__) . '/fixtures/templatemanifest';
$this->manifest = new SS_TemplateManifest($this->base, 'myproject', false, true);
$this->loader = new SS_TemplateLoader();
$this->refreshLoader();
// New ThemeManifest for that root
$this->manifest = new \SilverStripe\View\ThemeManifest($this->base, 'myproject', false, true);
// New Loader for that root
$this->loader = new \SilverStripe\View\TemplateLoader($this->base);
$this->loader->addSet('$default', $this->manifest);
}
/**
* Test that 'main' and 'Layout' templates are loaded from module
*/
public function testFindTemplatesInModule() {
$expect = array(
'main' => "$this->base/module/templates/Page.ss",
'Layout' => "$this->base/module/templates/Layout/Page.ss"
$this->assertEquals(
"$this->base/module/templates/Page.ss",
$this->loader->findTemplate('Page', ['$default'])
);
$this->assertEquals(
"$this->base/module/templates/Layout/Page.ss",
$this->loader->findTemplate(['type' => 'Layout', 'Page'], ['$default'])
);
$this->assertEquals($expect, $this->loader->findTemplates('Page'));
$this->assertEquals($expect, $this->loader->findTemplates('PAGE'));
$this->assertEquals($expect, $this->loader->findTemplates(array('Foo', 'Page')));
}
/**
* Test that 'main' and 'Layout' templates are loaded from set theme
*/
public function testFindTemplatesInTheme() {
$expect = array(
'main' => "$this->base/themes/theme/templates/Page.ss",
'Layout' => "$this->base/themes/theme/templates/Layout/Page.ss"
$this->assertEquals(
"$this->base/themes/theme/templates/Page.ss",
$this->loader->findTemplate('Page', ['theme'])
);
$this->assertEquals(
"$this->base/themes/theme/templates/Layout/Page.ss",
$this->loader->findTemplate(['type' => 'Layout', 'Page'], ['theme'])
);
$this->assertEquals($expect, $this->loader->findTemplates('Page', 'theme'));
$this->assertEquals($expect, $this->loader->findTemplates('PAGE', 'theme'));
$this->assertEquals($expect, $this->loader->findTemplates(array('Foo', 'Page'), 'theme'));
}
/**
* Test that 'main' and 'Layout' templates are loaded from project without a set theme
*/
public function testFindTemplatesInApplication() {
// TODO: replace with one that doesn't create temporary files (so bad)
$templates = array(
$this->base . '/myproject/templates/Page.ss',
$this->base . '/myproject/templates/Layout/Page.ss'
);
$this->createTestTemplates($templates);
$this->refreshLoader();
$expect = array(
'main' => "$this->base/myproject/templates/Page.ss",
'Layout' => "$this->base/myproject/templates/Layout/Page.ss"
$this->assertEquals(
"$this->base/myproject/templates/Page.ss",
$this->loader->findTemplate('Page', ['$default'])
);
$this->assertEquals(
"$this->base/myproject/templates/Layout/Page.ss",
$this->loader->findTemplate(['type' => 'Layout', 'Page'], ['$default'])
);
$this->assertEquals($expect, $this->loader->findTemplates('Page'));
$this->assertEquals($expect, $this->loader->findTemplates('PAGE'));
$this->assertEquals($expect, $this->loader->findTemplates(array('Foo', 'Page')));
$this->removeTestTemplates($templates);
}
/**
* Test that 'Layout' template is loaded from module
*/
public function testFindTemplatesInModuleLayout() {
$expect = array(
'main' => "$this->base/module/templates/Layout/Page.ss"
);
$this->assertEquals($expect, $this->loader->findTemplates('Layout/Page'));
}
/**
* Test that 'Layout' template is loaded from theme
*/
public function testFindTemplatesInThemeLayout() {
$expect = array(
'main' => "$this->base/themes/theme/templates/Layout/Page.ss"
);
$this->assertEquals($expect, $this->loader->findTemplates('Layout/Page', 'theme'));
}
/**
* Test that 'main' template is found in theme and 'Layout' is found in module
*/
public function testFindTemplatesMainThemeLayoutModule() {
$expect = array(
'main' => "$this->base/themes/theme/templates/CustomThemePage.ss",
'Layout' => "$this->base/module/templates/Layout/CustomThemePage.ss"
$this->assertEquals(
"$this->base/themes/theme/templates/CustomThemePage.ss",
$this->loader->findTemplate('CustomThemePage', ['theme', '$default'])
);
$this->assertEquals($expect, $this->loader->findTemplates(array('CustomThemePage', 'Page'), 'theme'));
}
/**
* Test that project template overrides module template of same name
*/
public function testFindTemplatesApplicationOverridesModule() {
$expect = array(
'main' => "$this->base/myproject/templates/CustomTemplate.ss"
$this->assertEquals(
"$this->base/module/templates/Layout/CustomThemePage.ss",
$this->loader->findTemplate(['type' => 'Layout', 'CustomThemePage'], ['theme', '$default'])
);
$this->assertEquals($expect, $this->loader->findTemplates('CustomTemplate'));
}
/**
* Test that project templates overrides theme templates
*/
public function testFindTemplatesApplicationOverridesTheme() {
$templates = array(
$this->base . '/myproject/templates/Page.ss',
$this->base . '/myproject/templates/Layout/Page.ss'
);
$this->createTestTemplates($templates);
$this->refreshLoader();
$expect = array(
'main' => "$this->base/myproject/templates/Page.ss",
'Layout' => "$this->base/myproject/templates/Layout/Page.ss"
);
$this->assertEquals($expect, $this->loader->findTemplates('Page'), 'theme');
$this->removeTestTemplates($templates);
}
/**
* Test that project 'Layout' template overrides theme 'Layout' template
*/
public function testFindTemplatesApplicationLayoutOverridesThemeLayout() {
$templates = array(
$this->base . '/myproject/templates/Layout/Page.ss'
);
$this->createTestTemplates($templates);
$this->refreshLoader();
$expect = array(
'main' => "$this->base/themes/theme/templates/Page.ss",
'Layout' => "$this->base/myproject/templates/Layout/Page.ss"
);
$this->assertEquals($expect, $this->loader->findTemplates('Page', 'theme'));
$this->removeTestTemplates($templates);
}
/**
* Test that project 'main' template overrides theme 'main' template
*/
public function testFindTemplatesApplicationMainOverridesThemeMain() {
$templates = array(
$this->base . '/myproject/templates/Page.ss'
);
$this->createTestTemplates($templates);
$this->refreshLoader();
$expect = array(
'main' => "$this->base/myproject/templates/Page.ss",
'Layout' => "$this->base/themes/theme/templates/Layout/Page.ss"
);
$this->assertEquals($expect, $this->loader->findTemplates('Page', 'theme'));
$this->removeTestTemplates($templates);
}
protected function refreshLoader() {
$this->manifest->regenerate(false);
$this->loader->pushManifest($this->manifest);
}
protected function createTestTemplates($templates) {

View File

@ -1,141 +0,0 @@
<?php
/**
* Tests for the template manifest.
*
* @package framework
* @subpackage tests
*/
class TemplateManifestTest extends SapphireTest {
protected $base;
protected $manifest;
protected $manifestTests;
public function setUp() {
parent::setUp();
$this->base = dirname(__FILE__) . '/fixtures/templatemanifest';
$this->manifest = new SS_TemplateManifest($this->base, 'myproject');
$this->manifestTests = new SS_TemplateManifest($this->base, 'myproject', true);
$this->manifest->regenerate(false);
$this->manifestTests->regenerate(false);
}
public function testGetTemplates() {
$expect = array(
'root' => array(
'main' => "{$this->base}/module/Root.ss"
),
'page' => array(
'main' => "{$this->base}/module/templates/Page.ss",
'Layout' => "{$this->base}/module/templates/Layout/Page.ss",
'themes' => array('theme' => array(
'main' => "{$this->base}/themes/theme/templates/Page.ss",
'Layout' => "{$this->base}/themes/theme/templates/Layout/Page.ss"
))
),
'custompage' => array(
'Layout' => "{$this->base}/module/templates/Layout/CustomPage.ss"
),
'customtemplate' => array(
'main' => "{$this->base}/module/templates/CustomTemplate.ss",
'myproject' => array(
'main' => "{$this->base}/myproject/templates/CustomTemplate.ss"
)
),
'subfolder' => array(
'main' => "{$this->base}/module/subfolder/templates/Subfolder.ss"
),
'customthemepage' => array (
'Layout' => "{$this->base}/module/templates/Layout/CustomThemePage.ss",
'themes' =>
array(
'theme' => array('main' => "{$this->base}/themes/theme/templates/CustomThemePage.ss",)
)
),
'mynamespace\myclass' => array(
'main' => "{$this->base}/module/templates/MyNamespace/MyClass.ss",
'Layout' => "{$this->base}/module/templates/MyNamespace/Layout/MyClass.ss",
'themes' => array(
'theme' => array(
'main' => "{$this->base}/themes/theme/templates/MyNamespace/MyClass.ss",
)
),
),
'mynamespace\mysubnamespace\mysubclass' => array(
'main' => "{$this->base}/module/templates/MyNamespace/MySubnamespace/MySubclass.ss",
),
'myclass' => array(
'main' => "{$this->base}/module/templates/MyNamespace/MyClass.ss",
'Layout' => "{$this->base}/module/templates/MyNamespace/Layout/MyClass.ss",
'themes' => array(
'theme' => array(
'main' => "{$this->base}/themes/theme/templates/MyNamespace/MyClass.ss",
)
),
),
'mysubclass' => array(
'main' => "{$this->base}/module/templates/MyNamespace/MySubnamespace/MySubclass.ss",
),
'include' => array('themes' => array(
'theme' => array(
'Includes' => "{$this->base}/themes/theme/templates/Includes/Include.ss"
)
))
);
$expectTests = $expect;
$expectTests['test'] = array(
'main' => "{$this->base}/module/tests/templates/Test.ss"
);
$manifest = $this->manifest->getTemplates();
$manifestTests = $this->manifestTests->getTemplates();
ksort($expect);
ksort($expectTests);
ksort($manifest);
ksort($manifestTests);
$this->assertEquals(
$expect, $manifest,
'All templates are correctly loaded in the manifest.'
);
$this->assertEquals(
$expectTests, $manifestTests,
'The test manifest is the same, but includes test templates.'
);
}
public function testGetTemplate() {
$expectPage = array(
'main' => "{$this->base}/module/templates/Page.ss",
'Layout' => "{$this->base}/module/templates/Layout/Page.ss",
'themes' => array('theme' => array(
'main' => "{$this->base}/themes/theme/templates/Page.ss",
'Layout' => "{$this->base}/themes/theme/templates/Layout/Page.ss"
))
);
$expectTests = array(
'main' => "{$this->base}/module/tests/templates/Test.ss"
);
$this->assertEquals($expectPage, $this->manifest->getTemplate('Page'));
$this->assertEquals($expectPage, $this->manifest->getTemplate('PAGE'));
$this->assertEquals($expectPage, $this->manifestTests->getTemplate('Page'));
$this->assertEquals($expectPage, $this->manifestTests->getTemplate('PAGE'));
$this->assertEquals(array(), $this->manifest->getTemplate('Test'));
$this->assertEquals($expectTests, $this->manifestTests->getTemplate('Test'));
$this->assertEquals(array(
'main' => "{$this->base}/module/templates/CustomTemplate.ss",
'myproject' => array(
'main' => "{$this->base}/myproject/templates/CustomTemplate.ss"
)), $this->manifestTests->getTemplate('CustomTemplate'));
}
}

View File

@ -110,7 +110,7 @@ class MemberDatetimeOptionsetFieldTest extends SapphireTest {
$field->setDescription('Test description');
$this->assertEquals('Test description', $field->getDescription());
$field->setDescriptionTemplate('MemberDatetimeOptionsetField_description_time');
$field->setDescriptionTemplate('forms/MemberDatetimeOptionsetField_description_time');
$this->assertNotEmpty($field->getDescription());
$this->assertNotEquals('Test description', $field->getDescription());
}

View File

@ -1,9 +1,11 @@
<?php
use SilverStripe\View\TemplateLoader;
/**
* @package framework
* @subpackage i18n
*/
class i18nSSLegacyAdapterTest extends SapphireTest {
public function setUp() {
@ -14,10 +16,10 @@ class i18nSSLegacyAdapterTest extends SapphireTest {
Filesystem::makeFolder($this->alternateBaseSavePath);
Config::inst()->update('Director', 'alternate_base_folder', $this->alternateBasePath);
// Push a template loader running from the fake webroot onto the stack.
$templateManifest = new SS_TemplateManifest($this->alternateBasePath, null, false, true);
$templateManifest->regenerate(false);
SS_TemplateLoader::instance()->pushManifest($templateManifest);
// Replace old template loader with new one with alternate base path
$this->_oldLoader = TemplateLoader::instance();
TemplateLoader::set_instance(new TemplateLoader($this->alternateBasePath));
$this->_oldTheme = Config::inst()->get('SSViewer', 'theme');
Config::inst()->update('SSViewer', 'theme', 'testtheme1');
@ -40,7 +42,7 @@ class i18nSSLegacyAdapterTest extends SapphireTest {
}
public function tearDown() {
SS_TemplateLoader::instance()->popManifest();
TemplateLoader::set_instance($this->_oldLoader);
SS_ClassLoader::instance()->popManifest();
i18n::set_locale($this->originalLocale);
Config::inst()->update('Director', 'alternate_base_folder', null);

View File

@ -1,6 +1,9 @@
<?php
use SilverStripe\ORM\DataObject;
use SilverStripe\View\TemplateLoader;
use SilverStripe\View\ThemeManifest;
require_once 'Zend/Translate.php';
/**
@ -35,10 +38,13 @@ class i18nTest extends SapphireTest {
FileSystem::makeFolder($this->alternateBaseSavePath);
Config::inst()->update('Director', 'alternate_base_folder', $this->alternateBasePath);
// Push a template loader running from the fake webroot onto the stack.
$templateManifest = new SS_TemplateManifest($this->alternateBasePath, null, false, true);
$templateManifest->regenerate(false);
SS_TemplateLoader::instance()->pushManifest($templateManifest);
// Replace old template loader with new one with alternate base path
$this->_oldLoader = TemplateLoader::instance();
TemplateLoader::set_instance($loader = new TemplateLoader($this->alternateBasePath));
$loader->addSet('$default', new ThemeManifest(
$this->alternateBasePath, project(), false, true
));
$this->_oldTheme = Config::inst()->get('SSViewer', 'theme');
Config::inst()->update('SSViewer', 'theme', 'testtheme1');
@ -58,7 +64,7 @@ class i18nTest extends SapphireTest {
}
public function tearDown() {
SS_TemplateLoader::instance()->popManifest();
TemplateLoader::set_instance($this->_oldLoader);
i18n::set_locale($this->originalLocale);
Config::inst()->update('Director', 'alternate_base_folder', null);
Config::inst()->update('SSViewer', 'theme', $this->_oldTheme);

View File

@ -1,4 +1,7 @@
<?php
use SilverStripe\View\TemplateLoader;
/**
* @package framework
* @subpackage tests
@ -34,13 +37,13 @@ class i18nTextCollectorTest extends SapphireTest {
$this->alternateBasePath, false, true, false
);
$manifest = new SS_TemplateManifest($this->alternateBasePath, null, false, true);
$manifest->regenerate(false);
SS_TemplateLoader::instance()->pushManifest($manifest);
// Replace old template loader with new one with alternate base path
$this->_oldLoader = TemplateLoader::instance();
TemplateLoader::set_instance(new TemplateLoader($this->alternateBasePath));
}
public function tearDown() {
SS_TemplateLoader::instance()->popManifest();
TemplateLoader::set_instance($this->_oldLoader);
// Pop if added during testing
if(SS_ClassLoader::instance()->getManifest() === $this->manifest) {
SS_ClassLoader::instance()->popManifest();

View File

@ -2,6 +2,5 @@
<% include SSViewerTestProcessHead %>
<body>
<% include SSViewerTestCommentsWithInclude %>
</body>
</html>

View File

@ -759,17 +759,11 @@ after')
$this->render('tests:( <% include Namespace/NamespaceInclude %> )', $data),
'Forward slashes work for namespace references in includes'
);
$this->assertEquals(
"tests:( NamespaceInclude\n )",
$this->render('tests:( <% include NamespaceInclude %> )', $data),
'Namespace can be missed for a namespaed include'
);
}
public function testRecursiveInclude() {
$view = new SSViewer(array('SSViewerTestRecursiveInclude'));
$view = new SSViewer(array('Includes/SSViewerTestRecursiveInclude'));
$data = new ArrayData(array(
'Title' => 'A',
@ -1160,21 +1154,21 @@ after')
$this->useTestTheme(dirname(__FILE__), 'layouttest', function() use ($self) {
// Test passing a string
$templates = SSViewer::get_templates_by_class(
'TestNamespace\SSViewerTest_Controller',
'TestNamespace\SSViewerTestModel_Controller',
'',
'Controller'
);
$self->assertEquals([
'TestNamespace\SSViewerTest_Controller',
'TestNamespace\SSViewerTestModel_Controller',
'Controller',
], $templates);
// Test to ensure we're stopping at the base class.
$templates = SSViewer::get_templates_by_class('TestNamespace\SSViewerTest_Controller', '', 'TestNamespace\SSViewerTest_Controller');
$templates = SSViewer::get_templates_by_class('TestNamespace\SSViewerTestModel_Controller', '', 'TestNamespace\SSViewerTestModel_Controller');
$self->assertCount(1, $templates);
// Make sure we can filter our templates by suffix.
$templates = SSViewer::get_templates_by_class('SSViewerTest', '_Controller');
$templates = SSViewer::get_templates_by_class('TestNamespace\SSViewerTestModel', '_Controller');
$self->assertCount(1, $templates);
// Let's throw something random in there.
@ -1183,38 +1177,6 @@ after')
});
}
/**
* @covers SSViewer::get_themes()
*/
public function testThemeRetrieval() {
$ds = DIRECTORY_SEPARATOR;
$testThemeBaseDir = TEMP_FOLDER . $ds . 'test-themes';
if(file_exists($testThemeBaseDir)) Filesystem::removeFolder($testThemeBaseDir);
mkdir($testThemeBaseDir);
mkdir($testThemeBaseDir . $ds . 'blackcandy');
mkdir($testThemeBaseDir . $ds . 'blackcandy_blog');
mkdir($testThemeBaseDir . $ds . 'darkshades');
mkdir($testThemeBaseDir . $ds . 'darkshades_blog');
$this->assertEquals(array(
'blackcandy' => 'blackcandy',
'darkshades' => 'darkshades'
), SSViewer::get_themes($testThemeBaseDir), 'Our test theme directory contains 2 themes');
$this->assertEquals(array(
'blackcandy' => 'blackcandy',
'blackcandy_blog' => 'blackcandy_blog',
'darkshades' => 'darkshades',
'darkshades_blog' => 'darkshades_blog'
), SSViewer::get_themes($testThemeBaseDir, true),
'Our test theme directory contains 2 themes and 2 sub-themes');
// Remove all the test themes we created
Filesystem::removeFolder($testThemeBaseDir);
}
public function testRewriteHashlinks() {
$orig = Config::inst()->get('SSViewer', 'rewrite_hash_links');
Config::inst()->update('SSViewer', 'rewrite_hash_links', true);
@ -1327,6 +1289,7 @@ EOC;
$origEnv = Config::inst()->get('Director', 'environment_type');
Config::inst()->update('Director', 'environment_type', 'dev');
Config::inst()->update('SSViewer', 'source_file_comments', true);
$i = FRAMEWORK_PATH . '/tests/templates/Includes';
$f = FRAMEWORK_PATH . '/tests/templates/SSViewerTestComments';
$templates = array(
array(
@ -1398,16 +1361,16 @@ EOC;
"<!-- template $f/SSViewerTestCommentsWithInclude.ss -->"
. "<div class='typography'>"
. "<!-- include 'SSViewerTestCommentsInclude' -->"
. "<!-- template $f/SSViewerTestCommentsInclude.ss -->"
. "<!-- template $i/SSViewerTestCommentsInclude.ss -->"
. "Included"
. "<!-- end template $f/SSViewerTestCommentsInclude.ss -->"
. "<!-- end template $i/SSViewerTestCommentsInclude.ss -->"
. "<!-- end include 'SSViewerTestCommentsInclude' -->"
. "</div>"
. "<!-- end template $f/SSViewerTestCommentsWithInclude.ss -->",
),
);
foreach ($templates as $template) {
$this->_renderWithSourceFileComments($template['name'], $template['expected']);
$this->_renderWithSourceFileComments('SSViewerTestComments/'.$template['name'], $template['expected']);
}
Config::inst()->update('SSViewer', 'source_file_comments', false);
Config::inst()->update('Director', 'environment_type', $origEnv);

View File

@ -2,7 +2,12 @@
namespace TestNamespace;
class SSViewerTest_Controller extends \Controller
{
use SilverStripe\ORM\DataObject;
class SSViewerTestModel extends DataObject {
}
class SSViewerTestModel_Controller extends \Controller {
}

View File

@ -1,6 +1,7 @@
<?php
use SilverStripe\Filesystem\Storage\GeneratedAssetHandler;
use SilverStripe\View\TemplateLoader;
/**
* Requirements tracker for JavaScript and CSS.
@ -818,7 +819,7 @@ class Requirements_Backend
public function javascript($file, $options = array()) {
// make sure that async/defer is set if it is set once even if file is included multiple times
$async = (
isset($options['async']) && isset($options['async']) == true
isset($options['async']) && isset($options['async']) == true
|| (
isset($this->javascript[$file])
&& isset($this->javascript[$file]['async'])
@ -842,7 +843,7 @@ class Requirements_Backend
if(isset($options['provides'])) {
$this->providedJavascript[$file] = array_values($options['provides']);
}
}
/**
@ -1798,21 +1799,27 @@ class Requirements_Backend
* (e.g. 'screen,projector')
*/
public function themedCSS($name, $module = null, $media = null) {
$theme = SSViewer::get_theme_folder();
$project = project();
$absbase = BASE_PATH . DIRECTORY_SEPARATOR;
$abstheme = $absbase . $theme;
$absproject = $absbase . $project;
$css = "/css/$name.css";
$project = project();
$absbase = BASE_PATH . DIRECTORY_SEPARATOR;
$absproject = $absbase . $project;
if(file_exists($absproject . $css)) {
$this->css($project . $css, $media);
} elseif($module && file_exists($abstheme . '_' . $module.$css)) {
$this->css($theme . '_' . $module . $css, $media);
} elseif(file_exists($abstheme . $css)) {
$this->css($theme . $css, $media);
} elseif($module) {
$this->css($module . $css, $media);
return $this->css($project . $css, $media);
}
foreach(SSViewer::get_themes() as $theme) {
$path = TemplateLoader::instance()->getPath($theme);
$abspath = BASE_PATH . '/' . $path;
if(file_exists($abspath . $css)) {
return $this->css($path . $css, $media);
}
}
if($module) {
return $this->css($module . $css, $media);
}
}

View File

@ -3382,7 +3382,7 @@ class SSTemplateParser extends Parser implements TemplateParser {
$template = $res['template'];
$arguments = $res['arguments'];
$res['php'] = '$val .= SSViewer::execute_template('.$template.', $scope->getItem(), array(' .
$res['php'] = '$val .= SSViewer::execute_template(["type" => "Includes", '.$template.'], $scope->getItem(), array(' .
implode(',', $arguments)."), \$scope);\n";
if($this->includeDebuggingComments) { // Add include filename comments on dev sites

View File

@ -830,7 +830,7 @@ class SSTemplateParser extends Parser implements TemplateParser {
$template = $res['template'];
$arguments = $res['arguments'];
$res['php'] = '$val .= SSViewer::execute_template('.$template.', $scope->getItem(), array(' .
$res['php'] = '$val .= SSViewer::execute_template(["type" => "Includes", '.$template.'], $scope->getItem(), array(' .
implode(',', $arguments)."), \$scope);\n";
if($this->includeDebuggingComments) { // Add include filename comments on dev sites

View File

@ -3,6 +3,7 @@
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Security\Permission;
use SilverStripe\View\TemplateLoader;
/**
* This tracks the current scope for an SSViewer instance. It has three goals:
@ -723,10 +724,19 @@ class SSViewer implements Flushable {
}
/**
* @var array $chosenTemplates Associative array for the different
* template containers: "main" and "Layout". Values are absolute file paths to *.ss files.
* @var array $templates List of templates to select from
*/
private $chosenTemplates = array();
private $templates = null;
/**
* @var string $chosen Absolute path to chosen template file
*/
private $chosen = null;
/**
* @var array Templates to use when looking up 'Layout' or 'Content'
*/
private $subTemplates = null;
/**
* @var boolean
@ -735,9 +745,17 @@ class SSViewer implements Flushable {
/**
* @config
* @var string The used "theme", which usually consists of templates, images and stylesheets.
* @var string A list (highest priority first) of themes to use
* Only used when {@link $theme_enabled} is set to TRUE.
*/
private static $themes = [];
/**
* @deprecated 4.0..5.0
* @config
* @var string The used "theme", which usually consists of templates, images and stylesheets.
* Only used when {@link $theme_enabled} is set to TRUE, and $themes is empty
*/
private static $theme = null;
/**
@ -791,65 +809,33 @@ class SSViewer implements Flushable {
return $viewer;
}
public static function set_themes($themes = []) {
Config::inst()->remove('SSViewer', 'themes');
Config::inst()->update('SSViewer', 'themes', $themes);
}
public static function add_themes($themes = []) {
Config::inst()->update('SSViewer', 'themes', $themes);
}
public static function get_themes() {
$res = ['$default'];
if (Config::inst()->get('SSViewer', 'theme_enabled')) {
if ($list = Config::inst()->get('SSViewer', 'themes')) $res = $list;
elseif ($theme = Config::inst()->get('SSViewer', 'theme')) $res = [$theme, '$default'];
}
return $res;
}
/**
* @deprecated 4.0 Use the "SSViewer.theme" config setting instead
* @param string $theme The "base theme" name (without underscores).
*/
public static function set_theme($theme) {
Deprecation::notice('4.0', 'Use the "SSViewer.theme" config setting instead');
Config::inst()->update('SSViewer', 'theme', $theme);
}
/**
* @deprecated 4.0 Use the "SSViewer.theme" config setting instead
* @return string
*/
public static function current_theme() {
Deprecation::notice('4.0', 'Use the "SSViewer.theme" config setting instead');
return Config::inst()->get('SSViewer', 'theme');
}
/**
* Returns the path to the theme folder
*
* @return string
*/
public static function get_theme_folder() {
$theme = Config::inst()->get('SSViewer', 'theme');
return $theme ? THEMES_DIR . "/" . $theme : project();
}
/**
* Returns an array of theme names present in a directory.
*
* @param string $path
* @param bool $subthemes Include subthemes (default false).
* @return array
*/
public static function get_themes($path = null, $subthemes = false) {
$path = rtrim($path ? $path : THEMES_PATH, '/');
$themes = array();
if (!is_dir($path)) return $themes;
foreach (scandir($path) as $item) {
if ($item[0] != '.' && is_dir("$path/$item")) {
if ($subthemes || strpos($item, '_') === false) {
$themes[$item] = $item;
}
}
}
return $themes;
}
/**
* @deprecated since version 4.0
* @return string
*/
public static function current_custom_theme(){
Deprecation::notice('4.0', 'Use the "SSViewer.theme" and "SSViewer.theme_enabled" config settings instead');
return Config::inst()->get('SSViewer', 'theme_enabled') ? Config::inst()->get('SSViewer', 'theme') : null;
Deprecation::notice('4.0', 'Use the "SSViewer#set_themes" instead');
self::set_themes([$theme]);
}
/**
@ -873,6 +859,7 @@ class SSViewer implements Flushable {
foreach($classes as $class) {
$template = $class . $suffix;
if(SSViewer::hasTemplate($template)) $templates[] = $template;
elseif(SSViewer::hasTemplate('Includes/'.$template)) $templates[] = 'Includes/'.$template;
// If the class is "Page_Controller", look for Page.ss
if(stripos($class,'_controller') !== false) {
@ -893,38 +880,34 @@ class SSViewer implements Flushable {
* array('MySpecificPage', 'MyPage', 'Page')
* </code>
*/
public function __construct($templateList, TemplateParser $parser = null) {
public function __construct($templates, TemplateParser $parser = null) {
if ($parser) {
$this->setParser($parser);
}
if(!is_array($templateList) && substr((string) $templateList,-3) == '.ss') {
$this->chosenTemplates['main'] = $templateList;
} else {
if(Config::inst()->get('SSViewer', 'theme_enabled')) {
$theme = Config::inst()->get('SSViewer', 'theme');
} else {
$theme = null;
}
$this->chosenTemplates = SS_TemplateLoader::instance()->findTemplates(
$templateList, $theme
);
}
$this->setTemplate($templates);
if(!$this->chosenTemplates) {
$templateList = (is_array($templateList)) ? $templateList : array($templateList);
if(!$this->chosen) {
$message = 'None of the following templates could be found: ';
$message .= print_r($templates, true);
$message = 'None of the following templates could be found';
if(!$theme) {
$themes = self::get_themes();
if(!$themes) {
$message .= ' (no theme in use)';
} else {
$message .= ' in theme "' . $theme . '"';
$message .= ' in themes "' . print_r($themes, true) . '"';
}
user_error($message . ': ' . implode(".ss, ", $templateList) . ".ss", E_USER_WARNING);
user_error($message, E_USER_WARNING);
}
}
public function setTemplate($templates) {
$this->templates = $templates;
$this->chosen = TemplateLoader::instance()->findTemplate($templates, self::get_themes());
$this->subTemplates = [];
}
/**
* Set the template parser that will be used in template generation
* @param \TemplateParser $parser
@ -954,19 +937,7 @@ class SSViewer implements Flushable {
* @return boolean
*/
public static function hasTemplate($templates) {
$manifest = SS_TemplateLoader::instance()->getManifest();
if(Config::inst()->get('SSViewer', 'theme_enabled')) {
$theme = Config::inst()->get('SSViewer', 'theme');
} else {
$theme = null;
}
foreach ((array) $templates as $template) {
if ($manifest->getCandidateTemplate($template, $theme)) return true;
}
return false;
return (bool)TemplateLoader::instance()->findTemplate($templates, self::get_themes());
}
/**
@ -1033,7 +1004,7 @@ class SSViewer implements Flushable {
}
public function exists() {
return $this->chosenTemplates;
return $this->chosen;
}
/**
@ -1043,21 +1014,7 @@ class SSViewer implements Flushable {
* @return string Full system path to a template file
*/
public static function getTemplateFileByType($identifier, $type) {
$loader = SS_TemplateLoader::instance();
if(Config::inst()->get('SSViewer', 'theme_enabled')) {
$theme = Config::inst()->get('SSViewer', 'theme');
} else {
$theme = null;
}
$found = $loader->findTemplates("$type/$identifier", $theme);
if (isset($found['main'])) {
return $found['main'];
}
else if (!empty($found)) {
$founds = array_values($found);
return $founds[0];
}
return TemplateLoader::instance()->findTemplate(['type' => $type, $identifier], self::get_themes());
}
/**
@ -1192,13 +1149,7 @@ class SSViewer implements Flushable {
public function process($item, $arguments = null, $inheritedScope = null) {
SSViewer::$topLevel[] = $item;
if(isset($this->chosenTemplates['main'])) {
$template = $this->chosenTemplates['main'];
} else {
$keys = array_keys($this->chosenTemplates);
$key = reset($keys);
$template = $this->chosenTemplates[$key];
}
$template = $this->chosen;
$cacheFile = TEMP_FOLDER . "/.cache"
. str_replace(array('\\','/',':'), '.', Director::makeRelative(realpath($template)));
@ -1218,14 +1169,27 @@ class SSViewer implements Flushable {
// Makes the rendered sub-templates available on the parent item,
// through $Content and $Layout placeholders.
foreach(array('Content', 'Layout') as $subtemplate) {
if(isset($this->chosenTemplates[$subtemplate])) {
$sub = null;
if(isset($this->subTemplates[$subtemplate])) {
$sub = $this->subTemplates[$subtemplate];
}
elseif(!is_array($this->templates)) {
$sub = ['type' => $subtemplate, $this->templates];
}
elseif(!array_key_exists('type', $this->templates) || !$this->templates['type']) {
$sub = array_merge($this->templates, ['type' => $subtemplate]);
}
if ($sub) {
$subtemplateViewer = clone $this;
// Disable requirements - this will be handled by the parent template
$subtemplateViewer->includeRequirements(false);
// The subtemplate is the only file we want to process, so set it as the "main" template file
$subtemplateViewer->chosenTemplates = array('main' => $this->chosenTemplates[$subtemplate]);
// Select the right template
$subtemplateViewer->setTemplate($sub);
$underlay[$subtemplate] = $subtemplateViewer->process($item, $arguments);
if ($subtemplateViewer->exists()) {
$underlay[$subtemplate] = $subtemplateViewer->process($item, $arguments);
}
}
}
@ -1301,7 +1265,7 @@ class SSViewer implements Flushable {
* 'Content' & 'Layout', and will have to contain 'main'
*/
public function templates() {
return $this->chosenTemplates;
return array_merge(['main' => $this->chosen], $this->subTemplates);
}
/**
@ -1309,7 +1273,8 @@ class SSViewer implements Flushable {
* @param string $file Full system path to the template file
*/
public function setTemplateFile($type, $file) {
$this->chosenTemplates[$type] = $file;
if (!$type || $type == 'main') $this->chosen = $file;
else $this->subTemplates[$type] = $file;
}
/**

118
view/TemplateLoader.php Normal file
View File

@ -0,0 +1,118 @@
<?php
namespace SilverStripe\View;
/**
* Handles finding templates from a stack of template manifest objects.
*
* @package framework
* @subpackage view
*/
class TemplateLoader {
/**
* @var TemplateLoader
*/
private static $instance;
protected $base;
protected $sets = [];
public static function instance() {
return self::$instance ? self::$instance : self::$instance = new self();
}
public static function set_instance(TemplateLoader $instance) {
self::$instance = $instance;
}
public function __construct($base = null) {
$this->base = $base ? $base : BASE_PATH;
}
public function addSet($set, $manifest) {
$this->sets[$set] = $manifest;
}
public function getPath($identifier) {
$slashPos = strpos($identifier, '/');
// If identifier starts with "/", it's a path from root
if ($slashPos === 0) {
return substr($identifier, 1);
}
// Otherwise if there is a "/", identifier is a vendor'ed module
elseif ($slashPos !== false) {
$parts = explode(':', $identifier, 2);
list($vendor, $module) = explode('/', $parts[0], 2);
$theme = count($parts) > 1 ? $parts[1] : '';
$path = $module . ($theme ? '/themes/'.$theme : '');
// Right now we require $module to be a silverstripe module (in root) or theme (in themes dir)
// If both exist, we prefer theme
if (is_dir(THEMES_PATH . '/' .$path)) {
return THEMES_DIR . '/' . $path;
}
else {
return $path;
}
}
// Otherwise it's a (deprecated) old-style "theme" identifier
else {
return THEMES_DIR.'/'.$identifier;
}
}
/**
* Attempts to find possible candidate templates from a set of template
* names from modules, current theme directory and finally the application
* folder.
*
* The template names can be passed in as plain strings, or be in the
* format "type/name", where type is the type of template to search for
* (e.g. Includes, Layout).
*
* @param string|array $templates
* @param string $theme
*
* @return array
*/
public function findTemplate($template, $themes = []) {
if(is_array($template)) {
$type = array_key_exists('type', $template) ? $template['type'] : '';
$templateList = array_key_exists('templates', $template) ? $template['templates'] : $template;
}
else {
$type = '';
$templateList = array($template);
}
if(count($templateList) == 1 && substr($templateList[0], -3) == '.ss') {
return $templateList[0];
}
foreach($templateList as $i => $template) {
$template = str_replace('\\', '/', $template);
$parts = explode('/', $template);
$tail = array_pop($parts);
$head = implode('/', $parts);
foreach($themes as $themename) {
$subthemes = isset($this->sets[$themename]) ? $this->sets[$themename]->getThemes() : [$themename];
foreach($subthemes as $theme) {
$themePath = $this->base . '/' . $this->getPath($theme);
$path = $themePath . '/templates/' . implode('/', array_filter([$head, $type, $tail])) . '.ss';
if (file_exists($path)) return $path;
}
}
}
}
}

140
view/ThemeManifest.php Normal file
View File

@ -0,0 +1,140 @@
<?php
namespace SilverStripe\View;
use \ManifestFileFinder;
/**
* A class which builds a manifest of all themes (which is really just a directory called "templates")
*
* @package framework
* @subpackage manifest
*/
class ThemeManifest {
const TEMPLATES_DIR = 'templates';
protected $base;
protected $tests;
protected $project;
protected $cache;
protected $cacheKey;
protected $themes = null;
/**
* Constructs a new template manifest. The manifest is not actually built
* or loaded from cache until needed.
*
* @param string $base The base path.
* @param string $project Path to application code
*
* @param bool $includeTests Include tests in the manifest.
* @param bool $forceRegen Force the manifest to be regenerated.
*/
public function __construct($base, $project, $includeTests = false, $forceRegen = false) {
$this->base = $base;
$this->tests = $includeTests;
$this->project = $project;
$cacheClass = defined('SS_MANIFESTCACHE') ? SS_MANIFESTCACHE : 'ManifestCache_File';
$this->cache = new $cacheClass('thememanifest'.($includeTests ? '_tests' : ''));
$this->cacheKey = $this->getCacheKey();
if ($forceRegen) {
$this->regenerate();
}
}
/**
* @return string
*/
public function getBase() {
return $this->base;
}
/**
* Generate a unique cache key to avoid manifest cache collisions.
* We compartmentalise based on the base path, the given project, and whether
* or not we intend to include tests.
* @return string
*/
public function getCacheKey() {
return sha1(sprintf(
"manifest-%s-%s-%u",
$this->base,
$this->project,
$this->tests
));
}
/**
* Returns a map of all themes information. The map is in the following format:
*
* <code>
* [
* 'mysite',
* 'framework',
* 'framework/admin'
* ]
* </code>
*
* @return array
*/
public function getThemes() {
if ($this->themes === null) $this->init();
return $this->themes;
}
/**
* Regenerates the manifest by scanning the base path.
*
* @param bool $cache
*/
public function regenerate($cache = true) {
$finder = new ManifestFileFinder();
$finder->setOptions(array(
'include_themes' => false,
'ignore_tests' => !$this->tests,
'dir_callback' => array($this, 'handleDirectory')
));
$this->themes = [];
$finder->find($this->base);
if ($cache) {
$this->cache->save($this->themes, $this->cacheKey);
}
}
public function handleDirectory($basename, $pathname, $depth)
{
if ($basename == self::TEMPLATES_DIR) {
// We only want part of the full path, so split into directories
$parts = explode('/', $pathname);
// Take the end (the part relative to base), except the very last directory
$themeParts = array_slice($parts, -$depth, $depth-1);
// Then join again
$path = '/'.implode('/', $themeParts);
// If this is in the project, add to beginning of list. Else add to end.
if ($themeParts[0] == $this->project) {
array_unshift($this->themes, $path);
}
else {
array_push($this->themes, $path);
}
}
}
protected function init() {
if ($data = $this->cache->load($this->cacheKey)) {
$this->themes = $data;
} else {
$this->regenerate();
}
}
}