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
70 changes: 70 additions & 0 deletions .github/workflows/tasks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: PHP Composer

on: [push, pull_request]

permissions:
contents: read

jobs:
build:
name: "php: ${{ matrix.php }} TYPO3: ${{ matrix.typo3 }}"
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 2
matrix:
php: [ '81', '82', '83', '84' ]
typo3: [ '11', '12', '13' ]
exclude:
- php: '84'
typo3: '11'
- php: '81'
typo3: '13'
outputs:
result: ${{ steps.set-result.outputs.result }}
php: ${{ matrix.php }}
typo3: ${{ matrix.typo3 }}
container:
image: ghcr.io/typo3/core-testing-php${{ matrix.php }}:latest
steps:
- name: Check OS
run: cat /etc/os-release
- name: Install Node.js (Alpine)
run: apk add --no-cache nodejs npm
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: |
~/.composer/cache/files
vendor
node_modules
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
- run: composer update --with typo3/minimal:^${{ matrix.typo3 }} --ignore-platform-req=php+
- run: ./vendor/bin/grumphp run --ansi --no-interaction
- run: composer test
- name: Save result
if: always()
run: |
mkdir -p summary
echo "PHP=${{ matrix.php }} TYPO3=${{ matrix.typo3 }} RESULT=${{ job.status }}" \
>> summary/results.txt
- uses: actions/upload-artifact@v4
if: always()
with:
name: ${{ matrix.typo3 }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}
path: summary/results.txt
summary:
runs-on: ubuntu-latest
needs: build
if: always()
steps:
- uses: actions/download-artifact@v4
with:
path: summary
- name: Show results
run: |
echo "### Matrix results"
cat summary/**/results.txt
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
public/
var/
vendor/
.idea/
34 changes: 17 additions & 17 deletions Classes/Cache/CacheManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,6 @@ class CacheManager extends \TYPO3\CMS\Core\Cache\CacheManager implements LoggerA
*/
protected array $seen = [];

/**
* @var array<string>
*/
protected array $breadcrumb = [];

