Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[ErrorHandler] Add a command to dump static error pages #58769

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 1, 2025
Merged
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
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@
"symfony/phpunit-bridge": "^6.4|^7.0",
"symfony/runtime": "self.version",
"symfony/security-acl": "~2.8|~3.0",
"symfony/webpack-encore-bundle": "^1.0|^2.0",
"twig/cssinliner-extra": "^2.12|^3",
"twig/inky-extra": "^2.12|^3",
"twig/markdown-extra": "^2.12|^3",
Expand Down
10 changes: 10 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
use Symfony\Component\Console\EventListener\ErrorListener;
use Symfony\Component\Console\Messenger\RunCommandMessageHandler;
use Symfony\Component\Dotenv\Command\DebugCommand as DotenvDebugCommand;
use Symfony\Component\ErrorHandler\Command\ErrorDumpCommand;
use Symfony\Component\Messenger\Command\ConsumeMessagesCommand;
use Symfony\Component\Messenger\Command\DebugCommand as MessengerDebugCommand;
use Symfony\Component\Messenger\Command\FailedMessagesRemoveCommand;
Expand All @@ -59,6 +60,7 @@
use Symfony\Component\Translation\Command\TranslationPushCommand;
use Symfony\Component\Translation\Command\XliffLintCommand;
use Symfony\Component\Validator\Command\DebugCommand as ValidatorDebugCommand;
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface;

return static function (ContainerConfigurator $container) {
$container->services()
Expand Down Expand Up @@ -385,6 +387,14 @@
])
->tag('console.command')

->set('console.command.error_dumper', ErrorDumpCommand::class)
->args([
service('filesystem'),
service('error_renderer.html'),
service(EntrypointLookupInterface::class)->nullOnInvalid(),
])
->tag('console.command')

->set('console.messenger.application', Application::class)
->share(false)
->call('setAutoExit', [false])
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/FrameworkBundle/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"symfony/config": "^7.3",
"symfony/dependency-injection": "^7.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/error-handler": "^6.4|^7.0",
"symfony/error-handler": "^7.3",
"symfony/event-dispatcher": "^6.4|^7.0",
"symfony/http-foundation": "^7.3",
"symfony/http-kernel": "^7.2",
Expand Down
5 changes: 5 additions & 0 deletions src/Symfony/Component/ErrorHandler/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

7.3
---

* Add `error:dump` command

7.1
---

Expand Down
85 changes: 85 additions & 0 deletions src/Symfony/Component/ErrorHandler/Command/ErrorDumpCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\ErrorHandler\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface;

