Skip to content

[PropertyAccess] Throw an UnexpectedTypeException when the type do not match #18210

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 18, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 57 additions & 14 deletions src/Symfony/Component/PropertyAccess/PropertyAccessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class PropertyAccessor implements PropertyAccessorInterface
private $magicCall;
private $readPropertyCache = array();
private $writePropertyCache = array();
private static $previousErrorHandler;

/**
* Should not be used by application code. Use
Expand Down Expand Up @@ -131,23 +132,66 @@ public function setValue(&$objectOrArray, $propertyPath, $value)
self::IS_REF => true,
));

for ($i = count($propertyValues) - 1; $i >= 0; --$i) {
$objectOrArray = &$propertyValues[$i][self::VALUE];
try {
if (PHP_VERSION_ID < 70000) {
self::$previousErrorHandler = set_error_handler(array(__CLASS__, 'handleError'));
}

if ($overwrite) {
$property = $propertyPath->getElement($i);
//$singular = $propertyPath->singulars[$i];
$singular = null;
for ($i = count($propertyValues) - 1; $i >= 0; --$i) {
$objectOrArray = &$propertyValues[$i][self::VALUE];

if ($propertyPath->isIndex($i)) {
$this->writeIndex($objectOrArray, $property, $value);
} else {
$this->writeProperty($objectOrArray, $property, $singular, $value);
if ($overwrite) {
$property = $propertyPath->getElement($i);
//$singular = $propertyPath->singulars[$i];
$singular = null;

if ($propertyPath->isIndex($i)) {
$this->writeIndex($objectOrArray, $property, $value);
} else {
$this->writeProperty($objectOrArray, $property, $singular, $value);
}
}

$value = &$objectOrArray;
$overwrite = !$propertyValues[$i][self::IS_REF];
}
} catch (\TypeError $e) {
try {
self::throwUnexpectedTypeException($e->getMessage(), $e->getTrace(), 0);
} catch (UnexpectedTypeException $e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is it thrown to catch it again ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throwing is done conditionally (see throwUnexpectedTypeException). Thus if the method throws, $e changes, otherwise $e stays as the original TypeError. This is exactly the logic we need here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I missed the rethrow below (this code should be cleaned to use finally instead in 3.0+)

}
} catch (\Exception $e) {
} catch (\Throwable $e) {
}

$value = &$objectOrArray;
$overwrite = !$propertyValues[$i][self::IS_REF];
if (PHP_VERSION_ID < 70000) {
restore_error_handler();
self::$previousErrorHandler = null;
}
if (isset($e)) {
throw $e;
}
}

/**
* @internal
*/
public static function handleError($type, $message, $file, $line, $context)
{
if (E_RECOVERABLE_ERROR === $type) {
self::throwUnexpectedTypeException($message, debug_backtrace(false), 1);
}

return null !== self::$previousErrorHandler && false !== call_user_func(self::$previousErrorHandler, $type, $message, $file, $line, $context);
}

private static function throwUnexpectedTypeException($message, $trace, $i)
{
if (isset($trace[$i]['file']) && __FILE__ === $trace[$i]['file']) {
$pos = strpos($message, $delim = 'must be of the type ') ?: strpos($message, $delim = 'must be an instance of ');
$pos += strlen($delim);

throw new UnexpectedTypeException($trace[$i]['args'][0], substr($message, $pos, strpos($message, ',', $pos) - $pos));
}
}

Expand Down Expand Up @@ -398,8 +442,7 @@ private function writeIndex(&$array, $index, $value)
* @param string|null $singular The singular form of the property name or null
* @param mixed $value The value to write
*
* @throws NoSuchPropertyException If the property does not exist or is not
* public.
* @throws NoSuchPropertyException If the property does not exist or is not public.
*/
private function writeProperty(&$object, $property, $singular, $value)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ interface PropertyAccessorInterface
* @param mixed $value The value to set at the end of the property path
*
* @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
* @throws Exception\UnexpectedTypeException If a value within the path is neither object nor array.
*/
public function setValue(&$objectOrArray, $propertyPath, $value);

Expand Down
30 changes: 30 additions & 0 deletions src/Symfony/Component/PropertyAccess/Tests/Fixtures/TypeHinted.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <dunglas@gmail.com>
*/
class TypeHinted
{
private $date;

public function setDate(\DateTime $date)
{
$this->date = $date;
}

public function getDate()
{
return $this->date;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
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
{
Expand Down Expand Up @@ -403,4 +404,22 @@ public function getValidPropertyPaths()
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());
}
}