Skip to content

Commit 79cfa6e

Browse files
pyrechxavierlacot
andcommitted
Add a command to dump static error pages
Co-authored-by: Xavier Lacot <xavier@lacot.org>
1 parent 3b5f623 commit 79cfa6e

File tree

9 files changed

+245
-0
lines changed

9 files changed

+245
-0
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@
156156
"symfony/phpunit-bridge": "^6.4|^7.0",
157157
"symfony/runtime": "self.version",
158158
"symfony/security-acl": "~2.8|~3.0",
159+
"symfony/webpack-encore-bundle": "^1.0|^2.0",
159160
"twig/cssinliner-extra": "^2.12|^3",
160161
"twig/inky-extra": "^2.12|^3",
161162
"twig/markdown-extra": "^2.12|^3",

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ CHANGELOG
2121
* Add support for configuring multiple serializer instances via the configuration
2222
* Add support for `SYMFONY_TRUSTED_PROXIES`, `SYMFONY_TRUSTED_HEADERS`, `SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER` and `SYMFONY_TRUSTED_HOSTS` env vars
2323
* Add `--no-fill` option to `translation:extract` command
24+
* Add `error:dump-pages` command
2425

2526
7.1
2627
---
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\Command;
13+
14+
use Symfony\Bundle\FrameworkBundle\Error\PagesDumper;
15+
use Symfony\Component\Console\Attribute\AsCommand;
16+
use Symfony\Component\Console\Command\Command;
17+
use Symfony\Component\Console\Input\InputArgument;
18+
use Symfony\Component\Console\Input\InputInterface;
19+
use Symfony\Component\Console\Output\OutputInterface;
20+
use Symfony\Component\Console\Style\SymfonyStyle;
21+
22+
/**
23+
* Dump error pages to plain html files that can be directly served by a web server.
24+
*
25+
* @author Loïck Piera <pyrech@gmail.com>
26+
*
27+
* @final
28+
*/
29+
#[AsCommand(
30+
name: 'error:dump-pages',
31+
description: 'Dump error pages to plain html files that can be directly served by a web server',
32+
)]
33+
class ErrorDumpPagesCommand extends Command
34+
{
35+
public function __construct(
36+
private PagesDumper $pagesDumper,
37+
) {
38+
parent::__construct();
39+
}
40+
41+
protected function configure(): void
42+
{
43+
$this
44+
->addOption(
45+
'directory',
46+
null,
47+
InputArgument::OPTIONAL,
48+
'Folder to dump the error pages to',
49+
)
50+
;
51+
}
52+
53+
protected function execute(InputInterface $input, OutputInterface $output): int
54+
{
55+
$io = new SymfonyStyle($input, $output);
56+
$io->title('Dumping error pages');
57+
58+
$this->pagesDumper->dump($input->getOption('directory'));
59+
$io->success('Error pages have been dumped');
60+
61+
return Command::SUCCESS;
62+
}
63+
}

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ public function load(array $configs, ContainerBuilder $container): void
222222
$loader->load('web.php');
223223
$loader->load('services.php');
224224
$loader->load('fragment_renderer.php');
225+
$loader->load('error.php');
225226
$loader->load('error_renderer.php');
226227

227228
if (!ContainerBuilder::willBeAvailable('symfony/clock', ClockInterface::class, ['symfony/framework-bundle'])) {
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\Error;
13+
14+
use Symfony\Bridge\Twig\ErrorRenderer\TwigErrorRenderer;
15+
use Symfony\Component\Filesystem\Filesystem;
16+
use Symfony\Component\HttpFoundation\Response;
17+
use Symfony\Component\HttpKernel\Exception\HttpException;
18+
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface;
19+
20+
/**
21+
* @author Loïck Piera <pyrech@gmail.com>
22+
*
23+
* @final
24+
*/
25+
class PagesDumper
26+
{
27+
public function __construct(
28+
private string $kernelCacheDir,
29+
private Filesystem $filesystem,
30+
private ?TwigErrorRenderer $twigErrorRenderer = null,
31+
private ?EntrypointLookupInterface $entrypointLookup = null,
32+
) {
33+
}
34+
35+
public function dump(?string $targetDirectory = null): void
36+
{
37+
if (null === $this->twigErrorRenderer) {
38+
throw new \LogicException('You cannot dump error pages if the Twig Bundle is not available. Try running "composer require symfony/twig-bundle".');
39+
}
40+
41+
$targetDirectory ??= $this->kernelCacheDir.\DIRECTORY_SEPARATOR.'error_pages';
42+
$errorStatusCodes = array_filter(
43+
array_keys(Response::$statusTexts),
44+
fn ($statusCode) => $statusCode >= 400
45+
);
46+
47+
foreach ($errorStatusCodes as $errorStatusCode) {
48+
// Avoid assets to be included only on the first dumped page
49+
$this->entrypointLookup?->reset();
50+
51+
$this->filesystem->dumpFile(
52+
$targetDirectory.\DIRECTORY_SEPARATOR.$errorStatusCode.'.html',
53+
$this->twigErrorRenderer->render(new HttpException((int) $errorStatusCode))->getAsString()
54+
);
55+
}
56+
}
57+
}

src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Symfony\Bundle\FrameworkBundle\Command\ContainerDebugCommand;
2626
use Symfony\Bundle\FrameworkBundle\Command\ContainerLintCommand;
2727
use Symfony\Bundle\FrameworkBundle\Command\DebugAutowiringCommand;
28+
use Symfony\Bundle\FrameworkBundle\Command\ErrorDumpPagesCommand;
2829
use Symfony\Bundle\FrameworkBundle\Command\EventDispatcherDebugCommand;
2930
use Symfony\Bundle\FrameworkBundle\Command\RouterDebugCommand;
3031
use Symfony\Bundle\FrameworkBundle\Command\RouterMatchCommand;
@@ -151,6 +152,12 @@
151152
])
152153
->tag('console.command')
153154

