Merge branch '2.7' into 2.8

* 2.7:
  [ci] Phpunit tests wont run if composer is installed in a wrapper
  [ci] Add version tag in phpunit wrapper to trigger cache-reset on demand
  fix race condition at mkdir (#16258)
  [VarDumper] Fix PHP7 type-hints compat
  [Bridge] [PhpUnit] fixes documentation markup.
  [PropertyAccess] Port of the performance optimization from 2.3
  trigger deprecation warning when using empty_value
  [PropertyAccess] Test access to dynamic properties
  [PropertyAccess] Fix dynamic property accessing.
  [Serializer] GetSetNormalizer shouldn't set/get static methods
  [Serializer] PropertyNormalizer shouldn't set static properties
  JsonDescriptor - encode container params only once

Conflicts:
	src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php
This commit is contained in:
Nicolas Grekas 2015-11-09 13:46:59 +01:00
commit d86a3f3172
21 changed files with 385 additions and 133 deletions

28
phpunit
View File

@ -1,6 +1,18 @@
#!/usr/bin/env php #!/usr/bin/env php
<?php <?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.
*/
// Please update when phpunit needs to be reinstalled with fresh deps:
// Cache-Id-Version: 2015-11-09 12:13 UTC
use Symfony\Component\Process\ProcessUtils; use Symfony\Component\Process\ProcessUtils;
error_reporting(-1); error_reporting(-1);
@ -10,19 +22,11 @@ require __DIR__.'/src/Symfony/Component/Process/ProcessUtils.php';
$PHPUNIT_VERSION = PHP_VERSION_ID >= 70000 ? '5.0' : '4.8'; $PHPUNIT_VERSION = PHP_VERSION_ID >= 70000 ? '5.0' : '4.8';
$PHPUNIT_DIR = __DIR__.'/.phpunit'; $PHPUNIT_DIR = __DIR__.'/.phpunit';
$PHP = defined('PHP_BINARY') ? PHP_BINARY : 'php'; $PHP = defined('PHP_BINARY') ? PHP_BINARY : 'php';
if (!file_exists($COMPOSER = __DIR__.'/composer.phar')) {
$COMPOSER = rtrim('\\' === DIRECTORY_SEPARATOR ? `where.exe composer.phar` : (`which composer.phar` ?: `which composer`));
if (!file_exists($COMPOSER)) {
stream_copy_to_stream(
fopen('https://getcomposer.org/composer.phar', 'rb'),
fopen($COMPOSER = __DIR__.'/composer.phar', 'wb')
);
}
}
$PHP = ProcessUtils::escapeArgument($PHP); $PHP = ProcessUtils::escapeArgument($PHP);
$COMPOSER = $PHP.' '.ProcessUtils::escapeArgument($COMPOSER);
$COMPOSER = file_exists($COMPOSER = __DIR__.'/composer.phar') || ($COMPOSER = rtrim('\\' === DIRECTORY_SEPARATOR ? `where.exe composer.phar` : `which composer.phar`))
? $PHP.' '.ProcessUtils::escapeArgument($COMPOSER)
: 'composer';
if (!file_exists("$PHPUNIT_DIR/phpunit-$PHPUNIT_VERSION/phpunit") || md5_file(__FILE__) !== @file_get_contents("$PHPUNIT_DIR/.md5")) { if (!file_exists("$PHPUNIT_DIR/phpunit-$PHPUNIT_VERSION/phpunit") || md5_file(__FILE__) !== @file_get_contents("$PHPUNIT_DIR/.md5")) {
// Build a standalone phpunit without symfony/yaml // Build a standalone phpunit without symfony/yaml

View File

@ -12,7 +12,7 @@ It comes with the following features:
By default any non-legacy-tagged or any non-@-silenced deprecation notices will By default any non-legacy-tagged or any non-@-silenced deprecation notices will
make tests fail. make tests fail.
This can be changed by setting the SYMFONY_DEPRECATIONS_HELPER environment This can be changed by setting the `SYMFONY_DEPRECATIONS_HELPER` environment
variable to `weak`. This will make the bridge ignore deprecation notices and variable to `weak`. This will make the bridge ignore deprecation notices and
is useful to projects that must use deprecated interfaces for backward is useful to projects that must use deprecated interfaces for backward
compatibility reasons. compatibility reasons.
@ -33,7 +33,7 @@ A summary of deprecation notices is displayed at the end of the test suite:
Usage Usage
----- -----
Add this bridge to the `require-dev` section of your composer.json file Add this bridge to the `require-dev` section of your `composer.json` file
(not in `require`) with e.g. `composer require --dev "symfony/phpunit-bridge"`. (not in `require`) with e.g. `composer require --dev "symfony/phpunit-bridge"`.
When running `phpunit`, you will see a summary of deprecation notices at the end When running `phpunit`, you will see a summary of deprecation notices at the end

View File

@ -157,7 +157,7 @@ class JsonDescriptor extends Descriptor
{ {
$key = isset($options['parameter']) ? $options['parameter'] : ''; $key = isset($options['parameter']) ? $options['parameter'] : '';
$this->writeData(array($key => $this->formatParameter($parameter)), $options); $this->writeData(array($key => $parameter), $options);
} }
/** /**

View File

@ -118,6 +118,7 @@ abstract class AbstractDescriptorTest extends \PHPUnit_Framework_TestCase
$data = $this->getDescriptionTestData(ObjectsProvider::getContainerParameter()); $data = $this->getDescriptionTestData(ObjectsProvider::getContainerParameter());
$data[0][] = array('parameter' => 'database_name'); $data[0][] = array('parameter' => 'database_name');
$data[1][] = array('parameter' => 'twig.form.resources');
return $data; return $data;
} }

View File

@ -72,9 +72,16 @@ class ObjectsProvider
{ {
$builder = new ContainerBuilder(); $builder = new ContainerBuilder();
$builder->setParameter('database_name', 'symfony'); $builder->setParameter('database_name', 'symfony');
$builder->setParameter('twig.form.resources', array(
'bootstrap_3_horizontal_layout.html.twig',
'bootstrap_3_layout.html.twig',
'form_div_layout.html.twig',
'form_table_layout.html.twig',
));
return array( return array(
'parameter' => $builder, 'parameter' => $builder,
'array_parameter' => $builder,
); );
} }

View File

@ -0,0 +1,3 @@
{
"twig.form.resources": ["bootstrap_3_horizontal_layout.html.twig", "bootstrap_3_layout.html.twig", "form_div_layout.html.twig", "form_table_layout.html.twig"]
}

View File

@ -0,0 +1,4 @@
twig.form.resources
===================
["bootstrap_3_horizontal_layout.html.twig","bootstrap_3_layo...

View File

@ -0,0 +1 @@
["bootstrap_3_horizontal_layout.html.twig","bootstrap_3_layo...

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<parameter key="twig.form.resources">["bootstrap_3_horizontal_layout.html.twig","bootstrap_3_layo...</parameter>

View File

@ -248,13 +248,8 @@ class ChoiceType extends AbstractType
return ''; return '';
}; };
$emptyValue = function (Options $options) {
return $options['required'] ? null : '';
};
// for BC with the "empty_value" option
$placeholder = function (Options $options) { $placeholder = function (Options $options) {
return $options['empty_value']; return $options['required'] ? null : '';
}; };
$choiceListNormalizer = function (Options $options, $choiceList) use ($choiceListFactory) { $choiceListNormalizer = function (Options $options, $choiceList) use ($choiceListFactory) {
@ -287,6 +282,12 @@ class ChoiceType extends AbstractType
}; };
$placeholderNormalizer = function (Options $options, $placeholder) { $placeholderNormalizer = function (Options $options, $placeholder) {
if (!is_object($options['empty_value']) || !$options['empty_value'] instanceof \Exception) {
@trigger_error('The form option "empty_value" is deprecated since version 2.6 and will be removed in 3.0. Use "placeholder" instead.', E_USER_DEPRECATED);
$placeholder = $options['empty_value'];
}
if ($options['multiple']) { if ($options['multiple']) {
// never use an empty value for this case // never use an empty value for this case
return; return;
@ -328,7 +329,7 @@ class ChoiceType extends AbstractType
'preferred_choices' => array(), 'preferred_choices' => array(),
'group_by' => null, 'group_by' => null,
'empty_data' => $emptyData, 'empty_data' => $emptyData,
'empty_value' => $emptyValue, // deprecated 'empty_value' => new \Exception(), // deprecated
'placeholder' => $placeholder, 'placeholder' => $placeholder,
'error_bubbling' => false, 'error_bubbling' => false,
'compound' => $compound, 'compound' => $compound,
@ -340,7 +341,6 @@ class ChoiceType extends AbstractType
)); ));
$resolver->setNormalizer('choice_list', $choiceListNormalizer); $resolver->setNormalizer('choice_list', $choiceListNormalizer);
$resolver->setNormalizer('empty_value', $placeholderNormalizer);
$resolver->setNormalizer('placeholder', $placeholderNormalizer); $resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer); $resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);

View File

@ -183,15 +183,17 @@ class DateType extends AbstractType
return $options['widget'] !== 'single_text'; return $options['widget'] !== 'single_text';
}; };
$emptyValue = $placeholderDefault = function (Options $options) { $placeholder = $placeholderDefault = function (Options $options) {
return $options['required'] ? null : ''; return $options['required'] ? null : '';
}; };
$placeholder = function (Options $options) {
return $options['empty_value'];
};
$placeholderNormalizer = function (Options $options, $placeholder) use ($placeholderDefault) { $placeholderNormalizer = function (Options $options, $placeholder) use ($placeholderDefault) {
if (!is_object($options['empty_value']) || !$options['empty_value'] instanceof \Exception) {
@trigger_error('The form option "empty_value" is deprecated since version 2.6 and will be removed in 3.0. Use "placeholder" instead.', E_USER_DEPRECATED);
$placeholder = $options['empty_value'];
}
if (is_array($placeholder)) { if (is_array($placeholder)) {
$default = $placeholderDefault($options); $default = $placeholderDefault($options);
@ -238,7 +240,7 @@ class DateType extends AbstractType
'format' => $format, 'format' => $format,
'model_timezone' => null, 'model_timezone' => null,
'view_timezone' => null, 'view_timezone' => null,
'empty_value' => $emptyValue, // deprecated 'empty_value' => new \Exception(), // deprecated
'placeholder' => $placeholder, 'placeholder' => $placeholder,
'html5' => true, 'html5' => true,
// Don't modify \DateTime classes by reference, we treat // Don't modify \DateTime classes by reference, we treat
@ -254,7 +256,6 @@ class DateType extends AbstractType
'choice_translation_domain' => false, 'choice_translation_domain' => false,
)); ));
$resolver->setNormalizer('empty_value', $placeholderNormalizer);
$resolver->setNormalizer('placeholder', $placeholderNormalizer); $resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer); $resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);

View File

@ -171,16 +171,17 @@ class TimeType extends AbstractType
return $options['widget'] !== 'single_text'; return $options['widget'] !== 'single_text';
}; };
$emptyValue = $placeholderDefault = function (Options $options) { $placeholder = $placeholderDefault = function (Options $options) {
return $options['required'] ? null : ''; return $options['required'] ? null : '';
}; };
// for BC with the "empty_value" option
$placeholder = function (Options $options) {
return $options['empty_value'];
};
$placeholderNormalizer = function (Options $options, $placeholder) use ($placeholderDefault) { $placeholderNormalizer = function (Options $options, $placeholder) use ($placeholderDefault) {
if (!is_object($options['empty_value']) || !$options['empty_value'] instanceof \Exception) {
@trigger_error('The form option "empty_value" is deprecated since version 2.6 and will be removed in 3.0. Use "placeholder" instead.', E_USER_DEPRECATED);
$placeholder = $options['empty_value'];
}
if (is_array($placeholder)) { if (is_array($placeholder)) {
$default = $placeholderDefault($options); $default = $placeholderDefault($options);
@ -224,7 +225,7 @@ class TimeType extends AbstractType
'with_seconds' => false, 'with_seconds' => false,
'model_timezone' => null, 'model_timezone' => null,
'view_timezone' => null, 'view_timezone' => null,
'empty_value' => $emptyValue, // deprecated 'empty_value' => new \Exception(), // deprecated
'placeholder' => $placeholder, 'placeholder' => $placeholder,
'html5' => true, 'html5' => true,
// Don't modify \DateTime classes by reference, we treat // Don't modify \DateTime classes by reference, we treat
@ -240,7 +241,6 @@ class TimeType extends AbstractType
'choice_translation_domain' => false, 'choice_translation_domain' => false,
)); ));
$resolver->setNormalizer('empty_value', $placeholderNormalizer);
$resolver->setNormalizer('placeholder', $placeholderNormalizer); $resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer); $resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);

View File

@ -32,12 +32,16 @@ class Store implements StoreInterface
* Constructor. * Constructor.
* *
* @param string $root The path to the cache directory * @param string $root The path to the cache directory
*
* @throws \RuntimeException
*/ */
public function __construct($root) public function __construct($root)
{ {
$this->root = $root; $this->root = $root;
if (!is_dir($this->root)) { if (!is_dir($this->root)) {
mkdir($this->root, 0777, true); if (false === @mkdir($this->root, 0777, true) && !is_dir($this->root)) {
throw new \RuntimeException(sprintf('Unable to create the store directory (%s).', $this->root));
}
} }
$this->keyCache = new \SplObjectStorage(); $this->keyCache = new \SplObjectStorage();
$this->locks = array(); $this->locks = array();
@ -74,7 +78,7 @@ class Store implements StoreInterface
public function lock(Request $request) public function lock(Request $request)
{ {
$path = $this->getPath($this->getCacheKey($request).'.lck'); $path = $this->getPath($this->getCacheKey($request).'.lck');
if (!is_dir(dirname($path)) && false === @mkdir(dirname($path), 0777, true)) { if (!is_dir(dirname($path)) && false === @mkdir(dirname($path), 0777, true) && !is_dir(dirname($path))) {
return false; return false;
} }
@ -338,7 +342,7 @@ class Store implements StoreInterface
private function save($key, $data) private function save($key, $data)
{ {
$path = $this->getPath($key); $path = $this->getPath($key);
if (!is_dir(dirname($path)) && false === @mkdir(dirname($path), 0777, true)) { if (!is_dir(dirname($path)) && false === @mkdir(dirname($path), 0777, true) && !is_dir(dirname($path))) {
return false; return false;
} }

View File

@ -41,8 +41,8 @@ class FileProfilerStorage implements ProfilerStorageInterface
} }
$this->folder = substr($dsn, 5); $this->folder = substr($dsn, 5);
if (!is_dir($this->folder)) { if (!is_dir($this->folder) && false === @mkdir($this->folder, 0777, true) && !is_dir($this->folder)) {
mkdir($this->folder, 0777, true); throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $this->folder));
} }
} }
@ -128,6 +128,8 @@ class FileProfilerStorage implements ProfilerStorageInterface
/** /**
* {@inheritdoc} * {@inheritdoc}
*
* @throws \RuntimeException
*/ */
public function write(Profile $profile) public function write(Profile $profile)
{ {
@ -137,8 +139,8 @@ class FileProfilerStorage implements ProfilerStorageInterface
if (!$profileIndexed) { if (!$profileIndexed) {
// Create directory // Create directory
$dir = dirname($file); $dir = dirname($file);
if (!is_dir($dir)) { if (!is_dir($dir) && false === @mkdir($dir, 0777, true) && !is_dir($dir)) {
mkdir($dir, 0777, true); throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $dir));
} }
} }

View File

@ -20,12 +20,24 @@ use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
* Default implementation of {@link PropertyAccessorInterface}. * Default implementation of {@link PropertyAccessorInterface}.
* *
* @author Bernhard Schussek <bschussek@gmail.com> * @author Bernhard Schussek <bschussek@gmail.com>
* @author Kévin Dunglas <dunglas@gmail.com>
*/ */
class PropertyAccessor implements PropertyAccessorInterface class PropertyAccessor implements PropertyAccessorInterface
{ {
const VALUE = 0; const VALUE = 0;
const IS_REF = 1; const IS_REF = 1;
const IS_REF_CHAINED = 2; const IS_REF_CHAINED = 2;
const ACCESS_HAS_PROPERTY = 0;
const ACCESS_TYPE = 1;
const ACCESS_NAME = 2;
const ACCESS_REF = 3;
const ACCESS_ADDER = 4;
const ACCESS_REMOVER = 5;
const ACCESS_TYPE_METHOD = 0;
const ACCESS_TYPE_PROPERTY = 1;
const ACCESS_TYPE_MAGIC = 2;
const ACCESS_TYPE_ADDER_AND_REMOVER = 3;
const ACCESS_TYPE_NOT_FOUND = 4;
/** /**
* @var bool * @var bool
@ -37,6 +49,16 @@ class PropertyAccessor implements PropertyAccessorInterface
*/ */
private $ignoreInvalidIndices; private $ignoreInvalidIndices;
/**
* @var array
*/
private $readPropertyCache = array();
/**
* @var array
*/
private $writePropertyCache = array();
/** /**
* Should not be used by application code. Use * Should not be used by application code. Use
* {@link PropertyAccess::createPropertyAccessor()} instead. * {@link PropertyAccess::createPropertyAccessor()} instead.
@ -78,7 +100,7 @@ class PropertyAccessor implements PropertyAccessorInterface
self::IS_REF => true, self::IS_REF => true,
self::IS_REF_CHAINED => true, self::IS_REF_CHAINED => true,
)); ));
$propertyMaxIndex = count($propertyValues) - 1; $propertyMaxIndex = count($propertyValues) - 1;
for ($i = $propertyMaxIndex; $i >= 0; --$i) { for ($i = $propertyMaxIndex; $i >= 0; --$i) {
@ -330,51 +352,31 @@ class PropertyAccessor implements PropertyAccessorInterface
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));
} }
$camelized = $this->camelize($property); $access = $this->getReadAccessInfo($object, $property);
$reflClass = new \ReflectionClass($object);
$getter = 'get'.$camelized;
$getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item)
$isser = 'is'.$camelized;
$hasser = 'has'.$camelized;
$classHasProperty = $reflClass->hasProperty($property);
if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) { if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
$result[self::VALUE] = $object->$getter(); $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
} elseif ($this->isMethodAccessible($reflClass, $getsetter, 0)) { } elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
$result[self::VALUE] = $object->$getsetter(); if ($access[self::ACCESS_REF]) {
} elseif ($reflClass->hasMethod($isser) && $reflClass->getMethod($isser)->isPublic()) { $result[self::VALUE] = &$object->{$access[self::ACCESS_NAME]};
$result[self::VALUE] = $object->$isser(); $result[self::IS_REF] = true;
} elseif ($reflClass->hasMethod($hasser) && $reflClass->getMethod($hasser)->isPublic()) { } else {
$result[self::VALUE] = $object->$hasser(); $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]};
} elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) { }
$result[self::VALUE] = &$object->$property; } elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) {
$result[self::IS_REF] = true;
} elseif ($reflClass->hasMethod('__get') && $reflClass->getMethod('__get')->isPublic()) {
$result[self::VALUE] = $object->$property;
} elseif (!$classHasProperty && property_exists($object, $property)) {
// Needed to support \stdClass instances. We need to explicitly // Needed to support \stdClass instances. We need to explicitly
// exclude $classHasProperty, otherwise if in the previous clause // exclude $classHasProperty, otherwise if in the previous clause
// a *protected* property was found on the class, property_exists() // a *protected* property was found on the class, property_exists()
// 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; $result[self::IS_REF] = true;
} elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) { } 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->$getter(); $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
} else { } else {
$methods = array($getter, $getsetter, $isser, $hasser, '__get'); throw new NoSuchPropertyException($access[self::ACCESS_NAME]);
if ($this->magicCall) {
$methods[] = '__call';
}
throw new NoSuchPropertyException(sprintf(
'Neither the property "%s" nor one of the methods "%s()" '.
'exist and have public access in class "%s".',
$property,
implode('()", "', $methods),
$reflClass->name
));
} }
// Objects are always passed around by reference // Objects are always passed around by reference
@ -385,6 +387,81 @@ class PropertyAccessor implements PropertyAccessorInterface
return $result; return $result;
} }
/**
* Guesses how to read the property value.
*
* @param string $object
* @param string $property
*
* @return array
*/
private function getReadAccessInfo($object, $property)
{
$key = get_class($object).'::'.$property;
if (isset($this->readPropertyCache[$key])) {
$access = $this->readPropertyCache[$key];
} else {
$access = array();
$reflClass = new \ReflectionClass($object);
$access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
$camelProp = $this->camelize($property);
$getter = 'get'.$camelProp;
$getsetter = lcfirst($camelProp); // jQuery style, e.g. read: last(), write: last($item)
$isser = 'is'.$camelProp;
$hasser = 'has'.$camelProp;
$classHasProperty = $reflClass->hasProperty($property);
if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
$access[self::ACCESS_NAME] = $getter;
} elseif ($reflClass->hasMethod($getsetter) && $reflClass->getMethod($getsetter)->isPublic()) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
$access[self::ACCESS_NAME] = $getsetter;
} elseif ($reflClass->hasMethod($isser) && $reflClass->getMethod($isser)->isPublic()) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
$access[self::ACCESS_NAME] = $isser;
} elseif ($reflClass->hasMethod($hasser) && $reflClass->getMethod($hasser)->isPublic()) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
$access[self::ACCESS_NAME] = $hasser;
} elseif ($reflClass->hasMethod('__get') && $reflClass->getMethod('__get')->isPublic()) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
$access[self::ACCESS_NAME] = $property;
$access[self::ACCESS_REF] = false;
} elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
$access[self::ACCESS_NAME] = $property;
$access[self::ACCESS_REF] = true;
$result[self::VALUE] = &$object->$property;
$result[self::IS_REF] = true;
} elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) {
// we call the getter and hope the __call do the job
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
$access[self::ACCESS_NAME] = $getter;
} else {
$methods = array($getter, $getsetter, $isser, $hasser, '__get');
if ($this->magicCall) {
$methods[] = '__call';
}
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
$access[self::ACCESS_NAME] = sprintf(
'Neither the property "%s" nor one of the methods "%s()" '.
'exist and have public access in class "%s".',
$property,
implode('()", "', $methods),
$reflClass->name
);
}
$this->readPropertyCache[$key] = $access;
}
return $access;
}
/** /**
* Sets the value of an index in a given array-accessible value. * Sets the value of an index in a given array-accessible value.
* *
@ -419,55 +496,26 @@ class PropertyAccessor implements PropertyAccessorInterface
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));
} }
$reflClass = new \ReflectionClass($object); $access = $this->getWriteAccessInfo($object, $property, $value);
$camelized = $this->camelize($property);
$singulars = (array) StringUtil::singularify($camelized);
if (is_array($value) || $value instanceof \Traversable) { if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) {
$methods = $this->findAdderAndRemover($reflClass, $singulars); $object->{$access[self::ACCESS_NAME]}($value);
} elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) {
// Use addXxx() and removeXxx() to write the collection $object->{$access[self::ACCESS_NAME]} = $value;
if (null !== $methods) { } elseif (self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]) {
$this->writeCollection($object, $property, $value, $methods[0], $methods[1]); $this->writeCollection($object, $property, $value, $access[self::ACCESS_ADDER], $access[self::ACCESS_REMOVER]);
} elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) {
return;
}
}
$setter = 'set'.$camelized;
$getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item)
$classHasProperty = $reflClass->hasProperty($property);
if ($this->isMethodAccessible($reflClass, $setter, 1)) {
$object->$setter($value);
} elseif ($this->isMethodAccessible($reflClass, $getsetter, 1)) {
$object->$getsetter($value);
} elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) {
$object->$property = $value;
} elseif ($this->isMethodAccessible($reflClass, '__set', 2)) {
$object->$property = $value;
} elseif (!$classHasProperty && property_exists($object, $property)) {
// Needed to support \stdClass instances. We need to explicitly // Needed to support \stdClass instances. We need to explicitly
// exclude $classHasProperty, otherwise if in the previous clause // exclude $classHasProperty, otherwise if in the previous clause
// a *protected* property was found on the class, property_exists() // a *protected* property was found on the class, property_exists()
// returns true, consequently the following line will result in a // returns true, consequently the following line will result in a
// fatal error. // fatal error.
$object->$property = $value; $object->$property = $value;
} elseif ($this->magicCall && $this->isMethodAccessible($reflClass, '__call', 2)) { } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
// we call the getter and hope the __call do the job $object->{$access[self::ACCESS_NAME]}($value);
$object->$setter($value);
} else { } else {
throw new NoSuchPropertyException(sprintf( throw new NoSuchPropertyException($access[self::ACCESS_NAME]);
'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '.
'"__set()" or "__call()" exist and have public access in class "%s".',
$property,
implode('', array_map(function ($singular) {
return '"add'.$singular.'()"/"remove'.$singular.'()", ';
}, $singulars)),
$setter,
$getsetter,
$reflClass->name
));
} }
} }
@ -519,6 +567,90 @@ class PropertyAccessor implements PropertyAccessorInterface
} }
} }
/**
* Guesses how to write the property value.
*
* @param string $object
* @param string $property
* @param mixed $value
*
* @return array
*/
private function getWriteAccessInfo($object, $property, $value)
{
$key = get_class($object).'::'.$property;
$guessedAdders = '';
if (isset($this->writePropertyCache[$key])) {
$access = $this->writePropertyCache[$key];
} else {
$access = array();
$reflClass = new \ReflectionClass($object);
$access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
$camelized = $this->camelize($property);
$singulars = (array) StringUtil::singularify($camelized);
if (is_array($value) || $value instanceof \Traversable) {
$methods = $this->findAdderAndRemover($reflClass, $singulars);
if (null === $methods) {
// It is sufficient to include only the adders in the error
// message. If the user implements the adder but not the remover,
// an exception will be thrown in findAdderAndRemover() that
// the remover has to be implemented as well.
$guessedAdders = '"add'.implode('()", "add', $singulars).'()", ';
} else {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER;
$access[self::ACCESS_ADDER] = $methods[0];
$access[self::ACCESS_REMOVER] = $methods[1];
}
}
if (!isset($access[self::ACCESS_TYPE])) {
$setter = 'set'.$this->camelize($property);
$getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item)
$classHasProperty = $reflClass->hasProperty($property);
if ($this->isMethodAccessible($reflClass, $setter, 1)) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
$access[self::ACCESS_NAME] = $setter;
} elseif ($this->isMethodAccessible($reflClass, $getsetter, 1)) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
$access[self::ACCESS_NAME] = $getsetter;
} elseif ($this->isMethodAccessible($reflClass, '__set', 2)) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
$access[self::ACCESS_NAME] = $property;
} elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
$access[self::ACCESS_NAME] = $property;
} elseif ($this->magicCall && $this->isMethodAccessible($reflClass, '__call', 2)) {
// we call the getter and hope the __call do the job
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
$access[self::ACCESS_NAME] = $setter;
} else {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
$access[self::ACCESS_NAME] = sprintf(
'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '.
'"__set()" or "__call()" exist and have public access in class "%s".',
$property,
implode('', array_map(function ($singular) {
return '"add'.$singular.'()"/"remove'.$singular.'()", ';
}, $singulars)),
$setter,
$getsetter,
$reflClass->name
);
}
}
$this->writePropertyCache[$key] = $access;
}
return $access;
}
/** /**
* Returns whether a property is writable in the given object. * Returns whether a property is writable in the given object.
* *

View File

@ -114,7 +114,7 @@ class GetSetMethodNormalizer extends AbstractNormalizer
if ($allowed && !$ignored) { if ($allowed && !$ignored) {
$setter = 'set'.ucfirst($attribute); $setter = 'set'.ucfirst($attribute);
if (in_array($setter, $classMethods)) { if (in_array($setter, $classMethods) && !$reflectionClass->getMethod($setter)->isStatic()) {
$object->$setter($value); $object->$setter($value);
} }
} }
@ -170,10 +170,13 @@ class GetSetMethodNormalizer extends AbstractNormalizer
{ {
$methodLength = strlen($method->name); $methodLength = strlen($method->name);
return ( return
((0 === strpos($method->name, 'get') && 3 < $methodLength) || !$method->isStatic() &&
(0 === strpos($method->name, 'is') && 2 < $methodLength)) && (
0 === $method->getNumberOfRequiredParameters() ((0 === strpos($method->name, 'get') && 3 < $methodLength) ||
); (0 === strpos($method->name, 'is') && 2 < $methodLength)) &&
0 === $method->getNumberOfRequiredParameters()
)
;
} }
} }

View File

@ -50,7 +50,7 @@ class PropertyNormalizer extends AbstractNormalizer
$allowedAttributes = $this->getAllowedAttributes($object, $context, true); $allowedAttributes = $this->getAllowedAttributes($object, $context, true);
foreach ($reflectionObject->getProperties() as $property) { foreach ($reflectionObject->getProperties() as $property) {
if (in_array($property->name, $this->ignoredAttributes)) { if (in_array($property->name, $this->ignoredAttributes) || $property->isStatic()) {
continue; continue;
} }
@ -110,6 +110,10 @@ class PropertyNormalizer extends AbstractNormalizer
if ($allowed && !$ignored && $reflectionClass->hasProperty($propertyName)) { if ($allowed && !$ignored && $reflectionClass->hasProperty($propertyName)) {
$property = $reflectionClass->getProperty($propertyName); $property = $reflectionClass->getProperty($propertyName);
if ($property->isStatic()) {
continue;
}
// Override visibility // Override visibility
if (!$property->isPublic()) { if (!$property->isPublic()) {
$property->setAccessible(true); $property->setAccessible(true);

View File

@ -549,11 +549,24 @@ class GetSetMethodNormalizerTest extends \PHPUnit_Framework_TestCase
); );
} }
public function testDenormalizeShouldNotSetStaticAttribute()
{
$obj = $this->normalizer->denormalize(array('staticObject' => true), __NAMESPACE__.'\GetSetDummy');
$this->assertEquals(new GetSetDummy(), $obj);
$this->assertNull(GetSetDummy::getStaticObject());
}
public function testNoTraversableSupport() public function testNoTraversableSupport()
{ {
$this->assertFalse($this->normalizer->supportsNormalization(new \ArrayObject())); $this->assertFalse($this->normalizer->supportsNormalization(new \ArrayObject()));
} }
public function testNoStaticGetSetSupport()
{
$this->assertFalse($this->normalizer->supportsNormalization(new ObjectWithJustStaticSetterDummy()));
}
public function testPrivateSetter() public function testPrivateSetter()
{ {
$obj = $this->normalizer->denormalize(array('foo' => 'foobar'), __NAMESPACE__.'\ObjectWithPrivateSetterDummy'); $obj = $this->normalizer->denormalize(array('foo' => 'foobar'), __NAMESPACE__.'\ObjectWithPrivateSetterDummy');
@ -568,6 +581,7 @@ class GetSetDummy
private $baz; private $baz;
protected $camelCase; protected $camelCase;
protected $object; protected $object;
private static $staticObject;
public function getFoo() public function getFoo()
{ {
@ -628,6 +642,16 @@ class GetSetDummy
{ {
return $this->object; return $this->object;
} }
public static function getStaticObject()
{
return self::$staticObject;
}
public static function setStaticObject($object)
{
self::$staticObject = $object;
}
} }
class GetConstructorDummy class GetConstructorDummy
@ -799,3 +823,18 @@ class ObjectWithPrivateSetterDummy
{ {
} }
} }
class ObjectWithJustStaticSetterDummy
{
private static $foo = 'bar';
public static function getFoo()
{
return self::$foo;
}
public static function setFoo($foo)
{
self::$foo = $foo;
}
}

View File

@ -409,6 +409,14 @@ class PropertyNormalizerTest extends \PHPUnit_Framework_TestCase
); );
} }
public function testDenormalizeShouldIgnoreStaticProperty()
{
$obj = $this->normalizer->denormalize(array('outOfScope' => true), __NAMESPACE__.'\PropertyDummy');
$this->assertEquals(new PropertyDummy(), $obj);
$this->assertEquals('out_of_scope', PropertyDummy::$outOfScope);
}
/** /**
* @expectedException \Symfony\Component\Serializer\Exception\LogicException * @expectedException \Symfony\Component\Serializer\Exception\LogicException
* @expectedExceptionMessage Cannot normalize attribute "bar" because injected serializer is not a normalizer * @expectedExceptionMessage Cannot normalize attribute "bar" because injected serializer is not a normalizer
@ -429,10 +437,16 @@ class PropertyNormalizerTest extends \PHPUnit_Framework_TestCase
{ {
$this->assertFalse($this->normalizer->supportsNormalization(new \ArrayObject())); $this->assertFalse($this->normalizer->supportsNormalization(new \ArrayObject()));
} }
public function testNoStaticPropertySupport()
{
$this->assertFalse($this->normalizer->supportsNormalization(new StaticPropertyDummy()));
}
} }
class PropertyDummy class PropertyDummy
{ {
public static $outOfScope = 'out_of_scope';
public $foo; public $foo;
private $bar; private $bar;
protected $camelCase; protected $camelCase;
@ -491,3 +505,9 @@ class PropertyCamelizedDummy
$this->kevinDunglas = $kevinDunglas; $this->kevinDunglas = $kevinDunglas;
} }
} }
class StaticPropertyDummy
{
private static $property = 'value';
}

View File

@ -227,7 +227,11 @@ class ReflectionCaster
)); ));
try { try {
if ($c->isArray()) { if (method_exists($c, 'hasType')) {
if ($c->hasType()) {
$a[$prefix.'typeHint'] = $c->getType()->__toString();
}
} elseif ($c->isArray()) {
$a[$prefix.'typeHint'] = 'array'; $a[$prefix.'typeHint'] = 'array';
} elseif (method_exists($c, 'isCallable') && $c->isCallable()) { } elseif (method_exists($c, 'isCallable') && $c->isCallable()) {
$a[$prefix.'typeHint'] = 'callable'; $a[$prefix.'typeHint'] = 'callable';

View File

@ -100,17 +100,38 @@ EOTXT
/** /**
* @requires PHP 7.0 * @requires PHP 7.0
*/ */
public function testReturnType() public function testReflectionParameterScalar()
{ {
$f = eval('return function ():int {};'); $f = eval('return function (int $a) {};');
$var = new \ReflectionParameter($f, 0);
$this->assertDumpMatchesFormat( $this->assertDumpMatchesFormat(
<<<'EOTXT' <<<'EOTXT'
ReflectionParameter {
+name: "a"
position: 0
typeHint: "int"
}
EOTXT
, $var
);
}
/**
* @requires PHP 7.0
*/
public function testReturnType()
{
$f = eval('return function ():int {};');
$line = __LINE__ - 1;
$this->assertDumpMatchesFormat(
<<<EOTXT
Closure { Closure {
returnType: "int" returnType: "int"
class: "Symfony\Component\VarDumper\Tests\Caster\ReflectionCasterTest" class: "Symfony\Component\VarDumper\Tests\Caster\ReflectionCasterTest"
this: Symfony\Component\VarDumper\Tests\Caster\ReflectionCasterTest { } this: Symfony\Component\VarDumper\Tests\Caster\ReflectionCasterTest { }
file: "%sReflectionCasterTest.php(105) : eval()'d code" file: "%sReflectionCasterTest.php($line) : eval()'d code"
line: "1 to 1" line: "1 to 1"
} }
EOTXT EOTXT