Merge branch '4.4' into 5.2

* 4.4:
  Fix PHP 8.1 null values
  [Console] Fix PHP 8.1 null error for preg_match flag
  Fix: Article
  Definition::removeMethodCall should remove all matching calls
  mark the LazyIterator class as internal
  fix extracting mixed type-hinted property types
  keep valid submitted choices when additional choices are submitted
This commit is contained in:
Robin Chalas 2021-02-15 19:55:04 +01:00
commit 4365af6ce8
36 changed files with 136 additions and 63 deletions

View File

@ -1446,7 +1446,7 @@ class Configuration implements ConfigurationInterface
->info('A network interface name, IP address, a host name or a UNIX socket to bind to.')
->end()
->booleanNode('verify_peer')
->info('Indicates if the peer should be verified in a SSL/TLS context.')
->info('Indicates if the peer should be verified in an SSL/TLS context.')
->end()
->booleanNode('verify_host')
->info('Indicates if the host should exist as a certificate common name.')
@ -1589,7 +1589,7 @@ class Configuration implements ConfigurationInterface
->info('A network interface name, IP address, a host name or a UNIX socket to bind to.')
->end()
->booleanNode('verify_peer')
->info('Indicates if the peer should be verified in a SSL/TLS context.')
->info('Indicates if the peer should be verified in an SSL/TLS context.')
->end()
->booleanNode('verify_host')
->info('Indicates if the host should exist as a certificate common name.')

View File

