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

eliashaeussler / typo3-warming / 12587343620

02 Jan 2025 06:59PM UTC coverage: 90.268%. First build
12587343620

Pull #769

github

web-flow
Merge dfcf7a38c into 4c89ccdb8
Pull Request #769: [!!!][FEATURE] Require `eliashaeussler/cache-warmup` ^4.0

69 of 81 new or added lines in 11 files covered. (85.19%)

1076 of 1192 relevant lines covered (90.27%)

8.43 hits per line

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

99.39
/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\Utility;
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(
14✔
51
        private readonly Configuration\Configuration $configuration,
52
        private readonly CacheWarmup\Crawler\Strategy\CrawlingStrategyFactory $crawlingStrategyFactory,
53
        private readonly Typo3SitemapLocator\Sitemap\SitemapLocator $sitemapLocator,
54
        private readonly Core\Site\SiteFinder $siteFinder,
55
        private readonly EventDispatcher\EventDispatcherInterface $eventDispatcher,
56
        private readonly Core\Package\PackageManager $packageManager,
57
    ) {
58
        parent::__construct();
14✔
59
    }
60

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

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

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

80
<info>Sites and pages</info>
81
<info>===============</info>
82

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

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

91
Examples:
92

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

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

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

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

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

116
<info>Additional options</info>
117
<info>==================</info>
118

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

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

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

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

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

160
<info>Crawling configuration</info>
161
<info>======================</info>
162

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

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

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

231
    /**
232
     * @throws Console\Exception\ExceptionInterface
233
     * @throws Core\Exception\SiteNotFoundException
234
     * @throws \JsonException
235
     */
236
    protected function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
14✔
237
    {
238
        // Initialize sub command
239
        $subCommand = new CacheWarmup\Command\CacheWarmupCommand($this->eventDispatcher);
14✔
240
        $subCommand->setApplication($this->getApplication() ?? new Console\Application());
14✔
241

242
        // Initialize sub command input
243
        $subCommandInput = new Console\Input\ArrayInput(
14✔
244
            $this->prepareCommandParameters($input),
14✔
245
            $subCommand->getDefinition(),
14✔
246
        );
14✔
247
        $subCommandInput->setInteractive(false);
14✔
248

249
        // Run cache warmup in sub command from eliashaeussler/cache-warmup
250
        $statusCode = $subCommand->run($subCommandInput, $output);
14✔
251

252
        // Fail if strict mode is enabled and at least one crawl was erroneous
253
        if ($input->getOption('strict') && $statusCode > 0) {
13✔
254
            return $statusCode;
1✔
255
        }
256

257
        return self::SUCCESS;
12✔
258
    }
259

260
    /**
261
     * @return array<string, mixed>
262
     * @throws CacheWarmup\Exception\Exception
263
     * @throws Core\Exception\SiteNotFoundException
264
     * @throws Core\Package\Exception
265
     * @throws \JsonException
266
     * @throws Typo3SitemapLocator\Exception\BaseUrlIsNotSupported
267
     * @throws Typo3SitemapLocator\Exception\SitemapIsMissing
268
     */
269
    private function prepareCommandParameters(Console\Input\InputInterface $input): array
14✔
270
    {
271
        // Resolve input options
272
        $languages = $this->resolveLanguages($input->getOption('languages'));
14✔
273
        $urls = array_unique($this->resolvePages($input->getOption('pages'), $languages));
14✔
274
        $sitemaps = array_unique($this->resolveSites($input->getOption('sites'), $languages));
14✔
275
        $config = $input->getOption('config');
14✔
276
        $limit = max(0, (int)$input->getOption('limit'));
14✔
277
        $strategy = $input->getOption('strategy');
14✔
278
        $format = $input->getOption('format');
14✔
279

280
        // Fetch input options from extension configuration
281
        $excludePatterns = $this->configuration->getExcludePatterns();
14✔
282
        $crawler = $this->configuration->getVerboseCrawler();
14✔
283
        $crawlerOptions = $this->configuration->getVerboseCrawlerOptions();
14✔
284
        $parserOptions = $this->configuration->getParserOptions();
14✔
285

286
        // Initialize sub-command parameters
287
        $subCommandParameters = [
14✔
288
            'sitemaps' => $sitemaps,
14✔
289
            '--urls' => $urls,
14✔
290
            '--limit' => $limit,
14✔
291
            '--crawler' => $crawler::class,
14✔
292
            '--format' => $format,
14✔
293
        ];
14✔
294

295
        // Add crawler options to sub-command parameters
296
        if ($crawlerOptions !== []) {
14✔
297
            $subCommandParameters['--crawler-options'] = json_encode($crawlerOptions, JSON_THROW_ON_ERROR);
1✔
298
        }
299

300
        // Add parser options to sub-command parameters
301
        if ($parserOptions !== []) {
14✔
302
            $subCommandParameters['--parser-options'] = json_encode($parserOptions, JSON_THROW_ON_ERROR);
1✔
303
        }
304

305
        // Add exclude patterns
306
        if ($excludePatterns !== []) {
14✔
307
            $subCommandParameters['--exclude'] = $excludePatterns;
1✔
308
        }
309

310
        // Add crawling strategy
311
        if ($strategy !== null) {
14✔
312
            $subCommandParameters['--strategy'] = $strategy;
1✔
313
        }
314

315
        // Add config file
316
        if ($config !== null) {
14✔
317
            if (Core\Utility\PathUtility::isExtensionPath($config)) {
1✔
318
                $config = $this->packageManager->resolvePackagePath($config);
1✔
319
            }
320

321
            $subCommandParameters['--config'] = $config;
1✔
322
        }
323

324
        return $subCommandParameters;
14✔
325
    }
