Merge branch '3.4' into 4.1

* 3.4:
  [Debug] ignore underscore vs backslash namespaces in DebugClassLoader
  [TwigBridge][Form] Prevent multiple rendering of form collection prototypes
  [FrameworkBundle] fix describing routes with no controllers
  [DI] move RegisterServiceSubscribersPass before DecoratorServicePass
  Update ValidationListener.php
  [Yaml] ensures that the mb_internal_encoding is reset to its initial value
  [WebLink] Fixed documentation link
  [Security] getTargetPath of TargetPathTrait must return string or null
  [Hackday][Serializer] Deserialization ignores argument type hint from phpdoc for array in constructor argument
  [Security] defer log message in guard authenticator
  merge conflicts
  Fix HeaderBag::get phpdoc
This commit is contained in:
Nicolas Grekas 2018-12-13 13:30:33 +01:00
commit 547bf26eee
16 changed files with 265 additions and 63 deletions

View File

@ -28,7 +28,7 @@
{%- endblock form_widget_compound -%} {%- endblock form_widget_compound -%}
{%- block collection_widget -%} {%- block collection_widget -%}
{% if prototype is defined %} {% if prototype is defined and not prototype.rendered %}
{%- set attr = attr|merge({'data-prototype': form_row(prototype) }) -%} {%- set attr = attr|merge({'data-prototype': form_row(prototype) }) -%}
{% endif %} {% endif %}
{{- block('form_widget') -}} {{- block('form_widget') -}}

View File

@ -56,7 +56,7 @@ class TextDescriptor extends Descriptor
if ($showControllers) { if ($showControllers) {
$controller = $route->getDefault('_controller'); $controller = $route->getDefault('_controller');
$row[] = $this->formatCallable($controller); $row[] = $controller ? $this->formatCallable($controller) : '';
} }
$tableRows[] = $row; $tableRows[] = $row;

View File

@ -214,7 +214,7 @@ class DebugClassLoader
$len = 0; $len = 0;
$ns = ''; $ns = '';
} else { } else {
$ns = \substr($class, 0, $len); $ns = \str_replace('_', '\\', \substr($class, 0, $len));
} }
// Detect annotations on the class // Detect annotations on the class
@ -245,13 +245,13 @@ class DebugClassLoader
if (!isset(self::$checkedClasses[$use])) { if (!isset(self::$checkedClasses[$use])) {
$this->checkClass($use); $this->checkClass($use);
} }
if (isset(self::$deprecated[$use]) && \strncmp($ns, $use, $len)) { if (isset(self::$deprecated[$use]) && \strncmp($ns, \str_replace('_', '\\', $use), $len)) {
$type = class_exists($class, false) ? 'class' : (interface_exists($class, false) ? 'interface' : 'trait'); $type = class_exists($class, false) ? 'class' : (interface_exists($class, false) ? 'interface' : 'trait');
$verb = class_exists($use, false) || interface_exists($class, false) ? 'extends' : (interface_exists($use, false) ? 'implements' : 'uses'); $verb = class_exists($use, false) || interface_exists($class, false) ? 'extends' : (interface_exists($use, false) ? 'implements' : 'uses');
$deprecations[] = sprintf('The "%s" %s %s "%s" that is deprecated%s.', $class, $type, $verb, $use, self::$deprecated[$use]); $deprecations[] = sprintf('The "%s" %s %s "%s" that is deprecated%s.', $class, $type, $verb, $use, self::$deprecated[$use]);
} }
if (isset(self::$internal[$use]) && \strncmp($ns, $use, $len)) { if (isset(self::$internal[$use]) && \strncmp($ns, \str_replace('_', '\\', $use), $len)) {
$deprecations[] = sprintf('The "%s" %s is considered internal%s. It may change without further notice. You should not use it from "%s".', $use, class_exists($use, false) ? 'class' : (interface_exists($use, false) ? 'interface' : 'trait'), self::$internal[$use], $class); $deprecations[] = sprintf('The "%s" %s is considered internal%s. It may change without further notice. You should not use it from "%s".', $use, class_exists($use, false) ? 'class' : (interface_exists($use, false) ? 'interface' : 'trait'), self::$internal[$use], $class);
} }
} }

