Search code examples
phpsymfonycachingsymfony-3.1cache-invalidation

Cache invalidation in Symfony 3.1 cache component


I use the cache component of Symfony 3.1 to store some custom metadata for my entities. I would like to invalidate it as soon as a change is made in any file associated with these metadata.

I didn't find a way to tell Symfony or the cache component in particular to watch for changes in a specific set of files, I am missing something?

Below the code I use to create my cache item pool:

<?php
class MetadataCacheFactory implements MetadataCacheFactoryInterface
{
    const CACHE_NAMESPACE = 'my_namespace';

    /** @var string */
    protected $cacheDir;

    public function __construct(KernelInterface $kernel)
    {
        $this->cacheDir = $kernel->getCacheDir();
    }

    /**
     * {@inheritdoc}
     */
    public function create(): CacheItemPoolInterface
    {
        return AbstractAdapter::createSystemCache(self::CACHE_NAMESPACE, 0, null, $this->cacheDir);
    }
}

And the code using it:

<?php
class ExampleMetadataFactory implements MetadataFactoryInterface
{
    const CACHE_KEY = 'example_metadata';

    [...]

    /** @var ExampleMetadata */
    protected $metadata;

    public function __construct(MetadataCacheFactoryInterface $cacheFactory)
    {
        $this->cache = $cacheFactory->create();
        $this->metadata = null;
    }

    /**
     * {@inheritdoc}
     */
    public function create(): ExampleMetadata
    {
        if ($this->metadata !== null) {
            return $this->metadata;
        }
        try {
            $cacheItem = $this->cache->getItem(md5(self::CACHE_KEY));
            if ($cacheItem->isHit()) {
                return $cacheItem->get();
            }
        } catch (CacheException $e) {
            // Ignore
        }
        $this->metadata = $this->createMetadata();
        if (!isset($cacheItem)) {
            return $this->metadata;
        }
        $cacheItem->set($this->metadata);
        $this->cache->save($cacheItem);
        return $this->metadata;
    }
}

When I look at the code at runtime, the AbstractAdapter choose to give me a PhpFilesAdapter (fair enough in dev I guess).

But when I look into the code of this adapter I find this:

<?php
protected function doFetch(array $ids) {
    [...]

    foreach ($ids as $id) {
        try {
            $file = $this->getFile($id);
            list($expiresAt, $values[$id]) = include $file;
            if ($now >= $expiresAt) {
                unset($values[$id]);
            }
        } catch (\Exception $e) {
            continue;
        }
    }
}

So there is no logic at all to check for expiration except for the expiration date.

Do you know a way to invalidate the cache on file change (in dev environment of course) ? Or have I to implement it myself..?

Thanks for your help.


Solution

  • I've found a solution : ConfigCache. You can give it an array of resources to watch and it will check their last modification time each time the cache is read.

    The example above becomes something like :

    <?php
    class MetadataCacheFactory implements MetadataCacheFactoryInterface
    {
        const CACHE_NAMESPACE = 'my_namespace';
    
        /** @var \Symfony\Component\HttpKernel\KernelInterface */
        protected $kernel;
    
        public function __construct(KernelInterface $kernel)
        {
            $this->kernel = $kernel;
        }
    
        /**
         * {@inheritdoc}
         */
        public function create(): ConfigCache
        {
            return new ConfigCache($this->kernel->getCacheDir().'/'.self::CACHE_NAMESPACE, $this->kernel->isDebug());
        }
    }
    

    And the code using it:

    <?php
    class ExampleMetadataFactory implements MetadataFactoryInterface
    {
        const CACHE_KEY = 'example_metadata';
    
        [...]
    
        /** @var ExampleMetadata */
        protected $metadata;
    
        public function __construct(MetadataCacheFactoryInterface $cacheFactory)
        {
            $this->cache = $cacheFactory->create();
            $this->metadata = null;
        }
    
        /**
         * {@inheritdoc}
         */
        public function create(): ExampleMetadata
        {
            if ($this->metadata !== null) {
                return $this->metadata;
            }
            try {
                if ($this->cache->isFresh()) {
                    $this->metadata = unserialize(file_get_contents($this->cache->getPath()));
                    return $this->metadata;
                }
            } catch (CacheException $e) {
                // Ignore
            }
            $resources = [];
            $this->metadata = $this->createMetadata($resources);
            $this->cache->write(serialize($this->metadata), $resources);
            return $this->metadata;
        }
    
        /**
         * Creates the metadata.
         *
         * @param array $resources
         *
         * @return ExampleMetadata
         */
        protected function createMetadata(&$resources): ExampleMetadata
        {
            $metadata = new ExampleMetadata();
            $resourcesCollection = $this->resourcesCollectionFactory->create();
            foreach ($resourcesCollection as $resourceClass => $resourcePath) {
                // The important part, it's an instance of \Symfony\Component\Config\Resource\FileResource.
                $resources[] = new FileResource($resourcePath);
                $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
                $metadata->registerResourceMetadata($resourceMetadata);
            }
            return $metadata;
        }
    }
    

    In the hope it can help somebody else.