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:
commit
d86a3f3172
28
phpunit
28
phpunit
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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"]
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
twig.form.resources
|
||||
===================
|
||||
|
||||
["bootstrap_3_horizontal_layout.html.twig","bootstrap_3_layo...
|
@ -0,0 +1 @@
|
||||
["bootstrap_3_horizontal_layout.html.twig","bootstrap_3_layo...
|
@ -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>
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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()
|
||||
)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user