View File

@ -52,11 +52,11 @@ class PassConfig
new ValidateEnvPlaceholdersPass(), new ValidateEnvPlaceholdersPass(),
new ResolveChildDefinitionsPass(), new ResolveChildDefinitionsPass(),
new ServiceLocatorTagPass(), new ServiceLocatorTagPass(),
new RegisterServiceSubscribersPass(),
new DecoratorServicePass(), new DecoratorServicePass(),
new ResolveParameterPlaceHoldersPass(false), new ResolveParameterPlaceHoldersPass(false),
new ResolveFactoryClassPass(), new ResolveFactoryClassPass(),
new CheckDefinitionValidityPass(), new CheckDefinitionValidityPass(),
new RegisterServiceSubscribersPass(),
new ResolveNamedArgumentsPass(), new ResolveNamedArgumentsPass(),
new AutowireRequiredMethodsPass(), new AutowireRequiredMethodsPass(),
new ResolveBindingsPass(), new ResolveBindingsPass(),

View File

@ -94,39 +94,40 @@ class ServiceLocator implements PsrContainerInterface
$class = isset($class[2]['object']) ? \get_class($class[2]['object']) : null; $class = isset($class[2]['object']) ? \get_class($class[2]['object']) : null;
$externalId = $this->externalId ?: $class; $externalId = $this->externalId ?: $class;
$msg = sprintf('Service "%s" not found: ', $id); $msg = array();
$msg[] = sprintf('Service "%s" not found:', $id);
if (!$this->container) { if (!$this->container) {
$class = null; $class = null;
} elseif ($this->container->has($id) || isset($this->container->getRemovedIds()[$id])) { } elseif ($this->container->has($id) || isset($this->container->getRemovedIds()[$id])) {
$msg .= 'even though it exists in the app\'s container, '; $msg[] = 'even though it exists in the app\'s container,';
} else { } else {
try { try {
$this->container->get($id); $this->container->get($id);
$class = null; $class = null;
} catch (ServiceNotFoundException $e) { } catch (ServiceNotFoundException $e) {
if ($e->getAlternatives()) { if ($e->getAlternatives()) {
$msg .= sprintf(' did you mean %s? Anyway, ', $this->formatAlternatives($e->getAlternatives(), 'or')); $msg[] = sprintf('did you mean %s? Anyway,', $this->formatAlternatives($e->getAlternatives(), 'or'));
} else { } else {
$class = null; $class = null;
} }
} }
} }
if ($externalId) { if ($externalId) {
$msg .= sprintf('the container inside "%s" is a smaller service locator that %s', $externalId, $this->formatAlternatives()); $msg[] = sprintf('the container inside "%s" is a smaller service locator that %s', $externalId, $this->formatAlternatives());
} else { } else {
$msg .= sprintf('the current service locator %s', $this->formatAlternatives()); $msg[] = sprintf('the current service locator %s', $this->formatAlternatives());
} }
if (!$class) { if (!$class) {
// no-op // no-op
} elseif (is_subclass_of($class, ServiceSubscriberInterface::class)) { } elseif (is_subclass_of($class, ServiceSubscriberInterface::class)) {
$msg .= sprintf(' Unless you need extra laziness, try using dependency injection instead. Otherwise, you need to declare it using "%s::getSubscribedServices()".', preg_replace('/([^\\\\]++\\\\)++/', '', $class)); $msg[] = sprintf('Unless you need extra laziness, try using dependency injection instead. Otherwise, you need to declare it using "%s::getSubscribedServices()".', preg_replace('/([^\\\\]++\\\\)++/', '', $class));
} else { } else {
$msg .= 'Try using dependency injection instead.'; $msg[] = 'Try using dependency injection instead.';
} }
return $msg; return implode(' ', $msg);
} }
private function formatAlternatives(array $alternatives = null, $separator = 'and') private function formatAlternatives(array $alternatives = null, $separator = 'and')

