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) {