Skip to content

Commit 4dab8f5

Browse files
committed
feature #21003 [Console][FrameworkBundle] Log console exceptions (jameshalsall, chalasr)
This PR was merged into the 3.3-dev branch. Discussion ---------- [Console][FrameworkBundle] Log console exceptions | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #10895 | License | MIT | Doc PR | symfony/symfony-docs#7373 Continues #19382, fixing some issues including: - ability to display the input string for any `InputInterface` implementation (cast to string if possible, use the command name otherwise) - if the input can be casted as string, cleanup the result (from `command "'command:name' --foo=bar" ` to `command "command:name --foo=bar"`) - made `ExceptionLister::$logger` private instead of protected - changed methods name from `onKernel*` to `onConsole*` (e.g. `onConsoleException`) and removed unnecessary doc blocks - Added more tests Log for an exception: > [2016-12-22 00:34:42] app.ERROR: Exception thrown while running command: "cache:clear -vvv". Message: "An error occured!" {"exception":"[object] (RuntimeException(code: 0): An error occured! at /Volumes/HD/Sites/tests/sf-demo-3.2/vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php:61)","command":"cache:clear -vvv","message":"An error occured!"} [] Commits ------- 919041c1ad Add Console ExceptionListener 9896547a4d Add basic support for automatic console exception logging
2 parents 4272e24 + cf6baf6 commit 4dab8f5

File tree

3 files changed

+205
-0
lines changed

3 files changed

+205
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
3.3.0
55
-----
66

7+
* added `ExceptionListener`
78
* added `AddConsoleCommandPass` (originally in FrameworkBundle)
89

910
3.2.0