View File

@ -17,6 +17,7 @@ use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
/** /**
* This class tests the integration of the different compiler passes. * This class tests the integration of the different compiler passes.
@ -120,6 +121,21 @@ class IntegrationTest extends TestCase
$this->assertFalse($container->hasDefinition('c'), 'Service C was not inlined.'); $this->assertFalse($container->hasDefinition('c'), 'Service C was not inlined.');
} }
public function testCanDecorateServiceSubscriber()
{
$container = new ContainerBuilder();
$container->register(ServiceSubscriberStub::class)
->addTag('container.service_subscriber')
->setPublic(true);
$container->register(DecoratedServiceSubscriber::class)
->setDecoratedService(ServiceSubscriberStub::class);
$container->compile();
$this->assertInstanceOf(DecoratedServiceSubscriber::class, $container->get(ServiceSubscriberStub::class));
}
/** /**
* @dataProvider getYamlCompileTests * @dataProvider getYamlCompileTests
*/ */
@ -220,6 +236,18 @@ class IntegrationTest extends TestCase
} }
} }
class ServiceSubscriberStub implements ServiceSubscriberInterface
{
public static function getSubscribedServices()
{
return array();
}
}
class DecoratedServiceSubscriber
{
}
class IntegrationTestStub extends IntegrationTestStubParent class IntegrationTestStub extends IntegrationTestStubParent
{ {
} }

View File

