Merge pull request #6706 from open-sausages/pulls/4.0/manifest-cache

API Build new ManifestCache based on PSR-16 SimpleCache
This commit is contained in:
Sam Minnée 2017-03-21 10:29:03 +13:00 committed by GitHub
commit cfa6a36697
31 changed files with 666 additions and 689 deletions

View File

@ -4,9 +4,8 @@ Before: '/i18n'
--- ---
SilverStripe\i18n\Data\Sources: SilverStripe\i18n\Data\Sources:
module_priority: module_priority:
- admin - silverstripe\admin
- framework - silverstripe\framework
- sapphire
--- ---
Name: defaulti18n Name: defaulti18n
--- ---

View File

@ -85,6 +85,6 @@ SilverStripe core environment variables are listed here, though you're free to d
| `SS_TRUSTED_PROXY_HOST_HEADER` | Used to define the proxy header to be used to determine the requested host name | | `SS_TRUSTED_PROXY_HOST_HEADER` | Used to define the proxy header to be used to determine the requested host name |
| `SS_TRUSTED_PROXY_IPS` | IP address or CIDR range to trust proxy headers from | | `SS_TRUSTED_PROXY_IPS` | IP address or CIDR range to trust proxy headers from |
| `SS_ALLOWED_HOSTS` | A comma deliminated list of hostnames the site is allowed to respond to | | `SS_ALLOWED_HOSTS` | A comma deliminated list of hostnames the site is allowed to respond to |
| `SS_MANIFESTCACHE` | The manifest cache to use (defaults to file based caching) | | `SS_MANIFESTCACHE` | The manifest cache to use (defaults to file based caching). Must be a CacheInterface or CacheFactory class name |
| `SS_IGNORE_DOT_ENV` | If set the .env file will be ignored. This is good for live to mitigate any performance implications of loading the .env file | | `SS_IGNORE_DOT_ENV` | If set the .env file will be ignored. This is good for live to mitigate any performance implications of loading the .env file |
| `SS_HOST` | The hostname to use when it isn't determinable by other means (eg: for CLI commands) | | `SS_HOST` | The hostname to use when it isn't determinable by other means (eg: for CLI commands) |

View File

@ -11,17 +11,10 @@ Others store aggregate information like nested configuration graphs.
## Storage ## Storage
By default, manifests are stored on the local filesystem through PHP's `serialize()` method. By default, manifests are serialised and cached via a cache generated by the [api:ManifestCacheFactory].
Combined with PHP opcode caching this provides fast access. This can be customised via `SS_MANIFESTCACHE` environment variable to point to either another
In order to share manifests between servers, or centralise cache management, [api:CacheFactory] or [CacheInterface](https://github.com/php-fig/cache/blob/master/src/CacheItemInterface.php)
other storage adapters are available. These can be configured by a `SS_MANIFESTCACHE` constant, implementor.
placed in your `.env`.
* `ManifestCache_File`: The default adapter using PHP's `serialize()`
* `ManifestCache_File_PHP`: Using `var_export()`, which is faster when a PHP opcode cache is installed
* `ManifestCache_APC`: Use PHP's [APC object cache](http://php.net/manual/en/book.apc.php)
You can write your own adapters by implementing the `ManifestCache` interface.
## Traversing the Filesystem ## Traversing the Filesystem

View File

@ -1067,6 +1067,7 @@ now generally safer to use the default inherited config, where in the past you w
* Falsey config values (null, 0, false, etc) can now replace non-falsey values. * Falsey config values (null, 0, false, etc) can now replace non-falsey values.
* Introduced new ModuleLoader manifest, which allows modules to be found via composer name. * Introduced new ModuleLoader manifest, which allows modules to be found via composer name.
E.g. `$cms = ModuleLoader::instance()->getManifest()->getModule('silverstripe/cms')` E.g. `$cms = ModuleLoader::instance()->getManifest()->getModule('silverstripe/cms')`
* `ClassManifest::getOwnerModule()` now returns a `Module` object instance.
* Certain methods have been moved from `Controller` to `RequestHandler`: * Certain methods have been moved from `Controller` to `RequestHandler`:
* `Link` * `Link`
* `redirect` * `redirect`
@ -1081,7 +1082,7 @@ now generally safer to use the default inherited config, where in the past you w
#### <a name="overview-general-removed"></a>General and Core Removed API #### <a name="overview-general-removed"></a>General and Core Removed API
* `CMSMain::buildbrokenlinks()` action is removed. * `CMSMain::buildbrokenlinks()` action is removed.
* `SS_Log::add_writer()` method is removed. * `SS_Log` class has been removed. Use `Injector::inst()->get(LoggerInterface::class)` instead.
* Removed `CMSBatchAction_Delete` * Removed `CMSBatchAction_Delete`
* Removed `CMSBatchAction_DeleteFromLive` * Removed `CMSBatchAction_DeleteFromLive`
* Removed `CMSMain.enabled_legacy_actions` config. * Removed `CMSMain.enabled_legacy_actions` config.
@ -1101,6 +1102,8 @@ now generally safer to use the default inherited config, where in the past you w
* Removed `SilverStripeInjectionCreator` * Removed `SilverStripeInjectionCreator`
* Removed `i18n::get_translatable_modules` method. * Removed `i18n::get_translatable_modules` method.
* Removed `i18nTextCollector_Writer_Php` * Removed `i18nTextCollector_Writer_Php`
* `i18nTextCollector` no longer collects from `themes/<theme>` root dir.
Modules which provide themes via `<moduleName>/themes/<theme>` are now preferred.
* Removed `i18nSSLegacyAdapter` * Removed `i18nSSLegacyAdapter`
* Removed `FunctionalTest::stat` * Removed `FunctionalTest::stat`
* Removed `LeftAndMainMarkingFilter` * Removed `LeftAndMainMarkingFilter`
@ -1116,6 +1119,11 @@ now generally safer to use the default inherited config, where in the past you w
* Removed `Config::FIRST_SET` and `Config::INHERITED` * Removed `Config::FIRST_SET` and `Config::INHERITED`
* Removed `RequestHandler.require_allowed_actions`. This is now fixed to on and cannot be * Removed `RequestHandler.require_allowed_actions`. This is now fixed to on and cannot be
disabled. disabled.
* Config or module searching methods have been removed from `ClassManifest`. Use `ModuleLoader`
to get this information instead:
- `getModules`
- `getConfigDirs`
- `getConfigs`
#### <a name="overview-general-deprecated"></a>General and Core Deprecated API #### <a name="overview-general-deprecated"></a>General and Core Deprecated API
@ -1567,6 +1575,7 @@ New `TimeField` methods replace `getConfig()` / `setConfig()`
* `i18n::get_language_name()` moved to `SilverStripe\i18n\Data\Locales::languageName()` * `i18n::get_language_name()` moved to `SilverStripe\i18n\Data\Locales::languageName()`
* `i18n.module_priority` config moved to `SilverStripe\i18n\Data\Sources.module_priority` * `i18n.module_priority` config moved to `SilverStripe\i18n\Data\Sources.module_priority`
* `i18n::get_owner_module()` moved to `SilverStripe\Core\Manifest\ClassManifest::getOwnerModule()` * `i18n::get_owner_module()` moved to `SilverStripe\Core\Manifest\ClassManifest::getOwnerModule()`
This now returns a `Module` object instance instead of a string.
* `i18n::get_existing_translations()` moved to `SilverStripe\i18n\Data\Sources::getKnownLocales()` * `i18n::get_existing_translations()` moved to `SilverStripe\i18n\Data\Sources::getKnownLocales()`
#### <a name="overview-i18n-removed"></a>i18n API Removed API #### <a name="overview-i18n-removed"></a>i18n API Removed API
@ -1592,6 +1601,7 @@ and have a slightly different API (e.g. `set()` instead of `save()`).
Before: Before:
:::php :::php
$cache = Cache::factory('myCache'); $cache = Cache::factory('myCache');
@ -1612,6 +1622,7 @@ Before:
After: After:
:::php :::php
use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\CacheInterface;
$cache = Injector::inst()->get(CacheInterface::class . '.myCache'); $cache = Injector::inst()->get(CacheInterface::class . '.myCache');
@ -1629,6 +1640,21 @@ After:
$cache->delete('myCacheKey'); $cache->delete('myCacheKey');
With the necessary minimal config in `_config/mycache.yml`
:::yml
---
Name: mycache
---
SilverStripe\Core\Injector\Injector:
Psr\SimpleCache\CacheInterface.myCache:
factory: SilverStripe\Core\Cache\CacheFactory
constructor:
namespace: 'mycache'
#### Configuration Changes #### Configuration Changes
Caches are now configured through dependency injection services instead of PHP. Caches are now configured through dependency injection services instead of PHP.

View File

@ -0,0 +1,91 @@
<?php
namespace SilverStripe\Core\Cache;
use BadMethodCallException;
use Monolog\Handler\ErrorLogHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use ReflectionClass;
use SilverStripe\Control\Director;
/**
* Assists with building of manifest cache prior to config being available
*/
class ManifestCacheFactory extends DefaultCacheFactory
{
public function __construct(array $args = [], LoggerInterface $logger = null)
{
// Build default manifest logger
if (!$logger) {
$logger = new Logger("manifestcache-log");
if (Director::isDev()) {
$logger->pushHandler(new StreamHandler('php://output'));
} else {
$logger->pushHandler(new ErrorLogHandler());
}
}
parent::__construct($args, $logger);
}
/**
* Note: While the returned object is used as a singleton (by the originating Injector->get() call),
* this cache object shouldn't be a singleton itself - it has varying constructor args for the same service name.
*
* @param string $service The class name of the service.
* @param array $params The constructor parameters.
* @return CacheInterface
*/
public function create($service, array $params = array())
{
// Override default cache generation with SS_MANIFESTCACHE
$cacheClass = getenv('SS_MANIFESTCACHE');
if (!$cacheClass) {
return parent::create($service, $params);
}
// Check if SS_MANIFESTCACHE is a factory
if (is_a($cacheClass, CacheFactory::class, true)) {
/** @var CacheFactory $factory */
$factory = new $cacheClass;
return $factory->create($service, $params);
}
// Check if SS_MANIFESTCACHE is a cache subclass
if (is_a($cacheClass, CacheInterface::class, true)) {
$args = array_merge($this->args, $params);
$namespace = isset($args['namespace']) ? $args['namespace'] : '';
return $this->createCache($cacheClass, [$namespace]);
}
// Validate type
throw new BadMethodCallException(
'SS_MANIFESTCACHE is not a valid CacheInterface or CacheFactory class name'
);
}
/**
* Create cache directly without config / injector
*
* @param string $class
* @param array $args
* @return CacheInterface
*/
public function createCache($class, $args)
{
/** @var CacheInterface $cache */
$reflection = new ReflectionClass($class);
$cache = $reflection->newInstanceArgs($args);
// Assign cache logger
if ($this->logger && $cache instanceof LoggerAwareInterface) {
$cache->setLogger($this->logger);
}
return $cache;
}
}

View File

@ -6,11 +6,13 @@ use Monolog\Handler\ErrorLogHandler;
use Monolog\Handler\StreamHandler; use Monolog\Handler\StreamHandler;
use Monolog\Logger; use Monolog\Logger;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Config\Collections\CachedConfigCollection; use SilverStripe\Config\Collections\CachedConfigCollection;
use SilverStripe\Config\Collections\MemoryConfigCollection; use SilverStripe\Config\Collections\MemoryConfigCollection;
use SilverStripe\Config\Transformer\PrivateStaticTransformer; use SilverStripe\Config\Transformer\PrivateStaticTransformer;
use SilverStripe\Config\Transformer\YamlTransformer; use SilverStripe\Config\Transformer\YamlTransformer;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Core\Cache\CacheFactory;
use SilverStripe\Core\Config\Middleware\ExtensionMiddleware; use SilverStripe\Core\Config\Middleware\ExtensionMiddleware;
use SilverStripe\Core\Config\Middleware\InheritanceMiddleware; use SilverStripe\Core\Config\Middleware\InheritanceMiddleware;
use SilverStripe\Core\Manifest\ClassLoader; use SilverStripe\Core\Manifest\ClassLoader;
@ -45,14 +47,18 @@ class CoreConfigFactory
* which conditionally generates a nested "core" config. * which conditionally generates a nested "core" config.
* *
* @param bool $flush * @param bool $flush
* @param CacheFactory $cacheFactory
* @return CachedConfigCollection * @return CachedConfigCollection
*/ */
public function createRoot($flush) public function createRoot($flush, CacheFactory $cacheFactory)
{ {
$instance = new CachedConfigCollection(); $instance = new CachedConfigCollection();
// Set root cache // Create config cache
$instance->setPool($this->createPool()); $cache = $cacheFactory->create(CacheInterface::class.'.configcache', [
'namespace' => 'configcache'
]);
$instance->setCache($cache);
$instance->setFlush($flush); $instance->setFlush($flush);
// Set collection creator // Set collection creator
@ -172,32 +178,4 @@ class CoreConfigFactory
return ModuleLoader::instance()->getManifest()->moduleExists($module); return ModuleLoader::instance()->getManifest()->moduleExists($module);
}); });
} }
/**
* @todo Refactor bootstrapping of manifest caching into app object
* @return FilesystemAdapter
*/
protected function createPool()
{
$cache = new FilesystemAdapter('configcache', 0, getTempFolder());
$cache->setLogger($this->createLogger());
return $cache;
}
/**
* Create default error logger
*
* @todo Refactor bootstrapping of manifest logging into app object
* @return LoggerInterface
*/
protected function createLogger()
{
$logger = new Logger("configcache-log");
if (Director::isDev()) {
$logger->pushHandler(new StreamHandler('php://output'));
} else {
$logger->pushHandler(new ErrorLogHandler());
}
return $logger;
}
} }

