Merge pull request #158 from biapy/master

Add a PlainTextHandler for command line and LoggerInterface (Monolog).
This commit is contained in:
Denis
2014-02-23 10:44:47 +02:00
2 changed files with 774 additions and 0 deletions
+331
View File
@@ -0,0 +1,331 @@
<?php
/**
* Whoops - php errors for cool kids
* @author Filipe Dobreira <http://github.com/filp>
* Plaintext handler for command line and logs.
* @author Pierre-Yves Landuré <https://howto.biapy.com/>
*/
namespace Whoops\Handler;
use Whoops\Handler\Handler;
use InvalidArgumentException;
use Whoops\Exception\Frame;
use \Psr\Log\LoggerInterface;
/**
* Handler outputing plaintext error messages. Can be used
* directly, or will be instantiated automagically by Whoops\Run
* if passed to Run::pushHandler
*/
class PlainTextHandler extends Handler
{
const VAR_DUMP_PREFIX = ' | ';
/**
* @var Psr\Log\LoggerInterface
*/
protected $logger;
/**
* @var bool
*/
private $addTraceToOutput = true;
/**
* @var bool|integer
*/
private $addTraceFunctionArgsToOutput = false;
/**
* @var integer
*/
private $traceFunctionArgsOutputLimit = 1024;
/**
* @var bool
*/
private $onlyForCommandLine = false;
/**
* @var bool
*/
private $outputOnlyIfCommandLine = true;
/**
* @var bool
*/
private $loggerOnly = false;
/**
* Constructor.
* @throws InvalidArgumentException If argument is not null or a LoggerInterface
* @param Psr\Log\LoggerInterface|null $logger
*/
public function __construct($logger = null)
{
$this->setLogger($logger);
}
/**
* Set the output logger interface.
* @throws InvalidArgumentException If argument is not null or a LoggerInterface
* @param Psr\Log\LoggerInterface|null $logger
*/
public function setLogger($logger = null)
{
if(! (is_null($logger)
|| $logger InstanceOf LoggerInterface)) {
throw new InvalidArgumentException(
'Argument to ' . __METHOD__ .
" must be a valid Logger Interface (aka. Monolog), " .
get_class($logger) . ' given.'
);
}
$this->logger = $logger;
}
/**
* @return Psr\Log\LoggerInterface|null
*/
public function getLogger()
{
return $this->logger;
}
/**
* Add error trace to output.
* @param bool|null $addTraceToOutput
* @return null|bool
*/
public function addTraceToOutput($addTraceToOutput = null)
{
if(func_num_args() == 0) {
return $this->addTraceToOutput;
}
$this->addTraceToOutput = (bool) $addTraceToOutput;
}
/**
* Add error trace function arguments to output.
* Set to True for all frame args, or integer for the n first frame args.
* @param bool|integer|null $addTraceFunctionArgsToOutput
* @return null|bool|integer
*/
public function addTraceFunctionArgsToOutput($addTraceFunctionArgsToOutput = null)
{
if(func_num_args() == 0) {
return $this->addTraceFunctionArgsToOutput;
}
if(! is_integer($addTraceFunctionArgsToOutput)) {
$this->addTraceFunctionArgsToOutput = (bool) $addTraceFunctionArgsToOutput;
}
else {
$this->addTraceFunctionArgsToOutput = $addTraceFunctionArgsToOutput;
}
}
/**
* Set the size limit in bytes of frame arguments var_dump output.
* If the limit is reached, the var_dump output is discarded.
* Prevent memory limit errors.
* @var integer
*/
public function setTraceFunctionArgsOutputLimit($traceFunctionArgsOutputLimit)
{
$this->traceFunctionArgsOutputLimit = (integer) $traceFunctionArgsOutputLimit;
}
/**
* Get the size limit in bytes of frame arguments var_dump output.
* If the limit is reached, the var_dump output is discarded.
* Prevent memory limit errors.
* @return integer
*/
public function getTraceFunctionArgsOutputLimit()
{
return $this->traceFunctionArgsOutputLimit;
}
/**
* Restrict error handling to command line calls.
* @param bool|null $onlyForCommandLine
* @return null|bool
*/
public function onlyForCommandLine($onlyForCommandLine = null)
{
if(func_num_args() == 0) {
return $this->onlyForCommandLine;
}
$this->onlyForCommandLine = (bool) $onlyForCommandLine;
}
/**
* Output the error message only if using command line.
* else, output to logger if available.
* Allow to safely add this handler to web pages.
* @param bool|null $outputOnlyIfCommandLine
* @return null|bool
*/
public function outputOnlyIfCommandLine($outputOnlyIfCommandLine = null)
{
if(func_num_args() == 0) {
return $this->outputOnlyIfCommandLine;
}
$this->outputOnlyIfCommandLine = (bool) $outputOnlyIfCommandLine;
}
/**
* Only output to logger.
* @param bool|null $loggerOnly
* @return null|bool
*/
public function loggerOnly($loggerOnly = null)
{
if(func_num_args() == 0) {
return $this->loggerOnly;
}
$this->loggerOnly = (bool) $loggerOnly;
}
/**
* Check, if possible, that this execution was triggered by a command line.
* @return bool
*/
private function isCommandLine()
{
return PHP_SAPI == 'cli';
}
/**
* Test if handler can process the exception..
* @return bool
*/
private function canProcess()
{
return $this->isCommandLine() || !$this->onlyForCommandLine();
}
/**
* Test if handler can output to stdout.
* @return bool
*/
private function canOutput()
{
return ($this->isCommandLine() || ! $this->outputOnlyIfCommandLine())
&& ! $this->loggerOnly();
}
/**
* Get the frame args var_dump.
* @param \Whoops\Exception\Frame $frame [description]
* @param integer $line [description]
* @return string
*/
private function getFrameArgsOutput(Frame $frame, $line)
{
if($this->addTraceFunctionArgsToOutput() === false
|| $this->addTraceFunctionArgsToOutput() < $line) {
return '';
}
// Dump the arguments:
ob_start();
var_dump($frame->getArgs());
if(ob_get_length() > $this->getTraceFunctionArgsOutputLimit()) {
// The argument var_dump is to big.
// Discarded to limit memory usage.
ob_clean();
return sprintf(
"\n%sArguments dump length greater than %d Bytes. Discarded.",
self::VAR_DUMP_PREFIX,
$this->getTraceFunctionArgsOutputLimit()
);
}
return sprintf("\n%s",
preg_replace('/^/m', self::VAR_DUMP_PREFIX, ob_get_clean())
);
}
/**
* Get the exception trace as plain text.
* @return string
*/
private function getTraceOutput()
{
if(! $this->addTraceToOutput()) {
return '';
}
$inspector = $this->getInspector();
$frames = $inspector->getFrames();
$response = "\nStack trace:";
$line = 1;
foreach($frames as $frame) {
/** @var Frame $frame */
$class = $frame->getClass();
$template = "\n%3d. %s->%s() %s:%d%s";
if(! $class) {
// Remove method arrow (->) from output.
$template = "\n%3d. %s%s() %s:%d%s";
}
$response .= sprintf(
$template,
$line,
$class,
$frame->getFunction(),
$frame->getFile(),
$frame->getLine(),
$this->getFrameArgsOutput($frame, $line)
);
$line++;
}
return $response;
}
/**
* @return int
*/
public function handle()
{
if(! $this->canProcess()) {
return Handler::DONE;
}
$exception = $this->getException();
$response = sprintf("%s: %s in file %s on line %d%s\n",
get_class($exception),
$exception->getMessage(),
$exception->getFile(),
$exception->getLine(),
$this->getTraceOutput()
);
if($this->getLogger()) {
$this->getLogger()->addError($response);
}
if(! $this->canOutput()) {
return Handler::DONE;
}
if(class_exists('\Whoops\Util\Misc')
&& \Whoops\Util\Misc::canSendHeaders()) {
header('Content-Type: text/plain');
}
echo $response;
return Handler::QUIT;
}
}
@@ -0,0 +1,443 @@
<?php
/**
* Whoops - php errors for cool kids
* @author Filipe Dobreira <http://github.com/filp>
*/
namespace Whoops\Handler;
use Whoops\TestCase;
use Whoops\Handler\PlainTextHandler;
use RuntimeException;
use StdClass;
class PlainTextHandlerTest extends TestCase
{
/**
* @throws InvalidArgumentException If argument is not null or a LoggerInterface
* @param Psr\Log\LoggerInterface|null $logger
* @return Whoops\Handler\PlainTextHandler
*/
private function getHandler($logger = null)
{
return new PlainTextHandler($logger);
}
/**
* @return RuntimeException
*/
public function getException($message = 'test message')
{
return new RuntimeException($message);
}
/**
* @param bool $withTrace
* @param bool $withTraceArgs
* @param bool $loggerOnly
* @param bool $onlyForCommandLine
* @param bool $outputOnlyIfCommandLine
* @return array
*/
private function getPlainTextFromHandler(
$withTrace = false,
$withTraceArgs = false,
$traceFunctionArgsOutputLimit = 1024,
$loggerOnly = false,
$onlyForCommandLine = false,
$outputOnlyIfCommandLine = true
)
{
$handler = $this->getHandler();
$handler->addTraceToOutput($withTrace);
$handler->addTraceFunctionArgsToOutput($withTraceArgs);
$handler->setTraceFunctionArgsOutputLimit($traceFunctionArgsOutputLimit);
$handler->loggerOnly($loggerOnly);
$handler->onlyForCommandLine($onlyForCommandLine);
$handler->outputOnlyIfCommandLine($outputOnlyIfCommandLine);
$run = $this->getRunInstance();
$run->pushHandler($handler);
$run->register();
$exception = $this->getException();
ob_start();
$run->handleException($exception);
return ob_get_clean();
}
/**
* @covers Whoops\Handler\PlainTextHandler::__construct
* @covers Whoops\Handler\PlainTextHandler::setLogger
* @expectedException InvalidArgumentException
*/
public function testConstructor()
{
$logger = new StdClass(); // guaranteed to be invalid!
$this->getHandler($logger);
}
/**
* @covers Whoops\Handler\PlainTextHandler::setLogger
* @expectedException InvalidArgumentException
*/
public function testSetLogger()
{
$logger = new StdClass(); // guaranteed to be invalid!
$this->getHandler()->setLogger($logger);
}
/**
* @covers Whoops\Handler\PlainTextHandler::addTraceToOutput
*/
public function testAddTraceToOutput()
{
$handler = $this->getHandler();
$handler->addTraceToOutput(true);
$this->assertEquals(true, $handler->addTraceToOutput());
$handler->addTraceToOutput(false);
$this->assertEquals(false, $handler->addTraceToOutput());
$handler->addTraceToOutput(null);
$this->assertEquals(null, $handler->addTraceToOutput());
$handler->addTraceToOutput(1);
$this->assertEquals(true, $handler->addTraceToOutput());
$handler->addTraceToOutput(0);
$this->assertEquals(false, $handler->addTraceToOutput());
$handler->addTraceToOutput('');
$this->assertEquals(false, $handler->addTraceToOutput());
$handler->addTraceToOutput('false');
$this->assertEquals(true, $handler->addTraceToOutput());
}
/**
* @covers Whoops\Handler\PlainTextHandler::addTraceFunctionArgsToOutput
*/
public function testAddTraceFunctionArgsToOutput()
{
$handler = $this->getHandler();
$handler->addTraceFunctionArgsToOutput(true);
$this->assertEquals(true, $handler->addTraceFunctionArgsToOutput());
$handler->addTraceFunctionArgsToOutput(false);
$this->assertEquals(false, $handler->addTraceFunctionArgsToOutput());
$handler->addTraceFunctionArgsToOutput(null);
$this->assertEquals(null, $handler->addTraceFunctionArgsToOutput());
$handler->addTraceFunctionArgsToOutput(1);
$this->assertEquals(1, $handler->addTraceFunctionArgsToOutput());
$handler->addTraceFunctionArgsToOutput(0);
$this->assertEquals(0, $handler->addTraceFunctionArgsToOutput());
$handler->addTraceFunctionArgsToOutput('');
$this->assertEquals(false, $handler->addTraceFunctionArgsToOutput());
$handler->addTraceFunctionArgsToOutput('false');
$this->assertEquals(true, $handler->addTraceFunctionArgsToOutput());
}
/**
* @covers Whoops\Handler\PlainTextHandler::setTraceFunctionArgsOutputLimit
* @covers Whoops\Handler\PlainTextHandler::getTraceFunctionArgsOutputLimit
*/
public function testGetSetTraceFunctionArgsOutputLimit()
{
$addTraceFunctionArgsToOutput = 10240;
$handler = $this->getHandler();
$handler->setTraceFunctionArgsOutputLimit($addTraceFunctionArgsToOutput);
$this->assertEquals($addTraceFunctionArgsToOutput, $handler->getTraceFunctionArgsOutputLimit());
$handler->setTraceFunctionArgsOutputLimit('1024kB');
$this->assertEquals(1024, $handler->getTraceFunctionArgsOutputLimit());
$handler->setTraceFunctionArgsOutputLimit('true');
$this->assertEquals(0, $handler->getTraceFunctionArgsOutputLimit());
}
/**
* @covers Whoops\Handler\PlainTextHandler::onlyForCommandLine
*/
public function testOnlyForCommandLine()
{
$handler = $this->getHandler();
$handler->onlyForCommandLine(true);
$this->assertEquals(true, $handler->onlyForCommandLine());
$handler->onlyForCommandLine(false);
$this->assertEquals(false, $handler->onlyForCommandLine());
$handler->onlyForCommandLine(null);
$this->assertEquals(null, $handler->onlyForCommandLine());
$handler->onlyForCommandLine(1);
$this->assertEquals(true, $handler->onlyForCommandLine());
$handler->onlyForCommandLine(0);
$this->assertEquals(false, $handler->onlyForCommandLine());
$handler->onlyForCommandLine('');
$this->assertEquals(false, $handler->onlyForCommandLine());
$handler->onlyForCommandLine('false');
$this->assertEquals(true, $handler->onlyForCommandLine());
}
/**
* @covers Whoops\Handler\PlainTextHandler::outputOnlyIfCommandLine
*/
public function testOutputOnlyIfCommandLine()
{
$handler = $this->getHandler();
$handler->outputOnlyIfCommandLine(true);
$this->assertEquals(true, $handler->outputOnlyIfCommandLine());
$handler->outputOnlyIfCommandLine(false);
$this->assertEquals(false, $handler->outputOnlyIfCommandLine());
$handler->outputOnlyIfCommandLine(null);
$this->assertEquals(null, $handler->outputOnlyIfCommandLine());
$handler->outputOnlyIfCommandLine(1);
$this->assertEquals(true, $handler->outputOnlyIfCommandLine());
$handler->outputOnlyIfCommandLine(0);
$this->assertEquals(false, $handler->outputOnlyIfCommandLine());
$handler->outputOnlyIfCommandLine('');
$this->assertEquals(false, $handler->outputOnlyIfCommandLine());
$handler->outputOnlyIfCommandLine('false');
$this->assertEquals(true, $handler->outputOnlyIfCommandLine());
}
/**
* @covers Whoops\Handler\PlainTextHandler::loggerOnly
*/
public function testLoggerOnly()
{
$handler = $this->getHandler();
$handler->loggerOnly(true);
$this->assertEquals(true, $handler->loggerOnly());
$handler->loggerOnly(false);
$this->assertEquals(false, $handler->loggerOnly());
$handler->loggerOnly(null);
$this->assertEquals(null, $handler->loggerOnly());
$handler->loggerOnly(1);
$this->assertEquals(true, $handler->loggerOnly());
$handler->loggerOnly(0);
$this->assertEquals(false, $handler->loggerOnly());
$handler->loggerOnly('');
$this->assertEquals(false, $handler->loggerOnly());
$handler->loggerOnly('false');
$this->assertEquals(true, $handler->loggerOnly());
}
/**
* @covers Whoops\Handler\PlainTextHandler::addTraceToOutput
* @covers Whoops\Handler\PlainTextHandler::handle
*/
public function testReturnsWithoutFramesOutput()
{
$text = $this->getPlainTextFromHandler(
$withTrace = false,
$withTraceArgs = true,
$traceFunctionArgsOutputLimit = 1024,
$loggerOnly = false,
$onlyForCommandLine = false,
$outputOnlyIfCommandLine = true
);
// Check that the response has the correct value:
// Check that the trace is NOT returned:
$this->assertEquals(
sprintf(
"%s: %s in file %s on line %d\n",
get_class($this->getException()),
'test message',
__FILE__,
30
),
$text
);
}
/**
* @covers Whoops\Handler\PlainTextHandler::addTraceToOutput
* @covers Whoops\Handler\PlainTextHandler::getTraceOutput
* @covers Whoops\Handler\PlainTextHandler::canProcess
* @covers Whoops\Handler\PlainTextHandler::canOutput
* @covers Whoops\Handler\PlainTextHandler::handle
*/
public function testReturnsWithFramesOutput()
{
$text = $this->getPlainTextFromHandler(
$withTrace = true,
$withTraceArgs = false,
$traceFunctionArgsOutputLimit = 1024,
$loggerOnly = false,
$onlyForCommandLine = false,
$outputOnlyIfCommandLine = true
);
$lines = explode("\n", $text);
// Check that the trace is returned without arguments:
$this->assertCount(18, $lines);
// Check that the response has the correct value:
$this->assertEquals('Stack trace:', $lines[1]);
// Check that the trace is returned:
$this->assertEquals(
sprintf(
'%3d. %s->%s() %s:%d',
2,
'Whoops\Handler\PlainTextHandlerTest',
'getException',
__FILE__,
62
),
$lines[3]
);
}
/**
* @covers Whoops\Handler\PlainTextHandler::addTraceToOutput
* @covers Whoops\Handler\PlainTextHandler::addTraceFunctionArgsToOutput
* @covers Whoops\Handler\PlainTextHandler::getTraceOutput
* @covers Whoops\Handler\PlainTextHandler::getFrameArgsOutput
* @covers Whoops\Handler\PlainTextHandler::canProcess
* @covers Whoops\Handler\PlainTextHandler::canOutput
* @covers Whoops\Handler\PlainTextHandler::handle
*/
public function testReturnsWithFramesAndArgsOutput()
{
$text = $this->getPlainTextFromHandler(
$withTrace = true,
$withTraceArgs = true,
$traceFunctionArgsOutputLimit = 2048,
$loggerOnly = false,
$onlyForCommandLine = false,
$outputOnlyIfCommandLine = true
);
$lines = explode("\n", $text);
// Check that the trace is returned with all arguments:
$this->assertGreaterThan(60, count($lines));
// Check that the response has the correct value:
$this->assertEquals('Stack trace:', $lines[1]);
// Check that the trace is returned:
$this->assertEquals(
sprintf(
'%3d. %s->%s() %s:%d',
2,
'Whoops\Handler\PlainTextHandlerTest',
'getException',
__FILE__,
62
),
$lines[8]
);
// Check that the trace arguments are returned:
$this->assertEquals(sprintf(
'%s string(%d) "%s"',
PlainTextHandler::VAR_DUMP_PREFIX,
strlen('test message'),
'test message'
), $lines[5]
);
}
/**
* @covers Whoops\Handler\PlainTextHandler::addTraceToOutput
* @covers Whoops\Handler\PlainTextHandler::addTraceFunctionArgsToOutput
* @covers Whoops\Handler\PlainTextHandler::getTraceOutput
* @covers Whoops\Handler\PlainTextHandler::getFrameArgsOutput
* @covers Whoops\Handler\PlainTextHandler::canProcess
* @covers Whoops\Handler\PlainTextHandler::canOutput
* @covers Whoops\Handler\PlainTextHandler::handle
*/
public function testReturnsWithFramesAndLimitedArgsOutput()
{
$text = $this->getPlainTextFromHandler(
$withTrace = true,
$withTraceArgs = 3,
$traceFunctionArgsOutputLimit = 1024,
$loggerOnly = false,
$onlyForCommandLine = false,
$outputOnlyIfCommandLine = true
);
$lines = explode("\n", $text);
// Check that the trace is returned with limited level of arguments:
$this->assertCount(41, $lines);
// Check that the response has the correct value:
$this->assertEquals('Stack trace:', $lines[1]);
// Check that the trace is returned:
$this->assertEquals(
sprintf(
'%3d. %s->%s() %s:%d',
2,
'Whoops\Handler\PlainTextHandlerTest',
'getException',
__FILE__,
62
),
$lines[8]
);
// Check that the trace arguments are returned:
$this->assertEquals(sprintf(
'%s string(%d) "%s"',
PlainTextHandler::VAR_DUMP_PREFIX,
strlen('test message'),
'test message'
), $lines[5]
);
}
/**
* @covers Whoops\Handler\PlainTextHandler::loggerOnly
* @covers Whoops\Handler\PlainTextHandler::canProcess
* @covers Whoops\Handler\PlainTextHandler::handle
*/
public function testReturnsWithLoggerOnlyOutput()
{
$text = $this->getPlainTextFromHandler(
$withTrace = true,
$withTraceArgs = true,
$traceFunctionArgsOutputLimit = 1024,
$loggerOnly = true,
$onlyForCommandLine = false,
$outputOnlyIfCommandLine = true
);
// Check that the response has the correct value:
$this->assertEquals('', $text);
}
}