--- /dev/null
+<?php
+
+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);
+ }
+ }
+
+ /**
+ * Resets the static cache.
+ *
+ * @todo Replace this once https://www.drupal.org/node/2260187 is in.
+ */
+ public static function reset() {
+ static::$cached = [];
+ }
+
+}