diff --git a/.gitignore b/.gitignore
index cfd9e8f5a..4f8970b11 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,6 @@ node_modules/
coverage/
/**/*.js.map
/**/*.css.map
-vendor/
+/vendor/
composer.lock
silverstripe-cache/
diff --git a/_config/config.yml b/_config/config.yml
index 322175122..e1b788372 100644
--- a/_config/config.yml
+++ b/_config/config.yml
@@ -1,11 +1,6 @@
---
Name: coreconfig
---
-SilverStripe\Core\Injector\Injector:
- SilverStripe\Core\Manifest\ResourceURLGenerator:
- class: SilverStripe\Control\SimpleResourceURLGenerator
- properties:
- NonceStyle: mtime
SilverStripe\Control\HTTP:
cache_control:
max-age: 0
diff --git a/_config/resources.yml b/_config/resources.yml
new file mode 100644
index 000000000..a694e4ec3
--- /dev/null
+++ b/_config/resources.yml
@@ -0,0 +1,8 @@
+---
+Name: coreresources
+---
+SilverStripe\Core\Injector\Injector:
+ SilverStripe\Core\Manifest\ResourceURLGenerator:
+ class: SilverStripe\Control\SimpleResourceURLGenerator
+ properties:
+ NonceStyle: mtime
diff --git a/docs/en/02_Developer_Guides/01_Templates/06_Themes.md b/docs/en/02_Developer_Guides/01_Templates/06_Themes.md
index cf4232798..4b2c1ae76 100644
--- a/docs/en/02_Developer_Guides/01_Templates/06_Themes.md
+++ b/docs/en/02_Developer_Guides/01_Templates/06_Themes.md
@@ -29,6 +29,8 @@ As you've added new files to your SilverStripe installation, make sure you clear
`?flush=1` to your website URL (e.g http://yoursite.com/?flush=1).
+### Configuring themes
+
After installing the files through either method, update the current theme in SilverStripe. This can be done by
either altering the `SSViewer.themes` setting in a [config.yml](../configuration) or by changing the current theme in
the Site Configuration panel (http://yoursite.com/admin/settings)
@@ -42,6 +44,31 @@ SilverStripe\View\SSViewer:
- '$default'
```
+There are a variety of ways in which you can specify a theme. The below describe the three
+main styles of syntax:
+
+1. You can use the following to point to a theme or path within your root project:
+
+ - `themename` -> A simple name with no slash represents a theme in the `/themes` directory
+ - `/some/path/to/theme` - Any `/` prefixed string will be treated as a direct filesystem path to a theme root.
+ - `$themeset` - Any `$` prefixed name will refer to a set of themes. By default only `$default` set is configured,
+ which represents all module roots with a `templates` directory.
+
+2. Using the `:` syntax you can also specify themes relative to the given module:
+
+ - `myvendor/mymodule:sometheme` - This will specify a standard theme within the given module.
+ This will lookup the theme in the `themes` subfolder within this module. E.g.
+ `/vendor/myvendor/mymodule/themes/sometheme`.
+ Note: This syntax also works without the vendor prefix (`mymodule:sometheme`)
+ - `myvendor/mymodule:/some/path` - Rather than looking in the themes subdir, look in the
+ exact path within the root of the given module.
+
+3. You can also specify a module root folder directly.
+
+ - `myvendor/mymodule` - Points to the base folder of the given module.
+ - `mymodule:` - Also points to the base folder of the given module, but without a vendor.
+ The `:` is necessary to distinguish this from a non-module theme.
+
### Manually
Unpack the contents of the zip file you download into the `themes` directory in your SilverStripe installation. The
diff --git a/docs/en/02_Developer_Guides/05_Extending/How_Tos/01_Publish_a_Module.md b/docs/en/02_Developer_Guides/05_Extending/How_Tos/01_Publish_a_Module.md
index e37fb4c5e..569013ae5 100644
--- a/docs/en/02_Developer_Guides/05_Extending/How_Tos/01_Publish_a_Module.md
+++ b/docs/en/02_Developer_Guides/05_Extending/How_Tos/01_Publish_a_Module.md
@@ -17,39 +17,87 @@ this:
**mycustommodule/composer.json**
-```js
-
- {
- "name": "your-vendor-name/module-name",
- "description": "One-liner describing your module",
- "type": "silverstripe-module",
- "homepage": "http://github.com/your-vendor-name/module-name",
- "keywords": ["silverstripe", "some-tag", "some-other-tag"],
- "license": "BSD-3-Clause",
- "authors": [
- {"name": "Your Name","email": "your@email.com"}
- ],
- "support": {
- "issues": "http://github.com/your-vendor-name/module-name/issues"
- },
- "require": {
- "silverstripe/cms": "~3.1",
- "silverstripe/framework": "~3.1"
- },
- "extra": {
- "installer-name": "module-name",
- "screenshots": [
- "relative/path/screenshot1.png",
- "http://myhost.com/screenshot2.png"
- ]
- }
- }
+```json
+{
+ "name": "your-vendor-name/module-name",
+ "description": "One-liner describing your module",
+ "type": "silverstripe-module",
+ "homepage": "http://github.com/your-vendor-name/module-name",
+ "keywords": ["silverstripe", "some-tag", "some-other-tag"],
+ "license": "BSD-3-Clause",
+ "authors": [
+ {"name": "Your Name","email": "your@email.com"}
+ ],
+ "support": {
+ "issues": "http://github.com/your-vendor-name/module-name/issues"
+ },
+ "require": {
+ "silverstripe/cms": "^4",
+ "silverstripe/framework": "^4"
+ },
+ "extra": {
+ "installer-name": "module-name",
+ "screenshots": [
+ "relative/path/screenshot1.png",
+ "http://myhost.com/screenshot2.png"
+ ]
+ }
+}
```
Once your module is published online with a service like Github.com or Bitbucket.com, submit the repository to
[Packagist](https://packagist.org/) to have the module accessible to developers. It'll automatically get picked
up by [addons.silverstripe.org](http://addons.silverstripe.org/) website.
+## Vendor modules
+
+By default `silverstripe-module` type libraries are installed to the root web folder, however a new type
+`silverstripe-vendormodule` allows you to publish your module to the vendor directory.
+
+The below is an example of a vendor module composer.json:
+
+```json
+{
+ "name": "tractorcow/test-vendor-module",
+ "description": "Test module for silverstripe/vendor-plugin",
+ "type": "silverstripe-vendormodule",
+ "require": {
+ "silverstripe/vendor-plugin": "^1.0",
+ "silverstripe/cms": "^4.0"
+ },
+ "license": "BSD-3-Clause",
+ "autoload": {
+ "psr-4": {
+ "TractorCow\\TestVendorModule\\": "src/"
+ }
+ },
+ "extra": {
+ "expose": [
+ "client"
+ ]
+ },
+ "minimum-stability": "dev"
+}
+```
+
+Note that these modules have the following distinct characteristics:
+
+ - Library type is `silverstripe-vendormodule`
+ - Any folder which should be exposed to the public webroot must be declared in the `extra.expose` config.
+ These paths will be automatically rewritten to public urls which don't directly serve files from the `vendor`
+ folder. For instance, `vendor/tractorcow/test-vendor-module/client` will be rewritten to
+ `resources/tractorcow/test-vendor-module/client`.
+ - Any module which uses the folder expose feature must require `silverstripe/vendor-plugin` in order to
+ support automatic rewriting and linking. For more information on this plugin you can see the
+ [silverstripe/vendor-plugin github page](https://github.com/silverstripe/vendor-plugin).
+
+Linking to resources in vendor modules uses exactly the same syntax as non-vendor modules. For example,
+this is how you would require a script in this module:
+
+```php
+Requirements::javascript('tractorcow/test-vendor-module:client/js/script.js');
+```
+
## Releasing versions
Over time you may have to release new versions of your module to continue to work with newer versions of SilverStripe.
@@ -76,4 +124,4 @@ Here's some common values for your `require` section
* `3.0.*`: Version `3.0`, including `3.0.1`, `3.0.2` etc, excluding `3.1`
* `~3.0`: Version `3.0` or higher, including `3.0.1` and `3.1` etc, excluding `4.0`
* `~3.0,<3.2`: Version `3.0` or higher, up until `3.2`, which is excluded
- * `~3.0,>3.0.4`: Version `3.0` or higher, starting with `3.0.4`
\ No newline at end of file
+ * `~3.0,>3.0.4`: Version `3.0` or higher, starting with `3.0.4`
diff --git a/docs/en/04_Changelogs/4.0.0.md b/docs/en/04_Changelogs/4.0.0.md
index c401ca838..487621855 100644
--- a/docs/en/04_Changelogs/4.0.0.md
+++ b/docs/en/04_Changelogs/4.0.0.md
@@ -64,6 +64,8 @@ guide developers in preparing existing 3.x code for compatibility with 4.0
[intervention/image](https://github.com/intervention/image) library to power manipualations.
* Dependencies can managed via [recipe-plugin](https://github.com/silverstripe/recipe-plugin). See [recipe-core](https://github.com/silverstripe/recipe-core) and [recipe-cms](https://github.com/silverstripe/recipe-cms) as examples.
* Authentication has been upgraded to a modular approach using re-usable interfaces and easier to hook in to LoginHandlers.
+* Support for modules installed in vendor folder. See [/developer_guides/extending/how_tos/publish_a_module](the
+ module publishing guide) for more information.
## Upgrading
diff --git a/src/Control/SimpleResourceURLGenerator.php b/src/Control/SimpleResourceURLGenerator.php
index 169237e98..d30b8251d 100644
--- a/src/Control/SimpleResourceURLGenerator.php
+++ b/src/Control/SimpleResourceURLGenerator.php
@@ -3,6 +3,8 @@
namespace SilverStripe\Control;
use InvalidArgumentException;
+use SilverStripe\Core\Config\Config;
+use SilverStripe\Core\Manifest\ModuleResource;
use SilverStripe\Core\Manifest\ResourceURLGenerator;
/**
@@ -11,6 +13,18 @@ use SilverStripe\Core\Manifest\ResourceURLGenerator;
*/
class SimpleResourceURLGenerator implements ResourceURLGenerator
{
+ /**
+ * Rewrites applied after generating url.
+ * Note: requires either silverstripe/vendor-plugin-helper or silverstripe/vendor-plugin
+ * to ensure the file is available.
+ *
+ * @config
+ * @var array
+ */
+ private static $url_rewrites = [
+ '#^vendor/#i' => 'resources/',
+ ];
+
/*
* @var string
*/
@@ -45,18 +59,34 @@ class SimpleResourceURLGenerator implements ResourceURLGenerator
/**
* Return the URL for a resource, prefixing with Director::baseURL() and suffixing with a nonce
*
- * @param string $relativePath File or directory path relative to BASE_PATH
+ * @param string|ModuleResource $relativePath File or directory path relative to BASE_PATH
* @return string Doman-relative URL
* @throws InvalidArgumentException If the resource doesn't exist
*/
public function urlForResource($relativePath)
{
- $absolutePath = preg_replace('/\?.*/', '', Director::baseFolder() . '/' . $relativePath);
-
- if (!file_exists($absolutePath)) {
+ if ($relativePath instanceof ModuleResource) {
+ // Load from module resource
+ $resource = $relativePath;
+ $relativePath = $resource->getRelativePath();
+ $exists = $resource->exists();
+ $absolutePath = $resource->getPath();
+ } else {
+ // Use normal string
+ $absolutePath = preg_replace('/\?.*/', '', Director::baseFolder() . '/' . $relativePath);
+ $exists = file_exists($absolutePath);
+ }
+ if (!$exists) {
throw new InvalidArgumentException("File {$relativePath} does not exist");
}
+ // Apply url rewrites
+ $rules = Config::inst()->get(static::class, 'url_rewrites') ?: [];
+ foreach ($rules as $from => $to) {
+ $relativePath = preg_replace($from, $to, $relativePath);
+ }
+
+ // Apply nonce
$nonce = '';
// Don't add nonce to directories
if ($this->nonceStyle && is_file($absolutePath)) {
diff --git a/src/Core/Manifest/ClassManifest.php b/src/Core/Manifest/ClassManifest.php
index 64ec024d3..3d6eba408 100644
--- a/src/Core/Manifest/ClassManifest.php
+++ b/src/Core/Manifest/ClassManifest.php
@@ -444,7 +444,7 @@ class ClassManifest
'name_regex' => '/^[^_].*\\.php$/',
'ignore_files' => array('index.php', 'main.php', 'cli-script.php'),
'ignore_tests' => !$includeTests,
- 'file_callback' => function ($basename, $pathname) use ($includeTests) {
+ 'file_callback' => function ($basename, $pathname, $depth) use ($includeTests, $finder) {
$this->handleFile($basename, $pathname, $includeTests);
},
));
diff --git a/src/Core/Manifest/ManifestFileFinder.php b/src/Core/Manifest/ManifestFileFinder.php
index ecb3b39bf..995f944aa 100644
--- a/src/Core/Manifest/ManifestFileFinder.php
+++ b/src/Core/Manifest/ManifestFileFinder.php
@@ -16,63 +16,230 @@ use SilverStripe\Assets\FileFinder;
class ManifestFileFinder extends FileFinder
{
- const CONFIG_FILE = '_config.php';
- const CONFIG_DIR = '_config';
+ const CONFIG_FILE = '_config.php';
+ const CONFIG_DIR = '_config';
const EXCLUDE_FILE = '_manifest_exclude';
- const LANG_DIR = 'lang';
- const TESTS_DIR = 'tests';
+ const LANG_DIR = 'lang';
+ const TESTS_DIR = 'tests';
+ const VENDOR_DIR = 'vendor';
+ const RESOURCES_DIR = 'resources';
protected static $default_options = array(
'include_themes' => false,
- 'ignore_tests' => true,
- 'min_depth' => 1,
- 'ignore_dirs' => array('node_modules')
+ 'ignore_tests' => true,
+ 'min_depth' => 1,
+ 'ignore_dirs' => ['node_modules']
);
public function acceptDir($basename, $pathname, $depth)
{
- // Skip over the assets directory in the site root.
- if ($depth == 1 && $basename == ASSETS_DIR) {
+ // Skip if ignored
+ if ($this->isInsideIgnored($basename, $pathname, $depth)) {
return false;
}
- // Skip over any lang directories in the top level of the module.
- if ($depth == 2 && $basename == self::LANG_DIR) {
- return false;
+ // Keep searching inside vendor
+ $inVendor = $this->isInsideVendor($basename, $pathname, $depth);
+ if ($inVendor) {
+ // Keep searching if we could have a subdir module
+ if ($depth < 3) {
+ return true;
+ }
+
+ // Stop searching if we are in a non-module library
+ $libraryPath = $this->upLevels($pathname, $depth - 3);
+ $libraryBase = basename($libraryPath);
+ if (!$this->isDirectoryModule($libraryBase, $libraryPath, 3)) {
+ return false;
+ }
}
- // Skip over the vendor directories
- if (($depth == 1 || $depth == 2) && $basename == 'vendor') {
- return false;
+ // Include themes
+ if ($this->getOption('include_themes') && $this->isInsideThemes($basename, $pathname, $depth)) {
+ return true;
}
- // If we're not in testing mode, then skip over any tests directories.
- if ($this->getOption('ignore_tests') && $basename == self::TESTS_DIR) {
- return false;
- }
-
- // Ignore any directories which contain a _manifest_exclude file.
- if (file_exists($pathname . '/' . self::EXCLUDE_FILE)) {
- return false;
- }
-
- // Only include top level module directories which have a configuration
- // _config.php file. However, if we're in themes mode then include
- // the themes dir without a config file.
- $lackingConfig = (
- $depth == 1
- && !($this->getOption('include_themes') && $basename == THEMES_DIR)
- && !file_exists($pathname . '/' . self::CONFIG_FILE)
- && !file_exists($pathname . '/' . self::CONFIG_DIR)
- && $basename !== self::CONFIG_DIR // include a root config dir
- && !file_exists("$pathname/../" . self::CONFIG_DIR) // include all paths if a root config dir exists
- && !file_exists("$pathname/../" . self::CONFIG_FILE)// include all paths if a root config file exists
- );
-
- if ($lackingConfig) {
+ // Skip if not in module
+ if (!$this->isInsideModule($basename, $pathname, $depth)) {
return false;
}
return parent::acceptDir($basename, $pathname, $depth);
}
+
+ /**
+ * Check if the given dir is, or is inside the vendor folder
+ *
+ * @param string $basename
+ * @param string $pathname
+ * @param int $depth
+ * @return bool
+ */
+ public function isInsideVendor($basename, $pathname, $depth)
+ {
+ $base = basename($this->upLevels($pathname, $depth - 1));
+ return $base === self::VENDOR_DIR;
+ }
+
+ /**
+ * Check if the given dir is, or is inside the themes folder
+ *
+ * @param string $basename
+ * @param string $pathname
+ * @param int $depth
+ * @return bool
+ */
+ public function isInsideThemes($basename, $pathname, $depth)
+ {
+ $base = basename($this->upLevels($pathname, $depth - 1));
+ return $base === THEMES_DIR;
+ }
+
+ /**
+ * Check if this folder or any parent is ignored
+ *
+ * @param string $basename
+ * @param string $pathname
+ * @param int $depth
+ * @return bool
+ */
+ public function isInsideIgnored($basename, $pathname, $depth)
+ {
+ return $this->anyParents($basename, $pathname, $depth, function ($basename, $pathname, $depth) {
+ return $this->isDirectoryIgnored($basename, $pathname, $depth);
+ });
+ }
+
+ /**
+ * Check if this folder is inside any module
+ *
+ * @param string $basename
+ * @param string $pathname
+ * @param int $depth
+ * @return bool
+ */
+ public function isInsideModule($basename, $pathname, $depth)
+ {
+ return $this->anyParents($basename, $pathname, $depth, function ($basename, $pathname, $depth) {
+ return $this->isDirectoryModule($basename, $pathname, $depth);
+ });
+ }
+
+ /**
+ * Check if any parents match the given callback
+ *
+ * @param string $basename
+ * @param string $pathname
+ * @param int $depth
+ * @param callable $callback
+ * @return bool
+ */
+ protected function anyParents($basename, $pathname, $depth, $callback)
+ {
+ // Check all ignored dir up the path
+ while ($depth >= 0) {
+ $ignored = $callback($basename, $pathname, $depth);
+ if ($ignored) {
+ return true;
+ }
+ $pathname = dirname($pathname);
+ $basename = basename($pathname);
+ $depth--;
+ }
+ return false;
+ }
+
+ /**
+ * Check if the given dir is a module root (not a subdir)
+ *
+ * @param string $basename
+ * @param string $pathname
+ * @param string $depth
+ * @return bool
+ */
+ public function isDirectoryModule($basename, $pathname, $depth)
+ {
+ // Depth can either be 0, 1, or 3 (if and only if inside vendor)
+ $inVendor = $this->isInsideVendor($basename, $pathname, $depth);
+ if ($depth > 0 && $depth !== ($inVendor ? 3 : 1)) {
+ return false;
+ }
+
+ // True if config file exists
+ if (file_exists($pathname . '/' . self::CONFIG_FILE)) {
+ return true;
+ }
+
+ // True if config dir exists
+ if (file_exists($pathname . '/' . self::CONFIG_DIR)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get a parent path the given levels above
+ *
+ * @param string $pathname
+ * @param int $depth Number of parents to rise
+ * @return string
+ */
+ protected function upLevels($pathname, $depth)
+ {
+ if ($depth < 0) {
+ return null;
+ }
+ while ($depth--) {
+ $pathname = dirname($pathname);
+ }
+ return $pathname;
+ }
+
+ /**
+ * Get all ignored directories
+ *
+ * @return array
+ */
+ protected function getIgnoredDirs()
+ {
+ $ignored = [self::LANG_DIR, 'node_modules'];
+ if ($this->getOption('ignore_tests')) {
+ $ignored[] = self::TESTS_DIR;
+ }
+ return $ignored;
+ }
+
+ /**
+ * Check if the given directory is ignored
+ * @param string $basename
+ * @param string $pathname
+ * @param string $depth
+ * @return bool
+ */
+ public function isDirectoryIgnored($basename, $pathname, $depth)
+ {
+ // Don't ignore root
+ if ($depth === 0) {
+ return false;
+ }
+
+ // Check if manifest-ignored is present
+ if (file_exists($pathname . '/' . self::EXCLUDE_FILE)) {
+ return true;
+ }
+
+ // Check if directory name is ignored
+ $ignored = $this->getIgnoredDirs();
+ if (in_array($basename, $ignored)) {
+ return true;
+ }
+
+ // Ignore these dirs in the root only
+ if ($depth === 1 && in_array($basename, [ASSETS_DIR, self::RESOURCES_DIR])) {
+ return true;
+ }
+
+ return false;
+ }
}
diff --git a/src/Core/Manifest/Module.php b/src/Core/Manifest/Module.php
index d59a1dd30..3179f55a5 100644
--- a/src/Core/Manifest/Module.php
+++ b/src/Core/Manifest/Module.php
@@ -4,19 +4,21 @@ namespace SilverStripe\Core\Manifest;
use Exception;
use Serializable;
-use SilverStripe\Core\Injector\Injector;
+use SilverStripe\Dev\Deprecation;
class Module implements Serializable
{
+ const TRIM_CHARS = '/\\';
+
/**
- * Directory
+ * Full directory path to this module with no trailing slash
*
* @var string
*/
protected $path = null;
/**
- * Base folder of application
+ * Base folder of application with no trailing slash
*
* @var string
*/
@@ -29,10 +31,23 @@ class Module implements Serializable
*/
protected $composerData = null;
+ /**
+ * Loaded resources for this module
+ *
+ * @var ModuleResource[]
+ */
+ protected $resources = [];
+
+ /**
+ * Construct a module
+ *
+ * @param string $path Absolute filesystem path to this module
+ * @param string $base base url for the application this module is installed in
+ */
public function __construct($path, $base)
{
- $this->path = rtrim($path, '/\\');
- $this->basePath = rtrim($base, '/\\');
+ $this->path = rtrim($path, self::TRIM_CHARS);
+ $this->basePath = rtrim($base, self::TRIM_CHARS);
$this->loadComposer();
}
@@ -62,6 +77,19 @@ class Module implements Serializable
return null;
}
+ /**
+ * Get list of folders that need to be made available
+ *
+ * @return array
+ */
+ public function getExposedFolders()
+ {
+ if (isset($this->composerData['extra']['expose'])) {
+ return $this->composerData['extra']['expose'];
+ }
+ return [];
+ }
+
/**
* Gets "short" name of this module. This is the base directory this module
* is installed in.
@@ -94,7 +122,7 @@ class Module implements Serializable
/**
* Get base path for this module
*
- * @return string
+ * @return string Path with no trailing slash E.g. /var/www/module
*/
public function getPath()
{
@@ -105,11 +133,11 @@ class Module implements Serializable
* Get path relative to base dir.
* If module path is base this will be empty string
*
- * @return string
+ * @return string Path with trimmed slashes. E.g. vendor/silverstripe/module.
*/
public function getRelativePath()
{
- return ltrim(substr($this->path, strlen($this->basePath)), '/\\');
+ return trim(substr($this->path, strlen($this->basePath)), self::TRIM_CHARS);
}
public function serialize()
@@ -120,6 +148,7 @@ class Module implements Serializable
public function unserialize($serialized)
{
list($this->path, $this->basePath, $this->composerData) = json_decode($serialized, true);
+ $this->resources = [];
}
/**
@@ -152,63 +181,69 @@ class Module implements Serializable
}
/**
- * Gets path to physical file resource relative to base directory.
- * Directories included
+ * Get resource for this module
*
- * This method makes no distinction between public / local resources,
- * which may change in the near future.
- *
- * @internal Experimental API and may change
- * @param string $path File or directory path relative to module directory
- * @return string Path relative to base directory
+ * @param string $path
+ * @return ModuleResource
+ */
+ public function getResource($path)
+ {
+ $path = trim($path, '/\\');
+ if (isset($this->resources[$path])) {
+ return $this->resources[$path];
+ }
+ return $this->resources[$path] = new ModuleResource($this, $path);
+ }
+
+ /**
+ * @deprecated 4.0...5.0 Use getResource($path)->getRelativePath() instead
+ * @param string $path
+ * @return string
*/
public function getRelativeResourcePath($path)
{
- $base = trim($this->getRelativePath(), '/\\');
- $path = trim($path, '/\\');
- return trim("{$base}/{$path}", '/\\');
+ Deprecation::notice('5.0', 'Use getResource($path)->getRelativePath() instead');
+ return $this
+ ->getResource($path)
+ ->getRelativePath();
}
/**
- * Gets path to physical file resource relative to base directory.
- * Directories included
- *
- * This method makes no distinction between public / local resources,
- * which may change in the near future.
- *
- * @internal Experimental API and may change
- * @param string $path File or directory path relative to module directory
- * @return string Path relative to base directory
+ * @deprecated 4.0...5.0 Use ->getResource($path)->getPath() instead
+ * @param string $path
+ * @return string
*/
public function getResourcePath($path)
{
- return $this->basePath . '/' . $this->getRelativeResourcePath($path);
+ Deprecation::notice('5.0', 'Use getResource($path)->getPath() instead');
+ return $this
+ ->getResource($path)
+ ->getPath();
}
/**
- * Gets the URL for a given resource.
- * Relies on the ModuleURLGenerator Injector service to do the heavy lifting
- *
- * @internal Experimental API and may change
- * @param string $path File or directory path relative to module directory
- * @return string URL, either domain-relative (starting with /) or absolute
+ * @deprecated 4.0...5.0 Use ->getResource($path)->getURL() instead
+ * @param string $path
+ * @return string
*/
public function getResourceURL($path)
{
- return Injector::inst()
- ->get(ResourceURLGenerator::class)
- ->urlForResource($this->getRelativeResourcePath($path));
+ Deprecation::notice('5.0', 'Use getResource($path)->getURL() instead');
+ return $this
+ ->getResource($path)
+ ->getURL();
}
/**
- * Check if this module has a given resource
- *
- * @internal Experimental API and may change
+ * @deprecated 4.0...5.0 Use ->getResource($path)->exists() instead
* @param string $path
- * @return bool
+ * @return string
*/
public function hasResource($path)
{
- return file_exists($this->getResourcePath($path));
+ Deprecation::notice('5.0', 'Use getResource($path)->exists() instead');
+ return $this
+ ->getResource($path)
+ ->exists();
}
}
diff --git a/src/Core/Manifest/ModuleManifest.php b/src/Core/Manifest/ModuleManifest.php
index 8ccce6d88..bcdc68b19 100644
--- a/src/Core/Manifest/ModuleManifest.php
+++ b/src/Core/Manifest/ModuleManifest.php
@@ -5,7 +5,6 @@ namespace SilverStripe\Core\Manifest;
use LogicException;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Cache\CacheFactory;
-use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Injector\Injector;
@@ -171,52 +170,25 @@ class ModuleManifest
$finder = new ManifestFileFinder();
$finder->setOptions(array(
'min_depth' => 0,
- '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
+ 'dir_callback' => function ($basename, $pathname, $depth) use ($finder) {
+ if ($finder->isDirectoryModule($basename, $pathname, $depth)) {
+ $this->addModule($pathname);
+ }
+ }
));
$finder->find($this->base);
- $finder = new ManifestFileFinder();
- $finder->setOptions(array(
- 'name_regex' => '/\.ya?ml$/',
- 'ignore_tests' => !$includeTests,
- 'file_callback' => array($this, 'addYAMLConfigFile'),
- 'max_depth' => 2
- ));
- $finder->find($this->base);
+ // Include root itself if module
+ if ($finder->isDirectoryModule(basename($this->base), $this->base, 0)) {
+ $this->addModule($this->base);
+ }
if ($this->cache) {
$this->cache->set($this->cacheKey, $this->modules);
}
}
- /**
- * Record finding of _config.php file
- *
- * @param string $basename
- * @param string $pathname
- */
- public function addSourceConfigFile($basename, $pathname)
- {
- $this->addModule(dirname($pathname));
- }
-
- /**
- * Handle lookup of _config/*.yml file
- *
- * @param string $basename
- * @param string $pathname
- */
- public function addYAMLConfigFile($basename, $pathname)
- {
- if (preg_match('{/([^/]+)/_config/}', $pathname, $match)) {
- $this->addModule(dirname(dirname($pathname)));
- }
- }
-
/**
* Get module by name
*
diff --git a/src/Core/Manifest/ModuleResource.php b/src/Core/Manifest/ModuleResource.php
new file mode 100644
index 000000000..e1e471665
--- /dev/null
+++ b/src/Core/Manifest/ModuleResource.php
@@ -0,0 +1,121 @@
+module = $module;
+ $this->relativePath = ltrim($relativePath, Module::TRIM_CHARS);
+ if (empty($this->relativePath)) {
+ throw new InvalidArgumentException("Resource cannot have empty path");
+ }
+ }
+
+ /**
+ * Return the full filesystem path to this resource.
+ *
+ * Note: In the case that this resource is mapped to the `resources` folder, this will
+ * return the original rather than the copy / symlink.
+ *
+ * @return string Path with no trailing slash E.g. /var/www/module
+ */
+ public function getPath()
+ {
+ return $this->module->getPath() . '/' . $this->relativePath;
+ }
+
+ /**
+ * Get the path of this resource relative to the base path.
+ *
+ * Note: In the case that this resource is mapped to the `resources` folder, this will
+ * return the original rather than the copy / symlink.
+ *
+ * @return string Relative path (no leading /)
+ */
+ public function getRelativePath()
+ {
+ $path = $this->module->getRelativePath() . '/' . $this->relativePath;
+ return ltrim($path, Module::TRIM_CHARS);
+ }
+
+ /**
+ * Public URL to this resource.
+ * Note: May be either absolute url, or root-relative url
+ *
+ * In the case that this resource is mapped to the `resources` folder this
+ * will be the mapped url rather than the original path.
+ *
+ * @return string
+ */
+ public function getURL()
+ {
+ /** @var ResourceURLGenerator $generator */
+ $generator = Injector::inst()->get(ResourceURLGenerator::class);
+ return $generator->urlForResource($this->getRelativePath());
+ }
+
+ /**
+ * Synonym for getURL() for APIs that expect a Link method
+ *
+ * @return mixed
+ */
+ public function Link()
+ {
+ return $this->getURL();
+ }
+
+ /**
+ * Determine if this resource exists
+ *
+ * @return bool
+ */
+ public function exists()
+ {
+ return file_exists($this->getPath());
+ }
+
+ /**
+ * Get relative path
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->getRelativePath();
+ }
+
+ /**
+ * @return Module
+ */
+ public function getModule()
+ {
+ return $this->module;
+ }
+}
diff --git a/src/Core/Manifest/ResourceURLGenerator.php b/src/Core/Manifest/ResourceURLGenerator.php
index 2a8ac48d7..422b54bd6 100644
--- a/src/Core/Manifest/ResourceURLGenerator.php
+++ b/src/Core/Manifest/ResourceURLGenerator.php
@@ -15,7 +15,7 @@ interface ResourceURLGenerator
* As well as returning the URL, this method may also perform any changes needed to ensure that this
* URL will resolve, for example, by copying files to another location
*
- * @param string $resource File or directory path relative to BASE_PATH
+ * @param string|ModuleResource $resource File or directory path relative to BASE_PATH, or ModuleResource instance
* @return string URL, either domain-relative (starting with /) or absolute
* @throws InvalidArgumentException If the resource doesn't exist or can't be sent to the browser
*/
diff --git a/src/View/ThemeResourceLoader.php b/src/View/ThemeResourceLoader.php
index 95c6f0b55..91cb9ff60 100644
--- a/src/View/ThemeResourceLoader.php
+++ b/src/View/ThemeResourceLoader.php
@@ -2,6 +2,8 @@
namespace SilverStripe\View;
+use InvalidArgumentException;
+use RuntimeException;
use SilverStripe\Core\Manifest\ModuleLoader;
/**
@@ -95,56 +97,59 @@ class ThemeResourceLoader
public function getPath($identifier)
{
$slashPos = strpos($identifier, '/');
+ $parts = explode(':', $identifier, 2);
// 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) {
- // Extract from /: format.
- // is optional, and if is omitted it defaults to the module root dir.
- // If is included, this is the name of the directory under moduleroot/themes/
- // which contains the theme.
- // is always the name of the install directory, not necessarily the composer name.
- $parts = explode(':', $identifier, 2);
-
if (count($parts) > 1) {
- $theme = $parts[1];
- // "module/vendor:/sub/path"
- if ($theme[0] === '/') {
- $subpath = $theme;
-
- // "module/vendor:subtheme"
- } else {
- $subpath = '/themes/' . $theme;
- }
-
- // "module/vendor"
- } else {
- $subpath = '';
+ throw new InvalidArgumentException("Invalid theme identifier {$identifier}");
}
+ return substr($identifier, 1);
+ }
- $package = $parts[0];
-
- // Find matching module for this package
- $module = ModuleLoader::inst()->getManifest()->getModule($package);
- if ($module) {
- $modulePath = $module->getRelativePath();
- } else {
- // fall back to dirname
- list(, $modulePath) = explode('/', $parts[0], 2);
-
- // If the module is in the themes// prefer that
- if (is_dir(THEMES_PATH . '/' .$modulePath)) {
- $modulePath = THEMES_DIR . '/' . $$modulePath;
- }
- }
-
- return ltrim($modulePath . $subpath, '/');
- } // Otherwise it's a (deprecated) old-style "theme" identifier
- else {
+ // If there is no slash / colon it's a legacy theme
+ if ($slashPos === false && count($parts) === 1) {
return THEMES_DIR.'/'.$identifier;
}
+
+ // Extract from /: format.
+ // is optional, and if is omitted it defaults to the module root dir.
+ // If is included, this is the name of the directory under moduleroot/themes/
+ // which contains the theme.
+ // 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 $modulePath . $subpath;
}
/**
diff --git a/tests/behat/src/ConfigContext.php b/tests/behat/src/ConfigContext.php
index c3a2dd41f..7da49e94b 100644
--- a/tests/behat/src/ConfigContext.php
+++ b/tests/behat/src/ConfigContext.php
@@ -120,7 +120,7 @@ class ConfigContext implements Context
$project = ModuleManifest::config()->get('project') ?: 'mysite';
$mysite = ModuleLoader::getModule($project);
assertNotNull($mysite, 'Project exists');
- $destPath = $mysite->getResourcePath("_config/{$filename}");
+ $destPath = $mysite->getResource("_config/{$filename}")->getPath();
assertFileNotExists($destPath, "Config file {$filename} hasn't aleady been loaded");
// Load
diff --git a/tests/php/Control/SimpleResourceURLGeneratorTest.php b/tests/php/Control/SimpleResourceURLGeneratorTest.php
new file mode 100644
index 000000000..70eab2207
--- /dev/null
+++ b/tests/php/Control/SimpleResourceURLGeneratorTest.php
@@ -0,0 +1,67 @@
+set(
+ 'alternate_base_folder',
+ __DIR__ .'/SimpleResourceURLGeneratorTest/_fakewebroot'
+ );
+ Director::config()->set(
+ 'alternate_base_url',
+ 'http://www.mysite.com/'
+ );
+ }
+
+ public function testAddMTime()
+ {
+ /** @var SimpleResourceURLGenerator $generator */
+ $generator = Injector::inst()->get(ResourceURLGenerator::class);
+ $mtime = filemtime(__DIR__ .'/SimpleResourceURLGeneratorTest/_fakewebroot/basemodule/client/file.js');
+ $this->assertEquals(
+ '/basemodule/client/file.js?m='.$mtime,
+ $generator->urlForResource('basemodule/client/file.js')
+ );
+ }
+
+ public function testVendorResource()
+ {
+ /** @var SimpleResourceURLGenerator $generator */
+ $generator = Injector::inst()->get(ResourceURLGenerator::class);
+ $mtime = filemtime(
+ __DIR__ .'/SimpleResourceURLGeneratorTest/_fakewebroot/vendor/silverstripe/mymodule/client/style.css'
+ );
+ $this->assertEquals(
+ '/resources/silverstripe/mymodule/client/style.css?m='.$mtime,
+ $generator->urlForResource('vendor/silverstripe/mymodule/client/style.css')
+ );
+ }
+
+ public function testModuleResource()
+ {
+ /** @var SimpleResourceURLGenerator $generator */
+ $generator = Injector::inst()->get(ResourceURLGenerator::class);
+ $module = new Module(
+ __DIR__ .'/SimpleResourceURLGeneratorTest/_fakewebroot/vendor/silverstripe/mymodule/',
+ __DIR__ .'/SimpleResourceURLGeneratorTest/_fakewebroot/'
+ );
+ $mtime = filemtime(
+ __DIR__ .'/SimpleResourceURLGeneratorTest/_fakewebroot/vendor/silverstripe/mymodule/client/style.css'
+ );
+ $this->assertEquals(
+ '/resources/silverstripe/mymodule/client/style.css?m='.$mtime,
+ $generator->urlForResource($module->getResource('client/style.css'))
+ );
+ }
+}
diff --git a/tests/php/Control/SimpleResourceURLGeneratorTest/_fakewebroot/basemodule/client/file.js b/tests/php/Control/SimpleResourceURLGeneratorTest/_fakewebroot/basemodule/client/file.js
new file mode 100644
index 000000000..f9b250a2c
--- /dev/null
+++ b/tests/php/Control/SimpleResourceURLGeneratorTest/_fakewebroot/basemodule/client/file.js
@@ -0,0 +1 @@
+/* basemodule/file.js */
diff --git a/tests/php/Control/SimpleResourceURLGeneratorTest/_fakewebroot/basemodule/client/style.css b/tests/php/Control/SimpleResourceURLGeneratorTest/_fakewebroot/basemodule/client/style.css
new file mode 100644
index 000000000..fcadb6704
--- /dev/null
+++ b/tests/php/Control/SimpleResourceURLGeneratorTest/_fakewebroot/basemodule/client/style.css
@@ -0,0 +1,2 @@
+/* basemodule/style.css */
+body {}
diff --git a/tests/php/Control/SimpleResourceURLGeneratorTest/_fakewebroot/vendor/silverstripe/mymodule/client/file.js b/tests/php/Control/SimpleResourceURLGeneratorTest/_fakewebroot/vendor/silverstripe/mymodule/client/file.js
new file mode 100644
index 000000000..cb5190a4c
--- /dev/null
+++ b/tests/php/Control/SimpleResourceURLGeneratorTest/_fakewebroot/vendor/silverstripe/mymodule/client/file.js
@@ -0,0 +1 @@
+/* mymodule/file.js */
diff --git a/tests/php/Control/SimpleResourceURLGeneratorTest/_fakewebroot/vendor/silverstripe/mymodule/client/style.css b/tests/php/Control/SimpleResourceURLGeneratorTest/_fakewebroot/vendor/silverstripe/mymodule/client/style.css
new file mode 100644
index 000000000..45fc078e8
--- /dev/null
+++ b/tests/php/Control/SimpleResourceURLGeneratorTest/_fakewebroot/vendor/silverstripe/mymodule/client/style.css
@@ -0,0 +1,2 @@
+/* mymodule/style.css */
+body {}
diff --git a/tests/php/Core/Manifest/ClassManifestTest.php b/tests/php/Core/Manifest/ClassManifestTest.php
index 6629e1735..7033fc8ba 100644
--- a/tests/php/Core/Manifest/ClassManifestTest.php
+++ b/tests/php/Core/Manifest/ClassManifestTest.php
@@ -32,39 +32,51 @@ class ClassManifestTest extends SapphireTest
parent::setUp();
$this->base = dirname(__FILE__) . '/fixtures/classmanifest';
- $this->manifest = new ClassManifest($this->base);
+ $this->manifest = new ClassManifest($this->base);
$this->manifest->init(false);
$this->manifestTests = new ClassManifest($this->base);
$this->manifestTests->init(true);
}
- public function testGetItemPath()
+ /**
+ * @return array
+ */
+ public function providerTestGetItemPath()
{
- $expect = array(
- 'CLASSA' => 'module/classes/ClassA.php',
- 'ClassA' => 'module/classes/ClassA.php',
- 'classa' => 'module/classes/ClassA.php',
- 'INTERFACEA' => 'module/interfaces/InterfaceA.php',
- 'InterfaceA' => 'module/interfaces/InterfaceA.php',
- 'interfacea' => 'module/interfaces/InterfaceA.php',
- 'TestTraitA' => 'module/traits/TestTraitA.php',
- 'TestNamespace\Testing\TestTraitB' => 'module/traits/TestTraitB.php'
- );
+ return [
+ ['CLASSA', 'module/classes/ClassA.php'],
+ ['ClassA', 'module/classes/ClassA.php'],
+ ['classa', 'module/classes/ClassA.php'],
+ ['INTERFACEA', 'module/interfaces/InterfaceA.php'],
+ ['InterfaceA', 'module/interfaces/InterfaceA.php'],
+ ['interfacea', 'module/interfaces/InterfaceA.php'],
+ ['TestTraitA', 'module/traits/TestTraitA.php'],
+ ['TestNamespace\\Testing\\TestTraitB', 'module/traits/TestTraitB.php'],
+ ['VendorClassA', 'vendor/silverstripe/modulec/code/VendorClassA.php'],
+ ['VendorTraitA', 'vendor/silverstripe/modulec/code/VendorTraitA.php'],
+ ];
+ }
- foreach ($expect as $name => $path) {
- $this->assertEquals("{$this->base}/$path", $this->manifest->getItemPath($name));
- }
+ /**
+ * @dataProvider providerTestGetItemPath
+ * @param string $name
+ * @param string $path
+ */
+ public function testGetItemPath($name, $path)
+ {
+ $this->assertEquals("{$this->base}/$path", $this->manifest->getItemPath($name));
}
public function testGetClasses()
{
- $expect = array(
- 'classa' => "{$this->base}/module/classes/ClassA.php",
- 'classb' => "{$this->base}/module/classes/ClassB.php",
- 'classc' => "{$this->base}/module/classes/ClassC.php",
- 'classd' => "{$this->base}/module/classes/ClassD.php",
- 'classe' => "{$this->base}/module/classes/ClassE.php",
- );
+ $expect = [
+ 'classa' => "{$this->base}/module/classes/ClassA.php",
+ 'classb' => "{$this->base}/module/classes/ClassB.php",
+ 'classc' => "{$this->base}/module/classes/ClassC.php",
+ 'classd' => "{$this->base}/module/classes/ClassD.php",
+ 'classe' => "{$this->base}/module/classes/ClassE.php",
+ 'vendorclassa' => "{$this->base}/vendor/silverstripe/modulec/code/VendorClassA.php",
+ ];
$this->assertEquals($expect, $this->manifest->getClasses());
}
@@ -77,6 +89,7 @@ class ClassManifestTest extends SapphireTest
'classc' => 'ClassC',
'classd' => 'ClassD',
'classe' => 'ClassE',
+ 'vendorclassa' => 'VendorClassA',
],
$this->manifest->getClassNames()
);
@@ -85,10 +98,11 @@ class ClassManifestTest extends SapphireTest
public function testGetTraitNames()
{
$this->assertEquals(
- array(
+ [
'testtraita' => 'TestTraitA',
- 'testnamespace\testing\testtraitb' => 'TestNamespace\Testing\TestTraitB',
- ),
+ 'testnamespace\\testing\\testtraitb' => 'TestNamespace\\Testing\\TestTraitB',
+ 'vendortraita' => 'VendorTraitA',
+ ],
$this->manifest->getTraitNames()
);
}
diff --git a/tests/php/Core/Manifest/ManifestFileFinderTest.php b/tests/php/Core/Manifest/ManifestFileFinderTest.php
index ee9d5a10e..1eb87bf3b 100644
--- a/tests/php/Core/Manifest/ManifestFileFinderTest.php
+++ b/tests/php/Core/Manifest/ManifestFileFinderTest.php
@@ -10,16 +10,23 @@ use SilverStripe\Dev\SapphireTest;
*/
class ManifestFileFinderTest extends SapphireTest
{
-
- protected $base;
+ protected $defaultBase;
public function __construct()
{
- $this->defaultBase = dirname(__FILE__) . '/fixtures/manifestfilefinder';
+ $this->defaultBase = __DIR__ . '/fixtures/manifestfilefinder';
parent::__construct();
}
- public function assertFinderFinds($finder, $base, $expect, $message = null)
+ /**
+ * Test that the finder can find the given files
+ *
+ * @param ManifestFileFinder $finder
+ * @param string $base
+ * @param array $expect
+ * @param string $message
+ */
+ public function assertFinderFinds(ManifestFileFinder $finder, $base, $expect, $message = null)
{
if (!$base) {
$base = $this->defaultBase;
@@ -45,9 +52,10 @@ class ManifestFileFinderTest extends SapphireTest
$this->assertFinderFinds(
$finder,
null,
- array(
- 'module/module.txt'
- )
+ [
+ 'module/module.txt',
+ 'vendor/myvendor/thismodule/module.txt',
+ ]
);
}
@@ -60,11 +68,14 @@ class ManifestFileFinderTest extends SapphireTest
$this->assertFinderFinds(
$finder,
null,
- array(
- 'module/module.txt',
- 'module/tests/tests.txt',
- 'module/code/tests/tests2.txt'
- )
+ [
+ 'module/module.txt',
+ 'module/tests/tests.txt',
+ 'module/code/tests/tests2.txt',
+ 'vendor/myvendor/thismodule/module.txt',
+ 'vendor/myvendor/thismodule/tests/tests.txt',
+ 'vendor/myvendor/thismodule/code/tests/tests2.txt',
+ ]
);
}
@@ -77,10 +88,11 @@ class ManifestFileFinderTest extends SapphireTest
$this->assertFinderFinds(
$finder,
null,
- array(
- 'module/module.txt',
- 'themes/themes.txt'
- )
+ [
+ 'module/module.txt',
+ 'themes/themes.txt',
+ 'vendor/myvendor/thismodule/module.txt',
+ ]
);
}
@@ -90,10 +102,8 @@ class ManifestFileFinderTest extends SapphireTest
$this->assertFinderFinds(
$finder,
- dirname(__FILE__) . '/fixtures/manifestfilefinder_rootconfigfile',
- array(
- 'code/code.txt',
- )
+ __DIR__ . '/fixtures/manifestfilefinder_rootconfigfile',
+ [ 'code/code.txt' ]
);
}
@@ -103,11 +113,11 @@ class ManifestFileFinderTest extends SapphireTest
$this->assertFinderFinds(
$finder,
- dirname(__FILE__) . '/fixtures/manifestfilefinder_rootconfigfolder',
- array(
+ __DIR__ . '/fixtures/manifestfilefinder_rootconfigfolder',
+ [
'_config/config.yml',
'code/code.txt',
- )
+ ]
);
}
}
diff --git a/tests/php/Core/Manifest/ModuleManifestTest.php b/tests/php/Core/Manifest/ModuleManifestTest.php
index ca7705058..2c1c8fa1a 100644
--- a/tests/php/Core/Manifest/ModuleManifestTest.php
+++ b/tests/php/Core/Manifest/ModuleManifestTest.php
@@ -31,9 +31,10 @@ class ModuleManifestTest extends SapphireTest
$modules = $this->manifest->getModules();
$this->assertEquals(
[
- 'silverstripe/root-module',
'module',
'silverstripe/awesome-module',
+ 'silverstripe/modulec',
+ 'silverstripe/root-module',
],
array_keys($modules)
);
@@ -71,32 +72,37 @@ class ModuleManifestTest extends SapphireTest
$this->assertEquals('moduleb', $module->getRelativePath());
}
- /*
- * Note: Tests experimental API
- * @internal
- */
public function testGetResourcePath()
{
- $module = $this->manifest->getModule('moduleb');
- $this->assertTrue($module->hasResource('composer.json'));
- $this->assertFalse($module->hasResource('package.json'));
+ // Root module
+ $moduleb = $this->manifest->getModule('moduleb');
+ $this->assertTrue($moduleb->getResource('composer.json')->exists());
+ $this->assertFalse($moduleb->getResource('package.json')->exists());
$this->assertEquals(
'moduleb/composer.json',
- $module->getRelativeResourcePath('composer.json')
+ $moduleb->getResource('composer.json')->getRelativePath()
+ );
+ }
+
+ public function testGetResourcePathsInVendor()
+ {
+ // Vendor module
+ $modulec = $this->manifest->getModule('silverstripe/modulec');
+ $this->assertTrue($modulec->getResource('composer.json')->exists());
+ $this->assertFalse($modulec->getResource('package.json')->exists());
+ $this->assertEquals(
+ 'vendor/silverstripe/modulec/composer.json',
+ $modulec->getResource('composer.json')->getRelativePath()
);
}
- /*
- * Note: Tests experimental API
- * @internal
- */
public function testGetResourcePathOnRoot()
{
$module = $this->manifest->getModule('silverstripe/root-module');
- $this->assertTrue($module->hasResource('composer.json'));
+ $this->assertTrue($module->getResource('composer.json')->exists());
$this->assertEquals(
'composer.json',
- $module->getRelativeResourcePath('composer.json')
+ $module->getResource('composer.json')->getRelativePath()
);
}
}
diff --git a/tests/php/Core/Manifest/ThemeResourceLoaderTest.php b/tests/php/Core/Manifest/ThemeResourceLoaderTest.php
index 9eb1aba38..6ff3f7c21 100644
--- a/tests/php/Core/Manifest/ThemeResourceLoaderTest.php
+++ b/tests/php/Core/Manifest/ThemeResourceLoaderTest.php
@@ -2,6 +2,7 @@
namespace SilverStripe\Core\Tests\Manifest;
+use SilverStripe\Control\Director;
use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\View\ThemeResourceLoader;
use SilverStripe\View\ThemeManifest;
@@ -37,6 +38,7 @@ class ThemeResourceLoaderTest extends SapphireTest
// Fake project root
$this->base = dirname(__FILE__) . '/fixtures/templatemanifest';
+ Director::config()->set('alternate_base_folder', $this->base);
ModuleManifest::config()->set('module_priority', ['$project', '$other_modules']);
ModuleManifest::config()->set('project', 'myproject');
@@ -87,8 +89,8 @@ class ThemeResourceLoaderTest extends SapphireTest
$this->loader->findTemplate(
'NestedThemePage',
[
- 'silverstripe/module:subtheme',
- '$default'
+ 'silverstripe/module:subtheme',
+ '$default'
]
)
);
@@ -99,7 +101,7 @@ class ThemeResourceLoaderTest extends SapphireTest
$this->loader->findTemplate(
'NestedThemePage',
[
- 'silverstripe/module:subtheme',
+ 'silverstripe/module:subtheme',
]
)
);
@@ -166,8 +168,8 @@ class ThemeResourceLoaderTest extends SapphireTest
"$this->base/themes/theme/templates/Page.ss",
$this->loader->findTemplate(
[
- "$this->base/themes/theme/templates/Page.ss",
- "Page"
+ "$this->base/themes/theme/templates/Page.ss",
+ "Page"
],
['theme']
)
@@ -178,8 +180,8 @@ class ThemeResourceLoaderTest extends SapphireTest
"$this->base/themes/theme/templates/Page.ss",
$this->loader->findTemplate(
[
- "$this->base/themes/theme/templates/NotAPage.ss",
- "$this->base/themes/theme/templates/Page.ss",
+ "$this->base/themes/theme/templates/NotAPage.ss",
+ "$this->base/themes/theme/templates/Page.ss",
],
['theme']
)
@@ -310,4 +312,69 @@ class ThemeResourceLoaderTest extends SapphireTest
unlink($template);
}
}
+
+ public function providerTestGetPath()
+ {
+ return [
+ // Legacy theme
+ [
+ 'theme',
+ 'themes/theme',
+ ],
+ // Module themes
+ [
+ 'silverstripe/vendormodule:vendortheme',
+ 'vendor/silverstripe/vendormodule/themes/vendortheme',
+ ],
+ [
+ 'module:subtheme',
+ 'module/themes/subtheme',
+ ],
+ // Module absolute paths
+ [
+ 'silverstripe/vendormodule:/themes/vendortheme',
+ 'vendor/silverstripe/vendormodule/themes/vendortheme',
+ ],
+ [
+ 'module:/themes/subtheme',
+ 'module/themes/subtheme',
+ ],
+ // Module root directory
+ [
+ 'silverstripe/vendormodule:/',
+ 'vendor/silverstripe/vendormodule',
+ ],
+ [
+ 'silverstripe/vendormodule:',
+ 'vendor/silverstripe/vendormodule',
+ ],
+ [
+ 'silverstripe/vendormodule',
+ 'vendor/silverstripe/vendormodule',
+ ],
+ [
+ 'module:',
+ 'module',
+ ],
+ // Absolute paths
+ [
+ '/vendor/silverstripe/vendormodule/themes/vendortheme',
+ 'vendor/silverstripe/vendormodule/themes/vendortheme',
+ ],
+ [
+ '/module/themes/subtheme',
+ 'module/themes/subtheme'
+ ]
+ ];
+ }
+
+ /**
+ * @dataProvider providerTestGetPath
+ * @param string $name Theme identifier
+ * @param string $path Path to theme
+ */
+ public function testGetPath($name, $path)
+ {
+ $this->assertEquals($path, $this->loader->getPath($name));
+ }
}
diff --git a/tests/php/Core/Manifest/fixtures/classmanifest/vendor/silverstripe/modulec/_config/config.yml b/tests/php/Core/Manifest/fixtures/classmanifest/vendor/silverstripe/modulec/_config/config.yml
new file mode 100644
index 000000000..2191b6338
--- /dev/null
+++ b/tests/php/Core/Manifest/fixtures/classmanifest/vendor/silverstripe/modulec/_config/config.yml
@@ -0,0 +1,4 @@
+---
+Name: blankconfig
+---
+{}
diff --git a/tests/php/Core/Manifest/fixtures/classmanifest/vendor/silverstripe/modulec/code/VendorClassA.php b/tests/php/Core/Manifest/fixtures/classmanifest/vendor/silverstripe/modulec/code/VendorClassA.php
new file mode 100644
index 000000000..cd3eff06a
--- /dev/null
+++ b/tests/php/Core/Manifest/fixtures/classmanifest/vendor/silverstripe/modulec/code/VendorClassA.php
@@ -0,0 +1,6 @@
+getManifest()->getModules();
$this->assertEquals(
- array(
+ [
'i18nnonstandardmodule',
+ 'i18nothermodule',
'i18ntestmodule',
- 'i18nothermodule'
- ),
+ ],
array_keys($modules)
);