Skip to content
Open
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
14 changes: 10 additions & 4 deletions Classes/Authentication/FrontendUserGroupInjector.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
use TYPO3\CMS\Frontend\Authentication\ModifyResolvedFrontendGroupsEvent;

/**
* Magic logic to add user groups injected into $this->>info['alwaysActiveGroups']
* Resolves the frontend user groups a warmup request should be rendered for,
* based on the group ids the FrontendRequestBuilder attached to the request.
*/
class FrontendUserGroupInjector
{
Expand All @@ -27,16 +28,21 @@ public function __construct(protected LoggerInterface $logger, protected Connect
public function frontendUserGroupModifier(ModifyResolvedFrontendGroupsEvent $event): void
{
$simulationData = $event->getRequest()->getAttribute('b13/warmup');
if (!is_array($simulationData)) {
if (!is_array($simulationData) || !is_array($simulationData['simulateFrontendUserGroupIds'] ?? null)) {
$this->logger->info(self::class . ' was activated, but no user groups were set');
return;
}
$userGroups = $this->fetchGroupsFromDatabase($simulationData['simulateFrontendUserGroupIds']);
$event->setGroups($userGroups);
$event->setGroups($this->fetchGroupsFromDatabase($simulationData['simulateFrontendUserGroupIds']));
}

/**
* @param int[] $groupUids
*/
private function fetchGroupsFromDatabase(array $groupUids): array
{
if ($groupUids === []) {
return [];
}
$groupRecords = [];
$this->logger->debug('Get usergroups with id: ' . implode(',', $groupUids));
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('fe_groups');
Expand Down
49 changes: 26 additions & 23 deletions Classes/Command/WarmupCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,29 @@

use B13\Warmup\Service\PageWarmupService;
use B13\Warmup\Service\RootlineWarmupService;
use B13\Warmup\Service\WarmupServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
* Called via cache:warmup
* Called via cache:warmupPages
*/
class WarmupCommand extends Command
{
private SymfonyStyle $io;

public function __construct(protected PageWarmupService $pageWarmupService, protected RootlineWarmupService $rootlineWarmupService, ?string $name = null)
{
public function __construct(
protected PageWarmupService $pageWarmupService,
protected RootlineWarmupService $rootlineWarmupService,
?string $name = null
) {
parent::__construct($name);
}

public function configure(): void
protected function configure(): void
{
$this
->addArgument(
Expand All @@ -48,37 +52,36 @@ protected function initialize(InputInterface $input, OutputInterface $output): v
$this->io = new SymfonyStyle($input, $output);
}

/**
* @inheritdoc
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->io->title('Welcome to the Cache Warmup');

$type = $input->getArgument('type');
$type = (string)$input->getArgument('type');
$services = $this->getWarmupServices($type);
if ($services === []) {
$this->io->error('Unknown type "' . $type . '", use one of "all", "rootline" or "pages".');
return Command::INVALID;
}

foreach ($this->getWarmupService($type) as $specificType => $service) {
foreach ($services as $specificType => $service) {
$this->io->section('Warming up ' . $specificType);
call_user_func_array([$service, 'warmUp'], [$this->io]);
$service->warmUp($this->io);
}

$this->io->success('All done');
return Command::SUCCESS;
}

private function getWarmupService(string $type): iterable
/**
* @return array<string, WarmupServiceInterface>
*/
private function getWarmupServices(string $type): array
{
switch ($type) {
case 'all':
yield 'rootline' => $this->rootlineWarmupService;
yield 'pages' => $this->pageWarmupService;
break;
case 'rootline':
yield 'rootline' => $this->rootlineWarmupService;
break;
case 'pages':
yield 'pages' => $this->pageWarmupService;
break;
}
return match ($type) {
'all' => ['rootline' => $this->rootlineWarmupService, 'pages' => $this->pageWarmupService],
'rootline' => ['rootline' => $this->rootlineWarmupService],
'pages' => ['pages' => $this->pageWarmupService],
default => [],
};
}
}
18 changes: 12 additions & 6 deletions Classes/FrontendRequestBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,31 @@ class FrontendRequestBuilder
{
public function __construct(protected Application $application, protected LoggerInterface $logger) {}

public function buildRequestForPage(UriInterface $uri, $frontendUserGroups = []): void
/**
* @param int[] $frontendUserGroups
*/
public function buildRequestForPage(UriInterface $uri, array $frontendUserGroups = []): void
{
$serverParams = [
'SCRIPT_NAME' => '/index.php',
'HTTP_HOST' => $uri->getHost(),
'HTTP_HOST' => $uri->getHost(),
'SERVER_NAME' => $uri->getHost(),
'HTTPS' => $uri->getScheme() === 'https' ? 'on' : 'off',
'REMOTE_ADDR' => '127.0.0.1',
];
$headers = [];
$serverRequest = new ServerRequest($uri, 'GET', null, $headers, $serverParams);
$serverRequest = new ServerRequest($uri, 'GET', null, [], $serverParams);
$serverRequest = $serverRequest->withAttribute('normalizedParams', NormalizedParams::createFromRequest($serverRequest));
$serverRequest = $serverRequest->withAttribute('b13/warmup', [
'simulateFrontendUserGroupIds' => $frontendUserGroups,
]);
try {
$this->application->handle($serverRequest);
} catch (\Exception $e) {
$this->logger->error('cannot fetch url ' . (string)$uri);
} catch (\Throwable $e) {
$this->logger->error('cannot fetch url {url}: {message}', [
'url' => (string)$uri,
'message' => $e->getMessage(),
'exception' => $e,
]);
}
}
}
57 changes: 30 additions & 27 deletions Classes/Service/PageWarmupService.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,34 @@

use B13\Warmup\FrontendRequestBuilder;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\ParameterType;
use Psr\Http\Message\UriInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\RootlineUtility;

class PageWarmupService
class PageWarmupService implements WarmupServiceInterface
{
/**
* Page types that never render a frontend page on their own.
*/
private const EXCLUDED_DOKTYPES = [
PageRepository::DOKTYPE_LINK,
PageRepository::DOKTYPE_SHORTCUT,
PageRepository::DOKTYPE_BE_USER_SECTION,
PageRepository::DOKTYPE_MOUNTPOINT,
PageRepository::DOKTYPE_SPACER,
PageRepository::DOKTYPE_SYSFOLDER,
// "Recycler", removed as a constant in v13 and migrated to DOKTYPE_BE_USER_SECTION,
// kept as a literal so v12 installations still skip these pages
255,
];

private SymfonyStyle $io;

public function __construct(
Expand All @@ -42,24 +55,15 @@ public function warmUp(SymfonyStyle $io): void
$this->io = $io;

// fetch all pages which are not deleted and in live workspace and not one of excluded types
$excludeDocTypes = [
PageRepository::DOKTYPE_LINK,
PageRepository::DOKTYPE_SHORTCUT,
PageRepository::DOKTYPE_BE_USER_SECTION,
PageRepository::DOKTYPE_MOUNTPOINT,
PageRepository::DOKTYPE_SPACER,
PageRepository::DOKTYPE_SYSFOLDER,
PageRepository::DOKTYPE_RECYCLER,
];
$queryBuilder = $this->connectionPool
->getQueryBuilderForTable('pages');
$queryBuilder->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class))
->add(GeneralUtility::makeInstance(HiddenRestriction::class))
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
->add(new WorkspaceRestriction())
->add(new HiddenRestriction())
->add(new DeletedRestriction());
$statement = $queryBuilder->select('*')->from('pages')->where(
$queryBuilder->expr()->notIn('doktype', $queryBuilder->createNamedParameter($excludeDocTypes, ArrayParameterType::INTEGER))
$queryBuilder->expr()->notIn('doktype', $queryBuilder->createNamedParameter(self::EXCLUDED_DOKTYPES, ArrayParameterType::INTEGER))
)->executeQuery();

$io->writeln('Starting to request pages at ' . date('d.m.Y H:i:s'));
Expand All @@ -68,17 +72,17 @@ public function warmUp(SymfonyStyle $io): void
while ($pageRecord = $statement->fetchAssociative()) {
try {
$languageUid = (int)$pageRecord['sys_language_uid'];
$pageUid = $pageRecord['uid'];
$pageUid = (int)$pageRecord['uid'];
if ($languageUid > 0) {
$pageUid = $pageRecord['l10n_parent'];
$pageUid = (int)$pageRecord['l10n_parent'];
}
$site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($pageUid);
$site = $this->siteFinder->getSiteByPageId($pageUid);
$siteLanguage = $site->getLanguageById($languageUid);
$url = $site->getRouter()->generateUri($pageUid, ['_language' => $siteLanguage]);
$this->executeRequestForPageRecord($url, $pageRecord);
$requestedPages++;
} catch (SiteNotFoundException $e) {
$io->error('Cache for Page ID ' . $pageRecord['uid'] . ' could not be warmed up');
} catch (\Throwable $e) {
$io->error('Cache for Page ID ' . $pageRecord['uid'] . ' could not be warmed up: ' . $e->getMessage());
}
}

Expand All @@ -94,16 +98,15 @@ protected function executeRequestForPageRecord(UriInterface $url, array $pageRec

protected function resolveRequestedUserGroupsForPage(array $pageRecord): array
{
$userGroups = $pageRecord['fe_group'];
$rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageRecord['uid'])->get();
$userGroups = (string)($pageRecord['fe_group'] ?? '');
$rootLine = GeneralUtility::makeInstance(RootlineUtility::class, (int)$pageRecord['uid'])->get();
foreach ($rootLine as $pageInRootLine) {
if ($pageInRootLine['extendToSubpages']) {
$userGroups .= ',' . $pageInRootLine['fe_group'];
if ($pageInRootLine['extendToSubpages'] ?? false) {
$userGroups .= ',' . (string)($pageInRootLine['fe_group'] ?? '');
}
}
$userGroups = GeneralUtility::intExplode(',', $userGroups, true);
$userGroups = array_filter($userGroups);
$userGroups = array_unique($userGroups);
return $userGroups;
return array_unique($userGroups);
}
}
32 changes: 18 additions & 14 deletions Classes/Service/RootlineWarmupService.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\RootlineUtility;

class RootlineWarmupService
class RootlineWarmupService implements WarmupServiceInterface
{
public function __construct(protected ConnectionPool $connectionPool, protected LoggerInterface $logger) {}
public function __construct(
protected ConnectionPool $connectionPool,
protected Context $context,
protected LoggerInterface $logger
) {}

public function warmUp(SymfonyStyle $io): void
{
Expand All @@ -34,29 +38,29 @@ public function warmUp(SymfonyStyle $io): void
->getQueryBuilderForTable('pages');
$queryBuilder->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class))
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
->add(new WorkspaceRestriction())
->add(new DeletedRestriction());
$statement = $queryBuilder->select('*')->from('pages')->executeQuery();
while ($pageRecord = $statement->fetchAssociative()) {
try {
$this->buildRootLineForPage($pageRecord);
} catch (\RuntimeException $e) {
$io->error('Rootline Cache for Page ID ' . $pageRecord['uid'] . ' could not be warmed up');
} catch (\Throwable $e) {
$io->error('Rootline Cache for Page ID ' . $pageRecord['uid'] . ' could not be warmed up: ' . $e->getMessage());
}
}
}

protected function buildRootLineForPage(array $pageRecord): void
{
$context = clone GeneralUtility::makeInstance(Context::class);
$context = clone $this->context;
$context->setAspect('visibility', new VisibilityAspect(false, false, false, false));
$pageUid = $pageRecord['uid'];
if ($pageRecord['sys_language_uid'] > 0) {
$context->setAspect('language', new LanguageAspect($pageRecord['sys_language_uid']));
$pageUid = $pageRecord['l10n_parent'];
$pageUid = (int)$pageRecord['uid'];
$languageUid = (int)($pageRecord['sys_language_uid'] ?? 0);
if ($languageUid > 0) {
$context->setAspect('language', new LanguageAspect($languageUid));
$pageUid = (int)$pageRecord['l10n_parent'];
}
$rootlineUtility = GeneralUtility::makeInstance(RootlineUtility::class, $pageUid, '', $context);
$this->logger->debug('buildRootLine', ['pageUid' => $pageRecord['uid'], 'cacheIdentifier' => $rootlineUtility->getCacheIdentifier($pageUid)]);
$rootlineUtility->get();
$this->logger->debug('buildRootLine', ['pageUid' => $pageRecord['uid']]);
GeneralUtility::makeInstance(RootlineUtility::class, $pageUid, '', $context)->get();
}
}
20 changes: 20 additions & 0 deletions Classes/Service/WarmupServiceInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace B13\Warmup\Service;

/*
* This file is part of TYPO3 CMS-based extension "warmup" by b13.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*/

use Symfony\Component\Console\Style\SymfonyStyle;

interface WarmupServiceInterface
{
public function warmUp(SymfonyStyle $io): void;
}
Loading