View File

@ -1,5 +1,6 @@
<?php <?php
use SilverStripe\Core\Cache\ManifestCacheFactory;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Config\CoreConfigFactory; use SilverStripe\Core\Config\CoreConfigFactory;
use SilverStripe\Core\Config\ConfigLoader; use SilverStripe\Core\Config\ConfigLoader;
@ -70,8 +71,14 @@ Injector::set_inst($injector);
$requestURL = isset($_REQUEST['url']) ? trim($_REQUEST['url'], '/') : false; $requestURL = isset($_REQUEST['url']) ? trim($_REQUEST['url'], '/') : false;
$flush = (isset($_GET['flush']) || $requestURL === trim(BASE_URL . '/dev/build', '/')); $flush = (isset($_GET['flush']) || $requestURL === trim(BASE_URL . '/dev/build', '/'));
global $manifest; // Manifest cache factory
$manifest = new ClassManifest(BASE_PATH, false, $flush); $manifestCacheFactory = new ManifestCacheFactory([
'namespace' => 'manifestcache',
'directory' => getTempFolder(),
]);
// Build class manifest
$manifest = new ClassManifest(BASE_PATH, false, $flush, $manifestCacheFactory);
// Register SilverStripe's class map autoload // Register SilverStripe's class map autoload
$loader = ClassLoader::instance(); $loader = ClassLoader::instance();
@ -79,11 +86,11 @@ $loader->registerAutoloader();
$loader->pushManifest($manifest); $loader->pushManifest($manifest);
// Init module manifest // Init module manifest
$moduleManifest = new ModuleManifest(BASE_PATH, false, $flush); $moduleManifest = new ModuleManifest(BASE_PATH, false, $flush, $manifestCacheFactory);
ModuleLoader::instance()->pushManifest($moduleManifest); ModuleLoader::instance()->pushManifest($moduleManifest);
// Build config manifest // Build config manifest
$configManifest = CoreConfigFactory::inst()->createRoot($flush); $configManifest = CoreConfigFactory::inst()->createRoot($flush, $manifestCacheFactory);
ConfigLoader::instance()->pushManifest($configManifest); ConfigLoader::instance()->pushManifest($configManifest);
// After loading config, boot _config.php files // After loading config, boot _config.php files
@ -94,7 +101,8 @@ SilverStripe\View\ThemeResourceLoader::instance()->addSet('$default', new Silver
BASE_PATH, BASE_PATH,
project(), project(),
false, false,
$flush $flush,
$manifestCacheFactory
)); ));
// If in live mode, ensure deprecation, strict and notices are not reported // If in live mode, ensure deprecation, strict and notices are not reported

View File

@ -6,56 +6,112 @@ use Exception;
use PhpParser\Error; use PhpParser\Error;
use PhpParser\NodeTraverser; use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver; use PhpParser\NodeVisitor\NameResolver;
use PhpParser\Parser;
use PhpParser\ParserFactory; use PhpParser\ParserFactory;
use SilverStripe\Control\Director; use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Cache\CacheFactory;
use SilverStripe\Dev\TestOnly;
/** /**
* A utility class which builds a manifest of all classes, interfaces and some * A utility class which builds a manifest of all classes, interfaces and caches it.
* additional items present in a directory, and caches it.
* *
* It finds the following information: * It finds the following information:
* - Class and interface names and paths. * - Class and interface names and paths.
* - All direct and indirect descendants of a class. * - All direct and indirect descendants of a class.
* - All implementors of an interface. * - All implementors of an interface.
* - All module configuration files.
*/ */
class ClassManifest class ClassManifest
{ {
/**
const CONF_FILE = '_config.php'; * base manifest directory
const CONF_DIR = '_config'; * @var string
*/
protected $base; protected $base;
/**
* Set if including test classes
*
* @see TestOnly
* @var bool
*/
protected $tests; protected $tests;
/** /**
* @var ManifestCache * Cache to use, if caching.
* Set to null if uncached.
*
* @var CacheInterface|null
*/ */
protected $cache; protected $cache;
/** /**
* Key to use for the top level cache of all items
*
* @var string * @var string
*/ */
protected $cacheKey; protected $cacheKey;
/**
* Map of classes to paths
*
* @var array
*/
protected $classes = array(); protected $classes = array();
protected $roots = array();
protected $children = array();
protected $descendants = array();
protected $interfaces = array();
protected $implementors = array();
protected $configs = array();
protected $configDirs = array();
protected $traits = array();
/** /**
* @var \PhpParser\Parser * List of root classes with no parent class
*
* @var array
*/
protected $roots = array();
/**
* List of direct children for any class
*
* @var array
*/
protected $children = array();
/**
* List of descendents for any class (direct + indirect children)
*
* @var array
*/
protected $descendants = array();
/**
* List of interfaces and paths to those files
*
* @var array
*/
protected $interfaces = array();
/**
* List of direct implementors of any interface
*
* @var array
*/
protected $implementors = array();
/**
* Map of traits to paths
*
* @var array
*/
protected $traits = array();
/**
* PHP Parser for parsing found files
*
* @var Parser
*/ */
private $parser; private $parser;
/** /**
* @var NodeTraverser * @var NodeTraverser
*/ */
private $traverser; private $traverser;
/** /**
* @var ClassManifestVisitor * @var ClassManifestVisitor
*/ */
@ -66,33 +122,44 @@ class ClassManifest
* from the cache or re-scanning for classes. * from the cache or re-scanning for classes.
* *
* @param string $base The manifest base path. * @param string $base The manifest base path.
* @param bool $includeTests Include the contents of "tests" directories. * @param bool $includeTests Include the contents of "tests" directories.
* @param bool $forceRegen Force the manifest to be regenerated. * @param bool $forceRegen Force the manifest to be regenerated.
* @param bool $cache If the manifest is regenerated, cache it. * @param CacheFactory $cacheFactory Optional cache to use. Set to null to not cache.
*/ */
public function __construct($base, $includeTests = false, $forceRegen = false, $cache = true) public function __construct(
{ $base,
$this->base = $base; $includeTests = false,
$forceRegen = false,
CacheFactory $cacheFactory = null
) {
$this->base = $base;
$this->tests = $includeTests; $this->tests = $includeTests;
$cacheClass = getenv('SS_MANIFESTCACHE') ?: 'SilverStripe\\Core\\Manifest\\ManifestCache_File'; // build cache from factory
if ($cacheFactory) {
$this->cache = new $cacheClass('classmanifest'.($includeTests ? '_tests' : '')); $this->cache = $cacheFactory->create(
CacheInterface::class.'.classmanifest',
[ 'namespace' => 'classmanifest' . ($includeTests ? '_tests' : '') ]
);
}
$this->cacheKey = 'manifest'; $this->cacheKey = 'manifest';
if (!$forceRegen && $data = $this->cache->load($this->cacheKey)) { if (!$forceRegen && $this->cache && ($data = $this->cache->get($this->cacheKey))) {
$this->classes = $data['classes']; $this->classes = $data['classes'];
$this->descendants = $data['descendants']; $this->descendants = $data['descendants'];
$this->interfaces = $data['interfaces']; $this->interfaces = $data['interfaces'];
$this->implementors = $data['implementors']; $this->implementors = $data['implementors'];
$this->configs = $data['configs']; $this->traits = $data['traits'];
$this->configDirs = $data['configDirs'];
$this->traits = $data['traits'];
} else { } else {
$this->regenerate($cache); $this->regenerate();
} }
} }
/**
* Get or create active parser
*
* @return Parser
*/
public function getParser() public function getParser()
{ {
if (!$this->parser) { if (!$this->parser) {
@ -246,49 +313,11 @@ class ClassManifest
} }
} }
/**
* Returns an array of paths to module config files.
*
* @return array
*/
public function getConfigs()
{
return $this->configs;
}
/**
* Returns an array of module names mapped to their paths.
*
* "Modules" in SilverStripe are simply directories with a _config.php
* file.
*
* @return array
*/
public function getModules()
{
$modules = array();
if ($this->configs) {
foreach ($this->configs as $configPath) {
$modules[basename(dirname($configPath))] = dirname($configPath);
}
}
if ($this->configDirs) {
foreach ($this->configDirs as $configDir) {
$path = preg_replace('/\/_config$/', '', dirname($configDir));
$modules[basename($path)] = $path;
}
}
return $modules;
}
/** /**
* Get module that owns this class * Get module that owns this class
* *
* @param string $class Class name * @param string $class Class name
* @return string * @return Module
*/ */
public function getOwnerModule($class) public function getOwnerModule($class)
{ {
@ -297,29 +326,32 @@ class ClassManifest
return null; return null;
} }
/** @var Module $rootModule */
$rootModule = null;
// Find based on loaded modules // Find based on loaded modules
foreach ($this->getModules() as $parent => $module) { $modules = ModuleLoader::instance()->getManifest()->getModules();
if (stripos($path, realpath($parent)) === 0) { foreach ($modules as $module) {
// Leave root module as fallback
if (empty($module->getRelativePath())) {
$rootModule = $module;
} elseif (stripos($path, realpath($module->getPath())) === 0) {
return $module; return $module;
} }
} }
// Assume top level folder is the module name // Fall back to top level module
$relativePath = substr($path, strlen(realpath(Director::baseFolder()))); return $rootModule;
$parts = explode('/', trim($relativePath, '/'));
return array_shift($parts);
} }
/** /**
* Completely regenerates the manifest file. * Completely regenerates the manifest file.
*
* @param bool $cache Cache the result.
*/ */
public function regenerate($cache = true) public function regenerate()
{ {
$resets = array( $resets = array(
'classes', 'roots', 'children', 'descendants', 'interfaces', 'classes', 'roots', 'children', 'descendants', 'interfaces',
'implementors', 'configs', 'configDirs', 'traits' 'implementors', 'traits'
); );
// Reset the manifest so stale info doesn't cause errors. // Reset the manifest so stale info doesn't cause errors.
@ -329,11 +361,10 @@ class ClassManifest
$finder = new ManifestFileFinder(); $finder = new ManifestFileFinder();
$finder->setOptions(array( $finder->setOptions(array(
'name_regex' => '/^((_config)|([^_].*))\\.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' => !$this->tests, 'ignore_tests' => !$this->tests,
'file_callback' => array($this, 'handleFile'), 'file_callback' => array($this, 'handleFile'),
'dir_callback' => array($this, 'handleDir')
)); ));
$finder->find($this->base); $finder->find($this->base);
@ -341,34 +372,20 @@ class ClassManifest
$this->coalesceDescendants($root); $this->coalesceDescendants($root);
} }
if ($cache) { if ($this->cache) {
$data = array( $data = array(
'classes' => $this->classes, 'classes' => $this->classes,
'descendants' => $this->descendants, 'descendants' => $this->descendants,
'interfaces' => $this->interfaces, 'interfaces' => $this->interfaces,
'implementors' => $this->implementors, 'implementors' => $this->implementors,
'configs' => $this->configs,
'configDirs' => $this->configDirs,
'traits' => $this->traits, 'traits' => $this->traits,
); );
$this->cache->save($data, $this->cacheKey); $this->cache->set($this->cacheKey, $data);
} }
} }
public function handleDir($basename, $pathname, $depth) public function handleFile($basename, $pathname)
{ {
if ($basename == self::CONF_DIR) {
$this->configDirs[] = $pathname;
}
}
public function handleFile($basename, $pathname, $depth)
{
if ($basename == self::CONF_FILE) {
$this->configs[] = $pathname;
return;
}
$classes = null; $classes = null;
$interfaces = null; $interfaces = null;
$traits = null; $traits = null;
@ -379,24 +396,16 @@ class ClassManifest
// since just using the datetime lead to problems with upgrading. // since just using the datetime lead to problems with upgrading.
$key = preg_replace('/[^a-zA-Z0-9_]/', '_', $basename) . '_' . md5_file($pathname); $key = preg_replace('/[^a-zA-Z0-9_]/', '_', $basename) . '_' . md5_file($pathname);
$valid = false; // Attempt to load from cache
if ($data = $this->cache->load($key)) { if ($this->cache
$valid = ( && ($data = $this->cache->get($key))
isset($data['classes']) && is_array($data['classes']) && $this->validateItemCache($data)
&& isset($data['interfaces']) ) {
&& is_array($data['interfaces']) $classes = $data['classes'];
&& isset($data['traits']) $interfaces = $data['interfaces'];
&& is_array($data['traits']) $traits = $data['traits'];
); } else {
// Build from php file parser
if ($valid) {
$classes = $data['classes'];
$interfaces = $data['interfaces'];
$traits = $data['traits'];
}
}
if (!$valid) {
$fileContents = ClassContentRemover::remove_class_content($pathname); $fileContents = ClassContentRemover::remove_class_content($pathname);
try { try {
$stmts = $this->getParser()->parse($fileContents); $stmts = $this->getParser()->parse($fileContents);
@ -410,14 +419,18 @@ class ClassManifest
$interfaces = $this->getVisitor()->getInterfaces(); $interfaces = $this->getVisitor()->getInterfaces();
$traits = $this->getVisitor()->getTraits(); $traits = $this->getVisitor()->getTraits();
$cache = array( // Save back to cache if configured
'classes' => $classes, if ($this->cache) {
'interfaces' => $interfaces, $cache = array(
'traits' => $traits, 'classes' => $classes,
); 'interfaces' => $interfaces,
$this->cache->save($cache, $key); 'traits' => $traits,
);
$this->cache->set($key, $cache);
}
} }
// Merge this data into the global list
foreach ($classes as $className => $classInfo) { foreach ($classes as $className => $classInfo) {
$extends = isset($classInfo['extends']) ? $classInfo['extends'] : null; $extends = isset($classInfo['extends']) ? $classInfo['extends'] : null;
$implements = isset($classInfo['interfaces']) ? $classInfo['interfaces'] : null; $implements = isset($classInfo['interfaces']) ? $classInfo['interfaces'] : null;
@ -496,4 +509,30 @@ class ClassManifest
return array(); return array();
} }
} }
/**
* Verify that cached data is valid for a single item
*
* @param array $data
* @return bool
*/
protected function validateItemCache($data)
{
foreach (['classes', 'interfaces', 'traits'] as $key) {
// Must be set
if (!isset($data[$key])) {
return false;
}
// and an array
if (!is_array($data[$key])) {
return false;
}
// Detect legacy cache keys (non-associative)
$array = $data[$key];
if (!empty($array) && is_numeric(key($array))) {
return false;
}
}
return true;
}
} }

