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

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

permissions:
contents: read

jobs:
test:
name: "Test (PHP ${{ matrix.php-versions }}, Neos ${{ matrix.neos-versions }})"

strategy:
fail-fast: false
matrix:
php-versions: ['8.2', '8.3', '8.4', '8.5']
neos-versions: ['8.3','8.4']

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Validate composer.json and composer.lock
run: composer validate --strict

- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v3
with:
path: vendor
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extensions: mbstring, xml, json, zlib, iconv, intl, pdo_sqlite
ini-values: date.timezone="Africa/Tunis", opcache.fast_shutdown=0, apc.enable_cli=on

- name: Set Neos Version
run: composer require neos/neos ^${{ matrix.neos-versions }} --no-progress --no-interaction

- name: Install dependencies
run: composer install --prefer-dist --no-progress

- name: Run test suite
run: composer test
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
composer.lock
Packages
vendor
15 changes: 4 additions & 11 deletions Classes/Aspects/ContentCacheAspect.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,7 @@
*/
class ContentCacheAspect
{
private $hadUncachedSegments = false;

private $cacheTags = [];

/**
* @var null|int
*/
private $shortestLifetime = null;
private bool $hadUncachedSegments = false;

/**
* @Flow\Inject
Expand All @@ -31,7 +24,7 @@ class ContentCacheAspect
/**
* @Flow\Before("method(Neos\Fusion\Core\Cache\ContentCache->(createUncachedSegment)())")
*/
public function grabUncachedSegment(JoinPointInterface $joinPoint)
public function grabUncachedSegment(JoinPointInterface $joinPoint): void
{
$this->hadUncachedSegments = true;
}
Expand All @@ -45,7 +38,7 @@ public function grabUncachedSegment(JoinPointInterface $joinPoint)
*
* @throws \Neos\Utility\Exception\PropertyNotAccessibleException
*/
public function interceptLegacyNodeCacheFlush(JoinPointInterface $joinPoint)
public function interceptLegacyNodeCacheFlush(JoinPointInterface $joinPoint): void
{
$object = $joinPoint->getProxy();

Expand All @@ -60,7 +53,7 @@ public function interceptLegacyNodeCacheFlush(JoinPointInterface $joinPoint)
*
* @throws \Neos\Utility\Exception\PropertyNotAccessibleException
*/
public function interceptNodeCacheFlush(JoinPointInterface $joinPoint)
public function interceptNodeCacheFlush(JoinPointInterface $joinPoint): void
{
$tags = $joinPoint->getMethodArgument('tagsToFlush');
$tags = array_map([$this, 'sanitizeTag'], array_keys($tags));
Expand Down
25 changes: 15 additions & 10 deletions Classes/Cache/MetadataAwareStringFrontend.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

namespace Flowpack\FullPageCache\Cache;

use Neos\Cache\Frontend\StringFrontend;
Expand All @@ -18,7 +19,7 @@ class MetadataAwareStringFrontend extends StringFrontend
/**
* Store metadata of all loaded cache entries indexed by identifier
*
* @var array
* @var array<string, array{identifier:string, tags: string[], lifetime: int|null}>
*/
protected $metadata = [];

Expand All @@ -38,6 +39,10 @@ class MetadataAwareStringFrontend extends StringFrontend
* Set a cache entry and store additional metadata (tags and lifetime)
*
* {@inheritdoc}
*
* @param string $content
* @param string[] $tags
* @return void
*/
public function set(string $entryIdentifier, $content, array $tags = [], ?int $lifetime = null)
{
Expand All @@ -47,6 +52,8 @@ public function set(string $entryIdentifier, $content, array $tags = [], ?int $l

/**
* {@inheritdoc}
*
* @return string|false
*/
public function get(string $entryIdentifier)
{
Expand All @@ -60,6 +67,7 @@ public function get(string $entryIdentifier)

/**
* {@inheritdoc}
* @return array<string,string>
*/
public function getByTag(string $tag): array
{
Expand All @@ -76,16 +84,12 @@ public function getByTag(string $tag): array
*
* @param string $content
* @param string $entryIdentifier The identifier metadata
* @param array $tags The tags metadata
* @param string[] $tags The tags metadata
* @param integer $lifetime The lifetime metadata
* @return string The content including the serialized metadata
* @throws InvalidDataTypeException
*/
protected function insertMetadata($content, $entryIdentifier, array $tags, $lifetime)
protected function insertMetadata(string $content, string $entryIdentifier, array $tags, ?int $lifetime)
{
if (!is_string($content)) {
throw new InvalidDataTypeException('Given data is of type "' . gettype($content) . '", but a string is expected for string cache.', 1433155737);
}
$metadata = [
'identifier' => $entryIdentifier,
'tags' => $tags,
Expand All @@ -105,7 +109,7 @@ protected function insertMetadata($content, $entryIdentifier, array $tags, $life
* @return string The content without metadata
* @throws InvalidDataTypeException
*/
protected function extractMetadata($entryIdentifier, $content)
protected function extractMetadata($entryIdentifier, $content): string
{
$separatorIndex = strpos($content, self::SEPARATOR);
if ($separatorIndex === false) {
Expand All @@ -115,6 +119,7 @@ protected function extractMetadata($entryIdentifier, $content)
} else {
throw $exception;
}
return $content;
}

$metadataJson = substr($content, 0, $separatorIndex);
Expand All @@ -134,9 +139,9 @@ protected function extractMetadata($entryIdentifier, $content)
}

/**
* @return array Metadata of all loaded entries (indexed by identifier)
* @return array<string, array{identifier:string, tags?: string[], lifetime?: int|null}> Metadata of all loaded entries (indexed by identifier)
*/
public function getAllMetadata()
public function getAllMetadata(): array
{
return $this->metadata;
}
Expand Down
16 changes: 11 additions & 5 deletions Classes/Middleware/FusionAutoconfigurationMiddleware.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<?php

declare(strict_types=1);

namespace Flowpack\FullPageCache\Middleware;

use Flowpack\FullPageCache\Domain\Dto\FusionCacheInformation;
use Neos\Flow\Annotations as Flow;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
Expand Down Expand Up @@ -47,7 +49,10 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
$response = $response->withoutHeader(self::HEADER_ENABLED);
}

list($hasUncachedSegments, $tags, $lifetime) = $this->getFusionCacheInformations();
$cacheMetadata = $this->getFusionCacheInformations();
$hasUncachedSegments = $cacheMetadata['hasUncachedSegments'];
$tags = $cacheMetadata['tags'];
$lifetime = $cacheMetadata['lifetime'];

if ($response->hasHeader('Set-Cookie') || $hasUncachedSegments) {
return $response;
Expand All @@ -61,23 +66,24 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
->withHeader(RequestCacheMiddleware::HEADER_TAGS, $tags);
}


if ($lifetime) {
$response = $response
->withHeader(RequestCacheMiddleware::HEADER_LIFTIME, $lifetime);
->withHeader(RequestCacheMiddleware::HEADER_LIFETIME, (string)$lifetime);
}

return $response;
}

/**
* Get cache tags and lifetime from the cache metadata that was extracted by the special cache frontend for content cache
*
* @return array with first "hasUncachedSegments", "tags" and "lifetime"
* @return array{hasUncachedSegments:bool, tags:string[], lifetime: ?int}
*/
public function getFusionCacheInformations(): array
{
$lifetime = null;
$tags = [];

$entriesMetadata = $this->contentCache->getAllMetadata();
foreach ($entriesMetadata as $identifier => $metadata) {
$entryTags = isset($metadata['tags']) ? $metadata['tags'] : [];
Expand All @@ -93,6 +99,6 @@ public function getFusionCacheInformations(): array
}
$hasUncachedSegments = $this->contentCacheAspect->hasUncachedSegments();

return [$hasUncachedSegments, $tags, $lifetime];
return ['hasUncachedSegments' => $hasUncachedSegments, 'tags' => $tags, 'lifetime' => $lifetime];
}
}
37 changes: 26 additions & 11 deletions Classes/Middleware/RequestCacheMiddleware.php
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
<?php

declare(strict_types=1);

namespace Flowpack\FullPageCache\Middleware;

use Flowpack\FullPageCache\Domain\Dto\CacheEntry;
use Neos\Flow\Annotations as Flow;
use Neos\Cache\Frontend\VariableFrontend;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use GuzzleHttp\Psr7\Message;
use function GuzzleHttp\Psr7\str;

/**
* @phpstan-type CacheEntryShape array{timestamp:int, response:string}
*/
class RequestCacheMiddleware implements MiddlewareInterface
{
public const HEADER_ENABLED = 'X-FullPageCache-Enabled';

public const HEADER_INFO = 'X-FullPageCache-Info';

// @deprecated use HEADER_LIFETIME instead
public const HEADER_LIFTIME = 'X-FullPageCache-Lifetime';
public const HEADER_LIFETIME = 'X-FullPageCache-Lifetime';

public const HEADER_TAGS = 'X-FullPageCache-Tags';

Expand All @@ -35,29 +41,35 @@ class RequestCacheMiddleware implements MiddlewareInterface
protected $cacheFrontend;

/**
* @var array
* @var string[]
* @Flow\InjectConfiguration(path="request.queryParams.allow")
*/
protected $allowedQueryParams;

/**
* @var array
* @var string[]
* @Flow\InjectConfiguration(path="request.queryParams.ignore")
*/
protected $ignoredQueryParams;

/**
* @var array
* @var string[]
* @Flow\InjectConfiguration(path="request.cookieParams.ignore")
*/
protected $ignoredCookieParams;

/**
* @var boolean
* @var int
* @Flow\InjectConfiguration(path="maxPublicCacheTime")
*/
protected $maxPublicCacheTime;

/**
* @var int
* @Flow\InjectConfiguration(path="maxSharedCacheTime")
*/
protected $maxSharedCacheTime;

public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
{
if (!$this->enabled) {
Expand All @@ -70,22 +82,24 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
return $next->handle($request)->withHeader(self::HEADER_INFO, 'SKIP');
}

if ($cacheEntry = $this->cacheFrontend->get($entryIdentifier)) {
/** @var ?CacheEntryShape $cacheEntry */
$cacheEntry = $this->cacheFrontend->get($entryIdentifier);
if ($cacheEntry) {
$age = time() - $cacheEntry['timestamp'];
$response = Message::parseResponse($cacheEntry['response']);
return $response
->withHeader('Age', $age)
->withHeader('Age', (string)$age)
->withHeader(self::HEADER_INFO, 'HIT: ' . $entryIdentifier);
}

$response = $next->handle($request->withHeader(self::HEADER_ENABLED, ''));

if ($response->hasHeader(self::HEADER_ENABLED)) {
$lifetime = $response->hasHeader(self::HEADER_LIFTIME) ? (int)$response->getHeaderLine(self::HEADER_LIFTIME) : null;
$lifetime = $response->hasHeader(self::HEADER_LIFETIME) ? (int)$response->getHeaderLine(self::HEADER_LIFETIME) : null;
$tags = $response->hasHeader(self::HEADER_TAGS) ? $response->getHeader(self::HEADER_TAGS) : [];
$response = $response
->withoutHeader(self::HEADER_ENABLED)
->withoutHeader(self::HEADER_LIFTIME)
->withoutHeader(self::HEADER_LIFETIME)
->withoutHeader(self::HEADER_TAGS);

$publicLifetime = 0;
Expand All @@ -105,8 +119,9 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
->withHeader('Cache-Control', 'public, max-age=' . $publicLifetime);
}

$this->cacheFrontend->set($entryIdentifier,[ 'timestamp' => time(), 'response' => Message::toString($response) ], $tags, $lifetime);
$response->getBody()->rewind();
/** @var CacheEntryShape $cacheEntry */
$cacheEntry = [ 'timestamp' => time(), 'response' => Message::toString($response) ];
$this->cacheFrontend->set($entryIdentifier, $cacheEntry, $tags, $lifetime);
return $response->withHeader(self::HEADER_INFO, 'MISS: ' . $entryIdentifier);
}

Expand Down
17 changes: 17 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
"neos/neos": "^8.0 || ^9.0 || dev-master",
"guzzlehttp/psr7": "^1.7, !=1.8.0 || ~2.0"
},
"require-dev": {
"phpstan/phpstan": "^2.2",
"squizlabs/php_codesniffer": "^3.7"
},
"autoload": {
"psr-4": {
"Flowpack\\FullPageCache\\": "Classes/"
Expand All @@ -16,5 +20,18 @@
"neos": {
"package-key": "Flowpack.FullPageCache"
}
},
"scripts": {
"fix:style": "phpcbf --colors --standard=PSR12 Classes",
"test:style": "phpcs --colors -n --standard=PSR12 Classes",
"test:stan": "phpstan analyse Classes",
"cc": "phpstan clear cache",
"fix": ["composer fix:style"],
"test": ["composer test:style" , "composer test:stan"]
},
"config": {
"allow-plugins": {
"neos/composer-plugin": true
}
}
}
5 changes: 5 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
parameters:
level: 8
paths:
- Classes
reportUnmatchedIgnoredErrors: false
Loading