Skip to content

Commit dd2484a

Browse files
committed
Add DebugCommand for easy debugging and testing
1 parent f4f5ea6 commit dd2484a

File tree

5 files changed

+218
-6
lines changed

5 files changed

+218
-6
lines changed

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

+5
Original file line numberDiff line numberDiff line change
@@ -194,5 +194,10 @@
194194
<argument type="service" id="debug.file_link_formatter" on-invalid="null" />
195195
<tag name="console.command" command="debug:form" />
196196
</service>
197+
198+
<service id="console.command.error_renderer_debug" class="Symfony\Component\ErrorRenderer\Command\DebugCommand">
199+
<argument type="collection" />
200+
<tag name="console.command" command="debug:error-renderer" />
201+
</service>
197202
</services>
198203
</container>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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\ErrorRenderer\Command;
13+
14+
use Symfony\Component\Console\Command\Command;
15+
use Symfony\Component\Console\Exception\InvalidArgumentException;
16+
use Symfony\Component\Console\Input\InputArgument;
17+
use Symfony\Component\Console\Input\InputInterface;
18+
use Symfony\Component\Console\Output\OutputInterface;
19+
use Symfony\Component\Console\Style\SymfonyStyle;
20+
use Symfony\Component\ErrorRenderer\ErrorRenderer\ErrorRendererInterface;
21+
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
22+
23+
/**
24+
* A console command for retrieving information about error renderers.
25+
*
26+
* @author Yonel Ceruto <yonelceruto@gmail.com>
27+
*/
28+
class DebugCommand extends Command
29+
{
30+
protected static $defaultName = 'debug:error-renderer';
31+
32+
private $renderers;
33+
34+
/**
35+
* @param ErrorRendererInterface[] $renderers
36+
*/
37+
public function __construct(array $renderers)
38+
{
39+
$this->renderers = $renderers;
40+
41+
parent::__construct();
42+
}
43+
44+
/**
45+
* {@inheritdoc}
46+
*/
47+
protected function configure(): void
48+
{
49+
$this
50+
->addArgument('format', InputArgument::OPTIONAL, sprintf('Outputs a sample in a specific format (one of %s)', implode(', ', array_keys($this->renderers))))
51+
->setDescription('Displays all available error renderers and their formats.')
52+
->setHelp(<<<'EOF'
53+
The <info>%command.name%</info> command displays all available error renderers and
54+
their formats:
55+
56+
<info>php %command.full_name%</info>
57+
58+
Or outputs a sample in a specific format:
59+
60+
<info>php %command.full_name% format</info>
61+
62+
EOF
63+
)
64+
;
65+
}
66+
67+
/**
68+
* {@inheritdoc}
69+
*/
70+
protected function execute(InputInterface $input, OutputInterface $output)
71+
{
72+
$io = new SymfonyStyle($input, $output);
73+
$renderers = $this->renderers;
74+
75+
if ($format = $input->getArgument('format')) {
76+
if (!isset($renderers[$format])) {
77+
throw new InvalidArgumentException(sprintf('No error renderer found for format "%s". Known format are %s.', $format, implode(', ', array_keys($this->renderers))));
78+
}
79+
80+
$exception = FlattenException::createFromThrowable(new \Exception('Something has intentionally gone wrong.'), 500, ['X-Debug' => false]);
81+
$io->writeln($renderers[$format]->render($exception));
82+
} else {
83+
$tableRows = [];
84+
foreach ($renderers as $format => $renderer) {
85+
$tableRows[] = [sprintf('<fg=cyan>%s</fg=cyan>', $format), \get_class($renderer)];
86+
}
87+
88+
$io->title('ErrorRenderer');
89+
$io->text('The following error renderers are available:');
90+
$io->newLine();
91+
$io->table(['Format', 'Class'], $tableRows);
92+
}
93+
}
94+
}

src/Symfony/Component/ErrorRenderer/DependencyInjection/ErrorRendererPass.php

