Commit f52b0680 authored by alexpott's avatar alexpott

Issue #2562487 by stefan.r, chx, tim.plunkett, dawehner, alexpott: Get better...

Issue #2562487 by stefan.r, chx, tim.plunkett, dawehner, alexpott: Get better error reporting from __toString
parent 46d59d6c
......@@ -65,6 +65,11 @@ function _drupal_error_handler_real($error_level, $message, $filename, $line, $c
$caller = Error::getLastCaller($backtrace);
// We treat recoverable errors as fatal.
$recoverable = $error_level == E_RECOVERABLE_ERROR;
// As __toString() methods must not throw exceptions (recoverable errors)
// in PHP, we allow them to trigger a fatal error by emitting a user error
// using trigger_error().
$to_string = $error_level == E_USER_ERROR && substr($caller['function'], -strlen('__toString()')) == '__toString()';
_drupal_log_error(array(
'%type' => isset($types[$error_level]) ? $severity_msg : 'Unknown error',
// The standard PHP error handler considers that the error messages
......@@ -75,7 +80,7 @@ function _drupal_error_handler_real($error_level, $message, $filename, $line, $c
'%line' => $caller['line'],
'severity_level' => $severity_level,
'backtrace' => $backtrace,
), $error_level == E_RECOVERABLE_ERROR);
), $recoverable || $to_string);
}
}
......
......@@ -5,6 +5,7 @@
* Contains \Drupal\Component\Datetime\DateTimePlus.
*/
namespace Drupal\Component\Datetime;
use Drupal\Component\Utility\ToStringTrait;
/**
* Wraps DateTime().
......@@ -28,6 +29,8 @@
*/
class DateTimePlus {
use ToStringTrait;
const FORMAT = 'Y-m-d H:i:s';
/**
......@@ -271,16 +274,12 @@ public function __construct($time = 'now', $timezone = NULL, $settings = array()
}
/**
* Implements __toString() for dates.
*
* The DateTime class does not implement this.
* Renders the timezone name.
*
* @see https://bugs.php.net/bug.php?id=62911
* @see http://www.serverphorums.com/read.php?7,555645
* @return string
*/
public function __toString() {
$format = static::FORMAT;
return $this->format($format) . ' ' . $this->getTimeZone()->getName();
protected function render() {
return $this->format(static::FORMAT) . ' ' . $this->getTimeZone()->getName();
}
/**
......
<?php
/**
* @file
* Contains \Drupal\Component\Utility\ToStringTrait.
*/
namespace Drupal\Component\Utility;
/**
* Wraps __toString in a trait to avoid some fatals.
*/
trait ToStringTrait {
/**
* Implements the magic __toString() method.
*/
public function __toString() {
try {
return (string) $this->render();
}
catch (\Exception $e) {
// User errors in __toString() methods are considered fatal in the Drupal
// error handler.
trigger_error(get_class($e) . ' thrown while calling __toString on a ' . get_class($this) . ' object in ' . $e->getFile() . ' on line ' . $e->getLine() . ': ' . $e->getMessage(), E_USER_ERROR);
// In case we are using another error handler that did not fatal on the
// E_USER_ERROR, we terminate execution. However, for test purposes allow
// a return value.
return $this->_die();
}
}
/**
* For test purposes, wrap die() in an overridable method.
*/
protected function _die() {
die();
}
/**
* Renders the object as a string.
*
* @return string|object
* The rendered string or an object implementing __toString().
*/
abstract public function render();
}
......@@ -8,6 +8,7 @@
namespace Drupal\Core\StringTranslation;
use Drupal\Component\Utility\SafeStringInterface;
use Drupal\Component\Utility\ToStringTrait;
/**
* Provides a class to wrap a translatable string.
......@@ -19,7 +20,9 @@
* @see \Drupal\Core\Annotation\Translation
*/
class TranslationWrapper implements SafeStringInterface {
use StringTranslationTrait;
use ToStringTrait;
/**
* The string to be translated.
......@@ -93,14 +96,6 @@ public function getOption($name) {
public function getOptions() {
return $this->options;
}
/**
* Implements the magic __toString() method.
*/
public function __toString() {
return $this->render();
}
/**
* Renders the object as a string.
*
......@@ -128,4 +123,5 @@ public function jsonSerialize() {
return $this->__toString();
}
}
<?php
/**
* @file
* Contains \Drupal\Tests\Core\StringTranslation\TranslationWrapperTest.
*/
namespace Drupal\Tests\Core\StringTranslation;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Tests\UnitTestCase;
/**
* Tests the TranslationWrapper class.
*
* @coversDefaultClass \Drupal\Core\StringTranslation\TranslationWrapper
* @group StringTranslation
*/
class TranslationWrapperTest extends UnitTestCase {
/**
* The error message of the last error in the error handler.
*
* @var string
*/
protected $lastErrorMessage;
/**
* The error number of the last error in the error handler.
*
* @var int
*/
protected $lastErrorNumber;
/**
* Custom error handler that saves the last error.
*
* We need this custom error handler because we cannot rely on the error to
* exception conversion as __toString is never allowed to leak any kind of
* exception.
*
* @param int $error_number
* The error number.
* @param string $error_message
* The error message.
*/
public function errorHandler($error_number, $error_message) {
$this->lastErrorNumber = $error_number;
$this->lastErrorMessage = $error_message;
}
/**
* Tests that errors are correctly handled when a __toString() fails.
*
* @covers ::__toString
*/
public function testToString() {
$string = 'May I have an exception please?';
$text = $this->getMockBuilder('Drupal\Core\StringTranslation\TranslationWrapper')
->setConstructorArgs([$string])
->setMethods(['_die'])
->getMock();
$text
->expects($this->once())
->method('_die')
->willReturn('');
$translation = $this->prophesize(TranslationInterface::class);
$translation->translate($string, [], [])->will(function () {
throw new \Exception('Yes you may.');
});
$text->setStringTranslation($translation->reveal());
// We set a custom error handler because of https://github.com/sebastianbergmann/phpunit/issues/487
set_error_handler([$this, 'errorHandler']);
// We want this to trigger an error.
(string) $text;
restore_error_handler();
$this->assertEquals(E_USER_ERROR, $this->lastErrorNumber);
$this->assertRegExp('/Exception thrown while calling __toString on a .*Mock_TranslationWrapper_.* object in .*TranslationWrapperTest.php on line [0-9]+: Yes you may./', $this->lastErrorMessage);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment