diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 87e9c71266ef7..38f9afb7ad442 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -543,6 +543,7 @@ private function writeIndex(&$array, $index, $value) * * @throws NoSuchPropertyException If the property does not exist or is not * public. + * @throws \TypeError */ private function writeProperty(&$object, $property, $value) { @@ -553,7 +554,7 @@ private function writeProperty(&$object, $property, $value) $access = $this->getWriteAccessInfo($object, $property, $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]) { @@ -567,12 +568,48 @@ private function writeProperty(&$object, $property, $value) $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 \TypeError} as in PHP 7 when using PHP 5. + * + * @param object $object + * @param string $method + * @param mixed $value + * + * @throws \TypeError + * @throws \Exception + */ + private function callMethod($object, $method, $value) { + if (PHP_MAJOR_VERSION >= 7) { + $object->{$method}($value); + + return; + } + + set_error_handler(function ($errno, $errstr) use ($object, $method) { + if (E_RECOVERABLE_ERROR === $errno && false !== strpos($errstr, sprintf('passed to %s::%s() must', get_class($object), $method))) { + throw new \TypeError($errstr); + } + + 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; + } + } + /** * Adjusts a collection-valued property by calling add*() and remove*() * methods. @@ -582,6 +619,8 @@ private function writeProperty(&$object, $property, $value) * @param array|\Traversable $collection The collection to write * @param string $addMethod The add*() method * @param string $removeMethod The remove*() method + * + * @throws \TypeError */ private function writeCollection($object, $property, $collection, $addMethod, $removeMethod) { @@ -613,11 +652,11 @@ private function writeCollection($object, $property, $collection, $addMethod, $r } foreach ($itemToRemove as $item) { - $object->{$removeMethod}($item); + $this->callMethod($object, $removeMethod, $item); } foreach ($itemsToAdd as $item) { - $object->{$addMethod}($item); + $this->callMethod($object, $addMethod, $item); } } diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php b/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php index be41ee175d985..5def1f4555cd2 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php @@ -47,6 +47,8 @@ interface PropertyAccessorInterface * @throws Exception\AccessException If a property/index does not exist or is not public * @throws Exception\UnexpectedTypeException If a value within the path is neither object * nor array + * @throws \TypeError If a the type of the value does not match the type + * of the parameter of the mutator method */ public function setValue(&$objectOrArray, $propertyPath, $value); diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClass.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClass.php index 7b1b927529afe..e63af3a8bac5d 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClass.php +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClass.php @@ -26,6 +26,7 @@ class TestClass private $publicIsAccessor; private $publicHasAccessor; private $publicGetter; + private $date; public function __construct($value) { @@ -173,4 +174,14 @@ public function getPublicGetter() { return $this->publicGetter; } + + public function setDate(\DateTimeInterface $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 ce4438550e8d3..f2ab76d1cdff5 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -510,4 +510,21 @@ public function testIsWritableForReferenceChainIssue($object, $path, $value) { $this->assertEquals($value, $this->propertyAccessor->isWritable($object, $path)); } + + /** + * @expectedException \TypeError + */ + public function testThrowTypeError() + { + $this->propertyAccessor->setValue(new TestClass('Kévin'), 'date', 'This is a string, \DateTime excepted.'); + } + + public function testSetTypeHint() + { + $date = new \DateTimeImmutable(); + $object = new TestClass('Kévin'); + + $this->propertyAccessor->setValue($object, 'date', $date); + $this->assertSame($date, $object->getDate()); + } } diff --git a/src/Symfony/Component/PropertyAccess/composer.json b/src/Symfony/Component/PropertyAccess/composer.json index fc657c1d21f9f..2cecd9c133071 100644 --- a/src/Symfony/Component/PropertyAccess/composer.json +++ b/src/Symfony/Component/PropertyAccess/composer.json @@ -16,7 +16,8 @@ } ], "require": { - "php": ">=5.5.9" + "php": ">=5.5.9", + "symfony/polyfill-php70": "~1.0" }, "autoload": { "psr-4": { "Symfony\\Component\\PropertyAccess\\": "" },