From 16a42c5fbf4b7d954d94611e1b7afaa1ca0a9833 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Sat, 28 Dec 2024 14:21:19 +0100 Subject: [PATCH 1/4] TASK: Fix typo in constant HEADER_LIFETIME The old name of the constant HEADER_LIFTIME is still supported as it costs nothing to keep this here. --- Classes/Middleware/FusionAutoconfigurationMiddleware.php | 2 +- Classes/Middleware/RequestCacheMiddleware.php | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Classes/Middleware/FusionAutoconfigurationMiddleware.php b/Classes/Middleware/FusionAutoconfigurationMiddleware.php index f06d087..87baacb 100644 --- a/Classes/Middleware/FusionAutoconfigurationMiddleware.php +++ b/Classes/Middleware/FusionAutoconfigurationMiddleware.php @@ -63,7 +63,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface if ($lifetime) { $response = $response - ->withHeader(RequestCacheMiddleware::HEADER_LIFTIME, $lifetime); + ->withHeader(RequestCacheMiddleware::HEADER_LIFETIME, $lifetime); } return $response; diff --git a/Classes/Middleware/RequestCacheMiddleware.php b/Classes/Middleware/RequestCacheMiddleware.php index dd9c6dc..38d60ce 100644 --- a/Classes/Middleware/RequestCacheMiddleware.php +++ b/Classes/Middleware/RequestCacheMiddleware.php @@ -19,6 +19,7 @@ class RequestCacheMiddleware implements MiddlewareInterface public const HEADER_INFO = 'X-FullPageCache-Info'; public const HEADER_LIFTIME = 'X-FullPageCache-Lifetime'; + public const HEADER_LIFETIME = 'X-FullPageCache-Lifetime'; public const HEADER_TAGS = 'X-FullPageCache-Tags'; @@ -81,11 +82,11 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $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; From aea163d4c571de6a067349f4e701b03721cee250 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Sat, 28 Dec 2024 14:52:04 +0100 Subject: [PATCH 2/4] TASK: Add PSR12 and PhpStan analysis --- .github/workflows/build.yml | 46 +++++++++++++++++++ .gitignore | 3 ++ Classes/Aspects/ContentCacheAspect.php | 1 + Classes/Cache/MetadataAwareStringFrontend.php | 1 + .../FusionAutoconfigurationMiddleware.php | 1 + Classes/Middleware/RequestCacheMiddleware.php | 4 +- composer.json | 16 +++++++ 7 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..408fe23 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,46 @@ +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'] + neos-versions: ['8.3'] + + 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: 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2cb1bd8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +composer.lock +Packages +vendor diff --git a/Classes/Aspects/ContentCacheAspect.php b/Classes/Aspects/ContentCacheAspect.php index 2049dd7..b32dfda 100644 --- a/Classes/Aspects/ContentCacheAspect.php +++ b/Classes/Aspects/ContentCacheAspect.php @@ -1,4 +1,5 @@ withHeader('Cache-Control', 'public, max-age=' . $publicLifetime); } - $this->cacheFrontend->set($entryIdentifier,[ 'timestamp' => time(), 'response' => Message::toString($response) ], $tags, $lifetime); + $this->cacheFrontend->set($entryIdentifier, [ 'timestamp' => time(), 'response' => Message::toString($response) ], $tags, $lifetime); $response->getBody()->rewind(); return $response->withHeader(self::HEADER_INFO, 'MISS: ' . $entryIdentifier); } diff --git a/composer.json b/composer.json index c541c5b..9c9968a 100644 --- a/composer.json +++ b/composer.json @@ -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": "^1.8", + "squizlabs/php_codesniffer": "^3.7" + }, "autoload": { "psr-4": { "Flowpack\\FullPageCache\\": "Classes/" @@ -16,5 +20,17 @@ "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", + "test": ["composer test:style" , "composer test:stan"] + }, + "config": { + "allow-plugins": { + "neos/composer-plugin": true + } } } From 4dda1346ea8cd5a2ffa756977dbd3c1d2af5fd1f Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Sat, 28 Dec 2024 16:49:06 +0100 Subject: [PATCH 3/4] TASK: PhpStan level 5 --- Classes/Aspects/ContentCacheAspect.php | 7 ------- Classes/Middleware/RequestCacheMiddleware.php | 4 ++-- composer.json | 1 + phpstan.neon | 5 +++++ 4 files changed, 8 insertions(+), 9 deletions(-) create mode 100644 phpstan.neon diff --git a/Classes/Aspects/ContentCacheAspect.php b/Classes/Aspects/ContentCacheAspect.php index b32dfda..19d8402 100644 --- a/Classes/Aspects/ContentCacheAspect.php +++ b/Classes/Aspects/ContentCacheAspect.php @@ -15,13 +15,6 @@ class ContentCacheAspect { private $hadUncachedSegments = false; - private $cacheTags = []; - - /** - * @var null|int - */ - private $shortestLifetime = null; - /** * @Flow\Inject * @var StringFrontend diff --git a/Classes/Middleware/RequestCacheMiddleware.php b/Classes/Middleware/RequestCacheMiddleware.php index cbc6b86..f3b6e49 100644 --- a/Classes/Middleware/RequestCacheMiddleware.php +++ b/Classes/Middleware/RequestCacheMiddleware.php @@ -54,7 +54,7 @@ class RequestCacheMiddleware implements MiddlewareInterface protected $ignoredCookieParams; /** - * @var boolean + * @var int * @Flow\InjectConfiguration(path="maxPublicCacheTime") */ protected $maxPublicCacheTime; @@ -75,7 +75,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $age = time() - $cacheEntry['timestamp']; $response = Message::parseResponse($cacheEntry['response']); return $response - ->withHeader('Age', $age) + ->withHeader('Age', (string)$age) ->withHeader(self::HEADER_INFO, 'HIT: ' . $entryIdentifier); } diff --git a/composer.json b/composer.json index 9c9968a..6068c74 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "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": { diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..eda5c2a --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: 5 + paths: + - Classes + reportUnmatchedIgnoredErrors: false From f3a885e1c58dace442d9a63211ce57e1bc93c8fa Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Sat, 28 Dec 2024 17:49:18 +0100 Subject: [PATCH 4/4] TASK: PhpStan Level 8 --- .github/workflows/build.yml | 7 ++++ Classes/Aspects/ContentCacheAspect.php | 6 ++-- Classes/Cache/MetadataAwareStringFrontend.php | 24 +++++++------ Classes/Domain/Dto/CacheEntry.php | 34 +++++++++++++++++++ Classes/Domain/Dto/FusionCacheInformation.php | 20 +++++++++++ .../FusionAutoconfigurationMiddleware.php | 20 +++++------ Classes/Middleware/RequestCacheMiddleware.php | 19 +++++------ phpstan.neon | 2 +- 8 files changed, 98 insertions(+), 34 deletions(-) create mode 100644 Classes/Domain/Dto/CacheEntry.php create mode 100644 Classes/Domain/Dto/FusionCacheInformation.php diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 408fe23..68f2fff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,6 +36,13 @@ jobs: 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 diff --git a/Classes/Aspects/ContentCacheAspect.php b/Classes/Aspects/ContentCacheAspect.php index 19d8402..2b70e20 100644 --- a/Classes/Aspects/ContentCacheAspect.php +++ b/Classes/Aspects/ContentCacheAspect.php @@ -13,7 +13,7 @@ */ class ContentCacheAspect { - private $hadUncachedSegments = false; + private bool $hadUncachedSegments = false; /** * @Flow\Inject @@ -24,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; } @@ -35,7 +35,7 @@ public function grabUncachedSegment(JoinPointInterface $joinPoint) * * @throws \Neos\Utility\Exception\PropertyNotAccessibleException */ - public function interceptNodeCacheFlush(JoinPointInterface $joinPoint) + public function interceptNodeCacheFlush(JoinPointInterface $joinPoint): void { $object = $joinPoint->getProxy(); diff --git a/Classes/Cache/MetadataAwareStringFrontend.php b/Classes/Cache/MetadataAwareStringFrontend.php index 2abd0ea..c990767 100644 --- a/Classes/Cache/MetadataAwareStringFrontend.php +++ b/Classes/Cache/MetadataAwareStringFrontend.php @@ -19,7 +19,7 @@ class MetadataAwareStringFrontend extends StringFrontend /** * Store metadata of all loaded cache entries indexed by identifier * - * @var array + * @var array */ protected $metadata = []; @@ -39,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) { @@ -48,6 +52,8 @@ public function set(string $entryIdentifier, $content, array $tags = [], int $li /** * {@inheritdoc} + * + * @return string|false */ public function get(string $entryIdentifier) { @@ -61,6 +67,7 @@ public function get(string $entryIdentifier) /** * {@inheritdoc} + * @return array */ public function getByTag(string $tag): array { @@ -77,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, @@ -106,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) { @@ -116,6 +119,7 @@ protected function extractMetadata($entryIdentifier, $content) } else { throw $exception; } + return $content; } $metadataJson = substr($content, 0, $separatorIndex); @@ -135,9 +139,9 @@ protected function extractMetadata($entryIdentifier, $content) } /** - * @return array Metadata of all loaded entries (indexed by identifier) + * @return array Metadata of all loaded entries (indexed by identifier) */ - public function getAllMetadata() + public function getAllMetadata(): array { return $this->metadata; } diff --git a/Classes/Domain/Dto/CacheEntry.php b/Classes/Domain/Dto/CacheEntry.php new file mode 100644 index 0000000..e1d860f --- /dev/null +++ b/Classes/Domain/Dto/CacheEntry.php @@ -0,0 +1,34 @@ +getBody()->rewind(); + + return new self( + time(), + $responseAsString + ); + } + + public function getResponse(): ResponseInterface + { + return Message::parseResponse($this->responseAsString) + ->withHeader('Age', (string)(time() - $this->timestamp)); + } +} diff --git a/Classes/Domain/Dto/FusionCacheInformation.php b/Classes/Domain/Dto/FusionCacheInformation.php new file mode 100644 index 0000000..8d082cc --- /dev/null +++ b/Classes/Domain/Dto/FusionCacheInformation.php @@ -0,0 +1,20 @@ +withoutHeader(self::HEADER_ENABLED); } - list($hasUncachedSegments, $tags, $lifetime) = $this->getFusionCacheInformations(); + $cacheMetadata = $this->getFusionCacheInformations(); - if ($response->hasHeader('Set-Cookie') || $hasUncachedSegments) { + if ($response->hasHeader('Set-Cookie') || $cacheMetadata->hasUncachedSegments) { return $response; } $response = $response ->withHeader(RequestCacheMiddleware::HEADER_ENABLED, ""); - if ($tags) { + if ($cacheMetadata->tags) { $response = $response - ->withHeader(RequestCacheMiddleware::HEADER_TAGS, $tags); + ->withHeader(RequestCacheMiddleware::HEADER_TAGS, $cacheMetadata->tags); } - if ($lifetime) { + + if ($cacheMetadata->lifetime) { $response = $response - ->withHeader(RequestCacheMiddleware::HEADER_LIFETIME, $lifetime); + ->withHeader(RequestCacheMiddleware::HEADER_LIFETIME, (string)$cacheMetadata->lifetime); } return $response; @@ -72,10 +74,8 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface /** * 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" */ - public function getFusionCacheInformations(): array + public function getFusionCacheInformations(): FusionCacheInformation { $lifetime = null; $tags = []; @@ -94,6 +94,6 @@ public function getFusionCacheInformations(): array } $hasUncachedSegments = $this->contentCacheAspect->hasUncachedSegments(); - return [$hasUncachedSegments, $tags, $lifetime]; + return new FusionCacheInformation($hasUncachedSegments, $tags, $lifetime); } } diff --git a/Classes/Middleware/RequestCacheMiddleware.php b/Classes/Middleware/RequestCacheMiddleware.php index f3b6e49..246d01f 100644 --- a/Classes/Middleware/RequestCacheMiddleware.php +++ b/Classes/Middleware/RequestCacheMiddleware.php @@ -4,6 +4,7 @@ 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; @@ -36,19 +37,19 @@ 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; @@ -72,11 +73,10 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface } if ($cacheEntry = $this->cacheFrontend->get($entryIdentifier)) { - $age = time() - $cacheEntry['timestamp']; - $response = Message::parseResponse($cacheEntry['response']); - return $response - ->withHeader('Age', (string)$age) - ->withHeader(self::HEADER_INFO, 'HIT: ' . $entryIdentifier); + if ($cacheEntry instanceof CacheEntry) { + return $cacheEntry->getResponse() + ->withHeader(self::HEADER_INFO, 'HIT: ' . $entryIdentifier); + } } $response = $next->handle($request->withHeader(self::HEADER_ENABLED, '')); @@ -106,8 +106,7 @@ 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(); + $this->cacheFrontend->set($entryIdentifier, CacheEntry::createFromResponse($response), $tags, $lifetime); return $response->withHeader(self::HEADER_INFO, 'MISS: ' . $entryIdentifier); } diff --git a/phpstan.neon b/phpstan.neon index eda5c2a..175248e 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,5 @@ parameters: - level: 5 + level: 8 paths: - Classes reportUnmatchedIgnoredErrors: false