Update conversion to strings on exceptions

Many changes were made on `ValidationException::stringify`:
- Add support for instances of `Exception`;
- Add support for instances of `Traversable`;
- Add support for resources;
- Improve `Array` conversion;
- Improve `Object` conversion;
- Improve conversion of all values by using JSON.

Now, all the parameters of the exception classes are just converted to
string when replacing parameters on exceptions, so the exception classes
now keep the original value of all parameters.
This commit is contained in:
Henrique Moody 2015-08-26 14:30:45 -03:00
parent c4e6f0875e
commit 748b280c34
28 changed files with 235 additions and 151 deletions

View file

@ -30,14 +30,6 @@ class AgeException extends AbstractNestedException
),
);
public function configure($name, array $params = array())
{
$params['minAge'] = static::stringify($params['minAge']);
$params['maxAge'] = static::stringify($params['maxAge']);
return parent::configure($name, $params);
}
public function chooseTemplate()
{
if (!$this->getParam('minAge')) {

View file

@ -16,11 +16,11 @@ class AlnumException extends AlphaException
public static $defaultTemplates = array(
self::MODE_DEFAULT => array(
self::STANDARD => '{{name}} must contain only letters (a-z) and digits (0-9)',
self::EXTRA => '{{name}} must contain only letters (a-z), digits (0-9) and "{{additionalChars}}"',
self::EXTRA => '{{name}} must contain only letters (a-z), digits (0-9) and {{additionalChars}}',
),
self::MODE_NEGATIVE => array(
self::STANDARD => '{{name}} must not contain letters (a-z) or digits (0-9)',
self::EXTRA => '{{name}} must not contain letters (a-z), digits (0-9) or "{{additionalChars}}"',
self::EXTRA => '{{name}} must not contain letters (a-z), digits (0-9) or {{additionalChars}}',
),
);
}

View file

@ -30,14 +30,6 @@ class BetweenException extends AbstractNestedException
),
);
public function configure($name, array $params = array())
{
$params['minValue'] = static::stringify($params['minValue']);
$params['maxValue'] = static::stringify($params['maxValue']);
return parent::configure($name, $params);
}
public function chooseTemplate()
{
if (!$this->getParam('minValue')) {

View file

@ -23,10 +23,10 @@ class ExtensionException extends ValidationException
*/
public static $defaultTemplates = array(
self::MODE_DEFAULT => array(
self::STANDARD => '{{name}} must have "{{extension}}" extension',
self::STANDARD => '{{name}} must have {{extension}} extension',
),
self::MODE_NEGATIVE => array(
self::STANDARD => '{{name}} must not have "{{extension}}" extension',
self::STANDARD => '{{name}} must not have {{extension}} extension',
),
);
}

View file

@ -15,10 +15,10 @@ class InException extends ValidationException
{
public static $defaultTemplates = array(
self::MODE_DEFAULT => array(
self::STANDARD => '{{name}} must be in ({{haystack}})',
self::STANDARD => '{{name}} must be in {{haystack}}',
),
self::MODE_NEGATIVE => array(
self::STANDARD => '{{name}} must not be in ({{haystack}})',
self::STANDARD => '{{name}} must not be in {{haystack}}',
),
);
}

View file

@ -42,16 +42,4 @@ class KeySetException extends AbstractGroupedException
return parent::chooseTemplate();
}
/**
* {@inheritdoc}
*/
public function setParam($name, $value)
{
if ($name === 'keys') {
$value = trim(json_encode($value), '[]');
}
return parent::setParam($name, $value);
}
}

View file

