From fa57deeba4099d74a8746909e3ba1767353c465b Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Thu, 21 Sep 2017 17:54:06 +1200 Subject: [PATCH] ENHANCEMENT Allow vendor modules with url rewriting API Introduce ModuleResource feature --- .gitignore | 2 +- _config/config.yml | 5 - _config/resources.yml | 8 + .../01_Templates/06_Themes.md | 27 ++ .../How_Tos/01_Publish_a_Module.md | 104 ++++++-- docs/en/04_Changelogs/4.0.0.md | 2 + src/Control/SimpleResourceURLGenerator.php | 38 ++- src/Core/Manifest/ClassManifest.php | 2 +- src/Core/Manifest/ManifestFileFinder.php | 245 +++++++++++++++--- src/Core/Manifest/Module.php | 121 ++++++--- src/Core/Manifest/ModuleManifest.php | 46 +--- src/Core/Manifest/ModuleResource.php | 121 +++++++++ src/Core/Manifest/ResourceURLGenerator.php | 2 +- src/View/ThemeResourceLoader.php | 89 ++++--- tests/behat/src/ConfigContext.php | 2 +- .../SimpleResourceURLGeneratorTest.php | 67 +++++ .../_fakewebroot/basemodule/client/file.js | 1 + .../_fakewebroot/basemodule/client/style.css | 2 + .../silverstripe/mymodule/client/file.js | 1 + .../silverstripe/mymodule/client/style.css | 2 + tests/php/Core/Manifest/ClassManifestTest.php | 64 +++-- .../Core/Manifest/ManifestFileFinderTest.php | 56 ++-- .../php/Core/Manifest/ModuleManifestTest.php | 36 +-- .../Core/Manifest/ThemeResourceLoaderTest.php | 81 +++++- .../silverstripe/modulec/_config/config.yml | 4 + .../modulec/code/VendorClassA.php | 6 + .../modulec/code/VendorTraitA.php | 6 + .../vendor/silverstripe/modulec/composer.json | 5 + .../anothervendor/library/notamodule.txt | 0 .../vendor/myvendor/thismodule/_config.php | 1 + .../myvendor/thismodule/code/tests/tests2.txt | 0 .../vendor/myvendor/thismodule/lang/lang.txt | 0 .../vendor/myvendor/thismodule/module.txt | 0 .../myvendor/thismodule/tests/tests.txt | 0 .../templatemanifest/module/composer.json | 4 + .../silverstripe/vendormodule/_config.php | 1 + .../silverstripe/vendormodule/composer.json | 4 + .../themes/vendortheme/css/vendorstyle.css | 0 .../vendortheme/javascript/vendorscript.js | 0 tests/php/i18n/i18nTextCollectorTest.php | 6 +- 40 files changed, 886 insertions(+), 275 deletions(-) create mode 100644 _config/resources.yml create mode 100644 src/Core/Manifest/ModuleResource.php create mode 100644 tests/php/Control/SimpleResourceURLGeneratorTest.php create mode 100644 tests/php/Control/SimpleResourceURLGeneratorTest/_fakewebroot/basemodule/client/file.js create mode 100644 tests/php/Control/SimpleResourceURLGeneratorTest/_fakewebroot/basemodule/client/style.css create mode 100644 tests/php/Control/SimpleResourceURLGeneratorTest/_fakewebroot/vendor/silverstripe/mymodule/client/file.js create mode 100644 tests/php/Control/SimpleResourceURLGeneratorTest/_fakewebroot/vendor/silverstripe/mymodule/client/style.css create mode 100644 tests/php/Core/Manifest/fixtures/classmanifest/vendor/silverstripe/modulec/_config/config.yml create mode 100644 tests/php/Core/Manifest/fixtures/classmanifest/vendor/silverstripe/modulec/code/VendorClassA.php create mode 100644 tests/php/Core/Manifest/fixtures/classmanifest/vendor/silverstripe/modulec/code/VendorTraitA.php create mode 100644 tests/php/Core/Manifest/fixtures/classmanifest/vendor/silverstripe/modulec/composer.json create mode 100644 tests/php/Core/Manifest/fixtures/manifestfilefinder/vendor/anothervendor/library/notamodule.txt create mode 100644 tests/php/Core/Manifest/fixtures/manifestfilefinder/vendor/myvendor/thismodule/_config.php create mode 100644 tests/php/Core/Manifest/fixtures/manifestfilefinder/vendor/myvendor/thismodule/code/tests/tests2.txt create mode 100644 tests/php/Core/Manifest/fixtures/manifestfilefinder/vendor/myvendor/thismodule/lang/lang.txt create mode 100644 tests/php/Core/Manifest/fixtures/manifestfilefinder/vendor/myvendor/thismodule/module.txt create mode 100644 tests/php/Core/Manifest/fixtures/manifestfilefinder/vendor/myvendor/thismodule/tests/tests.txt create mode 100644 tests/php/Core/Manifest/fixtures/templatemanifest/module/composer.json create mode 100644 tests/php/Core/Manifest/fixtures/templatemanifest/vendor/silverstripe/vendormodule/_config.php create mode 100644 tests/php/Core/Manifest/fixtures/templatemanifest/vendor/silverstripe/vendormodule/composer.json create mode 100644 tests/php/Core/Manifest/fixtures/templatemanifest/vendor/silverstripe/vendormodule/themes/vendortheme/css/vendorstyle.css create mode 100644 tests/php/Core/Manifest/fixtures/templatemanifest/vendor/silverstripe/vendormodule/themes/vendortheme/javascript/vendorscript.js 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) );