diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php
index 6685d34948..15cea530d5 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php
@@ -150,14 +150,12 @@ class UserPasswordEncoderCommandTest extends WebTestCase
public function testEncodePasswordEmptySaltOutput()
{
- $this->passwordEncoderCommandTester->execute(
- [
- 'command' => 'security:encode-password',
- 'password' => 'p@ssw0rd',
- 'user-class' => 'Symfony\Component\Security\Core\User\User',
- '--empty-salt' => true,
- ]
- );
+ $this->passwordEncoderCommandTester->execute([
+ 'command' => 'security:encode-password',
+ 'password' => 'p@ssw0rd',
+ 'user-class' => 'Symfony\Component\Security\Core\User\User',
+ '--empty-salt' => true,
+ ]);
$this->assertContains('Password encoding succeeded', $this->passwordEncoderCommandTester->getDisplay());
$this->assertContains(' Encoded password p@ssw0rd', $this->passwordEncoderCommandTester->getDisplay());
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig
index 56eff4c562..20b72098dc 100644
--- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig
+++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig
@@ -93,7 +93,11 @@
Execution timeline
- {% if collector.events is empty %}
+ {% if not collector.isStopwatchInstalled() %}
+
+
The Stopwatch component is not installed. If you want to see timing events, run: composer require symfony/stopwatch
.
+
+ {% elseif collector.events is empty %}
No timing events have been recorded. Check that symfony/stopwatch is installed and debugging enabled in the kernel.
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig
index 0230c84fb9..dd09415568 100644
--- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig
+++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig
@@ -5,8 +5,6 @@
background-color: #222;
border-top-left-radius: 4px;
bottom: 0;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
box-sizing: border-box;
display: none;
height: 36px;
@@ -36,8 +34,6 @@
}
.sf-toolbarreset * {
- -webkit-box-sizing: content-box;
- -moz-box-sizing: content-box;
box-sizing: content-box;
vertical-align: baseline;
letter-spacing: normal;
@@ -371,21 +367,8 @@ div.sf-toolbar .sf-toolbar-block a:hover {
text-align: right;
}
.sf-ajax-request-loading {
- -webkit-animation: sf-blink .5s ease-in-out infinite;
- -o-animation: sf-blink .5s ease-in-out infinite;
- -moz-animation: sf-blink .5s ease-in-out infinite;
animation: sf-blink .5s ease-in-out infinite;
}
-@-webkit-keyframes sf-blink {
- 0% { background: #222; }
- 50% { background: #444; }
- 100% { background: #222; }
-}
-@-moz-keyframes sf-blink {
- 0% { background: #222; }
- 50% { background: #444; }
- 100% { background: #222; }
-}
@keyframes sf-blink {
0% { background: #222; }
50% { background: #444; }
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/DateIntervalType.php b/src/Symfony/Component/Form/Extension/Core/Type/DateIntervalType.php
index 181ce74993..2c41b1c6a3 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/DateIntervalType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/DateIntervalType.php
@@ -205,38 +205,36 @@ class DateIntervalType extends AbstractType
}));
};
- $resolver->setDefaults(
- [
- 'with_years' => true,
- 'with_months' => true,
- 'with_days' => true,
- 'with_weeks' => false,
- 'with_hours' => false,
- 'with_minutes' => false,
- 'with_seconds' => false,
- 'with_invert' => false,
- 'years' => range(0, 100),
- 'months' => range(0, 12),
- 'weeks' => range(0, 52),
- 'days' => range(0, 31),
- 'hours' => range(0, 24),
- 'minutes' => range(0, 60),
- 'seconds' => range(0, 60),
- 'widget' => 'choice',
- 'input' => 'dateinterval',
- 'placeholder' => $placeholderDefault,
- 'by_reference' => true,
- 'error_bubbling' => false,
- // If initialized with a \DateInterval object, FormType initializes
- // this option to "\DateInterval". Since the internal, normalized
- // representation is not \DateInterval, but an array, we need to unset
- // this option.
- 'data_class' => null,
- 'compound' => $compound,
- 'empty_data' => $emptyData,
- 'labels' => [],
- ]
- );
+ $resolver->setDefaults([
+ 'with_years' => true,
+ 'with_months' => true,
+ 'with_days' => true,
+ 'with_weeks' => false,
+ 'with_hours' => false,
+ 'with_minutes' => false,
+ 'with_seconds' => false,
+ 'with_invert' => false,
+ 'years' => range(0, 100),
+ 'months' => range(0, 12),
+ 'weeks' => range(0, 52),
+ 'days' => range(0, 31),
+ 'hours' => range(0, 24),
+ 'minutes' => range(0, 60),
+ 'seconds' => range(0, 60),
+ 'widget' => 'choice',
+ 'input' => 'dateinterval',
+ 'placeholder' => $placeholderDefault,
+ 'by_reference' => true,
+ 'error_bubbling' => false,
+ // If initialized with a \DateInterval object, FormType initializes
+ // this option to "\DateInterval". Since the internal, normalized
+ // representation is not \DateInterval, but an array, we need to unset
+ // this option.
+ 'data_class' => null,
+ 'compound' => $compound,
+ 'empty_data' => $emptyData,
+ 'labels' => [],
+ ]);
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('labels', $labelsNormalizer);
diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php
index 5e684687ec..c57a466ec5 100644
--- a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php
+++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php
@@ -149,12 +149,10 @@ class DefaultChoiceListFactoryTest extends TestCase
public function testCreateFromChoicesGrouped()
{
- $list = $this->factory->createListFromChoices(
- [
- 'Group 1' => ['A' => $this->obj1, 'B' => $this->obj2],
- 'Group 2' => ['C' => $this->obj3, 'D' => $this->obj4],
- ]
- );
+ $list = $this->factory->createListFromChoices([
+ 'Group 1' => ['A' => $this->obj1, 'B' => $this->obj2],
+ 'Group 2' => ['C' => $this->obj3, 'D' => $this->obj4],
+ ]);
$this->assertObjectListWithGeneratedValues($list);
}
diff --git a/src/Symfony/Component/HttpKernel/DataCollector/TimeDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/TimeDataCollector.php
index 99149ab0be..f48db70568 100644
--- a/src/Symfony/Component/HttpKernel/DataCollector/TimeDataCollector.php
+++ b/src/Symfony/Component/HttpKernel/DataCollector/TimeDataCollector.php
@@ -47,6 +47,7 @@ class TimeDataCollector extends DataCollector implements LateDataCollectorInterf
'token' => $response->headers->get('X-Debug-Token'),
'start_time' => $startTime * 1000,
'events' => [],
+ 'stopwatch_installed' => \class_exists(Stopwatch::class, false),
];
}
@@ -139,6 +140,14 @@ class TimeDataCollector extends DataCollector implements LateDataCollectorInterf
return $this->data['start_time'];
}
+ /**
+ * @return bool whether or not the stopwatch component is installed
+ */
+ public function isStopwatchInstalled()
+ {
+ return $this->data['stopwatch_installed'];
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/TimeDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/TimeDataCollectorTest.php
index cf6a86695d..793fbd319f 100644
--- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/TimeDataCollectorTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/TimeDataCollectorTest.php
@@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\TimeDataCollector;
+use Symfony\Component\Stopwatch\Stopwatch;
/**
* @group time-sensitive
@@ -51,5 +52,6 @@ class TimeDataCollectorTest extends TestCase
$c->collect($request, new Response());
$this->assertEquals(123456000, $c->getStartTime());
+ $this->assertSame(\class_exists(Stopwatch::class, false), $c->isStopwatchInstalled());
}
}
diff --git a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php
index edf53cd2e2..078554e137 100644
--- a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php
+++ b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php
@@ -851,13 +851,11 @@ class OptionsResolverTest extends TestCase
$this->resolver->setDefined('foo');
$this->resolver->setAllowedTypes('foo', 'int[][]');
- $this->resolver->resolve(
- [
- 'foo' => [
- [1.2],
- ],
- ]
- );
+ $this->resolver->resolve([
+ 'foo' => [
+ [1.2],
+ ],
+ ]);
}
/**
@@ -1975,13 +1973,11 @@ class OptionsResolverTest extends TestCase
1, 2,
],
],
- ], $this->resolver->resolve(
- [
- 'foo' => [
- [1, 2],
- ],
- ]
- ));
+ ], $this->resolver->resolve([
+ 'foo' => [
+ [1, 2],
+ ],
+ ]));
}
public function testNested2Arrays()
@@ -2021,17 +2017,15 @@ class OptionsResolverTest extends TestCase
$this->resolver->setDefined('foo');
$this->resolver->setAllowedTypes('foo', 'float[][][][]');
- $this->resolver->resolve(
- [
- 'foo' => [
+ $this->resolver->resolve([
+ 'foo' => [
+ [
[
- [
- [1, 2],
- ],
+ [1, 2],
],
],
- ]
- );
+ ],
+ ]);
}
/**
diff --git a/src/Symfony/Component/Process/ExecutableFinder.php b/src/Symfony/Component/Process/ExecutableFinder.php
index a621fc6918..cb4345e7bb 100644
--- a/src/Symfony/Component/Process/ExecutableFinder.php
+++ b/src/Symfony/Component/Process/ExecutableFinder.php
@@ -51,7 +51,7 @@ class ExecutableFinder
public function find($name, $default = null, array $extraDirs = [])
{
if (ini_get('open_basedir')) {
- $searchPath = explode(PATH_SEPARATOR, ini_get('open_basedir'));
+ $searchPath = array_merge(explode(PATH_SEPARATOR, ini_get('open_basedir')), $extraDirs);
$dirs = [];
foreach ($searchPath as $path) {
// Silencing against https://bugs.php.net/69240
diff --git a/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php b/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php
index 76a5a9107b..d7dfac1101 100644
--- a/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php
@@ -133,8 +133,6 @@ class ExceptionListener
} catch (\Exception $e) {
$event->setException($e);
}
-
- return;
}
if (null !== $this->logger) {
@@ -152,7 +150,7 @@ class ExceptionListener
$subRequest = $this->httpUtils->createRequest($event->getRequest(), $this->errorPage);
$subRequest->attributes->set(Security::ACCESS_DENIED_ERROR, $exception);
- $event->setResponse($event->getKernel()->handle($subRequest, HttpKernelInterface::SUB_REQUEST, true));
+ $event->setResponse($event->getKernel()->handle($subRequest, HttpKernelInterface::SUB_REQUEST));
$event->allowCustomResponseCode();
}
} catch (\Exception $e) {
diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/ExceptionListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/ExceptionListenerTest.php
index ecbf614a69..910e9c4d2f 100644
--- a/src/Symfony/Component/Security/Http/Tests/Firewall/ExceptionListenerTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/Firewall/ExceptionListenerTest.php
@@ -131,10 +131,8 @@ class ExceptionListenerTest extends TestCase
{
$event = $this->createEvent($exception);
- $accessDeniedHandler = $this->getMockBuilder('Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface')->getMock();
- $accessDeniedHandler->expects($this->once())->method('handle')->will($this->returnValue(new Response('error')));
+ $listener = $this->createExceptionListener(null, $this->createTrustResolver(true), null, null, null, $this->createCustomAccessDeniedHandler(new Response('error')));
- $listener = $this->createExceptionListener(null, $this->createTrustResolver(true), null, null, null, $accessDeniedHandler);
$listener->onKernelException($event);
$this->assertEquals('error', $event->getResponse()->getContent());
@@ -148,16 +146,51 @@ class ExceptionListenerTest extends TestCase
{
$event = $this->createEvent($exception);
- $tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')->getMock();
- $tokenStorage->expects($this->once())->method('getToken')->will($this->returnValue($this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock()));
-
- $listener = $this->createExceptionListener($tokenStorage, $this->createTrustResolver(false), null, $this->createEntryPoint());
+ $listener = $this->createExceptionListener($this->createTokenStorage(), $this->createTrustResolver(false), null, $this->createEntryPoint());
$listener->onKernelException($event);
$this->assertEquals('OK', $event->getResponse()->getContent());
$this->assertSame(null === $eventException ? $exception : $eventException, $event->getException()->getPrevious());
}
+ /**
+ * @dataProvider getAccessDeniedExceptionProvider
+ */
+ public function testAccessDeniedExceptionNotFullFledgedAndWithAccessDeniedHandlerAndWithoutErrorPage(\Exception $exception, \Exception $eventException = null)
+ {
+ $event = $this->createEvent($exception);
+
+ $listener = $this->createExceptionListener($this->createTokenStorage(), $this->createTrustResolver(false), null, $this->createEntryPoint(), null, $this->createCustomAccessDeniedHandler(new Response('denied', 403)));
+ $listener->onKernelException($event);
+
+ $this->assertEquals('denied', $event->getResponse()->getContent());
+ $this->assertEquals(403, $event->getResponse()->getStatusCode());
+ $this->assertSame(null === $eventException ? $exception : $eventException, $event->getException()->getPrevious());
+ }
+
+ /**
+ * @dataProvider getAccessDeniedExceptionProvider
+ */
+ public function testAccessDeniedExceptionNotFullFledgedAndWithoutAccessDeniedHandlerAndWithErrorPage(\Exception $exception, \Exception $eventException = null)
+ {
+ $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock();
+ $kernel->expects($this->once())->method('handle')->will($this->returnValue(new Response('Unauthorized', 401)));
+
+ $event = $this->createEvent($exception, $kernel);
+
+ $httpUtils = $this->getMockBuilder('Symfony\Component\Security\Http\HttpUtils')->getMock();
+ $httpUtils->expects($this->once())->method('createRequest')->will($this->returnValue(Request::create('/error')));
+
+ $listener = $this->createExceptionListener($this->createTokenStorage(), $this->createTrustResolver(true), $httpUtils, null, '/error');
+ $listener->onKernelException($event);
+
+ $this->assertTrue($event->isAllowingCustomResponseCode());
+
+ $this->assertEquals('Unauthorized', $event->getResponse()->getContent());
+ $this->assertEquals(401, $event->getResponse()->getStatusCode());
+ $this->assertSame(null === $eventException ? $exception : $eventException, $event->getException()->getPrevious());
+ }
+
public function getAccessDeniedExceptionProvider()
{
return [
@@ -169,6 +202,22 @@ class ExceptionListenerTest extends TestCase
];
}
+ private function createTokenStorage()
+ {
+ $tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')->getMock();
+ $tokenStorage->expects($this->once())->method('getToken')->will($this->returnValue($this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock()));
+
+ return $tokenStorage;
+ }
+
+ private function createCustomAccessDeniedHandler(Response $response)
+ {
+ $accessDeniedHandler = $this->getMockBuilder('Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface')->getMock();
+ $accessDeniedHandler->expects($this->once())->method('handle')->will($this->returnValue($response));
+
+ return $accessDeniedHandler;
+ }
+
private function createEntryPoint(Response $response = null)
{
$entryPoint = $this->getMockBuilder('Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface')->getMock();
diff --git a/src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php b/src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php
index a6934f54b8..e863e013e7 100644
--- a/src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php
+++ b/src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php
@@ -69,7 +69,7 @@ final class MetadataAwareNameConverter implements AdvancedNameConverterInterface
return self::$denormalizeCache[$class][$propertyName] ?? $this->denormalizeFallback($propertyName, $class, $format, $context);
}
- private function getCacheValueForNormalization(string $propertyName, string $class): ?string
+ private function getCacheValueForNormalization($propertyName, string $class)
{
if (!$this->metadataFactory->hasMetadataFor($class)) {
return null;
@@ -83,12 +83,12 @@ final class MetadataAwareNameConverter implements AdvancedNameConverterInterface
return $attributesMetadata[$propertyName]->getSerializedName() ?? null;
}
- private function normalizeFallback(string $propertyName, string $class = null, string $format = null, array $context = []): string
+ private function normalizeFallback($propertyName, string $class = null, string $format = null, array $context = [])
{
return $this->fallbackNameConverter ? $this->fallbackNameConverter->normalize($propertyName, $class, $format, $context) : $propertyName;
}
- private function getCacheValueForDenormalization(string $propertyName, string $class): ?string
+ private function getCacheValueForDenormalization($propertyName, string $class)
{
if (!isset(self::$attributesMetadataCache[$class])) {
self::$attributesMetadataCache[$class] = $this->getCacheValueForAttributesMetadata($class);
@@ -97,7 +97,7 @@ final class MetadataAwareNameConverter implements AdvancedNameConverterInterface
return self::$attributesMetadataCache[$class][$propertyName] ?? null;
}
- private function denormalizeFallback(string $propertyName, string $class = null, string $format = null, array $context = []): string
+ private function denormalizeFallback($propertyName, string $class = null, string $format = null, array $context = [])
{
return $this->fallbackNameConverter ? $this->fallbackNameConverter->denormalize($propertyName, $class, $format, $context) : $propertyName;
}
diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php
index 88732bbd3e..f766286b2b 100644
--- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php
+++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php
@@ -43,7 +43,10 @@ class ObjectNormalizer extends AbstractObjectNormalizer
parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor, $classDiscriminatorResolver, $objectClassResolver, $defaultContext);
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
- $this->objectClassResolver = $objectClassResolver;
+
+ $this->objectClassResolver = $objectClassResolver ?? function ($class) {
+ return \is_object($class) ? \get_class($class) : $class;
+ };
}
/**
@@ -63,7 +66,7 @@ class ObjectNormalizer extends AbstractObjectNormalizer
$attributes = [];
// methods
- $class = $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
+ $class = ($this->objectClassResolver)($object);
$reflClass = new \ReflectionClass($class);
foreach ($reflClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflMethod) {
diff --git a/src/Symfony/Component/Serializer/Tests/NameConverter/MetadataAwareNameConverterTest.php b/src/Symfony/Component/Serializer/Tests/NameConverter/MetadataAwareNameConverterTest.php
index f4c5c5b305..7e2552ea8a 100644
--- a/src/Symfony/Component/Serializer/Tests/NameConverter/MetadataAwareNameConverterTest.php
+++ b/src/Symfony/Component/Serializer/Tests/NameConverter/MetadataAwareNameConverterTest.php
@@ -102,6 +102,7 @@ final class MetadataAwareNameConverterTest extends TestCase
['foo', 'baz'],
['bar', 'qux'],
['quux', 'quux'],
+ [0, 0],
];
}
@@ -111,6 +112,7 @@ final class MetadataAwareNameConverterTest extends TestCase
['foo', 'baz'],
['bar', 'qux'],
['quux', 'QUUX'],
+ [0, 0],
];
}
}
diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php
index b85ec8b5e2..4c4448dfe4 100644
--- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php
+++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php
@@ -1043,6 +1043,30 @@ class ObjectNormalizerTest extends TestCase
$this->assertArrayHasKey('foo-Symfony\Component\Serializer\Tests\Normalizer\ObjectDummy-json-bar', $normalizer->normalize(new ObjectDummy(), 'json', ['foo' => 'bar']));
}
+ public function testDefaultObjectClassResolver()
+ {
+ $normalizer = new ObjectNormalizer();
+
+ $obj = new ObjectDummy();
+ $obj->setFoo('foo');
+ $obj->bar = 'bar';
+ $obj->setBaz(true);
+ $obj->setCamelCase('camelcase');
+ $obj->unwantedProperty = 'notwanted';
+
+ $this->assertEquals(
+ [
+ 'foo' => 'foo',
+ 'bar' => 'bar',
+ 'baz' => true,
+ 'fooBar' => 'foobar',
+ 'camelCase' => 'camelcase',
+ 'object' => null,
+ ],
+ $normalizer->normalize($obj, 'any')
+ );
+ }
+
public function testObjectClassResolver()
{
$classResolver = function ($object) {
diff --git a/src/Symfony/Component/Translation/Tests/Dumper/YamlFileDumperTest.php b/src/Symfony/Component/Translation/Tests/Dumper/YamlFileDumperTest.php
index 24bc65ba24..e46da5a7e8 100644
--- a/src/Symfony/Component/Translation/Tests/Dumper/YamlFileDumperTest.php
+++ b/src/Symfony/Component/Translation/Tests/Dumper/YamlFileDumperTest.php
@@ -20,11 +20,10 @@ class YamlFileDumperTest extends TestCase
public function testTreeFormatCatalogue()
{
$catalogue = new MessageCatalogue('en');
- $catalogue->add(
- [
- 'foo.bar1' => 'value1',
- 'foo.bar2' => 'value2',
- ]);
+ $catalogue->add([
+ 'foo.bar1' => 'value1',
+ 'foo.bar2' => 'value2',
+ ]);
$dumper = new YamlFileDumper();
@@ -34,11 +33,10 @@ class YamlFileDumperTest extends TestCase
public function testLinearFormatCatalogue()
{
$catalogue = new MessageCatalogue('en');
- $catalogue->add(
- [
- 'foo.bar1' => 'value1',
- 'foo.bar2' => 'value2',
- ]);
+ $catalogue->add([
+ 'foo.bar1' => 'value1',
+ 'foo.bar2' => 'value2',
+ ]);
$dumper = new YamlFileDumper();
diff --git a/src/Symfony/Component/Validator/Tests/Constraints/ChoiceValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/ChoiceValidatorTest.php
index 3382191cca..e55d19aa82 100644
--- a/src/Symfony/Component/Validator/Tests/Constraints/ChoiceValidatorTest.php
+++ b/src/Symfony/Component/Validator/Tests/Constraints/ChoiceValidatorTest.php
@@ -54,11 +54,9 @@ class ChoiceValidatorTest extends ConstraintValidatorTestCase
{
$this->validator->validate(
null,
- new Choice(
- [
- 'choices' => ['foo', 'bar'],
- ]
- )
+ new Choice([
+ 'choices' => ['foo', 'bar'],
+ ])
);
$this->assertNoViolation();
@@ -100,13 +98,11 @@ class ChoiceValidatorTest extends ConstraintValidatorTestCase
public function testValidChoiceCallbackClosure()
{
- $constraint = new Choice(
- [
- 'callback' => function () {
- return ['foo', 'bar'];
- },
- ]
- );
+ $constraint = new Choice([
+ 'callback' => function () {
+ return ['foo', 'bar'];
+ },
+ ]);
$this->validator->validate('bar', $constraint);
diff --git a/src/Symfony/Component/VarExporter/Instantiator.php b/src/Symfony/Component/VarExporter/Instantiator.php
index 7eefc3c2d2..06abbc75a6 100644
--- a/src/Symfony/Component/VarExporter/Instantiator.php
+++ b/src/Symfony/Component/VarExporter/Instantiator.php
@@ -67,7 +67,7 @@ final class Instantiator
$wrappedInstance = [$reflector->newInstanceWithoutConstructor()];
} elseif (null === Registry::$prototypes[$class]) {
throw new NotInstantiableTypeException($class);
- } elseif ($reflector->implementsInterface('Serializable')) {
+ } elseif ($reflector->implementsInterface('Serializable') && (\PHP_VERSION_ID < 70400 || !method_exists($class, '__unserialize'))) {
$wrappedInstance = [unserialize('C:'.\strlen($class).':"'.$class.'":0:{}')];
} else {
$wrappedInstance = [unserialize('O:'.\strlen($class).':"'.$class.'":0:{}')];
diff --git a/src/Symfony/Component/VarExporter/Internal/Exporter.php b/src/Symfony/Component/VarExporter/Internal/Exporter.php
index fae9084ca0..74a324691e 100644
--- a/src/Symfony/Component/VarExporter/Internal/Exporter.php
+++ b/src/Symfony/Component/VarExporter/Internal/Exporter.php
@@ -74,10 +74,23 @@ class Exporter
}
$class = \get_class($value);
+ $reflector = Registry::$reflectors[$class] ?? Registry::getClassReflector($class);
+
+ if ($reflector->hasMethod('__serialize')) {
+ if (!$reflector->getMethod('__serialize')->isPublic()) {
+ throw new \Error(sprintf('Call to %s method %s::__serialize()', $reflector->getMethod('__serialize')->isProtected() ? 'protected' : 'private', $class));
+ }
+
+ if (!\is_array($properties = $value->__serialize())) {
+ throw new \Typerror($class.'::__serialize() must return an array');
+ }
+
+ goto prepare_value;
+ }
+
$properties = [];
$sleep = null;
$arrayValue = (array) $value;
- $reflector = Registry::$reflectors[$class] ?? Registry::getClassReflector($class);
$proto = Registry::$prototypes[$class];
if (($value instanceof \ArrayIterator || $value instanceof \ArrayObject) && null !== $proto) {
@@ -154,10 +167,11 @@ class Exporter
}
}
+ prepare_value:
$objectsPool[$value] = [$id = \count($objectsPool)];
$properties = self::prepare($properties, $objectsPool, $refsPool, $objectsCount, $valueIsStatic);
++$objectsCount;
- $objectsPool[$value] = [$id, $class, $properties, \method_exists($class, '__wakeup') ? $objectsCount : 0];
+ $objectsPool[$value] = [$id, $class, $properties, \method_exists($class, '__unserialize') ? -$objectsCount : (\method_exists($class, '__wakeup') ? $objectsCount : 0)];
$value = new Reference($id);
diff --git a/src/Symfony/Component/VarExporter/Internal/Hydrator.php b/src/Symfony/Component/VarExporter/Internal/Hydrator.php
index 07721df428..5f64adf96f 100644
--- a/src/Symfony/Component/VarExporter/Internal/Hydrator.php
+++ b/src/Symfony/Component/VarExporter/Internal/Hydrator.php
@@ -42,8 +42,12 @@ class Hydrator
foreach ($properties as $class => $vars) {
(self::$hydrators[$class] ?? self::getHydrator($class))($vars, $objects);
}
- foreach ($wakeups as $i) {
- $objects[$i]->__wakeup();
+ foreach ($wakeups as $k => $v) {
+ if (\is_array($v)) {
+ $objects[-$k]->__unserialize($v);
+ } else {
+ $objects[$v]->__wakeup();
+ }
}
return $value;
diff --git a/src/Symfony/Component/VarExporter/Internal/Registry.php b/src/Symfony/Component/VarExporter/Internal/Registry.php
index 629836b00b..b5069dd16a 100644
--- a/src/Symfony/Component/VarExporter/Internal/Registry.php
+++ b/src/Symfony/Component/VarExporter/Internal/Registry.php
@@ -65,14 +65,14 @@ class Registry
public static function getClassReflector($class, $instantiableWithoutConstructor = false, $cloneable = null)
{
- if (!\class_exists($class) && !\interface_exists($class, false) && !\trait_exists($class, false)) {
+ if (!($isClass = \class_exists($class)) && !\interface_exists($class, false) && !\trait_exists($class, false)) {
throw new ClassNotFoundException($class);
}
$reflector = new \ReflectionClass($class);
if ($instantiableWithoutConstructor) {
$proto = $reflector->newInstanceWithoutConstructor();
- } elseif (!$reflector->isInstantiable()) {
+ } elseif (!$isClass || $reflector->isAbstract()) {
throw new NotInstantiableTypeException($class);
} elseif ($reflector->name !== $class) {
$reflector = self::$reflectors[$name = $reflector->name] ?? self::getClassReflector($name, $instantiableWithoutConstructor, $cloneable);
@@ -86,14 +86,14 @@ class Registry
$proto = $reflector->newInstanceWithoutConstructor();
$instantiableWithoutConstructor = true;
} catch (\ReflectionException $e) {
- $proto = $reflector->implementsInterface('Serializable') ? 'C:' : 'O:';
+ $proto = $reflector->implementsInterface('Serializable') && (\PHP_VERSION_ID < 70400 || !\method_exists($class, '__unserialize')) ? 'C:' : 'O:';
if ('C:' === $proto && !$reflector->getMethod('unserialize')->isInternal()) {
$proto = null;
} elseif (false === $proto = @unserialize($proto.\strlen($class).':"'.$class.'":0:{}')) {
throw new NotInstantiableTypeException($class);
}
}
- if (null !== $proto && !$proto instanceof \Throwable && !$proto instanceof \Serializable && !\method_exists($class, '__sleep')) {
+ if (null !== $proto && !$proto instanceof \Throwable && !$proto instanceof \Serializable && !\method_exists($class, '__sleep') && (\PHP_VERSION_ID < 70400 || !\method_exists($class, '__serialize'))) {
try {
serialize($proto);
} catch (\Exception $e) {
@@ -103,7 +103,7 @@ class Registry
}
if (null === $cloneable) {
- if (($proto instanceof \Reflector || $proto instanceof \ReflectionGenerator || $proto instanceof \ReflectionType || $proto instanceof \IteratorIterator || $proto instanceof \RecursiveIteratorIterator) && (!$proto instanceof \Serializable && !\method_exists($proto, '__wakeup'))) {
+ if (($proto instanceof \Reflector || $proto instanceof \ReflectionGenerator || $proto instanceof \ReflectionType || $proto instanceof \IteratorIterator || $proto instanceof \RecursiveIteratorIterator) && (!$proto instanceof \Serializable && !\method_exists($proto, '__wakeup') && (\PHP_VERSION_ID < 70400 || !\method_exists($class, '__unserialize')))) {
throw new NotInstantiableTypeException($class);
}
diff --git a/src/Symfony/Component/VarExporter/README.md b/src/Symfony/Component/VarExporter/README.md
index c3a072127e..180554ed1a 100644
--- a/src/Symfony/Component/VarExporter/README.md
+++ b/src/Symfony/Component/VarExporter/README.md
@@ -3,7 +3,8 @@ VarExporter Component
The VarExporter component allows exporting any serializable PHP data structure to
plain PHP code. While doing so, it preserves all the semantics associated with
-the serialization mechanism of PHP (`__wakeup`, `__sleep`, `Serializable`).
+the serialization mechanism of PHP (`__wakeup`, `__sleep`, `Serializable`,
+`__serialize`, `__unserialize`).
It also provides an instantiator that allows creating and populating objects
without calling their constructor nor any other methods.
diff --git a/src/Symfony/Component/VarExporter/Tests/Fixtures/php74-serializable.php b/src/Symfony/Component/VarExporter/Tests/Fixtures/php74-serializable.php
new file mode 100644
index 0000000000..06cfac10f5
--- /dev/null
+++ b/src/Symfony/Component/VarExporter/Tests/Fixtures/php74-serializable.php
@@ -0,0 +1,16 @@
+ [
+ 'prop' => [
+ 'bar',
+ ],
+ ],
+ ],
+ $o[0],
+ []
+);
diff --git a/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php b/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php
index 5bb03d8ce1..6f54d76106 100644
--- a/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php
+++ b/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php
@@ -82,7 +82,7 @@ class VarExporterTest extends TestCase
$marshalledValue = VarExporter::export($value, $isStaticValue);
$this->assertSame($staticValueExpected, $isStaticValue);
- if ('var-on-sleep' !== $testName) {
+ if ('var-on-sleep' !== $testName && 'php74-serializable' !== $testName) {
$this->assertDumpEquals($dumpedValue, $value);
}
@@ -197,6 +197,10 @@ class VarExporterTest extends TestCase
yield ['abstract-parent', new ConcreteClass()];
yield ['foo-serializable', new FooSerializable('bar')];
+
+ yield ['private-constructor', PrivateConstructor::create('bar')];
+
+ yield ['php74-serializable', new Php74Serializable()];
}
}
@@ -250,6 +254,21 @@ class MyNotCloneable
}
}
+class PrivateConstructor
+{
+ public $prop;
+
+ public static function create($prop): self
+ {
+ return new self($prop);
+ }
+
+ private function __construct($prop)
+ {
+ $this->prop = $prop;
+ }
+}
+
class MyPrivateValue
{
protected $prot;
@@ -370,3 +389,36 @@ class FooSerializable implements \Serializable
list($this->foo) = unserialize($str);
}
}
+
+class Php74Serializable implements \Serializable
+{
+ public function __serialize()
+ {
+ return [$this->foo = new \stdClass()];
+ }
+
+ public function __unserialize(array $data)
+ {
+ list($this->foo) = $data;
+ }
+
+ public function __sleep()
+ {
+ throw new \BadMethodCallException();
+ }
+
+ public function __wakeup()
+ {
+ throw new \BadMethodCallException();
+ }
+
+ public function serialize()
+ {
+ throw new \BadMethodCallException();
+ }
+
+ public function unserialize($ser)
+ {
+ throw new \BadMethodCallException();
+ }
+}
diff --git a/src/Symfony/Component/VarExporter/VarExporter.php b/src/Symfony/Component/VarExporter/VarExporter.php
index 45ef7446b1..da9a8d4373 100644
--- a/src/Symfony/Component/VarExporter/VarExporter.php
+++ b/src/Symfony/Component/VarExporter/VarExporter.php
@@ -69,14 +69,30 @@ final class VarExporter
$classes = [];
$values = [];
- $wakeups = [];
+ $states = [];
foreach ($objectsPool as $i => $v) {
list(, $classes[], $values[], $wakeup) = $objectsPool[$v];
- if ($wakeup) {
- $wakeups[$wakeup] = $i;
+ if (0 < $wakeup) {
+ $states[$wakeup] = $i;
+ } elseif (0 > $wakeup) {
+ $states[-$wakeup] = [$i, array_pop($values)];
+ $values[] = [];
}
}
- ksort($wakeups);
+ ksort($states);
+
+ $wakeups = [null];
+ foreach ($states as $k => $v) {
+ if (\is_array($v)) {
+ $wakeups[-$v[0]] = $v[1];
+ } else {
+ $wakeups[] = $v;
+ }
+ }
+
+ if (null === $wakeups[0]) {
+ unset($wakeups[0]);
+ }
$properties = [];
foreach ($values as $i => $vars) {