Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions .github/workflows/tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,12 @@ jobs:
fail-fast: false
max-parallel: 2
matrix:
php: [ '81', '82', '83', '84' ]
php: [ '82', '83', '84' ]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add support for php 8.5?
or is that a big task?

typo3: [ '11', '12', '13' ]
exclude:
- php: '84'
typo3: '11'
- php: '81'
typo3: '13'

outputs:
result: ${{ steps.set-result.outputs.result }}
php: ${{ matrix.php }}
Expand All @@ -32,13 +31,16 @@ jobs:
- name: Install Node.js (Alpine)
run: apk add --no-cache nodejs npm
- uses: actions/checkout@v4
- name: Get composer project name
id: composer-name
run: echo "name=$(php -r "echo str_replace('/', '-', json_decode(file_get_contents('composer.json'))->name);")" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
path: |
~/.composer/cache/files
vendor
node_modules
key: ${{ matrix.typo3 }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}
key: ${{ matrix.typo3 }}-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
restore-keys: |
${{ matrix.typo3 }}-${{ matrix.php }}-composer-
- run: git config --global --add safe.directory $GITHUB_WORKSPACE
Expand All @@ -54,17 +56,23 @@ jobs:
- uses: actions/upload-artifact@v4
if: always()
with:
name: ${{ matrix.typo3 }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}
name: ${{ steps.composer-name.outputs.name }}-${{ matrix.typo3 }}-${{ matrix.php }}
path: summary/results.txt
summary:
runs-on: ubuntu-latest
needs: build
if: always()
steps:
- uses: actions/checkout@v4
- name: Get composer project name
id: composer-name
run: echo "name=$(python3 -c "import json; d=json.load(open('composer.json')); print(d['name'].replace('/', '-'))")" >> $GITHUB_OUTPUT
- uses: actions/download-artifact@v4
with:
path: summary
pattern: ${{ steps.composer-name.outputs.name }}-*
merge-multiple: false
- name: Show results
run: |
echo "### Matrix results"
cat summary/**/results.txt
find summary -name results.txt | sort | xargs cat
131 changes: 131 additions & 0 deletions Classes/Cache/CacheManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Weakbit\FallbackCache\Cache;

use Override;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
Expand All @@ -13,6 +14,7 @@
use TYPO3\CMS\Core\Cache\Exception\InvalidBackendException;
use TYPO3\CMS\Core\Cache\Exception\InvalidCacheException;
use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException;
use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheGroupException;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Cache\Frontend\VariableFrontend;
use TYPO3\CMS\Core\Utility\GeneralUtility;
Expand Down Expand Up @@ -154,6 +156,11 @@ private function isStatusRed(string $identifier): bool
};
}

private function isImmutable(string $identifier): bool
{
return (bool)($this->cacheConfigurations[$identifier]['tags'][0]['immutable'] ?? false);
}

/**
* @throws DuplicateIdentifierException
* @throws InvalidBackendException
Expand Down Expand Up @@ -203,4 +210,128 @@ private function registerFallback(string $identifier, string $fallback): void
$this->logger?->warning('Registering fallback cache ' . $fallback . ' for ' . $identifier);
$this->fallbacks[$identifier] = $fallback;
}

/**
* @inheritdoc
*/
#[Override]
public function flushCaches(): void
{
$this->createAllCaches();
foreach ($this->caches as $cache) {
if ($this->isImmutable($cache->getIdentifier())) {
continue;
}

$cache->flush();
}
}

/**
* @inheritdoc
*/
#[Override]
public function flushCachesInGroup($groupIdentifier): void
{
$this->createAllCaches();
if (!isset($this->cacheGroups[$groupIdentifier])) {
throw new NoSuchCacheGroupException("No cache in the specified group '" . $groupIdentifier . "'", 1390334120);
}

foreach ($this->cacheGroups[$groupIdentifier] as $cacheIdentifier) {
if (isset($this->caches[$cacheIdentifier])) {
if ($this->isImmutable($cacheIdentifier)) {
continue;
}

$this->caches[$cacheIdentifier]->flush();
}
}
}

/**
* @param string $groupIdentifier
* @param string $tag Tag to search for
* @inheritdoc
*/
#[Override]
public function flushCachesInGroupByTag($groupIdentifier, $tag): void
{
if (empty($tag)) {
return;
}

$this->createAllCaches();
if (!isset($this->cacheGroups[$groupIdentifier])) {
throw new NoSuchCacheGroupException("No cache in the specified group '" . $groupIdentifier . "'", 1390337129);
}

foreach ($this->cacheGroups[$groupIdentifier] as $cacheIdentifier) {
if (isset($this->caches[$cacheIdentifier])) {
if ($this->isImmutable($cacheIdentifier)) {
continue;
}

$this->caches[$cacheIdentifier]->flushByTag($tag);
}
}
}

/**
* @inheritdoc
*/
#[Override]
public function flushCachesInGroupByTags($groupIdentifier, array $tags): void
{
if ($tags === []) {
return;
}

$this->createAllCaches();
if (!isset($this->cacheGroups[$groupIdentifier])) {
throw new NoSuchCacheGroupException("No cache in the specified group '" . $groupIdentifier . "'", 1390337130);
}

foreach ($this->cacheGroups[$groupIdentifier] as $cacheIdentifier) {
if (isset($this->caches[$cacheIdentifier])) {
if ($this->isImmutable($cacheIdentifier)) {
continue;
}

$this->caches[$cacheIdentifier]->flushByTags($tags);
}
}
}

