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);
}
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();
while ($node = $node->parentNode) {
if (1 === $node->nodeType) {
if (XML_ELEMENT_NODE === $node->nodeType) {
$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(
'positive' => 2147483647,
'negative' => -2147483648,
);
private static $int32Max = 2147483647;
/**
* 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(
'positive' => 9223372036854775807,
'negative' => -9223372036854775808,
);
private static $int64Max = 9223372036854775807;
private static $enSymbols = array(
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 $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
*/
@ -835,7 +829,7 @@ class NumberFormatter
*/
private function getInt32Value($value)
{
if ($value > self::$int32Range['positive'] || $value < self::$int32Range['negative']) {
if ($value > self::$int32Max || $value < -self::$int32Max - 1) {
return false;
}
@ -848,20 +842,18 @@ class NumberFormatter
* @param mixed $value The value to be converted
*
* @return int|float|false The converted value
*
* @see https://bugs.php.net/bug.php?id=59597 Bug #59597
*/
private function getInt64Value($value)
{
if ($value > self::$int64Range['positive'] || $value < self::$int64Range['negative']) {
if ($value > self::$int64Max || $value < -self::$int64Max - 1) {
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
// The negative PHP_INT_MAX was being converted to float
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')))
) {
return (int) $value;
@ -874,7 +866,7 @@ class NumberFormatter
// 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
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)) &&
!(extension_loaded('intl') && method_exists('IntlDateFormatter', 'setTimeZone'))
) {

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\PropertyAccess;
use Symfony\Component\PropertyAccess\Exception\AccessException;
use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
@ -21,6 +22,7 @@ use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
class PropertyAccessor implements PropertyAccessorInterface
{
@ -32,7 +34,7 @@ class PropertyAccessor implements PropertyAccessorInterface
/**
* @internal
*/
const IS_REF = 1;
const REF = 1;
/**
* @internal
@ -113,6 +115,9 @@ class PropertyAccessor implements PropertyAccessorInterface
* @var 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
@ -136,7 +141,10 @@ class PropertyAccessor implements PropertyAccessorInterface
$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];
}
@ -150,51 +158,99 @@ class PropertyAccessor implements PropertyAccessorInterface
$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
array_unshift($propertyValues, array(
self::VALUE => &$objectOrArray,
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;
}
try {
if (PHP_VERSION_ID < 70000 && false === self::$previousErrorHandler) {
self::$previousErrorHandler = set_error_handler(self::$errorHandler);
}
$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 {
$this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices);
$zval = array(
self::VALUE => $objectOrArray,
);
$this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices);
return true;
} catch (AccessException $e) {
@ -228,31 +287,26 @@ class PropertyAccessor implements PropertyAccessorInterface
}
try {
$propertyValues = $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength() - 1);
// Add the root object to the list
array_unshift($propertyValues, array(
$zval = array(
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) {
$objectOrArray = $propertyValues[$i][self::VALUE];
$property = $propertyPath->getElement($i);
for ($i = count($propertyValues) - 1; 0 <= $i; --$i) {
$zval = $propertyValues[$i];
unset($propertyValues[$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;
}
} else {
if (!$this->isPropertyWritable($objectOrArray, $property)) {
if (!$this->isPropertyWritable($zval[self::VALUE], $propertyPath->getElement($i))) {
return false;
}
}
if (is_object($propertyValues[$i][self::VALUE]) || $propertyValues[$i][self::IS_REF_CHAINED]) {
if (is_object($zval[self::VALUE])) {
return true;
}
}
@ -268,83 +322,84 @@ class PropertyAccessor implements PropertyAccessorInterface
/**
* 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 int $lastIndex The index up to which should be read
* @param bool $ignoreInvalidIndices Whether to ignore invalid indices
* or throw an exception
* @param bool $ignoreInvalidIndices Whether to ignore invalid indices or throw an exception
*
* @return array The values read in the path.
*
* @throws UnexpectedTypeException If a value within the path is neither object nor array.
* @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)) {
throw new UnexpectedTypeException($objectOrArray, $propertyPath, 0);
if (!is_object($zval[self::VALUE]) && !is_array($zval[self::VALUE])) {
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) {
$property = $propertyPath->getElement($i);
$isIndex = $propertyPath->isIndex($i);
// Create missing nested arrays on demand
if ($isIndex &&
(
($objectOrArray instanceof \ArrayAccess && !isset($objectOrArray[$property])) ||
(is_array($objectOrArray) && !array_key_exists($property, $objectOrArray))
)
) {
if (!$ignoreInvalidIndices) {
if (!is_array($objectOrArray)) {
if (!$objectOrArray instanceof \Traversable) {
throw new NoSuchIndexException(sprintf(
'Cannot read index "%s" while trying to traverse path "%s".',
$property,
(string) $propertyPath
));
if ($isIndex) {
// Create missing nested arrays on demand
if (($zval[self::VALUE] instanceof \ArrayAccess && !$zval[self::VALUE]->offsetExists($property)) ||
(is_array($zval[self::VALUE]) && !isset($zval[self::VALUE][$property]) && !array_key_exists($property, $zval[self::VALUE]))
) {
if (!$ignoreInvalidIndices) {
if (!is_array($zval[self::VALUE])) {
if (!$zval[self::VALUE] instanceof \Traversable) {
throw new NoSuchIndexException(sprintf(
'Cannot read index "%s" while trying to traverse path "%s".',
$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(
'Cannot read index "%s" while trying to traverse path "%s". Available indices are "%s".',
$property,
(string) $propertyPath,
print_r(array_keys($objectOrArray), true)
));
if ($i + 1 < $propertyPath->getLength()) {
$zval[self::VALUE][$property] = array();
if (isset($zval[self::REF])) {
$zval[self::REF] = $zval[self::VALUE];
}
}
}
if ($i + 1 < $propertyPath->getLength()) {
$objectOrArray[$property] = array();
}
}
if ($isIndex) {
$propertyValue = &$this->readIndex($objectOrArray, $property);
$zval = $this->readIndex($zval, $property);
} 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
if ($i + 1 < $propertyPath->getLength() && !is_object($objectOrArray) && !is_array($objectOrArray)) {
throw new UnexpectedTypeException($objectOrArray, $propertyPath, $i + 1);
if ($i + 1 < $propertyPath->getLength() && !is_object($zval[self::VALUE]) && !is_array($zval[self::VALUE])) {
throw new UnexpectedTypeException($zval[self::VALUE], $propertyPath, $i + 1);
}
// Set the IS_REF_CHAINED flag to true if:
// current property is passed by reference and
// it is the first element in the property path or
// the IS_REF_CHAINED flag of its parent element is true
// Basically, this flag is true only when the reference chain from the top element to current element is not broken
$propertyValue[self::IS_REF_CHAINED] = $propertyValue[self::IS_REF] && (0 === $i || $propertyValues[$i - 1][self::IS_REF_CHAINED]);
if (isset($zval[self::REF]) && (0 === $i || isset($propertyValues[$i - 1][self::IS_REF_CHAINED]))) {
// Set the IS_REF_CHAINED flag to true if:
// current property is passed by reference and
// it is the first element in the property path or
// the IS_REF_CHAINED flag of its parent element is true
// 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;
@ -353,33 +408,30 @@ class PropertyAccessor implements PropertyAccessorInterface
/**
* Reads a key from an array-like structure.
*
* @param \ArrayAccess|array $array The array or \ArrayAccess object to read from
* @param string|int $index The key to read
* @param array $zval The array containing the array or \ArrayAccess object to read from
* @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
*/
private function &readIndex(&$array, $index)
private function readIndex($zval, $index)
{
if (!$array instanceof \ArrayAccess && !is_array($array)) {
throw new NoSuchIndexException(sprintf('Cannot read index "%s" from object of type "%s" because it doesn\'t implement \ArrayAccess.', $index, get_class($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($zval[self::VALUE])));
}
// Use an array instead of an object since performance is very crucial here
$result = array(
self::VALUE => null,
self::IS_REF => false,
);
$result = self::$resultProto;
if (isset($array[$index])) {
if (is_array($array)) {
$result[self::VALUE] = &$array[$index];
$result[self::IS_REF] = true;
} else {
$result[self::VALUE] = $array[$index];
// Objects are always passed around by reference
$result[self::IS_REF] = is_object($array[$index]) ? true : false;
if (isset($zval[self::VALUE][$index])) {
$result[self::VALUE] = $zval[self::VALUE][$index];
if (!isset($zval[self::REF])) {
// Save creating references when doing read-only lookups
} elseif (is_array($zval[self::VALUE])) {
$result[self::REF] = &$zval[self::REF][$index];
} 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.
*
* @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
* public.
* @throws NoSuchPropertyException If the property does not exist or is not public.
*/
private function &readProperty(&$object, $property)
private function readProperty($zval, $property)
{
// Use an array instead of an object since performance is
// very crucial here
$result = array(
self::VALUE => null,
self::IS_REF => false,
);
if (!is_object($object)) {
if (!is_object($zval[self::VALUE])) {
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]) {
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
} elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
if ($access[self::ACCESS_REF]) {
$result[self::VALUE] = &$object->{$access[self::ACCESS_NAME]};
$result[self::IS_REF] = true;
} else {
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]};
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]};
if ($access[self::ACCESS_REF] && isset($zval[self::REF])) {
$result[self::REF] = &$object->{$access[self::ACCESS_NAME]};
}
} elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) {
// 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
// fatal error.
$result[self::VALUE] = &$object->$property;
$result[self::IS_REF] = true;
$result[self::VALUE] = $object->$property;
if (isset($zval[self::REF])) {
$result[self::REF] = &$object->$property;
}
} elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
// we call the getter and hope the __call do the job
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
@ -438,8 +485,8 @@ class PropertyAccessor implements PropertyAccessorInterface
}
// Objects are always passed around by reference
if (is_object($result[self::VALUE])) {
$result[self::IS_REF] = true;
if (isset($zval[self::REF]) && is_object($result[self::VALUE])) {
$result[self::REF] = $result[self::VALUE];
}
return $result;
@ -448,21 +495,21 @@ class PropertyAccessor implements PropertyAccessorInterface
/**
* Guesses how to read the property value.
*
* @param string $object
* @param string $class
* @param string $property
*
* @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])) {
$access = $this->readPropertyCache[$key];
} else {
$access = array();
$reflClass = new \ReflectionClass($object);
$reflClass = new \ReflectionClass($class);
$access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
$camelProp = $this->camelize($property);
$getter = 'get'.$camelProp;
@ -519,45 +566,45 @@ class PropertyAccessor implements PropertyAccessorInterface
/**
* 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 string|int $index The index to write at
* @param mixed $value The value to write
* @param array $zval The array containing the array or \ArrayAccess object to write to
* @param string|int $index The index to write at
* @param mixed $value The value to write
*
* @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)) {
throw new NoSuchIndexException(sprintf('Cannot modify index "%s" in object of type "%s" because it doesn\'t implement \ArrayAccess', $index, get_class($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($zval[self::VALUE])));
}
$array[$index] = $value;
$zval[self::REF][$index] = $value;
}
/**
* 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 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, $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));
}
$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]) {
$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]) {
$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)) {
// Needed to support \stdClass instances. We need to explicitly
// 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*()
* methods.
* Adjusts a collection-valued property by calling add*() and remove*() 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 array|\Traversable $collection The collection to write
* @param string $addMethod The add*() 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
// Use iterator_to_array() instead of clone in order to prevent side effects
// see https://github.com/symfony/symfony/issues/4670
$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);
$previousValue = $this->readProperty($zval, $property);
$previousValue = $previousValue[self::VALUE];
if (is_array($previousValue) || $previousValue instanceof \Traversable) {
foreach ($previousValue as $previousItem) {
foreach ($collection as $key => $item) {
if ($item === $previousItem) {
// Item found, don't add
unset($itemsToAdd[$key]);
// Next $previousItem
continue 2;
}
}
// Item not found, add to remove list
$itemToRemove[] = $previousItem;
if ($previousValue instanceof \Traversable) {
$previousValue = iterator_to_array($previousValue);
}
if ($previousValue && is_array($previousValue)) {
if (is_object($collection)) {
$collection = iterator_to_array($collection);
}
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) {
$object->{$removeMethod}($item);
}
foreach ($itemsToAdd as $item) {
$object->{$addMethod}($item);
foreach ($collection as $item) {
if (!$previousValue || !in_array($item, $previousValue, true)) {
$zval[self::VALUE]->{$addMethod}($item);
}
}
}
/**
* Guesses how to write the property value.
*
* @param string $object
* @param string $class
* @param string $property
* @param mixed $value
*
* @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])) {
$access = $this->writePropertyCache[$key];
} else {
$access = array();
$reflClass = new \ReflectionClass($object);
$reflClass = new \ReflectionClass($class);
$access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
$camelized = $this->camelize($property);
$singulars = (array) StringUtil::singularify($camelized);
@ -778,8 +816,7 @@ class PropertyAccessor implements PropertyAccessorInterface
* @param string $methodName The method name
* @param int $parameters The number of parameters
*
* @return bool Whether the method is public and has $parameters
* required parameters
* @return bool Whether the method is public and has $parameters required 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\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 Exception\UnexpectedTypeException If a value within the path is neither object nor array
*/
public function setValue(&$objectOrArray, $propertyPath, $value);

View File

@ -96,7 +96,7 @@ class PropertyPath implements \IteratorAggregate, PropertyPathInterface
$remaining = $propertyPath;
// first element is evaluated differently - no leading dot for properties
$pattern = '/^(([^\.\[]+)|\[([^\]]+)\])(.*)/';
$pattern = '/^(([^\.\[]++)|\[([^\]]++)\])(.*)/';
while (preg_match($pattern, $remaining, $matches)) {
if ('' !== $matches[2]) {
@ -111,7 +111,7 @@ class PropertyPath implements \IteratorAggregate, PropertyPathInterface
$position += strlen($matches[1]);
$remaining = $matches[4];
$pattern = '/^(\.([^\.|\[]+)|\[([^\]]+)\])(.*)/';
$pattern = '/^(\.([^\.|\[]++)|\[([^\]]++)\])(.*)/';
}
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\TestClassSetValue;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassIsWritable;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TypeHinted;
class PropertyAccessorTest extends \PHPUnit_Framework_TestCase
{
@ -510,4 +511,22 @@ class PropertyAccessorTest extends \PHPUnit_Framework_TestCase
{
$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.
*
* 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
* validating this class, option or getter result successfully.
* validating this class, property or getter result successfully.
*
* Constraint instances are immutable and serializable.
*

View File

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

View File

@ -143,4 +143,16 @@ class EmailValidatorTest extends AbstractConstraintValidatorTest
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();
}
}