View File

@ -1,14 +0,0 @@
<?php
namespace SilverStripe\Core\Manifest;
/**
* A basic caching interface that manifests use to store data.
*/
interface ManifestCache
{
public function __construct($name);
public function load($key);
public function save($data, $key);
public function clear();
}

View File

@ -1,31 +0,0 @@
<?php
namespace SilverStripe\Core\Manifest;
/**
* Stores manifest data in APC.
* Note: benchmarks seem to indicate this is not particularly faster than _File
*/
class ManifestCache_APC implements ManifestCache
{
protected $pre;
function __construct($name)
{
$this->pre = $name;
}
function load($key)
{
return apc_fetch($this->pre . $key);
}
function save($data, $key)
{
apc_store($this->pre . $key, $data);
}
function clear()
{
}
}

View File

@ -1,37 +0,0 @@
<?php
namespace SilverStripe\Core\Manifest;
/**
* Stores manifest data in files in TEMP_DIR dir on filesystem
*/
class ManifestCache_File implements ManifestCache
{
protected $folder = null;
function __construct($name)
{
$this->folder = TEMP_FOLDER . DIRECTORY_SEPARATOR . $name;
if (!is_dir($this->folder)) {
mkdir($this->folder);
}
}
function load($key)
{
$file = $this->folder . DIRECTORY_SEPARATOR . 'cache_' . $key;
return file_exists($file) ? unserialize(file_get_contents($file)) : null;
}
function save($data, $key)
{
$file = $this->folder . DIRECTORY_SEPARATOR . 'cache_' . $key;
file_put_contents($file, serialize($data));
}
function clear()
{
array_map('unlink', glob($this->folder . DIRECTORY_SEPARATOR . 'cache_*'));
}
}

View File

@ -1,29 +0,0 @@
<?php
namespace SilverStripe\Core\Manifest;
/**
* Same as ManifestCache_File, but stores the data as valid PHP which gets included to load
* This is a bit faster if you have an opcode cache installed, but slower otherwise
*/
class ManifestCache_File_PHP extends ManifestCache_File
{
function load($key)
{
global $loaded_manifest;
$loaded_manifest = null;
$file = $this->folder . DIRECTORY_SEPARATOR . 'cache_' . $key;
if (file_exists($file)) {
include $file;
}
return $loaded_manifest;
}
function save($data, $key)
{
$file = $this->folder . DIRECTORY_SEPARATOR. 'cache_' . $key;
file_put_contents($file, '<?php $loaded_manifest = ' . var_export($data, true) . ';');
}
}

View File

