diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..68f2fff --- /dev/null +++ b/.github/workflows/build.yml @@ -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'] + 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: 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 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..2b70e20 100644 --- a/Classes/Aspects/ContentCacheAspect.php +++ b/Classes/Aspects/ContentCacheAspect.php @@ -1,4 +1,5 @@ (createUncachedSegment)())") */ - public function grabUncachedSegment(JoinPointInterface $joinPoint) + public function grabUncachedSegment(JoinPointInterface $joinPoint): void { $this->hadUncachedSegments = true; } @@ -41,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 59c7ace..c990767 100644 --- a/Classes/Cache/MetadataAwareStringFrontend.php +++ b/Classes/Cache/MetadataAwareStringFrontend.php @@ -1,4 +1,5 @@ */ protected $metadata = []; @@ -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) { @@ -47,6 +52,8 @@ public function set(string $entryIdentifier, $content, array $tags = [], int $li /** * {@inheritdoc} + * + * @return string|false */ public function get(string $entryIdentifier) { @@ -60,6 +67,7 @@ public function get(string $entryIdentifier) /** * {@inheritdoc} + * @return array */ public function getByTag(string $tag): array { @@ -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, @@ -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) { @@ -115,6 +119,7 @@ protected function extractMetadata($entryIdentifier, $content) } else { throw $exception; } + return $content; } $metadataJson = substr($content, 0, $separatorIndex); @@ -134,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_LIFTIME, $lifetime); + ->withHeader(RequestCacheMiddleware::HEADER_LIFETIME, (string)$cacheMetadata->lifetime); } return $response; @@ -71,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 = []; @@ -93,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 dd9c6dc..246d01f 100644 --- a/Classes/Middleware/RequestCacheMiddleware.php +++ b/Classes/Middleware/RequestCacheMiddleware.php @@ -1,8 +1,10 @@ cacheFrontend->get($entryIdentifier)) { - $age = time() - $cacheEntry['timestamp']; - $response = Message::parseResponse($cacheEntry['response']); - return $response - ->withHeader('Age', $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, '')); 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; @@ -105,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/composer.json b/composer.json index c541c5b..6068c74 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,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 + } } } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..175248e --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: 8 + paths: + - Classes + reportUnmatchedIgnoredErrors: false