Merge branch '3.4'
* 3.4: [TwigBridge] fix BC for FormExtension if renderer is FormRenderer [Form] Fix 5.5 compatibility for ResizeFormListener [BrowserKit] Handle deprecations triggered in insulated requests [Bridge\PhpUnit] Handle deprecations triggered in separate processes Fix LogLevel::DEBUG as min level [Validator] added magic method __isset() to File Constraint class Support array of types in allowed type [DI] Fix possible incorrect php-code when dumped strings contains newlines [Translation] minor: remove unused variable in test added ability to handle parent classes for PropertyNormalizer replace parameters in dummy identity translator never match invalid IP addresses
This commit is contained in:
commit
4198ad3f88
@ -238,6 +238,20 @@ class DeprecationErrorHandler
|
||||
}
|
||||
}
|
||||
|
||||
public static function collectDeprecations($outputFile)
|
||||
{
|
||||
$deprecations = array();
|
||||
$previousErrorHandler = set_error_handler(function ($type, $msg, $file, $line, $context = array()) use (&$deprecations, &$previousErrorHandler) {
|
||||
if (E_USER_DEPRECATED !== $type && E_DEPRECATED !== $type) {
|
||||
return $previousErrorHandler ? $previousErrorHandler($type, $msg, $file, $line, $context) : false;
|
||||
}
|
||||
$deprecations[] = array(error_reporting(), $msg);
|
||||
});
|
||||
register_shutdown_function(function () use ($outputFile, &$deprecations) {
|
||||
file_put_contents($outputFile, serialize($deprecations));
|
||||
});
|
||||
}
|
||||
|
||||
private static function hasColorSupport()
|
||||
{
|
||||
if ('\\' === DIRECTORY_SEPARATOR) {
|
||||
|
@ -39,6 +39,7 @@ class SymfonyTestsListenerTrait
|
||||
private $testsWithWarnings;
|
||||
private $reportUselessTests;
|
||||
private $error;
|
||||
private $runsInSeparateProcess = false;
|
||||
|
||||
/**
|
||||
* @param array $mockedNamespaces List of namespaces, indexed by mocked features (time-sensitive or dns-sensitive)
|
||||
@ -174,6 +175,12 @@ class SymfonyTestsListenerTrait
|
||||
$this->reportUselessTests = $test->getTestResultObject()->isStrictAboutTestsThatDoNotTestAnything();
|
||||
}
|
||||
|
||||
// This event is triggered before the test is re-run in isolation
|
||||
if ($this->willBeIsolated($test)) {
|
||||
$this->runsInSeparateProcess = tempnam(sys_get_temp_dir(), 'deprec');
|
||||
putenv('SYMFONY_DEPRECATIONS_SERIALIZE='.$this->runsInSeparateProcess);
|
||||
}
|
||||
|
||||
if (class_exists('PHPUnit_Util_Blacklist', false)) {
|
||||
$Test = 'PHPUnit_Util_Test';
|
||||
$AssertionFailedError = 'PHPUnit_Framework_AssertionFailedError';
|
||||
@ -183,12 +190,14 @@ class SymfonyTestsListenerTrait
|
||||
}
|
||||
$groups = $Test::getGroups(get_class($test), $test->getName(false));
|
||||
|
||||
if (in_array('time-sensitive', $groups, true)) {
|
||||
ClockMock::register(get_class($test));
|
||||
ClockMock::withClockMock(true);
|
||||
}
|
||||
if (in_array('dns-sensitive', $groups, true)) {
|
||||
DnsMock::register(get_class($test));
|
||||
if (!$this->runsInSeparateProcess) {
|
||||
if (in_array('time-sensitive', $groups, true)) {
|
||||
ClockMock::register(get_class($test));
|
||||
ClockMock::withClockMock(true);
|
||||
}
|
||||
if (in_array('dns-sensitive', $groups, true)) {
|
||||
DnsMock::register(get_class($test));
|
||||
}
|
||||
}
|
||||
|
||||
$annotations = $Test::parseTestMethodAnnotations(get_class($test), $test->getName(false));
|
||||
@ -236,17 +245,22 @@ class SymfonyTestsListenerTrait
|
||||
$this->reportUselessTests = null;
|
||||
}
|
||||
|
||||
$errored = false;
|
||||
|
||||
if (null !== $this->error) {
|
||||
if ($BaseTestRunner::STATUS_PASSED === $test->getStatus()) {
|
||||
$test->getTestResultObject()->addError($test, $this->error, 0);
|
||||
$errored = true;
|
||||
}
|
||||
|
||||
if ($errored = null !== $this->error) {
|
||||
$test->getTestResultObject()->addError($test, $this->error, 0);
|
||||
$this->error = null;
|
||||
}
|
||||
|
||||
if ($this->runsInSeparateProcess) {
|
||||
foreach (unserialize(file_get_contents($this->runsInSeparateProcess)) as $deprecation) {
|
||||
if ($deprecation[0]) {
|
||||
trigger_error($deprecation[1], E_USER_DEPRECATED);
|
||||
} else {
|
||||
@trigger_error($deprecation[1], E_USER_DEPRECATED);
|
||||
}
|
||||
}
|
||||
$this->runsInSeparateProcess = false;
|
||||
}
|
||||
|
||||
if ($this->expectedDeprecations) {
|
||||
if (!in_array($test->getStatus(), array($BaseTestRunner::STATUS_SKIPPED, $BaseTestRunner::STATUS_INCOMPLETE), true)) {
|
||||
$test->addToAssertionCount(count($this->expectedDeprecations));
|
||||
@ -268,7 +282,7 @@ class SymfonyTestsListenerTrait
|
||||
$this->expectedDeprecations = $this->gatheredDeprecations = array();
|
||||
$this->previousErrorHandler = null;
|
||||
}
|
||||
if (-2 < $this->state && ($test instanceof \PHPUnit_Framework_TestCase || $test instanceof TestCase)) {
|
||||
if (!$this->runsInSeparateProcess && -2 < $this->state && ($test instanceof \PHPUnit_Framework_TestCase || $test instanceof TestCase)) {
|
||||
if (in_array('time-sensitive', $groups, true)) {
|
||||
ClockMock::withClockMock(false);
|
||||
}
|
||||
@ -290,4 +304,21 @@ class SymfonyTestsListenerTrait
|
||||
}
|
||||
$this->gatheredDeprecations[] = $msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Test $test
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function willBeIsolated($test)
|
||||
{
|
||||
if ($test->isInIsolation()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$r = new \ReflectionProperty($test, 'runTestInSeparateProcess');
|
||||
$r->setAccessible(true);
|
||||
|
||||
return $r->getValue($test);
|
||||
}
|
||||
}
|
||||
|
23
src/Symfony/Bridge/PhpUnit/Tests/ProcessIsolationTest.php
Normal file
23
src/Symfony/Bridge/PhpUnit/Tests/ProcessIsolationTest.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Bridge\PhpUnit\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Don't remove this test case, it tests the legacy group.
|
||||
*
|
||||
* @group legacy
|
||||
*
|
||||
* @runTestsInSeparateProcesses
|
||||
*/
|
||||
class ProcessIsolationTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @expectedDeprecation Test abc
|
||||
*/
|
||||
public function testIsolation()
|
||||
{
|
||||
@trigger_error('Test abc', E_USER_DEPRECATED);
|
||||
}
|
||||
}
|
@ -14,6 +14,10 @@ use Symfony\Bridge\PhpUnit\DeprecationErrorHandler;
|
||||
|
||||
// Detect if we're loaded by an actual run of phpunit
|
||||
if (!defined('PHPUNIT_COMPOSER_INSTALL') && !class_exists('PHPUnit_TextUI_Command', false) && !class_exists('PHPUnit\TextUI\Command', false)) {
|
||||
if ($ser = getenv('SYMFONY_DEPRECATIONS_SERIALIZE')) {
|
||||
DeprecationErrorHandler::collectDeprecations($ser);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -95,7 +95,7 @@ class TranslationExtension extends AbstractExtension
|
||||
public function trans($message, array $arguments = array(), $domain = null, $locale = null)
|
||||
{
|
||||
if (null === $this->translator) {
|
||||
return $message;
|
||||
return strtr($message, $arguments);
|
||||
}
|
||||
|
||||
return $this->translator->trans($message, $arguments, $domain, $locale);
|
||||
@ -104,7 +104,7 @@ class TranslationExtension extends AbstractExtension
|
||||
public function transchoice($message, $count, array $arguments = array(), $domain = null, $locale = null)
|
||||
{
|
||||
if (null === $this->translator) {
|
||||
return $message;
|
||||
return strtr($message, $arguments);
|
||||
}
|
||||
|
||||
return $this->translator->transChoice($message, $count, array_merge(array('%count%' => $count), $arguments), $domain, $locale);
|
||||
|
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Twig\Tests\Extension;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bridge\Twig\Extension\FormExtension;
|
||||
use Symfony\Bridge\Twig\Form\TwigRendererInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Form\FormRendererInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* @group legacy
|
||||
*/
|
||||
class FormExtensionTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider rendererDataProvider
|
||||
*/
|
||||
public function testInitRuntimeAndAccessRenderer($rendererConstructor, $expectedAccessedRenderer)
|
||||
{
|
||||
$extension = new FormExtension($rendererConstructor);
|
||||
$extension->initRuntime($this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock());
|
||||
$this->assertSame($expectedAccessedRenderer, $extension->renderer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider rendererDataProvider
|
||||
*/
|
||||
public function testAccessRendererAndInitRuntime($rendererConstructor, $expectedAccessedRenderer)
|
||||
{
|
||||
$extension = new FormExtension($rendererConstructor);
|
||||
$this->assertSame($expectedAccessedRenderer, $extension->renderer);
|
||||
$extension->initRuntime($this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock());
|
||||
}
|
||||
|
||||
public function rendererDataProvider()
|
||||
{
|
||||
$twigRenderer = $this->getMockBuilder(TwigRendererInterface::class)->getMock();
|
||||
$twigRenderer->expects($this->once())
|
||||
->method('setEnvironment');
|
||||
|
||||
yield array($twigRenderer, $twigRenderer);
|
||||
|
||||
$twigRenderer = $this->getMockBuilder(TwigRendererInterface::class)->getMock();
|
||||
$twigRenderer->expects($this->once())
|
||||
->method('setEnvironment');
|
||||
|
||||
$container = $this->getMockBuilder(ContainerInterface::class)->getMock();
|
||||
$container->expects($this->once())
|
||||
->method('get')
|
||||
->with('service_id')
|
||||
->willReturn($twigRenderer);
|
||||
|
||||
yield array(array($container, 'service_id'), $twigRenderer);
|
||||
|
||||
$formRenderer = $this->getMockBuilder(FormRendererInterface::class)->getMock();
|
||||
|
||||
$container = $this->getMockBuilder(ContainerInterface::class)->getMock();
|
||||
$container->expects($this->once())
|
||||
->method('get')
|
||||
->with('service_id')
|
||||
->willReturn($formRenderer);
|
||||
|
||||
yield array(array($container, 'service_id'), $formRenderer);
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@
|
||||
"require-dev": {
|
||||
"fig/link-util": "^1.0",
|
||||
"symfony/asset": "~3.4|~4.0",
|
||||
"symfony/dependency-injection": "~3.4|~4.0",
|
||||
"symfony/finder": "~3.4|~4.0",
|
||||
"symfony/form": "~3.4|~4.0",
|
||||
"symfony/http-foundation": "~3.4|~4.0",
|
||||
|
@ -346,9 +346,23 @@ abstract class Client
|
||||
*/
|
||||
protected function doRequestInProcess($request)
|
||||
{
|
||||
$deprecationsFile = tempnam(sys_get_temp_dir(), 'deprec');
|
||||
putenv('SYMFONY_DEPRECATIONS_SERIALIZE='.$deprecationsFile);
|
||||
$process = new PhpProcess($this->getScript($request), null, null);
|
||||
$process->run();
|
||||
|
||||
if (file_exists($deprecationsFile)) {
|
||||
$deprecations = file_get_contents($deprecationsFile);
|
||||
unlink($deprecationsFile);
|
||||
foreach ($deprecations ? unserialize($deprecations) : array() as $deprecation) {
|
||||
if ($deprecation[0]) {
|
||||
trigger_error($deprecation[1], E_USER_DEPRECATED);
|
||||
} else {
|
||||
@trigger_error($deprecation[1], E_USER_DEPRECATED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$process->isSuccessful() || !preg_match('/^O\:\d+\:/', $process->getOutput())) {
|
||||
throw new \RuntimeException(sprintf('OUTPUT: %s ERROR OUTPUT: %s', $process->getOutput(), $process->getErrorOutput()));
|
||||
}
|
||||
|
@ -1832,7 +1832,13 @@ EOF;
|
||||
|
||||
private function doExport($value)
|
||||
{
|
||||
$export = var_export($value, true);
|
||||
if (is_string($value) && false !== strpos($value, "\n")) {
|
||||
$cleanParts = explode("\n", $value);
|
||||
$cleanParts = array_map(function ($part) { return var_export($part, true); }, $cleanParts);
|
||||
$export = implode('."\n".', $cleanParts);
|
||||
} else {
|
||||
$export = var_export($value, true);
|
||||
}
|
||||
|
||||
if ("'" === $export[0] && $export !== $resolvedExport = $this->container->resolveEnvPlaceholders($export, "'.\$this->getEnv('string:%s').'")) {
|
||||
$export = $resolvedExport;
|
||||
|
@ -71,6 +71,7 @@ class PhpDumperTest extends TestCase
|
||||
'optimize concatenation with empty string' => 'string1%empty_value%string2',
|
||||
'optimize concatenation from the start' => '%empty_value%start',
|
||||
'optimize concatenation at the end' => 'end%empty_value%',
|
||||
'new line' => "string with \nnew line",
|
||||
));
|
||||
$definition->setPublic(true);
|
||||
|
||||
|
@ -63,7 +63,7 @@ class ProjectServiceContainer extends Container
|
||||
*/
|
||||
protected function getTestService()
|
||||
{
|
||||
return $this->services['test'] = new \stdClass(array('only dot' => '.', 'concatenation as value' => '.\'\'.', 'concatenation from the start value' => '\'\'.', '.' => 'dot as a key', '.\'\'.' => 'concatenation as a key', '\'\'.' => 'concatenation from the start key', 'optimize concatenation' => 'string1-string2', 'optimize concatenation with empty string' => 'string1string2', 'optimize concatenation from the start' => 'start', 'optimize concatenation at the end' => 'end'));
|
||||
return $this->services['test'] = new \stdClass(array('only dot' => '.', 'concatenation as value' => '.\'\'.', 'concatenation from the start value' => '\'\'.', '.' => 'dot as a key', '.\'\'.' => 'concatenation as a key', '\'\'.' => 'concatenation from the start key', 'optimize concatenation' => 'string1-string2', 'optimize concatenation with empty string' => 'string1string2', 'optimize concatenation from the start' => 'start', 'optimize concatenation at the end' => 'end', 'new line' => 'string with '."\n".'new line'));
|
||||
}
|
||||
|
||||
public function getParameter($name)
|
||||
|
@ -145,12 +145,12 @@ class ResizeFormListener implements EventSubscriberInterface
|
||||
throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)');
|
||||
}
|
||||
|
||||
if ($entryFilter = $this->deleteEmpty) {
|
||||
if ($this->deleteEmpty) {
|
||||
$previousData = $form->getData();
|
||||
/** @var FormInterface $child */
|
||||
foreach ($form as $name => $child) {
|
||||
$isNew = !isset($previousData[$name]);
|
||||
$isEmpty = is_callable($entryFilter) ? $entryFilter($child->getData()) : $child->isEmpty();
|
||||
$isEmpty = is_callable($this->deleteEmpty) ? call_user_func($this->deleteEmpty, $child->getData()) : $child->isEmpty();
|
||||
|
||||
// $isNew can only be true if allowAdd is true, so we don't
|
||||
// need to check allowAdd again
|
||||
|
@ -275,4 +275,51 @@ class ResizeFormListenerTest extends TestCase
|
||||
$this->assertArrayNotHasKey(0, $event->getData());
|
||||
$this->assertArrayNotHasKey(2, $event->getData());
|
||||
}
|
||||
|
||||
public function testOnSubmitDeleteEmptyNotCompoundEntriesIfAllowDelete()
|
||||
{
|
||||
$this->form->setData(array('0' => 'first', '1' => 'second'));
|
||||
$this->form->add($this->getForm('0'));
|
||||
$this->form->add($this->getForm('1'));
|
||||
|
||||
$data = array(0 => 'first', 1 => '');
|
||||
foreach ($data as $child => $dat) {
|
||||
$this->form->get($child)->setData($dat);
|
||||
}
|
||||
$event = new FormEvent($this->form, $data);
|
||||
$listener = new ResizeFormListener('text', array(), false, true, true);
|
||||
$listener->onSubmit($event);
|
||||
|
||||
$this->assertEquals(array(0 => 'first'), $event->getData());
|
||||
}
|
||||
|
||||
public function testOnSubmitDeleteEmptyCompoundEntriesIfAllowDelete()
|
||||
{
|
||||
$this->form->setData(array('0' => array('name' => 'John'), '1' => array('name' => 'Jane')));
|
||||
$form1 = $this->getBuilder('0')
|
||||
->setCompound(true)
|
||||
->setDataMapper($this->getDataMapper())
|
||||
->getForm();
|
||||
$form1->add($this->getForm('name'));
|
||||
$form2 = $this->getBuilder('1')
|
||||
->setCompound(true)
|
||||
->setDataMapper($this->getDataMapper())
|
||||
->getForm();
|
||||
$form2->add($this->getForm('name'));
|
||||
$this->form->add($form1);
|
||||
$this->form->add($form2);
|
||||
|
||||
$data = array('0' => array('name' => 'John'), '1' => array('name' => ''));
|
||||
foreach ($data as $child => $dat) {
|
||||
$this->form->get($child)->setData($dat);
|
||||
}
|
||||
$event = new FormEvent($this->form, $data);
|
||||
$callback = function ($data) {
|
||||
return '' === $data['name'];
|
||||
};
|
||||
$listener = new ResizeFormListener('text', array(), false, true, $callback);
|
||||
$listener->onSubmit($event);
|
||||
|
||||
$this->assertEquals(array('0' => array('name' => 'John')), $event->getData());
|
||||
}
|
||||
}
|
||||
|
@ -87,6 +87,10 @@ class IpUtils
|
||||
$netmask = 32;
|
||||
}
|
||||
|
||||
if (false === ip2long($address)) {
|
||||
return self::$checkedIps[$cacheKey] = false;
|
||||
}
|
||||
|
||||
return self::$checkedIps[$cacheKey] = 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask);
|
||||
}
|
||||
|
||||
|
@ -82,4 +82,21 @@ class IpUtilsTest extends TestCase
|
||||
|
||||
IpUtils::checkIp('2a01:198:603:0:396e:4789:8e99:890f', '2a01:198:603:0::/65');
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider invalidIpAddressData
|
||||
*/
|
||||
public function testInvalidIpAddressesDoNotMatch($requestIp, $proxyIp)
|
||||
{
|
||||
$this->assertFalse(IpUtils::checkIp4($requestIp, $proxyIp));
|
||||
}
|
||||
|
||||
public function invalidIpAddressData()
|
||||
{
|
||||
return array(
|
||||
'invalid proxy wildcard' => array('192.168.20.13', '*'),
|
||||
'invalid proxy missing netmask' => array('192.168.20.13', '0.0.0.0'),
|
||||
'invalid request IP with invalid proxy wildcard' => array('0.0.0.0', '*'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ CHANGELOG
|
||||
-----
|
||||
|
||||
* added `OptionsResolverIntrospector` to inspect options definitions inside an `OptionsResolver` instance
|
||||
* added array of types support in allowed types (e.g int[])
|
||||
|
||||
2.6.0
|
||||
-----
|
||||
|
@ -792,21 +792,12 @@ class OptionsResolver implements Options
|
||||
// Validate the type of the resolved option
|
||||
if (isset($this->allowedTypes[$option])) {
|
||||
$valid = false;
|
||||
$invalidTypes = array();
|
||||
|
||||
foreach ($this->allowedTypes[$option] as $type) {
|
||||
$type = isset(self::$typeAliases[$type]) ? self::$typeAliases[$type] : $type;
|
||||
|
||||
if (function_exists($isFunction = 'is_'.$type)) {
|
||||
if ($isFunction($value)) {
|
||||
$valid = true;
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($value instanceof $type) {
|
||||
$valid = true;
|
||||
if ($valid = $this->verifyTypes($type, $value, $invalidTypes)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -818,7 +809,7 @@ class OptionsResolver implements Options
|
||||
$option,
|
||||
$this->formatValue($value),
|
||||
implode('" or "', $this->allowedTypes[$option]),
|
||||
$this->formatTypeOf($value)
|
||||
implode('|', array_keys($invalidTypes))
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -895,6 +886,45 @@ class OptionsResolver implements Options
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param mixed $value
|
||||
* @param array &$invalidTypes
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function verifyTypes($type, $value, array &$invalidTypes)
|
||||
{
|
||||
if ('[]' === substr($type, -2) && is_array($value)) {
|
||||
$originalType = $type;
|
||||
$type = substr($type, 0, -2);
|
||||
$invalidValues = array_filter( // Filter out valid values, keeping invalid values in the resulting array
|
||||
$value,
|
||||
function ($value) use ($type) {
|
||||
return (function_exists($isFunction = 'is_'.$type) && !$isFunction($value)) || !$value instanceof $type;
|
||||
}
|
||||
);
|
||||
|
||||
if (!$invalidValues) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$invalidTypes[$this->formatTypeOf($value, $originalType)] = true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((function_exists($isFunction = 'is_'.$type) && $isFunction($value)) || $value instanceof $type) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$invalidTypes) {
|
||||
$invalidTypes[$this->formatTypeOf($value, null)] = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a resolved option with the given name exists.
|
||||
*
|
||||
@ -963,13 +993,38 @@ class OptionsResolver implements Options
|
||||
* parameters should usually not be included in messages aimed at
|
||||
* non-technical people.
|
||||
*
|
||||
* @param mixed $value The value to return the type of
|
||||
* @param mixed $value The value to return the type of
|
||||
* @param string $type
|
||||
*
|
||||
* @return string The type of the value
|
||||
*/
|
||||
private function formatTypeOf($value)
|
||||
private function formatTypeOf($value, $type)
|
||||
{
|
||||
return is_object($value) ? get_class($value) : gettype($value);
|
||||
$suffix = '';
|
||||
|
||||
if ('[]' === substr($type, -2)) {
|
||||
$suffix = '[]';
|
||||
$type = substr($type, 0, -2);
|
||||
while ('[]' === substr($type, -2)) {
|
||||
$type = substr($type, 0, -2);
|
||||
$value = array_shift($value);
|
||||
if (!is_array($value)) {
|
||||
break;
|
||||
}
|
||||
$suffix .= '[]';
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$subTypes = array();
|
||||
foreach ($value as $val) {
|
||||
$subTypes[$this->formatTypeOf($val, null)] = true;
|
||||
}
|
||||
|
||||
return implode('|', array_keys($subTypes)).$suffix;
|
||||
}
|
||||
}
|
||||
|
||||
return (is_object($value) ? get_class($value) : gettype($value)).$suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -500,6 +500,65 @@ class OptionsResolverTest extends TestCase
|
||||
$this->resolver->resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
|
||||
* @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[]", but is of type "DateTime[]".
|
||||
*/
|
||||
public function testResolveFailsIfInvalidTypedArray()
|
||||
{
|
||||
$this->resolver->setDefined('foo');
|
||||
$this->resolver->setAllowedTypes('foo', 'int[]');
|
||||
|
||||
$this->resolver->resolve(array('foo' => array(new \DateTime())));
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
|
||||
* @expectedExceptionMessage The option "foo" with value "bar" is expected to be of type "int[]", but is of type "string".
|
||||
*/
|
||||
public function testResolveFailsWithNonArray()
|
||||
{
|
||||
$this->resolver->setDefined('foo');
|
||||
$this->resolver->setAllowedTypes('foo', 'int[]');
|
||||
|
||||
$this->resolver->resolve(array('foo' => 'bar'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
|
||||
* @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[]", but is of type "integer|stdClass|array|DateTime[]".
|
||||
*/
|
||||
public function testResolveFailsIfTypedArrayContainsInvalidTypes()
|
||||
{
|
||||
$this->resolver->setDefined('foo');
|
||||
$this->resolver->setAllowedTypes('foo', 'int[]');
|
||||
$values = range(1, 5);
|
||||
$values[] = new \stdClass();
|
||||
$values[] = array();
|
||||
$values[] = new \DateTime();
|
||||
$values[] = 123;
|
||||
|
||||
$this->resolver->resolve(array('foo' => $values));
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
|
||||
* @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[][]", but is of type "double[][]".
|
||||
*/
|
||||
public function testResolveFailsWithCorrectLevelsButWrongScalar()
|
||||
{
|
||||
$this->resolver->setDefined('foo');
|
||||
$this->resolver->setAllowedTypes('foo', 'int[][]');
|
||||
|
||||
$this->resolver->resolve(
|
||||
array(
|
||||
'foo' => array(
|
||||
array(1.2),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideInvalidTypes
|
||||
*/
|
||||
@ -568,6 +627,32 @@ class OptionsResolverTest extends TestCase
|
||||
$this->assertNotEmpty($this->resolver->resolve());
|
||||
}
|
||||
|
||||
public function testResolveSucceedsIfTypedArray()
|
||||
{
|
||||
$this->resolver->setDefault('foo', null);
|
||||
$this->resolver->setAllowedTypes('foo', array('null', 'DateTime[]'));
|
||||
|
||||
$data = array(
|
||||
'foo' => array(
|
||||
new \DateTime(),
|
||||
new \DateTime(),
|
||||
),
|
||||
);
|
||||
$result = $this->resolver->resolve($data);
|
||||
$this->assertEquals($data, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
|
||||
*/
|
||||
public function testResolveFailsIfNotInstanceOfClass()
|
||||
{
|
||||
$this->resolver->setDefault('foo', 'bar');
|
||||
$this->resolver->setAllowedTypes('foo', '\stdClass');
|
||||
|
||||
$this->resolver->resolve();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// addAllowedTypes()
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -79,7 +79,7 @@ class PropertyNormalizer extends AbstractObjectNormalizer
|
||||
}
|
||||
|
||||
try {
|
||||
$reflectionProperty = new \ReflectionProperty(is_string($classOrObject) ? $classOrObject : get_class($classOrObject), $attribute);
|
||||
$reflectionProperty = $this->getReflectionProperty($classOrObject, $attribute);
|
||||
if ($reflectionProperty->isStatic()) {
|
||||
return false;
|
||||
}
|
||||
@ -98,13 +98,15 @@ class PropertyNormalizer extends AbstractObjectNormalizer
|
||||
$reflectionObject = new \ReflectionObject($object);
|
||||
$attributes = array();
|
||||
|
||||
foreach ($reflectionObject->getProperties() as $property) {
|
||||
if (!$this->isAllowedAttribute($object, $property->name)) {
|
||||
continue;
|
||||
}
|
||||
do {
|
||||
foreach ($reflectionObject->getProperties() as $property) {
|
||||
if (!$this->isAllowedAttribute($reflectionObject->getName(), $property->name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attributes[] = $property->name;
|
||||
}
|
||||
$attributes[] = $property->name;
|
||||
}
|
||||
} while ($reflectionObject = $reflectionObject->getParentClass());
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
@ -115,7 +117,7 @@ class PropertyNormalizer extends AbstractObjectNormalizer
|
||||
protected function getAttributeValue($object, $attribute, $format = null, array $context = array())
|
||||
{
|
||||
try {
|
||||
$reflectionProperty = new \ReflectionProperty(get_class($object), $attribute);
|
||||
$reflectionProperty = $this->getReflectionProperty($object, $attribute);
|
||||
} catch (\ReflectionException $reflectionException) {
|
||||
return;
|
||||
}
|
||||
@ -134,7 +136,7 @@ class PropertyNormalizer extends AbstractObjectNormalizer
|
||||
protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = array())
|
||||
{
|
||||
try {
|
||||
$reflectionProperty = new \ReflectionProperty(get_class($object), $attribute);
|
||||
$reflectionProperty = $this->getReflectionProperty($object, $attribute);
|
||||
} catch (\ReflectionException $reflectionException) {
|
||||
return;
|
||||
}
|
||||
@ -150,4 +152,26 @@ class PropertyNormalizer extends AbstractObjectNormalizer
|
||||
|
||||
$reflectionProperty->setValue($object, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|object $classOrObject
|
||||
* @param string $attribute
|
||||
*
|
||||
* @return \ReflectionProperty
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
private function getReflectionProperty($classOrObject, $attribute)
|
||||
{
|
||||
$reflectionClass = new \ReflectionClass($classOrObject);
|
||||
while (true) {
|
||||
try {
|
||||
return $reflectionClass->getProperty($attribute);
|
||||
} catch (\ReflectionException $e) {
|
||||
if (!$reflectionClass = $reflectionClass->getParentClass()) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Serializer\Tests\Fixtures;
|
||||
|
||||
class GroupDummyChild extends GroupDummy
|
||||
{
|
||||
private $baz;
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBaz()
|
||||
{
|
||||
return $this->baz;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $baz
|
||||
*/
|
||||
public function setBaz($baz)
|
||||
{
|
||||
$this->baz = $baz;
|
||||
}
|
||||
}
|
@ -20,6 +20,7 @@ use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Symfony\Component\Serializer\Tests\Fixtures\GroupDummy;
|
||||
use Symfony\Component\Serializer\Tests\Fixtures\GroupDummyChild;
|
||||
use Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy;
|
||||
use Symfony\Component\Serializer\Tests\Fixtures\PropertyCircularReferenceDummy;
|
||||
use Symfony\Component\Serializer\Tests\Fixtures\PropertySiblingHolder;
|
||||
@ -65,6 +66,35 @@ class PropertyNormalizerTest extends TestCase
|
||||
$this->assertEquals('bar', $obj->getBar());
|
||||
}
|
||||
|
||||
public function testNormalizeWithParentClass()
|
||||
{
|
||||
$group = new GroupDummyChild();
|
||||
$group->setBaz('baz');
|
||||
$group->setFoo('foo');
|
||||
$group->setBar('bar');
|
||||
$group->setKevin('Kevin');
|
||||
$group->setCoopTilleuls('coop');
|
||||
$this->assertEquals(
|
||||
array('foo' => 'foo', 'bar' => 'bar', 'kevin' => 'Kevin',
|
||||
'coopTilleuls' => 'coop', 'fooBar' => null, 'symfony' => null, 'baz' => 'baz', ),
|
||||
$this->normalizer->normalize($group, 'any')
|
||||
);
|
||||
}
|
||||
|
||||
public function testDenormalizeWithParentClass()
|
||||
{
|
||||
$obj = $this->normalizer->denormalize(
|
||||
array('foo' => 'foo', 'bar' => 'bar', 'kevin' => 'Kevin', 'baz' => 'baz'),
|
||||
GroupDummyChild::class,
|
||||
'any'
|
||||
);
|
||||
$this->assertEquals('foo', $obj->getFoo());
|
||||
$this->assertEquals('bar', $obj->getBar());
|
||||
$this->assertEquals('Kevin', $obj->getKevin());
|
||||
$this->assertEquals('baz', $obj->getBaz());
|
||||
$this->assertNull($obj->getSymfony());
|
||||
}
|
||||
|
||||
public function testConstructorDenormalize()
|
||||
{
|
||||
$obj = $this->normalizer->denormalize(
|
||||
@ -147,12 +177,14 @@ class PropertyNormalizerTest extends TestCase
|
||||
'bar' => 'bar',
|
||||
), $this->normalizer->normalize($obj, null, array(PropertyNormalizer::GROUPS => array('c'))));
|
||||
|
||||
// The PropertyNormalizer is not able to hydrate properties from parent classes
|
||||
// The PropertyNormalizer is also able to hydrate properties from parent classes
|
||||
$this->assertEquals(array(
|
||||
'symfony' => 'symfony',
|
||||
'foo' => 'foo',
|
||||
'fooBar' => 'fooBar',
|
||||
'bar' => 'bar',
|
||||
'kevin' => 'kevin',
|
||||
'coopTilleuls' => 'coopTilleuls',
|
||||
), $this->normalizer->normalize($obj, null, array(PropertyNormalizer::GROUPS => array('a', 'c'))));
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ class TranslatorTest extends TestCase
|
||||
*/
|
||||
public function testConstructorInvalidLocale($locale)
|
||||
{
|
||||
$translator = new Translator($locale);
|
||||
new Translator($locale);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -88,6 +88,15 @@ class File extends Constraint
|
||||
return parent::__get($option);
|
||||
}
|
||||
|
||||
public function __isset($option)
|
||||
{
|
||||
if ('maxSize' === $option) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return parent::__isset($option);
|
||||
}
|
||||
|
||||
private function normalizeBinaryFormat($maxSize)
|
||||
{
|
||||
$factors = array(
|
||||
|
Reference in New Issue
Block a user