diff --git a/_config/modules.yml b/_config/modules.yml
new file mode 100644
index 000000000..1c5f6fa6e
--- /dev/null
+++ b/_config/modules.yml
@@ -0,0 +1,28 @@
+---
+Name: coremodulesorter
+---
+SilverStripe\Core\Injector\Injector:
+ SilverStripe\Core\Manifest\PrioritySorter.modulesorter:
+ class: SilverStripe\Core\Manifest\PrioritySorter
+ properties:
+ RestKey: $other_modules
+---
+Name: modules-other
+---
+SilverStripe\Core\Manifest\ModuleManifest:
+ module_priority:
+ - $other_modules
+---
+Name: modules-project
+Before: '*'
+---
+SilverStripe\Core\Manifest\ModuleManifest:
+ module_priority:
+ - $project
+---
+Name: modules-framework
+After: modules-other
+---
+SilverStripe\Core\Manifest\ModuleManifest:
+ module_priority:
+ - silverstripe\framework
diff --git a/docs/en/02_Developer_Guides/01_Templates/05_Template_Inheritance.md b/docs/en/02_Developer_Guides/01_Templates/05_Template_Inheritance.md
index e8cc58a8d..54508672d 100644
--- a/docs/en/02_Developer_Guides/01_Templates/05_Template_Inheritance.md
+++ b/docs/en/02_Developer_Guides/01_Templates/05_Template_Inheritance.md
@@ -43,20 +43,59 @@ The manifest is created whenever you flush your SilverStripe cache by appending
example by visiting `http://yoursite.com/?flush=1`. When your include the `flush=1` flag, the manifest class will search
your entire project for the appropriate `.ss` files located in `template` directory and save that information for later.
-It will each and prioritize templates in the following priority:
-
-1. mysite (or other name given to site folder)
-2. module-specific themes (e.g. themes/simple_blog)
-3. themes (e.g. themes/simple)
-4. modules (e.g. blog)
-5. framework
-
Whenever you add or remove template files, rebuild the manifest by visiting `http://yoursite.com/?flush=1`. You can
flush the cache from any page, (.com/home?flush=1, .com/admin?flush=1, etc.). Flushing the cache can be slow, so you
only need to do it when you're developing new templates.
+## Template Priority
+
+The order in which templates are selected from themes can be explicitly declared
+through configuration. To specify the order you want, make a list of the module
+names under `SilverStripe\Core\Manifest\ModuleManifest.module_priority` in a
+configuration YAML file.
+
+
+*some-module/_config.yml*
+```yml
+SilverStripe\Core\Manifest\ModuleManifest:
+ module_priority:
+ - 'example/module-one'
+ - 'example/module-two'
+ - '$other_modules'
+ - 'example/module-three'
+```
+
+The placeholder `$other_modules` is used to mark where all of the modules not specified
+in the list should appear. (In alphabetical order of their containing directory names).
+
+In this example, the module named `example/module-one` has the highest level of precedence,
+followed by `example/module-two`. The module `example/module-three` is guaranteed the lowest
+level of precedence.
+
+### Defining a "project"
+
+It is a good idea to define one of your modules as the `project`. Commonly, this is the
+`mysite/` module, but there is nothing compulsory about that module name. The "project"
+module can be specified as a variable in the `module_priorities` list, as well.
+
+*some-module/_config.yml*
+```yml
+SilverStripe\Core\Manifest\ModuleManifest:
+ project: 'myapp'
+ module_priority:
+ - '$project'
+ - '$other_modules'
+```
+
+### About module "names"
+
+Module names are derived their local `composer.json` files using the following precedence:
+* The value of the `name` attribute in `composer.json`
+* The value of `extras.installer_name` in `composer.json`
+* The basename of the directory that contains the module
+
## Nested Layouts through `$Layout`
SilverStripe has basic support for nested layouts through a fixed template variable named `$Layout`. It's used for
diff --git a/docs/en/02_Developer_Guides/16_Execution_Pipeline/03_App_Object_and_Kernel.md b/docs/en/02_Developer_Guides/16_Execution_Pipeline/03_App_Object_and_Kernel.md
index 311b0164d..baf2cb2dc 100644
--- a/docs/en/02_Developer_Guides/16_Execution_Pipeline/03_App_Object_and_Kernel.md
+++ b/docs/en/02_Developer_Guides/16_Execution_Pipeline/03_App_Object_and_Kernel.md
@@ -50,7 +50,7 @@ you should call `->activate()` on the kernel instance you would like to unnest t
# Application
-An application represents a basic excution controller for the top level application entry point.
+An application represents a basic execution controller for the top level application entry point.
The role of the application is to:
- Control bootstrapping of a provided kernel instance
diff --git a/src/Core/CoreKernel.php b/src/Core/CoreKernel.php
index 140d248af..5db708329 100644
--- a/src/Core/CoreKernel.php
+++ b/src/Core/CoreKernel.php
@@ -26,6 +26,7 @@ use SilverStripe\Logging\ErrorHandler;
use SilverStripe\ORM\DB;
use SilverStripe\View\ThemeManifest;
use SilverStripe\View\ThemeResourceLoader;
+use SilverStripe\Dev\Deprecation;
/**
* Simple Kernel container
@@ -116,7 +117,7 @@ class CoreKernel implements Kernel
$themeResourceLoader = ThemeResourceLoader::inst();
$themeResourceLoader->addSet('$default', new ThemeManifest(
$basePath,
- project(),
+ null, // project is defined in config, and this argument is deprecated
$manifestCacheFactory
));
$this->setThemeResourceLoader($themeResourceLoader);
@@ -196,8 +197,15 @@ class CoreKernel implements Kernel
*/
protected function bootConfigs()
{
+ global $project;
+ $projectBefore = $project;
+ $config = ModuleManifest::config();
// After loading all other app manifests, include _config.php files
$this->getModuleLoader()->getManifest()->activateConfig();
+ if ($project && $project !== $projectBefore) {
+ Deprecation::notice('5.0', '$project global is deprecated');
+ $config->set('project', $project);
+ }
}
/**
@@ -498,10 +506,15 @@ class CoreKernel implements Kernel
$config->setFlush(true);
}
}
+ // tell modules to sort, now that config is available
+ $this->getModuleLoader()->getManifest()->sort();
// Find default templates
$defaultSet = $this->getThemeResourceLoader()->getSet('$default');
if ($defaultSet instanceof ThemeManifest) {
+ $defaultSet->setProject(
+ ModuleManifest::config()->get('project')
+ );
$defaultSet->init($this->getIncludeTests(), $flush);
}
}
diff --git a/src/Core/Manifest/ClassManifest.php b/src/Core/Manifest/ClassManifest.php
index 6fd04c03b..2a48d4ee5 100644
--- a/src/Core/Manifest/ClassManifest.php
+++ b/src/Core/Manifest/ClassManifest.php
@@ -323,27 +323,8 @@ class ClassManifest
*/
public function getOwnerModule($class)
{
- $path = realpath($this->getItemPath($class));
- if (!$path) {
- return null;
- }
-
- /** @var Module $rootModule */
- $rootModule = null;
-
- // Find based on loaded modules
- $modules = ModuleLoader::inst()->getManifest()->getModules();
- foreach ($modules as $module) {
- // Leave root module as fallback
- if (empty($module->getRelativePath())) {
- $rootModule = $module;
- } elseif (stripos($path, realpath($module->getPath())) === 0) {
- return $module;
- }
- }
-
- // Fall back to top level module
- return $rootModule;
+ $path = $this->getItemPath($class);
+ return ModuleLoader::inst()->getManifest()->getModuleByPath($path);
}
/**
diff --git a/src/Core/Manifest/ModuleManifest.php b/src/Core/Manifest/ModuleManifest.php
index 04abc183e..c208868a5 100644
--- a/src/Core/Manifest/ModuleManifest.php
+++ b/src/Core/Manifest/ModuleManifest.php
@@ -5,12 +5,18 @@ namespace SilverStripe\Core\Manifest;
use LogicException;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Cache\CacheFactory;
+use SilverStripe\Core\Config\Configurable;
+use SilverStripe\Core\Injector\Injector;
/**
* A utility class which builds a manifest of configuration items
*/
class ModuleManifest
{
+ use Configurable;
+
+ const PROJECT_KEY = '$project';
+
/**
* The base path used when building the manifest
*
@@ -42,7 +48,7 @@ class ModuleManifest
*
* @var Module[]
*/
- protected $modules = array();
+ protected $modules = [];
/**
* Adds a path as a module
@@ -105,8 +111,8 @@ class ModuleManifest
// build cache from factory
if ($this->cacheFactory) {
$this->cache = $this->cacheFactory->create(
- CacheInterface::class.'.modulemanifest',
- [ 'namespace' => 'modulemanifest' . ($includeTests ? '_tests' : '') ]
+ CacheInterface::class . '.modulemanifest',
+ ['namespace' => 'modulemanifest' . ($includeTests ? '_tests' : '')]
);
}
@@ -124,7 +130,10 @@ class ModuleManifest
*/
public function activateConfig()
{
- foreach ($this->getModules() as $module) {
+ $modules = $this->getModules();
+ // Work in reverse priority, so the higher priority modules get later execution
+ /** @var Module $module */
+ foreach (array_reverse($modules) as $module) {
$module->activate();
}
}
@@ -145,20 +154,20 @@ class ModuleManifest
$finder = new ManifestFileFinder();
$finder->setOptions(array(
'min_depth' => 0,
- 'name_regex' => '/(^|[\/\\\\])_config.php$/',
- 'ignore_tests' => !$includeTests,
+ 'name_regex' => '/(^|[\/\\\\])_config.php$/',
+ 'ignore_tests' => !$includeTests,
'file_callback' => array($this, 'addSourceConfigFile'),
// Cannot be max_depth: 1 due to "/framework/admin/_config.php"
- 'max_depth' => 2
+ 'max_depth' => 2
));
$finder->find($this->base);
$finder = new ManifestFileFinder();
$finder->setOptions(array(
- 'name_regex' => '/\.ya?ml$/',
- 'ignore_tests' => !$includeTests,
+ 'name_regex' => '/\.ya?ml$/',
+ 'ignore_tests' => !$includeTests,
'file_callback' => array($this, 'addYAMLConfigFile'),
- 'max_depth' => 2
+ 'max_depth' => 2
));
$finder->find($this->base);
@@ -225,4 +234,66 @@ class ModuleManifest
{
return $this->modules;
}
+
+ /**
+ * Sort modules sorted by priority
+ *
+ * @return void
+ */
+ public function sort()
+ {
+ $order = static::config()->uninherited('module_priority');
+ $project = static::config()->get('project');
+ /* @var PrioritySorter $sorter */
+ $sorter = Injector::inst()->createWithArgs(
+ PrioritySorter::class . '.modulesorter',
+ [
+ $this->modules,
+ $order ?: []
+ ]
+ );
+
+ if ($project) {
+ $sorter->setVariable(self::PROJECT_KEY, $project);
+ }
+
+ $this->modules = $sorter->getSortedList();
+ }
+
+ /**
+ * Get module that contains the given path
+ *
+ * @param string $path Full filesystem path to the given file
+ * @return Module The module, or null if not a path in any module
+ */
+ public function getModuleByPath($path)
+ {
+ // Ensure path exists
+ $path = realpath($path);
+ if (!$path) {
+ return null;
+ }
+
+ /** @var Module $rootModule */
+ $rootModule = null;
+
+ // Find based on loaded modules
+ $modules = ModuleLoader::inst()->getManifest()->getModules();
+ foreach ($modules as $module) {
+ // Check if path is in module
+ if (stripos($path, realpath($module->getPath())) !== 0) {
+ continue;
+ }
+
+ // If this is the root module, keep looking in case there is a more specific module later
+ if (empty($module->getRelativePath())) {
+ $rootModule = $module;
+ } else {
+ return $module;
+ }
+ }
+
+ // Fall back to top level module
+ return $rootModule;
+ }
}
diff --git a/src/Core/Manifest/PrioritySorter.php b/src/Core/Manifest/PrioritySorter.php
new file mode 100644
index 000000000..8ac39aee6
--- /dev/null
+++ b/src/Core/Manifest/PrioritySorter.php
@@ -0,0 +1,212 @@
+ new Product(...),
+ * 'product-two' => new Product(...),
+ * 'product-three' => new Product(...),
+ * 'product-four' => new Product(...),
+ * ];
+ *
+ * $priorities = [
+ * '$featured',
+ * 'product-two',
+ * '...rest',
+ * ];
+ *
+ * $sorter = new PrioritySorter($items, $priorities);
+ * $sorter->setVariable('$featured', 'product-three');
+ * $sorter->getSortedList();
+ *
+ * [
+ * 'product-three' => [object] Product,
+ * 'product-two' => [object] Product,
+ * 'product-one' => [object] Product,
+ * 'product-four' => [object] Product
+ * ]
+ *
+ */
+class PrioritySorter
+{
+ use Injectable;
+
+ /**
+ * The key that is used to denote all remaining items that have not
+ * been specified in priorities
+ * @var string
+ */
+ protected $restKey = '...rest';
+
+ /**
+ * A map of variables to their values
+ * @var array
+ */
+ protected $variables = [];
+
+ /**
+ * An associative array of items, whose keys can be used in the $priorities list
+ *
+ * @var array
+ */
+ protected $items;
+
+ /**
+ * An indexed array of keys in the $items list, reflecting the desired sort
+ *
+ * @var array
+ */
+ protected $priorities;
+
+ /**
+ * The keys of the $items array
+ *
+ * @var array
+ */
+ protected $names;
+
+ /**
+ * PrioritySorter constructor.
+ * @param array $items
+ * @param array $priorities
+ */
+ public function __construct(array $items = [], array $priorities = [])
+ {
+ $this->setItems($items);
+ $this->priorities = $priorities;
+ }
+
+ /**
+ * Sorts the items and returns a new version of $this->items
+ *
+ * @return array
+ */
+ public function getSortedList()
+ {
+ $this->addVariables();
+
+ // Find all items that don't have their order specified by the config system
+ $unspecified = array_diff($this->names, $this->priorities);
+
+ if (!empty($unspecified)) {
+ $this->includeRest($unspecified);
+ }
+
+ $sortedList = [];
+ foreach ($this->priorities as $itemName) {
+ if (isset($this->items[$itemName])) {
+ $sortedList[$itemName] = $this->items[$itemName];
+ }
+ }
+
+ return $sortedList;
+ }
+
+ /**
+ * Set the priorities for the items
+ *
+ * @param array $priorities An array of keys used in $this->items
+ * @return $this
+ */
+ public function setPriorities(array $priorities)
+ {
+ $this->priorities = $priorities;
+
+ return $this;
+ }
+
+ /**
+ * Sets the list of all items
+ *
+ * @param array $items
+ * @return $this
+ */
+ public function setItems(array $items)
+ {
+ $this->items = $items;
+ $this->names = array_keys($items);
+
+ return $this;
+ }
+
+ /**
+ * Add a variable for replacination, e.g. addVariable->('$project', 'myproject')
+ *
+ * @param string $name
+ * @param $value
+ * @return $this
+ */
+ public function setVariable($name, $value)
+ {
+ $this->variables[$name] = $value;
+
+ return $this;
+ }
+
+ /**
+ * The key used for "all other items"
+ *
+ * @param $key
+ * @return $this
+ */
+ public function setRestKey($key)
+ {
+ $this->restKey = $key;
+
+ return $this;
+ }
+
+ /**
+ * If variables are defined, interpolate their values
+ */
+ protected function addVariables()
+ {
+ // Remove variables from the list
+ $varValues = array_values($this->variables);
+ $this->names = array_filter($this->names, function ($name) use ($varValues) {
+ return !in_array($name, $varValues);
+ });
+
+ // Replace variables with their values
+ $this->priorities = array_map(function ($name) {
+ return $this->resolveValue($name);
+ }, $this->priorities);
+ }
+
+ /**
+ * If the "rest" key exists in the order array,
+ * replace it by the unspecified items
+ */
+ protected function includeRest(array $list)
+ {
+ $otherItemsIndex = false;
+ if ($this->restKey) {
+ $otherItemsIndex = array_search($this->restKey, $this->priorities);
+ }
+ if ($otherItemsIndex !== false) {
+ array_splice($this->priorities, $otherItemsIndex, 1, $list);
+ } else {
+ // Otherwise just jam them on the end
+ $this->priorities = array_merge($this->priorities, $list);
+ }
+ }
+
+ /**
+ * Ensure variables get converted to their values
+ *
+ * @param $name
+ * @return mixed
+ */
+ protected function resolveValue($name)
+ {
+ return isset($this->variables[$name]) ? $this->variables[$name] : $name;
+ }
+}
diff --git a/src/Dev/Deprecation.php b/src/Dev/Deprecation.php
index 0c3264944..fbb350c7c 100644
--- a/src/Dev/Deprecation.php
+++ b/src/Dev/Deprecation.php
@@ -108,13 +108,7 @@ class Deprecation
$callingfile = realpath($backtrace[1]['file']);
- $modules = ModuleLoader::inst()->getManifest()->getModules();
- foreach ($modules as $module) {
- if (strpos($callingfile, realpath($module->getPath())) === 0) {
- return $module;
- }
- }
- return null;
+ return ModuleLoader::inst()->getManifest()->getModuleByPath($callingfile);
}
/**
diff --git a/src/Forms/HTMLEditor/TinyMCEConfig.php b/src/Forms/HTMLEditor/TinyMCEConfig.php
index 0b8f7c118..174cfa240 100644
--- a/src/Forms/HTMLEditor/TinyMCEConfig.php
+++ b/src/Forms/HTMLEditor/TinyMCEConfig.php
@@ -5,7 +5,9 @@ namespace SilverStripe\Forms\HTMLEditor;
use SilverStripe\Core\Convert;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
+use SilverStripe\Core\Manifest\Module;
use SilverStripe\Core\Manifest\ModuleLoader;
+use SilverStripe\Dev\Deprecation;
use SilverStripe\i18n\i18n;
use SilverStripe\View\Requirements;
use SilverStripe\View\SSViewer;
@@ -191,13 +193,23 @@ class TinyMCEConfig extends HTMLEditorConfig
* - themes
* - skins
*
- * If left blank defaults to [admin dir]/tinyme
+ * Supports vendor/module:path
*
* @config
* @var string
*/
private static $base_dir = null;
+ /**
+ * Extra editor.css file paths.
+ *
+ * Supports vendor/module:path syntax
+ *
+ * @config
+ * @var array
+ */
+ private static $editor_css = [];
+
/**
* TinyMCE JS settings
*
@@ -555,7 +567,7 @@ class TinyMCEConfig extends HTMLEditorConfig
$settings['document_base_url'] = Director::absoluteBaseURL();
// https://www.tinymce.com/docs/api/class/tinymce.editormanager/#baseURL
- $tinyMCEBaseURL = $this->getAdminModule()->getResourceURL('thirdparty/tinymce');
+ $tinyMCEBaseURL = Controller::join_links(Director::baseURL(), $this->getTinyMCEPath());
$settings['baseURL'] = $tinyMCEBaseURL;
// map all plugins to absolute urls for loading
@@ -615,13 +627,15 @@ class TinyMCEConfig extends HTMLEditorConfig
$editor = array();
// Add standard editor.css
- $editor[] = $this->getAdminModule()->getResourceURL('client/dist/styles/editor.css');
+ foreach ($this->config()->get('editor_css') as $editorCSS) {
+ $editor[] = Director::absoluteURL($this->resolvePath($editorCSS));
+ }
// Themed editor.css
$themes = $this->config()->get('user_themes') ?: SSViewer::get_themes();
$themedEditor = ThemeResourceLoader::inst()->findThemedCSS('editor', $themes);
if ($themedEditor) {
- $editor[] = Director::absoluteURL($themedEditor, Director::BASE);
+ $editor[] = Director::absoluteURL($themedEditor);
}
return $editor;
@@ -685,32 +699,46 @@ class TinyMCEConfig extends HTMLEditorConfig
}
/**
- * @return string|false
+ * @return string
* @throws Exception
*/
public function getTinyMCEPath()
{
$configDir = static::config()->get('base_dir');
if ($configDir) {
- return $configDir;
- }
-
- if ($admin = $this->getAdminModule()) {
- return $admin->getRelativeResourcePath('thirdparty/tinymce');
+ return $this->resolvePath($configDir);
}
throw new Exception(sprintf(
- 'If the silverstripe/admin module is not installed,
- you must set the TinyMCE path in %s.base_dir',
+ 'If the silverstripe/admin module is not installed you must set the TinyMCE path in %s.base_dir',
__CLASS__
));
}
/**
- * @return \SilverStripe\Core\Manifest\Module
+ * @return Module
+ * @deprecated 4.0..5.0
*/
protected function getAdminModule()
{
+ Deprecation::notice('5.0', 'Set base_dir or editor_css config instead');
return ModuleLoader::getModule('silverstripe/admin');
}
+
+ /**
+ * Expand resource path to a relative filesystem path
+ *
+ * @param string $path
+ * @return string
+ */
+ protected function resolvePath($path)
+ {
+ if (preg_match('#(?[^/]+/[^/]+)\s*:\s*(?[^:]+)#', $path, $results)) {
+ $module = ModuleLoader::getModule($results['module']);
+ if ($module) {
+ return $module->getRelativeResourcePath($results['path']);
+ }
+ }
+ return $path;
+ }
}
diff --git a/src/View/ThemeManifest.php b/src/View/ThemeManifest.php
index a2fa87195..7c782a424 100644
--- a/src/View/ThemeManifest.php
+++ b/src/View/ThemeManifest.php
@@ -5,6 +5,7 @@ namespace SilverStripe\View;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Cache\CacheFactory;
use SilverStripe\Core\Manifest\ManifestFileFinder;
+use SilverStripe\Core\Manifest\ModuleLoader;
/**
* A class which builds a manifest of all themes (which is really just a directory called "templates")
@@ -62,7 +63,7 @@ class ThemeManifest implements ThemeList
* @param string $project Path to application code
* @param CacheFactory $cacheFactory Cache factory to generate backend cache with
*/
- public function __construct($base, $project, CacheFactory $cacheFactory = null)
+ public function __construct($base, $project = null, CacheFactory $cacheFactory = null)
{
$this->base = $base;
$this->project = $project;
@@ -116,6 +117,9 @@ class ThemeManifest implements ThemeList
));
}
+ /**
+ * @return \string[]
+ */
public function getThemes()
{
return $this->themes;
@@ -137,7 +141,11 @@ class ThemeManifest implements ThemeList
));
$this->themes = [];
- $finder->find($this->base);
+
+ $modules = ModuleLoader::inst()->getManifest()->getModules();
+ foreach ($modules as $module) {
+ $finder->find($module->getPath());
+ }
if ($this->cache) {
$this->cache->set($this->cacheKey, $this->themes);
@@ -156,19 +164,20 @@ class ThemeManifest implements ThemeList
if ($basename !== self::TEMPLATES_DIR) {
return;
}
+ $dir = trim(substr(dirname($pathname), strlen($this->base)), '/\\');
+ $this->themes[] = "/".$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);
+ /**
+ * Sets the project
+ *
+ * @param string $project
+ * @return $this
+ */
+ public function setProject($project)
+ {
+ $this->project = $project;
- // If this is in the project, add to beginning of list. Else add to end.
- if ($themeParts && $themeParts[0] == $this->project) {
- array_unshift($this->themes, $path);
- } else {
- array_push($this->themes, $path);
- }
+ return $this;
}
}
diff --git a/src/View/ThemeResourceLoader.php b/src/View/ThemeResourceLoader.php
index fe3ecf48c..95c6f0b55 100644
--- a/src/View/ThemeResourceLoader.php
+++ b/src/View/ThemeResourceLoader.php
@@ -15,6 +15,11 @@ class ThemeResourceLoader
*/
private static $instance;
+ /**
+ * The base path of the application
+ *
+ * @var string
+ */
protected $base;
/**
@@ -201,7 +206,6 @@ class ThemeResourceLoader
$tail = array_pop($parts);
$head = implode('/', $parts);
-
$themePaths = $this->getThemePaths($themes);
foreach ($themePaths as $themePath) {
// Join path
diff --git a/src/i18n/Data/Sources.php b/src/i18n/Data/Sources.php
index ce269df13..b64d112c2 100644
--- a/src/i18n/Data/Sources.php
+++ b/src/i18n/Data/Sources.php
@@ -5,10 +5,12 @@ namespace SilverStripe\i18n\Data;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Manifest\ModuleLoader;
+use SilverStripe\Core\Manifest\ModuleManifest;
use SilverStripe\Core\Resettable;
use SilverStripe\i18n\i18n;
use SilverStripe\View\SSViewer;
use SilverStripe\View\ThemeResourceLoader;
+use SilverStripe\Dev\Deprecation;
/**
* Data sources for localisation strings. I.e. yml files stored
@@ -34,43 +36,21 @@ class Sources implements Resettable
*/
public function getSortedModules()
{
- // Get list of module => path pairs, and then just the names
- $modules = ModuleLoader::inst()->getManifest()->getModules();
- $moduleNames = array_keys($modules);
-
- // Remove the "project" module from the list - we'll add it back specially later if needed
- global $project;
- if (($idx = array_search($project, $moduleNames)) !== false) {
- array_splice($moduleNames, $idx, 1);
+ $i18nOrder = Sources::config()->uninherited('module_priority');
+ $sortedModules = [];
+ if ($i18nOrder) {
+ Deprecation::notice('5.0', sprintf(
+ '%s.module_priority is deprecated. Use %s.module_priority instead.',
+ __CLASS__,
+ ModuleManifest::class
+ ));
}
- // Get the order from the config system (lowest to highest)
- $order = Sources::config()->uninherited('module_priority');
+ foreach (ModuleLoader::inst()->getManifest()->getModules() as $module) {
+ $sortedModules[$module->getName()] = $module->getPath();
+ };
- // Find all modules that don't have their order specified by the config system
- $unspecified = array_diff($moduleNames, $order);
-
- // If the placeholder "other_modules" exists in the order array, replace it by the unspecified modules
- if (($idx = array_search('other_modules', $order)) !== false) {
- array_splice($order, $idx, 1, $unspecified);
- } else {
- // Otherwise just jam them on the front
- array_splice($order, 0, 0, $unspecified);
- }
-
- // Put the project at end (highest priority)
- if (!in_array($project, $order)) {
- $order[] = $project;
- }
-
- $sortedModulePaths = array();
- foreach ($order as $module) {
- if (isset($modules[$module])) {
- $sortedModulePaths[$module] = $modules[$module]->getPath();
- }
- }
- $sortedModulePaths = array_reverse($sortedModulePaths, true);
- return $sortedModulePaths;
+ return $sortedModules;
}
/**
diff --git a/src/includes/functions.php b/src/includes/functions.php
index dabab3298..80d87ad95 100644
--- a/src/includes/functions.php
+++ b/src/includes/functions.php
@@ -3,6 +3,7 @@
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\i18n\i18n;
+use SilverStripe\Core\Manifest\ModuleManifest;
///////////////////////////////////////////////////////////////////////////////
// HELPER FUNCTIONS
@@ -35,12 +36,7 @@ function singleton($className)
function project()
{
- global $project;
- // Set default project
- if (empty($project) && file_exists(BASE_PATH . '/mysite')) {
- $project = 'mysite';
- }
- return $project;
+ return ModuleManifest::config()->get('project');
}
/**
diff --git a/tests/php/Core/Manifest/PrioritySorterTest.php b/tests/php/Core/Manifest/PrioritySorterTest.php
new file mode 100644
index 000000000..8c1f2c345
--- /dev/null
+++ b/tests/php/Core/Manifest/PrioritySorterTest.php
@@ -0,0 +1,97 @@
+ 'I am module one',
+ 'module/two' => 'I am module two',
+ 'module/three' => 'I am module three',
+ 'module/four' => 'I am module four',
+ 'module/five' => 'I am module five',
+ ];
+ $this->sorter = new PrioritySorter($modules);
+ }
+
+ public function testModuleSortingWithNoVarsAndNoRest()
+ {
+ $this->sorter->setPriorities([
+ 'module/three',
+ 'module/one',
+ 'module/two',
+ ]);
+
+ $result = $this->sorter->getSortedList();
+ $keys = array_keys($result);
+ $this->assertEquals('module/three', $keys[0]);
+ $this->assertEquals('module/one', $keys[1]);
+ $this->assertEquals('module/two', $keys[2]);
+ $this->assertEquals('module/four', $keys[3]);
+ $this->assertEquals('module/five', $keys[4]);
+ }
+
+ public function testModuleSortingWithVarsAndNoRest()
+ {
+ $this->sorter->setPriorities([
+ 'module/three',
+ '$project',
+ ])
+ ->setVariable('$project', 'module/one');
+
+ $result = $this->sorter->getSortedList();
+ $keys = array_keys($result);
+ $this->assertEquals('module/three', $keys[0]);
+ $this->assertEquals('module/one', $keys[1]);
+ $this->assertEquals('module/two', $keys[2]);
+ $this->assertEquals('module/four', $keys[3]);
+ $this->assertEquals('module/five', $keys[4]);
+ }
+
+ public function testModuleSortingWithNoVarsAndWithRest()
+ {
+ $this->sorter->setPriorities([
+ 'module/two',
+ '$other_modules',
+ 'module/four',
+ ])
+ ->setRestKey('$other_modules');
+ $result = $this->sorter->getSortedList();
+ $keys = array_keys($result);
+ $this->assertEquals('module/two', $keys[0]);
+ $this->assertEquals('module/one', $keys[1]);
+ $this->assertEquals('module/three', $keys[2]);
+ $this->assertEquals('module/five', $keys[3]);
+ $this->assertEquals('module/four', $keys[4]);
+ }
+
+ public function testModuleSortingWithVarsAndWithRest()
+ {
+ $this->sorter->setPriorities([
+ 'module/two',
+ 'other_modules',
+ '$project',
+ ])
+ ->setVariable('$project', 'module/four')
+ ->setRestKey('other_modules');
+
+ $result = $this->sorter->getSortedList();
+ $keys = array_keys($result);
+ $this->assertEquals('module/two', $keys[0]);
+ $this->assertEquals('module/one', $keys[1]);
+ $this->assertEquals('module/three', $keys[2]);
+ $this->assertEquals('module/five', $keys[3]);
+ $this->assertEquals('module/four', $keys[4]);
+ }
+}
diff --git a/tests/php/Core/Manifest/ThemeResourceLoaderTest.php b/tests/php/Core/Manifest/ThemeResourceLoaderTest.php
index 41ddff23c..9eb1aba38 100644
--- a/tests/php/Core/Manifest/ThemeResourceLoaderTest.php
+++ b/tests/php/Core/Manifest/ThemeResourceLoaderTest.php
@@ -2,9 +2,11 @@
namespace SilverStripe\Core\Tests\Manifest;
+use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\View\ThemeResourceLoader;
use SilverStripe\View\ThemeManifest;
use SilverStripe\Dev\SapphireTest;
+use SilverStripe\Core\Manifest\ModuleManifest;
/**
* Tests for the {@link TemplateLoader} class.
@@ -35,14 +37,29 @@ class ThemeResourceLoaderTest extends SapphireTest
// Fake project root
$this->base = dirname(__FILE__) . '/fixtures/templatemanifest';
+ ModuleManifest::config()->set('module_priority', ['$project', '$other_modules']);
+ ModuleManifest::config()->set('project', 'myproject');
+
+ $moduleManifest = new ModuleManifest($this->base);
+ $moduleManifest->init();
+ $moduleManifest->sort();
+ ModuleLoader::inst()->pushManifest($moduleManifest);
+
// New ThemeManifest for that root
- $this->manifest = new ThemeManifest($this->base, 'myproject');
+ $this->manifest = new ThemeManifest($this->base);
+ $this->manifest->setProject('myproject');
$this->manifest->init();
// New Loader for that root
$this->loader = new ThemeResourceLoader($this->base);
$this->loader->addSet('$default', $this->manifest);
}
+ protected function tearDown()
+ {
+ ModuleLoader::inst()->popManifest();
+ parent::tearDown();
+ }
+
/**
* Test that 'main' and 'Layout' templates are loaded from module
*/
diff --git a/tests/php/Dev/DeprecationTest.php b/tests/php/Dev/DeprecationTest.php
index f6430ebc9..e10037691 100644
--- a/tests/php/Dev/DeprecationTest.php
+++ b/tests/php/Dev/DeprecationTest.php
@@ -115,7 +115,9 @@ class DeprecationTest extends SapphireTest
protected function callThatOriginatesFromFramework()
{
- $this->assertEquals('silverstripe/framework', TestDeprecation::get_module()->getName());
+ $module = TestDeprecation::get_module();
+ $this->assertNotNull($module);
+ $this->assertEquals('silverstripe/framework', $module->getName());
$this->assertNull(Deprecation::notice('2.0', 'Deprecation test passed'));
}
}
diff --git a/tests/php/Forms/HTMLEditor/HTMLEditorConfigTest.php b/tests/php/Forms/HTMLEditor/HTMLEditorConfigTest.php
index b4ea219e4..ebdde9b06 100644
--- a/tests/php/Forms/HTMLEditor/HTMLEditorConfigTest.php
+++ b/tests/php/Forms/HTMLEditor/HTMLEditorConfigTest.php
@@ -99,7 +99,7 @@ class HTMLEditorConfigTest extends SapphireTest
// Plugin specified with standard location
$this->assertContains('plugin4', array_keys($plugins));
$this->assertEquals(
- '/subdir/silverstripe-admin/thirdparty/tinymce/plugins/plugin4/plugin.min.js',
+ '/subdir/test/thirdparty/tinymce/plugins/plugin4/plugin.min.js',
$plugins['plugin4']
);
@@ -109,12 +109,15 @@ class HTMLEditorConfigTest extends SapphireTest
public function testPluginCompression()
{
+ // This test requires the real tiny_mce_gzip.php file
$module = ModuleLoader::inst()->getManifest()->getModule('silverstripe/admin');
if (!$module) {
$this->markTestSkipped('No silverstripe/admin module loaded');
}
- TinyMCEConfig::config()->remove('base_dir');
+ TinyMCEConfig::config()->set('base_dir', 'silverstripe/admin:thirdparty/tinymce');
Config::modify()->set(Director::class, 'alternate_base_url', 'http://mysite.com/subdir');
+
+ // Build new config
$c = new TinyMCEConfig();
$c->setTheme('modern');
$c->setOption('language', 'es');