ENHANCEMENT Allow vendor modules with url rewriting

API Introduce ModuleResource feature
This commit is contained in:
Damian Mooyman 2017-09-21 17:54:06 +12:00
parent 91943c03e5
commit fa57deeba4
No known key found for this signature in database
GPG Key ID: 78B823A10DE27D1A
40 changed files with 886 additions and 275 deletions

2
.gitignore vendored
View File

@ -8,6 +8,6 @@ node_modules/
coverage/ coverage/
/**/*.js.map /**/*.js.map
/**/*.css.map /**/*.css.map
vendor/ /vendor/
composer.lock composer.lock
silverstripe-cache/ silverstripe-cache/

View File

@ -1,11 +1,6 @@
--- ---
Name: coreconfig Name: coreconfig
--- ---
SilverStripe\Core\Injector\Injector:
SilverStripe\Core\Manifest\ResourceURLGenerator:
class: SilverStripe\Control\SimpleResourceURLGenerator
properties:
NonceStyle: mtime
SilverStripe\Control\HTTP: SilverStripe\Control\HTTP:
cache_control: cache_control:
max-age: 0 max-age: 0

8
_config/resources.yml Normal file
View File

@ -0,0 +1,8 @@
---
Name: coreresources
---
SilverStripe\Core\Injector\Injector:
SilverStripe\Core\Manifest\ResourceURLGenerator:
class: SilverStripe\Control\SimpleResourceURLGenerator
properties:
NonceStyle: mtime

View File