@ -115,6 +115,20 @@ class ServiceLocatorTest extends TestCase
$subscriber->getFoo(); $subscriber->getFoo();
} }
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException
* @expectedExceptionMessage Service "foo" not found: even though it exists in the app's container, the container inside "foo" is a smaller service locator that is empty... Try using dependency injection instead.
*/
public function testGetThrowsServiceNotFoundException()
{
$container = new Container();
$container->set('foo', new \stdClass());
$locator = new ServiceLocator(array());
$locator = $locator->withContext('foo', $container);
$locator->get('foo');
}
public function testInvoke() public function testInvoke()
{ {
$locator = new ServiceLocator(array( $locator = new ServiceLocator(array(

View File

@ -51,7 +51,7 @@ class ValidationListener implements EventSubscriberInterface
$form = $event->getForm(); $form = $event->getForm();
if ($form->isRoot()) { if ($form->isRoot()) {
// Validate the form in group "Default" // Form groups are validated internally (FormValidator). Here we don't set groups as they are retrieved into the validator.
foreach ($this->validator->validate($form) as $violation) { foreach ($this->validator->validate($form) as $violation) {
// Allow the "invalid" constraint to be put onto // Allow the "invalid" constraint to be put onto
// non-synchronized forms // non-synchronized forms

View File

@ -101,9 +101,9 @@ class HeaderBag implements \IteratorAggregate, \Countable
/** /**
* Returns a header value by name. * Returns a header value by name.
* *
* @param string $key The header name * @param string $key The header name
* @param string|string[]|null $default The default value * @param string|null $default The default value
* @param bool $first Whether to return the first value or all header values * @param bool $first Whether to return the first value or all header values
* *
* @return string|string[]|null The first header value or default value if $first is true, an array of values otherwise * @return string|string[]|null The first header value or default value if $first is true, an array of values otherwise
*/ */

View File

@ -96,14 +96,22 @@ class GuardAuthenticationListener implements ListenerInterface
$request = $event->getRequest(); $request = $event->getRequest();
try { try {
if (null !== $this->logger) { if (null !== $this->logger) {
$this->logger->debug('Calling getCredentials() on guard authenticator.', array('firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator))); $this->logger->debug('Checking support on guard authenticator.', array('firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)));
} }
// abort the execution of the authenticator if it doesn't support the request // abort the execution of the authenticator if it doesn't support the request
if (!$guardAuthenticator->supports($request)) { if (!$guardAuthenticator->supports($request)) {
if (null !== $this->logger) {
$this->logger->debug('Guard authenticator does not support the request.', array('firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)));
}
return; return;
} }
if (null !== $this->logger) {
$this->logger->debug('Calling getCredentials() on guard authenticator.', array('firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)));
}
// allow the authenticator to fetch authentication info from the request // allow the authenticator to fetch authentication info from the request
$credentials = $guardAuthenticator->getCredentials($request); $credentials = $guardAuthenticator->getCredentials($request);

View File

@ -38,7 +38,7 @@ trait TargetPathTrait
* @param SessionInterface $session * @param SessionInterface $session
* @param string $providerKey The name of your firewall * @param string $providerKey The name of your firewall
* *
* @return string * @return string|null
*/ */
private function getTargetPath(SessionInterface $session, $providerKey) private function getTargetPath(SessionInterface $session, $providerKey)
{ {

View File

@ -358,25 +358,9 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
unset($data[$key]); unset($data[$key]);
continue; continue;
} }
try {
if (null !== $constructorParameter->getClass()) {
if (!$this->serializer instanceof DenormalizerInterface) {
throw new LogicException(sprintf('Cannot create an instance of %s from serialized data because the serializer inject in "%s" is not a denormalizer', $constructorParameter->getClass(), static::class));
}
$parameterClass = $constructorParameter->getClass()->getName();
$parameterData = $this->serializer->denormalize($parameterData, $parameterClass, $format, $this->createChildContext($context, $paramName));
}
} catch (\ReflectionException $e) {
throw new RuntimeException(sprintf('Could not determine the class of the parameter "%s".', $key), 0, $e);
} catch (MissingConstructorArgumentsException $e) {
if (!$constructorParameter->getType()->allowsNull()) {
throw $e;
}
$parameterData = null;
}
// Don't run set for a parameter passed to the constructor // Don't run set for a parameter passed to the constructor
$params[] = $parameterData; $params[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $context, $format);
unset($data[$key]); unset($data[$key]);
} elseif (array_key_exists($key, $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? array())) { } elseif (array_key_exists($key, $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? array())) {
$params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key]; $params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
@ -397,6 +381,31 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
return new $class(); return new $class();
} }
/**
* @internal
*/
protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, $parameterName, $parameterData, array $context, $format = null)
{
try {
if (null !== $parameter->getClass()) {
if (!$this->serializer instanceof DenormalizerInterface) {
throw new LogicException(sprintf('Cannot create an instance of %s from serialized data because the serializer inject in "%s" is not a denormalizer', $parameter->getClass(), static::class));
}
$parameterClass = $parameter->getClass()->getName();
$parameterData = $this->serializer->denormalize($parameterData, $parameterClass, $format, $this->createChildContext($context, $parameterName));
}
} catch (\ReflectionException $e) {
throw new RuntimeException(sprintf('Could not determine the class of the parameter "%s".', $parameterName), 0, $e);
} catch (MissingConstructorArgumentsException $e) {
if (!$parameter->getType()->allowsNull()) {
throw $e;
}
$parameterData = null;
}
return $parameterData;
}
/** /**
* @param array $parentContext * @param array $parentContext
* @param string $attribute * @param string $attribute

View File

@ -357,6 +357,18 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be one of "%s" ("%s" given).', $attribute, $currentClass, implode('", "', array_keys($expectedTypes)), \gettype($data))); throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be one of "%s" ("%s" given).', $attribute, $currentClass, implode('", "', array_keys($expectedTypes)), \gettype($data)));
} }
/**
* @internal
*/
protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, $parameterName, $parameterData, array $context, $format = null)
{
if (null === $this->propertyTypeExtractor || null === $types = $this->propertyTypeExtractor->getTypes($class->getName(), $parameterName)) {
return parent::denormalizeParameter($class, $parameter, $parameterName, $parameterData, $context, $format);
}
return $this->validateAndDenormalize($class->getName(), $parameterName, $parameterData, $format, $context);
}
/** /**
* @return Type[]|null * @return Type[]|null
*/ */

View File

@ -0,0 +1,128 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
class DeserializeNestedArrayOfObjectsTest extends TestCase
{
public function provider()
{
return array(
//from property PhpDoc
array(Zoo::class),
//from argument constructor PhpDoc
array(ZooImmutable::class),
);
}
/**
* @dataProvider provider
*/
public function testPropertyPhpDoc($class)
{
//GIVEN
$json = <<<EOF
{
"animals": [
{"name": "Bug"}
]
}
EOF;
$serializer = new Serializer(array(
new ObjectNormalizer(null, null, null, new PhpDocExtractor()),
new ArrayDenormalizer(),
), array('json' => new JsonEncoder()));
//WHEN
/** @var Zoo $zoo */
$zoo = $serializer->deserialize($json, $class, 'json');
//THEN
self::assertCount(1, $zoo->getAnimals());
self::assertInstanceOf(Animal::class, $zoo->getAnimals()[0]);
}
}
class Zoo
{
/** @var Animal[] */
private $animals = array();
/**
* @return Animal[]
*/
public function getAnimals()
{
return $this->animals;
}
/**
* @param Animal[] $animals
*/
public function setAnimals(array $animals)
{
$this->animals = $animals;
}
}
class ZooImmutable
{
/** @var Animal[] */
private $animals = array();
/**
* @param Animal[] $animals
*/
public function __construct(array $animals = array())
{
$this->animals = $animals;
}
/**
* @return Animal[]
*/
public function getAnimals()
{
return $this->animals;
}
}
class Animal
{
/** @var string */
private $name;
public function __construct()
{
echo '';
}
/**
* @return string|null
*/
public function getName()
{
return $this->name;
}
/**
* @param string|null $name
*/
public function setName($name)
{
$this->name = $name;
}
}

View File

@ -11,7 +11,7 @@ It can also be used with extensions defined in the [HTML5 link type extensions w
Resources Resources
--------- ---------
* [Documentation](https://symfony.com/doc/current/components/weblink/introduction.html) * [Documentation](https://symfony.com/doc/current/components/web_link.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and * [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls) [send Pull Requests](https://github.com/symfony/symfony/pulls)

View File

@ -78,35 +78,37 @@ class Inline
mb_internal_encoding('ASCII'); mb_internal_encoding('ASCII');
} }
$i = 0; try {
$tag = self::parseTag($value, $i, $flags); $i = 0;
switch ($value[$i]) { $tag = self::parseTag($value, $i, $flags);
case '[': switch ($value[$i]) {
$result = self::parseSequence($value, $flags, $i, $references); case '[':
++$i; $result = self::parseSequence($value, $flags, $i, $references);
break; ++$i;
case '{': break;
$result = self::parseMapping($value, $flags, $i, $references); case '{':
++$i; $result = self::parseMapping($value, $flags, $i, $references);
break; ++$i;
default: break;
$result = self::parseScalar($value, $flags, null, $i, null === $tag, $references); default:
} $result = self::parseScalar($value, $flags, null, $i, null === $tag, $references);
}
if (null !== $tag && '' !== $tag) { if (null !== $tag && '' !== $tag) {
return new TaggedValue($tag, $result); return new TaggedValue($tag, $result);
} }
// some comments are allowed at the end // some comments are allowed at the end
if (preg_replace('/\s+#.*$/A', '', substr($value, $i))) { if (preg_replace('/\s+#.*$/A', '', substr($value, $i))) {
throw new ParseException(sprintf('Unexpected characters near "%s".', substr($value, $i)), self::$parsedLineNumber + 1, $value, self::$parsedFilename); throw new ParseException(sprintf('Unexpected characters near "%s".', substr($value, $i)), self::$parsedLineNumber + 1, $value, self::$parsedFilename);
} }
if (isset($mbEncoding)) { return $result;
mb_internal_encoding($mbEncoding); } finally {
if (isset($mbEncoding)) {
mb_internal_encoding($mbEncoding);
}
} }
return $result;
} }
/** /**