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:
commit
7e2fbe13c8
@ -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());
|
||||
|
@ -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>
|
||||
|
@ -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; }
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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}
|
||||
*/
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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:{}')];
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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],
|
||||
],
|
||||
]
|
||||
);
|
@ -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],
|
||||
[]
|
||||
);
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
Reference in New Issue
Block a user