@ -30,14 +30,6 @@ class LengthException extends ValidationException
),
);
public function configure($name, array $params = array())
{
$params['minValue'] = static::stringify($params['minValue']);
$params['maxValue'] = static::stringify($params['maxValue']);
return parent::configure($name, $params);
}
public function chooseTemplate()
{
if (!$this->getParam('minValue')) {

View file

@ -23,10 +23,10 @@ class MimetypeException extends ValidationException
*/
public static $defaultTemplates = array(
self::MODE_DEFAULT => array(
self::STANDARD => '{{name}} must have "{{mimetype}}" mimetype',
self::STANDARD => '{{name}} must have {{mimetype}} mimetype',
),
self::MODE_NEGATIVE => array(
self::STANDARD => '{{name}} must not have "{{mimetype}}" mimetype',
self::STANDARD => '{{name}} must not have {{mimetype}} mimetype',
),
);
}

View file

@ -12,7 +12,9 @@
namespace Respect\Validation\Exceptions;
use DateTime;
use Exception;
use InvalidArgumentException;
use Traversable;
class ValidationException extends InvalidArgumentException implements ValidationExceptionInterface
{
@ -27,10 +29,22 @@ class ValidationException extends InvalidArgumentException implements Validation
self::STANDARD => 'Data validation failed for %s',
),
);
/**
* @var int Maximum depth when stringifying nested arrays
* @var int
*/
private static $maxDepthStringify = 3;
private static $maxDepthStringify = 5;
/**
* @var int
*/
private static $maxCountStringify = 10;
/**
* @var string
*/
private static $maxReplacementStringify = '...';
protected $id = 'validation';
protected $mode = self::MODE_DEFAULT;
protected $name = '';
@ -42,23 +56,61 @@ class ValidationException extends InvalidArgumentException implements Validation
return preg_replace_callback(
'/{{(\w+)}}/',
function ($match) use ($vars) {
return isset($vars[$match[1]]) ? $vars[$match[1]] : $match[0];
if (!isset($vars[$match[1]])) {
return $match[0];
}
$value = $vars[$match[1]];
if ('name' == $match[1]) {
return $value;
}
return ValidationException::stringify($value);
},
$template
);
}
public static function stringify($value)
/**
* @param mixed $value
* @param int $depth
*
* @return string
*/
public static function stringify($value, $depth = 1)
{
if (is_string($value)) {
return $value;
} elseif (is_array($value)) {
return self::stringifyArray($value);
} elseif (is_object($value)) {
return static::stringifyObject($value);
} else {
return (string) $value;
if ($depth >= self::$maxDepthStringify) {
return self::$maxReplacementStringify;
}
if (is_array($value)) {
return static::stringifyArray($value, $depth);
}
if (is_object($value)) {
return static::stringifyObject($value, $depth);
}
if (is_resource($value)) {
return sprintf('`[resource] (%s)`', get_resource_type($value));
}
if (is_float($value)) {
if (is_infinite($value)) {
return ($value > 0 ? '' : '-').'INF';
}
if (is_nan($value)) {
return 'NaN';
}
}
$options = 0;
if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
$options = (JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
return (@json_encode($value, $options) ?: $value);
}
/**
@ -67,37 +119,77 @@ class ValidationException extends InvalidArgumentException implements Validation
*
* @return string
*/
private static function stringifyArray($value, $depth = 0)
public static function stringifyArray(array $value, $depth = 1)
{
$items = array();
foreach ($value as $val) {
if (is_object($val)) {
$items[] = self::stringifyObject($val);
} elseif (is_array($val)) {
if ($depth >= self::$maxDepthStringify) {
$items[] = '...';
} else {
$items[] = '('.self::stringifyArray($val, $depth + 1).')';
}
} elseif (is_string($val)) {
$items[] = "'$val'";
} else {
$items[] = (string) $val;
$nextDepth = ($depth + 1);
if ($nextDepth >= self::$maxDepthStringify) {
return self::$maxReplacementStringify;
}
if (empty($value)) {
return '{ }';
}
$total = count($value);
$string = '';
$current = 0;
foreach ($value as $childKey => $childValue) {
if ($current++ >= self::$maxCountStringify) {
$string .= self::$maxReplacementStringify;
break;
}
if (!is_int($childKey)) {
$string .= sprintf('%s: ', static::stringify($childKey, $nextDepth));
}
$string .= static::stringify($childValue, $nextDepth);
if ($current !== $total) {
$string .= ', ';
}
}
return implode(', ', $items);
return sprintf('{ %s }', $string);
}
public static function stringifyObject($value)
/**
* @param mixed $value
* @param int $depth
*
* @return string
*/
public static function stringifyObject($value, $depth = 2)
{
if (method_exists($value, '__toString')) {
return (string) $value;
} elseif ($value instanceof DateTime) {
return $value->format('Y-m-d H:i:s');
} else {
return 'Object of class '.get_class($value);
$nextDepth = $depth + 1;
if ($value instanceof DateTime) {
return sprintf('"%s"', $value->format('Y-m-d H:i:s'));
}
$class = get_class($value);
if ($value instanceof Traversable) {
return sprintf('`[traversable] (%s: %s)`', $class, static::stringify(iterator_to_array($value), $nextDepth));
}
if ($value instanceof Exception) {
$properties = array(
'message' => $value->getMessage(),
'code' => $value->getCode(),
'file' => $value->getFile().':'.$value->getLine(),
);
return sprintf('`[exception] (%s: %s)`', $class, static::stringify($properties, $nextDepth));
}
if (method_exists($value, '__toString')) {
return static::stringify($value->__toString(), $nextDepth);
}
$properties = static::stringify(get_object_vars($value), $nextDepth);
return sprintf('`[object] (%s: %s)`', $class, str_replace('`', '', $properties));
}
public function __toString()
@ -175,7 +267,7 @@ class ValidationException extends InvalidArgumentException implements Validation
public function setName($name)
{
$this->name = static::stringify($name);
$this->name = $name;
return $this;
}
@ -190,7 +282,7 @@ class ValidationException extends InvalidArgumentException implements Validation
public function setParam($key, $value)
{
$this->params[$key] = ($key == 'translator') ? $value : static::stringify($value);
$this->params[$key] = $value;
return $this;
}

View file

@ -61,8 +61,7 @@ abstract class AbstractRule implements Validatable
public function reportError($input, array $extraParams = array())
{
$exception = $this->createException();
$input = ValidationException::stringify($input);
$name = $this->name ?: "\"$input\"";
$name = $this->name ?: ValidationException::stringify($input);
$params = array_merge(
get_class_vars(__CLASS__),
get_object_vars($this),

View file

@ -208,28 +208,9 @@ class Validator extends AllOf
return $this->addRule(static::buildRule($method, $arguments));
}
/**
* @param mixed $input
* @param array $extraParams
*
* @return AllOfException
*/
public function reportError($input, array $extraParams = array())
protected function createException()
{
$exception = new AllOfException();
$input = AllOfException::stringify($input);
$name = $this->getName() ?: "\"$input\"";
$params = array_merge(
$extraParams,
get_object_vars($this),
get_class_vars(__CLASS__)
);
$exception->configure($name, $params);
if (!is_null($this->template)) {
$exception->setTemplate($this->template);
}
return $exception;
return new AllOfException();
}
/**

View file

@ -47,5 +47,5 @@ try {
Array
(
[allOf] => Invalid Validation Form
[first_name_length] => Invalid length for security_question fiif
[first_name_length] => Invalid length for security_question "fiif"
)

View file

@ -40,6 +40,6 @@ try {
--EXPECTF--
Array
(
[allOf] => These rules must pass for Validation Form
[allOf] => All of the required rules must pass for Validation Form
[first_name_length] => security_question must have a length between 5 and 256
)

View file

@ -14,6 +14,6 @@ try {
}
?>
--EXPECTF--
\-These rules must pass for "0"
|-"0" must be a string
\-"0" must have a length between 2 and 15
\-All of the required rules must pass for 0
|-0 must be a string
\-0 must have a length between 2 and 15

View file

@ -20,7 +20,7 @@ try {
}
?>
--EXPECTF--
\-These rules must pass for User Subscription Form
\-All of the required rules must pass for User Subscription Form
|-Key username must be valid
| \-username must have a length between 2 and 32
\-Key birthdate must be valid

View file

@ -25,4 +25,4 @@ try {
}
?>
--EXPECTF--
\-These rules must not pass for "2"
\-These rules must not pass for 2

View file

@ -6,6 +6,7 @@ require 'vendor/autoload.php';
use Respect\Validation\Exceptions\NestedValidationExceptionInterface;
use Respect\Validation\Validator;
use Respect\Validation\Rules\Callback;
try {
Validator::callback('is_int')->setTemplate('{{name}} is not tasty')->assert('something');

View file

@ -11,6 +11,12 @@
namespace Respect\Validation\Exceptions;
use ArrayIterator;
use DateTime;
use Exception;
use SplFileInfo;
use stdClass;
class ValidationExceptionTest extends \PHPUnit_Framework_TestCase
{
public function testItImplementsValidationExceptionInterface()
@ -42,7 +48,7 @@ class ValidationExceptionTest extends \PHPUnit_Framework_TestCase
*/
public function testStringifyShouldConvertStringsProperly($input, $result)
{
$this->assertEquals(
$this->assertStringMatchesFormat(
$result,
ValidationException::stringify($input)
);
@ -67,19 +73,6 @@ class ValidationExceptionTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('foo', $x->getTemplate());
}
/**
* @dataProvider providerForStringify
*/
public function testSettingExceptionParamsMakesThemAvailable($input, $expected)
{
$x = new ValidationException();
$x->setParam('foo', $input);
$this->assertEquals(
$expected,
$x->getParam('foo')
);
}
/**
* @link https://github.com/Respect/Validation/pull/214
*/
@ -91,20 +84,73 @@ class ValidationExceptionTest extends \PHPUnit_Framework_TestCase
public function providerForStringify()
{
$object1 = new SplFileInfo('stringify.phpt'); // __toString()
$object2 = new DateTime('1988-09-09 23:59:59');
$object3 = new stdClass();
$object4 = new stdClass();
$object4->foo = 1;
$object4->bar = false;
$object5 = new stdClass();
$objectRecursive = $object5;
for ($i = 0; $i < 10; ++$i) {
$objectRecursive->name = new stdClass();
$objectRecursive = $objectRecursive->name;
}
$exception = new Exception('My message');
$iterator1 = new ArrayIterator(array(1, 2, 3));
$iterator2 = new ArrayIterator(array('a' => 1, 'b' => 2, 'c' => 3));
return array(
array('foo', 'foo'),
array('', '""'),
array('foo', '"foo"'),
array(INF, 'INF'),
array(-INF, '-INF'),
array(acos(4), 'NaN'),
array(123, '123'),
array(array(), ''),
array(array(array(), 'foo'), "(), 'foo'"),
array(array(array(1), 'foo'), "(1), 'foo'"),
array(array(1, array(2, array(3))), '1, (2, (3))'),
array(array(1, array(2, array(3, array(4)))), '1, (2, (3, (4)))'),
array(array(1, array(2, array(3, array(4, array(5))))), '1, (2, (3, (4, ...)))'),
array(array('foo', 'bar'), "'foo', 'bar'"),
array(array('foo', -1), "'foo', -1"),
array(array(new \stdClass, 'foo'), "Object of class stdClass, 'foo'"),
array(new \stdClass, 'Object of class stdClass'),
array($x = new \DateTime, $x->format('Y-m-d H:i:s')),
array(123.456, '123.456'),
array(array(), '{ }'),
array(array(false), '{ false }'),
array(array(1,2,3,4,5,6,7,8,9,10), '{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }'),
array(range(1, 80), '{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ... }'),
array(
array('foo' => true, 'bar' => array('baz' => 123, 'qux' => array(1, 2, 3))),
'{ "foo": true, "bar": { "baz": 123, "qux": { 1, 2, 3 } } }'
),
array(
array('foo' => true, 'bar' => array('baz' => 123, 'qux' => array('norf' => array(1,2,3)))),
'{ "foo": true, "bar": { "baz": 123, "qux": { "norf": ... } } }'
),
array(array(array(), 'foo'), '{ { }, "foo" }'),
array(array(array(1), 'foo'), '{ { 1 }, "foo" }'),
array(array(1, array(2, array(3))), '{ 1, { 2, { 3 } } }'),
array(array(1, array(2, array(3, array(4)))), '{ 1, { 2, { 3, ... } } }'),
array(array(1, array(2, array(3, array(4, array(5))))), '{ 1, { 2, { 3, ... } } }'),
array(array('foo', 'bar'), '{ "foo", "bar" }'),
array(array('foo', -1), '{ "foo", -1 }'),
array($object1, '"stringify.phpt"'),
array($object2, sprintf('"%s"', $object2->format('Y-m-d H:i:s'))),
array($object3, '`[object] (stdClass: { })`'),
array($object4, '`[object] (stdClass: { "foo": 1, "bar": false })`'),
array($object5, '`[object] (stdClass: { "name": [object] (stdClass: ...) })`'),
array(
$exception,
'`[exception] (Exception: { "message": "My message", "code": 0, "file": "%s:%d" })`'
),
array($iterator1, '`[traversable] (ArrayIterator: { 1, 2, 3 })`'),
array($iterator2, '`[traversable] (ArrayIterator: { "a": 1, "b": 2, "c": 3 })`'),
array(stream_context_create(), '`[resource] (stream-context)`'),
array(tmpfile(), '`[resource] (stream)`'),
array(xml_parser_create(), '`[resource] (xml)`'),
array(
array($object4, array(42, 43), true, null, tmpfile()),
'{ `[object] (stdClass: { "foo": 1, "bar": false })`, { 42, 43 }, true, null, `[resource] (stream)` }'
),
);
}
@ -113,17 +159,17 @@ class ValidationExceptionTest extends \PHPUnit_Framework_TestCase
return array(
array(
'{{foo}} {{bar}} {{baz}}',
'hello world respect',
'"hello" "world" "respect"',
array('foo' => 'hello', 'bar' => 'world', 'baz' => 'respect'),
),
array(
'{{foo}} {{bar}} {{baz}}',
'hello {{bar}} respect',
'"hello" {{bar}} "respect"',
array('foo' => 'hello', 'baz' => 'respect'),
),
array(
'{{foo}} {{bar}} {{baz}}',
'hello {{bar}} respect',
'"hello" {{bar}} "respect"',
array('foo' => 'hello', 'bot' => 111, 'baz' => 'respect'),
),
);

View file

@ -43,7 +43,7 @@ class FiniteTest extends \PHPUnit_Framework_TestCase
/**
* @expectedException Respect\Validation\Exceptions\FiniteException
* @expectedExceptionMessage "INF" must be a finite number
* @expectedExceptionMessage INF must be a finite number
*/
public function testShouldThrowFiniteExceptionWhenChecking()
{

View file

@ -42,7 +42,7 @@ class InTest extends \PHPUnit_Framework_TestCase
/**
* @expectedException Respect\Validation\Exceptions\InException
* @expectedExceptionMessage "x" must be in ('foo', 'bar')
* @expectedExceptionMessage "x" must be in { "foo", "bar" }
*/
public function testInCheckExceptionMessageWithArray()
{

View file

@ -43,7 +43,7 @@ class InfiniteTest extends \PHPUnit_Framework_TestCase
/**
* @expectedException Respect\Validation\Exceptions\InfiniteException
* @expectedExceptionMessage "123456" must be an infinite number
* @expectedExceptionMessage 123456 must be an infinite number
*/
public function testShouldThrowInfiniteExceptionWhenChecking()
{
@ -54,6 +54,7 @@ class InfiniteTest extends \PHPUnit_Framework_TestCase
{
return array(
array(INF),
array(INF * -1),
);
}

View file

@ -145,7 +145,7 @@ class KeySetTest extends PHPUnit_Framework_TestCase
/**
* @expectedException Respect\Validation\Exceptions\KeySetException
* @expectedExceptionMessage Must have keys "foo","bar"
* @expectedExceptionMessage Must have keys { "foo", "bar" }
*/
public function testShouldCheckKeys()
{
@ -160,7 +160,7 @@ class KeySetTest extends PHPUnit_Framework_TestCase
/**
* @expectedException Respect\Validation\Exceptions\KeySetException
* @expectedExceptionMessage Must have keys "foo","bar"
* @expectedExceptionMessage Must have keys { "foo", "bar" }
*/
public function testShouldAssertKeys()
{

View file

@ -96,7 +96,7 @@ class MimetypeTest extends PHPUnit_Framework_TestCase
/**
* @expectedException Respect\Validation\Exceptions\MimetypeException
* @expectedExceptionMessage MimetypeTest.php" must have "application/json" mimetype
* @expectedExceptionMessageRegExp #".+/MimetypeTest.php" must have "application.?/json" mimetype#
*/
public function testShouldThowsMimetypeExceptionWhenCheckingValue()
{

View file

@ -43,7 +43,7 @@ class ScalarTest extends \PHPUnit_Framework_TestCase
/**
* @expectedException Respect\Validation\Exceptions\ScalarException
* @expectedExceptionMessage "" must be a scalar value
* @expectedExceptionMessage null must be a scalar value
*/
public function testShouldThrowScalarExceptionWhenChecking()
{

View file

@ -60,7 +60,7 @@ class SfTest extends \PHPUnit_Framework_TestCase
} catch (\Respect\Validation\Exceptions\AllOfException $exception) {
$fullValidationMessage = $exception->getFullMessage();
$expectedValidationException = <<<EOF
\-These rules must pass for "34:90:70"
\-All of the required rules must pass for "34:90:70"
\-Time
EOF;

View file

@ -112,7 +112,7 @@ class SizeTest extends PHPUnit_Framework_TestCase
/**
* @expectedException Respect\Validation\Exceptions\SizeException
* @expectedExceptionMessage "vfs://root/1gb.txt" must be greater than 2pb
* @expectedExceptionMessageRegExp #"vfs:.?/.?/root.?/1gb.txt" must be greater than "2pb"#
*/
public function testShouldThrowsSizeExceptionWhenAsserting()
{

View file

@ -66,7 +66,7 @@ class TypeTest extends \PHPUnit_Framework_TestCase
/**
* @expectedException Respect\Validation\Exceptions\TypeException
* @expectedExceptionMessage "Something" must be integer
* @expectedExceptionMessage "Something" must be "integer"
*/
public function testShouldThrowTypeExceptionWhenCheckingAnInvalidInput()
{

View file

@ -47,7 +47,7 @@ class WhenTest extends \PHPUnit_Framework_TestCase
/**
* @expectedException Respect\Validation\Exceptions\AlwaysInvalidException
* @expectedExceptionMessage "15" is not valid
* @expectedExceptionMessage 15 is not valid
*/
public function testWhenWithoutElseAssert()
{