326

327
    /**
328
     * @param array<string> $pages
329
     * @param list<int> $languages
330
     * @return list<string>
331
     * @throws Core\Exception\SiteNotFoundException
332
     */
333
    private function resolvePages(array $pages, array $languages): array
14✔
334
    {
335
        $resolvedUrls = [];
14✔
336

337
        foreach ($pages as $pageList) {
14✔
338
            $normalizedPages = Core\Utility\GeneralUtility::intExplode(',', $pageList, true);
8✔
339

340
            foreach ($normalizedPages as $page) {
8✔
341
                $languageIds = $languages;
8✔
342

343
                if ($languageIds === [self::ALL_LANGUAGES]) {
8✔
344
                    $site = $this->siteFinder->getSiteByPageId($page);
7✔
345
                    $languageIds = array_keys($site->getLanguages());
7✔
346
                }
347

348
                foreach ($languageIds as $languageId) {
8✔
349
                    $uri = Utility\HttpUtility::generateUri($page, $languageId);
8✔
350

351
                    if ($uri !== null) {
8✔
352
                        $resolvedUrls[] = (string)$uri;
8✔
353
                    }
354
                }
355
            }
356
        }
357

358
        return $resolvedUrls;
14✔
359
    }
360

361
    /**
362
     * @param array<string> $sites
363
     * @param list<int> $languages
364
     * @return list<Domain\Model\SiteAwareSitemap>
365
     * @throws CacheWarmup\Exception\LocalFilePathIsMissingInUrl
366
     * @throws CacheWarmup\Exception\UrlIsEmpty
367
     * @throws CacheWarmup\Exception\UrlIsInvalid
368
     * @throws Core\Exception\SiteNotFoundException
369
     * @throws Typo3SitemapLocator\Exception\BaseUrlIsNotSupported
370
     * @throws Typo3SitemapLocator\Exception\SitemapIsMissing
371
     */
372
    private function resolveSites(array $sites, array $languages): array
14✔
373
    {
374
        $resolvedSitemaps = [];
14✔
375

376
        foreach ($sites as $siteList) {
14✔
377
            $siteList = Core\Utility\GeneralUtility::trimExplode(',', $siteList, true);
5✔
378

379
            if (in_array(self::ALL_SITES, $siteList, true)) {
5✔
380
                $siteList = $this->siteFinder->getAllSites();
1✔
381
            }
382

383
            foreach ($siteList as $site) {
5✔
384
                if (Core\Utility\MathUtility::canBeInterpretedAsInteger($site)) {
5✔
385
                    $site = $this->siteFinder->getSiteByRootPageId((int)$site);
3✔
386
                } elseif (is_string($site)) {
2✔
387
                    $site = $this->siteFinder->getSiteByIdentifier($site);
1✔
388
                }
389

390
                $languageIds = $languages;
5✔
391

392
                if ([self::ALL_LANGUAGES] === $languageIds) {
5✔
393
                    $languageIds = array_keys($site->getLanguages());
4✔
394
                }
395

396
                foreach ($languageIds as $languageId) {
5✔
397
                    $sitemaps = $this->sitemapLocator->locateBySite($site, $site->getLanguageById($languageId));
5✔
398

399
                    foreach ($sitemaps as $sitemap) {
5✔
400
                        $resolvedSitemaps[] = Domain\Model\SiteAwareSitemap::fromLocatedSitemap($sitemap);
5✔
401
                    }
402
                }
403
            }
404
        }
405

406
        return $resolvedSitemaps;
14✔
407
    }
408

409
    /**
410
     * @param array<string> $languages
411
     * @return list<int>
412
     */
413
    private function resolveLanguages(array $languages): array
14✔
414
    {
415
        $resolvedLanguages = [];
14✔
416

417
        if ($languages === []) {
14✔
418
            // Run cache warmup for all languages by default
419
            return [self::ALL_LANGUAGES];
12✔
420
        }
421

422
        foreach ($languages as $languageList) {
2✔
423
            $normalizedLanguages = Core\Utility\GeneralUtility::intExplode(',', $languageList, true);
2✔
424

425
            foreach ($normalizedLanguages as $languageId) {
2✔
426
                $resolvedLanguages[] = $languageId;
2✔
427
            }
428
        }
429

430
        return $resolvedLanguages;
2✔
431
    }
432
}
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