diff --git a/Classes/Authentication/FrontendUserGroupInjector.php b/Classes/Authentication/FrontendUserGroupInjector.php index 7c518b0..83f6205 100644 --- a/Classes/Authentication/FrontendUserGroupInjector.php +++ b/Classes/Authentication/FrontendUserGroupInjector.php @@ -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 { @@ -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'); diff --git a/Classes/Command/WarmupCommand.php b/Classes/Command/WarmupCommand.php index 6ee9fd3..462ade8 100644 --- a/Classes/Command/WarmupCommand.php +++ b/Classes/Command/WarmupCommand.php @@ -14,6 +14,7 @@ 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; @@ -21,18 +22,21 @@ 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( @@ -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 + */ + 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 => [], + }; } } diff --git a/Classes/FrontendRequestBuilder.php b/Classes/FrontendRequestBuilder.php index 27afc90..76b5076 100644 --- a/Classes/FrontendRequestBuilder.php +++ b/Classes/FrontendRequestBuilder.php @@ -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, + ]); } } } diff --git a/Classes/Service/PageWarmupService.php b/Classes/Service/PageWarmupService.php index f9e869b..56aab65 100644 --- a/Classes/Service/PageWarmupService.php +++ b/Classes/Service/PageWarmupService.php @@ -14,7 +14,6 @@ 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; @@ -22,13 +21,27 @@ 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( @@ -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')); @@ -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()); } } @@ -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); } } diff --git a/Classes/Service/RootlineWarmupService.php b/Classes/Service/RootlineWarmupService.php index b02bbf3..8308f7b 100644 --- a/Classes/Service/RootlineWarmupService.php +++ b/Classes/Service/RootlineWarmupService.php @@ -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 { @@ -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(); } } diff --git a/Classes/Service/WarmupServiceInterface.php b/Classes/Service/WarmupServiceInterface.php new file mode 100644 index 0000000..f8ef2e6 --- /dev/null +++ b/Classes/Service/WarmupServiceInterface.php @@ -0,0 +1,20 @@ + 'b13 GmbH', 'author_email' => 'typo3@b13.com', 'state' => 'stable', - 'version' => '2.0.0', + 'version' => '3.0.0', 'constraints' => [ 'depends' => [ - 'typo3' => '12.0.0-13.4.99', + 'typo3' => '12.4.0-14.3.99', ], 'conflicts' => [ ], diff --git a/ext_localconf.php b/ext_localconf.php deleted file mode 100644 index 2cb2f79..0000000 --- a/ext_localconf.php +++ /dev/null @@ -1,18 +0,0 @@ - 'Add Frontend Groups based on CLI Request Builder', - 'description' => 'Adds frontend usergroups by verifying data from the Frontend Request Builder.', - 'subtype' => 'getGroupsFE', - 'available' => false, - 'priority' => 90, - 'quality' => 90, - 'className' => \B13\Warmup\Authentication\FrontendUserGroupInjector::class, - ] -);