@ -97,7 +97,10 @@ return static function (ContainerConfigurator $container) {
->tag('form.type')
->set('form.type.choice', ChoiceType::class)
->args([service('form.choice_list_factory')])
->args([
service('form.choice_list_factory'),
service('translator')->ignoreOnInvalid(),
])
->tag('form.type')
->set('form.type.file', FileType::class)

View File

@ -311,7 +311,7 @@ abstract class AbstractBrowser
* @param string $button The text content, id, value or name of the form <button> or <input type="submit">
* @param array $fieldValues Use this syntax: ['my_form[name]' => '...', 'my_form[email]' => '...']
* @param string $method The HTTP method used to submit the form
* @param array $serverParameters These values override the ones stored in $_SERVER (HTTP headers must include a HTTP_ prefix as PHP does)
* @param array $serverParameters These values override the ones stored in $_SERVER (HTTP headers must include an HTTP_ prefix as PHP does)
*/
public function submitForm(string $button, array $fieldValues = [], string $method = 'POST', array $serverParameters = []): Crawler
{
@ -332,7 +332,7 @@ abstract class AbstractBrowser
* @param string $uri The URI to fetch
* @param array $parameters The Request parameters
* @param array $files The files
* @param array $server The server parameters (HTTP headers are referenced with a HTTP_ prefix as PHP does)
* @param array $server The server parameters (HTTP headers are referenced with an HTTP_ prefix as PHP does)
* @param string $content The raw body data
* @param bool $changeHistory Whether to update the history or not (only used internally for back(), forward(), and reload())
*

View File

@ -18,7 +18,7 @@ use Symfony\Component\Config\Definition\NodeInterface;
use Symfony\Component\Config\Definition\PrototypedArrayNode;
/**
* Dumps a XML reference configuration for the given configuration/node instance.
* Dumps an XML reference configuration for the given configuration/node instance.
*
* @author Wouter J <waldio.webdesign@gmail.com>
*/

View File

@ -48,12 +48,12 @@ class StringInput extends ArgvInput
$length = \strlen($input);
$cursor = 0;
while ($cursor < $length) {
if (preg_match('/\s+/A', $input, $match, null, $cursor)) {
} elseif (preg_match('/([^="\'\s]+?)(=?)('.self::REGEX_QUOTED_STRING.'+)/A', $input, $match, null, $cursor)) {
if (preg_match('/\s+/A', $input, $match, 0, $cursor)) {
} elseif (preg_match('/([^="\'\s]+?)(=?)('.self::REGEX_QUOTED_STRING.'+)/A', $input, $match, 0, $cursor)) {
$tokens[] = $match[1].$match[2].stripcslashes(str_replace(['"\'', '\'"', '\'\'', '""'], '', substr($match[3], 1, \strlen($match[3]) - 2)));
} elseif (preg_match('/'.self::REGEX_QUOTED_STRING.'/A', $input, $match, null, $cursor)) {
} elseif (preg_match('/'.self::REGEX_QUOTED_STRING.'/A', $input, $match, 0, $cursor)) {
$tokens[] = stripcslashes(substr($match[0], 1, \strlen($match[0]) - 2));
} elseif (preg_match('/'.self::REGEX_STRING.'/A', $input, $match, null, $cursor)) {
} elseif (preg_match('/'.self::REGEX_STRING.'/A', $input, $match, 0, $cursor)) {
$tokens[] = stripcslashes($match[1]);
} else {
// should never happen

View File

@ -31,7 +31,7 @@ class ResolveClassPass implements CompilerPassInterface
}
if (preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)++$/', $id)) {
if ($definition instanceof ChildDefinition && !class_exists($id)) {
throw new InvalidArgumentException(sprintf('Service definition "%s" has a parent but no class, and its name looks like a FQCN. Either the class is missing or you want to inherit it from the parent service. To resolve this ambiguity, please rename this service to a non-FQCN (e.g. using dots), or create the missing class.', $id));
throw new InvalidArgumentException(sprintf('Service definition "%s" has a parent but no class, and its name looks like an FQCN. Either the class is missing or you want to inherit it from the parent service. To resolve this ambiguity, please rename this service to a non-FQCN (e.g. using dots), or create the missing class.', $id));
}
$definition->setClass($id);
}

View File

@ -370,7 +370,6 @@ class Definition
foreach ($this->calls as $i => $call) {
if ($call[0] === $method) {
unset($this->calls[$i]);
break;
}
}

View File

@ -26,7 +26,7 @@ trait BindTrait
* injected in the matching parameters (of the constructor, of methods
* called and of controller actions).
*
* @param string $nameOrFqcn A parameter name with its "$" prefix, or a FQCN
* @param string $nameOrFqcn A parameter name with its "$" prefix, or an FQCN
* @param mixed $valueOrRef The value or reference to bind
*
* @return $this

View File

@ -373,7 +373,7 @@ class XmlFileLoader extends FileLoader
}
/**
* Parses a XML file to a \DOMDocument.
* Parses an XML file to a \DOMDocument.
*
* @throws InvalidArgumentException When loading of XML file returns error
*/

View File

@ -86,7 +86,7 @@ class ResolveClassPassTest extends TestCase
public function testAmbiguousChildDefinition()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Service definition "App\Foo\Child" has a parent but no class, and its name looks like a FQCN. Either the class is missing or you want to inherit it from the parent service. To resolve this ambiguity, please rename this service to a non-FQCN (e.g. using dots), or create the missing class.');
$this->expectExceptionMessage('Service definition "App\Foo\Child" has a parent but no class, and its name looks like an FQCN. Either the class is missing or you want to inherit it from the parent service. To resolve this ambiguity, please rename this service to a non-FQCN (e.g. using dots), or create the missing class.');
$container = new ContainerBuilder();
$container->register('App\Foo', null);
$container->setDefinition('App\Foo\Child', new ChildDefinition('App\Foo'));

View File

@ -411,4 +411,20 @@ class DefinitionTest extends TestCase
$def->addError('Second error');
$this->assertSame(['First error', 'Second error'], $def->getErrors());
}
public function testMultipleMethodCalls()
{
$def = new Definition('stdClass');
$def->addMethodCall('configure', ['arg1']);
$this->assertTrue($def->hasMethodCall('configure'));
$this->assertCount(1, $def->getMethodCalls());
$def->addMethodCall('configure', ['arg2']);
$this->assertTrue($def->hasMethodCall('configure'));
$this->assertCount(2, $def->getMethodCalls());
$def->removeMethodCall('configure');
$this->assertFalse($def->hasMethodCall('configure'));
}
}

