Merge branch '4.2'

* 4.2:
  fixed bad merge
  Show more accurate message in profiler when missing stopwatch
  CS Fixes: Not double split with one array argument
  [Serializer] Add default object class resolver
  Remove redundant animation prefixes
  Remove redundant `box-sizing` prefixes
  [VarExporter] support PHP7.4 __serialize & __unserialize
  Rework firewall access denied rule
  MetadataAwareNameConverter: Do not assume that property names are strings
  [VarExporter] fix exporting classes with private constructors
  fixed CS
  Fix missing $extraDirs when open_basedir returns
This commit is contained in:
Fabien Potencier 2019-04-10 21:42:49 +02:00
commit 7e2fbe13c8
26 changed files with 317 additions and 141 deletions

View File

@ -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());

View File

@ -93,7 +93,11 @@
<h2>Execution timeline</h2>
{% if collector.events is empty %}
{% if not collector.isStopwatchInstalled() %}
<div class="empty">
<p>The Stopwatch component is not installed. If you want to see timing events, run: <code>composer require symfony/stopwatch</code>.</p>
</div>
{% elseif collector.events is empty %}
<div class="empty">
<p>No timing events have been recorded. Check that symfony/stopwatch is installed and debugging enabled in the kernel.</p>
</div>

View File

@ -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; }

View File

@ -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);

View File

@ -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);
}

View File

@ -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}
*/

View File

@ -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());
}
}

View File

@ -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],
],
],
]
);
],
]);
}
/**

View File

@ -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

View File

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

View File

@ -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();

View File

@ -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;
}

View File

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

View File

@ -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],
];
}
}

View File

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

View File

@ -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();

View File

@ -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);

View File

@ -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:{}')];

View File

@ -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);

View File

@ -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;

View File

@ -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);
}

View File

@ -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.

View File

@ -0,0 +1,16 @@
<?php
return \Symfony\Component\VarExporter\Internal\Hydrator::hydrate(
$o = [
clone (($p = &\Symfony\Component\VarExporter\Internal\Registry::$prototypes)['Symfony\\Component\\VarExporter\\Tests\\Php74Serializable'] ?? \Symfony\Component\VarExporter\Internal\Registry::p('Symfony\\Component\\VarExporter\\Tests\\Php74Serializable')),
clone ($p['stdClass'] ?? \Symfony\Component\VarExporter\Internal\Registry::p('stdClass')),
],
null,
[],
$o[0],
[
[
$o[1],
],
]
);

View File

@ -0,0 +1,17 @@
<?php
return \Symfony\Component\VarExporter\Internal\Hydrator::hydrate(
$o = [
clone (\Symfony\Component\VarExporter\Internal\Registry::$prototypes['Symfony\\Component\\VarExporter\\Tests\\PrivateConstructor'] ?? \Symfony\Component\VarExporter\Internal\Registry::p('Symfony\\Component\\VarExporter\\Tests\\PrivateConstructor')),
],
null,
[
'stdClass' => [
'prop' => [
'bar',
],
],
],
$o[0],
[]
);

View File

@ -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();
}
}

View File

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