1

I need to switch the Symfony cache adapter depending on ENV conditions. Like if some variable is set, use "cache.adapter.apcu" or use "cache.adapter.filesystem" otherwise.

Is it possible somehow? The documentation is not really helpful with it.

P.S.: It is not possible for us to do this via the creation of a whole new environment

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Gino Pane
  • 4,740
  • 5
  • 31
  • 46
  • 1
    Yes, it's possible but not readily available. You can write your own TogglingAdapter which has both other adapters as dependencies and whenever it is called it will check the env var to pick which one to use. – dbrumann Jun 11 '21 at 09:56
  • Any chance you could expand on this @dbrumann? I can't find any docs on creating a custom cache adapter. – jrjohnson May 19 '22 at 06:56
  • @jrjohnson I have added an answer. It's not the most elegant solution, but I hope it helps. https://stackoverflow.com/questions/67934675/symfony-5-switch-cache-adapter-on-condition/72302965#72302965 – dbrumann May 19 '22 at 10:32

3 Answers3

1

Here is a basic example for a CacheAdapter which has adapters fed into it and then picking one based on a parameter (or alternatively envvar):

<?php

namespace App\Cache;

use Psr\Cache\CacheItemInterface;
use Psr\Cache\InvalidArgumentException;
use Psr\Container\ContainerInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Symfony\Contracts\Service\ServiceSubscriberTrait;

class EnvironmentAwareCacheAdapter implements AdapterInterface, ServiceSubscriberInterface
{
    use ServiceSubscriberTrait;

    private string $environment;

    public function __construct(string $environment)
    {
        $this->environment = $environment;
    }

    public function getItem($key)
    {
        return $this->container->get($this->environment)->getItem($key);
    }

    public function getItems(array $keys = [])
    {
        return $this->container->get($this->environment)->getItems($key);
    }

    // ...
}

This is how you would configure it:


services:
   App\Cache\EnvironmentAwareCacheAdapter:
       arguments:
           $environment: '%kernel.environment%'
       tags:
           - { name: 'container.service_subscriber', key: 'dev', id: 'cache.app' }
           - { name: 'container.service_subscriber', key: 'prod', id: 'cache.system' }

It's not the most elegant solution and is missing error handling and possibly a fallback. Basically, by adding tags with an appropriately named key and the alias to an existing cache as id, you can then refer to that cache with the key in your own adapter. So, depending on your environment you will pick either one. You can replace the key and the constructor argument with anything else you like. I hope that helps.

dbrumann
  • 16,803
  • 2
  • 42
  • 58
  • Looks promising – Gino Pane May 19 '22 at 11:32
  • Btw I ended up with chain cache using apcu and filesystem without per-env switch – Gino Pane May 19 '22 at 11:44
  • 1
    Thanks for this! It's where I've started as well, but I'm stuck on how to integrate it with the cache pool and cache.app system. I think I may need to create a new question for this, but I just don't see how to use a factory or another pattern to determine what cache adapters go into a pool based on configuration. Specifically I only want to use Redis if some ENV are configured, otherwise use APCu. – jrjohnson May 19 '22 at 21:39
  • If it is really just APP_ENV-specific and not based on a more complex set of configurations, using `when@dev:` instead is a better option, maybe in combination with the chain adapter. Your cache is clearly defined at compile time and your code behaves consistently. If you switch at runtime by fetching a cache from a prepared container/factory, you might run into weird behaviors and cache states. In any case, feel free to open a new question, specific to your use case. If I see it, I will have a look. – dbrumann May 20 '22 at 12:39
  • 1
    Thanks, my issue turned out to be with an auto injected $namespace value into the adapter that throws a weird error without an `arguments: [~]` or something in an arguments value in the service definition. Once I get through all the specifics I'll add an answer here. – jrjohnson May 20 '22 at 21:15
0

It seems like you can not set up your cache configuration to use a environment variable like so:

framework:
    cache:
        app: %env(resolve:CACHE_ADAPTER)%

It is the constraint of FrameworkBundle that provides the cache service. And this constraint will not be "fixed" (Using environment variables at compile time #25173).

To make it possible you need to make your own cache provider that can just pass all arguments to the needed cache provider. You will have access to environment variables at runtime and so you can use it as a proxy that knows what provider to use.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Michael Sivolobov
  • 12,388
  • 3
  • 43
  • 64
  • Unfortunately it expects the exact service and DI cannot resolve any parametrized input: ```You have requested a non-existent service: "%env(resolve:CACHE_ADAPTER)%"``` – Gino Pane Jun 11 '21 at 10:07
  • @GinoPane, yeah... you are right. Just checked it. I updated my answer – Michael Sivolobov Jun 11 '21 at 11:03
  • @MichaelSivolobov do you have any guidance on how to create my own cache provider? I'm not sure where to start and I'm not finding any documentation on this. – jrjohnson May 19 '22 at 07:10
0

You can create a CompilerPass:

class CachePass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $adapter = $container->resolveEnvPlaceholders(
            $container->getParameter('app.cache.app.adapter'),
            true
        );
        /** @var ChildDefinition $cacheApp */
        $cacheApp = $container->getDefinition('cache.app');
        $cacheApp->setParent($adapter);
    }
}

Kernel.php

    $container->addCompilerPass(new CachePass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 33);
    // Priority 33 is important. It is before symfony's CachePoolPass

cache.yaml

framework:
  cache:
    default_redis_provider: 'redis://%app.cache.redis_adapter.host%'

services.yaml

parameters:
    env(CACHE_APP_ADAPTER): 'filesystem'
    app.cache.app.adapter: 'cache.adapter.%env(CACHE_APP_ADAPTER)%'
    app.cache.redis_adapter.host: '%env(REDIS_HOST)%'

.env

# filesystem or redis
CACHE_APP_ADAPTER=redis
REDIS_HOST=localhost:5200
Alex83690
  • 758
  • 9
  • 30