OroCacheBundle
OroCacheBundle introduces the configuration of the application data cache storage used by the application bundles for different cache types.
Data Cache Service
We recommend using service oro.data.cache
to cache the application data. This service uses Symfony Cache ChainAdapter to improve cache operations.
You can inject it into the service where you need to cache the data:
services:
acme_demo.my_data_cache.service:
class: Acme\Bundle\DemoBundle\Provider\CacheService
public: false
arguments:
- '@oro.data.cache'
You can also set a namespace for the cache pool that extends oro.data.cache
:
services:
acme_demo.data.cache:
parent: oro.data.cache
tags:
- { name: 'cache.pool', namespace: 'acme_demo' }
acme_demo.my_data_cache.service:
class: Acme\Bundle\DemoBundle\Provider\CacheService
arguments:
- '@acme.data.cache'
Caching Static Configuration
A static configuration is defined in the configuration files and does not depend on the application data. Usually such configuration is loaded from configuration files located in different bundles, e.g. from Resources/config/oro/my_config.yml files that can be located in any bundle. There are several possible ways to store the collected configuration to avoid loading and merging it on each request:
As a parameter in the dependency injection container. The disadvantage of this approach is not very good DX (Developer Experience) because each time when the configuration is changed the whole container should be rebuilt.
As a data file in the system cache. This approach has better DX as this is the only file that needs rebuilding after the configuration is changed. However, the disadvantage is that data should be deserialized every time it is requested.
As a PHP file in the system cache. It has the same DX as the previous approach but with two important additional advantages: the deserialization of the data is not needed and the loaded data is cached by OPcache.
To implement 3rd approach for your configuration, you need to take the following steps:
Create PHP class that defines the schema of your configuration and validation and merging rules for it. E.g.:
namespace Acme\Bundle\DemoBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class MyConfiguration implements ConfigurationInterface
{
/**
* @inheritDoc
*/
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('my_config');
// build the configuration tree here
return $treeBuilder;
}
}
Create the configuration provider PHP class that you will use to get the configuration data. E.g.:
namespace Acme\Bundle\DemoBundle\Provider;
use Acme\Bundle\DemoBundle\DependencyInjection\MyConfiguration;
use Oro\Bundle\ActionBundle\Configuration\ConfigurationProviderInterface;
use Oro\Component\Config\Cache\PhpArrayConfigProvider;
use Oro\Component\Config\Loader\CumulativeConfigLoader;
use Oro\Component\Config\Loader\CumulativeConfigProcessorUtil;
use Oro\Component\Config\Loader\YamlCumulativeFileLoader;
use Oro\Component\Config\ResourcesContainerInterface;
class MyConfigurationProvider extends PhpArrayConfigProvider implements ConfigurationProviderInterface
{
private const CONFIG_FILE = 'Resources/config/oro/my_config.yml';
/**
* @inheritDoc
*/
public function getConfiguration(): array
{
return $this->doGetConfig();
}
/**
* {@inheritdoc}
*/
protected function doLoadConfig(ResourcesContainerInterface $resourcesContainer)
{
$configs = [];
$configLoader = new CumulativeConfigLoader(
'my_config',
new YamlCumulativeFileLoader(self::CONFIG_FILE)
);
$resources = $configLoader->load($resourcesContainer);
foreach ($resources as $resource) {
$configs[] = $resource->data;
}
return CumulativeConfigProcessorUtil::processConfiguration(
self::CONFIG_FILE,
new MyConfiguration(),
$configs
);
}
}
Register the created configuration provider as a service using
oro.static_config_provider.abstract
service as the parent one. E.g.:
services:
acme_demo.my_configuration_provider:
class: Acme\Bundle\DemoBundle\Provider\MyConfigurationProvider
public: false
parent: oro.static_config_provider.abstract
arguments:
- '%kernel.cache_dir%/oro/my_config.php'
- '%kernel.debug%'
The cache warmer is registered automatically with the priority 200
. This priority adds the warmer at the begin
of the warmers chain that prevents double warmup in case some Application cache depends on the static config cache.
The warmer service ID is the configuration provider service ID prefixed with .warmer
. If you want to change
the priority or use your own warmer, you can register the service following these naming conventions.
In this case a default warmer will not be registered for your configuration provider.
An example of a custom warmer:
services:
acme_demo.my_configuration_provider.warmer:
class: Oro\Component\Config\Cache\ConfigCacheWarmer
public: false
arguments:
- '@acme_demo.my_configuration_provider'
tags:
- { name: kernel.cache_warmer }
If your Application cache depends on your configuration, use isCacheFresh($timestamp)
and getCacheTimestamp()
methods of the configuration provider to check if the Application cache needs to be rebuilt.
Here is an example how to use these methods:
private function ensureConfigLoaded()
{
if (null !== $this->configuration) {
return;
}
$cacheItem = $this->cache->getItem(self::CACHE_KEY);
$config = $this->fetchConfigFromCache($cacheItem);
if (null === $config) {
$config = $this->loadConfig();
$this->saveConfigToCache($cacheItem, $config);
}
$this->configuration = $config;
}
private function fetchConfigFromCache(CacheItemInterface $cacheItem): ?array
{
$config = null;
if ($cacheItem->isHit()) {
[$timestamp, $value] = $cacheItem->get();
if ($this->mappingConfigProvider->isCacheFresh($timestamp)) {
$config = $value;
}
}
return $config;
}
private function saveMappingConfigToCache(CacheItemInterface $cacheItem, array $config): void
{
$cacheItem->set([$this->mappingConfigProvider->getCacheTimestamp(), $config]);
$this->cache->save($cacheItem);
}
private function loadConfig(): array
{
$config = $this->configProvider->getConfiguration();
// add some additional processing of the configuration here
return $config;
}
Caching Symfony Validation Rules
By default, rules for Symfony Validation Component are cached using oro.cache.adapter.persistent
service,
but you can change this to make validation caching suit some custom requirements. To do this, you need
to redefine the cache.validator
cache pool configuring some custom service provider.
Memory Based Cache
One of the most important things when dealing with caches is proper cache invalidation. When using memory based cache, we need to make sure that we do not keep old values in the memory. Consider this example:
class LocalizationManager
{
private Symfony\Component\Cache\Adapter\ArrayAdapter $cacheProvider;
public function getLocalization($id)
{
$localization = $this->cacheProvider->get($id, function () {
// ... all other operations, fetch from DB if cache is empty
// ... save in cache data from DB, see Symfony\Contracts\Cache\CacheInterface
});
return $localization;
}
}
Since $cacheProvider
in our example is an implementation of memory ArrayAdapter
, we will keep the data
there until the process ends. With HTTP request this would work perfectly well, but when our
LocalizationManager
is used in some long-running CLI processes, we have to manually clear memory cache
after every change with Localizations. Missing cache clearing for any of these cases leads
to outdated data in LocalizationManager
.
The MemoryCache class can be used when you need fast implementation of a memory cache.
Default Cache Implementation
There are two abstract services you can use as a parent for your cache services. Default implementations is following:
For services based on oro.data.cache
the: Symfony Cache ChainAdapter with Symfony ArrayAdapter on the top of Symfony FilesystemAdapter
For services based on oro.data.cache.without_memory_cache
the Symfony Cache ChainAdapter is not used.
Caching Data based on Complex Objects
Cache Hit Ratio is an important measure of cache efficiency. Choosing the right Cache Key might be a complex task, especially when the cache key should depend on the data stored in a Complex Object. Cache key generation strategy can vary in different cases and rely on different fields of the same object. To configure cache metadata for different scopes, use Resources/config/oro/cache_metadata.yml files that can be located in any bundle.
Here is an example of such configuration:
Oro\Bundle\OrderBundle\Entity\OrderAddress:
attributes:
id:
groups: ['shipping_context']
country:
groups: ['shipping_context', 'promotion']
Data from this configuration is used by the oro.cache.generator.object_cache_key
service to provide cache keys for the
given object and scope.
Sometimes there is a need to configure cache key generation metadata for objects that cannot be normalized by
the default normalizer GetSetMethodNormalizer
. In such case new normalizer should be created and registered as a service
tagged with oro_cache_generator_normalizer
.
acme_demo.serializer.normalizer:
class: Acme\Bundle\DemoBundle\Normalizer\CustomObjectNormalizer
tags:
- { name: 'oro_cache_generator_normalizer' }