How to Create a Custom Cache Class in Magento 2 with Tests

In Magento 2, caching plays a critical role in optimizing performance by reducing the load on backend services and databases. However, sometimes the default cache types might not be sufficient for your custom module needs, and you may want to create your own cache system.

This guide will walk you through how to create a custom cache class in Magento 2, implement caching logic in your module, and write integration tests to ensure everything works as expected.

Step 1: Create a Custom Cache Class

Magento provides a flexible cache architecture where you can define and manage your own cache types. We’ll start by creating a custom cache type that will store specific data required by our custom logic.

Directory Structure:

Your custom module should be structured as follows:

app/code/Custom/DataCache/
├── etc
│   └── cache.xml
├── Helper
│   └── Config.php
├── Model
│   └── Cache
│       └── Type
│           └── CustomCache.php
│   └── DataCacheManager.php

CustomCache.php

This class defines your custom cache type by extending Magento’s TagScope. The TYPE_IDENTIFIER and CACHE_TAG are used to manage and identify the cache.



namespace Custom\DataCache\Model\Cache\Type;

use Magento\Framework\Cache\Frontend\Decorator\TagScope;
use Magento\Framework\App\Cache\Type\FrontendPool;

class CustomCache extends TagScope
{
    const TYPE_IDENTIFIER = 'custom_cache';
    const CACHE_TAG = 'CUSTOM_CACHE_TAG';

    public function __construct(FrontendPool $cacheFrontendPool)
    {
        parent::__construct($cacheFrontendPool, self::TYPE_IDENTIFIER, [self::CACHE_TAG]);
    }
}

This custom cache class, CustomCache, will be used to store and retrieve cached data for our module.

cache.xml

Next, we need to register the custom cache type by adding a cache.xml file in the etc directory:

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Cache/etc/cache.xsd">
    <type name="custom_cache" translate="label" instance="Custom\DataCache\Model\Cache\Type\CustomCache" cacheable="true" label="Custom Data Cache"/>
config>

This XML file registers the custom cache type with Magento’s caching system, allowing it to be managed like any other cache type (e.g., flushable via CLI or Admin).

Step 2: Create the Data Cache Manager

The next step is to create a DataCacheManager class, which will handle the logic of fetching data and caching it. This class will first check if the cache is enabled and try to retrieve the cached data. If the data isn’t available in the cache, it will fetch the data from a data provider, store it in the cache, and then return the data.

DataCacheManager.php



namespace Custom\DataCache\Model;

use Magento\Framework\App\CacheInterface;
use Magento\Framework\App\Cache\StateInterface;
use Magento\Framework\Serialize\SerializerInterface;
use Custom\DataCache\Helper\Config;
use Custom\Api\DataProviderInterface;

class DataCacheManager
{
    private const CACHE_LIFETIME = 86400; // 1 day

    public function __construct(
        private SerializerInterface $serializer,
        private CacheInterface $cache,
        private StateInterface $cacheState,
        private Config $config,
        private DataProviderInterface $dataProvider
    ) {
    }

    public function getCachedData(int $identifier): mixed
    {
        if (!$this->cacheState->isEnabled('custom_cache')) {
            return $this->dataProvider->fetchData($identifier);
        }

        $cachedValue = $this->cache->load('custom_cache_' . $identifier);

        if (is_string($cachedValue)) {
            return $this->serializer->unserialize($cachedValue);
        }

        // Fetch data from the data provider
        $data = $this->dataProvider->fetchData($identifier);

        // Save the data to cache
        $this->cache->save(
            $this->serializer->serialize($data),
            'custom_cache_' . $identifier,
            ['custom_cache_tag'],
            $this->config->getCacheLifetime() ?? self::CACHE_LIFETIME
        );

        return $data;
    }
}

This class handles the core logic of caching. It checks whether the cache is enabled, and if it is, it will try to retrieve the cached data. If the data is not available, it will fetch the data from a provider (e.g., an API or database), cache it, and return the data.

Step 3: Write Tests for the Custom Cache

Testing is crucial to ensure your custom cache logic works correctly. Magento’s integration tests allow you to test your cache behavior in a real Magento environment.

Directory Structure for Tests:

Your test files should be structured like this:

app/code/Custom/DataCache/Test/Integration/
└── DataCacheManagerTest.php

DataCacheManagerTest.php

Here is the test class that verifies the functionality of your custom cache logic.



