• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

eliashaeussler / typo3-warming / 14534307929

18 Apr 2025 11:15AM UTC coverage: 91.827% (-0.2%) from 92.065%
14534307929

Pull #769

github

eliashaeussler
[!!!][TASK] Drop support for PHP 8.1
Pull Request #769: [!!!][FEATURE] Require `eliashaeussler/cache-warmup` ^4.0

85 of 90 new or added lines in 12 files covered. (94.44%)

2 existing lines in 1 file now uncovered.

1191 of 1297 relevant lines covered (91.83%)

9.29 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.43
/Classes/Command/WarmupCommand.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of the TYPO3 CMS extension "warming".
7
 *
8
 * Copyright (C) 2021-2025 Elias Häußler <elias@haeussler.dev>
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, either version 2 of the License, or
13
 * (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
 * GNU General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU General Public License
21
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22
 */
23

24
namespace EliasHaeussler\Typo3Warming\Command;
25

26
use EliasHaeussler\CacheWarmup;
27
use EliasHaeussler\Typo3SitemapLocator;
28
use EliasHaeussler\Typo3Warming\Configuration;
29
use EliasHaeussler\Typo3Warming\Domain;
30
use EliasHaeussler\Typo3Warming\Http;
31
use Psr\EventDispatcher;
32
use Symfony\Component\Console;
33
use TYPO3\CMS\Core;
34

35
/**
36
 * WarmupCommand
37
 *
38
 * @author Elias Häußler <elias@haeussler.dev>
39
 * @license GPL-2.0-or-later
40
 */
41
#[Console\Attribute\AsCommand(
42
    name: 'warming:cachewarmup',
43
    description: 'Warm up Frontend caches of single pages and/or whole sites using their XML sitemaps.',
44
)]
45
final class WarmupCommand extends Console\Command\Command
46
{
47
    private const ALL_LANGUAGES = -1;
48
    private const ALL_SITES = 'all';
49

50
    public function __construct(
15✔
51
        private readonly Configuration\Configuration $configuration,
52
        private readonly CacheWarmup\Crawler\Strategy\CrawlingStrategyFactory $crawlingStrategyFactory,
53
        private readonly Typo3SitemapLocator\Sitemap\SitemapLocator $sitemapLocator,
54
        private readonly Domain\Repository\SiteRepository $siteRepository,
55
        private readonly Domain\Repository\SiteLanguageRepository $siteLanguageRepository,
56
        private readonly EventDispatcher\EventDispatcherInterface $eventDispatcher,
57
        private readonly Core\Package\PackageManager $packageManager,
58
        private readonly Http\Message\PageUriBuilder $pageUriBuilder,
59
    ) {
60
        parent::__construct();
15✔
61
    }
62

63
    protected function configure(): void
15✔
64
    {
65
        $v = fn(mixed $value) => $value;
15✔
66
        $decoratedCrawlingStrategies = \implode(PHP_EOL, array_map(
15✔
67
            static fn(string $strategy) => '  │  * <info>' . $strategy . '</info>',
15✔
68
            $this->crawlingStrategyFactory->getAll(),
15✔
69
        ));
15✔
70

71
        $crawlingStrategy = $this->configuration->getStrategy();
15✔
72
        if ($crawlingStrategy !== null) {
15✔
NEW
73
            $crawlingStrategy = $crawlingStrategy::getName();
×
74
        }
75

76
        $this->setDescription('Warm up Frontend caches of single pages and/or whole sites using their XML sitemaps.');
15✔
77
        $this->setHelp(
15✔
78
            <<<HELP
15✔
79
This command can be used in many ways to warm up frontend caches.
15✔
80
Some possible combinations and options are shown below.
81

82
<info>Sites and pages</info>
83
<info>===============</info>
84

85
To warm up caches, either <info>pages</info> or <info>sites</info> can be specified.
86
Both types can also be combined or extended by the specification of one or more <info>languages</info>.
87
If you omit the language option, the caches of all languages of the requested pages and sites
88
will be warmed up.
89

90
You can also use the special keyword <info>all</info> for <info>sites</info>.
91
This will cause all available sites to be warmed up.
92

93
Examples:
94

95
* <comment>warming:cachewarmup -p 1,2,3</comment>
96
  ├─ Pages: <info>1, 2 and 3</info>
97
  └─ Languages: <info>all</info>
98

99
* <comment>warming:cachewarmup -s 1</comment>
100
* <comment>warming:cachewarmup -s main</comment>
101
  ├─ Sites: <info>Root page ID 1</info> or <info>identifier "main"</info>
102
  └─ Languages: <info>all</info>
103

104
* <comment>warming:cachewarmup -p 1 -s 1</comment>
105
* <comment>warming:cachewarmup -p 1 -s main</comment>
106
  ├─ Pages: <info>1</info>
107
  ├─ Sites: <info>Root page ID 1</info> or <info>identifier "main"</info>
108
  └─ Languages: <info>all</info>
109

110
* <comment>warming:cachewarmup -s 1 -l 0,1</comment>
111
  ├─ Sites: <info>Root page ID 1</info> or <info>identifier "main"</info>
112
  └─ Languages: <info>0 and 1</info>
113

114
* <comment>warming:cachewarmup -s all</comment>
115
  ├─ Sites: <info>all</info>
116
  └─ Languages: <info>all</info>
117

118
<info>Additional options</info>
119
<info>==================</info>
120

121
* <comment>Configuration file</comment>
122
  ├─ A preconfigured set of configuration options can be written to a configuration file.
123
  │  This file can be passed using the <info>--config</info> option.
124
  │  It may also contain extension paths, e.g. <info>EXT:sitepackage/Configuration/cache-warmup.json</info>.
125
  │  The following file formats are currently supported:
126
  │  * <info>json</info>
127
  │  * <info>php</info>
128
  │  * <info>yaml</info>
129
  │  * <info>yml</info>
130
  └─ Example: <comment>warming:cachewarmup --config path/to/cache-warmup.json</comment>
131

132
* <comment>Strict mode</comment>
133
  ├─ You can pass the <info>--strict</info> (or <info>-x</info>) option to terminate execution with an error code
134
  │  if individual caches warm up incorrectly.
135
  │  This is especially useful for automated execution of cache warmups.
136
  ├─ Default: <info>false</info>
137
  └─ Example: <comment>warming:cachewarmup -s 1 --strict</comment>
138

139
* <comment>Crawl limit</comment>
140
  ├─ The maximum number of pages to be warmed up can be defined via the extension configuration <info>limit</info>.
141
  │  It can be overridden by using the <info>--limit</info> option.
142
  │  The value <info>0</info> deactivates the crawl limit.
143
  ├─ Default: <info>{$v($this->configuration->getLimit())}</info>
15✔
144
  ├─ Example: <comment>warming:cachewarmup -s 1 --limit 100</comment> (limits crawling to 100 pages)
145
  └─ Example: <comment>warming:cachewarmup -s 1 --limit 0</comment> (no limit)
146

147
* <comment>Crawling strategy</comment>
148
  ├─ A crawling strategy defines how URLs will be crawled, e.g. by sorting them by a specific property.
149
  │  It can be defined via the extension configuration <info>strategy</info> or by using the <info>--strategy</info> option.
150
  │  The following strategies are currently available:
151
{$decoratedCrawlingStrategies}
15✔
152
  ├─ Default: <info>{$v($crawlingStrategy ?? 'none')}</info>
15✔
153
  └─ Example: <comment>warming:cachewarmup --strategy {$v(CacheWarmup\Crawler\Strategy\SortByPriorityStrategy::getName())}</comment>
15✔
154

155
* <comment>Format output</comment>
156
  ├─ By default, all user-oriented output is printed as plain text to the console.
157
  │  However, you can use other formatters by using the <info>--format</info> (or <info>-f</info>) option.
158
  ├─ Default: <info>{$v(CacheWarmup\Formatter\TextFormatter::getType())}</info>
15✔
159
  ├─ Example: <comment>warming:cachewarmup --format {$v(CacheWarmup\Formatter\TextFormatter::getType())}</comment> (normal output as plaintext)
15✔
160
  └─ Example: <comment>warming:cachewarmup --format {$v(CacheWarmup\Formatter\JsonFormatter::getType())}</comment> (displays output as JSON)
15✔
161

162
<info>Crawling configuration</info>
163
<info>======================</info>
164

165
* <comment>Alternative crawler</comment>
166
  ├─ Use the extension configuration <info>verboseCrawler</info> to use an alternative crawler for
167
  │  command-line requests. For warmup requests triggered via the TYPO3 backend, you can use the
168
  │  extension configuration <info>crawler</info>.
169
  ├─ Currently used default crawler: <info>{$v($this->configuration->getCrawler()::class)}</info>
15✔
170
  └─ Currently used verbose crawler: <info>{$v($this->configuration->getVerboseCrawler()::class)}</info>
15✔
171

172
* <comment>Custom User-Agent header</comment>
173
  ├─ When the default crawler is used, each warmup request is executed with a special User-Agent header.
174
  │  This header is generated from the encryption key of the TYPO3 installation.
175
  │  It can be used, for example, to exclude warmup requests from your search statistics.
176
  └─ Current User-Agent: <info>{$v($this->configuration->getUserAgent())}</info>
15✔
177
HELP
15✔
178
        );
15✔
179

180
        $this->addOption(
15✔
181
            'pages',
15✔
182
            'p',
15✔
183
            Console\Input\InputOption::VALUE_REQUIRED | Console\Input\InputOption::VALUE_IS_ARRAY,
15✔
184
            'Pages whose Frontend caches are to be warmed up.',
15✔
185
        );
15✔
186
        $this->addOption(
15✔
187
            'sites',
15✔
188
            's',
15✔
189
            Console\Input\InputOption::VALUE_REQUIRED | Console\Input\InputOption::VALUE_IS_ARRAY,
15✔
190
            'Site identifiers or root page IDs of sites whose caches are to be warmed up.',
15✔
191
        );
15✔
192
        $this->addOption(
15✔
193
            'languages',
15✔
194
            'l',
15✔
195
            Console\Input\InputOption::VALUE_REQUIRED | Console\Input\InputOption::VALUE_IS_ARRAY,
15✔
196
            'Optional identifiers of languages for which caches are to be warmed up.',
15✔
197
        );
15✔
198
        $this->addOption(
15✔
199
            'config',
15✔
200
            'c',
15✔
201
            Console\Input\InputOption::VALUE_REQUIRED,
15✔
202
            'Path to optional configuration file',
15✔
203
        );
15✔
204
        $this->addOption(
15✔
205
            'limit',
15✔
206
            null,
15✔
207
            Console\Input\InputOption::VALUE_REQUIRED,
15✔
208
            'Maximum number of pages to be crawled. Set to <info>0</info> to disable the limit.',
15✔
209
            $this->configuration->getLimit(),
15✔
210
        );
15✔
211
        $this->addOption(
15✔
212
            'strategy',
15✔
213
            null,
15✔
214
            Console\Input\InputOption::VALUE_REQUIRED,
15✔
215
            'Optional strategy to prepare URLs before crawling them.',
15✔
216
            $crawlingStrategy,
15✔
217
        );
15✔
218
        $this->addOption(
15✔
219
            'format',
15✔
220
            'f',
15✔
221
            Console\Input\InputOption::VALUE_REQUIRED,
15✔
222
            'Formatter used to print the cache warmup result',
15✔
223
            CacheWarmup\Formatter\TextFormatter::getType(),
15✔
224
        );
15✔
225
        $this->addOption(
15✔
226
            'strict',
15✔
227
            'x',
15✔
228
            Console\Input\InputOption::VALUE_NONE,
15✔
229
            'Fail if an error occurred during cache warmup.',
15✔
230
        );
15✔
231
    }
232

233
    protected function initialize(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): void
15✔
234
    {
235
        Core\Core\Bootstrap::initializeBackendAuthentication();
15✔
236
    }
237

238
    /**
239
     * @throws CacheWarmup\Exception\Exception
240
     * @throws Console\Exception\ExceptionInterface
241
     * @throws Core\Package\Exception\UnknownPackageException
242
     * @throws Core\Package\Exception\UnknownPackagePathException
243
     * @throws Typo3SitemapLocator\Exception\BaseUrlIsNotSupported
244
     * @throws Typo3SitemapLocator\Exception\SitemapIsMissing
245
     * @throws \JsonException
246
     */
247
    protected function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
15✔
248
    {
249
        // Initialize sub command
250
        $subCommand = new CacheWarmup\Command\CacheWarmupCommand($this->eventDispatcher);
15✔
251
        $subCommand->setApplication($this->getApplication() ?? new Console\Application());
15✔
252

253
        // Initialize sub command input
254
        $subCommandInput = new Console\Input\ArrayInput(
15✔
255
            $this->prepareCommandParameters($input),
15✔
256
            $subCommand->getDefinition(),
15✔
257
        );
15✔
258
        $subCommandInput->setInteractive(false);
15✔
259

260
        // Run cache warmup in sub command from eliashaeussler/cache-warmup
261
        $statusCode = $subCommand->run($subCommandInput, $output);
15✔
262

263
        // Fail if strict mode is enabled and at least one crawl was erroneous
264
        if ($input->getOption('strict') && $statusCode > 0) {
14✔
265
            return $statusCode;
1✔
266
        }
267

268
        return self::SUCCESS;
13✔
269
    }
270

271
    /**
272
     * @return array<string, mixed>
273
     * @throws CacheWarmup\Exception\Exception
274
     * @throws Core\Package\Exception\UnknownPackageException
275
     * @throws Core\Package\Exception\UnknownPackagePathException
276
     * @throws Typo3SitemapLocator\Exception\BaseUrlIsNotSupported
277
     * @throws Typo3SitemapLocator\Exception\SitemapIsMissing
278
     * @throws \JsonException
279
     */
280
    private function prepareCommandParameters(Console\Input\InputInterface $input): array
15✔
281
    {
282
        // Resolve input options
283
        $languages = $this->resolveLanguages($input->getOption('languages'));
15✔
284
        $urls = array_unique($this->resolvePages($input->getOption('pages'), $languages));
15✔
285
        $sitemaps = array_unique($this->resolveSites($input->getOption('sites'), $languages));
15✔
286
        $config = $input->getOption('config');
15✔
287
        $limit = max(0, (int)$input->getOption('limit'));
15✔
288
        $strategy = $input->getOption('strategy');
15✔
289
        $format = $input->getOption('format');
15✔
290

291
        // Fetch input options from extension configuration
292
        $excludePatterns = $this->configuration->getExcludePatterns();
15✔
293
        $crawler = $this->configuration->getVerboseCrawler();
15✔
294
        $crawlerOptions = $this->configuration->getVerboseCrawlerOptions();
15✔
295
        $parserOptions = $this->configuration->getParserOptions();
15✔
296

297
        // Initialize sub-command parameters
298
        $subCommandParameters = [
15✔
299
            'sitemaps' => $sitemaps,
15✔
300
            '--urls' => $urls,
15✔
301
            '--limit' => $limit,
15✔
302
            '--crawler' => $crawler::class,
15✔
303
            '--format' => $format,
15✔
304
        ];
15✔
305

306
        // Add crawler options to sub-command parameters
307
        if ($crawlerOptions !== []) {
15✔
308
            $subCommandParameters['--crawler-options'] = json_encode($crawlerOptions, JSON_THROW_ON_ERROR);
1✔
309
        }
310

311
        // Add parser options to sub-command parameters
312
        if ($parserOptions !== []) {
15✔
313
            $subCommandParameters['--parser-options'] = json_encode($parserOptions, JSON_THROW_ON_ERROR);
1✔
314
        }
315

316
        // Add exclude patterns
317
        if ($excludePatterns !== []) {
15✔
318
            $subCommandParameters['--exclude'] = $excludePatterns;
1✔
319
        }
320

321
        // Add crawling strategy
322
        if ($strategy !== null) {
15✔
323
            $subCommandParameters['--strategy'] = $strategy;
1✔
324
        }
325

326
        // Add config file
327
        if ($config !== null) {
15✔
328
            if (Core\Utility\PathUtility::isExtensionPath($config)) {
1✔
329
                $config = $this->packageManager->resolvePackagePath($config);
1✔
330
            }
331

332
            $subCommandParameters['--config'] = $config;
1✔
333
        }
334

335
        return $subCommandParameters;
15✔
336
    }
337

338
    /**
339
     * @param array<string> $pages
340
     * @param list<int<-1, max>> $languages
341
     * @return list<string>
342
     */
343
    private function resolvePages(array $pages, array $languages): array
15✔
344
    {
345
        $resolvedUrls = [];
15✔
346

347
        foreach ($pages as $pageList) {
15✔
348
            $normalizedPages = Core\Utility\GeneralUtility::intExplode(',', $pageList, true);
9✔
349

350
            /** @var positive-int $page */
351
            foreach ($normalizedPages as $page) {
9✔
352
                $languageIds = $languages;
9✔
353

354
                if (\in_array(self::ALL_LANGUAGES, $languageIds, true)) {
9✔
355
                    $site = $this->siteRepository->findOneByPageId($page);
8✔
356
                    $languageIds = array_keys($site?->getLanguages() ?? []);
8✔
357
                }
358

359
                foreach ($languageIds as $languageId) {
9✔
360
                    $uri = $this->pageUriBuilder->build($page, $languageId);
9✔
361

362
                    if ($uri !== null) {
9✔
363
                        $resolvedUrls[] = (string)$uri;
9✔
364
                    }
365
                }
366
            }
367
        }
368

369
        return $resolvedUrls;
15✔
370
    }
371

372
    /**
373
     * @param array<string> $sites
374
     * @param list<int<-1, max>> $languages
375
     * @return list<Domain\Model\SiteAwareSitemap>
376
     * @throws CacheWarmup\Exception\LocalFilePathIsMissingInUrl
377
     * @throws CacheWarmup\Exception\UrlIsEmpty
378
     * @throws CacheWarmup\Exception\UrlIsInvalid
379
     * @throws Typo3SitemapLocator\Exception\BaseUrlIsNotSupported
380
     * @throws Typo3SitemapLocator\Exception\SitemapIsMissing
381
     */
382
    private function resolveSites(array $sites, array $languages): array
15✔
383
    {
384
        $requestedSites = [];
15✔
385
        $resolvedSitemaps = [];
15✔
386

387
        foreach ($sites as $siteList) {
15✔
388
            $requestedSites += Core\Utility\GeneralUtility::trimExplode(',', $siteList, true);
5✔
389

390
            if (\in_array(self::ALL_SITES, $requestedSites, true)) {
5✔
391
                $requestedSites = $this->siteRepository->findAll();
1✔
392

393
                break;
394
            }
395
        }
396

397
        foreach ($requestedSites as $site) {
15✔
398
            if (Core\Utility\MathUtility::canBeInterpretedAsInteger($site)) {
5✔
399
                /** @var positive-int $rootPageId */
400
                $rootPageId = (int)$site;
3✔
401
                $site = $this->siteRepository->findOneByRootPageId($rootPageId);
3✔
402
            } elseif (is_string($site)) {
2✔
403
                $site = $this->siteRepository->findOneByIdentifier($site);
1✔
404
            }
405

406
            // Skip inaccessible sites
407
            if ($site === null) {
5✔
408
                continue;
409
            }
410

411
            $languageIds = $languages;
5✔
412

413
            if ([self::ALL_LANGUAGES] === $languageIds) {
5✔
414
                $languageIds = array_keys($site->getLanguages());
4✔
415
            }
416

417
            foreach ($languageIds as $languageId) {
5✔
418
                $siteLanguage = $this->siteLanguageRepository->findOneByLanguageId($site, $languageId);
5✔
419

420
                // Skip inaccessible site languages
421
                if ($siteLanguage === null) {
5✔
422
                    continue;
423
                }
424

425
                $sitemaps = $this->sitemapLocator->locateBySite($site, $siteLanguage);
5✔
426

427
                foreach ($sitemaps as $sitemap) {
5✔
428
                    $resolvedSitemaps[] = Domain\Model\SiteAwareSitemap::fromLocatedSitemap($sitemap);
5✔
429
                }
430
            }
431
        }
432

433
        return $resolvedSitemaps;
15✔
434
    }
435

436
    /**
437
     * @param array<string> $languages
438
     * @return list<int<-1, max>>
439
     */
440
    private function resolveLanguages(array $languages): array
15✔
441
    {
442
        $resolvedLanguages = [];
15✔
443

444
        if ($languages === []) {
15✔
445
            // Run cache warmup for all languages by default
446
            return [self::ALL_LANGUAGES];
12✔
447
        }
448

449
        foreach ($languages as $languageList) {
3✔
450
            $normalizedLanguages = Core\Utility\GeneralUtility::intExplode(',', $languageList, true);
3✔
451

452
            if (\in_array(self::ALL_LANGUAGES, $normalizedLanguages, true)) {
3✔
453
                return [self::ALL_LANGUAGES];
1✔
454
            }
455

456
            foreach ($normalizedLanguages as $languageId) {
3✔
457
                if ($languageId >= 0) {
3✔
458
                    $resolvedLanguages[] = $languageId;
3✔
459
                }
460
            }
461
        }
462

463
        return $resolvedLanguages;
2✔
464
    }
465
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc