API Replace ManifestCache with ManifestCacheFactory

API Remove lots of deprecated module code from ClassManifest
This commit is contained in:
Damian Mooyman 2017-03-14 15:20:51 +13:00
parent 89d5151f07
commit 54ba08a306
30 changed files with 648 additions and 627 deletions

View File

@ -4,9 +4,8 @@ Before: '/i18n'
---
SilverStripe\i18n\Data\Sources:
module_priority:
- admin
- framework
- sapphire
- silverstripe\admin
- silverstripe\framework
---
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_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_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_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
By default, manifests are stored on the local filesystem through PHP's `serialize()` method.
Combined with PHP opcode caching this provides fast access.
In order to share manifests between servers, or centralise cache management,
other storage adapters are available. These can be configured by a `SS_MANIFESTCACHE` constant,
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.
By default, manifests are serialised and cached via a cache generated by the [api:ManifestCacheFactory].
This can be customised via `SS_MANIFESTCACHE` environment variable to point to either another
[api:CacheFactory] or [CacheInterface](https://github.com/php-fig/cache/blob/master/src/CacheItemInterface.php)
implementor.
## Traversing the Filesystem

View File

@ -1065,6 +1065,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.
* Introduced new ModuleLoader manifest, which allows modules to be found via composer name.
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`:
* `Link`
* `redirect`
@ -1099,6 +1100,8 @@ now generally safer to use the default inherited config, where in the past you w
* Removed `SilverStripeInjectionCreator`
* Removed `i18n::get_translatable_modules` method.
* 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 `FunctionalTest::stat`
* Removed `LeftAndMainMarkingFilter`
@ -1114,6 +1117,11 @@ now generally safer to use the default inherited config, where in the past you w
* Removed `Config::FIRST_SET` and `Config::INHERITED`
* Removed `RequestHandler.require_allowed_actions`. This is now fixed to on and cannot be
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
@ -1565,6 +1573,7 @@ New `TimeField` methods replace `getConfig()` / `setConfig()`
* `i18n::get_language_name()` moved to `SilverStripe\i18n\Data\Locales::languageName()`
* `i18n.module_priority` config moved to `SilverStripe\i18n\Data\Sources.module_priority`
* `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()`
#### <a name="overview-i18n-removed"></a>i18n API Removed API

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\Logger;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Config\Collections\CachedConfigCollection;
use SilverStripe\Config\Collections\MemoryConfigCollection;
use SilverStripe\Config\Transformer\PrivateStaticTransformer;
use SilverStripe\Config\Transformer\YamlTransformer;
use SilverStripe\Control\Director;
use SilverStripe\Core\Cache\CacheFactory;
use SilverStripe\Core\Config\Middleware\ExtensionMiddleware;
use SilverStripe\Core\Config\Middleware\InheritanceMiddleware;
use SilverStripe\Core\Manifest\ClassLoader;
@ -45,14 +47,18 @@ class CoreConfigFactory
* which conditionally generates a nested "core" config.
*
* @param bool $flush
* @param CacheFactory $cacheFactory
* @return CachedConfigCollection
*/
public function createRoot($flush)
public function createRoot($flush, CacheFactory $cacheFactory)
{
$instance = new CachedConfigCollection();
// Set root cache
$instance->setPool($this->createPool());
// Create config cache
$cache = $cacheFactory->create(CacheInterface::class.'.configcache', [
'namespace' => 'configcache'
]);
$instance->setCache($cache);
$instance->setFlush($flush);
// Set collection creator
@ -172,32 +178,4 @@ class CoreConfigFactory
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
use SilverStripe\Core\Cache\ManifestCacheFactory;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Config\CoreConfigFactory;
use SilverStripe\Core\Config\ConfigLoader;
@ -70,8 +71,14 @@ Injector::set_inst($injector);
$requestURL = isset($_REQUEST['url']) ? trim($_REQUEST['url'], '/') : false;
$flush = (isset($_GET['flush']) || $requestURL === trim(BASE_URL . '/dev/build', '/'));
global $manifest;
$manifest = new ClassManifest(BASE_PATH, false, $flush);
// Manifest cache factory
$manifestCacheFactory = new ManifestCacheFactory([
'namespace' => 'manifestcache',
'directory' => getTempFolder(),
]);
// Build class manifest
$manifest = new ClassManifest(BASE_PATH, false, $flush, $manifestCacheFactory);
// Register SilverStripe's class map autoload
$loader = ClassLoader::instance();
@ -79,11 +86,11 @@ $loader->registerAutoloader();
$loader->pushManifest($manifest);
// Init module manifest
$moduleManifest = new ModuleManifest(BASE_PATH, false, $flush);
$moduleManifest = new ModuleManifest(BASE_PATH, false, $flush, $manifestCacheFactory);
ModuleLoader::instance()->pushManifest($moduleManifest);
// Build config manifest
$configManifest = CoreConfigFactory::inst()->createRoot($flush);
$configManifest = CoreConfigFactory::inst()->createRoot($flush, $manifestCacheFactory);
ConfigLoader::instance()->pushManifest($configManifest);
// After loading config, boot _config.php files
@ -94,7 +101,8 @@ SilverStripe\View\ThemeResourceLoader::instance()->addSet('$default', new Silver
BASE_PATH,
project(),
false,
$flush
$flush,
$manifestCacheFactory
));
// 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\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\Parser;
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
* additional items present in a directory, and caches it.
* A utility class which builds a manifest of all classes, interfaces and caches it.
*
* It finds the following information:
* - Class and interface names and paths.
* - All direct and indirect descendants of a class.
* - All implementors of an interface.
* - All module configuration files.
*/
class ClassManifest
{
const CONF_FILE = '_config.php';
const CONF_DIR = '_config';
/**
* base manifest directory
* @var string
*/
protected $base;
/**
* Set if including test classes
*
* @see TestOnly
* @var bool
*/
protected $tests;
/**
* @var ManifestCache
* Cache to use, if caching.
* Set to null if uncached.
*
* @var CacheInterface|null
*/
protected $cache;
/**
* Key to use for the top level cache of all items
*
* @var string
*/
protected $cacheKey;
/**
* Map of classes to paths
*
* @var 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;
/**
* @var NodeTraverser
*/
private $traverser;
/**
* @var ClassManifestVisitor
*/
@ -66,33 +122,44 @@ class ClassManifest
* from the cache or re-scanning for classes.
*
* @param string $base The manifest base path.
* @param bool $includeTests Include the contents of "tests" directories.
* @param bool $forceRegen Force the manifest to be regenerated.
* @param bool $cache If the manifest is regenerated, cache it.
* @param bool $includeTests Include the contents of "tests" directories.
* @param bool $forceRegen Force the manifest to be regenerated.
* @param CacheFactory $cacheFactory Optional cache to use. Set to null to not cache.
*/
public function __construct($base, $includeTests = false, $forceRegen = false, $cache = true)
{
$this->base = $base;
public function __construct(
$base,
$includeTests = false,
$forceRegen = false,
CacheFactory $cacheFactory = null
) {
$this->base = $base;
$this->tests = $includeTests;
$cacheClass = getenv('SS_MANIFESTCACHE') ?: 'SilverStripe\\Core\\Manifest\\ManifestCache_File';
$this->cache = new $cacheClass('classmanifest'.($includeTests ? '_tests' : ''));
// build cache from factory
if ($cacheFactory) {
$this->cache = $cacheFactory->create(
CacheInterface::class.'.classmanifest',
[ 'namespace' => 'classmanifest' . ($includeTests ? '_tests' : '') ]
);
}
$this->cacheKey = 'manifest';
if (!$forceRegen && $data = $this->cache->load($this->cacheKey)) {
$this->classes = $data['classes'];
$this->descendants = $data['descendants'];
$this->interfaces = $data['interfaces'];
if (!$forceRegen && $this->cache && ($data = $this->cache->get($this->cacheKey))) {
$this->classes = $data['classes'];
$this->descendants = $data['descendants'];
$this->interfaces = $data['interfaces'];
$this->implementors = $data['implementors'];
$this->configs = $data['configs'];
$this->configDirs = $data['configDirs'];
$this->traits = $data['traits'];
$this->traits = $data['traits'];
} else {
$this->regenerate($cache);
$this->regenerate();
}
}
/**
* Get or create active parser
*
* @return Parser
*/
public function getParser()
{
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
*
* @param string $class Class name
* @return string
* @return Module
*/
public function getOwnerModule($class)
{
@ -297,29 +326,32 @@ class ClassManifest
return null;
}
/** @var Module $rootModule */
$rootModule = null;
// Find based on loaded modules
foreach ($this->getModules() as $parent => $module) {
if (stripos($path, realpath($parent)) === 0) {
$modules = ModuleLoader::instance()->getManifest()->getModules();
foreach ($modules as $module) {
// Leave root module as fallback
if (empty($module->getRelativePath())) {
$rootModule = $module;
} elseif (stripos($path, realpath($module->getPath())) === 0) {
return $module;
}
}
// Assume top level folder is the module name
$relativePath = substr($path, strlen(realpath(Director::baseFolder())));
$parts = explode('/', trim($relativePath, '/'));
return array_shift($parts);
// Fall back to top level module
return $rootModule;
}
/**
* Completely regenerates the manifest file.
*
* @param bool $cache Cache the result.
*/
public function regenerate($cache = true)
public function regenerate()
{
$resets = array(
'classes', 'roots', 'children', 'descendants', 'interfaces',
'implementors', 'configs', 'configDirs', 'traits'
'implementors', 'traits'
);
// Reset the manifest so stale info doesn't cause errors.
@ -329,11 +361,10 @@ class ClassManifest
$finder = new ManifestFileFinder();
$finder->setOptions(array(
'name_regex' => '/^((_config)|([^_].*))\\.php$/',
'name_regex' => '/^[^_].*\\.php$/',
'ignore_files' => array('index.php', 'main.php', 'cli-script.php'),
'ignore_tests' => !$this->tests,
'file_callback' => array($this, 'handleFile'),
'dir_callback' => array($this, 'handleDir')
));
$finder->find($this->base);
@ -341,34 +372,20 @@ class ClassManifest
$this->coalesceDescendants($root);
}
if ($cache) {
if ($this->cache) {
$data = array(
'classes' => $this->classes,
'descendants' => $this->descendants,
'interfaces' => $this->interfaces,
'implementors' => $this->implementors,
'configs' => $this->configs,
'configDirs' => $this->configDirs,
'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;
$interfaces = null;
$traits = null;
@ -379,24 +396,16 @@ class ClassManifest
// since just using the datetime lead to problems with upgrading.
$key = preg_replace('/[^a-zA-Z0-9_]/', '_', $basename) . '_' . md5_file($pathname);
$valid = false;
if ($data = $this->cache->load($key)) {
$valid = (
isset($data['classes']) && is_array($data['classes'])
&& isset($data['interfaces'])
&& is_array($data['interfaces'])
&& isset($data['traits'])
&& is_array($data['traits'])
);
if ($valid) {
$classes = $data['classes'];
$interfaces = $data['interfaces'];
$traits = $data['traits'];
}
}
if (!$valid) {
// Attempt to load from cache
if ($this->cache
&& ($data = $this->cache->get($key))
&& $this->validateItemCache($data)
) {
$classes = $data['classes'];
$interfaces = $data['interfaces'];
$traits = $data['traits'];
} else {
// Build from php file parser
$fileContents = ClassContentRemover::remove_class_content($pathname);
try {
$stmts = $this->getParser()->parse($fileContents);
@ -410,14 +419,18 @@ class ClassManifest
$interfaces = $this->getVisitor()->getInterfaces();
$traits = $this->getVisitor()->getTraits();
$cache = array(
'classes' => $classes,
'interfaces' => $interfaces,
'traits' => $traits,
);
$this->cache->save($cache, $key);
// Save back to cache if configured
if ($this->cache) {
$cache = array(
'classes' => $classes,
'interfaces' => $interfaces,
'traits' => $traits,
);
$this->cache->set($key, $cache);
}
}
// Merge this data into the global list
foreach ($classes as $className => $classInfo) {
$extends = isset($classInfo['extends']) ? $classInfo['extends'] : null;
$implements = isset($classInfo['interfaces']) ? $classInfo['interfaces'] : null;
@ -496,4 +509,30 @@ class ClassManifest
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;
use LogicException;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Cache\CacheFactory;
/**
* A utility class which builds a manifest of configuration items
@ -31,7 +33,7 @@ class ModuleManifest
protected $includeTests;
/**
* @var ManifestCache
* @var CacheInterface
*/
protected $cache;
@ -87,37 +89,31 @@ class ModuleManifest
* @param string $base The project base path.
* @param bool $includeTests
* @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->cacheKey = sha1($base).'_modules';
$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
if (!$forceRegen) {
$this->modules = $this->cache->load($this->cacheKey) ?: [];
if (!$forceRegen && $this->cache) {
$this->modules = $this->cache->get($this->cacheKey) ?: [];
}
if (empty($this->modules)) {
$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.
*/
@ -136,9 +132,8 @@ class ModuleManifest
* Does _not_ build the actual variant
*
* @param bool $includeTests
* @param bool $cache Cache the result.
*/
public function regenerate($includeTests = false, $cache = true)
public function regenerate($includeTests = false)
{
$this->modules = [];
@ -162,8 +157,8 @@ class ModuleManifest
));
$finder->find($this->base);
if ($cache) {
$this->cache->save($this->modules, $this->cacheKey);
if ($this->cache) {
$this->cache->set($this->cacheKey, $this->modules);
}
}
@ -172,9 +167,8 @@ class ModuleManifest
*
* @param string $basename
* @param string $pathname
* @param int $depth
*/
public function addSourceConfigFile($basename, $pathname, $depth)
public function addSourceConfigFile($basename, $pathname)
{
$this->addModule(dirname($pathname));
}
@ -184,9 +178,8 @@ class ModuleManifest
*
* @param string $basename
* @param string $pathname
* @param int $depth
*/
public function addYAMLConfigFile($basename, $pathname, $depth)
public function addYAMLConfigFile($basename, $pathname)
{
if (preg_match('{/([^/]+)/_config/}', $pathname, $match)) {
$this->addModule(dirname(dirname($pathname)));

View File

@ -4,6 +4,8 @@ namespace SilverStripe\Dev;
use SilverStripe\Control\Director;
use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Manifest\Module;
use SilverStripe\Core\Manifest\ModuleLoader;
/**
* Handles raising an notice when accessing a deprecated method
@ -96,7 +98,7 @@ class Deprecation
* #notice)
*
* @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)
{
@ -106,10 +108,10 @@ class Deprecation
$callingfile = realpath($backtrace[1]['file']);
$manifest = ClassLoader::instance()->getManifest();
foreach ($manifest->getModules() as $name => $path) {
if (strpos($callingfile, realpath($path)) === 0) {
return $name;
$modules = ModuleLoader::instance()->getManifest()->getModules();
foreach ($modules as $module) {
if (strpos($callingfile, realpath($module->getPath())) === 0) {
return $module;
}
}
return null;
@ -193,8 +195,16 @@ class Deprecation
if (self::$module_version_overrides) {
$module = self::get_calling_module_from_trace($backtrace = debug_backtrace(0));
if (isset(self::$module_version_overrides[$module])) {
$checkVersion = self::$module_version_overrides[$module];
if ($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

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

View File

@ -4,7 +4,7 @@ namespace SilverStripe\i18n\Data;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\Core\Resettable;
use SilverStripe\i18n\i18n;
use SilverStripe\View\SSViewer;
@ -35,7 +35,7 @@ class Sources implements Resettable
public function getSortedModules()
{
// 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);
// 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;
}
$sortedModules = array();
$sortedModulePaths = array();
foreach ($order as $module) {
if (isset($modules[$module])) {
$sortedModules[$module] = $modules[$module];
$sortedModulePaths[$module] = $modules[$module]->getPath();
}
}
$sortedModules = array_reverse($sortedModules, true);
return $sortedModules;
$sortedModulePaths = array_reverse($sortedModulePaths, true);
return $sortedModulePaths;
}
/**

View File

@ -5,6 +5,8 @@ namespace SilverStripe\i18n\TextCollection;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Manifest\Module;
use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\Dev\Debug;
use SilverStripe\Control\Director;
use ReflectionClass;
@ -172,7 +174,7 @@ class i18nTextCollector
}
// Write each module language file
foreach ($entitiesByModule as $module => $entities) {
foreach ($entitiesByModule as $moduleName => $entities) {
// Skip empty translations
if (empty($entities)) {
continue;
@ -180,42 +182,11 @@ class i18nTextCollector
// Clean sorting prior to writing
ksort($entities);
$path = $this->baseSavePath . '/' . $module;
$this->getWriter()->write($entities, $this->defaultLocale, $path);
$module = ModuleLoader::instance()->getManifest()->getModule($moduleName);
$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
*
@ -237,7 +208,13 @@ class i18nTextCollector
// Restrict modules we update to just the specified ones (if any passed)
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]);
}
}
@ -350,9 +327,12 @@ class i18nTextCollector
protected function findModuleForClass($class)
{
if (ClassInfo::exists($class)) {
return ClassLoader::instance()
$module = ClassLoader::instance()
->getManifest()
->getOwnerModule($class);
if ($module) {
return $module->getName();
}
}
// 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
$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));
if (count($modules) === 1) {
@ -413,24 +394,32 @@ class i18nTextCollector
{
// A master string tables array (one mst per module)
$entitiesByModule = array();
$modules = $this->getModules($this->basePath);
$modules = ModuleLoader::instance()->getManifest()->getModules();
foreach ($modules as $module) {
// we store the master string tables
$processedEntities = $this->processModule($module);
if (isset($entitiesByModule[$module])) {
$entitiesByModule[$module] = array_merge_recursive($entitiesByModule[$module], $processedEntities);
$moduleName = $module->getName();
if (isset($entitiesByModule[$moduleName])) {
$entitiesByModule[$moduleName] = array_merge_recursive(
$entitiesByModule[$moduleName],
$processedEntities
);
} else {
$entitiesByModule[$module] = $processedEntities;
$entitiesByModule[$moduleName] = $processedEntities;
}
// Extract all entities for "foreign" modules ('module' key in array form)
// @see CMSMenu::provideI18nEntities for an example usage
foreach ($entitiesByModule[$module] as $fullName => $spec) {
$specModule = $module;
foreach ($entitiesByModule[$moduleName] as $fullName => $spec) {
$specModuleName = $moduleName;
// Rewrite spec if module is specified
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']);
// If only element is defalt, simplify
@ -440,24 +429,34 @@ class i18nTextCollector
}
// Remove from source module
if ($specModule !== $module) {
unset($entitiesByModule[$module][$fullName]);
if ($specModuleName !== $moduleName) {
unset($entitiesByModule[$moduleName][$fullName]);
}
// Write to target module
if (!isset($entitiesByModule[$specModule])) {
$entitiesByModule[$specModule] = [];
if (!isset($entitiesByModule[$specModuleName])) {
$entitiesByModule[$specModuleName] = [];
}
$entitiesByModule[$specModule][$fullName] = $spec;
$entitiesByModule[$specModuleName][$fullName] = $spec;
}
}
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;
}
@ -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
* @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
*/
protected function processModule($module)
protected function processModule(Module $module)
{
$entities = array();
@ -481,15 +480,14 @@ class i18nTextCollector
if ($extension === 'php') {
$entities = array_merge(
$entities,
$this->collectFromCode($content, $module),
$this->collectFromCode($content, $filePath, $module),
$this->collectFromEntityProviders($filePath, $module)
);
} elseif ($extension === 'ss') {
// templates use their filename as a namespace
$namespace = basename($filePath);
$entities = array_merge(
$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
*
* @param string $module
* @param Module $module Module instance
* @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
if (stripos($module, 'themes/') === 0) {
if (stripos($module->getRelativePath(), 'themes/') === 0) {
return $this->getFilesRecursive($modulePath, null, 'ss');
}
// If Framework or non-standard module structure, so we'll scan all subfolders
if ($module === FRAMEWORK_DIR || !is_dir("{$modulePath}/code")) {
// If non-standard module structure, search all root files
if (!is_dir("{$modulePath}/code") && !is_dir("{$modulePath}/src")) {
return $this->getFilesRecursive($modulePath);
}
// 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
if (is_dir("{$modulePath}/templates")) {
@ -538,12 +540,15 @@ class i18nTextCollector
* Note: Translations without default values are omitted.
*
* @param string $content The text content of a parsed template-file
* @param string $module Module's name or 'themes'. Could also be a namespace
* Generated by templates includes. E.g. 'UploadField.ss'
* @param string $fileName Filename Optional filename
* @param Module $module Module being collected
* @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();
$tokens = token_get_all("<?php\n" . $content);
@ -713,7 +718,7 @@ class i18nTextCollector
// Normalise all keys
foreach ($entities as $key => $entity) {
unset($entities[$key]);
$entities[$this->normalizeEntity($key, $module)] = $entity;
$entities[$this->normalizeEntity($key, $namespace)] = $entity;
}
ksort($entities);
@ -725,12 +730,15 @@ class i18nTextCollector
*
* @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 $module Module's name or 'themes'
* @param Module $module Module being collected
* @param array $parsedFiles
* @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
$entities = Parser::getTranslatables($content, $this->getWarnOnEmptyDefault());
@ -738,13 +746,13 @@ class i18nTextCollector
// Collect in actual template
if (preg_match_all('/(_t\([^\)]*?\))/ms', $content, $matches)) {
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) {
unset($entities[$entity]);
$entities[$this->normalizeEntity($entity, $module)] = $spec;
$entities[$this->normalizeEntity($entity, $namespace)] = $spec;
}
ksort($entities);
@ -760,10 +768,10 @@ class i18nTextCollector
*
* @uses i18nEntityProvider
* @param string $filePath
* @param string $module
* @param Module $module
* @return array
*/
public function collectFromEntityProviders($filePath, $module = null)
public function collectFromEntityProviders($filePath, Module $module = null)
{
$entities = array();
$classes = ClassInfo::classes_for_file($filePath);

View File

@ -12,8 +12,25 @@ use SilverStripe\Dev\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()
{
@ -21,8 +38,8 @@ class ClassLoaderTest extends SapphireTest
$this->baseManifest1 = dirname(__FILE__) . '/fixtures/classmanifest';
$this->baseManifest2 = dirname(__FILE__) . '/fixtures/classmanifest_other';
$this->testManifest1 = new ClassManifest($this->baseManifest1, false, true, false);
$this->testManifest2 = new ClassManifest($this->baseManifest2, false, true, false);
$this->testManifest1 = new ClassManifest($this->baseManifest1, false);
$this->testManifest2 = new ClassManifest($this->baseManifest2, false);
}
public function testExclusive()

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Core\Tests\Manifest;
use Exception;
use SilverStripe\Core\Manifest\ClassManifest;
use SilverStripe\Dev\SapphireTest;
@ -11,8 +12,19 @@ use SilverStripe\Dev\SapphireTest;
class ClassManifestTest extends SapphireTest
{
/**
* @var string
*/
protected $base;
/**
* @var ClassManifest
*/
protected $manifest;
/**
* @var ClassManifest
*/
protected $manifestTests;
public function setUp()
@ -20,8 +32,8 @@ class ClassManifestTest extends SapphireTest
parent::setUp();
$this->base = dirname(__FILE__) . '/fixtures/classmanifest';
$this->manifest = new ClassManifest($this->base, false, true, false);
$this->manifestTests = new ClassManifest($this->base, true, true, false);
$this->manifest = new ClassManifest($this->base, false);
$this->manifestTests = new ClassManifest($this->base, true);
}
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()
{
$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
* which contain classes with the same name
*
* @expectedException Exception
*/
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
{
/**
* @var string
*/
protected $base;
/**
@ -26,7 +28,7 @@ class NamespacedClassManifestTest extends SapphireTest
parent::setUp();
$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);
}
@ -40,7 +42,7 @@ class NamespacedClassManifestTest extends SapphireTest
{
$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
// including all core classes
$method = new ReflectionMethod($this->manifest, 'coalesceDescendants');
@ -149,19 +151,4 @@ class NamespacedClassManifestTest extends SapphireTest
$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
{
/**
* @var string
*/
private $base;
/**
@ -34,7 +36,7 @@ class ThemeResourceLoaderTest extends SapphireTest
// Fake project root
$this->base = dirname(__FILE__) . '/fixtures/templatemanifest';
// 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
$this->loader = new ThemeResourceLoader($this->base);
$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()
{
$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'));
}
}

View File

@ -6,6 +6,8 @@ use SilverStripe\Control\Director;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Manifest\ClassManifest;
use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\Core\Manifest\ModuleManifest;
use SilverStripe\i18n\i18n;
use SilverStripe\i18n\Messages\MessageProvider;
use SilverStripe\i18n\Messages\Symfony\ModuleYamlLoader;
@ -41,6 +43,13 @@ trait i18nTestManifest
*/
protected $manifests = 0;
/**
* Number of module manifests
*
* @var int
*/
protected $moduleManifests = 0;
protected function getExtraDataObjects()
{
return [
@ -72,17 +81,16 @@ trait i18nTestManifest
$this->alternateBasePath = __DIR__ . $s . 'i18nTest' . $s . "_fakewebroot";
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
$this->oldThemeResourceLoader = ThemeResourceLoader::instance();
ThemeResourceLoader::set_instance($loader = new ThemeResourceLoader($this->alternateBasePath));
$loader->addSet(
'$default',
new ThemeManifest(
$this->alternateBasePath,
project(),
false,
true
)
new ThemeManifest($this->alternateBasePath, project(), false)
);
SSViewer::set_themes([
@ -94,7 +102,7 @@ trait i18nTestManifest
i18n::set_locale('en_US');
// Set new manifest against the root
$classManifest = new ClassManifest($this->alternateBasePath, true, true, false);
$classManifest = new ClassManifest($this->alternateBasePath, true);
$this->pushManifest($classManifest);
// Setup uncached translator
@ -131,6 +139,12 @@ trait i18nTestManifest
ClassLoader::instance()->pushManifest($manifest);
}
protected function pushModuleManifest(ModuleManifest $manifest)
{
$this->moduleManifests++;
ModuleLoader::instance()->pushManifest($manifest);
}
/**
* Pop off all extra manifests
*/
@ -141,5 +155,9 @@ trait i18nTestManifest
ClassLoader::instance()->popManifest();
$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 SilverStripe\Assets\Filesystem;
use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\i18n\i18n;
use SilverStripe\i18n\TextCollection\i18nTextCollector;
use SilverStripe\i18n\Messages\YamlWriter;
use SilverStripe\i18n\Tests\i18nTextCollectorTest\Collector;
use SilverStripe\View\SSViewer;
class i18nTextCollectorTest extends SapphireTest
{
@ -42,6 +42,7 @@ class i18nTextCollectorTest extends SapphireTest
public function testConcatenationInEntityValues()
{
$c = i18nTextCollector::create();
$module = ModuleLoader::instance()->getManifest()->getModule('i18ntestmodule');
$php = <<<PHP
_t(
@ -66,7 +67,7 @@ PHP;
],
'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->setWarnOnEmptyDefault(false);
$mymodule = ModuleLoader::instance()->getManifest()->getModule('i18ntestmodule');
$html = <<<SS
<% _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_9 "An item|{count} items" is "Test Pluralisation" count=4 %>
SS;
$c->collectFromTemplate($html, 'mymodule', 'Test');
$c->collectFromTemplate($html, null, $mymodule);
$this->assertEquals(
[
@ -103,7 +105,7 @@ SS;
'comment' => 'Test Pluralisation'
],
],
$c->collectFromTemplate($html, 'mymodule', 'Test')
$c->collectFromTemplate($html, null, $mymodule)
);
// Test warning is raised on empty default
@ -112,19 +114,20 @@ SS;
PHPUnit_Framework_Error_Notice::class,
'Missing localisation default for key i18nTestModule.INJECTIONS_3'
);
$c->collectFromTemplate($html, 'mymodule', 'Test');
$c->collectFromTemplate($html, null, $mymodule);
}
public function testCollectFromTemplateSimple()
{
$c = i18nTextCollector::create();
$mymodule = ModuleLoader::instance()->getManifest()->getModule('i18ntestmodule');
$html = <<<SS
<% _t('Test.SINGLEQUOTE','Single Quote'); %>
SS;
$this->assertEquals(
[ 'Test.SINGLEQUOTE' => 'Single Quote' ],
$c->collectFromTemplate($html, 'mymodule', 'Test')
$c->collectFromTemplate($html, null, $mymodule)
);
$html = <<<SS
@ -132,7 +135,7 @@ SS;
SS;
$this->assertEquals(
[ 'Test.DOUBLEQUOTE' => "Double Quote and Spaces" ],
$c->collectFromTemplate($html, 'mymodule', 'Test')
$c->collectFromTemplate($html, null, $mymodule)
);
$html = <<<SS
@ -140,7 +143,7 @@ SS;
SS;
$this->assertEquals(
[ 'Test.NOSEMICOLON' => "No Semicolon" ],
$c->collectFromTemplate($html, 'mymodule', 'Test')
$c->collectFromTemplate($html, null, $mymodule)
);
}
@ -148,6 +151,7 @@ SS;
{
$c = i18nTextCollector::create();
$c->setWarnOnEmptyDefault(false);
$mymodule = ModuleLoader::instance()->getManifest()->getModule('i18ntestmodule');
$html = <<<SS
<% _t(
@ -157,7 +161,7 @@ SS;
SS;
$this->assertEquals(
[ 'Test.NEWLINES' => "New Lines" ],
$c->collectFromTemplate($html, 'mymodule', 'Test')
$c->collectFromTemplate($html, 'Test', $mymodule)
);
$html = <<<SS
@ -172,7 +176,7 @@ SS;
'default' => ' Prio and Value with "Double Quotes"',
'comment' => 'Comment with "Double Quotes"',
]],
$c->collectFromTemplate($html, 'mymodule', 'Test')
$c->collectFromTemplate($html, 'Test', $mymodule)
);
$html = <<<SS
@ -188,7 +192,7 @@ SS;
'default' => " Prio and Value with 'Single Quotes'",
'comment' => "Comment with 'Single Quotes'",
]],
$c->collectFromTemplate($html, 'mymodule', 'Test')
$c->collectFromTemplate($html, 'Test', $mymodule)
);
// Test empty
@ -197,7 +201,7 @@ SS;
SS;
$this->assertEquals(
[],
$c->collectFromTemplate($html, 'mymodule', 'Test')
$c->collectFromTemplate($html, null, $mymodule)
);
// Test warning is raised on empty default
@ -206,20 +210,21 @@ SS;
PHPUnit_Framework_Error_Notice::class,
'Missing localisation default for key Test.PRIOANDCOMMENT'
);
$c->collectFromTemplate($html, 'mymodule', 'Test');
$c->collectFromTemplate($html, 'Test', $mymodule);
}
public function testCollectFromCodeSimple()
{
$c = i18nTextCollector::create();
$mymodule = ModuleLoader::instance()->getManifest()->getModule('i18ntestmodule');
$php = <<<PHP
_t('Test.SINGLEQUOTE','Single Quote');
PHP;
$this->assertEquals(
[ 'Test.SINGLEQUOTE' => 'Single Quote' ],
$c->collectFromCode($php, 'mymodule')
$c->collectFromCode($php, null, $mymodule)
);
$php = <<<PHP
@ -227,13 +232,14 @@ _t( "Test.DOUBLEQUOTE", "Double Quote and Spaces" );
PHP;
$this->assertEquals(
[ 'Test.DOUBLEQUOTE' => "Double Quote and Spaces" ],
$c->collectFromCode($php, 'mymodule')
$c->collectFromCode($php, null, $mymodule)
);
}
public function testCollectFromCodeAdvanced()
{
$c = i18nTextCollector::create();
$mymodule = ModuleLoader::instance()->getManifest()->getModule('i18ntestmodule');
$php = <<<PHP
_t(
@ -243,7 +249,7 @@ _t(
PHP;
$this->assertEquals(
[ 'Test.NEWLINES' => "New Lines" ],
$c->collectFromCode($php, 'mymodule')
$c->collectFromCode($php, null, $mymodule)
);
$php = <<<PHP
@ -261,7 +267,7 @@ PHP;
'comment' => 'Comment with "Double Quotes"',
]
],
$c->collectFromCode($php, 'mymodule')
$c->collectFromCode($php, null, $mymodule)
);
$php = <<<PHP
@ -277,7 +283,7 @@ PHP;
'default' => " Value with 'Single Quotes'",
'comment' => "Comment with 'Single Quotes'"
] ],
$c->collectFromCode($php, 'mymodule')
$c->collectFromCode($php, null, $mymodule)
);
$php = <<<PHP
@ -288,7 +294,7 @@ _t(
PHP;
$this->assertEquals(
[ 'Test.PRIOANDCOMMENT' => "Value with 'Escaped Single Quotes'" ],
$c->collectFromCode($php, 'mymodule')
$c->collectFromCode($php, null, $mymodule)
);
$php = <<<PHP
@ -301,14 +307,14 @@ _t(
PHP;
$this->assertEquals(
[ 'Test.PRIOANDCOMMENT' => "Doublequoted Value with 'Unescaped Single Quotes'"],
$c->collectFromCode($php, 'mymodule')
$c->collectFromCode($php, null, $mymodule)
);
}
public function testCollectFromCodeNamespace()
{
$c = i18nTextCollector::create();
$mymodule = ModuleLoader::instance()->getManifest()->getModule('i18ntestmodule');
$php = <<<PHP
<?php
namespace SilverStripe\Framework\Core;
@ -324,7 +330,7 @@ class MyClass extends Base implements SomeService {
PHP;
$this->assertEquals(
[ 'SilverStripe\\Framework\\Core\\MyClass.NEWLINES' => "New Lines" ],
$c->collectFromCode($php, 'mymodule')
$c->collectFromCode($php, null, $mymodule)
);
}
@ -332,6 +338,7 @@ PHP;
public function testNewlinesInEntityValues()
{
$c = i18nTextCollector::create();
$mymodule = ModuleLoader::instance()->getManifest()->getModule('i18ntestmodule');
$php = <<<PHP
_t(
@ -344,7 +351,7 @@ PHP;
$eol = PHP_EOL;
$this->assertEquals(
[ 'Test.NEWLINESINGLEQUOTE' => "Line 1{$eol}Line 2" ],
$c->collectFromCode($php, 'mymodule')
$c->collectFromCode($php, null, $mymodule)
);
$php = <<<PHP
@ -356,7 +363,7 @@ Line 2"
PHP;
$this->assertEquals(
[ '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->setWarnOnEmptyDefault(false); // Disable warnings for tests
$mymodule = ModuleLoader::instance()->getManifest()->getModule('i18ntestmodule');
$php = <<<PHP
_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");
PHP;
$collectedTranslatables = $c->collectFromCode($php, 'mymodule');
$collectedTranslatables = $c->collectFromCode($php, null, $mymodule);
$expectedArray = [
'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"));
PHP;
$c->setWarnOnEmptyDefault(true);
$c->collectFromCode($php, 'mymodule');
$c->collectFromCode($php, null, $mymodule);
}
public function testUncollectableCode()
{
$c = i18nTextCollector::create();
$mymodule = ModuleLoader::instance()->getManifest()->getModule('i18ntestmodule');
$php = <<<PHP
_t(static::class.'.KEY1', 'Default');
@ -430,7 +439,7 @@ _t(__CLASS__.'.KEY3', 'Default');
_t('Collectable.KEY4', 'Default');
PHP;
$collectedTranslatables = $c->collectFromCode($php, 'mymodule');
$collectedTranslatables = $c->collectFromCode($php, null, $mymodule);
// Only one item is collectable
$expectedArray = [ 'Collectable.KEY4' => 'Default' ];
@ -441,20 +450,21 @@ PHP;
{
$c = i18nTextCollector::create();
$c->setWarnOnEmptyDefault(false); // Disable warnings for tests
$mymodule = ModuleLoader::instance()->getManifest()->getModule('i18ntestmodule');
$templateFilePath = $this->alternateBasePath . '/i18ntestmodule/templates/Layout/i18nTestModule.ss';
$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(
'Layout Template no namespace',
$matches['RandomNamespace.LAYOUTTEMPLATENONAMESPACE']
$matches['i18nTestModule.ss.LAYOUTTEMPLATENONAMESPACE']
);
$this->assertArrayHasKey('RandomNamespace.SPRINTFNONAMESPACE', $matches);
$this->assertArrayHasKey('i18nTestModule.ss.SPRINTFNONAMESPACE', $matches);
$this->assertEquals(
'My replacement no namespace: %s',
$matches['RandomNamespace.SPRINTFNONAMESPACE']
$matches['i18nTestModule.ss.SPRINTFNONAMESPACE']
);
$this->assertArrayHasKey('i18nTestModule.LAYOUTTEMPLATE', $matches);
$this->assertEquals(
@ -474,44 +484,6 @@ PHP;
$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()
{
$c = i18nTextCollector::create();
@ -608,63 +580,6 @@ PHP;
" MAINTEMPLATE: 'Main Template Other Module'\n",
$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()
@ -793,16 +708,14 @@ PHP;
public function testModuleDetection()
{
$collector = new Collector();
$modules = $collector->getModules_Test($this->alternateBasePath);
$modules = ModuleLoader::instance()->getManifest()->getModules();
$this->assertEquals(
array(
'i18nnonstandardmodule',
'i18nothermodule',
'i18ntestmodule',
'themes/testtheme1',
'themes/testtheme2'
'i18nothermodule'
),
$modules
array_keys($modules)
);
$this->assertEquals('i18ntestmodule', $collector->findModuleForClass_Test('i18nTestNamespacedClass'));
@ -853,22 +766,5 @@ PHP;
$this->assertArrayHasKey("{$otherRoot}/code/i18nProviderClass.php", $otherFiles);
$this->assertArrayHasKey("{$otherRoot}/code/i18nTestModuleDecorator.php", $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;
use SilverStripe\Core\Manifest\Module;
use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\Dev\TestOnly;
use SilverStripe\i18n\TextCollection\i18nTextCollector;
@ -10,18 +12,17 @@ use SilverStripe\i18n\TextCollection\i18nTextCollector;
*/
class Collector extends i18nTextCollector implements TestOnly
{
public function getModules_Test($directory)
{
return $this->getModules($directory);
}
public function resolveDuplicateConflicts_Test($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);
}