diff --git a/core/lib/Drupal/Component/Discovery/YamlDiscovery.php b/core/lib/Drupal/Component/Discovery/YamlDiscovery.php index 1d147fd08ba1ff29acc98a36e35507f24e874b04..002327de8b5d972502ecac6121b1cb13f2beac80 100644 --- a/core/lib/Drupal/Component/Discovery/YamlDiscovery.php +++ b/core/lib/Drupal/Component/Discovery/YamlDiscovery.php @@ -8,6 +8,7 @@ namespace Drupal\Component\Discovery; use Drupal\Component\Serialization\Yaml; +use Drupal\Component\FileCache\FileCacheFactory; /** * Provides discovery for YAML files within a given set of directories. @@ -47,10 +48,27 @@ public function __construct($name, array $directories) { */ public function findAll() { $all = array(); - foreach ($this->findFiles() as $provider => $file) { - // If a file is empty or its contents are commented out, return an empty - // array instead of NULL for type consistency. - $all[$provider] = Yaml::decode(file_get_contents($file)) ?: []; + + $files = $this->findFiles(); + $provider_by_files = array_flip($files); + + $file_cache = FileCacheFactory::get('yaml_discovery:' . $this->name); + + // Try to load from the file cache first. + foreach ($file_cache->getMultiple($files) as $file => $data) { + $all[$provider_by_files[$file]] = $data; + unset($provider_by_files[$file]); + } + + // If there are files left that were not returned from the cache, load and + // parse them now. This list was flipped above and is keyed by filename. + if ($provider_by_files) { + foreach ($provider_by_files as $file => $provider) { + // If a file is empty or its contents are commented out, return an empty + // array instead of NULL for type consistency. + $all[$provider] = Yaml::decode(file_get_contents($file)) ?: []; + $file_cache->set($file, $all[$provider]); + } } return $all; diff --git a/core/lib/Drupal/Component/FileCache/ApcuFileCacheBackend.php b/core/lib/Drupal/Component/FileCache/ApcuFileCacheBackend.php new file mode 100644 index 0000000000000000000000000000000000000000..25b6097504ddffde554fc17e73634b03d89327e0 --- /dev/null +++ b/core/lib/Drupal/Component/FileCache/ApcuFileCacheBackend.php @@ -0,0 +1,36 @@ +<?php + +/** + * @file + * Contains \Drupal\Component\FileCache\ApcuFileCacheBackend. + */ + +namespace Drupal\Component\FileCache; + +/** + * APCu backend for the file cache. + */ +class ApcuFileCacheBackend implements FileCacheBackendInterface { + + /** + * {@inheritdoc} + */ + public function fetch(array $cids) { + return apc_fetch($cids); + } + + /** + * {@inheritdoc} + */ + public function store($cid, $data) { + apc_store($cid, $data); + } + + /** + * {@inheritdoc} + */ + public function delete($cid) { + apc_delete($cid); + } + +} diff --git a/core/lib/Drupal/Component/FileCache/FileCache.php b/core/lib/Drupal/Component/FileCache/FileCache.php new file mode 100644 index 0000000000000000000000000000000000000000..761d490f99699e820649917c2a10ca8bd2c0a17e --- /dev/null +++ b/core/lib/Drupal/Component/FileCache/FileCache.php @@ -0,0 +1,156 @@ +<?php + +/** + * @file + * Contains \Drupal\Component\FileCache\FileCache. + */ + +namespace Drupal\Component\FileCache; + +/** + * Allows to cache data based on file modification dates. + */ +class FileCache implements FileCacheInterface { + + /** + * Prefix that is used for cache entries. + * + * @var string + */ + protected $prefix; + + /** + * Static cache that contains already loaded cache entries. + * + * @var array + */ + protected static $cached = []; + + /** + * The collection identifier of this cache. + * + * @var string + */ + protected $collection; + + /** + * The cache backend backing this FileCache object. + * + * @var \Drupal\Component\FileCache\FileCacheBackendInterface + */ + protected $cache; + + /** + * Constructs a FileCache object. + * + * @param string $prefix + * The cache prefix. + * @param string $collection + * A collection identifier to ensure that the same files could be cached for + * different purposes without clashing. + * @param string|null $cache_backend_class + * (optional) The class that should be used as cache backend. + * @param array $cache_backend_configuration + * (optional) The configuration for the backend class. + */ + public function __construct($prefix, $collection, $cache_backend_class = NULL, array $cache_backend_configuration = []) { + + if (empty($prefix)) { + throw new \InvalidArgumentException('Required prefix configuration is missing'); + } + + $this->prefix = $prefix; + $this->collection = $collection; + + if (isset($cache_backend_class)) { + $this->cache = new $cache_backend_class($cache_backend_configuration); + } + } + + /** + * {@inheritdoc} + */ + public function get($filepath) { + $filepaths = [$filepath]; + $cached = $this->getMultiple($filepaths); + return isset($cached[$filepath]) ? $cached[$filepath] : NULL; + } + + /** + * {@inheritdoc} + */ + public function getMultiple(array $filepaths) { + $file_data = []; + $remaining_cids = []; + + // First load from the static cache what we can. + foreach ($filepaths as $filepath) { + if (!file_exists($filepath)) { + continue; + } + + $realpath = realpath($filepath); + // If the file exists but realpath returns nothing, it is using a stream + // wrapper, those are not supported. + if (empty($realpath)) { + continue; + } + + $cid = $this->prefix . ':' . $this->collection . ':' . $realpath; + if (isset(static::$cached[$cid]) && static::$cached[$cid]['mtime'] == filemtime($filepath)) { + $file_data[$filepath] = static::$cached[$cid]['data']; + } + else { + // Collect a list of cache IDs that we still need to fetch from cache + // backend. + $remaining_cids[$cid] = $filepath; + } + } + + // If there are any cache IDs left to fetch from the cache backend. + if ($remaining_cids && $this->cache) { + $cache_results = $this->cache->fetch(array_keys($remaining_cids)) ?: []; + foreach ($cache_results as $cid => $cached) { + $filepath = $remaining_cids[$cid]; + if ($cached['mtime'] == filemtime($filepath)) { + $file_data[$cached['filepath']] = $cached['data']; + static::$cached[$cid] = $cached; + } + } + } + + return $file_data; + } + + /** + * {@inheritdoc} + */ + public function set($filepath, $data) { + $realpath = realpath($filepath); + $cached = [ + 'mtime' => filemtime($filepath), + 'filepath' => $filepath, + 'data' => $data, + ]; + + $cid = $this->prefix . ':' . $this->collection . ':' . $realpath; + static::$cached[$cid] = $cached; + if ($this->cache) { + $this->cache->store($cid, $cached); + } + } + + /** + * {@inheritdoc} + */ + public function delete($filepath) { + $realpath = realpath($filepath); + $cid = $this->prefix . ':' . $this->collection . ':' . $realpath; + + unset(static::$cached[$cid]); + if ($this->cache) { + $this->cache->delete($cid); + } + } + +} diff --git a/core/lib/Drupal/Component/FileCache/FileCacheBackendInterface.php b/core/lib/Drupal/Component/FileCache/FileCacheBackendInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..ce31615564ced2103d8264b31dd9ce90d29667fc --- /dev/null +++ b/core/lib/Drupal/Component/FileCache/FileCacheBackendInterface.php @@ -0,0 +1,44 @@ +<?php + +/** + * @file + * Contains \Drupal\Component\FileCache\FileCacheBackendInterface. + */ + +namespace Drupal\Component\FileCache; + +/** + * Defines an interface inspired by APCu for FileCache backends. + */ +interface FileCacheBackendInterface { + + /** + * Fetches data from the cache backend. + * + * @param array $cids + * The cache IDs to fetch. + * + * @return array + * An array containing cache entries keyed by cache ID. + */ + public function fetch(array $cids); + + /** + * Stores data into a cache backend. + * + * @param string $cid + * The cache ID to store data to. + * @param mixed $data + * The data to store. + */ + public function store($cid, $data); + + /** + * Deletes data from a cache backend. + * + * @param string $cid + * The cache ID to delete. + */ + public function delete($cid); + +} diff --git a/core/lib/Drupal/Component/FileCache/FileCacheFactory.php b/core/lib/Drupal/Component/FileCache/FileCacheFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..df391c0183ff9937e6f43520903350204b611a1b --- /dev/null +++ b/core/lib/Drupal/Component/FileCache/FileCacheFactory.php @@ -0,0 +1,107 @@ +<?php + +/** + * @file + * Contains Drupal\Component\FileCache\FileCacheFactory. + */ + +namespace Drupal\Component\FileCache; + +/** + * Creates a FileCache object. + */ +class FileCacheFactory { + + /** + * The configuration used to create FileCache objects. + * + * @var array $configuration + */ + protected static $configuration; + + /** + * The cache prefix. + * + * @var string + */ + protected static $prefix; + + /** + * Instantiates a FileCache object for a given collection identifier. + * + * @param string $collection + * The collection identifier for this FileCache. + * @param array $default_configuration + * (optional) The default configuration for this FileCache collection. This + * can be used to e.g. specify default usage of a FileCache class. + * + * @return \Drupal\Component\FileCache\FileCacheInterface + * The initialized FileCache object. + */ + public static function get($collection, $default_configuration = []) { + $default_configuration += [ + 'class' => '\Drupal\Component\FileCache\FileCache', + 'collection' => $collection, + 'cache_backend_class' => NULL, + 'cache_backend_configuration' => [], + ]; + + $configuration = []; + if (isset(static::$configuration[$collection])) { + $configuration = static::$configuration[$collection]; + } + elseif (isset(static::$configuration['default'])) { + $configuration = static::$configuration['default']; + } + + // Add defaults to the configuration. + $configuration = $configuration + $default_configuration; + + $class = $configuration['class']; + return new $class(static::getPrefix(), $configuration['collection'], $configuration['cache_backend_class'], $configuration['cache_backend_configuration']); + } + + /** + * Gets the configuration used for constructing future file cache objects. + * + * @return array + * The configuration that is used. + */ + public static function getConfiguration() { + return static::$configuration; + } + + /** + * Sets the configuration to use for constructing future file cache objects. + * + * @param array $configuration + * The configuration to use. + */ + public static function setConfiguration($configuration) { + static::$configuration = $configuration; + } + + /** + * Returns the cache prefix. + * + * @return string + * The cache prefix. + */ + public static function getPrefix() { + return static::$prefix; + } + + /** + * Sets the cache prefix that should be used. + * + * Should be set to a secure, unique key to prevent cache pollution by a + * third party. + * + * @param string $prefix + * The cache prefix. + */ + public static function setPrefix($prefix) { + static::$prefix = $prefix; + } + +} diff --git a/core/lib/Drupal/Component/FileCache/FileCacheInterface.php b/core/lib/Drupal/Component/FileCache/FileCacheInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..e347783538f76ba372f7df0a2e8bec0eb9b04ffb --- /dev/null +++ b/core/lib/Drupal/Component/FileCache/FileCacheInterface.php @@ -0,0 +1,64 @@ +<?php + +/** + * @file + * Contains \Drupal\Component\FileCache\FileCacheInterface. + */ + +namespace Drupal\Component\FileCache; + +/** + * Interface for objects that allow caching file data. + * + * Parsing YAML, annotations or similar data out of files can be a + * time-consuming process, especially since those files usually don't change + * and identical data is parsed over and over again. + * + * File cache is a self-contained caching layer for such processing, that relies + * on the file modification to ensure that cached data is still up to date and + * does not need to be invalidated externally. + */ +interface FileCacheInterface { + + /** + * Gets data based on a filename. + * + * @param string $filepath + * Path of the file that the cached data is based on. + * + * @return mixed|null + * The data that was persisted with set() or NULL if there is no data + * or the file has been modified. + */ + public function get($filepath); + + /** + * Gets data based on filenames. + * + * @param string[] $filepaths + * List of file paths used as cache identifiers. + * + * @return array + * List of cached data keyed by the passed in file paths. + */ + public function getMultiple(array $filepaths); + + /** + * Stores data based on a filename. + * + * @param string $filepath + * Path of the file that the cached data is based on. + * @param mixed $data + * The data that should be cached. + */ + public function set($filepath, $data); + + /** + * Deletes data from the cache. + * + * @param string $filepath + * Path of the file that the cached data is based on. + */ + public function delete($filepath); + +} diff --git a/core/lib/Drupal/Component/FileCache/NullFileCache.php b/core/lib/Drupal/Component/FileCache/NullFileCache.php new file mode 100644 index 0000000000000000000000000000000000000000..84c64ccbef69c6538fb04d14f16e5a1a1a39efe9 --- /dev/null +++ b/core/lib/Drupal/Component/FileCache/NullFileCache.php @@ -0,0 +1,58 @@ +<?php + +/** + * @file + * Contains \Drupal\Component\FileCache\NullFileCache. + */ + +namespace Drupal\Component\FileCache; + +/** + * Null implementation for the file cache. + */ +class NullFileCache implements FileCacheInterface { + + /** + * Constructs a FileCache object. + * + * @param string $prefix + * A prefix that is used as a prefix, should be set to a secure, unique key + * to prevent cache pollution by a third party. + * @param string $collection + * A collection identifier to ensure that the same files could be cached for + * different purposes without clashing. + * @param string|null $cache_backend_class + * (optional) The class that should be used as cache backend. + * @param array $cache_backend_configuration + * (optional) The configuration for the backend class. + */ + public function __construct($prefix, $collection, $cache_backend_class = NULL, array $cache_backend_configuration = []) { + } + + /** + * {@inheritdoc} + */ + public function get($filepath) { + return NULL; + } + + /** + * {@inheritdoc} + */ + public function getMultiple(array $filepaths) { + return []; + } + + /** + * {@inheritdoc} + */ + public function set($filepath, $data) { + } + + /** + * {@inheritdoc} + */ + public function delete($filepath) { + } + +} diff --git a/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php b/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php index d4ae53e2d0bf215d6182a2a40216a4cf9b59e8b6..77484de60f2dfe913f046358858e1fa2aeb81d8a 100644 --- a/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php +++ b/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php @@ -7,6 +7,7 @@ namespace Drupal\Core\DependencyInjection; +use Drupal\Component\FileCache\FileCacheFactory; use Drupal\Component\Serialization\Yaml; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -34,30 +35,22 @@ class YamlFileLoader { /** - * Statically cached yaml files. - * - * Especially during tests, YAML files are re-parsed often. - * - * @var array + * @var \Drupal\Core\DependencyInjection\ContainerBuilder $container */ - static protected $yaml = array(); + protected $container; /** - * @param \Drupal\Core\DependencyInjection\ContainerBuilder $container + * File cache object. + * + * @var \Drupal\Component\FileCache\FileCacheInterface */ - protected $container; + protected $fileCache; - public function __construct(ContainerBuilder $container) - { - $this->container = $container; - } - /** - * Resets the internal cache. This method is mostly useful for tests. - */ - public static function reset() + public function __construct(ContainerBuilder $container) { - static::$yaml = array(); + $this->container = $container; + $this->fileCache = FileCacheFactory::get('container_yaml_loader'); } /** @@ -67,10 +60,14 @@ public static function reset() */ public function load($file) { - if (!isset(static::$yaml[$file])) { - static::$yaml[$file] = $this->loadFile($file); + // Load from the file cache, fall back to loading the file. + // @todo Refactor this to cache parsed definition objects in + // https://www.drupal.org/node/2464053 + $content = $this->fileCache->get($file); + if (!$content) { + $content = $this->loadFile($file); + $this->fileCache->set($file, $content); } - $content = static::$yaml[$file]; // Not supported. //$this->container->addResource(new FileResource($path)); diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index 859240280e833b015df4bf736602dce921d64c96..e57f9defb97234a9b80bdab6b50f65b44178cba5 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -7,6 +7,7 @@ namespace Drupal\Core; +use Drupal\Component\FileCache\FileCacheFactory; use Drupal\Component\ProxyBuilder\ProxyDumper; use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\Timer; @@ -410,6 +411,28 @@ public function boot() { if (!$this->sitePath) { throw new \Exception('Kernel does not have site path set before calling boot()'); } + + // Initialize the FileCacheFactory component. We have to do it here instead + // of in \Drupal\Component\FileCache\FileCacheFactory because we can not use + // the Settings object in a component. + $configuration = Settings::get('file_cache'); + + // Provide a default configuration, if not set. + if (!isset($configuration['default'])) { + $configuration['default'] = [ + 'class' => '\Drupal\Component\FileCache\FileCache', + 'cache_backend_class' => NULL, + 'cache_backend_configuration' => [], + ]; + // @todo Use extension_loaded('apcu') for non-testbot + // https://www.drupal.org/node/2447753. + if (function_exists('apc_fetch')) { + $configuration['default']['cache_backend_class'] = '\Drupal\Component\FileCache\ApcuFileCacheBackend'; + } + } + FileCacheFactory::setConfiguration($configuration); + FileCacheFactory::setPrefix(hash('sha256', Settings::get('hash_salt'))); + // Initialize the container. $this->initializeContainer(); diff --git a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php index 4681977ddde9568e69ba1c9f28503a4d373d251b..fbd8f1b3e2b14d74d77cae9fcfc32de7c3e9179b 100644 --- a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Extension; +use Drupal\Component\FileCache\FileCacheFactory; use Drupal\Core\Extension\Discovery\RecursiveExtensionFilterIterator; use Drupal\Core\Site\Settings; @@ -80,6 +81,13 @@ class ExtensionDiscovery { */ protected $root; + /** + * The file cache object. + * + * @var \Drupal\Component\FileCache\FileCacheInterface + */ + protected $fileCache; + /** * Constructs a new ExtensionDiscovery object. * @@ -88,6 +96,7 @@ class ExtensionDiscovery { */ public function __construct($root) { $this->root = $root; + $this->fileCache = FileCacheFactory::get('extension_discovery'); } /** @@ -406,6 +415,12 @@ protected function scanDirectory($dir, $include_tests) { if (!preg_match(static::PHP_FUNCTION_PATTERN, $fileinfo->getBasename('.info.yml'))) { continue; } + + if ($cached_extension = $this->fileCache->get($fileinfo->getPathName())) { + $files[$cached_extension->getType()][$key] = $cached_extension; + continue; + } + // Determine extension type from info file. $type = FALSE; $file = $fileinfo->openFile('r'); @@ -441,6 +456,7 @@ protected function scanDirectory($dir, $include_tests) { $extension->origin = $dir; $files[$type][$key] = $extension; + $this->fileCache->set($fileinfo->getPathName(), $extension); } return $files; } diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php index 7b98640845f502538a1052e289a0edd9d8fc9927..9dd2f1ad4f9ca8b48b2e33125534909090c7e90c 100644 --- a/core/modules/simpletest/src/WebTestBase.php +++ b/core/modules/simpletest/src/WebTestBase.php @@ -7,6 +7,7 @@ namespace Drupal\simpletest; +use Drupal\Component\FileCache\FileCacheFactory; use Drupal\Component\Serialization\Json; use Drupal\Component\Serialization\Yaml; use Drupal\Component\Utility\Crypt; @@ -1071,8 +1072,9 @@ protected function setContainerParameter($name, $value) { $services['parameters'][$name] = $value; file_put_contents($filename, Yaml::encode($services)); - // Clear the YML file cache. - YamlFileLoader::reset(); + // Ensure that the cache is deleted for the yaml file loader. + $file_cache = FileCacheFactory::get('container_yaml_loader'); + $file_cache->delete($filename); } /** diff --git a/core/tests/Drupal/Tests/Component/FileCache/FileCacheFactoryTest.php b/core/tests/Drupal/Tests/Component/FileCache/FileCacheFactoryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..206de2c699e153f8f1953fecbad5e9ed4a1426f4 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/FileCache/FileCacheFactoryTest.php @@ -0,0 +1,95 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Component\FileCache\FileCacheFactoryTest. + */ + +namespace Drupal\Tests\Component\FileCache; + +use Drupal\Component\FileCache\FileCacheFactory; +use Drupal\Tests\UnitTestCase; + +/** + * @coversDefaultClass \Drupal\Component\FileCache\FileCacheFactory + * @group FileCache + */ +class FileCacheFactoryTest extends UnitTestCase { + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $settings = [ + 'collection' => 'test-23', + 'cache_backend_class' => '\Drupal\Tests\Component\FileCache\StaticFileCacheBackend', + 'cache_backend_configuration' => [ + 'bin' => 'dog', + ], + ]; + $configuration = FileCacheFactory::getConfiguration(); + if (!$configuration) { + $configuration = []; + } + $configuration += [ 'test_foo_settings' => $settings ]; + FileCacheFactory::setConfiguration($configuration); + FileCacheFactory::setPrefix('prefix'); + } + + /** + * @covers ::get + */ + public function testGet() { + $file_cache = FileCacheFactory::get('test_foo_settings', []); + + // Ensure the right backend and configuration is used. + $filename = __DIR__ . '/Fixtures/llama-23.txt'; + $realpath = realpath($filename); + $cid = 'prefix:test-23:' . $realpath; + + $file_cache->set($filename, 23); + + $static_cache = new StaticFileCacheBackend(['bin' => 'dog']); + $result = $static_cache->fetch([$cid]); + $this->assertNotEmpty($result); + + // Cleanup static caches. + $file_cache->delete($filename); + } + + /** + * @covers ::get + * + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Required prefix configuration is missing + */ + public function testGetNoPrefix() { + FileCacheFactory::setPrefix(NULL); + FileCacheFactory::get('test_foo_settings', []); + } + + /** + * @covers ::getConfiguration + * @covers ::setConfiguration + */ + public function testGetSetConfiguration() { + $configuration = FileCacheFactory::getConfiguration(); + $configuration['test_foo_bar'] = ['bar' => 'llama']; + FileCacheFactory::setConfiguration($configuration); + $configuration = FileCacheFactory::getConfiguration(); + $this->assertEquals(['bar' => 'llama'], $configuration['test_foo_bar']); + } + + /** + * @covers ::getPrefix + * @covers ::setPrefix + */ + public function testGetSetPrefix() { + $prefix = $this->randomMachineName(); + FileCacheFactory::setPrefix($prefix); + $this->assertEquals($prefix, FileCacheFactory::getPrefix()); + } + +} diff --git a/core/tests/Drupal/Tests/Component/FileCache/FileCacheTest.php b/core/tests/Drupal/Tests/Component/FileCache/FileCacheTest.php new file mode 100644 index 0000000000000000000000000000000000000000..9e51798526d945566f5825b315e1ca1259626451 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/FileCache/FileCacheTest.php @@ -0,0 +1,147 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Component\FileCache\FileCacheTest. + */ + +namespace Drupal\Tests\Component\FileCache; + +use Drupal\Component\FileCache\FileCache; +use Drupal\Tests\UnitTestCase; + +/** + * @coversDefaultClass \Drupal\Component\FileCache\FileCache + * @group FileCache + */ +class FileCacheTest extends UnitTestCase { + + /** + * FileCache object used for the tests. + * + * @var \Drupal\Component\FileCache\FileCacheInterface + */ + protected $fileCache; + + /** + * Static FileCache object used for verification of tests. + * + * @var \Drupal\Component\FileCache\FileCacheBackendInterface + */ + protected $staticFileCache; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->fileCache = new FileCache('prefix', 'test', '\Drupal\Tests\Component\FileCache\StaticFileCacheBackend', ['bin' => 'llama']); + $this->staticFileCache = new StaticFileCacheBackend(['bin' => 'llama']); + } + + /** + * @covers ::get + * @covers ::__construct + */ + public function testGet() { + // Test a cache miss. + $result = $this->fileCache->get(__DIR__ . '/Fixtures/no-llama-42.yml'); + $this->assertNull($result); + + // Test a cache hit. + $filename = __DIR__ . '/Fixtures/llama-42.txt'; + $realpath = realpath($filename); + $cid = 'prefix:test:' . $realpath; + $data = [ + 'mtime' => filemtime($realpath), + 'filepath' => $realpath, + 'data' => 42, + ]; + + $this->staticFileCache->store($cid, $data); + + $result = $this->fileCache->get($filename); + $this->assertEquals(42, $result); + + // Cleanup static caches. + $this->fileCache->delete($filename); + } + + /** + * @covers ::getMultiple + */ + public function testGetMultiple() { + // Test a cache miss. + $result = $this->fileCache->getMultiple([__DIR__ . '/Fixtures/no-llama-42.yml']); + $this->assertEmpty($result); + + // Test a cache hit. + $filename = __DIR__ . '/Fixtures/llama-42.txt'; + $realpath = realpath($filename); + $cid = 'prefix:test:' . $realpath; + $data = [ + 'mtime' => filemtime($realpath), + 'filepath' => $realpath, + 'data' => 42, + ]; + + $this->staticFileCache->store($cid, $data); + + $result = $this->fileCache->getMultiple([$filename]); + $this->assertEquals([$filename => 42], $result); + + // Test a static cache hit. + $file2 = __DIR__ . '/Fixtures/llama-23.txt'; + $this->fileCache->set($file2, 23); + + $result = $this->fileCache->getMultiple([$filename, $file2]); + $this->assertEquals([$filename => 42, $file2 => 23], $result); + + // Cleanup static caches. + $this->fileCache->delete($filename); + $this->fileCache->delete($file2); + } + + /** + * @covers ::set + */ + public function testSet() { + $filename = __DIR__ . '/Fixtures/llama-23.txt'; + $realpath = realpath($filename); + $cid = 'prefix:test:' . $realpath; + $data = [ + 'mtime' => filemtime($realpath), + 'filepath' => $realpath, + 'data' => 23, + ]; + + $this->fileCache->set($filename, 23); + $result = $this->staticFileCache->fetch([$cid]); + $this->assertEquals([$cid => $data], $result); + + // Cleanup static caches. + $this->fileCache->delete($filename); + } + + /** + * @covers ::delete + */ + public function testDelete() { + $filename = __DIR__ . '/Fixtures/llama-23.txt'; + $realpath = realpath($filename); + $cid = 'prefix:test:' . $realpath; + + $this->fileCache->set($filename, 23); + + // Ensure data is removed after deletion. + $this->fileCache->delete($filename); + + $result = $this->staticFileCache->fetch([$cid]); + $this->assertEquals([], $result); + + $result = $this->fileCache->get($filename); + $this->assertNull($result); + } + +} diff --git a/core/tests/Drupal/Tests/Component/FileCache/Fixtures/llama-23.txt b/core/tests/Drupal/Tests/Component/FileCache/Fixtures/llama-23.txt new file mode 100644 index 0000000000000000000000000000000000000000..409940768f2a684935a7d15a29f96e82c487f439 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/FileCache/Fixtures/llama-23.txt @@ -0,0 +1 @@ +23 diff --git a/core/tests/Drupal/Tests/Component/FileCache/Fixtures/llama-42.txt b/core/tests/Drupal/Tests/Component/FileCache/Fixtures/llama-42.txt new file mode 100644 index 0000000000000000000000000000000000000000..d81cc0710eb6cf9efd5b920a8453e1e07157b6cd --- /dev/null +++ b/core/tests/Drupal/Tests/Component/FileCache/Fixtures/llama-42.txt @@ -0,0 +1 @@ +42 diff --git a/core/tests/Drupal/Tests/Component/FileCache/StaticFileCacheBackend.php b/core/tests/Drupal/Tests/Component/FileCache/StaticFileCacheBackend.php new file mode 100644 index 0000000000000000000000000000000000000000..ed82b8118132e5f2692360e97556b4fa5cd2b425 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/FileCache/StaticFileCacheBackend.php @@ -0,0 +1,76 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Component\FileCache\StaticFileCacheBackend. + */ + +namespace Drupal\Tests\Component\FileCache; + +use Drupal\Component\FileCache\FileCacheBackendInterface; + +/** + * Allows to cache data based on file modification dates in a static cache. + */ +class StaticFileCacheBackend implements FileCacheBackendInterface { + + /** + * Internal static cache. + * + * @var array + */ + protected static $cache = []; + + /** + * Bin used for storing the data in the static cache. + * + * @var string + */ + protected $bin; + + /** + * Constructs a PHP Storage FileCache backend. + * + * @param array $configuration + * (optional) Configuration used to configure this object. + */ + public function __construct($configuration) { + $this->bin = isset($configuration['bin']) ? $configuration['bin'] : 'file_cache'; + } + + /** + * {@inheritdoc} + */ + public function fetch(array $cids) { + $result = []; + foreach ($cids as $cid) { + if (isset(static::$cache[$this->bin][$cid])) { + $result[$cid] = static::$cache[$this->bin][$cid]; + } + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function store($cid, $data) { + static::$cache[$this->bin][$cid] = $data; + } + + /** + * {@inheritdoc} + */ + public function delete($cid) { + unset(static::$cache[$this->bin][$cid]); + } + + /** + * Allows tests to reset the static cache to avoid side effects. + */ + public static function reset() { + static::$cache = []; + } + +} diff --git a/core/tests/Drupal/Tests/UnitTestCase.php b/core/tests/Drupal/Tests/UnitTestCase.php index c15bff26a3c4ff337912067bad950346d5057f14..9e70a73b9a664ff7e351526068ba6013e3da2581 100644 --- a/core/tests/Drupal/Tests/UnitTestCase.php +++ b/core/tests/Drupal/Tests/UnitTestCase.php @@ -7,6 +7,7 @@ namespace Drupal\Tests; +use Drupal\Component\FileCache\FileCacheFactory; use Drupal\Component\Utility\Random; use Drupal\Core\Cache\CacheTagsInvalidatorInterface; use Drupal\Core\DependencyInjection\ContainerBuilder; @@ -41,6 +42,10 @@ protected function setUp() { // a previous test does not leak into this test. \Drupal::unsetContainer(); + // Ensure that the NullFileCache implementation is used for the FileCache as + // unit tests should not be relying on caches implicitly. + FileCacheFactory::setConfiguration(['default' => ['class' => '\Drupal\Component\FileCache\NullFileCache']]); + $this->root = dirname(dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__)))); }