@ -3,6 +3,8 @@
namespace SilverStripe\Core\Manifest; namespace SilverStripe\Core\Manifest;
use LogicException; use LogicException;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Cache\CacheFactory;
/** /**
* A utility class which builds a manifest of configuration items * A utility class which builds a manifest of configuration items
@ -31,7 +33,7 @@ class ModuleManifest
protected $includeTests; protected $includeTests;
/** /**
* @var ManifestCache * @var CacheInterface
*/ */
protected $cache; protected $cache;
@ -87,37 +89,31 @@ class ModuleManifest
* @param string $base The project base path. * @param string $base The project base path.
* @param bool $includeTests * @param bool $includeTests
* @param bool $forceRegen Force the manifest to be regenerated. * @param bool $forceRegen Force the manifest to be regenerated.
* @param CacheFactory $cacheFactory Cache factory to use
*/ */
public function __construct($base, $includeTests = false, $forceRegen = false) public function __construct($base, $includeTests = false, $forceRegen = false, CacheFactory $cacheFactory = null)
{ {
$this->base = $base; $this->base = $base;
$this->cacheKey = sha1($base).'_modules'; $this->cacheKey = sha1($base).'_modules';
$this->includeTests = $includeTests; $this->includeTests = $includeTests;
$this->cache = $this->getCache($includeTests); // build cache from factory
if ($cacheFactory) {
$this->cache = $cacheFactory->create(
CacheInterface::class.'.modulemanifest',
[ 'namespace' => 'modulemanifest' . ($includeTests ? '_tests' : '') ]
);
}
// Unless we're forcing regen, try loading from cache // Unless we're forcing regen, try loading from cache
if (!$forceRegen) { if (!$forceRegen && $this->cache) {
$this->modules = $this->cache->load($this->cacheKey) ?: []; $this->modules = $this->cache->get($this->cacheKey) ?: [];
} }
if (empty($this->modules)) { if (empty($this->modules)) {
$this->regenerate($includeTests); $this->regenerate($includeTests);
} }
} }
/**
* Provides a hook for mock unit tests despite no DI
*
* @param bool $includeTests
* @return ManifestCache
*/
protected function getCache($includeTests = false)
{
// Cache
$cacheClass = getenv('SS_MANIFESTCACHE') ?: ManifestCache_File::class;
return new $cacheClass('classmanifest'.($includeTests ? '_tests' : ''));
}
/** /**
* Includes all of the php _config.php files found by this manifest. * Includes all of the php _config.php files found by this manifest.
*/ */
@ -136,9 +132,8 @@ class ModuleManifest
* Does _not_ build the actual variant * Does _not_ build the actual variant
* *
* @param bool $includeTests * @param bool $includeTests
* @param bool $cache Cache the result.
*/ */
public function regenerate($includeTests = false, $cache = true) public function regenerate($includeTests = false)
{ {
$this->modules = []; $this->modules = [];
@ -162,8 +157,8 @@ class ModuleManifest
)); ));
$finder->find($this->base); $finder->find($this->base);
if ($cache) { if ($this->cache) {
$this->cache->save($this->modules, $this->cacheKey); $this->cache->set($this->cacheKey, $this->modules);
} }
} }
@ -172,9 +167,8 @@ class ModuleManifest
* *
* @param string $basename * @param string $basename
* @param string $pathname * @param string $pathname
* @param int $depth
*/ */
public function addSourceConfigFile($basename, $pathname, $depth) public function addSourceConfigFile($basename, $pathname)
{ {
$this->addModule(dirname($pathname)); $this->addModule(dirname($pathname));
} }
@ -184,9 +178,8 @@ class ModuleManifest
* *
* @param string $basename * @param string $basename
* @param string $pathname * @param string $pathname
* @param int $depth
*/ */
public function addYAMLConfigFile($basename, $pathname, $depth) public function addYAMLConfigFile($basename, $pathname)
{ {
if (preg_match('{/([^/]+)/_config/}', $pathname, $match)) { if (preg_match('{/([^/]+)/_config/}', $pathname, $match)) {
$this->addModule(dirname(dirname($pathname))); $this->addModule(dirname(dirname($pathname)));

View File

@ -4,6 +4,8 @@ namespace SilverStripe\Dev;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Core\Manifest\ClassLoader; use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Manifest\Module;
use SilverStripe\Core\Manifest\ModuleLoader;
/** /**
* Handles raising an notice when accessing a deprecated method * Handles raising an notice when accessing a deprecated method
@ -96,7 +98,7 @@ class Deprecation
* #notice) * #notice)
* *
* @param array $backtrace A backtrace as returned from debug_backtrace * @param array $backtrace A backtrace as returned from debug_backtrace
* @return string The name of the module the call came from, or null if we can't determine * @return Module The module being called
*/ */
protected static function get_calling_module_from_trace($backtrace) protected static function get_calling_module_from_trace($backtrace)
{ {
@ -106,10 +108,10 @@ class Deprecation
$callingfile = realpath($backtrace[1]['file']); $callingfile = realpath($backtrace[1]['file']);
$manifest = ClassLoader::instance()->getManifest(); $modules = ModuleLoader::instance()->getManifest()->getModules();
foreach ($manifest->getModules() as $name => $path) { foreach ($modules as $module) {
if (strpos($callingfile, realpath($path)) === 0) { if (strpos($callingfile, realpath($module->getPath())) === 0) {
return $name; return $module;
} }
} }
return null; return null;
@ -193,8 +195,16 @@ class Deprecation
if (self::$module_version_overrides) { if (self::$module_version_overrides) {
$module = self::get_calling_module_from_trace($backtrace = debug_backtrace(0)); $module = self::get_calling_module_from_trace($backtrace = debug_backtrace(0));
if (isset(self::$module_version_overrides[$module])) { if ($module) {
$checkVersion = self::$module_version_overrides[$module]; if (($name = $module->getComposerName())
&& isset(self::$module_version_overrides[$name])
) {
$checkVersion = self::$module_version_overrides[$name];
} elseif (($name = $module->getShortName())
&& isset(self::$module_version_overrides[$name])
) {
$checkVersion = self::$module_version_overrides[$name];
}
} }
} }

View File

@ -1,61 +0,0 @@
<?php
namespace SilverStripe\Logging;
use Psr\Log\LoggerInterface;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Deprecation;
/**
* Wrapper class for a logging handler like {@link Zend_Log}
* which takes a message (or a map of context variables) and
* sends it to one or more {@link Zend_Log_Writer_Abstract}
* subclasses for output.
*
* These priorities are currently supported:
* - Log::ERR
* - Log::WARN
* - Log::NOTICE
* - Log::INFO
* - Log::DEBUG
*/
class Log
{
const ERR = 'error';
const WARN = 'warning';
const NOTICE = 'notice';
const INFO = 'info';
const DEBUG = 'debug';
/**
* Get the logger currently in use, or create a new one if it doesn't exist.
*
* @deprecated 4.0..5.0
* @return LoggerInterface
*/
public static function get_logger()
{
Deprecation::notice('5.0', 'Use Injector::inst()->get(LoggerInterface::class) instead');
return Injector::inst()->get(LoggerInterface::class);
}
/**
* Dispatch a message by priority level.
*
* The message parameter can be either a string (a simple error
* message), or an array of variables. The latter is useful for passing
* along a list of debug information for the writer to handle, such as
* error code, error line, error context (backtrace).
*
* @param mixed $message Exception object or array of error context variables
* @param string $priority Priority. Possible values: Log::ERR, Log::WARN, Log::NOTICE, Log::INFO or Log::DEBUG
*
* @deprecated 4.0.0:5.0.0 Use Injector::inst()->get('Logger')->log($priority, $message) instead
*/
public static function log($message, $priority)
{
Deprecation::notice('5.0', 'Use Injector::inst()->get(LoggerInterface::class)->log($priority, $message) instead');
Injector::inst()->get(LoggerInterface::class)->log($priority, $message);
}
}

View File

@ -2,7 +2,8 @@
namespace SilverStripe\View; namespace SilverStripe\View;
use SilverStripe\Core\Manifest\ManifestCache; use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Cache\CacheFactory;
use SilverStripe\Core\Manifest\ManifestFileFinder; use SilverStripe\Core\Manifest\ManifestFileFinder;
/** /**
@ -37,7 +38,7 @@ class ThemeManifest implements ThemeList
/** /**
* Cache * Cache
* *
* @var ManifestCache * @var CacheInterface
*/ */
protected $cache; protected $cache;
@ -51,7 +52,7 @@ class ThemeManifest implements ThemeList
/** /**
* List of theme root directories * List of theme root directories
* *
* @var string * @var string[]
*/ */
protected $themes = null; protected $themes = null;
@ -64,18 +65,26 @@ class ThemeManifest implements ThemeList
* *
* @param bool $includeTests Include tests in the manifest. * @param bool $includeTests Include tests in the manifest.
* @param bool $forceRegen Force the manifest to be regenerated. * @param bool $forceRegen Force the manifest to be regenerated.
* @param CacheFactory $cacheFactory Cache factory to generate backend cache with
*/ */
public function __construct($base, $project, $includeTests = false, $forceRegen = false) public function __construct(
{ $base,
$project,
$includeTests = false,
$forceRegen = false,
CacheFactory $cacheFactory = null
) {
$this->base = $base; $this->base = $base;
$this->tests = $includeTests; $this->tests = $includeTests;
$this->project = $project; $this->project = $project;
$cacheClass = getenv('SS_MANIFESTCACHE') // build cache from factory
?: 'SilverStripe\\Core\\Manifest\\ManifestCache_File'; if ($cacheFactory) {
$this->cache = $cacheFactory->create(
$this->cache = new $cacheClass('thememanifest'.($includeTests ? '_tests' : '')); CacheInterface::class.'.thememanifest',
[ 'namespace' => 'thememanifest' . ($includeTests ? '_tests' : '') ]
);
}
$this->cacheKey = $this->getCacheKey(); $this->cacheKey = $this->getCacheKey();
if ($forceRegen) { if ($forceRegen) {
@ -117,10 +126,8 @@ class ThemeManifest implements ThemeList
/** /**
* Regenerates the manifest by scanning the base path. * Regenerates the manifest by scanning the base path.
*
* @param bool $cache
*/ */
public function regenerate($cache = true) public function regenerate()
{ {
$finder = new ManifestFileFinder(); $finder = new ManifestFileFinder();
$finder->setOptions(array( $finder->setOptions(array(
@ -133,8 +140,8 @@ class ThemeManifest implements ThemeList
$this->themes = []; $this->themes = [];
$finder->find($this->base); $finder->find($this->base);
if ($cache) { if ($this->cache) {
$this->cache->save($this->themes, $this->cacheKey); $this->cache->set($this->cacheKey, $this->themes);
} }
} }
@ -171,7 +178,7 @@ class ThemeManifest implements ThemeList
*/ */
protected function init() protected function init()
{ {
if ($data = $this->cache->load($this->cacheKey)) { if ($this->cache && ($data = $this->cache->get($this->cacheKey))) {
$this->themes = $data; $this->themes = $data;
} else { } else {
$this->regenerate(); $this->regenerate();

View File

@ -4,7 +4,7 @@ namespace SilverStripe\i18n\Data;
use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Manifest\ClassLoader; use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\Core\Resettable; use SilverStripe\Core\Resettable;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
@ -35,7 +35,7 @@ class Sources implements Resettable
public function getSortedModules() public function getSortedModules()
{ {
// Get list of module => path pairs, and then just the names // Get list of module => path pairs, and then just the names
$modules = ClassLoader::instance()->getManifest()->getModules(); $modules = ModuleLoader::instance()->getManifest()->getModules();
$moduleNames = array_keys($modules); $moduleNames = array_keys($modules);
// Remove the "project" module from the list - we'll add it back specially later if needed // Remove the "project" module from the list - we'll add it back specially later if needed
@ -63,14 +63,14 @@ class Sources implements Resettable
$order[] = $project; $order[] = $project;
} }
$sortedModules = array(); $sortedModulePaths = array();
foreach ($order as $module) { foreach ($order as $module) {
if (isset($modules[$module])) { if (isset($modules[$module])) {
$sortedModules[$module] = $modules[$module]; $sortedModulePaths[$module] = $modules[$module]->getPath();
} }
} }
$sortedModules = array_reverse($sortedModules, true); $sortedModulePaths = array_reverse($sortedModulePaths, true);
return $sortedModules; return $sortedModulePaths;
} }
/** /**

View File

@ -5,6 +5,8 @@ namespace SilverStripe\i18n\TextCollection;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Manifest\ClassLoader; use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Manifest\Module;
use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\Dev\Debug; use SilverStripe\Dev\Debug;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use ReflectionClass; use ReflectionClass;
@ -172,7 +174,7 @@ class i18nTextCollector
} }
// Write each module language file // Write each module language file
foreach ($entitiesByModule as $module => $entities) { foreach ($entitiesByModule as $moduleName => $entities) {
// Skip empty translations // Skip empty translations
if (empty($entities)) { if (empty($entities)) {
continue; continue;
@ -180,42 +182,11 @@ class i18nTextCollector
// Clean sorting prior to writing // Clean sorting prior to writing
ksort($entities); ksort($entities);
$path = $this->baseSavePath . '/' . $module; $module = ModuleLoader::instance()->getManifest()->getModule($moduleName);
$this->getWriter()->write($entities, $this->defaultLocale, $path); $this->write($module, $entities);
} }
} }
/**
* Gets the list of modules in this installer
*
* @param string $directory Path to look in
* @return array List of modules as paths relative to base
*/
protected function getModules($directory)
{
// Include self as head module
$modules = array();
// Get all standard modules
foreach (glob($directory."/*", GLOB_ONLYDIR) as $path) {
// Check for _config
if (!is_file("$path/_config.php") && !is_dir("$path/_config")) {
continue;
}
$modules[] = basename($path);
}
// Get all themes
foreach (glob($directory."/themes/*", GLOB_ONLYDIR) as $path) {
// Check for templates
if (is_dir("$path/templates")) {
$modules[] = 'themes/'.basename($path);
}
}
return $modules;
}
/** /**
* Extract all strings from modules and return these grouped by module name * Extract all strings from modules and return these grouped by module name
* *
@ -237,7 +208,13 @@ class i18nTextCollector
// Restrict modules we update to just the specified ones (if any passed) // Restrict modules we update to just the specified ones (if any passed)
if (!empty($restrictToModules)) { if (!empty($restrictToModules)) {
foreach (array_diff(array_keys($entitiesByModule), $restrictToModules) as $module) { // Normalise module names
$modules = array_filter(array_map(function ($name) {
$module = ModuleLoader::instance()->getManifest()->getModule($name);
return $module ? $module->getName() : null;
}, $restrictToModules));
// Remove modules
foreach (array_diff(array_keys($entitiesByModule), $modules) as $module) {
unset($entitiesByModule[$module]); unset($entitiesByModule[$module]);
} }
} }
@ -350,9 +327,12 @@ class i18nTextCollector
protected function findModuleForClass($class) protected function findModuleForClass($class)
{ {
if (ClassInfo::exists($class)) { if (ClassInfo::exists($class)) {
return ClassLoader::instance() $module = ClassLoader::instance()
->getManifest() ->getManifest()
->getOwnerModule($class); ->getOwnerModule($class);
if ($module) {
return $module->getName();
}
} }
// If we can't find a class, see if it needs to be fully qualified // If we can't find a class, see if it needs to be fully qualified
@ -368,7 +348,8 @@ class i18nTextCollector
// Find all modules for candidate classes // Find all modules for candidate classes
$modules = array_unique(array_map(function ($class) { $modules = array_unique(array_map(function ($class) {
return ClassLoader::instance()->getManifest()->getOwnerModule($class); $module = ClassLoader::instance()->getManifest()->getOwnerModule($class);
return $module ? $module->getName() : null;
}, $classes)); }, $classes));
if (count($modules) === 1) { if (count($modules) === 1) {
@ -413,24 +394,32 @@ class i18nTextCollector
{ {
// A master string tables array (one mst per module) // A master string tables array (one mst per module)
$entitiesByModule = array(); $entitiesByModule = array();
$modules = $this->getModules($this->basePath); $modules = ModuleLoader::instance()->getManifest()->getModules();
foreach ($modules as $module) { foreach ($modules as $module) {
// we store the master string tables // we store the master string tables
$processedEntities = $this->processModule($module); $processedEntities = $this->processModule($module);
if (isset($entitiesByModule[$module])) { $moduleName = $module->getName();
$entitiesByModule[$module] = array_merge_recursive($entitiesByModule[$module], $processedEntities); if (isset($entitiesByModule[$moduleName])) {
$entitiesByModule[$moduleName] = array_merge_recursive(
$entitiesByModule[$moduleName],
$processedEntities
);
} else { } else {
$entitiesByModule[$module] = $processedEntities; $entitiesByModule[$moduleName] = $processedEntities;
} }
// Extract all entities for "foreign" modules ('module' key in array form) // Extract all entities for "foreign" modules ('module' key in array form)
// @see CMSMenu::provideI18nEntities for an example usage // @see CMSMenu::provideI18nEntities for an example usage
foreach ($entitiesByModule[$module] as $fullName => $spec) { foreach ($entitiesByModule[$moduleName] as $fullName => $spec) {
$specModule = $module; $specModuleName = $moduleName;
// Rewrite spec if module is specified // Rewrite spec if module is specified
if (is_array($spec) && isset($spec['module'])) { if (is_array($spec) && isset($spec['module'])) {
$specModule = $spec['module']; // Normalise name (in case non-composer name is specified)
$specModule = ModuleLoader::instance()->getManifest()->getModule($spec['module']);
if ($specModule) {
$specModuleName = $specModule->getName();
}
unset($spec['module']); unset($spec['module']);
// If only element is defalt, simplify // If only element is defalt, simplify
@ -440,24 +429,34 @@ class i18nTextCollector
} }
// Remove from source module // Remove from source module
if ($specModule !== $module) { if ($specModuleName !== $moduleName) {
unset($entitiesByModule[$module][$fullName]); unset($entitiesByModule[$moduleName][$fullName]);
} }
// Write to target module // Write to target module
if (!isset($entitiesByModule[$specModule])) { if (!isset($entitiesByModule[$specModuleName])) {
$entitiesByModule[$specModule] = []; $entitiesByModule[$specModuleName] = [];
} }
$entitiesByModule[$specModule][$fullName] = $spec; $entitiesByModule[$specModuleName][$fullName] = $spec;
} }
} }
return $entitiesByModule; return $entitiesByModule;
} }
/**
public function write($module, $entities) * Write entities to a module
*
* @param Module $module
* @param array $entities
* @return $this
*/
public function write(Module $module, $entities)
{ {
$this->getWriter()->write($entities, $this->defaultLocale, $this->baseSavePath . '/' . $module); $this->getWriter()->write(
$entities,
$this->defaultLocale,
$this->baseSavePath . '/' . $module->getRelativePath()
);
return $this; return $this;
} }
@ -465,10 +464,10 @@ class i18nTextCollector
* Builds a master string table from php and .ss template files for the module passed as the $module param * Builds a master string table from php and .ss template files for the module passed as the $module param
* @see collectFromCode() and collectFromTemplate() * @see collectFromCode() and collectFromTemplate()
* *
* @param string $module A module's name or just 'themes/<themename>' * @param Module $module Module instance
* @return array An array of entities found in the files that comprise the module * @return array An array of entities found in the files that comprise the module
*/ */
protected function processModule($module) protected function processModule(Module $module)
{ {
$entities = array(); $entities = array();
@ -481,15 +480,14 @@ class i18nTextCollector
if ($extension === 'php') { if ($extension === 'php') {
$entities = array_merge( $entities = array_merge(
$entities, $entities,
$this->collectFromCode($content, $module), $this->collectFromCode($content, $filePath, $module),
$this->collectFromEntityProviders($filePath, $module) $this->collectFromEntityProviders($filePath, $module)
); );
} elseif ($extension === 'ss') { } elseif ($extension === 'ss') {
// templates use their filename as a namespace // templates use their filename as a namespace
$namespace = basename($filePath);
$entities = array_merge( $entities = array_merge(
$entities, $entities,
$this->collectFromTemplate($content, $module, $namespace) $this->collectFromTemplate($content, $filePath, $module)
); );
} }
} }
@ -503,25 +501,29 @@ class i18nTextCollector
/** /**
* Retrieves the list of files for this module * Retrieves the list of files for this module
* *
* @param string $module * @param Module $module Module instance
* @return array List of files to parse * @return array List of files to parse
*/ */
protected function getFileListForModule($module) protected function getFileListForModule(Module $module)
{ {
$modulePath = "{$this->basePath}/{$module}"; $modulePath = $module->getPath();
// Search all .ss files in themes // Search all .ss files in themes
if (stripos($module, 'themes/') === 0) { if (stripos($module->getRelativePath(), 'themes/') === 0) {
return $this->getFilesRecursive($modulePath, null, 'ss'); return $this->getFilesRecursive($modulePath, null, 'ss');
} }
// If Framework or non-standard module structure, so we'll scan all subfolders // If non-standard module structure, search all root files
if ($module === FRAMEWORK_DIR || !is_dir("{$modulePath}/code")) { if (!is_dir("{$modulePath}/code") && !is_dir("{$modulePath}/src")) {
return $this->getFilesRecursive($modulePath); return $this->getFilesRecursive($modulePath);
} }
// Get code files // Get code files
$files = $this->getFilesRecursive("{$modulePath}/code", null, 'php'); if (is_dir("{$modulePath}/src")) {
$files = $this->getFilesRecursive("{$modulePath}/src", null, 'php');
} else {
$files = $this->getFilesRecursive("{$modulePath}/code", null, 'php');
}
// Search for templates in this module // Search for templates in this module
if (is_dir("{$modulePath}/templates")) { if (is_dir("{$modulePath}/templates")) {
@ -538,12 +540,15 @@ class i18nTextCollector
* Note: Translations without default values are omitted. * Note: Translations without default values are omitted.
* *
* @param string $content The text content of a parsed template-file * @param string $content The text content of a parsed template-file
* @param string $module Module's name or 'themes'. Could also be a namespace * @param string $fileName Filename Optional filename
* Generated by templates includes. E.g. 'UploadField.ss' * @param Module $module Module being collected
* @return array Map of localised keys to default values provided for this code * @return array Map of localised keys to default values provided for this code
*/ */
public function collectFromCode($content, $module) public function collectFromCode($content, $fileName, Module $module)
{ {
// Get namespace either from $fileName or $module fallback
$namespace = $fileName ? basename($fileName) : $module->getName();
$entities = array(); $entities = array();
$tokens = token_get_all("<?php\n" . $content); $tokens = token_get_all("<?php\n" . $content);
@ -713,7 +718,7 @@ class i18nTextCollector
// Normalise all keys // Normalise all keys
foreach ($entities as $key => $entity) { foreach ($entities as $key => $entity) {
unset($entities[$key]); unset($entities[$key]);
$entities[$this->normalizeEntity($key, $module)] = $entity; $entities[$this->normalizeEntity($key, $namespace)] = $entity;
} }
ksort($entities); ksort($entities);
@ -725,12 +730,15 @@ class i18nTextCollector
* *
* @param string $content The text content of a parsed template-file * @param string $content The text content of a parsed template-file
* @param string $fileName The name of a template file when method is used in self-referencing mode * @param string $fileName The name of a template file when method is used in self-referencing mode
* @param string $module Module's name or 'themes' * @param Module $module Module being collected
* @param array $parsedFiles * @param array $parsedFiles
* @return array $entities An array of entities representing the extracted template function calls * @return array $entities An array of entities representing the extracted template function calls
*/ */
public function collectFromTemplate($content, $fileName, $module, &$parsedFiles = array()) public function collectFromTemplate($content, $fileName, Module $module, &$parsedFiles = array())
{ {
// Get namespace either from $fileName or $module fallback
$namespace = $fileName ? basename($fileName) : $module->getName();
// use parser to extract <%t style translatable entities // use parser to extract <%t style translatable entities
$entities = Parser::getTranslatables($content, $this->getWarnOnEmptyDefault()); $entities = Parser::getTranslatables($content, $this->getWarnOnEmptyDefault());
@ -738,13 +746,13 @@ class i18nTextCollector
// Collect in actual template // Collect in actual template
if (preg_match_all('/(_t\([^\)]*?\))/ms', $content, $matches)) { if (preg_match_all('/(_t\([^\)]*?\))/ms', $content, $matches)) {
foreach ($matches[1] as $match) { foreach ($matches[1] as $match) {
$entities = array_merge($entities, $this->collectFromCode($match, $module)); $entities = array_merge($entities, $this->collectFromCode($match, $fileName, $module));
} }
} }
foreach ($entities as $entity => $spec) { foreach ($entities as $entity => $spec) {
unset($entities[$entity]); unset($entities[$entity]);
$entities[$this->normalizeEntity($entity, $module)] = $spec; $entities[$this->normalizeEntity($entity, $namespace)] = $spec;
} }
ksort($entities); ksort($entities);
@ -760,10 +768,10 @@ class i18nTextCollector
* *
* @uses i18nEntityProvider * @uses i18nEntityProvider
* @param string $filePath * @param string $filePath
* @param string $module * @param Module $module
* @return array * @return array
*/ */
public function collectFromEntityProviders($filePath, $module = null) public function collectFromEntityProviders($filePath, Module $module = null)
{ {
$entities = array(); $entities = array();
$classes = ClassInfo::classes_for_file($filePath); $classes = ClassInfo::classes_for_file($filePath);

View File

@ -12,8 +12,25 @@ use SilverStripe\Dev\SapphireTest;
class ClassLoaderTest extends SapphireTest class ClassLoaderTest extends SapphireTest
{ {
protected $base; /**
protected $manifest; * @var string
*/
protected $baseManifest1;
/**
* @var string
*/
protected $baseManifest2;
/**
* @var ClassManifest
*/
protected $testManifest1;
/**
* @var ClassManifest
*/
protected $testManifest2;
public function setUp() public function setUp()
{ {
@ -21,8 +38,8 @@ class ClassLoaderTest extends SapphireTest
$this->baseManifest1 = dirname(__FILE__) . '/fixtures/classmanifest'; $this->baseManifest1 = dirname(__FILE__) . '/fixtures/classmanifest';
$this->baseManifest2 = dirname(__FILE__) . '/fixtures/classmanifest_other'; $this->baseManifest2 = dirname(__FILE__) . '/fixtures/classmanifest_other';
$this->testManifest1 = new ClassManifest($this->baseManifest1, false, true, false); $this->testManifest1 = new ClassManifest($this->baseManifest1, false);
$this->testManifest2 = new ClassManifest($this->baseManifest2, false, true, false); $this->testManifest2 = new ClassManifest($this->baseManifest2, false);
} }
public function testExclusive() public function testExclusive()

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Core\Tests\Manifest; namespace SilverStripe\Core\Tests\Manifest;
use Exception;
use SilverStripe\Core\Manifest\ClassManifest; use SilverStripe\Core\Manifest\ClassManifest;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
@ -11,8 +12,19 @@ use SilverStripe\Dev\SapphireTest;
class ClassManifestTest extends SapphireTest class ClassManifestTest extends SapphireTest
{ {
/**
* @var string
*/
protected $base; protected $base;
/**
* @var ClassManifest
*/
protected $manifest; protected $manifest;
/**
* @var ClassManifest
*/
protected $manifestTests; protected $manifestTests;
public function setUp() public function setUp()
@ -20,8 +32,8 @@ 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, false, true, false); $this->manifest = new ClassManifest($this->base, false);
$this->manifestTests = new ClassManifest($this->base, true, true, false); $this->manifestTests = new ClassManifest($this->base, true);
} }
public function testGetItemPath() public function testGetItemPath()
@ -125,23 +137,6 @@ class ClassManifestTest extends SapphireTest
} }
} }
public function testGetConfigs()
{
$expect = array("{$this->base}/module/_config.php");
$this->assertEquals($expect, $this->manifest->getConfigs());
$this->assertEquals($expect, $this->manifestTests->getConfigs());
}
public function testGetModules()
{
$expect = array(
"module" => "{$this->base}/module",
"moduleb" => "{$this->base}/moduleb"
);
$this->assertEquals($expect, $this->manifest->getModules());
$this->assertEquals($expect, $this->manifestTests->getModules());
}
public function testTestManifestIncludesTestClasses() public function testTestManifestIncludesTestClasses()
{ {
$this->assertNotContains('testclassa', array_keys($this->manifest->getClasses())); $this->assertNotContains('testclassa', array_keys($this->manifest->getClasses()));
@ -156,11 +151,10 @@ class ClassManifestTest extends SapphireTest
/** /**
* Assert that ClassManifest throws an exception when it encounters two files * Assert that ClassManifest throws an exception when it encounters two files
* which contain classes with the same name * which contain classes with the same name
*
* @expectedException Exception
*/ */
public function testManifestWarnsAboutDuplicateClasses() public function testManifestWarnsAboutDuplicateClasses()
{ {
$dummy = new ClassManifest(dirname(__FILE__) . '/fixtures/classmanifest_duplicates', false, true, false); $this->setExpectedException(Exception::class);
new ClassManifest(dirname(__FILE__) . '/fixtures/classmanifest_duplicates', false);
} }
} }

View File

@ -0,0 +1,72 @@
<?php
namespace SilverStripe\Core\Tests\Manifest;
use SilverStripe\Core\Manifest\ModuleManifest;
use SilverStripe\Dev\SapphireTest;
class ModuleManifestTest extends SapphireTest
{
/**
* @var string
*/
protected $base;
/**
* @var ModuleManifest
*/
protected $manifest;
public function setUp()
{
parent::setUp();
$this->base = dirname(__FILE__) . '/fixtures/classmanifest';
$this->manifest = new ModuleManifest($this->base, false);
}
public function testGetModules()
{
$modules = $this->manifest->getModules();
$this->assertEquals(
[
'module',
'silverstripe/awesome-module',
],
array_keys($modules)
);
}
public function testGetLegacyModule()
{
$module = $this->manifest->getModule('module');
$this->assertNotEmpty($module);
$this->assertEquals('module', $module->getName());
$this->assertEquals('module', $module->getShortName());
$this->assertEquals('module', $module->getRelativePath());
$this->assertEmpty($module->getComposerName());
}
public function testGetComposerModule()
{
// Get by installer-name (folder)
$moduleByShortName = $this->manifest->getModule('moduleb');
$this->assertNotEmpty($moduleByShortName);
// Can also get this by full composer name
$module = $this->manifest->getModule('silverstripe/awesome-module');
$this->assertNotEmpty($module);
$this->assertEquals($moduleByShortName->getPath(), $module->getPath());
// correctly respects vendor
$this->assertEmpty($this->manifest->getModule('wrongvendor/awesome-module'));
$this->assertEmpty($this->manifest->getModule('wrongvendor/moduleb'));
// Properties of module
$this->assertEquals('silverstripe/awesome-module', $module->getName());
$this->assertEquals('silverstripe/awesome-module', $module->getComposerName());
$this->assertEquals('moduleb', $module->getShortName());
$this->assertEquals('moduleb', $module->getRelativePath());
}
}

View File

@ -13,7 +13,9 @@ use ReflectionMethod;
*/ */
class NamespacedClassManifestTest extends SapphireTest class NamespacedClassManifestTest extends SapphireTest
{ {
/**
* @var string
*/
protected $base; protected $base;
/** /**
@ -26,7 +28,7 @@ class NamespacedClassManifestTest extends SapphireTest
parent::setUp(); parent::setUp();
$this->base = dirname(__FILE__) . '/fixtures/namespaced_classmanifest'; $this->base = dirname(__FILE__) . '/fixtures/namespaced_classmanifest';
$this->manifest = new ClassManifest($this->base, false, true, false); $this->manifest = new ClassManifest($this->base, false);
ClassLoader::instance()->pushManifest($this->manifest, false); ClassLoader::instance()->pushManifest($this->manifest, false);
} }
@ -40,7 +42,7 @@ class NamespacedClassManifestTest extends SapphireTest
{ {
$this->assertContains('SilverStripe\Framework\Tests\ClassI', ClassInfo::implementorsOf('SilverStripe\\Security\\PermissionProvider')); $this->assertContains('SilverStripe\Framework\Tests\ClassI', ClassInfo::implementorsOf('SilverStripe\\Security\\PermissionProvider'));
//because we're using a nested manifest we have to "coalesce" the descendants again to correctly populate the // because we're using a nested manifest we have to "coalesce" the descendants again to correctly populate the
// descendants of the core classes we want to test against - this is a limitation of the test manifest not // descendants of the core classes we want to test against - this is a limitation of the test manifest not
// including all core classes // including all core classes
$method = new ReflectionMethod($this->manifest, 'coalesceDescendants'); $method = new ReflectionMethod($this->manifest, 'coalesceDescendants');
@ -149,19 +151,4 @@ class NamespacedClassManifestTest extends SapphireTest
$this->assertEquals($impl, $this->manifest->getImplementorsOf($interface)); $this->assertEquals($impl, $this->manifest->getImplementorsOf($interface));
} }
} }
public function testGetConfigs()
{
$expect = array("{$this->base}/module/_config.php");
$this->assertEquals($expect, $this->manifest->getConfigs());
}
public function testGetModules()
{
$expect = array(
"module" => "{$this->base}/module",
"moduleb" => "{$this->base}/moduleb"
);
$this->assertEquals($expect, $this->manifest->getModules());
}
} }

View File

@ -11,7 +11,9 @@ use SilverStripe\Dev\SapphireTest;
*/ */
class ThemeResourceLoaderTest extends SapphireTest class ThemeResourceLoaderTest extends SapphireTest
{ {
/**
* @var string
*/
private $base; private $base;
/** /**
@ -34,7 +36,7 @@ class ThemeResourceLoaderTest extends SapphireTest
// Fake project root // Fake project root
$this->base = dirname(__FILE__) . '/fixtures/templatemanifest'; $this->base = dirname(__FILE__) . '/fixtures/templatemanifest';
// New ThemeManifest for that root // New ThemeManifest for that root
$this->manifest = new ThemeManifest($this->base, 'myproject', false, true); $this->manifest = new ThemeManifest($this->base, 'myproject', false);
// New Loader for that root // New Loader for that root
$this->loader = new ThemeResourceLoader($this->base); $this->loader = new ThemeResourceLoader($this->base);
$this->loader->addSet('$default', $this->manifest); $this->loader->addSet('$default', $this->manifest);

View File

@ -0,0 +1 @@
module: {}

View File

@ -0,0 +1,8 @@
{
"name": "silverstripe/awesome-module",
"description": "dummy test module",
"require": {},
"extra": {
"installer-name": "moduleb"
}
}

View File

@ -116,7 +116,7 @@ class DeprecationTest extends SapphireTest
protected function callThatOriginatesFromFramework() protected function callThatOriginatesFromFramework()
{ {
$this->assertEquals(TestDeprecation::get_module(), basename(FRAMEWORK_PATH)); $this->assertEquals('silverstripe/framework', TestDeprecation::get_module()->getName());
$this->assertNull(Deprecation::notice('2.0', 'Deprecation test passed')); $this->assertNull(Deprecation::notice('2.0', 'Deprecation test passed'));
} }
} }

View File

@ -6,6 +6,8 @@ use SilverStripe\Control\Director;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Manifest\ClassManifest; use SilverStripe\Core\Manifest\ClassManifest;
use SilverStripe\Core\Manifest\ClassLoader; use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\Core\Manifest\ModuleManifest;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
use SilverStripe\i18n\Messages\MessageProvider; use SilverStripe\i18n\Messages\MessageProvider;
use SilverStripe\i18n\Messages\Symfony\ModuleYamlLoader; use SilverStripe\i18n\Messages\Symfony\ModuleYamlLoader;
@ -41,6 +43,13 @@ trait i18nTestManifest
*/ */
protected $manifests = 0; protected $manifests = 0;
/**
* Number of module manifests
*
* @var int
*/
protected $moduleManifests = 0;
protected function getExtraDataObjects() protected function getExtraDataObjects()
{ {
return [ return [
@ -72,17 +81,16 @@ trait i18nTestManifest
$this->alternateBasePath = __DIR__ . $s . 'i18nTest' . $s . "_fakewebroot"; $this->alternateBasePath = __DIR__ . $s . 'i18nTest' . $s . "_fakewebroot";
Director::config()->update('alternate_base_folder', $this->alternateBasePath); Director::config()->update('alternate_base_folder', $this->alternateBasePath);
// New module manifest
$moduleManifest = new ModuleManifest($this->alternateBasePath, false);
$this->pushModuleManifest($moduleManifest);
// Replace old template loader with new one with alternate base path // Replace old template loader with new one with alternate base path
$this->oldThemeResourceLoader = ThemeResourceLoader::instance(); $this->oldThemeResourceLoader = ThemeResourceLoader::instance();
ThemeResourceLoader::set_instance($loader = new ThemeResourceLoader($this->alternateBasePath)); ThemeResourceLoader::set_instance($loader = new ThemeResourceLoader($this->alternateBasePath));
$loader->addSet( $loader->addSet(
'$default', '$default',
new ThemeManifest( new ThemeManifest($this->alternateBasePath, project(), false)
$this->alternateBasePath,
project(),
false,
true
)
); );
SSViewer::set_themes([ SSViewer::set_themes([
@ -94,7 +102,7 @@ trait i18nTestManifest
i18n::set_locale('en_US'); i18n::set_locale('en_US');
// Set new manifest against the root // Set new manifest against the root
$classManifest = new ClassManifest($this->alternateBasePath, true, true, false); $classManifest = new ClassManifest($this->alternateBasePath, true);
$this->pushManifest($classManifest); $this->pushManifest($classManifest);
// Setup uncached translator // Setup uncached translator
@ -131,6 +139,12 @@ trait i18nTestManifest
ClassLoader::instance()->pushManifest($manifest); ClassLoader::instance()->pushManifest($manifest);
} }
protected function pushModuleManifest(ModuleManifest $manifest)
{
$this->moduleManifests++;
ModuleLoader::instance()->pushManifest($manifest);
}
/** /**
* Pop off all extra manifests * Pop off all extra manifests
*/ */
@ -141,5 +155,9 @@ trait i18nTestManifest
ClassLoader::instance()->popManifest(); ClassLoader::instance()->popManifest();
$this->manifests--; $this->manifests--;
} }
while ($this->moduleManifests > 0) {
ModuleLoader::instance()->popManifest();
$this->moduleManifests--;
}
} }
} }

View File

@ -4,12 +4,12 @@ namespace SilverStripe\i18n\Tests;
use PHPUnit_Framework_Error_Notice; use PHPUnit_Framework_Error_Notice;
use SilverStripe\Assets\Filesystem; use SilverStripe\Assets\Filesystem;
use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
use SilverStripe\i18n\TextCollection\i18nTextCollector; use SilverStripe\i18n\TextCollection\i18nTextCollector;
use SilverStripe\i18n\Messages\YamlWriter; use SilverStripe\i18n\Messages\YamlWriter;
use SilverStripe\i18n\Tests\i18nTextCollectorTest\Collector; use SilverStripe\i18n\Tests\i18nTextCollectorTest\Collector;
use SilverStripe\View\SSViewer;
class i18nTextCollectorTest extends SapphireTest class i18nTextCollectorTest extends SapphireTest
{ {
@ -42,6 +42,7 @@ class i18nTextCollectorTest extends SapphireTest
public function testConcatenationInEntityValues() public function testConcatenationInEntityValues()
{ {
$c = i18nTextCollector::create(); $c = i18nTextCollector::create();
$module = ModuleLoader::instance()->getManifest()->getModule('i18ntestmodule');
$php = <<<PHP $php = <<<PHP
_t( _t(
@ -66,7 +67,7 @@ PHP;
], ],
'Test.CONCATENATED2' => "Line \"4\" and Line 5" 'Test.CONCATENATED2' => "Line \"4\" and Line 5"
), ),
$c->collectFromCode($php, 'mymodule') $c->collectFromCode($php, null, $module)
); );
} }
@ -74,6 +75,7 @@ PHP;
{ {
$c = i18nTextCollector::create(); $c = i18nTextCollector::create();
$c->setWarnOnEmptyDefault(false); $c->setWarnOnEmptyDefault(false);
$mymodule = ModuleLoader::instance()->getManifest()->getModule('i18ntestmodule');
$html = <<<SS $html = <<<SS
<% _t('Test.SINGLEQUOTE','Single Quote'); %> <% _t('Test.SINGLEQUOTE','Single Quote'); %>
@ -85,7 +87,7 @@ PHP;
<%t i18nTestModule.INJECTIONS_4 name=\$absoluteBaseURL greeting=\$get_locale goodbye="global calls" %> <%t i18nTestModule.INJECTIONS_4 name=\$absoluteBaseURL greeting=\$get_locale goodbye="global calls" %>
<%t i18nTestModule.INJECTIONS_9 "An item|{count} items" is "Test Pluralisation" count=4 %> <%t i18nTestModule.INJECTIONS_9 "An item|{count} items" is "Test Pluralisation" count=4 %>
SS; SS;
$c->collectFromTemplate($html, 'mymodule', 'Test'); $c->collectFromTemplate($html, null, $mymodule);
$this->assertEquals( $this->assertEquals(
[ [
@ -103,7 +105,7 @@ SS;
'comment' => 'Test Pluralisation' 'comment' => 'Test Pluralisation'
], ],
], ],
$c->collectFromTemplate($html, 'mymodule', 'Test') $c->collectFromTemplate($html, null, $mymodule)
); );
// Test warning is raised on empty default // Test warning is raised on empty default
@ -112,19 +114,20 @@ SS;
PHPUnit_Framework_Error_Notice::class, PHPUnit_Framework_Error_Notice::class,
'Missing localisation default for key i18nTestModule.INJECTIONS_3' 'Missing localisation default for key i18nTestModule.INJECTIONS_3'
); );
$c->collectFromTemplate($html, 'mymodule', 'Test'); $c->collectFromTemplate($html, null, $mymodule);
} }
public function testCollectFromTemplateSimple() public function testCollectFromTemplateSimple()
{ {
$c = i18nTextCollector::create(); $c = i18nTextCollector::create();
$mymodule = ModuleLoader::instance()->getManifest()->getModule('i18ntestmodule');
$html = <<<SS $html = <<<SS
<% _t('Test.SINGLEQUOTE','Single Quote'); %> <% _t('Test.SINGLEQUOTE','Single Quote'); %>
SS; SS;
$this->assertEquals( $this->assertEquals(
[ 'Test.SINGLEQUOTE' => 'Single Quote' ], [ 'Test.SINGLEQUOTE' => 'Single Quote' ],
$c->collectFromTemplate($html, 'mymodule', 'Test') $c->collectFromTemplate($html, null, $mymodule)
); );
$html = <<<SS $html = <<<SS
@ -132,7 +135,7 @@ SS;
SS; SS;
$this->assertEquals( $this->assertEquals(
[ 'Test.DOUBLEQUOTE' => "Double Quote and Spaces" ], [ 'Test.DOUBLEQUOTE' => "Double Quote and Spaces" ],
$c->collectFromTemplate($html, 'mymodule', 'Test') $c->collectFromTemplate($html, null, $mymodule)
); );
$html = <<<SS $html = <<<SS
@ -140,7 +143,7 @@ SS;
SS; SS;
$this->assertEquals( $this->assertEquals(
[ 'Test.NOSEMICOLON' => "No Semicolon" ], [ 'Test.NOSEMICOLON' => "No Semicolon" ],
$c->collectFromTemplate($html, 'mymodule', 'Test') $c->collectFromTemplate($html, null, $mymodule)
); );
} }
@ -148,6 +151,7 @@ SS;
{ {
$c = i18nTextCollector::create(); $c = i18nTextCollector::create();
$c->setWarnOnEmptyDefault(false); $c->setWarnOnEmptyDefault(false);
$mymodule = ModuleLoader::instance()->getManifest()->getModule('i18ntestmodule');
$html = <<<SS $html = <<<SS
<% _t( <% _t(
@ -157,7 +161,7 @@ SS;
SS; SS;
$this->assertEquals( $this->assertEquals(
[ 'Test.NEWLINES' => "New Lines" ], [ 'Test.NEWLINES' => "New Lines" ],
$c->collectFromTemplate($html, 'mymodule', 'Test') $c->collectFromTemplate($html, 'Test', $mymodule)
); );
$html = <<<SS $html = <<<SS
@ -172,7 +176,7 @@ SS;
'default' => ' Prio and Value with "Double Quotes"', 'default' => ' Prio and Value with "Double Quotes"',
'comment' => 'Comment with "Double Quotes"', 'comment' => 'Comment with "Double Quotes"',
]], ]],
$c->collectFromTemplate($html, 'mymodule', 'Test') $c->collectFromTemplate($html, 'Test', $mymodule)
); );
$html = <<<SS $html = <<<SS
@ -188,7 +192,7 @@ SS;
'default' => " Prio and Value with 'Single Quotes'", 'default' => " Prio and Value with 'Single Quotes'",
'comment' => "Comment with 'Single Quotes'", 'comment' => "Comment with 'Single Quotes'",
]], ]],
$c->collectFromTemplate($html, 'mymodule', 'Test') $c->collectFromTemplate($html, 'Test', $mymodule)
); );
// Test empty // Test empty
@ -197,7 +201,7 @@ SS;
SS; SS;
$this->assertEquals( $this->assertEquals(
[], [],
$c->collectFromTemplate($html, 'mymodule', 'Test') $c->collectFromTemplate($html, null, $mymodule)
); );
// Test warning is raised on empty default // Test warning is raised on empty default
@ -206,20 +210,21 @@ SS;
PHPUnit_Framework_Error_Notice::class, PHPUnit_Framework_Error_Notice::class,
'Missing localisation default for key Test.PRIOANDCOMMENT' 'Missing localisation default for key Test.PRIOANDCOMMENT'
); );
$c->collectFromTemplate($html, 'mymodule', 'Test'); $c->collectFromTemplate($html, 'Test', $mymodule);
} }
public function testCollectFromCodeSimple() public function testCollectFromCodeSimple()
{ {
$c = i18nTextCollector::create(); $c = i18nTextCollector::create();
$mymodule = ModuleLoader::instance()->getManifest()->getModule('i18ntestmodule');
$php = <<<PHP $php = <<<PHP
_t('Test.SINGLEQUOTE','Single Quote'); _t('Test.SINGLEQUOTE','Single Quote');
PHP; PHP;
$this->assertEquals( $this->assertEquals(
[ 'Test.SINGLEQUOTE' => 'Single Quote' ], [ 'Test.SINGLEQUOTE' => 'Single Quote' ],
$c->collectFromCode($php, 'mymodule') $c->collectFromCode($php, null, $mymodule)
); );
$php = <<<PHP $php = <<<PHP
@ -227,13 +232,14 @@ _t( "Test.DOUBLEQUOTE", "Double Quote and Spaces" );
PHP; PHP;
$this->assertEquals( $this->assertEquals(
[ 'Test.DOUBLEQUOTE' => "Double Quote and Spaces" ], [ 'Test.DOUBLEQUOTE' => "Double Quote and Spaces" ],
$c->collectFromCode($php, 'mymodule') $c->collectFromCode($php, null, $mymodule)
); );
} }
public function testCollectFromCodeAdvanced() public function testCollectFromCodeAdvanced()
{ {
$c = i18nTextCollector::create(); $c = i18nTextCollector::create();
$mymodule = ModuleLoader::instance()->getManifest()->getModule('i18ntestmodule');
$php = <<<PHP $php = <<<PHP
_t( _t(
@ -243,7 +249,7 @@ _t(
PHP; PHP;
$this->assertEquals( $this->assertEquals(
[ 'Test.NEWLINES' => "New Lines" ], [ 'Test.NEWLINES' => "New Lines" ],
$c->collectFromCode($php, 'mymodule') $c->collectFromCode($php, null, $mymodule)
); );
$php = <<<PHP $php = <<<PHP
@ -261,7 +267,7 @@ PHP;
'comment' => 'Comment with "Double Quotes"', 'comment' => 'Comment with "Double Quotes"',
] ]
], ],
$c->collectFromCode($php, 'mymodule') $c->collectFromCode($php, null, $mymodule)
); );
$php = <<<PHP $php = <<<PHP
@ -277,7 +283,7 @@ PHP;
'default' => " Value with 'Single Quotes'", 'default' => " Value with 'Single Quotes'",
'comment' => "Comment with 'Single Quotes'" 'comment' => "Comment with 'Single Quotes'"
] ], ] ],
$c->collectFromCode($php, 'mymodule') $c->collectFromCode($php, null, $mymodule)
); );
$php = <<<PHP $php = <<<PHP
@ -288,7 +294,7 @@ _t(
PHP; PHP;
$this->assertEquals( $this->assertEquals(
[ 'Test.PRIOANDCOMMENT' => "Value with 'Escaped Single Quotes'" ], [ 'Test.PRIOANDCOMMENT' => "Value with 'Escaped Single Quotes'" ],
$c->collectFromCode($php, 'mymodule') $c->collectFromCode($php, null, $mymodule)
); );
$php = <<<PHP $php = <<<PHP
@ -301,14 +307,14 @@ _t(
PHP; PHP;
$this->assertEquals( $this->assertEquals(
[ 'Test.PRIOANDCOMMENT' => "Doublequoted Value with 'Unescaped Single Quotes'"], [ 'Test.PRIOANDCOMMENT' => "Doublequoted Value with 'Unescaped Single Quotes'"],
$c->collectFromCode($php, 'mymodule') $c->collectFromCode($php, null, $mymodule)
); );
} }
public function testCollectFromCodeNamespace() public function testCollectFromCodeNamespace()
{ {
$c = i18nTextCollector::create(); $c = i18nTextCollector::create();
$mymodule = ModuleLoader::instance()->getManifest()->getModule('i18ntestmodule');
$php = <<<PHP $php = <<<PHP
<?php <?php
namespace SilverStripe\Framework\Core; namespace SilverStripe\Framework\Core;
@ -324,7 +330,7 @@ class MyClass extends Base implements SomeService {
PHP; PHP;
$this->assertEquals( $this->assertEquals(
[ 'SilverStripe\\Framework\\Core\\MyClass.NEWLINES' => "New Lines" ], [ 'SilverStripe\\Framework\\Core\\MyClass.NEWLINES' => "New Lines" ],
$c->collectFromCode($php, 'mymodule') $c->collectFromCode($php, null, $mymodule)
); );
} }
@ -332,6 +338,7 @@ PHP;
public function testNewlinesInEntityValues() public function testNewlinesInEntityValues()
{ {
$c = i18nTextCollector::create(); $c = i18nTextCollector::create();
$mymodule = ModuleLoader::instance()->getManifest()->getModule('i18ntestmodule');
$php = <<<PHP $php = <<<PHP
_t( _t(
@ -344,7 +351,7 @@ PHP;
$eol = PHP_EOL; $eol = PHP_EOL;
$this->assertEquals( $this->assertEquals(
[ 'Test.NEWLINESINGLEQUOTE' => "Line 1{$eol}Line 2" ], [ 'Test.NEWLINESINGLEQUOTE' => "Line 1{$eol}Line 2" ],
$c->collectFromCode($php, 'mymodule') $c->collectFromCode($php, null, $mymodule)
); );
$php = <<<PHP $php = <<<PHP
@ -356,7 +363,7 @@ Line 2"
PHP; PHP;
$this->assertEquals( $this->assertEquals(
[ 'Test.NEWLINEDOUBLEQUOTE' => "Line 1{$eol}Line 2" ], [ 'Test.NEWLINEDOUBLEQUOTE' => "Line 1{$eol}Line 2" ],
$c->collectFromCode($php, 'mymodule') $c->collectFromCode($php, null, $mymodule)
); );
} }
@ -367,6 +374,7 @@ PHP;
{ {
$c = i18nTextCollector::create(); $c = i18nTextCollector::create();
$c->setWarnOnEmptyDefault(false); // Disable warnings for tests $c->setWarnOnEmptyDefault(false); // Disable warnings for tests
$mymodule = ModuleLoader::instance()->getManifest()->getModule('i18ntestmodule');
$php = <<<PHP $php = <<<PHP
_t('i18nTestModule.NEWMETHODSIG',"New _t method signature test"); _t('i18nTestModule.NEWMETHODSIG',"New _t method signature test");
@ -385,7 +393,7 @@ _t('i18nTestModule.INJECTIONS8', ["name"=>"Cat", "greeting"=>"meow", "goodbye"=>
_t('i18nTestModule.INJECTIONS9', "An item|{count} items", ['count' => 4], "Test Pluralisation"); _t('i18nTestModule.INJECTIONS9', "An item|{count} items", ['count' => 4], "Test Pluralisation");
PHP; PHP;
$collectedTranslatables = $c->collectFromCode($php, 'mymodule'); $collectedTranslatables = $c->collectFromCode($php, null, $mymodule);
$expectedArray = [ $expectedArray = [
'i18nTestModule.INJECTIONS2' => "Hello {name} {greeting}. But it is late, {goodbye}", 'i18nTestModule.INJECTIONS2' => "Hello {name} {greeting}. But it is late, {goodbye}",
@ -416,12 +424,13 @@ PHP;
_t('i18nTestModule.INJECTIONS4', array("name"=>"Cat", "greeting"=>"meow", "goodbye"=>"meow")); _t('i18nTestModule.INJECTIONS4', array("name"=>"Cat", "greeting"=>"meow", "goodbye"=>"meow"));
PHP; PHP;
$c->setWarnOnEmptyDefault(true); $c->setWarnOnEmptyDefault(true);
$c->collectFromCode($php, 'mymodule'); $c->collectFromCode($php, null, $mymodule);
} }
public function testUncollectableCode() public function testUncollectableCode()
{ {
$c = i18nTextCollector::create(); $c = i18nTextCollector::create();
$mymodule = ModuleLoader::instance()->getManifest()->getModule('i18ntestmodule');
$php = <<<PHP $php = <<<PHP
_t(static::class.'.KEY1', 'Default'); _t(static::class.'.KEY1', 'Default');
@ -430,7 +439,7 @@ _t(__CLASS__.'.KEY3', 'Default');
_t('Collectable.KEY4', 'Default'); _t('Collectable.KEY4', 'Default');
PHP; PHP;
$collectedTranslatables = $c->collectFromCode($php, 'mymodule'); $collectedTranslatables = $c->collectFromCode($php, null, $mymodule);
// Only one item is collectable // Only one item is collectable
$expectedArray = [ 'Collectable.KEY4' => 'Default' ]; $expectedArray = [ 'Collectable.KEY4' => 'Default' ];
@ -441,20 +450,21 @@ PHP;
{ {
$c = i18nTextCollector::create(); $c = i18nTextCollector::create();
$c->setWarnOnEmptyDefault(false); // Disable warnings for tests $c->setWarnOnEmptyDefault(false); // Disable warnings for tests
$mymodule = ModuleLoader::instance()->getManifest()->getModule('i18ntestmodule');
$templateFilePath = $this->alternateBasePath . '/i18ntestmodule/templates/Layout/i18nTestModule.ss'; $templateFilePath = $this->alternateBasePath . '/i18ntestmodule/templates/Layout/i18nTestModule.ss';
$html = file_get_contents($templateFilePath); $html = file_get_contents($templateFilePath);
$matches = $c->collectFromTemplate($html, 'mymodule', 'RandomNamespace'); $matches = $c->collectFromTemplate($html, $templateFilePath, $mymodule);
$this->assertArrayHasKey('RandomNamespace.LAYOUTTEMPLATENONAMESPACE', $matches); $this->assertArrayHasKey('i18nTestModule.ss.LAYOUTTEMPLATENONAMESPACE', $matches);
$this->assertEquals( $this->assertEquals(
'Layout Template no namespace', 'Layout Template no namespace',
$matches['RandomNamespace.LAYOUTTEMPLATENONAMESPACE'] $matches['i18nTestModule.ss.LAYOUTTEMPLATENONAMESPACE']
); );
$this->assertArrayHasKey('RandomNamespace.SPRINTFNONAMESPACE', $matches); $this->assertArrayHasKey('i18nTestModule.ss.SPRINTFNONAMESPACE', $matches);
$this->assertEquals( $this->assertEquals(
'My replacement no namespace: %s', 'My replacement no namespace: %s',
$matches['RandomNamespace.SPRINTFNONAMESPACE'] $matches['i18nTestModule.ss.SPRINTFNONAMESPACE']
); );
$this->assertArrayHasKey('i18nTestModule.LAYOUTTEMPLATE', $matches); $this->assertArrayHasKey('i18nTestModule.LAYOUTTEMPLATE', $matches);
$this->assertEquals( $this->assertEquals(
@ -474,44 +484,6 @@ PHP;
$this->assertArrayNotHasKey('i18nTestModuleInclude.ss.SPRINTFINCLUDENONAMESPACE', $matches); $this->assertArrayNotHasKey('i18nTestModuleInclude.ss.SPRINTFINCLUDENONAMESPACE', $matches);
} }
public function testCollectFromThemesTemplates()
{
$c = i18nTextCollector::create();
SSViewer::set_themes([ 'testtheme1' ]);
// Collect from layout
$layoutFilePath = $this->alternateBasePath . '/themes/testtheme1/templates/Layout/i18nTestTheme1.ss';
$layoutHTML = file_get_contents($layoutFilePath);
$layoutMatches = $c->collectFromTemplate($layoutHTML, 'themes/testtheme1', 'i18nTestTheme1.ss');
// all entities from i18nTestTheme1.ss
$this->assertEquals(
[
'i18nTestTheme1.LAYOUTTEMPLATE' => 'Theme1 Layout Template',
'i18nTestTheme1.SPRINTFNAMESPACE' => 'Theme1 My replacement: %s',
'i18nTestTheme1.ss.LAYOUTTEMPLATENONAMESPACE' => 'Theme1 Layout Template no namespace',
'i18nTestTheme1.ss.SPRINTFNONAMESPACE' => 'Theme1 My replacement no namespace: %s',
],
$layoutMatches
);
// Collect from include
$includeFilePath = $this->alternateBasePath . '/themes/testtheme1/templates/Includes/i18nTestTheme1Include.ss';
$includeHTML = file_get_contents($includeFilePath);
$includeMatches = $c->collectFromTemplate($includeHTML, 'themes/testtheme1', 'i18nTestTheme1Include.ss');
// all entities from i18nTestTheme1Include.ss
$this->assertEquals(
[
'i18nTestTheme1Include.SPRINTFINCLUDENAMESPACE' => 'Theme1 My include replacement: %s',
'i18nTestTheme1Include.WITHNAMESPACE' => 'Theme1 Include Entity with Namespace',
'i18nTestTheme1Include.ss.NONAMESPACE' => 'Theme1 Include Entity without Namespace',
'i18nTestTheme1Include.ss.SPRINTFINCLUDENONAMESPACE' => 'Theme1 My include replacement no namespace: %s'
],
$includeMatches
);
}
public function testCollectMergesWithExisting() public function testCollectMergesWithExisting()
{ {
$c = i18nTextCollector::create(); $c = i18nTextCollector::create();
@ -608,63 +580,6 @@ PHP;
" MAINTEMPLATE: 'Main Template Other Module'\n", " MAINTEMPLATE: 'Main Template Other Module'\n",
$otherModuleLangFileContent $otherModuleLangFileContent
); );
// testtheme1
$theme1LangFile = "{$this->alternateBaseSavePath}/themes/testtheme1/lang/" . $c->getDefaultLocale() . '.yml';
$this->assertTrue(
file_exists($theme1LangFile),
'Master theme language file can be written to themes/testtheme1 /lang folder'
);
$theme1LangFileContent = file_get_contents($theme1LangFile);
$this->assertContains(
" MAINTEMPLATE: 'Theme1 Main Template'\n",
$theme1LangFileContent
);
$this->assertContains(
" LAYOUTTEMPLATE: 'Theme1 Layout Template'\n",
$theme1LangFileContent
);
$this->assertContains(
" SPRINTFNAMESPACE: 'Theme1 My replacement: %s'\n",
$theme1LangFileContent
);
$this->assertContains(
" LAYOUTTEMPLATENONAMESPACE: 'Theme1 Layout Template no namespace'\n",
$theme1LangFileContent
);
$this->assertContains(
" SPRINTFNONAMESPACE: 'Theme1 My replacement no namespace: %s'\n",
$theme1LangFileContent
);
$this->assertContains(
" SPRINTFINCLUDENAMESPACE: 'Theme1 My include replacement: %s'\n",
$theme1LangFileContent
);
$this->assertContains(
" WITHNAMESPACE: 'Theme1 Include Entity with Namespace'\n",
$theme1LangFileContent
);
$this->assertContains(
" NONAMESPACE: 'Theme1 Include Entity without Namespace'\n",
$theme1LangFileContent
);
$this->assertContains(
" SPRINTFINCLUDENONAMESPACE: 'Theme1 My include replacement no namespace: %s'\n",
$theme1LangFileContent
);
// testtheme2
$theme2LangFile = "{$this->alternateBaseSavePath}/themes/testtheme2/lang/" . $c->getDefaultLocale() . '.yml';
$this->assertTrue(
file_exists($theme2LangFile),
'Master theme language file can be written to themes/testtheme2 /lang folder'
);
$theme2LangFileContent = file_get_contents($theme2LangFile);
$this->assertContains(
" MAINTEMPLATE: 'Theme2 Main Template'\n",
$theme2LangFileContent
);
} }
public function testCollectFromEntityProvidersInCustomObject() public function testCollectFromEntityProvidersInCustomObject()
@ -793,16 +708,14 @@ PHP;
public function testModuleDetection() public function testModuleDetection()
{ {
$collector = new Collector(); $collector = new Collector();
$modules = $collector->getModules_Test($this->alternateBasePath); $modules = ModuleLoader::instance()->getManifest()->getModules();
$this->assertEquals( $this->assertEquals(
array( array(
'i18nnonstandardmodule', 'i18nnonstandardmodule',
'i18nothermodule',
'i18ntestmodule', 'i18ntestmodule',
'themes/testtheme1', 'i18nothermodule'
'themes/testtheme2'
), ),
$modules array_keys($modules)
); );
$this->assertEquals('i18ntestmodule', $collector->findModuleForClass_Test('i18nTestNamespacedClass')); $this->assertEquals('i18ntestmodule', $collector->findModuleForClass_Test('i18nTestNamespacedClass'));
@ -853,22 +766,5 @@ PHP;
$this->assertArrayHasKey("{$otherRoot}/code/i18nProviderClass.php", $otherFiles); $this->assertArrayHasKey("{$otherRoot}/code/i18nProviderClass.php", $otherFiles);
$this->assertArrayHasKey("{$otherRoot}/code/i18nTestModuleDecorator.php", $otherFiles); $this->assertArrayHasKey("{$otherRoot}/code/i18nTestModuleDecorator.php", $otherFiles);
$this->assertArrayHasKey("{$otherRoot}/templates/i18nOtherModule.ss", $otherFiles); $this->assertArrayHasKey("{$otherRoot}/templates/i18nOtherModule.ss", $otherFiles);
// Themes should detect all ss files only
$theme1Files = $collector->getFileListForModule_Test('themes/testtheme1');
$theme1Root = $this->alternateBasePath . '/themes/testtheme1/templates';
$this->assertEquals(3, count($theme1Files));
// Find only ss files
$this->assertArrayHasKey("{$theme1Root}/Includes/i18nTestTheme1Include.ss", $theme1Files);
$this->assertArrayHasKey("{$theme1Root}/Layout/i18nTestTheme1.ss", $theme1Files);
$this->assertArrayHasKey("{$theme1Root}/i18nTestTheme1Main.ss", $theme1Files);
// Only 1 file here
$theme2Files = $collector->getFileListForModule_Test('themes/testtheme2');
$this->assertEquals(1, count($theme2Files));
$this->assertArrayHasKey(
$this->alternateBasePath . '/themes/testtheme2/templates/i18nTestTheme2.ss',
$theme2Files
);
} }
} }

View File

@ -2,6 +2,8 @@
namespace SilverStripe\i18n\Tests\i18nTextCollectorTest; namespace SilverStripe\i18n\Tests\i18nTextCollectorTest;
use SilverStripe\Core\Manifest\Module;
use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\Dev\TestOnly; use SilverStripe\Dev\TestOnly;
use SilverStripe\i18n\TextCollection\i18nTextCollector; use SilverStripe\i18n\TextCollection\i18nTextCollector;
@ -10,18 +12,17 @@ use SilverStripe\i18n\TextCollection\i18nTextCollector;
*/ */
class Collector extends i18nTextCollector implements TestOnly class Collector extends i18nTextCollector implements TestOnly
{ {
public function getModules_Test($directory)
{
return $this->getModules($directory);
}
public function resolveDuplicateConflicts_Test($entitiesByModule) public function resolveDuplicateConflicts_Test($entitiesByModule)
{ {
return $this->resolveDuplicateConflicts($entitiesByModule); return $this->resolveDuplicateConflicts($entitiesByModule);
} }
public function getFileListForModule_Test($module) public function getFileListForModule_Test($modulename)
{ {
$module = ModuleLoader::instance()->getManifest()->getModule($modulename);
if (!$module) {
throw new \BadMethodCallException("No module named {$modulename}");
}
return $this->getFileListForModule($module); return $this->getFileListForModule($module);
} }