diff --git a/.gitattributes b/.gitattributes
index 854f8bc..21900d7 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,5 +1,6 @@
+/Build/ export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
-/.travis.yml export-ignore
-/phpunit.xml.dist export-ignore
/Tests export-ignore
+/.github export-ignore
+
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..dbb2de8
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,52 @@
+name: CI
+
+on: [push, pull_request]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ TYPO3: [ '14' ]
+ php: [ '8.2', '8.5']
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Set up PHP Version
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ tools: composer:v2
+
+ - name: Start MySQL
+ run: sudo /etc/init.d/mysql start
+
+ - name: Validate composer.json and composer.lock
+ run: composer validate
+
+ - name: Cache dependencies
+ uses: actions/cache@v4
+ with:
+ path: ~/.composer/cache
+ key: dependencies-composer-${{ hashFiles('composer.json') }}
+
+ - name: Install composer dependencies
+ run: |
+ composer install --no-progress --no-interaction
+ - name: Phpstan
+ run: vendor/bin/phpstan analyze -c Build/phpstan.neon
+ - name: Phpcsfix
+ run: vendor/bin/php-cs-fixer fix --config=Build/php-cs-fixer.php --dry-run --stop-on-violation --using-cache=no
+ - name: Unit Tests
+ run: |
+ vendor/bin/phpunit -c Build/phpunit/UnitTests.xml Tests/Unit
+ - name: Functional Tests
+ run: |
+ export typo3DatabaseName="typo3";
+ export typo3DatabaseHost="127.0.0.1";
+ export typo3DatabaseUsername="root";
+ export typo3DatabasePassword="root";
+ vendor/bin/phpunit -c Build/phpunit/FunctionalTests.xml Tests/Functional
diff --git a/.gitignore b/.gitignore
index 342d335..2862eaf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
-Build
composer.lock
+Build/phpunit/.phpunit.result.cache
vendor
public
-.phpunit.result.cache
+.php-cs-fixer.cache
diff --git a/Build/php-cs-fixer.php b/Build/php-cs-fixer.php
new file mode 100644
index 0000000..c699c6f
--- /dev/null
+++ b/Build/php-cs-fixer.php
@@ -0,0 +1,11 @@
+getFinder()->exclude(['var', 'public', 'vendor'])->in(__DIR__ . '/..');
+$config->addRules([
+ 'nullable_type_declaration' => [
+ 'syntax' => 'question_mark',
+ ],
+ 'nullable_type_declaration_for_default_null_value' => true,
+]);
+return $config;
diff --git a/Build/phpstan.neon b/Build/phpstan.neon
new file mode 100644
index 0000000..a7bd078
--- /dev/null
+++ b/Build/phpstan.neon
@@ -0,0 +1,5 @@
+parameters:
+ level: 5
+ paths:
+ - %currentWorkingDirectory%/Classes
+ - %currentWorkingDirectory%/Tests
diff --git a/Build/phpunit/FunctionalTests.xml b/Build/phpunit/FunctionalTests.xml
new file mode 100644
index 0000000..79f6e2e
--- /dev/null
+++ b/Build/phpunit/FunctionalTests.xml
@@ -0,0 +1,24 @@
+
+
+
+ ../../Tests/Functional/
+
+
+
+
+
+
+
diff --git a/Build/phpunit/FunctionalTestsBootstrap.php b/Build/phpunit/FunctionalTestsBootstrap.php
new file mode 100644
index 0000000..29a4cbb
--- /dev/null
+++ b/Build/phpunit/FunctionalTestsBootstrap.php
@@ -0,0 +1,21 @@
+defineOriginalRootPath();
+ $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/tests');
+ $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/transient');
+});
diff --git a/Build/phpunit/UnitTests.xml b/Build/phpunit/UnitTests.xml
new file mode 100644
index 0000000..30215c6
--- /dev/null
+++ b/Build/phpunit/UnitTests.xml
@@ -0,0 +1,24 @@
+
+
+
+ ../../Tests/Unit/
+
+
+
+
+
+
+
diff --git a/Build/phpunit/UnitTestsBootstrap.php b/Build/phpunit/UnitTestsBootstrap.php
new file mode 100644
index 0000000..5760477
--- /dev/null
+++ b/Build/phpunit/UnitTestsBootstrap.php
@@ -0,0 +1,68 @@
+getWebRoot(), '/'));
+ }
+ if (!getenv('TYPO3_PATH_WEB')) {
+ putenv('TYPO3_PATH_WEB=' . rtrim($testbase->getWebRoot(), '/'));
+ }
+
+ $testbase->defineSitePath();
+
+ $requestType = \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_BE | \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_CLI;
+ \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::run(0, $requestType);
+
+ $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3conf/ext');
+ $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3temp/assets');
+ $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3temp/var/tests');
+ $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3temp/var/transient');
+
+ // Retrieve an instance of class loader and inject to core bootstrap
+ $classLoader = require $testbase->getPackagesPath() . '/autoload.php';
+ \TYPO3\CMS\Core\Core\Bootstrap::initializeClassLoader($classLoader);
+
+ // Initialize default TYPO3_CONF_VARS
+ $configurationManager = new \TYPO3\CMS\Core\Configuration\ConfigurationManager();
+ $GLOBALS['TYPO3_CONF_VARS'] = $configurationManager->getDefaultConfiguration();
+
+ $cache = new \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend(
+ 'core',
+ new \TYPO3\CMS\Core\Cache\Backend\NullBackend([])
+ );
+ // Set all packages to active
+ $packageManager = \TYPO3\CMS\Core\Core\Bootstrap::createPackageManager(\TYPO3\CMS\Core\Package\UnitTestPackageManager::class, \TYPO3\CMS\Core\Core\Bootstrap::createPackageCache($cache));
+
+ \TYPO3\CMS\Core\Utility\GeneralUtility::setSingletonInstance(\TYPO3\CMS\Core\Package\PackageManager::class, $packageManager);
+ \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::setPackageManager($packageManager);
+
+ $testbase->dumpClassLoadingInformation();
+
+ \TYPO3\CMS\Core\Utility\GeneralUtility::purgeInstances();
+})();
diff --git a/Build/sites/main/config.yaml b/Build/sites/main/config.yaml
new file mode 100644
index 0000000..f1cfaaa
--- /dev/null
+++ b/Build/sites/main/config.yaml
@@ -0,0 +1,20 @@
+base: 'http://localhost/'
+baseVariants: { }
+errorHandling: { }
+languages:
+ -
+ title: english
+ enabled: true
+ base: /
+ typo3Language: default
+ locale: en_US.UTF-8
+ iso-639-1: en
+ navigationTitle: ''
+ hreflang: ''
+ direction: ''
+ flag: global
+ languageId: '0'
+ websiteTitle: ''
+rootPageId: 1
+routes: { }
+websiteTitle: 'Testing'
diff --git a/Classes/Http/ResourcePusher.php b/Classes/Http/ResourcePusher.php
index dbc6bc3..37fa713 100644
--- a/Classes/Http/ResourcePusher.php
+++ b/Classes/Http/ResourcePusher.php
@@ -16,9 +16,11 @@
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use TYPO3\CMS\Core\Cache\CacheDataCollector;
+use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Http\NormalizedParams;
use TYPO3\CMS\Core\Utility\PathUtility;
-use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
/**
* Takes existing accumulated resources and pushes them as HTTP2 headers as middleware.
@@ -30,15 +32,27 @@
*/
class ResourcePusher implements MiddlewareInterface
{
+ public function __construct(
+ #[Autowire(service: 'cache.tx_http2')]
+ private readonly FrontendInterface $cache
+ ) {}
+
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request);
- /** @var TypoScriptFrontendController $frontendController */
- $frontendController = $request->getAttribute('frontend.controller');
- $resources = $frontendController->config['b13/http2'] ?? null;
+ $response = $response->withHeader('foo', 'bar');
+
+ /** @var CacheDataCollector $cacheDataCollector */
+ $cacheDataCollector = $request->getAttribute('frontend.cache.collector');
+ $identifier = $cacheDataCollector->getPageCacheIdentifier();
+ $resources = [];
+ if ($this->cache->has($identifier)) {
+ $resources = $this->cache->get($identifier);
+ }
+
/** @var NormalizedParams $normalizedParams */
$normalizedParams = $request->getAttribute('normalizedParams');
- if (is_array($resources) && $normalizedParams->isHttps()) {
+ if (!empty($resources) && $normalizedParams->isHttps()) {
foreach ($resources['scripts'] ?? [] as $resource) {
$response = $this->addPreloadHeaderToResponse($response, $resource, 'script');
}
diff --git a/Classes/PageRendererHook.php b/Classes/PageRendererHook.php
index b74fc61..8628655 100644
--- a/Classes/PageRendererHook.php
+++ b/Classes/PageRendererHook.php
@@ -13,10 +13,14 @@
*/
use Psr\Http\Message\ServerRequestInterface;
+use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use TYPO3\CMS\Core\Cache\CacheDataCollector;
+use TYPO3\CMS\Core\Cache\CacheTag;
+use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Http\ApplicationType;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
/**
* Hooks into PageRenderer before the rendering is taken care of, and remember the files
@@ -24,9 +28,14 @@
*
* This considers that everything is required, thus is marked as "preload", not via "prefetch".
*/
+#[Autoconfigure(public: true)]
class PageRendererHook
{
- public function __construct(protected ResourceMatcher $matcher) {}
+ public function __construct(
+ #[Autowire(service: 'cache.tx_http2')]
+ private readonly FrontendInterface $cache,
+ protected ResourceMatcher $matcher
+ ) {}
/**
* @param array $params
@@ -40,10 +49,8 @@ public function accumulateResources(array $params, PageRenderer $pageRenderer)
return;
}
// If this is a second run (non-cached cObjects adding more data), then the existing cached data is fetched
- if (ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()) {
- /** @var TypoScriptFrontendController $frontendController */
- $frontendController = $request->getAttribute('frontend.controller');
- $allResources = $frontendController->config['b13/http2'] ?? [];
+ if (ApplicationType::fromRequest($request)->isFrontend()) {
+ $allResources = $this->getFromCached($request);
} else {
$allResources = [];
}
@@ -74,16 +81,35 @@ public function accumulateResources(array $params, PageRenderer $pageRenderer)
*/
protected function process(array $allResources, ServerRequestInterface $request): void
{
- if (ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()) {
- /** @var TypoScriptFrontendController $frontendController */
- $frontendController = $request->getAttribute('frontend.controller');
- $frontendController->config['b13/http2'] = $allResources;
+ if (ApplicationType::fromRequest($request)->isFrontend()) {
+ $this->addToCached($request, $allResources);
} elseif (GeneralUtility::getIndpEnv('TYPO3_SSL')) {
// Push directly into the TYPO3 Backend, but only if TYPO3 is running in SSL
GeneralUtility::makeInstance(ResourcePusher::class)->pushAll($allResources);
}
}
+ protected function getFromCached(ServerRequestInterface $request): array
+ {
+ /** @var CacheDataCollector $cacheDataCollector */
+ $cacheDataCollector = $request->getAttribute('frontend.cache.collector');
+ $identifier = $cacheDataCollector->getPageCacheIdentifier();
+ if ($this->cache->has($identifier)) {
+ return $this->cache->get($identifier);
+ }
+ return [];
+ }
+
+ protected function addToCached(ServerRequestInterface $request, array $data): void
+ {
+ /** @var CacheDataCollector $cacheDataCollector */
+ $cacheDataCollector = $request->getAttribute('frontend.cache.collector');
+ $identifier = $cacheDataCollector->getPageCacheIdentifier();
+ $cacheTags = array_map(fn(CacheTag $cacheTag) => $cacheTag->name, $cacheDataCollector->getCacheTags());
+ $cacheTimeout = $cacheDataCollector->resolveLifetime();
+ $this->cache->set($identifier, $data, $cacheTags, $cacheTimeout);
+ }
+
protected function getRequest(): ?ServerRequestInterface
{
return $GLOBALS['TYPO3_REQUEST'] ?? null;
diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml
index b8227c0..9e45e73 100644
--- a/Configuration/Services.yaml
+++ b/Configuration/Services.yaml
@@ -7,5 +7,7 @@ services:
B13\Http2\:
resource: '../Classes/*'
- B13\Http2\PageRendererHook:
- public: true
+ cache.tx_http2:
+ class: TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
+ factory: ['@TYPO3\CMS\Core\Cache\CacheManager', 'getCache']
+ arguments: ['tx_http2']
\ No newline at end of file
diff --git a/Resources/Public/Icons/Extension.svg b/Resources/Public/Icons/Extension.svg
index 9496dfd..98fbc6b 100644
--- a/Resources/Public/Icons/Extension.svg
+++ b/Resources/Public/Icons/Extension.svg
@@ -1,35 +1 @@
-
-
+
\ No newline at end of file
diff --git a/Tests/Functional/Fixtures/Response.csv b/Tests/Functional/Fixtures/Response.csv
new file mode 100644
index 0000000..b039862
--- /dev/null
+++ b/Tests/Functional/Fixtures/Response.csv
@@ -0,0 +1,9 @@
+"pages"
+,"uid","pid","title","slug"
+,1,0,"root","/"
+"sys_template"
+,"uid","pid","root","config"
+,1,1,1,"page = PAGE
+page.10 = TEXT
+page.includeJS.dummy-js = EXT:frontend/Resources/Public/JavaScript/default_frontend.js
+"
diff --git a/Tests/Functional/ResponseTest.php b/Tests/Functional/ResponseTest.php
new file mode 100644
index 0000000..e878870
--- /dev/null
+++ b/Tests/Functional/ResponseTest.php
@@ -0,0 +1,40 @@
+ 'typo3conf/sites'];
+
+ #[Test]
+ public function linkHeaderExists(): void
+ {
+ $this->importCSVDataSet(__DIR__ . '/Fixtures/Response.csv');
+ $request = new InternalRequest('http://localhost/');
+ $request = $request->withServerParams(['HTTPS' => 'on']);
+ $response = $this->executeFrontendSubRequest($request);
+ self::assertTrue($response->hasHeader('link'));
+ $link = $response->getHeaderLine('link');
+ self::assertStringContainsString('JavaScript/default_frontend.js', $link);
+ self::assertStringContainsString('>; rel=preload; as=script', $link);
+ // cached
+ $response = $this->executeFrontendSubRequest($request);
+ self::assertTrue($response->hasHeader('link'));
+ $link = $response->getHeaderLine('link');
+ self::assertStringContainsString('JavaScript/default_frontend.js', $link);
+ self::assertStringContainsString('>; rel=preload; as=script', $link);
+ }
+}
diff --git a/composer.json b/composer.json
index c22e615..066af7e 100644
--- a/composer.json
+++ b/composer.json
@@ -3,12 +3,15 @@
"type": "typo3-cms-extension",
"description": "Speed up TYPO3 rendering via HTTP/2 Server Push",
"require": {
- "typo3/cms-core": "^12.4 || ^13.4",
- "typo3/cms-frontend": "^12.4 || ^13.4"
+ "typo3/cms-core": "^14.1",
+ "typo3/cms-frontend": "^14.1"
},
"require-dev": {
"phpunit/phpunit": "^11.0",
- "squizlabs/php_codesniffer": "^3.0"
+ "friendsofphp/php-cs-fixer": "^3.94",
+ "phpstan/phpstan": "^2.1",
+ "typo3/testing-framework": "^9.1",
+ "typo3/coding-standards": "^0.8.0"
},
"homepage": "https://b13.com",
"license": ["GPL-2.0-or-later"],
@@ -32,11 +35,6 @@
"B13\\Http2\\Tests\\": "Tests"
}
},
- "scripts": {
- "test": "phpunit",
- "check-style": "phpcs -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 Classes Tests",
- "fix-style": "phpcbf -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 Classes Tests"
- },
"config": {
"allow-plugins": {
"typo3/class-alias-loader": true,
diff --git a/ext_localconf.php b/ext_localconf.php
index 083208c..8e1879c 100644
--- a/ext_localconf.php
+++ b/ext_localconf.php
@@ -1,4 +1,9 @@
accumulateResources';
+
+if (!is_array($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['tx_http2'] ?? null)) {
+ $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['tx_http2'] = ['groups' => ['pages']];
+}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
deleted file mode 100644
index 490682e..0000000
--- a/phpunit.xml.dist
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
- Tests
-
-
-
-
- Classes/
-
-
-
-
-
-
-
-
-
-