Merge branch '2.3' into 2.7

* 2.3:
  [Validator] use correct term for a property in docblock (not "option")
  [PropertyAccess] Remove most ref mismatches to improve perf
  [Validator] EmailValidator cannot extract hostname if email contains multiple @ symbols
  [NumberFormatter] Fix invalid numeric literal on PHP 7
  Use XML_ELEMENT_NODE in nodeType check
  [PropertyAccess] Reduce overhead of UnexpectedTypeException tracking
  [PropertyAccess] Throw an UnexpectedTypeException when the type do not match
  [FrameworkBundle] Add tests for the Controller class

Conflicts:
	src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTest.php
	src/Symfony/Component/Intl/NumberFormatter/NumberFormatter.php
	src/Symfony/Component/PropertyAccess/PropertyAccessor.php
	src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php
	src/Symfony/Component/PropertyAccess/PropertyPath.php
	src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php
	src/Symfony/Component/Validator/Constraints/EmailValidator.php
This commit is contained in:
Nicolas Grekas 2016-03-22 09:55:46 +01:00
commit 86c0a17721
11 changed files with 441 additions and 234 deletions

View File

@ -209,4 +209,122 @@ class TestController extends Controller
{ {
return parent::isCsrfTokenValid($id, $token); return parent::isCsrfTokenValid($id, $token);
} }
public function testGenerateUrl()
{
$router = $this->getMock('Symfony\Component\Routing\RouterInterface');
$router->expects($this->once())->method('generate')->willReturn('/foo');
$container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
$container->expects($this->at(0))->method('get')->will($this->returnValue($router));
$controller = new Controller();
$controller->setContainer($container);
$this->assertEquals('/foo', $controller->generateUrl('foo'));
}
public function testRedirect()
{
$controller = new Controller();
$response = $controller->redirect('http://dunglas.fr', 301);
$this->assertInstanceOf('Symfony\Component\HttpFoundation\RedirectResponse', $response);
$this->assertSame('http://dunglas.fr', $response->getTargetUrl());
$this->assertSame(301, $response->getStatusCode());
}
public function testRenderViewTemplating()
{
$templating = $this->getMock('Symfony\Bundle\FrameworkBundle\Templating\EngineInterface');
$templating->expects($this->once())->method('render')->willReturn('bar');
$container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
$container->expects($this->at(0))->method('get')->will($this->returnValue($templating));
$controller = new Controller();
$controller->setContainer($container);
$this->assertEquals('bar', $controller->renderView('foo'));
}
public function testRenderTemplating()
{
$templating = $this->getMock('Symfony\Bundle\FrameworkBundle\Templating\EngineInterface');
$templating->expects($this->once())->method('renderResponse')->willReturn(new Response('bar'));
$container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
$container->expects($this->at(0))->method('get')->will($this->returnValue($templating));
$controller = new Controller();
$controller->setContainer($container);
$this->assertEquals('bar', $controller->render('foo')->getContent());
}
public function testStreamTemplating()
{
$templating = $this->getMock('Symfony\Component\Routing\RouterInterface');
$container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
$container->expects($this->at(0))->method('get')->will($this->returnValue($templating));
$controller = new Controller();
$controller->setContainer($container);
$this->assertInstanceOf('Symfony\Component\HttpFoundation\StreamedResponse', $controller->stream('foo'));
}
public function testCreateNotFoundException()
{
$controller = new Controller();
$this->assertInstanceOf('Symfony\Component\HttpKernel\Exception\NotFoundHttpException', $controller->createNotFoundException());
}
public function testCreateForm()
{
$form = $this->getMock('Symfony\Component\Form\FormInterface');
$formFactory = $this->getMock('Symfony\Component\Form\FormFactoryInterface');
$formFactory->expects($this->once())->method('create')->willReturn($form);
$container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
$container->expects($this->at(0))->method('get')->will($this->returnValue($formFactory));
$controller = new Controller();
$controller->setContainer($container);
$this->assertEquals($form, $controller->createForm('foo'));
}
public function testCreateFormBuilder()
{
$formBuilder = $this->getMock('Symfony\Component\Form\FormBuilderInterface');
$formFactory = $this->getMock('Symfony\Component\Form\FormFactoryInterface');
$formFactory->expects($this->once())->method('createBuilder')->willReturn($formBuilder);
$container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
$container->expects($this->at(0))->method('get')->will($this->returnValue($formFactory));
$controller = new Controller();
$controller->setContainer($container);
$this->assertEquals($formBuilder, $controller->createFormBuilder('foo'));
}
public function testGetDoctrine()
{
$doctrine = $this->getMock('Doctrine\Common\Persistence\ManagerRegistry');
$container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
$container->expects($this->at(0))->method('has')->will($this->returnValue(true));
$container->expects($this->at(1))->method('get')->will($this->returnValue($doctrine));
$controller = new Controller();
$controller->setContainer($container);
$this->assertEquals($doctrine, $controller->getDoctrine());
}
} }

View File