+11-6
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ class ErrorRendererPass implements CompilerPassInterface
2424
{
2525
private $rendererService;
2626
private $rendererTag;
27+
private $debugCommandService;
2728

28-
public function __construct(string $rendererService = 'error_renderer', string $rendererTag = 'error_renderer.renderer')
29+
public function __construct(string $rendererService = 'error_renderer', string $rendererTag = 'error_renderer.renderer', string $debugCommandService = 'console.command.error_renderer_debug')
2930
{
3031
$this->rendererService = $rendererService;
3132
$this->rendererTag = $rendererTag;
33+
$this->debugCommandService = $debugCommandService;
3234
}
3335

3436
/**
@@ -40,27 +42,30 @@ public function process(ContainerBuilder $container)
4042
return;
4143
}
4244

43-
$renderers = $registered = [];
45+
$renderers = [];
4446
foreach ($container->findTaggedServiceIds($this->rendererTag, true) as $serviceId => $tags) {
4547
/** @var ErrorRendererInterface $class */
4648
$class = $container->getDefinition($serviceId)->getClass();
4749

4850
foreach ($tags as $tag) {
4951
$format = $tag['format'] ?? $class::getFormat();
50-
if (!isset($registered[$format])) {
51-
$priority = $tag['priority'] ?? 0;
52+
$priority = $tag['priority'] ?? 0;
53+
if (!isset($renderers[$priority][$format])) {
5254
$renderers[$priority][$format] = new Reference($serviceId);
53-
$registered[$format] = true;
5455
}
5556
}
5657
}
5758

5859
if ($renderers) {
59-
krsort($renderers);
60+
ksort($renderers);
6061
$renderers = array_merge(...$renderers);
6162
}
6263

6364
$definition = $container->getDefinition($this->rendererService);
6465
$definition->replaceArgument(0, ServiceLocatorTagPass::register($container, $renderers));
66+
67+
if ($container->hasDefinition($this->debugCommandService)) {
68+
$container->getDefinition($this->debugCommandService)->replaceArgument(0, $renderers);
69+
}
6570
}
6671
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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\ErrorRenderer\Tests\Command;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Console\Application;
16+
use Symfony\Component\Console\Tester\CommandTester;
17+
use Symfony\Component\ErrorRenderer\Command\DebugCommand;
18+
use Symfony\Component\ErrorRenderer\ErrorRenderer\JsonErrorRenderer;
19+
use Symfony\Component\ErrorRenderer\ErrorRenderer\TxtErrorRenderer;
20+
use Symfony\Component\ErrorRenderer\ErrorRenderer\XmlErrorRenderer;
21+
22+
class DebugCommandTest extends TestCase
23+
{
24+
public function testAvailableRenderers()
25+
{
26+
$tester = $this->createCommandTester();
27+
$ret = $tester->execute([], ['decorated' => false]);
28+
29+
$this->assertEquals(0, $ret, 'Returns 0 in case of success');
30+
$this->assertSame(<<<TXT
31+
32+
ErrorRenderer
33+
=============
34+
35+
The following error renderers are available:
36+
37+
-------- -----------------------------------------------------------------
38+
Format Class
39+
-------- -----------------------------------------------------------------
40+
json Symfony\Component\ErrorRenderer\ErrorRenderer\JsonErrorRenderer
41+
xml Symfony\Component\ErrorRenderer\ErrorRenderer\XmlErrorRenderer
42+
txt Symfony\Component\ErrorRenderer\ErrorRenderer\TxtErrorRenderer
43+
-------- -----------------------------------------------------------------
44+
45+
46+
TXT
47+
, $tester->getDisplay(true));
48+
}
49+
50+
public function testFormatArgument()
51+
{
52+
$tester = $this->createCommandTester();
53+
$ret = $tester->execute(['format' => 'json'], ['decorated' => false]);
54+
55+
$this->assertEquals(0, $ret, 'Returns 0 in case of success');
56+
$this->assertSame(<<<TXT
57+
{
58+
"title": "Internal Server Error",
59+
"status": 500,
60+
"detail": "Something has intentionally gone wrong."
61+
}
62+
63+
TXT
64+
, $tester->getDisplay(true));
65+
}
66+
67+
private function createCommandTester()
68+
{
69+
$command = new DebugCommand([
70+
'json' => new JsonErrorRenderer(false),
71+
'xml' => new XmlErrorRenderer(false),
72+
'txt' => new TxtErrorRenderer(false),
73+
]);
74+
75+
$application = new Application();
76+
$application->add($command);
77+
78+
return new CommandTester($application->find('debug:error-renderer'));
79+
}
80+
81+
/**
82+
* @expectedException \Symfony\Component\Console\Exception\InvalidArgumentException
83+
* @expectedExceptionMessage No error renderer found for format "foo". Known format are json, xml, txt.
84+
*/
85+
public function testInvalidFormat()
86+
{
87+
$tester = $this->createCommandTester();
88+
$tester->execute(['format' => 'foo'], ['decorated' => false]);
89+
}
90+
}

src/Symfony/Component/ErrorRenderer/Tests/DependencyInjection/ErrorRendererPassTest.php

+18
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,22 @@ public function testProcess()
4848
];
4949
$this->assertEquals($expected, $serviceLocatorDefinition->getArgument(0));
5050
}
51+
52+
public function testServicesAreOrderedAccordingToPriority()
53+
{
54+
$container = new ContainerBuilder();
55+
56+
$definition = $container->register('error_renderer')->setArguments([null]);
57+
$container->register('r2')->addTag('error_renderer.renderer', ['format' => 'json', 'priority' => 100]);
58+
$container->register('r1')->addTag('error_renderer.renderer', ['format' => 'json', 'priority' => 200]);
59+
$container->register('r3')->addTag('error_renderer.renderer', ['format' => 'json']);
60+
61+
(new ErrorRendererPass())->process($container);
62+
63+
$expected = [
64+
'json' => new ServiceClosureArgument(new Reference('r1')),
65+
];
66+
$serviceLocatorDefinition = $container->getDefinition((string) $definition->getArgument(0));
67+
$this->assertEquals($expected, $serviceLocatorDefinition->getArgument(0));
68+
}
5169
}

0 commit comments

Comments
 (0)