EventListener/ExceptionListener.php

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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\Console\EventListener;
13+
14+
use Psr\Log\LoggerInterface;
15+
use Symfony\Component\Console\Event\ConsoleEvent;
16+
use Symfony\Component\Console\ConsoleEvents;
17+
use Symfony\Component\Console\Event\ConsoleExceptionEvent;
18+
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
19+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
20+
21+
/**
22+
* @author James Halsall <james.t.halsall@googlemail.com>
23+
* @author Robin Chalas <robin.chalas@gmail.com>
24+
*/
25+
class ExceptionListener implements EventSubscriberInterface
26+
{
27+
private $logger;
28+
29+
public function __construct(LoggerInterface $logger = null)
30+
{
31+
$this->logger = $logger;
32+
}
33+
34+
public function onConsoleException(ConsoleExceptionEvent $event)
35+
{
36+
if (null === $this->logger) {
37+
return;
38+
}
39+
40+
$exception = $event->getException();
41+
42+
$this->logger->error('Exception thrown while running command "{command}". Message: "{message}"', array('exception' => $exception, 'command' => $this->getInputString($event), 'message' => $exception->getMessage()));
43+
}
44+
45+
public function onConsoleTerminate(ConsoleTerminateEvent $event)
46+
{
47+
if (null === $this->logger) {
48+
return;
49+
}
50+
51+
$exitCode = $event->getExitCode();
52+
53+
if (0 === $exitCode) {
54+
return;
55+
}
56+
57+
$this->logger->error('Command "{command}" exited with code "{code}"', array('command' => $this->getInputString($event), 'code' => $exitCode));
58+
}
59+
60+
public static function getSubscribedEvents()
61+
{
62+
return array(
63+
ConsoleEvents::EXCEPTION => array('onConsoleException', -128),
64+
ConsoleEvents::TERMINATE => array('onConsoleTerminate', -128),
65+
);
66+
}
67+
68+
private static function getInputString(ConsoleEvent $event)
69+
{
70+
$commandName = $event->getCommand()->getName();
71+
$input = $event->getInput();
72+
73+
if (method_exists($input, '__toString')) {
74+
return str_replace(array("'$commandName'", "\"$commandName\""), $commandName, (string) $input);
75+
}
76+
77+
return $commandName;
78+
}
79+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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\Console\Tests\EventListener;
13+
14+
use Psr\Log\LoggerInterface;
15+
use Symfony\Component\Console\Command\Command;
16+
use Symfony\Component\Console\Event\ConsoleExceptionEvent;
17+
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
18+
use Symfony\Component\Console\EventListener\ExceptionListener;
19+
use Symfony\Component\Console\Input\ArgvInput;
20+
use Symfony\Component\Console\Input\ArrayInput;
21+
use Symfony\Component\Console\Input\StringInput;
22+
use Symfony\Component\Console\Input\InputInterface;
23+
use Symfony\Component\Console\Output\OutputInterface;
24+
25+
class ExceptionListenerTest extends \PHPUnit_Framework_TestCase
26+
{
27+
public function testOnConsoleException()
28+
{
29+
$exception = new \RuntimeException('An error occurred');
30+
31+
$logger = $this->getLogger();
32+
$logger
33+
->expects($this->once())
34+
->method('error')
35+
->with('Exception thrown while running command "{command}". Message: "{message}"', array('exception' => $exception, 'command' => 'test:run --foo=baz buzz', 'message' => 'An error occurred'))
36+
;
37+
38+
$listener = new ExceptionListener($logger);
39+
$listener->onConsoleException($this->getConsoleExceptionEvent($exception, new ArgvInput(array('console.php', 'test:run', '--foo=baz', 'buzz')), 1));
40+
}
41+
42+
public function testOnConsoleTerminateForNonZeroExitCodeWritesToLog()
43+
{
44+
$logger = $this->getLogger();
45+
$logger
46+
->expects($this->once())
47+
->method('error')
48+
->with('Command "{command}" exited with code "{code}"', array('command' => 'test:run', 'code' => 255))
49+
;
50+
51+
$listener = new ExceptionListener($logger);
52+
$listener->onConsoleTerminate($this->getConsoleTerminateEvent(new ArgvInput(array('console.php', 'test:run')), 255));
53+
}
54+
55+
public function testOnConsoleTerminateForZeroExitCodeDoesNotWriteToLog()
56+
{
57+
$logger = $this->getLogger();
58+
$logger
59+
->expects($this->never())
60+
->method('error')
61+
;
62+
63+
$listener = new ExceptionListener($logger);
64+
$listener->onConsoleTerminate($this->getConsoleTerminateEvent(new ArgvInput(array('console.php', 'test:run')), 0));
65+
}
66+
67+
public function testGetSubscribedEvents()
68+
{
69+
$this->assertEquals(
70+
array(
71+
'console.exception' => array('onConsoleException', -128),
72+
'console.terminate' => array('onConsoleTerminate', -128),
73+
),
74+
ExceptionListener::getSubscribedEvents()
75+
);
76+
}
77+
78+
public function testAllKindsOfInputCanBeLogged()
79+
{
80+
$logger = $this->getLogger();
81+
$logger
82+
->expects($this->exactly(3))
83+
->method('error')
84+
->with('Command "{command}" exited with code "{code}"', array('command' => 'test:run --foo=bar', 'code' => 255))
85+
;
86+
87+
$listener = new ExceptionListener($logger);
88+
$listener->onConsoleTerminate($this->getConsoleTerminateEvent(new ArgvInput(array('console.php', 'test:run', '--foo=bar')), 255));
89+
$listener->onConsoleTerminate($this->getConsoleTerminateEvent(new ArrayInput(array('name' => 'test:run', '--foo' => 'bar')), 255));
90+
$listener->onConsoleTerminate($this->getConsoleTerminateEvent(new StringInput('test:run --foo=bar'), 255));
91+
}
92+
93+
public function testCommandNameIsDisplayedForNonStringableInput()
94+
{
95+
$logger = $this->getLogger();
96+
$logger
97+
->expects($this->once())
98+
->method('error')
99+
->with('Command "{command}" exited with code "{code}"', array('command' => 'test:run', 'code' => 255))
100+
;
101+
102+
$listener = new ExceptionListener($logger);
103+
$listener->onConsoleTerminate($this->getConsoleTerminateEvent($this->getMockBuilder(InputInterface::class)->getMock(), 255));
104+
}
105+
106+
private function getLogger()
107+
{
108+
return $this->getMockForAbstractClass(LoggerInterface::class);
109+
}
110+
111+
private function getConsoleExceptionEvent(\Exception $exception, InputInterface $input, $exitCode)
112+
{
113+
return new ConsoleExceptionEvent(new Command('test:run'), $input, $this->getOutput(), $exception, $exitCode);
114+
}
115+
116+
private function getConsoleTerminateEvent(InputInterface $input, $exitCode)
117+
{
118+
return new ConsoleTerminateEvent(new Command('test:run'), $input, $this->getOutput(), $exitCode);
119+
}
120+
121+
private function getOutput()
122+
{
123+
return $this->getMockBuilder(OutputInterface::class)->getMock();
124+
}
125+
}

0 commit comments

Comments
 (0)