/**
* Dump error pages to plain HTML files that can be directly served by a web server.
*
* @author Loïck Piera <[email protected]>
*/
#[AsCommand(
name: 'error:dump',
description: 'Dump error pages to plain HTML files that can be directly served by a web server',
)]
final class ErrorDumpCommand extends Command
{
public function __construct(
private readonly Filesystem $filesystem,
private readonly ErrorRendererInterface $errorRenderer,
private readonly ?EntrypointLookupInterface $entrypointLookup = null,
) {
parent::__construct();
}

protected function configure(): void
{
$this
->addArgument('path', InputArgument::REQUIRED, 'Path where to dump the error pages in')
->addArgument('status-codes', InputArgument::IS_ARRAY, 'Status codes to dump error pages for, all of them by default')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force directory removal before dumping new error pages')
;
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$path = $input->getArgument('path');

$io = new SymfonyStyle($input, $output);
$io->title('Dumping error pages');

$this->dump($io, $path, $input->getArgument('status-codes'), (bool) $input->getOption('force'));
$io->success(\sprintf('Error pages have been dumped in "%s".', $path));

return Command::SUCCESS;
}

private function dump(SymfonyStyle $io, string $path, array $statusCodes, bool $force = false): void
{
if (!$statusCodes) {
$statusCodes = array_filter(array_keys(Response::$statusTexts), fn ($statusCode) => $statusCode >= 400);
}

if ($force || ($this->filesystem->exists($path) && $io->confirm(\sprintf('The "%s" directory already exists. Do you want to remove it before dumping the error pages?', $path), false))) {
$this->filesystem->remove($path);
}

foreach ($statusCodes as $statusCode) {
// Avoid assets to be included only on the first dumped page
$this->entrypointLookup?->reset();

$this->filesystem->dumpFile($path.\DIRECTORY_SEPARATOR.$statusCode.'.html', $this->errorRenderer->render(new HttpException((int) $statusCode))->getAsString());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\ErrorHandler\Tests\Command;

use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\TwigBundle\Tests\TestCase;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\ErrorHandler\Command\ErrorDumpCommand;
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface;

class ErrorDumpCommandTest extends TestCase
{
private string $tmpDir = '';

protected function setUp(): void
{
$this->tmpDir = sys_get_temp_dir().'/error_pages';

$fs = new Filesystem();
$fs->remove($this->tmpDir);
}

public function testDumpPages()
{
$tester = $this->getCommandTester($this->getKernel(), []);
$tester->execute([
'path' => $this->tmpDir,
]);

$this->assertFileExists($this->tmpDir.\DIRECTORY_SEPARATOR.'404.html');
$this->assertStringContainsString('Error 404', file_get_contents($this->tmpDir.\DIRECTORY_SEPARATOR.'404.html'));
}

public function testDumpPagesOnlyForGivenStatusCodes()
{
$fs = new Filesystem();
$fs->mkdir($this->tmpDir);
$fs->touch($this->tmpDir.\DIRECTORY_SEPARATOR.'test.html');

$tester = $this->getCommandTester($this->getKernel());
$tester->execute([
'path' => $this->tmpDir,
'status-codes' => ['400', '500'],
]);

$this->assertFileExists($this->tmpDir.\DIRECTORY_SEPARATOR.'test.html');
$this->assertFileDoesNotExist($this->tmpDir.\DIRECTORY_SEPARATOR.'404.html');

$this->assertFileExists($this->tmpDir.\DIRECTORY_SEPARATOR.'400.html');
$this->assertStringContainsString('Error 400', file_get_contents($this->tmpDir.\DIRECTORY_SEPARATOR.'400.html'));
}

public function testForceRemovalPages()
{
$fs = new Filesystem();
$fs->mkdir($this->tmpDir);
$fs->touch($this->tmpDir.\DIRECTORY_SEPARATOR.'test.html');

$tester = $this->getCommandTester($this->getKernel());
$tester->execute([
'path' => $this->tmpDir,
'--force' => true,
]);

$this->assertFileDoesNotExist($this->tmpDir.\DIRECTORY_SEPARATOR.'test.html');
$this->assertFileExists($this->tmpDir.\DIRECTORY_SEPARATOR.'404.html');
}

private function getKernel(): MockObject&KernelInterface
{
return $this->createMock(KernelInterface::class);
}

private function getCommandTester(KernelInterface $kernel): CommandTester
{
$errorRenderer = $this->createStub(ErrorRendererInterface::class);
$errorRenderer
->method('render')
->willReturnCallback(function (HttpException $e) {
$exception = FlattenException::createFromThrowable($e);
$exception->setAsString(\sprintf('<html><body>Error %s</body></html>', $e->getStatusCode()));

return $exception;
})
;

$entrypointLookup = $this->createMock(EntrypointLookupInterface::class);

$application = new Application($kernel);
$application->add(new ErrorDumpCommand(
new Filesystem(),
$errorRenderer,
$entrypointLookup,
));

return new CommandTester($application->find('error:dump'));
}
}
4 changes: 3 additions & 1 deletion src/Symfony/Component/ErrorHandler/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
"symfony/var-dumper": "^6.4|^7.0"
},
"require-dev": {
"symfony/console": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/serializer": "^6.4|^7.0",
"symfony/deprecation-contracts": "^2.5|^3"
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/webpack-encore-bundle": "^1.0|^2.0"
},
"conflict": {
"symfony/deprecation-contracts": "<2.5",
Expand Down
Loading