@ -482,7 +482,7 @@ class Crawler extends \SplObjectStorage
$nodes = array(); $nodes = array();
while ($node = $node->parentNode) { while ($node = $node->parentNode) {
if (1 === $node->nodeType) { if (XML_ELEMENT_NODE === $node->nodeType) {
$nodes[] = $node; $nodes[] = $node;
} }
} }

View File

@ -231,24 +231,18 @@ class NumberFormatter
); );
/** /**
* The maximum values of the integer type in 32 bit platforms. * The maximum value of the integer type in 32 bit platforms.
* *
* @var array * @var int
*/ */
private static $int32Range = array( private static $int32Max = 2147483647;
'positive' => 2147483647,
'negative' => -2147483648,
);
/** /**
* The maximum values of the integer type in 64 bit platforms. * The maximum value of the integer type in 64 bit platforms.
* *
* @var array * @var int|float
*/ */
private static $int64Range = array( private static $int64Max = 9223372036854775807;
'positive' => 9223372036854775807,
'negative' => -9223372036854775808,
);
private static $enSymbols = array( private static $enSymbols = array(
self::DECIMAL => array('.', ',', ';', '%', '0', '#', '-', '+', '¤', '¤¤', '.', 'E', '‰', '*', '∞', 'NaN', '@', ','), self::DECIMAL => array('.', ',', ';', '%', '0', '#', '-', '+', '¤', '¤¤', '.', 'E', '‰', '*', '∞', 'NaN', '@', ','),
@ -526,7 +520,7 @@ class NumberFormatter
* @param int $type Type of the formatting, one of the format type constants. NumberFormatter::TYPE_DOUBLE by default * @param int $type Type of the formatting, one of the format type constants. NumberFormatter::TYPE_DOUBLE by default
* @param int $position Offset to begin the parsing on return this value will hold the offset at which the parsing ended * @param int $position Offset to begin the parsing on return this value will hold the offset at which the parsing ended
* *
* @return bool|string The parsed value of false on error * @return int|float|false The parsed value of false on error
* *
* @see http://www.php.net/manual/en/numberformatter.parse.php * @see http://www.php.net/manual/en/numberformatter.parse.php
*/ */
@ -835,7 +829,7 @@ class NumberFormatter
*/ */
private function getInt32Value($value) private function getInt32Value($value)
{ {
if ($value > self::$int32Range['positive'] || $value < self::$int32Range['negative']) { if ($value > self::$int32Max || $value < -self::$int32Max - 1) {
return false; return false;
} }
@ -848,20 +842,18 @@ class NumberFormatter
* @param mixed $value The value to be converted * @param mixed $value The value to be converted
* *
* @return int|float|false The converted value * @return int|float|false The converted value
*
* @see https://bugs.php.net/bug.php?id=59597 Bug #59597
*/ */
private function getInt64Value($value) private function getInt64Value($value)
{ {
if ($value > self::$int64Range['positive'] || $value < self::$int64Range['negative']) { if ($value > self::$int64Max || $value < -self::$int64Max - 1) {
return false; return false;
} }
if (PHP_INT_SIZE !== 8 && ($value > self::$int32Range['positive'] || $value <= self::$int32Range['negative'])) { if (PHP_INT_SIZE !== 8 && ($value > self::$int32Max || $value <= -self::$int32Max - 1)) {
// Bug #59597 was fixed on PHP 5.3.14 and 5.4.4 // Bug #59597 was fixed on PHP 5.3.14 and 5.4.4
// The negative PHP_INT_MAX was being converted to float // The negative PHP_INT_MAX was being converted to float
if ( if (
$value == self::$int32Range['negative'] && $value == -self::$int32Max - 1 &&
((PHP_VERSION_ID < 50400 && PHP_VERSION_ID >= 50314) || PHP_VERSION_ID >= 50404 || (extension_loaded('intl') && method_exists('IntlDateFormatter', 'setTimeZone'))) ((PHP_VERSION_ID < 50400 && PHP_VERSION_ID >= 50314) || PHP_VERSION_ID >= 50404 || (extension_loaded('intl') && method_exists('IntlDateFormatter', 'setTimeZone')))
) { ) {
return (int) $value; return (int) $value;
@ -874,7 +866,7 @@ class NumberFormatter
// Bug #59597 was fixed on PHP 5.3.14 and 5.4.4 // Bug #59597 was fixed on PHP 5.3.14 and 5.4.4
// A 32 bit integer was being generated instead of a 64 bit integer // A 32 bit integer was being generated instead of a 64 bit integer
if ( if (
($value > self::$int32Range['positive'] || $value < self::$int32Range['negative']) && ($value > self::$int32Max || $value < -self::$int32Max - 1) &&
(PHP_VERSION_ID < 50314 || (PHP_VERSION_ID >= 50400 && PHP_VERSION_ID < 50404)) && (PHP_VERSION_ID < 50314 || (PHP_VERSION_ID >= 50400 && PHP_VERSION_ID < 50404)) &&
!(extension_loaded('intl') && method_exists('IntlDateFormatter', 'setTimeZone')) !(extension_loaded('intl') && method_exists('IntlDateFormatter', 'setTimeZone'))
) { ) {

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\PropertyAccess; namespace Symfony\Component\PropertyAccess;
use Symfony\Component\PropertyAccess\Exception\AccessException; use Symfony\Component\PropertyAccess\Exception\AccessException;
use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
@ -21,6 +22,7 @@ use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
* *
* @author Bernhard Schussek <bschussek@gmail.com> * @author Bernhard Schussek <bschussek@gmail.com>
* @author Kévin Dunglas <dunglas@gmail.com> * @author Kévin Dunglas <dunglas@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/ */
class PropertyAccessor implements PropertyAccessorInterface class PropertyAccessor implements PropertyAccessorInterface
{ {
@ -32,7 +34,7 @@ class PropertyAccessor implements PropertyAccessorInterface
/** /**
* @internal * @internal
*/ */
const IS_REF = 1; const REF = 1;
/** /**
* @internal * @internal
@ -113,6 +115,9 @@ class PropertyAccessor implements PropertyAccessorInterface
* @var array * @var array
*/ */
private $writePropertyCache = array(); private $writePropertyCache = array();
private static $previousErrorHandler = false;
private static $errorHandler = array(__CLASS__, 'handleError');
private static $resultProto = array(self::VALUE => null);
/** /**
* Should not be used by application code. Use * Should not be used by application code. Use
@ -136,7 +141,10 @@ class PropertyAccessor implements PropertyAccessorInterface
$propertyPath = new PropertyPath($propertyPath); $propertyPath = new PropertyPath($propertyPath);
} }
$propertyValues = &$this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices); $zval = array(
self::VALUE => $objectOrArray,
);
$propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices);
return $propertyValues[count($propertyValues) - 1][self::VALUE]; return $propertyValues[count($propertyValues) - 1][self::VALUE];
} }
@ -150,51 +158,99 @@ class PropertyAccessor implements PropertyAccessorInterface
$propertyPath = new PropertyPath($propertyPath); $propertyPath = new PropertyPath($propertyPath);
} }
$propertyValues = &$this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength() - 1); $zval = array(
self::VALUE => $objectOrArray,
self::REF => &$objectOrArray,
);
$propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength() - 1);
$overwrite = true;
// Add the root object to the list try {
array_unshift($propertyValues, array( if (PHP_VERSION_ID < 70000 && false === self::$previousErrorHandler) {
self::VALUE => &$objectOrArray, self::$previousErrorHandler = set_error_handler(self::$errorHandler);
self::IS_REF => true,
self::IS_REF_CHAINED => true,
));
$propertyMaxIndex = count($propertyValues) - 1;
for ($i = $propertyMaxIndex; $i >= 0; --$i) {
$objectOrArray = &$propertyValues[$i][self::VALUE];
$property = $propertyPath->getElement($i);
// You only need set value for current element if:
// 1. it's the parent of the last index element
// OR
// 2. its child is not passed by reference
//
// This may avoid uncessary value setting process for array elements.
// For example:
// '[a][b][c]' => 'old-value'
// If you want to change its value to 'new-value',
// you only need set value for '[a][b][c]' and it's safe to ignore '[a][b]' and '[a]'
//
if ($i === $propertyMaxIndex || !$propertyValues[$i + 1][self::IS_REF]) {
if ($propertyPath->isIndex($i)) {
$this->writeIndex($objectOrArray, $property, $value);
} else {
$this->writeProperty($objectOrArray, $property, $value);
}
// if current element is an object
// OR
// if current element's reference chain is not broken - current element
// as well as all its ancients in the property path are all passed by reference,
// then there is no need to continue the value setting process
if (is_object($propertyValues[$i][self::VALUE]) || $propertyValues[$i][self::IS_REF_CHAINED]) {
return;
}
} }
$value = &$objectOrArray; for ($i = count($propertyValues) - 1; 0 <= $i; --$i) {
$zval = $propertyValues[$i];
unset($propertyValues[$i]);
// You only need set value for current element if:
// 1. it's the parent of the last index element
// OR
// 2. its child is not passed by reference
//
// This may avoid uncessary value setting process for array elements.
// For example:
// '[a][b][c]' => 'old-value'
// If you want to change its value to 'new-value',
// you only need set value for '[a][b][c]' and it's safe to ignore '[a][b]' and '[a]'
//
if ($overwrite) {
$property = $propertyPath->getElement($i);
if ($propertyPath->isIndex($i)) {
if ($overwrite = !isset($zval[self::REF])) {
$ref = &$zval[self::REF];
}
$this->writeIndex($zval, $property, $value);
if ($overwrite) {
$zval[self::VALUE] = $zval[self::REF];
}
} else {
$this->writeProperty($zval, $property, $value);
}
// if current element is an object
// OR
// if current element's reference chain is not broken - current element
// as well as all its ancients in the property path are all passed by reference,
// then there is no need to continue the value setting process
if (is_object($zval[self::VALUE]) || isset($zval[self::IS_REF_CHAINED])) {
return;
}
}
$value = $zval[self::VALUE];
}
} catch (\TypeError $e) {
try {
self::throwInvalidArgumentException($e->getMessage(), $e->getTrace(), 0);
} catch (InvalidArgumentException $e) {
}
} catch (\Exception $e) {
} catch (\Throwable $e) {
}
if (PHP_VERSION_ID < 70000 && false !== self::$previousErrorHandler) {
restore_error_handler();
self::$previousErrorHandler = false;
}
if (isset($e)) {
throw $e;
}
}
/**
* @internal
*/
public static function handleError($type, $message, $file, $line, $context)
{
if (E_RECOVERABLE_ERROR === $type) {
self::throwInvalidArgumentException($message, debug_backtrace(false), 1);
}
return null !== self::$previousErrorHandler && false !== call_user_func(self::$previousErrorHandler, $type, $message, $file, $line, $context);
}
private static function throwInvalidArgumentException($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);
$type = $trace[$i]['args'][0];
$type = is_object($type) ? get_class($type) : gettype($type);
throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given', substr($message, $pos, strpos($message, ',', $pos) - $pos), $type));
} }
} }
@ -208,7 +264,10 @@ class PropertyAccessor implements PropertyAccessorInterface
} }
try { try {
$this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices); $zval = array(
self::VALUE => $objectOrArray,
);
$this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices);
return true; return true;
} catch (AccessException $e) { } catch (AccessException $e) {
@ -228,31 +287,26 @@ class PropertyAccessor implements PropertyAccessorInterface
} }
try { try {
$propertyValues = $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength() - 1); $zval = array(
// Add the root object to the list
array_unshift($propertyValues, array(
self::VALUE => $objectOrArray, self::VALUE => $objectOrArray,
self::IS_REF => true, );
self::IS_REF_CHAINED => true, $propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength() - 1);
));
for ($i = count($propertyValues) - 1; $i >= 0; --$i) { for ($i = count($propertyValues) - 1; 0 <= $i; --$i) {
$objectOrArray = $propertyValues[$i][self::VALUE]; $zval = $propertyValues[$i];
unset($propertyValues[$i]);
$property = $propertyPath->getElement($i);
if ($propertyPath->isIndex($i)) { if ($propertyPath->isIndex($i)) {
if (!$objectOrArray instanceof \ArrayAccess && !is_array($objectOrArray)) { if (!$zval[self::VALUE] instanceof \ArrayAccess && !is_array($zval[self::VALUE])) {
return false; return false;
} }
} else { } else {
if (!$this->isPropertyWritable($objectOrArray, $property)) { if (!$this->isPropertyWritable($zval[self::VALUE], $propertyPath->getElement($i))) {
return false; return false;
} }
} }
if (is_object($propertyValues[$i][self::VALUE]) || $propertyValues[$i][self::IS_REF_CHAINED]) { if (is_object($zval[self::VALUE])) {
return true; return true;
} }
} }
@ -268,83 +322,84 @@ class PropertyAccessor implements PropertyAccessorInterface
/** /**
* Reads the path from an object up to a given path index. * Reads the path from an object up to a given path index.
* *
* @param object|array $objectOrArray The object or array to read from * @param array $zval The array containing the object or array to read from
* @param PropertyPathInterface $propertyPath The property path to read * @param PropertyPathInterface $propertyPath The property path to read
* @param int $lastIndex The index up to which should be read * @param int $lastIndex The index up to which should be read
* @param bool $ignoreInvalidIndices Whether to ignore invalid indices * @param bool $ignoreInvalidIndices Whether to ignore invalid indices or throw an exception
* or throw an exception
* *
* @return array The values read in the path. * @return array The values read in the path.
* *
* @throws UnexpectedTypeException If a value within the path is neither object nor array. * @throws UnexpectedTypeException If a value within the path is neither object nor array.
* @throws NoSuchIndexException If a non-existing index is accessed * @throws NoSuchIndexException If a non-existing index is accessed
*/ */
private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface $propertyPath, $lastIndex, $ignoreInvalidIndices = true) private function readPropertiesUntil($zval, PropertyPathInterface $propertyPath, $lastIndex, $ignoreInvalidIndices = true)
{ {
if (!is_object($objectOrArray) && !is_array($objectOrArray)) { if (!is_object($zval[self::VALUE]) && !is_array($zval[self::VALUE])) {
throw new UnexpectedTypeException($objectOrArray, $propertyPath, 0); throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath, 0);
} }
$propertyValues = array(); // Add the root object to the list
$propertyValues = array($zval);
for ($i = 0; $i < $lastIndex; ++$i) { for ($i = 0; $i < $lastIndex; ++$i) {
$property = $propertyPath->getElement($i); $property = $propertyPath->getElement($i);
$isIndex = $propertyPath->isIndex($i); $isIndex = $propertyPath->isIndex($i);
// Create missing nested arrays on demand if ($isIndex) {
if ($isIndex && // Create missing nested arrays on demand
( if (($zval[self::VALUE] instanceof \ArrayAccess && !$zval[self::VALUE]->offsetExists($property)) ||
($objectOrArray instanceof \ArrayAccess && !isset($objectOrArray[$property])) || (is_array($zval[self::VALUE]) && !isset($zval[self::VALUE][$property]) && !array_key_exists($property, $zval[self::VALUE]))
(is_array($objectOrArray) && !array_key_exists($property, $objectOrArray)) ) {
) if (!$ignoreInvalidIndices) {
) { if (!is_array($zval[self::VALUE])) {
if (!$ignoreInvalidIndices) { if (!$zval[self::VALUE] instanceof \Traversable) {
if (!is_array($objectOrArray)) { throw new NoSuchIndexException(sprintf(
if (!$objectOrArray instanceof \Traversable) { 'Cannot read index "%s" while trying to traverse path "%s".',
throw new NoSuchIndexException(sprintf( $property,
'Cannot read index "%s" while trying to traverse path "%s".', (string) $propertyPath
$property, ));
(string) $propertyPath }
));
$zval[self::VALUE] = iterator_to_array($zval[self::VALUE]);
} }
$objectOrArray = iterator_to_array($objectOrArray); throw new NoSuchIndexException(sprintf(
'Cannot read index "%s" while trying to traverse path "%s". Available indices are "%s".',
$property,
(string) $propertyPath,
print_r(array_keys($zval[self::VALUE]), true)
));
} }
throw new NoSuchIndexException(sprintf( if ($i + 1 < $propertyPath->getLength()) {
'Cannot read index "%s" while trying to traverse path "%s". Available indices are "%s".', $zval[self::VALUE][$property] = array();
$property,
(string) $propertyPath, if (isset($zval[self::REF])) {
print_r(array_keys($objectOrArray), true) $zval[self::REF] = $zval[self::VALUE];
)); }
}
} }
if ($i + 1 < $propertyPath->getLength()) { $zval = $this->readIndex($zval, $property);
$objectOrArray[$property] = array();
}
}
if ($isIndex) {
$propertyValue = &$this->readIndex($objectOrArray, $property);
} else { } else {
$propertyValue = &$this->readProperty($objectOrArray, $property); $zval = $this->readProperty($zval, $property);
} }
$objectOrArray = &$propertyValue[self::VALUE];
// the final value of the path must not be validated // the final value of the path must not be validated
if ($i + 1 < $propertyPath->getLength() && !is_object($objectOrArray) && !is_array($objectOrArray)) { if ($i + 1 < $propertyPath->getLength() && !is_object($zval[self::VALUE]) && !is_array($zval[self::VALUE])) {
throw new UnexpectedTypeException($objectOrArray, $propertyPath, $i + 1); throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath, $i + 1);
} }
// Set the IS_REF_CHAINED flag to true if: if (isset($zval[self::REF]) && (0 === $i || isset($propertyValues[$i - 1][self::IS_REF_CHAINED]))) {
// current property is passed by reference and // Set the IS_REF_CHAINED flag to true if:
// it is the first element in the property path or // current property is passed by reference and
// the IS_REF_CHAINED flag of its parent element is true // it is the first element in the property path or
// Basically, this flag is true only when the reference chain from the top element to current element is not broken // the IS_REF_CHAINED flag of its parent element is true
$propertyValue[self::IS_REF_CHAINED] = $propertyValue[self::IS_REF] && (0 === $i || $propertyValues[$i - 1][self::IS_REF_CHAINED]); // Basically, this flag is true only when the reference chain from the top element to current element is not broken
$zval[self::IS_REF_CHAINED] = true;
}
$propertyValues[] = &$propertyValue; $propertyValues[] = $zval;
} }
return $propertyValues; return $propertyValues;
@ -353,33 +408,30 @@ class PropertyAccessor implements PropertyAccessorInterface
/** /**
* Reads a key from an array-like structure. * Reads a key from an array-like structure.
* *
* @param \ArrayAccess|array $array The array or \ArrayAccess object to read from * @param array $zval The array containing the array or \ArrayAccess object to read from
* @param string|int $index The key to read * @param string|int $index The key to read
* *
* @return mixed The value of the key * @return array The array containing the value of the key
* *
* @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array * @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array
*/ */
private function &readIndex(&$array, $index) private function readIndex($zval, $index)
{ {
if (!$array instanceof \ArrayAccess && !is_array($array)) { if (!$zval[self::VALUE] instanceof \ArrayAccess && !is_array($zval[self::VALUE])) {
throw new NoSuchIndexException(sprintf('Cannot read index "%s" from object of type "%s" because it doesn\'t implement \ArrayAccess.', $index, get_class($array))); throw new NoSuchIndexException(sprintf('Cannot read index "%s" from object of type "%s" because it doesn\'t implement \ArrayAccess.', $index, get_class($zval[self::VALUE])));
} }
// Use an array instead of an object since performance is very crucial here $result = self::$resultProto;
$result = array(
self::VALUE => null,
self::IS_REF => false,
);
if (isset($array[$index])) { if (isset($zval[self::VALUE][$index])) {
if (is_array($array)) { $result[self::VALUE] = $zval[self::VALUE][$index];
$result[self::VALUE] = &$array[$index];
$result[self::IS_REF] = true; if (!isset($zval[self::REF])) {
} else { // Save creating references when doing read-only lookups
$result[self::VALUE] = $array[$index]; } elseif (is_array($zval[self::VALUE])) {
// Objects are always passed around by reference $result[self::REF] = &$zval[self::REF][$index];
$result[self::IS_REF] = is_object($array[$index]) ? true : false; } elseif (is_object($result[self::VALUE])) {
$result[self::REF] = $result[self::VALUE];
} }
} }
@ -387,39 +439,32 @@ class PropertyAccessor implements PropertyAccessorInterface
} }
/** /**
* Reads the a property from an object or array. * Reads the a property from an object.
* *
* @param object $object The object to read from. * @param array $zval The array containing the object to read from
* @param string $property The property to read. * @param string $property The property to read.
* *
* @return mixed The value of the read property * @return array The array containing the value of the property
* *
* @throws NoSuchPropertyException If the property does not exist or is not * @throws NoSuchPropertyException If the property does not exist or is not public.
* public.
*/ */
private function &readProperty(&$object, $property) private function readProperty($zval, $property)
{ {
// Use an array instead of an object since performance is if (!is_object($zval[self::VALUE])) {
// very crucial here
$result = array(
self::VALUE => null,
self::IS_REF => false,
);
if (!is_object($object)) {
throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you intended to write the property path as "[%s]" instead.', $property, $property)); throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you intended to write the property path as "[%s]" instead.', $property, $property));
} }
$access = $this->getReadAccessInfo($object, $property); $result = self::$resultProto;
$object = $zval[self::VALUE];
$access = $this->getReadAccessInfo(get_class($object), $property);
if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) { if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}(); $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
} elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) { } elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
if ($access[self::ACCESS_REF]) { $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]};
$result[self::VALUE] = &$object->{$access[self::ACCESS_NAME]};
$result[self::IS_REF] = true; if ($access[self::ACCESS_REF] && isset($zval[self::REF])) {
} else { $result[self::REF] = &$object->{$access[self::ACCESS_NAME]};
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]};
} }
} elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) { } elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) {
// Needed to support \stdClass instances. We need to explicitly // Needed to support \stdClass instances. We need to explicitly
@ -428,8 +473,10 @@ class PropertyAccessor implements PropertyAccessorInterface
// returns true, consequently the following line will result in a // returns true, consequently the following line will result in a
// fatal error. // fatal error.
$result[self::VALUE] = &$object->$property; $result[self::VALUE] = $object->$property;
$result[self::IS_REF] = true; if (isset($zval[self::REF])) {
$result[self::REF] = &$object->$property;
}
} elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) { } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
// we call the getter and hope the __call do the job // we call the getter and hope the __call do the job
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}(); $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
@ -438,8 +485,8 @@ class PropertyAccessor implements PropertyAccessorInterface
} }
// Objects are always passed around by reference // Objects are always passed around by reference
if (is_object($result[self::VALUE])) { if (isset($zval[self::REF]) && is_object($result[self::VALUE])) {
$result[self::IS_REF] = true; $result[self::REF] = $result[self::VALUE];
} }
return $result; return $result;
@ -448,21 +495,21 @@ class PropertyAccessor implements PropertyAccessorInterface
/** /**
* Guesses how to read the property value. * Guesses how to read the property value.
* *
* @param string $object * @param string $class
* @param string $property * @param string $property
* *
* @return array * @return array
*/ */
private function getReadAccessInfo($object, $property) private function getReadAccessInfo($class, $property)
{ {
$key = get_class($object).'::'.$property; $key = $class.'::'.$property;
if (isset($this->readPropertyCache[$key])) { if (isset($this->readPropertyCache[$key])) {
$access = $this->readPropertyCache[$key]; $access = $this->readPropertyCache[$key];
} else { } else {
$access = array(); $access = array();
$reflClass = new \ReflectionClass($object); $reflClass = new \ReflectionClass($class);
$access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property); $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
$camelProp = $this->camelize($property); $camelProp = $this->camelize($property);
$getter = 'get'.$camelProp; $getter = 'get'.$camelProp;
@ -519,45 +566,45 @@ class PropertyAccessor implements PropertyAccessorInterface
/** /**
* Sets the value of an index in a given array-accessible value. * Sets the value of an index in a given array-accessible value.
* *
* @param \ArrayAccess|array $array An array or \ArrayAccess object to write to * @param array $zval The array containing the array or \ArrayAccess object to write to
* @param string|int $index The index to write at * @param string|int $index The index to write at
* @param mixed $value The value to write * @param mixed $value The value to write
* *
* @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array * @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array
*/ */
private function writeIndex(&$array, $index, $value) private function writeIndex($zval, $index, $value)
{ {
if (!$array instanceof \ArrayAccess && !is_array($array)) { if (!$zval[self::VALUE] instanceof \ArrayAccess && !is_array($zval[self::VALUE])) {
throw new NoSuchIndexException(sprintf('Cannot modify index "%s" in object of type "%s" because it doesn\'t implement \ArrayAccess', $index, get_class($array))); throw new NoSuchIndexException(sprintf('Cannot modify index "%s" in object of type "%s" because it doesn\'t implement \ArrayAccess', $index, get_class($zval[self::VALUE])));
} }
$array[$index] = $value; $zval[self::REF][$index] = $value;
} }
/** /**
* Sets the value of a property in the given object. * Sets the value of a property in the given object.
* *
* @param object $object The object to write to * @param array $zval The array containing the object to write to
* @param string $property The property to write * @param string $property The property to write
* @param mixed $value The value to write * @param mixed $value The value to write
* *
* @throws NoSuchPropertyException If the property does not exist or is not * @throws NoSuchPropertyException If the property does not exist or is not public.
* public.
*/ */
private function writeProperty(&$object, $property, $value) private function writeProperty($zval, $property, $value)
{ {
if (!is_object($object)) { if (!is_object($zval[self::VALUE])) {
throw new NoSuchPropertyException(sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%s]" instead?', $property, $property)); throw new NoSuchPropertyException(sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%s]" instead?', $property, $property));
} }
$access = $this->getWriteAccessInfo($object, $property, $value); $object = $zval[self::VALUE];
$access = $this->getWriteAccessInfo(get_class($object), $property, $value);
if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) { if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
$object->{$access[self::ACCESS_NAME]}($value); $object->{$access[self::ACCESS_NAME]}($value);
} elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) { } elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
$object->{$access[self::ACCESS_NAME]} = $value; $object->{$access[self::ACCESS_NAME]} = $value;
} elseif (self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]) { } elseif (self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]) {
$this->writeCollection($object, $property, $value, $access[self::ACCESS_ADDER], $access[self::ACCESS_REMOVER]); $this->writeCollection($zval, $property, $value, $access[self::ACCESS_ADDER], $access[self::ACCESS_REMOVER]);
} elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) { } elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) {
// Needed to support \stdClass instances. We need to explicitly // Needed to support \stdClass instances. We need to explicitly
// exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if // exclude $access[self::ACCESS_HAS_PROPERTY], otherwise if
@ -574,72 +621,63 @@ class PropertyAccessor implements PropertyAccessorInterface
} }
/** /**
* Adjusts a collection-valued property by calling add*() and remove*() * Adjusts a collection-valued property by calling add*() and remove*() methods.
* methods.
* *
* @param object $object The object to write to * @param array $zval The array containing the object to write to
* @param string $property The property to write * @param string $property The property to write
* @param array|\Traversable $collection The collection to write * @param array|\Traversable $collection The collection to write
* @param string $addMethod The add*() method * @param string $addMethod The add*() method
* @param string $removeMethod The remove*() method * @param string $removeMethod The remove*() method
*/ */
private function writeCollection($object, $property, $collection, $addMethod, $removeMethod) private function writeCollection($zval, $property, $collection, $addMethod, $removeMethod)
{ {
// At this point the add and remove methods have been found // At this point the add and remove methods have been found
// Use iterator_to_array() instead of clone in order to prevent side effects $previousValue = $this->readProperty($zval, $property);
// see https://github.com/symfony/symfony/issues/4670 $previousValue = $previousValue[self::VALUE];
$itemsToAdd = is_object($collection) ? iterator_to_array($collection) : $collection;
$itemToRemove = array();
$propertyValue = &$this->readProperty($object, $property);
$previousValue = $propertyValue[self::VALUE];
// remove reference to avoid modifications
unset($propertyValue);
if (is_array($previousValue) || $previousValue instanceof \Traversable) { if ($previousValue instanceof \Traversable) {
foreach ($previousValue as $previousItem) { $previousValue = iterator_to_array($previousValue);
foreach ($collection as $key => $item) { }
if ($item === $previousItem) { if ($previousValue && is_array($previousValue)) {
// Item found, don't add if (is_object($collection)) {
unset($itemsToAdd[$key]); $collection = iterator_to_array($collection);
// Next $previousItem
continue 2;
}
}
// Item not found, add to remove list
$itemToRemove[] = $previousItem;
} }
foreach ($previousValue as $key => $item) {
if (!in_array($item, $collection, true)) {
unset($previousValue[$key]);
$zval[self::VALUE]->{$removeMethod}($item);
}
}
} else {
$previousValue = false;
} }
foreach ($itemToRemove as $item) { foreach ($collection as $item) {
$object->{$removeMethod}($item); if (!$previousValue || !in_array($item, $previousValue, true)) {
} $zval[self::VALUE]->{$addMethod}($item);
}
foreach ($itemsToAdd as $item) {
$object->{$addMethod}($item);
} }
} }
/** /**
* Guesses how to write the property value. * Guesses how to write the property value.
* *
* @param string $object * @param string $class
* @param string $property * @param string $property
* @param mixed $value * @param mixed $value
* *
* @return array * @return array
*/ */
private function getWriteAccessInfo($object, $property, $value) private function getWriteAccessInfo($class, $property, $value)
{ {
$key = get_class($object).'::'.$property; $key = $class.'::'.$property;
if (isset($this->writePropertyCache[$key])) { if (isset($this->writePropertyCache[$key])) {
$access = $this->writePropertyCache[$key]; $access = $this->writePropertyCache[$key];
} else { } else {
$access = array(); $access = array();
$reflClass = new \ReflectionClass($object); $reflClass = new \ReflectionClass($class);
$access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property); $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
$camelized = $this->camelize($property); $camelized = $this->camelize($property);
$singulars = (array) StringUtil::singularify($camelized); $singulars = (array) StringUtil::singularify($camelized);
@ -778,8 +816,7 @@ class PropertyAccessor implements PropertyAccessorInterface
* @param string $methodName The method name * @param string $methodName The method name
* @param int $parameters The number of parameters * @param int $parameters The number of parameters
* *
* @return bool Whether the method is public and has $parameters * @return bool Whether the method is public and has $parameters required parameters
* required parameters
*/ */
private function isMethodAccessible(\ReflectionClass $class, $methodName, $parameters) private function isMethodAccessible(\ReflectionClass $class, $methodName, $parameters)
{ {

View File

@ -45,8 +45,7 @@ interface PropertyAccessorInterface
* *
* @throws Exception\InvalidArgumentException If the property path is invalid * @throws Exception\InvalidArgumentException If the property path is invalid
* @throws Exception\AccessException If a property/index does not exist or is not public * @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 * @throws Exception\UnexpectedTypeException If a value within the path is neither object nor array
* nor array
*/ */
public function setValue(&$objectOrArray, $propertyPath, $value); public function setValue(&$objectOrArray, $propertyPath, $value);

View File

@ -96,7 +96,7 @@ class PropertyPath implements \IteratorAggregate, PropertyPathInterface
$remaining = $propertyPath; $remaining = $propertyPath;
// first element is evaluated differently - no leading dot for properties // first element is evaluated differently - no leading dot for properties
$pattern = '/^(([^\.\[]+)|\[([^\]]+)\])(.*)/'; $pattern = '/^(([^\.\[]++)|\[([^\]]++)\])(.*)/';
while (preg_match($pattern, $remaining, $matches)) { while (preg_match($pattern, $remaining, $matches)) {
if ('' !== $matches[2]) { if ('' !== $matches[2]) {
@ -111,7 +111,7 @@ class PropertyPath implements \IteratorAggregate, PropertyPathInterface
$position += strlen($matches[1]); $position += strlen($matches[1]);
$remaining = $matches[4]; $remaining = $matches[4];
$pattern = '/^(\.([^\.|\[]+)|\[([^\]]+)\])(.*)/'; $pattern = '/^(\.([^\.|\[]++)|\[([^\]]++)\])(.*)/';
} }
if ('' !== $remaining) { if ('' !== $remaining) {

View File

@ -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;
}
}

View File

@ -19,6 +19,7 @@ use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicGet;
use Symfony\Component\PropertyAccess\Tests\Fixtures\Ticket5775Object; use Symfony\Component\PropertyAccess\Tests\Fixtures\Ticket5775Object;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassSetValue; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassSetValue;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassIsWritable; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassIsWritable;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TypeHinted;
class PropertyAccessorTest extends \PHPUnit_Framework_TestCase class PropertyAccessorTest extends \PHPUnit_Framework_TestCase
{ {
@ -510,4 +511,22 @@ class PropertyAccessorTest extends \PHPUnit_Framework_TestCase
{ {
$this->assertEquals($value, $this->propertyAccessor->isWritable($object, $path)); $this->assertEquals($value, $this->propertyAccessor->isWritable($object, $path));
} }
/**
* @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException
* @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());
}
} }

View File

@ -19,9 +19,9 @@ use Symfony\Component\Validator\Exception\MissingOptionsException;
/** /**
* Contains the properties of a constraint definition. * Contains the properties of a constraint definition.
* *
* A constraint can be defined on a class, an option or a getter method. * A constraint can be defined on a class, a property or a getter method.
* The Constraint class encapsulates all the configuration required for * The Constraint class encapsulates all the configuration required for
* validating this class, option or getter result successfully. * validating this class, property or getter result successfully.
* *
* Constraint instances are immutable and serializable. * Constraint instances are immutable and serializable.
* *

View File

@ -93,7 +93,7 @@ class EmailValidator extends ConstraintValidator
return; return;
} }
$host = substr($value, strpos($value, '@') + 1); $host = substr($value, strrpos($value, '@') + 1);
// Check for host DNS resource records // Check for host DNS resource records
if ($constraint->checkMX) { if ($constraint->checkMX) {

View File

@ -143,4 +143,16 @@ class EmailValidatorTest extends AbstractConstraintValidatorTest
array('AAAA', Email::HOST_CHECK_FAILED_ERROR), array('AAAA', Email::HOST_CHECK_FAILED_ERROR),
); );
} }
public function testHostnameIsProperlyParsed()
{
DnsMock::withMockedHosts(array('baz.com' => array(array('type' => 'MX'))));
$this->validator->validate(
'"foo@bar"@baz.com',
new Email(array('checkMX' => true))
);
$this->assertNoViolation();
}
} }