/**
* @param array<string, array<mixed>> $cacheConfigurations
*/
Expand All @@ -54,12 +49,16 @@ public function setCacheConfigurations(array $cacheConfigurations): void
try {
$cache = $this->getCache('weakbit__fallback_cache');
if (!$cache instanceof VariableFrontend) {
throw new InvalidCacheException('Cache must be an instance of VariableFrontend');
throw new InvalidCacheException('Cache must be an instance of VariableFrontend', 1736962058);
}

$status = $cache->get('status');
if (is_array($status)) {
static::$status = $status;
foreach ($status as $identifier => $state) {
if (is_string($identifier) && $state instanceof StatusEnum) {
static::$status[$identifier] = $state;
}
}
}
} catch (Throwable $throwable) {
$this->logger?->error($throwable->getMessage());
Expand All @@ -68,11 +67,11 @@ public function setCacheConfigurations(array $cacheConfigurations): void

public function getCache($identifier): FrontendInterface
{
// could set to red during runtie of this(!) process
// could set to red during runtime of this(!) process
if (!isset(static::$status[$identifier]) || static::$status[$identifier] !== StatusEnum::RED) {
// can be null e.g. if the class was not found.
/** @var FrontendInterface|null $cache */
$cache = parent::getCache($identifier);
$cache = @parent::getCache($identifier);
if ($cache) {
return $cache;
}
Expand All @@ -81,14 +80,14 @@ public function getCache($identifier): FrontendInterface
try {
$fallback = $this->getFallbackCacheOf($identifier);
if (null === $fallback) {
throw new NoFallbackFoundException('No fallback found for ' . $identifier);
throw new NoFallbackFoundException('No fallback found for ' . $identifier, 5859365252);
}

$cache = $this->getCache($this->fallbacks[$identifier]);
} catch (RecursiveFallbackCacheException | NoFallbackFoundException $exception) {
$this->logger?->error($exception->getMessage());
$chain = $this->getBreadcrumb($identifier);
throw new RuntimeException('Could not instanciate cache using the chain ' . $chain, $exception->getCode(), $exception);
throw new RuntimeException('Could not create cache using the chain ' . $chain, $exception->getCode(), $exception);
}

return $cache;
Expand All @@ -112,8 +111,8 @@ private function getBreadcrumb(string $identifier, string $breadcrumb = ''): str

public function addCacheStatus(string $identifier, StatusEnum $status): void
{
// do not overwrite the highest state
if (isset(static::$status[$identifier]) && static::$status[$identifier] === StatusEnum::RED) {
// Always set RED, never overwrite RED with YELLOW, but always allow GREEN
if (isset(static::$status[$identifier]) && (static::$status[$identifier] === StatusEnum::RED && $status === StatusEnum::YELLOW)) {
return;
}

Expand All @@ -134,11 +133,10 @@ protected function createCache($identifier): void

try {
parent::createCache($identifier);
} catch (InvalidCacheException | InvalidBackendException | RecursiveFallbackCacheException $exception) {
} catch (InvalidCacheException | InvalidBackendException $exception) {
throw $exception;
} catch (Throwable $throwable) {
$eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
assert($eventDispatcher instanceof EventDispatcherInterface);
$eventDispatcher->dispatch(new CacheStatusEvent(StatusEnum::RED, $identifier, $throwable));
$this->createCacheWithFallback($identifier);
}
Expand Down Expand Up @@ -168,9 +166,11 @@ private function createCacheWithFallback(string $identifier): void
return;
}

if (!$this->hasCache($fallback)) {
if (!isset($this->caches[$fallback])) {
$this->createCache($fallback);
}

$this->caches[$identifier] = $this->caches[$fallback];
}

public function getFallbackCacheOf(string $identifier): ?string
Expand All @@ -191,7 +191,7 @@ public function getFallbackCacheOf(string $identifier): ?string
}

/**
* registers all fallback caches in the chain to prevent endess loops
* registers all fallback caches in the chain to prevent endless loops
*/
private function isSeen(?string $fallback): bool
{
Expand Down
12 changes: 0 additions & 12 deletions Classes/Cache/CacheStatusInterface.php

This file was deleted.

15 changes: 11 additions & 4 deletions Classes/Cache/Frontend/VariableFrontend.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

/** @noinspection PhpUnused */

declare(strict_types=1);

namespace Weakbit\FallbackCache\Cache\Frontend;
Expand All @@ -22,11 +24,17 @@ class VariableFrontend implements FrontendInterface
*/
public function __construct(private readonly string $identifier, BackendInterface $backend)
{
// Need to get directly as the CacheManager is no fully compilant here (on cli)
// Need to get directly as the CacheManager is no fully compliant here (on cli)
// @phpstan-ignore-next-line
if (!isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations'][$this->identifier])) {
throw new Exception('No cache configuration found for identifier ' . $this->identifier, 5511139720);
}

$configuration = $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations'][$this->identifier];
$concreteClassName = $configuration['conrete_frontend'] ?? $configuration['frontend'] ?? \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class;
assert(is_array($configuration));
$concreteClassName = $configuration['concrete_frontend'] ?? $configuration['frontend'] ?? \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class;
if (!is_string($concreteClassName)) {
throw new Exception('Invalid concrete frontend class name');
throw new Exception('Invalid concrete frontend class name', 5511139721);
}

/** @var class-string $concreteClassName */
Expand Down Expand Up @@ -129,7 +137,6 @@ public function get($entryIdentifier)
private function handle(Throwable|Exception $exception): void
{
$eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
assert($eventDispatcher instanceof EventDispatcherInterface);
$eventDispatcher->dispatch(new CacheStatusEvent(StatusEnum::YELLOW, $this->identifier, $exception));
}

Expand Down
1 change: 1 addition & 0 deletions Classes/Enum/StatusEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ enum StatusEnum: string
{
case RED = 'red';
case YELLOW = 'yellow';
case GREEN = 'green';
}
7 changes: 7 additions & 0 deletions Classes/EventListener/CacheStatusEventListener.php
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
<?php

/** @noinspection PhpUnused */

declare(strict_types=1);

namespace Weakbit\FallbackCache\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Cache\CacheManager;
use Weakbit\FallbackCache\Event\CacheStatusEvent;

#[AsEventListener(
identifier: \Weakbit\FallbackCache\EventListener\CacheStatusEventListener::class,
event: CacheStatusEvent::class,
)]
class CacheStatusEventListener
{
public function __invoke(CacheStatusEvent $event): void
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
<?php

/** @noinspection PhpUnused */

declare(strict_types=1);

namespace Weakbit\FallbackCache\EventListener;

use TYPO3\CMS\Backend\Backend\Event\SystemInformationToolbarCollectorEvent;
use TYPO3\CMS\Backend\Toolbar\Enumeration\InformationStatus;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException;
use TYPO3\CMS\Core\Cache\Frontend\VariableFrontend;
Expand All @@ -15,6 +18,10 @@
/**
* displays some information about caches in the system information toolbar
*/
#[AsEventListener(
identifier: \Weakbit\FallbackCache\EventListener\SystemInformationToolbarCollectorEventListener::class,
event: SystemInformationToolbarCollectorEvent::class,
)]
class SystemInformationToolbarCollectorEventListener
{
public function __invoke(SystemInformationToolbarCollectorEvent $event): void
Expand Down
28 changes: 28 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
ACT_IMAGE ?= efrecon/act:v0.2.80
WORKDIR ?= /work
UID_GID := $(shell id -u):$(shell id -g)
DOCKER_GID := $(shell stat -c '%g' /var/run/docker.sock)

PLATFORM ?= -P ubuntu-latest=ghcr.io/catthehacker/ubuntu:act-24.04

# Secrets-File in ENV format
SECRETS ?= --secret-file .secrets

ACT_ARGS ?= \
--artifact-server-path /home/act/.cache/artifacts-fallback-cache

define DOCKER_RUN
docker run --rm -it \
-u $(UID_GID) \
--group-add $(DOCKER_GID) \
-e HOME=/home/act \
-e XDG_CACHE_HOME=/home/act/.cache \
-e ACT_CACHE_DIR=/home/act/.cache/actcache \
-v /var/run/docker.sock:/var/run/docker.sock \
-v $(PWD):$(WORKDIR) -w $(WORKDIR) \
-v $$HOME/.cache:/home/act/.cache \
$(ACT_IMAGE)
endef

ci:
$(DOCKER_RUN) $(PLATFORM) $(SECRETS) $(ACT_ARGS)
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ In addition, the Cache itself has a new Interface it can implement and tell this

After the fallback period the cache on the primary system is outdated and has to be cleared!

# Recommendation

It is recommended to set
```PHP
$GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][CacheManager::class] = [ 'className' => \Weakbit\FallbackCache\Cache\CacheManager::class ];
```
in additional.php or the equivalent settings.php file.

This ensures the override is applied early and reliably, avoiding issues with loading order or race conditions that can occur if set in extension files like ext_localconf.php.

## Example
This defines a pages cache with the fallback cache: pages_fallback.

Expand All @@ -29,7 +39,7 @@ $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages'] =
// If the cache creation fails (Status red) this cache is used
'fallback' => 'pages_fallback',
// The concrete frontend the 'frontend' is based on
'conrete_frontend' => \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class,
'concrete_frontend' => \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class,
'groups' => [
'pages',
]
Expand Down
Loading