155+
->set('console.command.error_dump_pages', ErrorDumpPagesCommand::class)
156+
->args([
157+
service('error.pages_dumper'),
158+
])
159+
->tag('console.command')
160+
154161
->set('console.command.event_dispatcher_debug', EventDispatcherDebugCommand::class)
155162
->args([
156163
tagged_locator('event_dispatcher.dispatcher', 'name'),
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
13+
14+
use Symfony\Bundle\FrameworkBundle\Error\PagesDumper;
15+
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface;
16+
17+
return static function (ContainerConfigurator $container) {
18+
$container->services()
19+
->set('error.pages_dumper', PagesDumper::class)
20+
->args([
21+
param('kernel.cache_dir'),
22+
service('filesystem'),
23+
service('twig.error_renderer.html')->nullOnInvalid(),
24+
service(EntrypointLookupInterface::class)->nullOnInvalid(),
25+
])
26+
;
27+
};
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\Tests\Error;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Bridge\Twig\ErrorRenderer\TwigErrorRenderer;
16+
use Symfony\Bundle\FrameworkBundle\Error\PagesDumper;
17+
use Symfony\Component\ErrorHandler\Exception\FlattenException;
18+
use Symfony\Component\Filesystem\Filesystem;
19+
use Symfony\Component\HttpKernel\Exception\HttpException;
20+
21+
class PagesDumperTest extends TestCase
22+
{
23+
private string $tmpDir = '';
24+
private string $tmpCacheDir = '';
25+
private ?PagesDumper $pagesDumper = null;
26+
27+
public function testDumpPagesInGivenDirectory()
28+
{
29+
$pagesDumper = $this->getPagesDumper();
30+
$pagesDumper->dump($this->tmpDir);
31+
32+
$this->assertFileExists($this->tmpDir.\DIRECTORY_SEPARATOR.'404.html');
33+
$this->assertStringContainsString('Error 404', file_get_contents($this->tmpDir.\DIRECTORY_SEPARATOR.'404.html'));
34+
}
35+
36+
public function testDumpPagesInDefaultCacheDirectory()
37+
{
38+
$pagesDumper = $this->getPagesDumper();
39+
$pagesDumper->dump();
40+
41+
$this->assertFileExists($this->tmpCacheDir.\DIRECTORY_SEPARATOR.'error_pages'.\DIRECTORY_SEPARATOR.'404.html');
42+
$this->assertStringContainsString('Error 404', file_get_contents($this->tmpCacheDir.\DIRECTORY_SEPARATOR.'error_pages'.\DIRECTORY_SEPARATOR.'404.html'));
43+
}
44+
45+
protected function getPagesDumper(): PagesDumper
46+
{
47+
$twigErrorRenderer = $this->createMock(TwigErrorRenderer::class);
48+
$twigErrorRenderer
49+
->expects($this->any())
50+
->method('render')
51+
->willReturnCallback(function (...$args) {
52+
$this->assertInstanceOf(HttpException::class, $args[0]);
53+
54+
$exception = FlattenException::createFromThrowable($args[0]);
55+
$exception->setAsString(\sprintf('<html><body>Error %s</body></html>', $args[0]->getStatusCode()));
56+
57+
return $exception;
58+
})
59+
;
60+
61+
return new PagesDumper(
62+
$this->tmpCacheDir,
63+
new Filesystem(),
64+
$twigErrorRenderer,
65+
null,
66+
);
67+
}
68+
69+
protected function setUp(): void
70+
{
71+
$this->tmpDir = sys_get_temp_dir().'/error_pages';
72+
$this->tmpCacheDir = sys_get_temp_dir().'/cache';
73+
74+
$this->deleteTmpDir($this->tmpDir);
75+
$this->deleteTmpDir($this->tmpCacheDir);
76+
}
77+
78+
protected function deleteTmpDir($dir)
79+
{
80+
if (!file_exists($dir)) {
81+
return;
82+
}
83+
84+
$fs = new Filesystem();
85+
$fs->remove($dir);
86+
}
87+
}

src/Symfony/Bundle/FrameworkBundle/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"symfony/uid": "^6.4|^7.0",
7373
"symfony/web-link": "^6.4|^7.0",
7474
"symfony/webhook": "^7.2",
75+
"symfony/webpack-encore-bundle": "^1.0|^2.0",
7576
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
7677
"twig/twig": "^3.12"
7778
},

0 commit comments

Comments
 (0)