View File

@ -14,7 +14,7 @@ namespace Symfony\Component\DomCrawler\Field;
/**
* ChoiceFormField represents a choice form field.
*
* It is constructed from a HTML select tag, or a HTML checkbox, or radio inputs.
* It is constructed from an HTML select tag, or an HTML checkbox, or radio inputs.
*
* @author Fabien Potencier <fabien@symfony.com>
*/

View File

@ -13,6 +13,8 @@ namespace Symfony\Component\Finder\Iterator;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*
* @internal
*/
class LazyIterator implements \IteratorAggregate
{

View File

@ -26,7 +26,7 @@ class DateTimeToHtml5LocalDateTimeTransformer extends BaseDateTimeTransformer
* Transforms a \DateTime into a local date and time string.
*
* According to the HTML standard, the input string of a datetime-local
* input is a RFC3339 date followed by 'T', followed by a RFC3339 time.
* input is an RFC3339 date followed by 'T', followed by an RFC3339 time.
* https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-local-date-and-time-string
*
* @param \DateTime|\DateTimeInterface $dateTime A DateTime object

View File

@ -36,6 +36,7 @@ use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransfo
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer;
use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
@ -43,12 +44,17 @@ use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Contracts\Translation\TranslatorInterface;
class ChoiceType extends AbstractType
{
private $choiceListFactory;
private $translator;
public function __construct(ChoiceListFactoryInterface $choiceListFactory = null)
/**
* @param TranslatorInterface $translator
*/
public function __construct(ChoiceListFactoryInterface $choiceListFactory = null, $translator = null)
{
$this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(
new PropertyAccessDecorator(
@ -66,6 +72,11 @@ class ChoiceType extends AbstractType
if ($ref->getNumberOfParameters() < 3) {
trigger_deprecation('symfony/form', '5.1', 'Not defining a third parameter "callable|null $filter" in "%s::%s()" is deprecated.', $ref->class, $ref->name);
}
if (null !== $translator && !$translator instanceof TranslatorInterface) {
throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be han instance of "%s", "%s" given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator)));
}
$this->translator = $translator;
}
/**
@ -73,6 +84,7 @@ class ChoiceType extends AbstractType
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$unknownValues = [];
$choiceList = $this->createChoiceList($options);
$builder->setAttribute('choice_list', $choiceList);
@ -100,10 +112,12 @@ class ChoiceType extends AbstractType
$this->addSubForms($builder, $choiceListView->preferredChoices, $options);
$this->addSubForms($builder, $choiceListView->choices, $options);
}
if ($options['expanded'] || $options['multiple']) {
// Make sure that scalar, submitted values are converted to arrays
// which can be submitted to the checkboxes/radio buttons
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($choiceList, $options, &$unknownValues) {
$form = $event->getForm();
$data = $event->getData();
@ -118,6 +132,10 @@ class ChoiceType extends AbstractType
// Convert the submitted data to a string, if scalar, before
// casting it to an array
if (!\is_array($data)) {
if ($options['multiple']) {
throw new TransformationFailedException('Expected an array.');
}
$data = (array) (string) $data;
}
@ -129,17 +147,26 @@ class ChoiceType extends AbstractType
$unknownValues = $valueMap;
// Reconstruct the data as mapping from child names to values
$data = [];
$knownValues = [];
/** @var FormInterface $child */
foreach ($form as $child) {
$value = $child->getConfig()->getOption('value');
if ($options['expanded']) {
/** @var FormInterface $child */
foreach ($form as $child) {
$value = $child->getConfig()->getOption('value');
// Add the value to $data with the child's name as key
if (isset($valueMap[$value])) {
$data[$child->getName()] = $value;
unset($unknownValues[$value]);
continue;
// Add the value to $data with the child's name as key
if (isset($valueMap[$value])) {
$knownValues[$child->getName()] = $value;
unset($unknownValues[$value]);
continue;
}
}
} else {
foreach ($data as $value) {
if ($choiceList->getChoicesForValues([$value])) {
$knownValues[] = $value;
unset($unknownValues[$value]);
}
}
}
@ -147,16 +174,34 @@ class ChoiceType extends AbstractType
// field exists for it or not
unset($unknownValues['']);
// Throw exception if unknown values were submitted
if (\count($unknownValues) > 0) {
// Throw exception if unknown values were submitted (multiple choices will be handled in a different event listener below)
if (\count($unknownValues) > 0 && !$options['multiple']) {
throw new TransformationFailedException(sprintf('The choices "%s" do not exist in the choice list.', implode('", "', array_keys($unknownValues))));
}
$event->setData($data);
$event->setData($knownValues);
});
}
if ($options['multiple']) {
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use (&$unknownValues) {
// Throw exception if unknown values were submitted
if (\count($unknownValues) > 0) {
$form = $event->getForm();
$clientDataAsString = is_scalar($form->getViewData()) ? (string) $form->getViewData() : \gettype($form->getViewData());
$messageTemplate = 'The value {{ value }} is not valid.';
if (null !== $this->translator) {
$message = $this->translator->trans($messageTemplate, ['{{ value }}' => $clientDataAsString], 'validators');
} else {
$message = strtr($messageTemplate, ['{{ value }}' => $clientDataAsString]);
}
$form->addError(new FormError($message, $messageTemplate, ['{{ value }}' => $clientDataAsString], null, new TransformationFailedException(sprintf('The choices "%s" do not exist in the choice list.', implode('", "', array_keys($unknownValues))))));
}
});
// <select> tag with "multiple" option or list of checkbox inputs
$builder->addViewTransformer(new ChoicesToValuesTransformer($choiceList));
} else {

View File

@ -209,7 +209,7 @@ class DateTimeType extends AbstractType
{
$view->vars['widget'] = $options['widget'];
// Change the input to a HTML5 datetime input if
// Change the input to an HTML5 datetime input if
// * the widget is set to "single_text"
// * the format matches the one expected by HTML5
// * the html5 is set to true

View File

@ -189,7 +189,7 @@ class DateType extends AbstractType
{
$view->vars['widget'] = $options['widget'];
// Change the input to a HTML5 date input if
// Change the input to an HTML5 date input if
// * the widget is set to "single_text"
// * the format matches the one expected by HTML5
// * the html5 is set to true

View File

@ -221,7 +221,7 @@ class TimeType extends AbstractType
'with_seconds' => $options['with_seconds'],
]);
// Change the input to a HTML5 time input if
// Change the input to an HTML5 time input if
// * the widget is set to "single_text"
// * the html5 is set to true
if ($options['html5'] && 'single_text' === $options['widget']) {

View File

@ -842,9 +842,9 @@ class ChoiceTypeTest extends BaseTypeTest
$form->submit(['a', 'foobar']);
$this->assertNull($form->getData());
$this->assertEquals(['a', 'foobar'], $form->getViewData());
$this->assertFalse($form->isSynchronized());
$this->assertEquals(['a'], $form->getData());
$this->assertEquals(['a'], $form->getViewData());
$this->assertFalse($form->isValid());
}
public function testSubmitMultipleNonExpandedObjectChoices()
@ -1385,17 +1385,17 @@ class ChoiceTypeTest extends BaseTypeTest
$form->submit(['a', 'foobar']);
$this->assertNull($form->getData());
$this->assertSame(['a', 'foobar'], $form->getViewData());
$this->assertSame(['a'], $form->getData());
$this->assertSame(['a'], $form->getViewData());
$this->assertEmpty($form->getExtraData());
$this->assertFalse($form->isSynchronized());
$this->assertFalse($form->isValid());
$this->assertFalse($form[0]->getData());
$this->assertTrue($form[0]->getData());
$this->assertFalse($form[1]->getData());
$this->assertFalse($form[2]->getData());
$this->assertFalse($form[3]->getData());
$this->assertFalse($form[4]->getData());
$this->assertNull($form[0]->getViewData());
$this->assertSame('a', $form[0]->getViewData());
$this->assertNull($form[1]->getViewData());
$this->assertNull($form[2]->getViewData());
$this->assertNull($form[3]->getViewData());
@ -2070,8 +2070,13 @@ class ChoiceTypeTest extends BaseTypeTest
$form->submit($multiple ? (array) $submittedData : $submittedData);
// When the choice does not exist the transformation fails
$this->assertFalse($form->isSynchronized());
$this->assertNull($form->getData());
$this->assertFalse($form->isValid());
if ($multiple) {
$this->assertSame([], $form->getData());
} else {
$this->assertNull($form->getData());
}
}
/**

View File

@ -146,7 +146,7 @@ class HeaderUtils
}
/**
* Generates a HTTP Content-Disposition field-value.
* Generates an HTTP Content-Disposition field-value.
*
* @param string $disposition One of "inline" or "attachment"
* @param string $filename A unicode string

View File

@ -1752,7 +1752,7 @@ class Request
}
/**
* Returns true if the request is a XMLHttpRequest.
* Returns true if the request is an XMLHttpRequest.
*
* It works if your JavaScript library sets an X-Requested-With HTTP header.
* It is known to work with common JavaScript frameworks:

View File

@ -1264,7 +1264,7 @@ class Response
*/
protected function ensureIEOverSSLCompatibility(Request $request): void
{
if (false !== stripos($this->headers->get('Content-Disposition'), 'attachment') && 1 == preg_match('/MSIE (.*?);/i', $request->server->get('HTTP_USER_AGENT'), $match) && true === $request->isSecure()) {
if (false !== stripos($this->headers->get('Content-Disposition') ?? '', 'attachment') && 1 == preg_match('/MSIE (.*?);/i', $request->server->get('HTTP_USER_AGENT') ?? '', $match) && true === $request->isSecure()) {
if ((int) preg_replace('/(MSIE )(.*?);/', '$2', $match[0]) < 9) {
$this->headers->remove('Cache-Control');
}

View File

@ -805,7 +805,7 @@ class RequestTest extends TestCase
['bar=&foo=bar', 'bar=&foo=bar', '->works with empty parameters'],
['foo=bar&bar=', 'bar=&foo=bar', 'sorts keys alphabetically'],
// GET parameters, that are submitted from a HTML form, encode spaces as "+" by default (as defined in enctype application/x-www-form-urlencoded).
// GET parameters, that are submitted from an HTML form, encode spaces as "+" by default (as defined in enctype application/x-www-form-urlencoded).
// PHP also converts "+" to spaces when filling the global _GET or when using the function parse_str.
['baz=Foo%20Baz&bar=Foo+Bar', 'bar=Foo%20Bar&baz=Foo%20Baz', 'normalizes spaces in both encodings "%20" and "+"'],

View File

@ -199,7 +199,7 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl
}
/**
* Gets a HTTP kernel from the container.
* Gets an HTTP kernel from the container.
*
* @return HttpKernelInterface
*/

View File

@ -150,7 +150,7 @@ class Query extends AbstractQuery
}
/**
* Returns a LDAP search resource. If this query resulted in multiple searches, only the first
* Returns an LDAP search resource. If this query resulted in multiple searches, only the first
* page will be returned.
*
* @return resource

View File

@ -386,7 +386,7 @@ class PdoStore implements PersistingStoreInterface
}
/**
* Provides a SQL function to get the current timestamp regarding the current connection's driver.
* Provides an SQL function to get the current timestamp regarding the current connection's driver.
*/
private function getCurrentTimestampStatement(): string
{

View File

@ -102,7 +102,7 @@ final class ParameterizedHeader extends UnstructuredHeader
}
/**
* Render a RFC 2047 compliant header parameter from the $name and $value.
* Render an RFC 2047 compliant header parameter from the $name and $value.
*/
private function createParameter(string $name, string $value): string
{

View File

@ -164,8 +164,8 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
try {
$reflectionProperty = new \ReflectionProperty($class, $property);
$type = $reflectionProperty->getType();
if (null !== $type) {
return $this->extractFromReflectionType($type, $reflectionProperty->getDeclaringClass());
if (null !== $type && $types = $this->extractFromReflectionType($type, $reflectionProperty->getDeclaringClass())) {
return $types;
}
} catch (\ReflectionException $e) {
// noop

View File

@ -255,6 +255,7 @@ class ReflectionExtractorTest extends TestCase
['string', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Stringable'), new Type(Type::BUILTIN_TYPE_STRING)]],
['payload', null],
['data', null],
['mixedProperty', null],
];
}

View File

@ -4,6 +4,8 @@ namespace Symfony\Component\PropertyInfo\Tests\Fixtures;
class Php80Dummy
{
public mixed $mixedProperty;
public function getFoo(): array|null
{
}

View File

@ -68,7 +68,7 @@ interface RememberMeServicesInterface
* although this is not recommended.
*
* Instead, implementations should typically look for a request parameter
* (such as a HTTP POST parameter) that indicates the browser has explicitly
* (such as an HTTP POST parameter) that indicates the browser has explicitly
* requested for the authentication to be remembered.
*/
public function loginSuccess(Request $request, Response $response, TokenInterface $token);

View File

@ -21,7 +21,7 @@ final class SecurityEvents
* into your website. It is important to distinguish this action from
* non-interactive authentication methods, such as:
* - authentication based on your session.
* - authentication using a HTTP basic or HTTP digest header.
* - authentication using an HTTP basic or HTTP digest header.
*
* @Event("Symfony\Component\Security\Http\Event\InteractiveLoginEvent")
*/

View File

@ -110,7 +110,7 @@ class XmlFileLoader extends FileLoader
}
/**
* Parses a XML File.
* Parses an XML File.
*
* @throws MappingException
*/

View File

@ -53,11 +53,11 @@ class XliffLintCommand extends Command
protected function configure()
{
$this
->setDescription('Lints a XLIFF file and outputs encountered errors')
->setDescription('Lints an XLIFF file and outputs encountered errors')
->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN')
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt')
->setHelp(<<<EOF
The <info>%command.name%</info> command lints a XLIFF file and outputs to STDOUT
The <info>%command.name%</info> command lints an XLIFF file and outputs to STDOUT
the first encountered syntax error.
You can validates XLIFF contents passed from STDIN:

View File

@ -19,19 +19,19 @@ use Symfony\Component\Translation\Exception\InvalidResourceException;
class MoFileLoader extends FileLoader
{
/**
* Magic used for validating the format of a MO file as well as
* Magic used for validating the format of an MO file as well as
* detecting if the machine used to create that file was little endian.
*/
public const MO_LITTLE_ENDIAN_MAGIC = 0x950412de;
/**
* Magic used for validating the format of a MO file as well as
* Magic used for validating the format of an MO file as well as
* detecting if the machine used to create that file was big endian.
*/
public const MO_BIG_ENDIAN_MAGIC = 0xde120495;
/**
* The size of the header of a MO file in bytes.
* The size of the header of an MO file in bytes.
*/
public const MO_HEADER_SIZE = 28;

View File

@ -14,7 +14,7 @@ namespace Symfony\Component\WebLink;
use Psr\Link\LinkInterface;
/**
* Serializes a list of Link instances to a HTTP Link header.
* Serializes a list of Link instances to an HTTP Link header.
*
* @see https://tools.ietf.org/html/rfc5988
*