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
<?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;
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_DIR = __DIR__.'/.phpunit';
$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);
$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")) {
// 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
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
is useful to projects that must use deprecated interfaces for backward
compatibility reasons.
@ -33,7 +33,7 @@ A summary of deprecation notices is displayed at the end of the test suite:
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"`.
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'] : '';
$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[0][] = array('parameter' => 'database_name');
$data[1][] = array('parameter' => 'twig.form.resources');
return $data;
}

View File

@ -72,9 +72,16 @@ class ObjectsProvider
{
$builder = new ContainerBuilder();
$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(
'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 '';
};
$emptyValue = function (Options $options) {
return $options['required'] ? null : '';
};
// for BC with the "empty_value" option
$placeholder = function (Options $options) {
return $options['empty_value'];
return $options['required'] ? null : '';
};
$choiceListNormalizer = function (Options $options, $choiceList) use ($choiceListFactory) {
@ -287,6 +282,12 @@ class ChoiceType extends AbstractType
};
$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']) {
// never use an empty value for this case
return;
@ -328,7 +329,7 @@ class ChoiceType extends AbstractType
'preferred_choices' => array(),
'group_by' => null,
'empty_data' => $emptyData,
'empty_value' => $emptyValue, // deprecated
'empty_value' => new \Exception(), // deprecated
'placeholder' => $placeholder,
'error_bubbling' => false,
'compound' => $compound,
@ -340,7 +341,6 @@ class ChoiceType extends AbstractType
));
$resolver->setNormalizer('choice_list', $choiceListNormalizer);
$resolver->setNormalizer('empty_value', $placeholderNormalizer);
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);

View File

@ -183,15 +183,17 @@ class DateType extends AbstractType
return $options['widget'] !== 'single_text';
};
$emptyValue = $placeholderDefault = function (Options $options) {
$placeholder = $placeholderDefault = function (Options $options) {
return $options['required'] ? null : '';
};
$placeholder = function (Options $options) {
return $options['empty_value'];
};
$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)) {
$default = $placeholderDefault($options);
@ -238,7 +240,7 @@ class DateType extends AbstractType
'format' => $format,
'model_timezone' => null,
'view_timezone' => null,
'empty_value' => $emptyValue, // deprecated
'empty_value' => new \Exception(), // deprecated
'placeholder' => $placeholder,
'html5' => true,
// Don't modify \DateTime classes by reference, we treat
@ -254,7 +256,6 @@ class DateType extends AbstractType
'choice_translation_domain' => false,
));
$resolver->setNormalizer('empty_value', $placeholderNormalizer);
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);

View File

@ -171,16 +171,17 @@ class TimeType extends AbstractType
return $options['widget'] !== 'single_text';
};
$emptyValue = $placeholderDefault = function (Options $options) {
$placeholder = $placeholderDefault = function (Options $options) {
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) {
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)) {
$default = $placeholderDefault($options);
@ -224,7 +225,7 @@ class TimeType extends AbstractType
'with_seconds' => false,
'model_timezone' => null,
'view_timezone' => null,
'empty_value' => $emptyValue, // deprecated
'empty_value' => new \Exception(), // deprecated
'placeholder' => $placeholder,
'html5' => true,
// Don't modify \DateTime classes by reference, we treat
@ -240,7 +241,6 @@ class TimeType extends AbstractType
'choice_translation_domain' => false,
));
$resolver->setNormalizer('empty_value', $placeholderNormalizer);
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);

View File

