From 10c8d5eadbc100b4cc63c56d40633edfeec70db7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 9 Feb 2016 17:23:20 +0100 Subject: [PATCH] [PropertyAccess] Throw an UnexpectedTypeException when the type do not match --- .../PropertyAccess/PropertyAccessor.php | 71 ++++++++++++++++++- .../PropertyAccessorInterface.php | 1 - .../Tests/Fixtures/TypeHinted.php | 30 ++++++++ .../Tests/PropertyAccessorTest.php | 19 +++++ 4 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Fixtures/TypeHinted.php diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 3b9b49490d..40ba789900 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -400,6 +400,7 @@ class PropertyAccessor implements PropertyAccessorInterface * * @throws NoSuchPropertyException If the property does not exist or is not * public. + * @throws UnexpectedTypeException */ private function writeProperty(&$object, $property, $singular, $value) { @@ -410,7 +411,7 @@ class PropertyAccessor implements PropertyAccessorInterface $access = $this->getWriteAccessInfo($object, $property, $singular, $value); if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) { - $object->{$access[self::ACCESS_NAME]}($value); + $this->callMethod($object, $access[self::ACCESS_NAME], $value); } elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) { $object->{$access[self::ACCESS_NAME]} = $value; } elseif (self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]) { @@ -457,12 +458,78 @@ class PropertyAccessor implements PropertyAccessorInterface $object->$property = $value; } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) { - $object->{$access[self::ACCESS_NAME]}($value); + $this->callMethod($object, $access[self::ACCESS_NAME], $value); } else { throw new NoSuchPropertyException($access[self::ACCESS_NAME]); } } + /** + * Throws a {@see UnexpectedTypeException} as in PHP 7 when using PHP 5. + * + * @param object $object + * @param string $method + * @param mixed $value + * + * @throws UnexpectedTypeException + * @throws \Exception + */ + private function callMethod($object, $method, $value) { + if (PHP_MAJOR_VERSION >= 7) { + try { + $object->{$method}($value); + } catch (\TypeError $e) { + throw $this->createUnexpectedTypeException($object, $method, $value); + } + + return; + } + + $that = $this; + set_error_handler(function ($errno, $errstr) use ($object, $method, $value, $that) { + if (E_RECOVERABLE_ERROR === $errno && false !== strpos($errstr, sprintf('passed to %s::%s() must', get_class($object), $method))) { + throw $that->createUnexpectedTypeException($object, $method, $value); + } + + return false; + }); + + try { + $object->{$method}($value); + restore_error_handler(); + } catch (\Exception $e) { + // Cannot use finally in 5.5 because of https://bugs.php.net/bug.php?id=67047 + restore_error_handler(); + + throw $e; + } + } + + /** + * Creates an UnexpectedTypeException. + * + * @param object $object + * @param string $method + * @param mixed $value + * + * @return UnexpectedTypeException + */ + private function createUnexpectedTypeException($object, $method, $value) + { + $reflectionMethod = new \ReflectionMethod($object, $method); + $parameters = $reflectionMethod->getParameters(); + + $expectedType = 'unknown'; + if (isset($parameters[0])) { + $class = $parameters[0]->getClass(); + if (null !== $class) { + $expectedType = $class->getName(); + } + } + + return new UnexpectedTypeException($value, $expectedType); + } + /** * Guesses how to write the property value. * diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php b/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php index ecedabc134..755f5ccb3d 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php @@ -45,7 +45,6 @@ interface PropertyAccessorInterface * * @throws Exception\NoSuchPropertyException If a property does not exist or is not public. * @throws Exception\UnexpectedTypeException If a value within the path is neither object - * nor array */ public function setValue(&$objectOrArray, $propertyPath, $value); diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TypeHinted.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TypeHinted.php new file mode 100644 index 0000000000..ca4c5745ae --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TypeHinted.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +/** + * @author Kévin Dunglas + */ +class TypeHinted +{ + private $date; + + public function setDate(\DateTime $date) + { + $this->date = $date; + } + + public function getDate() + { + return $this->date; + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index 51bc6eabc2..85ea848027 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -16,6 +16,7 @@ use Symfony\Component\PropertyAccess\Tests\Fixtures\Author; use Symfony\Component\PropertyAccess\Tests\Fixtures\Magician; use Symfony\Component\PropertyAccess\Tests\Fixtures\MagicianCall; use Symfony\Component\PropertyAccess\Tests\Fixtures\Ticket5775Object; +use Symfony\Component\PropertyAccess\Tests\Fixtures\TypeHinted; class PropertyAccessorTest extends \PHPUnit_Framework_TestCase { @@ -403,4 +404,22 @@ class PropertyAccessorTest extends \PHPUnit_Framework_TestCase array(array('root' => array('index' => array())), '[root][index][firstName]', null), ); } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException + * @expectedExceptionMessage Expected argument of type "DateTime", "string" given + */ + public function testThrowTypeError() + { + $this->propertyAccessor->setValue(new TypeHinted(), 'date', 'This is a string, \DateTime excepted.'); + } + + public function testSetTypeHint() + { + $date = new \DateTime(); + $object = new TypeHinted(); + + $this->propertyAccessor->setValue($object, 'date', $date); + $this->assertSame($date, $object->getDate()); + } }