mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
394 lines
13 KiB
PHP
394 lines
13 KiB
PHP
<?php
|
|
|
|
namespace SilverStripe\View;
|
|
|
|
use InvalidArgumentException;
|
|
use Psr\SimpleCache\CacheInterface;
|
|
use SilverStripe\Core\Flushable;
|
|
use SilverStripe\Core\Injector\Injector;
|
|
use SilverStripe\Core\Manifest\ModuleLoader;
|
|
use SilverStripe\Core\Path;
|
|
|
|
/**
|
|
* Handles finding templates from a stack of template manifest objects.
|
|
*/
|
|
class ThemeResourceLoader implements Flushable
|
|
{
|
|
|
|
/**
|
|
* @var ThemeResourceLoader
|
|
*/
|
|
private static $instance;
|
|
|
|
/**
|
|
* The base path of the application
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $base;
|
|
|
|
/**
|
|
* List of template "sets" that contain a test manifest, and have an alias.
|
|
* E.g. '$default'
|
|
*
|
|
* @var ThemeList[]
|
|
*/
|
|
protected $sets = [];
|
|
|
|
/**
|
|
* @var CacheInterface
|
|
*/
|
|
protected $cache;
|
|
|
|
/**
|
|
* @return ThemeResourceLoader
|
|
*/
|
|
public static function inst()
|
|
{
|
|
return self::$instance ? self::$instance : self::$instance = new self();
|
|
}
|
|
|
|
/**
|
|
* Set instance
|
|
*
|
|
* @param ThemeResourceLoader $instance
|
|
*/
|
|
public static function set_instance(ThemeResourceLoader $instance)
|
|
{
|
|
self::$instance = $instance;
|
|
}
|
|
|
|
public function __construct($base = null)
|
|
{
|
|
$this->base = $base ? $base : BASE_PATH;
|
|
}
|
|
|
|
/**
|
|
* Add a new theme manifest for a given identifier. E.g. '$default'
|
|
*
|
|
* @param string $set
|
|
* @param ThemeList $manifest
|
|
*/
|
|
public function addSet($set, ThemeList $manifest)
|
|
{
|
|
$this->sets[$set] = $manifest;
|
|
}
|
|
|
|
/**
|
|
* Get a named theme set
|
|
*
|
|
* @param string $set
|
|
* @return ThemeList
|
|
*/
|
|
public function getSet($set)
|
|
{
|
|
if (isset($this->sets[$set])) {
|
|
return $this->sets[$set];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Given a theme identifier, determine the path from the root directory
|
|
*
|
|
* The mapping from $identifier to path follows these rules:
|
|
* - A simple theme name ('mytheme') which maps to the standard themes dir (/themes/mytheme)
|
|
* - A theme path with a leading slash ('/mymodule/themes/mytheme') which maps directly to that path.
|
|
* - or a vendored theme path. (vendor/mymodule:mytheme) which maps to the nested 'theme' within
|
|
* that module. ('/mymodule/themes/mytheme').
|
|
* - A vendored module with no nested theme (vendor/mymodule) which maps to the root directory
|
|
* of that module. ('/mymodule').
|
|
*
|
|
* @param string $identifier Theme identifier.
|
|
* @return string Path from root, not including leading or trailing forward slash. E.g. themes/mytheme
|
|
*/
|
|
public function getPath($identifier)
|
|
{
|
|
$slashPos = strpos($identifier, '/');
|
|
$parts = explode(':', $identifier, 2);
|
|
|
|
// If identifier starts with "/", it's a path from root
|
|
if ($slashPos === 0) {
|
|
if (count($parts) > 1) {
|
|
throw new InvalidArgumentException("Invalid theme identifier {$identifier}");
|
|
}
|
|
return Path::normalise($identifier, true);
|
|
}
|
|
|
|
// If there is no slash / colon it's a legacy theme
|
|
if ($slashPos === false && count($parts) === 1) {
|
|
return Path::join(THEMES_DIR, $identifier);
|
|
}
|
|
|
|
// Extract from <vendor>/<module>:<theme> format.
|
|
// <vendor> is optional, and if <theme> is omitted it defaults to the module root dir.
|
|
// If <theme> is included, this is the name of the directory under moduleroot/themes/
|
|
// which contains the theme.
|
|
// <module> is always the name of the install directory, not necessarily the composer name.
|
|
|
|
// Find module from first part
|
|
$moduleName = $parts[0];
|
|
$module = ModuleLoader::inst()->getManifest()->getModule($moduleName);
|
|
if ($module) {
|
|
$modulePath = $module->getRelativePath();
|
|
} else {
|
|
// If no module could be found, assume based on basename
|
|
// with a warning
|
|
if (strstr('/', $moduleName)) {
|
|
list(, $modulePath) = explode('/', $parts[0], 2);
|
|
} else {
|
|
$modulePath = $moduleName;
|
|
}
|
|
trigger_error("No module named {$moduleName} found. Assuming path {$modulePath}", E_USER_WARNING);
|
|
}
|
|
|
|
// Parse relative path for this theme within this module
|
|
$theme = count($parts) > 1 ? $parts[1] : '';
|
|
if (empty($theme)) {
|
|
// "module/vendor:"
|
|
// "module/vendor"
|
|
$subpath = '';
|
|
} elseif (strpos($theme, '/') === 0) {
|
|
// "module/vendor:/sub/path"
|
|
$subpath = rtrim($theme, '/');
|
|
} else {
|
|
// "module/vendor:subtheme"
|
|
$subpath = '/themes/' . $theme;
|
|
}
|
|
|
|
// Join module with subpath
|
|
return Path::normalise($modulePath . $subpath, true);
|
|
}
|
|
|
|
/**
|
|
* 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).
|
|
*
|
|
* The results of this method will be cached for future use.
|
|
*
|
|
* @param string|array $template Template name, or template spec in array format with the keys
|
|
* 'type' (type string) and 'templates' (template hierarchy in order of precedence).
|
|
* If 'templates' is ommitted then any other item in the array will be treated as the template
|
|
* list, or list of templates each in the array spec given.
|
|
* Templates with an .ss extension will be treated as file paths, and will bypass
|
|
* theme-coupled resolution.
|
|
* @param array $themes List of themes to use to resolve themes. Defaults to {@see SSViewer::get_themes()}
|
|
* @return string Absolute path to resolved template file, or null if not resolved.
|
|
* File location will be in the format themes/<theme>/templates/<directories>/<type>/<basename>.ss
|
|
* Note that type (e.g. 'Layout') is not the root level directory under 'templates'.
|
|
*/
|
|
public function findTemplate($template, $themes = null)
|
|
{
|
|
if ($themes === null) {
|
|
$themes = SSViewer::get_themes();
|
|
}
|
|
|
|
// Look for a cached result for this data set
|
|
$cacheKey = md5(json_encode($template) . json_encode($themes));
|
|
if ($this->getCache()->has($cacheKey)) {
|
|
return $this->getCache()->get($cacheKey);
|
|
}
|
|
|
|
$type = '';
|
|
if (is_array($template)) {
|
|
// Check if templates has type specified
|
|
if (array_key_exists('type', $template)) {
|
|
$type = $template['type'];
|
|
unset($template['type']);
|
|
}
|
|
// Templates are either nested in 'templates' or just the rest of the list
|
|
$templateList = array_key_exists('templates', $template) ? $template['templates'] : $template;
|
|
} else {
|
|
$templateList = [$template];
|
|
}
|
|
|
|
foreach ($templateList as $i => $template) {
|
|
// Check if passed list of templates in array format
|
|
if (is_array($template)) {
|
|
$path = $this->findTemplate($template, $themes);
|
|
if ($path) {
|
|
$this->getCache()->set($cacheKey, $path);
|
|
return $path;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// If we have an .ss extension, this is a path, not a template name. We should
|
|
// pass in templates without extensions in order for template manifest to find
|
|
// files dynamically.
|
|
if (substr($template, -3) == '.ss' && file_exists($template)) {
|
|
$this->getCache()->set($cacheKey, $template);
|
|
return $template;
|
|
}
|
|
|
|
// Check string template identifier
|
|
$template = str_replace('\\', '/', $template);
|
|
$parts = explode('/', $template);
|
|
|
|
$tail = array_pop($parts);
|
|
$head = implode('/', $parts);
|
|
$themePaths = $this->getThemePaths($themes);
|
|
foreach ($themePaths as $themePath) {
|
|
// Join path
|
|
$pathParts = [ $this->base, $themePath, 'templates', $head, $type, $tail ];
|
|
try {
|
|
$path = Path::join($pathParts) . '.ss';
|
|
if (file_exists($path)) {
|
|
$this->getCache()->set($cacheKey, $path);
|
|
return $path;
|
|
}
|
|
} catch (InvalidArgumentException $e) {
|
|
// No-op
|
|
}
|
|
}
|
|
}
|
|
|
|
// No template found
|
|
$this->getCache()->set($cacheKey, null);
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Resolve themed CSS path
|
|
*
|
|
* @param string $name Name of CSS file without extension
|
|
* @param array $themes List of themes, Defaults to {@see SSViewer::get_themes()}
|
|
* @return string Path to resolved CSS file (relative to base dir)
|
|
*/
|
|
public function findThemedCSS($name, $themes = null)
|
|
{
|
|
if ($themes === null) {
|
|
$themes = SSViewer::get_themes();
|
|
}
|
|
|
|
if (substr($name, -4) !== '.css') {
|
|
$name .= '.css';
|
|
}
|
|
|
|
$filename = $this->findThemedResource("css/$name", $themes);
|
|
if ($filename === null) {
|
|
$filename = $this->findThemedResource($name, $themes);
|
|
}
|
|
|
|
return $filename;
|
|
}
|
|
|
|
/**
|
|
* Resolve themed javascript path
|
|
*
|
|
* A javascript file in the current theme path name 'themename/javascript/$name.js' is first searched for,
|
|
* and it that doesn't exist and the module parameter is set then a javascript file with that name in
|
|
* the module is used.
|
|
*
|
|
* @param string $name The name of the file - eg '/js/File.js' would have the name 'File'
|
|
* @param array $themes List of themes, Defaults to {@see SSViewer::get_themes()}
|
|
* @return string Path to resolved javascript file (relative to base dir)
|
|
*/
|
|
public function findThemedJavascript($name, $themes = null)
|
|
{
|
|
if ($themes === null) {
|
|
$themes = SSViewer::get_themes();
|
|
}
|
|
|
|
if (substr($name, -3) !== '.js') {
|
|
$name .= '.js';
|
|
}
|
|
|
|
$filename = $this->findThemedResource("javascript/$name", $themes);
|
|
if ($filename === null) {
|
|
$filename = $this->findThemedResource($name, $themes);
|
|
}
|
|
|
|
return $filename;
|
|
}
|
|
|
|
/**
|
|
* Resolve a themed resource
|
|
*
|
|
* A themed resource and be any file that resides in a theme folder.
|
|
*
|
|
* @param string $resource A file path relative to the root folder of a theme
|
|
* @param array $themes An order listed of themes to search, Defaults to {@see SSViewer::get_themes()}
|
|
* @return string
|
|
*/
|
|
public function findThemedResource($resource, $themes = null)
|
|
{
|
|
if ($themes === null) {
|
|
$themes = SSViewer::get_themes();
|
|
}
|
|
|
|
$paths = $this->getThemePaths($themes);
|
|
|
|
foreach ($paths as $themePath) {
|
|
$relativePath = Path::join($themePath, $resource);
|
|
$absolutePath = Path::join($this->base, $relativePath);
|
|
if (file_exists($absolutePath)) {
|
|
return $relativePath;
|
|
}
|
|
}
|
|
|
|
// Resource exists in no context
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Resolve all themes to the list of root folders relative to site root
|
|
*
|
|
* @param array $themes List of themes to resolve. Supports named theme sets. Defaults to {@see SSViewer::get_themes()}.
|
|
* @return array List of root-relative folders in order of precendence.
|
|
*/
|
|
public function getThemePaths($themes = null)
|
|
{
|
|
if ($themes === null) {
|
|
$themes = SSViewer::get_themes();
|
|
}
|
|
|
|
$paths = [];
|
|
foreach ($themes as $themename) {
|
|
// Expand theme sets
|
|
$set = $this->getSet($themename);
|
|
$subthemes = $set ? $set->getThemes() : [$themename];
|
|
|
|
// Resolve paths
|
|
foreach ($subthemes as $theme) {
|
|
$paths[] = $this->getPath($theme);
|
|
}
|
|
}
|
|
return $paths;
|
|
}
|
|
|
|
/**
|
|
* Flush any cached data
|
|
*/
|
|
public static function flush()
|
|
{
|
|
self::inst()->getCache()->clear();
|
|
}
|
|
|
|
/**
|
|
* @return CacheInterface
|
|
*/
|
|
public function getCache()
|
|
{
|
|
if (!$this->cache) {
|
|
$this->setCache(Injector::inst()->get(CacheInterface::class . '.ThemeResourceLoader'));
|
|
}
|
|
return $this->cache;
|
|
}
|
|
|
|
/**
|
|
* @param CacheInterface $cache
|
|
* @return ThemeResourceLoader
|
|
*/
|
|
public function setCache(CacheInterface $cache)
|
|
{
|
|
$this->cache = $cache;
|
|
return $this;
|
|
}
|
|
}
|