TYPO3 10 comes with many treats for developers. I have already published in-depth acticles about two of the big ones. Since we will be building on that knowledge here, a short disclaimer before we start:

This post contains somewhat advanced usage examples for dependency injection and events.
If you are unfamiliar with the basics, please refer to my in-depth articles about Dependency Injection and PSR-14 Events in TYPO3 as well as to the official documentation.

With that out of the way, let's dive into todays example. You don't need to know much about the application I am building to understand the following concepts. We will need a cache and tag our cache entries and we will flush cache tags based on events. It's all about having it broken down to small parts that can be adapted and used in different contexts as well.

In need of a Cache

My application needs its own cache within TYPO3s caching framework (official Documentation). Thats easily configured. All we really need is a name for our cache, let's call it ext-api-cache. We register it according to the documentation:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['ext-api-cache']['backend'] =
	\TYPO3\CMS\Core\Cache\Backend\RedisBackend::class;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['ext-api-cache']['options'] = [
	'database' => 1,
	'hostname' => $redisHost,
	'port' => $redisPort
];

This could be any other cache backend as well. Having redis configured is of no importance for this article.

After our cache is registered and exists, we need to be able to read from and write into it from within our application. Let's say we have a controller of some sort that will gladly return cached values whenever possible. We will inject the cache into the controller using a factory.

Top

Dependency Injection with a Factory

"Using a factory to create services" is literally the name of a documentation chapter of the symfony dependency injection component that is used by TYPO3 as well. The idea is to use the CacheManager as a factory and inject the resolved cache by specifying an alias (symfony documentation) in the Services.yaml. Defining an alias looks like this:

services:
  cache.ext-api-cache:
    class: TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
    factory: ['@TYPO3\CMS\Core\Cache\CacheManager', 'getCache']
    arguments: ['ext-api-cache']

This configuration says: The alias cache.ext-api-cache represents an instance of TYPO3\CMS\Core\Cache\Frontend\FrontendInterface that is returned by the method getCache() of the class TYPO3\CMS\Core\Cache\CacheManager with the provided argument 'ext-api-cache'.

Tip: All caches shipped by the core have already been aliased to enable quick and easy injection (Feature RST, Patch).

Next our controller needs to somehow retrieve this now aliased cache. We use the recommended constructor injection for this. We expect an implementation of TYPO3\CMS\Core\Cache\Frontend\FrontendInterface since that is what cacheManager->getCache() promises to return.

We have to wire the controller to the alias we need in the Services.yaml:

services:
  DanielGoerz\MyExt\Controller\ApiController:
    arguments:
      $cache: '@cache.ext-api-cache'

This enables the constructor injection of our custom cache by using the argument $cache:

use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
class ApiController
{
    /**
     * @var FrontendInterface
     */
    private $cache;

    public function __construct(FrontendInterface $cache)
    {
        $this->cache = $cache;
    }
}

Now that the cache is injected properly, let's use it. We have different aspects writing to the same cache, so it is beneficial to use cache tags (official documentation), so that we can later flush dedicated parts of the cache. The controller does its thing and will happily consult the cache from now on:

public function exampleAction(int $parameter): string
{
    $cacheIdentifier = 'ext_api_record_' . $parameter;
    if (!($response = $this->cache->get($cacheIdentifier))) {
        $response = $this->createTheResponseSomehow();
        $this->cache->set($cacheIdentifier, $response, [Configuration::MY_CACHE_TAG]);
    }
    return $response;
}

It is not important whether we have any $parameter like a recordId, the language or something else along those lines. The important points to take away from this example are that the cache is consulted and if empty it is filled for the next time. Additionally, a cache tag is used (in this case the name of the cache tag is stored in a PHP constant).

With our cache filled at last, let's move on and say we have to flush all cache entries with the tag MY_CACHE_TAG under certain circumstances. Let's also say we have implemented an event that is fired if those circumstances occur. With that we move to the last part of this example.

Top

Event Interfaces and invokable Event Listeners

Let's recap the situation: We have some values cached and tagged and we have an event that is triggered by an occasion that requires all our tagged cache entries to be flushed.

Let's say the event is called AfterSomeRecordModifiedEvent. We can now implement a listener for this event that clears the cache tags. But then we realize that we also have to react to the AfterSomeRecordCreatedEvent and that there will be many more. A nice solution to this is to use an interface or class hierarchy of events. Yes, we can register listeners for interfaces and subclasses of events. Let's go with an interface here.

Our events need to be able to return the cache tags that shall be flushed, so the interface would look something like this:

interface FlushableCacheTagInterface
{
    /**
     * @return String[]
     */
    public function getCacheTagsToFlush(): array;
}

Our events can then implement this interface like this

class AfterSomeRecordModifiedEvent implements FlushableCacheTagInterface
{
	/* other stuff the event has to offer */

    public function getCacheTagsToFlush(): array
    {
        return [Configuration::MY_CACHE_TAG];
    }
}

After that we register a listener for this interface. Since this listener is only doing one thing, namely clearing the cache tags in our custom cache, we can make it invokable (PHP documentation) so that the class itself can be called as a function. With this we can omit specifying a method when registering our listener in our Services.yaml:

services:
  DanielGoerz\MyExt\Cache\CacheTagCleaner:
    arguments:
      $cache: '@cache.ext-api-cache'
    tags:
      - name: event.listener
        identifier: 'ext-myext-cache-tag-clearer'
        event: DanielGoerz\MyExt\Event\FlushableCacheTagInterface

And with this we combine both: the dependency injection of our cache via an alias and the registration of a listener to a custom interface.

So finally, our event listener that clears cache tags based on events that implement the corresponding interface looks like this:

final class CacheTagCleaner
{
    /**
     * @var FrontendInterface
     */
    private $cache;

    public function __construct(FrontendInterface $cache)
    {
        $this->cache = $cache;
    }

    public function __invoke(FlushableCacheTagInterface $event): void
    {
        $this->cache->flushByTags($event->getCacheTagsToFlush());
    }
}

The beauty of this lies in the fact that we can implement our interface in any event of our application and the cache tags will be flushed automatically.

And I guess that's it.

I hope this little excercise was of some use to you and makes you want to use and embrace the new things we got with TYPO3 v10.

For what it's worth: I am very happy that I can write such clean code in my TYPO3 projects from now on. <3

In any case: thanks for reading.