silverstripe-framework/core/manifest/TemplateManifest.php
Sam Minnee 65eb0bde6a API: Look for templates of namespaced classes in subfolders.
This change will mean that SilverStripe\Control\Controller will look for its
template in templates/SilverStripe/Control/Controller.ss.

In order to preserve some backwards campatibility, non-namespaced classes
can have the templates stored in any template subfolder, but once you
add a namespace to a class, the namespaced path expression will need to
be a subfolder of <module>/templates or themes/<theme>/templates.

Layout and Content templates are stil supported as special template type,
Includes still functions but is a no-op. Other template subfolders should
not be used.
2016-06-07 14:36:00 +12:00

272 lines
6.8 KiB
PHP

<?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();
}
}
}