/**
* @inheritdoc
*/
#[Override]
public function flushCachesByTag($tag): void
{
$this->createAllCaches();
foreach ($this->caches as $cache) {
if ($this->isImmutable($cache->getIdentifier())) {
continue;
}

$cache->flushByTag($tag);
}
}

/**
* @inheritdoc
*/
#[Override]
public function flushCachesByTags(array $tags): void
{
$this->createAllCaches();
foreach ($this->caches as $cache) {
if ($this->isImmutable($cache->getIdentifier())) {
continue;
}

$cache->flushByTags($tags);
}
}
}
2 changes: 1 addition & 1 deletion Classes/EventListener/CacheStatusEventListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
use Weakbit\FallbackCache\Event\CacheStatusEvent;

#[AsEventListener(
identifier: \Weakbit\FallbackCache\EventListener\CacheStatusEventListener::class,
identifier: CacheStatusEventListener::class,
event: CacheStatusEvent::class,
)]
class CacheStatusEventListener
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* displays some information about caches in the system information toolbar
*/
#[AsEventListener(
identifier: \Weakbit\FallbackCache\EventListener\SystemInformationToolbarCollectorEventListener::class,
identifier: SystemInformationToolbarCollectorEventListener::class,
event: SystemInformationToolbarCollectorEvent::class,
)]
class SystemInformationToolbarCollectorEventListener
Expand Down
Binary file added Documentation/system-information.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 63 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,71 @@ You can *chain* them and also define a fallback for the fallback cache.

You could end the chain with a cache with the NullBackend, if that also fails the hope for this TYPO3 request is lost. But using no cache may bring down your server, but that depends on the server and application.

## Immutable Cache Configuration

This extension provides the ability to mark certain caches as "immutable", which means they will not be affected by cache flushing operations. This is particularly useful for caches that contain data that rarely changes and is expensive to regenerate, such as compiled templates, code caches, or reference data.

⚠️ **WARNING**: Immutable caches must be manually managed by developers. The system will NOT automatically clear these caches during regular maintenance operations!

### How to Configure Immutable Caches

To mark a cache as immutable, add the `tags` configuration with the `immutable` property set to `true`:



```PHP
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['my_immutable_cache'] = [
'frontend' => \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class,
'backend' => \TYPO3\CMS\Core\Cache\Backend\FileBackend::class,
'options' => [
'defaultLifetime' => 604800,
],
'groups' => [
'system',
],
'tags' => [
['name' => 'cache', 'identifier' => 'my_immutable_cache', 'immutable' => true]
]
];
```

### Behavior of Immutable Caches

When a cache is marked as immutable:

1. It will **not** be cleared when `flushCaches()` is called
2. It will **not** be cleared when `flushCachesByTag()` is called with any tags
3. It will **not** be cleared when `flushCachesInGroup()` is called, even if the cache belongs to that group

This feature ensures that important cache entries remain available even during maintenance operations or when other parts of the system trigger cache flushes.

### When to Use Immutable Caches

Consider using immutable caches for:

- Compiled templates or CSS/JS assets that rarely change
- Code caches that are expensive to regenerate
- Core configuration data that is only updated during system upgrades
- Any cache data where regeneration would cause significant load on the system

When you need to update an immutable cache, you'll need to manually clear it using direct backend operations or by temporarily removing the immutable flag.

# How to Access the Cache Status

1. Log in to your TYPO3 backend
2. Look at the top toolbar (the black bar at the top of the screen)
3. Find the system information icon (typically shows system details like TYPO3 version)
4. Click on this icon to see a dropdown menu
5. The cache status will be displayed

![Cache Status in System Information Toolbar](Documentation/system-information.png)

# TODO
- [ ] Give a possibility to see the actual state of the caches

- [ ] Refactor addCacheStatus to comply with external calls

# Credits

Inspired by https://packagist.org/packages/b13/graceful-cache

Uses code from https://github.com/marketing-factory/typo3_prometheus
6 changes: 6 additions & 0 deletions Tests/FallbackCacheTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Weakbit\FallbackCache\Tests;

use PHPUnit\Framework\Attributes\Test;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\Cache\Backend\FileBackend;
use TYPO3\CMS\Core\Cache\Backend\NullBackend;
Expand Down Expand Up @@ -114,6 +115,7 @@ protected function setUp(): void
* @test
* @throws NoSuchCacheException
*/
#[Test]
public function testGetGoodCacheReturnsCache(): void
{
$cacheManager = GeneralUtility::makeInstance(CacheManager::class);
Expand All @@ -126,6 +128,7 @@ public function testGetGoodCacheReturnsCache(): void
* @test
* @throws NoSuchCacheException
*/
#[Test]
public function testGetBadCacheThrowsException(): void
{
$cacheManager = GeneralUtility::makeInstance(CacheManager::class);
Expand All @@ -138,6 +141,7 @@ public function testGetBadCacheThrowsException(): void
* @test
* @throws NoSuchCacheException
*/
#[Test]
public function testGetBrokenCacheReturnsFallbackCache(): void
{
$cacheManager = GeneralUtility::makeInstance(CacheManager::class);
Expand All @@ -150,6 +154,7 @@ public function testGetBrokenCacheReturnsFallbackCache(): void
* @test
* @throws NoSuchCacheException
*/
#[Test]
public function testGetStatusRedCacheReturnsFallbackCache(): void
{
$cacheManager = GeneralUtility::makeInstance(CacheManager::class);
Expand All @@ -170,6 +175,7 @@ public function testGetStatusRedCacheReturnsFallbackCache(): void
* @test
* @throws NoSuchCacheException
*/
#[Test]
public function testCacheBackendStateVerificationAfterFallback(): void
{
$cacheManager = GeneralUtility::makeInstance(CacheManager::class);
Expand Down
Loading