@ -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). `?flush=1` to your website URL (e.g http://yoursite.com/?flush=1).
</div> </div>
### Configuring themes
After installing the files through either method, update the current theme in SilverStripe. This can be done by 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 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) the Site Configuration panel (http://yoursite.com/admin/settings)
@ -42,6 +44,31 @@ SilverStripe\View\SSViewer:
- '$default' - '$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 ### Manually
Unpack the contents of the zip file you download into the `themes` directory in your SilverStripe installation. The Unpack the contents of the zip file you download into the `themes` directory in your SilverStripe installation. The

View File

@ -17,39 +17,87 @@ this:
**mycustommodule/composer.json** **mycustommodule/composer.json**
```js ```json
{
{ "name": "your-vendor-name/module-name",
"name": "your-vendor-name/module-name", "description": "One-liner describing your module",
"description": "One-liner describing your module", "type": "silverstripe-module",
"type": "silverstripe-module", "homepage": "http://github.com/your-vendor-name/module-name",
"homepage": "http://github.com/your-vendor-name/module-name", "keywords": ["silverstripe", "some-tag", "some-other-tag"],
"keywords": ["silverstripe", "some-tag", "some-other-tag"], "license": "BSD-3-Clause",
"license": "BSD-3-Clause", "authors": [
"authors": [ {"name": "Your Name","email": "your@email.com"}
{"name": "Your Name","email": "your@email.com"} ],
], "support": {
"support": { "issues": "http://github.com/your-vendor-name/module-name/issues"
"issues": "http://github.com/your-vendor-name/module-name/issues" },
}, "require": {
"require": { "silverstripe/cms": "^4",
"silverstripe/cms": "~3.1", "silverstripe/framework": "^4"
"silverstripe/framework": "~3.1" },
}, "extra": {
"extra": { "installer-name": "module-name",
"installer-name": "module-name", "screenshots": [
"screenshots": [ "relative/path/screenshot1.png",
"relative/path/screenshot1.png", "http://myhost.com/screenshot2.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 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 [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. 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 ## Releasing versions
Over time you may have to release new versions of your module to continue to work with newer versions of SilverStripe. 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`, 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`: 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.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` * `~3.0,>3.0.4`: Version `3.0` or higher, starting with `3.0.4`

View File

@ -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. [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. * 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. * 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.
## <a name="upgrading"></a>Upgrading ## <a name="upgrading"></a>Upgrading

View File

@ -3,6 +3,8 @@
namespace SilverStripe\Control; namespace SilverStripe\Control;
use InvalidArgumentException; use InvalidArgumentException;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Manifest\ModuleResource;
use SilverStripe\Core\Manifest\ResourceURLGenerator; use SilverStripe\Core\Manifest\ResourceURLGenerator;
/** /**
@ -11,6 +13,18 @@ use SilverStripe\Core\Manifest\ResourceURLGenerator;
*/ */
class SimpleResourceURLGenerator implements 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 * @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 * 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 * @return string Doman-relative URL
* @throws InvalidArgumentException If the resource doesn't exist * @throws InvalidArgumentException If the resource doesn't exist
*/ */
public function urlForResource($relativePath) public function urlForResource($relativePath)
{ {
$absolutePath = preg_replace('/\?.*/', '', Director::baseFolder() . '/' . $relativePath); if ($relativePath instanceof ModuleResource) {
// Load from module resource
if (!file_exists($absolutePath)) { $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"); 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 = ''; $nonce = '';
// Don't add nonce to directories // Don't add nonce to directories
if ($this->nonceStyle && is_file($absolutePath)) { if ($this->nonceStyle && is_file($absolutePath)) {

View File

@ -444,7 +444,7 @@ class ClassManifest
'name_regex' => '/^[^_].*\\.php$/', 'name_regex' => '/^[^_].*\\.php$/',
'ignore_files' => array('index.php', 'main.php', 'cli-script.php'), 'ignore_files' => array('index.php', 'main.php', 'cli-script.php'),
'ignore_tests' => !$includeTests, 'ignore_tests' => !$includeTests,
'file_callback' => function ($basename, $pathname) use ($includeTests) { 'file_callback' => function ($basename, $pathname, $depth) use ($includeTests, $finder) {
$this->handleFile($basename, $pathname, $includeTests); $this->handleFile($basename, $pathname, $includeTests);
}, },
)); ));

View File

@ -16,63 +16,230 @@ use SilverStripe\Assets\FileFinder;
class ManifestFileFinder extends FileFinder class ManifestFileFinder extends FileFinder
{ {
const CONFIG_FILE = '_config.php'; const CONFIG_FILE = '_config.php';
const CONFIG_DIR = '_config'; const CONFIG_DIR = '_config';
const EXCLUDE_FILE = '_manifest_exclude'; const EXCLUDE_FILE = '_manifest_exclude';
const LANG_DIR = 'lang'; const LANG_DIR = 'lang';
const TESTS_DIR = 'tests'; const TESTS_DIR = 'tests';
const VENDOR_DIR = 'vendor';
const RESOURCES_DIR = 'resources';
protected static $default_options = array( protected static $default_options = array(
'include_themes' => false, 'include_themes' => false,
'ignore_tests' => true, 'ignore_tests' => true,
'min_depth' => 1, 'min_depth' => 1,
'ignore_dirs' => array('node_modules') 'ignore_dirs' => ['node_modules']
); );
public function acceptDir($basename, $pathname, $depth) public function acceptDir($basename, $pathname, $depth)
{ {
// Skip over the assets directory in the site root. // Skip if ignored
if ($depth == 1 && $basename == ASSETS_DIR) { if ($this->isInsideIgnored($basename, $pathname, $depth)) {
return false; return false;
} }
// Skip over any lang directories in the top level of the module. // Keep searching inside vendor
if ($depth == 2 && $basename == self::LANG_DIR) { $inVendor = $this->isInsideVendor($basename, $pathname, $depth);
return false; 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 // Include themes
if (($depth == 1 || $depth == 2) && $basename == 'vendor') { if ($this->getOption('include_themes') && $this->isInsideThemes($basename, $pathname, $depth)) {
return false; return true;
} }
// If we're not in testing mode, then skip over any tests directories. // Skip if not in module
if ($this->getOption('ignore_tests') && $basename == self::TESTS_DIR) { if (!$this->isInsideModule($basename, $pathname, $depth)) {
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) {
return false; return false;
} }
return parent::acceptDir($basename, $pathname, $depth); 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;
}
} }

View File

@ -4,19 +4,21 @@ namespace SilverStripe\Core\Manifest;
use Exception; use Exception;
use Serializable; use Serializable;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\Deprecation;
class Module implements Serializable class Module implements Serializable
{ {
const TRIM_CHARS = '/\\';
/** /**
* Directory * Full directory path to this module with no trailing slash
* *
* @var string * @var string
*/ */
protected $path = null; protected $path = null;
/** /**
* Base folder of application * Base folder of application with no trailing slash
* *
* @var string * @var string
*/ */
@ -29,10 +31,23 @@ class Module implements Serializable
*/ */
protected $composerData = null; 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) public function __construct($path, $base)
{ {
$this->path = rtrim($path, '/\\'); $this->path = rtrim($path, self::TRIM_CHARS);
$this->basePath = rtrim($base, '/\\'); $this->basePath = rtrim($base, self::TRIM_CHARS);
$this->loadComposer(); $this->loadComposer();
} }
@ -62,6 +77,19 @@ class Module implements Serializable
return null; 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 * Gets "short" name of this module. This is the base directory this module
* is installed in. * is installed in.
@ -94,7 +122,7 @@ class Module implements Serializable
/** /**
* Get base path for this module * Get base path for this module
* *
* @return string * @return string Path with no trailing slash E.g. /var/www/module
*/ */
public function getPath() public function getPath()
{ {
@ -105,11 +133,11 @@ class Module implements Serializable
* Get path relative to base dir. * Get path relative to base dir.
* If module path is base this will be empty string * 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() 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() public function serialize()
@ -120,6 +148,7 @@ class Module implements Serializable
public function unserialize($serialized) public function unserialize($serialized)
{ {
list($this->path, $this->basePath, $this->composerData) = json_decode($serialized, true); 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. * Get resource for this module
* Directories included
* *
* This method makes no distinction between public / local resources, * @param string $path
* which may change in the near future. * @return ModuleResource
* */
* @internal Experimental API and may change public function getResource($path)
* @param string $path File or directory path relative to module directory {
* @return string Path relative to base directory $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) public function getRelativeResourcePath($path)
{ {
$base = trim($this->getRelativePath(), '/\\'); Deprecation::notice('5.0', 'Use getResource($path)->getRelativePath() instead');
$path = trim($path, '/\\'); return $this
return trim("{$base}/{$path}", '/\\'); ->getResource($path)
->getRelativePath();
} }
/** /**
* Gets path to physical file resource relative to base directory. * @deprecated 4.0...5.0 Use ->getResource($path)->getPath() instead
* Directories included * @param string $path
* * @return string
* 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
*/ */
public function getResourcePath($path) 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. * @deprecated 4.0...5.0 Use ->getResource($path)->getURL() instead
* Relies on the ModuleURLGenerator Injector service to do the heavy lifting * @param string $path
* * @return string
* @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
*/ */
public function getResourceURL($path) public function getResourceURL($path)
{ {
return Injector::inst() Deprecation::notice('5.0', 'Use getResource($path)->getURL() instead');
->get(ResourceURLGenerator::class) return $this
->urlForResource($this->getRelativeResourcePath($path)); ->getResource($path)
->getURL();
} }
/** /**
* Check if this module has a given resource * @deprecated 4.0...5.0 Use ->getResource($path)->exists() instead
*
* @internal Experimental API and may change
* @param string $path * @param string $path
* @return bool * @return string
*/ */
public function hasResource($path) public function hasResource($path)
{ {
return file_exists($this->getResourcePath($path)); Deprecation::notice('5.0', 'Use getResource($path)->exists() instead');
return $this
->getResource($path)
->exists();
} }
} }

View File

@ -5,7 +5,6 @@ namespace SilverStripe\Core\Manifest;
use LogicException; use LogicException;
use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Cache\CacheFactory; use SilverStripe\Core\Cache\CacheFactory;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
@ -171,52 +170,25 @@ class ModuleManifest
$finder = new ManifestFileFinder(); $finder = new ManifestFileFinder();
$finder->setOptions(array( $finder->setOptions(array(
'min_depth' => 0, 'min_depth' => 0,
'name_regex' => '/(^|[\/\\\\])_config.php$/',
'ignore_tests' => !$includeTests, 'ignore_tests' => !$includeTests,
'file_callback' => array($this, 'addSourceConfigFile'), 'dir_callback' => function ($basename, $pathname, $depth) use ($finder) {
// Cannot be max_depth: 1 due to "/framework/admin/_config.php" if ($finder->isDirectoryModule($basename, $pathname, $depth)) {
'max_depth' => 2 $this->addModule($pathname);
}
}
)); ));
$finder->find($this->base); $finder->find($this->base);
$finder = new ManifestFileFinder(); // Include root itself if module
$finder->setOptions(array( if ($finder->isDirectoryModule(basename($this->base), $this->base, 0)) {
'name_regex' => '/\.ya?ml$/', $this->addModule($this->base);
'ignore_tests' => !$includeTests, }
'file_callback' => array($this, 'addYAMLConfigFile'),
'max_depth' => 2
));
$finder->find($this->base);
if ($this->cache) { if ($this->cache) {
$this->cache->set($this->cacheKey, $this->modules); $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 * Get module by name
* *

View File

@ -0,0 +1,121 @@
<?php
namespace SilverStripe\Core\Manifest;
use InvalidArgumentException;
use SilverStripe\Core\Injector\Injector;
/**
* This object represents a single resource file attached to a module, and can be used
* as a reference to this to be later turned into either a URL or file path.
*/
class ModuleResource
{
/**
* @var Module
*/
protected $module = null;
/**
* Path to this resource relative to the module (no leading slash)
*
* @var string
*/
protected $relativePath = null;
/**
* ModuleResource constructor.
*
* @param Module $module
* @param string $relativePath
*/
public function __construct(Module $module, $relativePath)
{
$this->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;
}
}

View File

@ -15,7 +15,7 @@ interface ResourceURLGenerator
* As well as returning the URL, this method may also perform any changes needed to ensure that this * 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 * 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 * @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 * @throws InvalidArgumentException If the resource doesn't exist or can't be sent to the browser
*/ */

View File

@ -2,6 +2,8 @@
namespace SilverStripe\View; namespace SilverStripe\View;
use InvalidArgumentException;
use RuntimeException;
use SilverStripe\Core\Manifest\ModuleLoader; use SilverStripe\Core\Manifest\ModuleLoader;
/** /**
@ -95,56 +97,59 @@ class ThemeResourceLoader
public function getPath($identifier) public function getPath($identifier)
{ {
$slashPos = strpos($identifier, '/'); $slashPos = strpos($identifier, '/');
$parts = explode(':', $identifier, 2);
// If identifier starts with "/", it's a path from root // If identifier starts with "/", it's a path from root
if ($slashPos === 0) { if ($slashPos === 0) {
return substr($identifier, 1);
} // Otherwise if there is a "/", identifier is a vendor'ed module
elseif ($slashPos !== false) {
// 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.
$parts = explode(':', $identifier, 2);
if (count($parts) > 1) { if (count($parts) > 1) {
$theme = $parts[1]; throw new InvalidArgumentException("Invalid theme identifier {$identifier}");
// "module/vendor:/sub/path"
if ($theme[0] === '/') {
$subpath = $theme;
// "module/vendor:subtheme"
} else {
$subpath = '/themes/' . $theme;
}
// "module/vendor"
} else {
$subpath = '';
} }
return substr($identifier, 1);
}
$package = $parts[0]; // If there is no slash / colon it's a legacy theme
if ($slashPos === false && count($parts) === 1) {
// 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/<module>/ 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 {
return THEMES_DIR.'/'.$identifier; return 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 $modulePath . $subpath;
} }
/** /**

View File

@ -120,7 +120,7 @@ class ConfigContext implements Context
$project = ModuleManifest::config()->get('project') ?: 'mysite'; $project = ModuleManifest::config()->get('project') ?: 'mysite';
$mysite = ModuleLoader::getModule($project); $mysite = ModuleLoader::getModule($project);
assertNotNull($mysite, 'Project exists'); 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"); assertFileNotExists($destPath, "Config file {$filename} hasn't aleady been loaded");
// Load // Load

View File

@ -0,0 +1,67 @@
<?php
namespace SilverStripe\Control\Tests;
use SilverStripe\Control\Director;
use SilverStripe\Control\SimpleResourceURLGenerator;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Manifest\Module;
use SilverStripe\Core\Manifest\ResourceURLGenerator;
use SilverStripe\Dev\SapphireTest;
class SimpleResourceURLGeneratorTest extends SapphireTest
{
protected function setUp()
{
parent::setUp();
Director::config()->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'))
);
}
}

View File

@ -0,0 +1 @@
/* basemodule/file.js */

View File

@ -0,0 +1,2 @@
/* basemodule/style.css */
body {}

View File

@ -0,0 +1 @@
/* mymodule/file.js */

View File

@ -0,0 +1,2 @@
/* mymodule/style.css */
body {}

View File

@ -32,39 +32,51 @@ class ClassManifestTest extends SapphireTest
parent::setUp(); parent::setUp();
$this->base = dirname(__FILE__) . '/fixtures/classmanifest'; $this->base = dirname(__FILE__) . '/fixtures/classmanifest';
$this->manifest = new ClassManifest($this->base); $this->manifest = new ClassManifest($this->base);
$this->manifest->init(false); $this->manifest->init(false);
$this->manifestTests = new ClassManifest($this->base); $this->manifestTests = new ClassManifest($this->base);
$this->manifestTests->init(true); $this->manifestTests->init(true);
} }
public function testGetItemPath() /**
* @return array
*/
public function providerTestGetItemPath()
{ {
$expect = array( return [
'CLASSA' => 'module/classes/ClassA.php', ['CLASSA', 'module/classes/ClassA.php'],
'ClassA' => 'module/classes/ClassA.php', ['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', ['InterfaceA', 'module/interfaces/InterfaceA.php'],
'interfacea' => 'module/interfaces/InterfaceA.php', ['interfacea', 'module/interfaces/InterfaceA.php'],
'TestTraitA' => 'module/traits/TestTraitA.php', ['TestTraitA', 'module/traits/TestTraitA.php'],
'TestNamespace\Testing\TestTraitB' => 'module/traits/TestTraitB.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() public function testGetClasses()
{ {
$expect = array( $expect = [
'classa' => "{$this->base}/module/classes/ClassA.php", 'classa' => "{$this->base}/module/classes/ClassA.php",
'classb' => "{$this->base}/module/classes/ClassB.php", 'classb' => "{$this->base}/module/classes/ClassB.php",
'classc' => "{$this->base}/module/classes/ClassC.php", 'classc' => "{$this->base}/module/classes/ClassC.php",
'classd' => "{$this->base}/module/classes/ClassD.php", 'classd' => "{$this->base}/module/classes/ClassD.php",
'classe' => "{$this->base}/module/classes/ClassE.php", 'classe' => "{$this->base}/module/classes/ClassE.php",
); 'vendorclassa' => "{$this->base}/vendor/silverstripe/modulec/code/VendorClassA.php",
];
$this->assertEquals($expect, $this->manifest->getClasses()); $this->assertEquals($expect, $this->manifest->getClasses());
} }
@ -77,6 +89,7 @@ class ClassManifestTest extends SapphireTest
'classc' => 'ClassC', 'classc' => 'ClassC',
'classd' => 'ClassD', 'classd' => 'ClassD',
'classe' => 'ClassE', 'classe' => 'ClassE',
'vendorclassa' => 'VendorClassA',
], ],
$this->manifest->getClassNames() $this->manifest->getClassNames()
); );
@ -85,10 +98,11 @@ class ClassManifestTest extends SapphireTest
public function testGetTraitNames() public function testGetTraitNames()
{ {
$this->assertEquals( $this->assertEquals(
array( [
'testtraita' => 'TestTraitA', 'testtraita' => 'TestTraitA',
'testnamespace\testing\testtraitb' => 'TestNamespace\Testing\TestTraitB', 'testnamespace\\testing\\testtraitb' => 'TestNamespace\\Testing\\TestTraitB',
), 'vendortraita' => 'VendorTraitA',
],
$this->manifest->getTraitNames() $this->manifest->getTraitNames()
); );
} }

View File

@ -10,16 +10,23 @@ use SilverStripe\Dev\SapphireTest;
*/ */
class ManifestFileFinderTest extends SapphireTest class ManifestFileFinderTest extends SapphireTest
{ {
protected $defaultBase;
protected $base;
public function __construct() public function __construct()
{ {
$this->defaultBase = dirname(__FILE__) . '/fixtures/manifestfilefinder'; $this->defaultBase = __DIR__ . '/fixtures/manifestfilefinder';
parent::__construct(); 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) { if (!$base) {
$base = $this->defaultBase; $base = $this->defaultBase;
@ -45,9 +52,10 @@ class ManifestFileFinderTest extends SapphireTest
$this->assertFinderFinds( $this->assertFinderFinds(
$finder, $finder,
null, null,
array( [
'module/module.txt' 'module/module.txt',
) 'vendor/myvendor/thismodule/module.txt',
]
); );
} }
@ -60,11 +68,14 @@ class ManifestFileFinderTest extends SapphireTest
$this->assertFinderFinds( $this->assertFinderFinds(
$finder, $finder,
null, null,
array( [
'module/module.txt', 'module/module.txt',
'module/tests/tests.txt', 'module/tests/tests.txt',
'module/code/tests/tests2.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( $this->assertFinderFinds(
$finder, $finder,
null, null,
array( [
'module/module.txt', 'module/module.txt',
'themes/themes.txt' 'themes/themes.txt',
) 'vendor/myvendor/thismodule/module.txt',
]
); );
} }
@ -90,10 +102,8 @@ class ManifestFileFinderTest extends SapphireTest
$this->assertFinderFinds( $this->assertFinderFinds(
$finder, $finder,
dirname(__FILE__) . '/fixtures/manifestfilefinder_rootconfigfile', __DIR__ . '/fixtures/manifestfilefinder_rootconfigfile',
array( [ 'code/code.txt' ]
'code/code.txt',
)
); );
} }
@ -103,11 +113,11 @@ class ManifestFileFinderTest extends SapphireTest
$this->assertFinderFinds( $this->assertFinderFinds(
$finder, $finder,
dirname(__FILE__) . '/fixtures/manifestfilefinder_rootconfigfolder', __DIR__ . '/fixtures/manifestfilefinder_rootconfigfolder',
array( [
'_config/config.yml', '_config/config.yml',
'code/code.txt', 'code/code.txt',
) ]
); );
} }
} }

View File

@ -31,9 +31,10 @@ class ModuleManifestTest extends SapphireTest
$modules = $this->manifest->getModules(); $modules = $this->manifest->getModules();
$this->assertEquals( $this->assertEquals(
[ [
'silverstripe/root-module',
'module', 'module',
'silverstripe/awesome-module', 'silverstripe/awesome-module',
'silverstripe/modulec',
'silverstripe/root-module',
], ],
array_keys($modules) array_keys($modules)
); );
@ -71,32 +72,37 @@ class ModuleManifestTest extends SapphireTest
$this->assertEquals('moduleb', $module->getRelativePath()); $this->assertEquals('moduleb', $module->getRelativePath());
} }
/*
* Note: Tests experimental API
* @internal
*/
public function testGetResourcePath() public function testGetResourcePath()
{ {
$module = $this->manifest->getModule('moduleb'); // Root module
$this->assertTrue($module->hasResource('composer.json')); $moduleb = $this->manifest->getModule('moduleb');
$this->assertFalse($module->hasResource('package.json')); $this->assertTrue($moduleb->getResource('composer.json')->exists());
$this->assertFalse($moduleb->getResource('package.json')->exists());
$this->assertEquals( $this->assertEquals(
'moduleb/composer.json', '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() public function testGetResourcePathOnRoot()
{ {
$module = $this->manifest->getModule('silverstripe/root-module'); $module = $this->manifest->getModule('silverstripe/root-module');
$this->assertTrue($module->hasResource('composer.json')); $this->assertTrue($module->getResource('composer.json')->exists());
$this->assertEquals( $this->assertEquals(
'composer.json', 'composer.json',
$module->getRelativeResourcePath('composer.json') $module->getResource('composer.json')->getRelativePath()
); );
} }
} }

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Core\Tests\Manifest; namespace SilverStripe\Core\Tests\Manifest;
use SilverStripe\Control\Director;
use SilverStripe\Core\Manifest\ModuleLoader; use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\View\ThemeResourceLoader; use SilverStripe\View\ThemeResourceLoader;
use SilverStripe\View\ThemeManifest; use SilverStripe\View\ThemeManifest;
@ -37,6 +38,7 @@ class ThemeResourceLoaderTest extends SapphireTest
// Fake project root // Fake project root
$this->base = dirname(__FILE__) . '/fixtures/templatemanifest'; $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('module_priority', ['$project', '$other_modules']);
ModuleManifest::config()->set('project', 'myproject'); ModuleManifest::config()->set('project', 'myproject');
@ -87,8 +89,8 @@ class ThemeResourceLoaderTest extends SapphireTest
$this->loader->findTemplate( $this->loader->findTemplate(
'NestedThemePage', 'NestedThemePage',
[ [
'silverstripe/module:subtheme', 'silverstripe/module:subtheme',
'$default' '$default'
] ]
) )
); );
@ -99,7 +101,7 @@ class ThemeResourceLoaderTest extends SapphireTest
$this->loader->findTemplate( $this->loader->findTemplate(
'NestedThemePage', 'NestedThemePage',
[ [
'silverstripe/module:subtheme', 'silverstripe/module:subtheme',
] ]
) )
); );
@ -166,8 +168,8 @@ class ThemeResourceLoaderTest extends SapphireTest
"$this->base/themes/theme/templates/Page.ss", "$this->base/themes/theme/templates/Page.ss",
$this->loader->findTemplate( $this->loader->findTemplate(
[ [
"$this->base/themes/theme/templates/Page.ss", "$this->base/themes/theme/templates/Page.ss",
"Page" "Page"
], ],
['theme'] ['theme']
) )
@ -178,8 +180,8 @@ class ThemeResourceLoaderTest extends SapphireTest
"$this->base/themes/theme/templates/Page.ss", "$this->base/themes/theme/templates/Page.ss",
$this->loader->findTemplate( $this->loader->findTemplate(
[ [
"$this->base/themes/theme/templates/NotAPage.ss", "$this->base/themes/theme/templates/NotAPage.ss",
"$this->base/themes/theme/templates/Page.ss", "$this->base/themes/theme/templates/Page.ss",
], ],
['theme'] ['theme']
) )
@ -310,4 +312,69 @@ class ThemeResourceLoaderTest extends SapphireTest
unlink($template); 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));
}
} }

View File

@ -0,0 +1,4 @@
---
Name: blankconfig
---
{}

View File

@ -0,0 +1,6 @@
<?php
class VendorClassA
{
}

View File

@ -0,0 +1,6 @@
<?php
trait VendorTraitA
{
}

View File

@ -0,0 +1,5 @@
{
"name": "silverstripe/modulec",
"description": "dummy test module",
"require": {}
}

View File

@ -0,0 +1,4 @@
{
"name": "silverstripe/module",
"type": "silverstripe-module"
}

View File

@ -0,0 +1,4 @@
{
"name": "silverstripe/vendormodule",
"type": "silverstripe-vendormodule"
}

View File

@ -712,11 +712,11 @@ PHP;
$collector = new Collector(); $collector = new Collector();
$modules = ModuleLoader::inst()->getManifest()->getModules(); $modules = ModuleLoader::inst()->getManifest()->getModules();
$this->assertEquals( $this->assertEquals(
array( [
'i18nnonstandardmodule', 'i18nnonstandardmodule',
'i18nothermodule',
'i18ntestmodule', 'i18ntestmodule',
'i18nothermodule' ],
),
array_keys($modules) array_keys($modules)
); );