declare(strict_types=1);

namespace Custom\DataCache\Test\Integration;

use Magento\Framework\App\CacheInterface;
use Magento\Framework\Serialize\SerializerInterface;
use Magento\TestFramework\Fixture\Cache;
use Magento\TestFramework\Helper\Bootstrap;
use Custom\DataCache\Model\DataCacheManager;
use Custom\DataCache\Model\Cache\Type\CustomCache;
use Custom\Api\DataProviderInterface;
use PHPUnit\Framework\TestCase;

class DataCacheManagerTest extends TestCase
{
    private const IDENTIFIER = 1;
    private const ORIGINAL_DATA = 'original_data';
    private const CACHED_DATA = 'cached_data';

    private ?DataCacheManager $dataCacheManager = null;
    private ?CacheInterface $cache = null;
    private ?SerializerInterface $serializer = null;
    private ?DataProviderInterface $dataProvider = null;

    protected function setUp(): void
    {
        $objectManager = Bootstrap::getObjectManager();

        // Mock the DataProviderInterface
        $this->dataProvider = $this->createMock(DataProviderInterface::class);

        // Create the DataCacheManager instance with the mock data provider
        $this->dataCacheManager = $objectManager->create(DataCacheManager::class, [
            'dataProvider' => $this->dataProvider
        ]);

        // Get CacheInterface and SerializerInterface instances
        $this->cache = $objectManager->get(CacheInterface::class);
        $this->serializer = $objectManager->get(SerializerInterface::class);
    }

    #[Cache(CustomCache::TYPE_IDENTIFIER, false)]
    public function testGetDataWhenCacheDisabled(): void
    {
        // Mock DataProvider to return ORIGINAL_DATA
        $this->dataProvider->method('fetchData')
            ->with(self::IDENTIFIER)
            ->willReturn(self::ORIGINAL_DATA);

        // Fetch data when cache is disabled
        $data = $this->dataCacheManager->getCachedData(self::IDENTIFIER);

        // Assert that data is fetched directly from DataProvider
        $this->assertEquals(self::ORIGINAL_DATA, $data);
    }

    #[Cache(CustomCache::TYPE_IDENTIFIER, true)]
    public function testGetDataFromCache(): void
    {
        // Cache CACHED_DATA for the identifier
        $this->cache->save(
            $this->serializer->serialize(self::CACHED_DATA),
            CustomCache::TYPE_IDENTIFIER . self::IDENTIFIER,
            [CustomCache::CACHE_TAG]
        );

        // Fetch data when cache is enabled and value is cached
        $data = $this->dataCacheManager->getCachedData(self::IDENTIFIER);

        // Data should be retrieved from cache
        $this->assertEquals(self::CACHED_DATA, $data);
    }

    #[Cache(CustomCache::TYPE_IDENTIFIER, true)]
    public function testGetDataSavedToCache(): void
    {
        $this->cache->remove(CustomCache::TYPE_IDENTIFIER . self::IDENTIFIER);

        // Mock DataProvider to return ORIGINAL_DATA
        $this->dataProvider->method('fetchData')
            ->with(self::IDENTIFIER)
            ->willReturn(self::ORIGINAL_DATA);

        // Fetch data when cache is enabled and value is not cached
        $this->dataCacheManager->getCachedData(self::IDENTIFIER);

        $savedData = $this->cache->load(
            CustomCache::TYPE_IDENTIFIER . self::IDENTIFIER,
        );

        $this->assertEquals(self::ORIGINAL_DATA, $this->serializer->unserialize($savedData));
    }
}

Key Test Cases:

  1. Cache Disabled: Ensure that when the cache is disabled, the data is fetched directly from the data provider.
  2. Cache Hit: If the data is cached, it should be retrieved from the cache.
  3. Cache Miss and Save: When the data is not in the cache, it should be fetched from the data provider and stored in the cache.

Step 4: Running the Tests

To run the integration tests, execute the following command in your Magento 2 installation:

vendor/bin/phpunit --testsuite integration

Ensure that your dev/tests/integration/phpunit.xml file is configured properly and includes your custom module for testing.

Conclusion

By following this guide, you’ve learned how to create a custom cache class in Magento 2. We covered how to implement the cache in a modular way, register it with Magento’s caching system, and write integration tests to verify the

behavior. Creating a custom cache is an effective way to improve performance by caching frequently accessed data specific to your module.

Happy coding!