@ -32,12 +32,16 @@ class Store implements StoreInterface
* Constructor.
*
* @param string $root The path to the cache directory
*
* @throws \RuntimeException
*/
public function __construct($root)
{
$this->root = $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->locks = array();
@ -74,7 +78,7 @@ class Store implements StoreInterface
public function lock(Request $request)
{
$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;
}
@ -338,7 +342,7 @@ class Store implements StoreInterface
private function save($key, $data)
{
$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;
}

View File

@ -41,8 +41,8 @@ class FileProfilerStorage implements ProfilerStorageInterface
}
$this->folder = substr($dsn, 5);
if (!is_dir($this->folder)) {
mkdir($this->folder, 0777, true);
if (!is_dir($this->folder) && false === @mkdir($this->folder, 0777, true) && !is_dir($this->folder)) {
throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $this->folder));
}
}
@ -128,6 +128,8 @@ class FileProfilerStorage implements ProfilerStorageInterface
/**
* {@inheritdoc}
*
* @throws \RuntimeException
*/
public function write(Profile $profile)
{
@ -137,8 +139,8 @@ class FileProfilerStorage implements ProfilerStorageInterface
if (!$profileIndexed) {
// Create directory
$dir = dirname($file);
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
if (!is_dir($dir) && false === @mkdir($dir, 0777, true) && !is_dir($dir)) {
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}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class PropertyAccessor implements PropertyAccessorInterface
{
const VALUE = 0;
const IS_REF = 1;
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
@ -37,6 +49,16 @@ class PropertyAccessor implements PropertyAccessorInterface
*/
private $ignoreInvalidIndices;
/**
* @var array
*/
private $readPropertyCache = array();
/**
* @var array
*/
private $writePropertyCache = array();
/**
* Should not be used by application code. Use
* {@link PropertyAccess::createPropertyAccessor()} instead.
@ -78,7 +100,7 @@ class PropertyAccessor implements PropertyAccessorInterface
self::IS_REF => true,
self::IS_REF_CHAINED => true,
));
$propertyMaxIndex = count($propertyValues) - 1;
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));
}
$camelized = $this->camelize($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);
$access = $this->getReadAccessInfo($object, $property);
if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) {
$result[self::VALUE] = $object->$getter();
} elseif ($this->isMethodAccessible($reflClass, $getsetter, 0)) {
$result[self::VALUE] = $object->$getsetter();
} elseif ($reflClass->hasMethod($isser) && $reflClass->getMethod($isser)->isPublic()) {
$result[self::VALUE] = $object->$isser();
} elseif ($reflClass->hasMethod($hasser) && $reflClass->getMethod($hasser)->isPublic()) {
$result[self::VALUE] = $object->$hasser();
} elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) {
$result[self::VALUE] = &$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)) {
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]};
}
} elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) {
// Needed to support \stdClass instances. We need to explicitly
// exclude $classHasProperty, otherwise if in the previous clause
// a *protected* property was found on the class, property_exists()
// returns true, consequently the following line will result in a
// fatal error.
$result[self::VALUE] = &$object->$property;
$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
$result[self::VALUE] = $object->$getter();
$result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}();
} else {
$methods = array($getter, $getsetter, $isser, $hasser, '__get');
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
));
throw new NoSuchPropertyException($access[self::ACCESS_NAME]);
}
// Objects are always passed around by reference
@ -385,6 +387,81 @@ class PropertyAccessor implements PropertyAccessorInterface
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.
*
@ -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));
}
$reflClass = new \ReflectionClass($object);
$camelized = $this->camelize($property);
$singulars = (array) StringUtil::singularify($camelized);
$access = $this->getWriteAccessInfo($object, $property, $value);
if (is_array($value) || $value instanceof \Traversable) {
$methods = $this->findAdderAndRemover($reflClass, $singulars);
// Use addXxx() and removeXxx() to write the collection
if (null !== $methods) {
$this->writeCollection($object, $property, $value, $methods[0], $methods[1]);
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)) {
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]);
} elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) {
// Needed to support \stdClass instances. We need to explicitly
// exclude $classHasProperty, otherwise if in the previous clause
// a *protected* property was found on the class, property_exists()
// returns true, consequently the following line will result in a
// fatal error.
$object->$property = $value;
} elseif ($this->magicCall && $this->isMethodAccessible($reflClass, '__call', 2)) {
// we call the getter and hope the __call do the job
$object->$setter($value);
} elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) {
$object->{$access[self::ACCESS_NAME]}($value);
} else {
throw new NoSuchPropertyException(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
));
throw new NoSuchPropertyException($access[self::ACCESS_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.
*

View File

@ -114,7 +114,7 @@ class GetSetMethodNormalizer extends AbstractNormalizer
if ($allowed && !$ignored) {
$setter = 'set'.ucfirst($attribute);
if (in_array($setter, $classMethods)) {
if (in_array($setter, $classMethods) && !$reflectionClass->getMethod($setter)->isStatic()) {
$object->$setter($value);
}
}
@ -170,10 +170,13 @@ class GetSetMethodNormalizer extends AbstractNormalizer
{
$methodLength = strlen($method->name);
return (
((0 === strpos($method->name, 'get') && 3 < $methodLength) ||
(0 === strpos($method->name, 'is') && 2 < $methodLength)) &&
0 === $method->getNumberOfRequiredParameters()
);
return
!$method->isStatic() &&
(
((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);
foreach ($reflectionObject->getProperties() as $property) {
if (in_array($property->name, $this->ignoredAttributes)) {
if (in_array($property->name, $this->ignoredAttributes) || $property->isStatic()) {
continue;
}
@ -110,6 +110,10 @@ class PropertyNormalizer extends AbstractNormalizer
if ($allowed && !$ignored && $reflectionClass->hasProperty($propertyName)) {
$property = $reflectionClass->getProperty($propertyName);
if ($property->isStatic()) {
continue;
}
// Override visibility
if (!$property->isPublic()) {
$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()
{
$this->assertFalse($this->normalizer->supportsNormalization(new \ArrayObject()));
}
public function testNoStaticGetSetSupport()
{
$this->assertFalse($this->normalizer->supportsNormalization(new ObjectWithJustStaticSetterDummy()));
}
public function testPrivateSetter()
{
$obj = $this->normalizer->denormalize(array('foo' => 'foobar'), __NAMESPACE__.'\ObjectWithPrivateSetterDummy');
@ -568,6 +581,7 @@ class GetSetDummy
private $baz;
protected $camelCase;
protected $object;
private static $staticObject;
public function getFoo()
{
@ -628,6 +642,16 @@ class GetSetDummy
{
return $this->object;
}
public static function getStaticObject()
{
return self::$staticObject;
}
public static function setStaticObject($object)
{
self::$staticObject = $object;
}
}
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
* @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()));
}
public function testNoStaticPropertySupport()
{
$this->assertFalse($this->normalizer->supportsNormalization(new StaticPropertyDummy()));
}
}
class PropertyDummy
{
public static $outOfScope = 'out_of_scope';
public $foo;
private $bar;
protected $camelCase;
@ -491,3 +505,9 @@ class PropertyCamelizedDummy
$this->kevinDunglas = $kevinDunglas;
}
}
class StaticPropertyDummy
{
private static $property = 'value';
}

View File

@ -227,7 +227,11 @@ class ReflectionCaster
));
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';
} elseif (method_exists($c, 'isCallable') && $c->isCallable()) {
$a[$prefix.'typeHint'] = 'callable';

View File

@ -100,17 +100,38 @@ EOTXT
/**
* @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(
<<<'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 {
returnType: "int"
class: "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"
}
EOTXT