diff --git a/.gitattributes b/.gitattributes index 84c7add05..14c3c3594 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,3 @@ /Tests export-ignore /phpunit.xml.dist export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore +/.git* export-ignore diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..4689c4dad --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +Please do not submit any Pull Requests here. They will be closed. +--- + +Please submit your PR here instead: +https://github.com/symfony/symfony + +This repository is what we call a "subtree split": a read-only subset of that main repository. +We're looking forward to your PR there! diff --git a/.github/workflows/close-pull-request.yml b/.github/workflows/close-pull-request.yml new file mode 100644 index 000000000..e55b47817 --- /dev/null +++ b/.github/workflows/close-pull-request.yml @@ -0,0 +1,20 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: | + Thanks for your Pull Request! We love contributions. + + However, you should instead open your PR on the main repository: + https://github.com/symfony/symfony + + This repository is what we call a "subtree split": a read-only subset of that main repository. + We're looking forward to your PR there! diff --git a/AbstractExtension.php b/AbstractExtension.php index 908f4f86f..27665727c 100644 --- a/AbstractExtension.php +++ b/AbstractExtension.php @@ -50,7 +50,7 @@ public function getType(string $name): FormTypeInterface } if (!isset($this->types[$name])) { - throw new InvalidArgumentException(sprintf('The type "%s" cannot be loaded by this extension.', $name)); + throw new InvalidArgumentException(\sprintf('The type "%s" cannot be loaded by this extension.', $name)); } return $this->types[$name]; diff --git a/AbstractRendererEngine.php b/AbstractRendererEngine.php index bccdf69cc..1968f5a37 100644 --- a/AbstractRendererEngine.php +++ b/AbstractRendererEngine.php @@ -25,8 +25,6 @@ abstract class AbstractRendererEngine implements FormRendererEngineInterface, Re */ public const CACHE_KEY_VAR = 'cache_key'; - protected array $defaultThemes; - /** * @var array[] */ @@ -53,9 +51,9 @@ abstract class AbstractRendererEngine implements FormRendererEngineInterface, Re * @param array $defaultThemes The default themes. The type of these * themes is open to the implementation. */ - public function __construct(array $defaultThemes = []) - { - $this->defaultThemes = $defaultThemes; + public function __construct( + protected array $defaultThemes = [], + ) { } public function setTheme(FormView $view, mixed $themes, bool $useDefaultThemes = true): void diff --git a/AbstractTypeExtension.php b/AbstractTypeExtension.php index 9f6da0a33..b32f3b522 100644 --- a/AbstractTypeExtension.php +++ b/AbstractTypeExtension.php @@ -18,9 +18,6 @@ */ abstract class AbstractTypeExtension implements FormTypeExtensionInterface { - /** - * @return void - */ public function configureOptions(OptionsResolver $resolver): void { } diff --git a/Button.php b/Button.php index c0bad6e0a..c35f93d60 100644 --- a/Button.php +++ b/Button.php @@ -26,15 +26,14 @@ class Button implements \IteratorAggregate, FormInterface { private ?FormInterface $parent = null; - private FormConfigInterface $config; private bool $submitted = false; /** * Creates a new button from a form configuration. */ - public function __construct(FormConfigInterface $config) - { - $this->config = $config; + public function __construct( + private FormConfigInterface $config, + ) { } /** @@ -312,7 +311,7 @@ public function handleRequest(mixed $request = null): static * * @return $this * - * @throws Exception\AlreadySubmittedException if the button has already been submitted + * @throws AlreadySubmittedException if the button has already been submitted */ public function submit(array|string|null $submittedData, bool $clearMissing = true): static { diff --git a/ButtonBuilder.php b/ButtonBuilder.php index afb51cd43..4f7079142 100644 --- a/ButtonBuilder.php +++ b/ButtonBuilder.php @@ -31,19 +31,19 @@ class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface private ResolvedFormTypeInterface $type; private string $name; private array $attributes = []; - private array $options; /** * @throws InvalidArgumentException if the name is empty */ - public function __construct(?string $name, array $options = []) - { + public function __construct( + ?string $name, + private array $options = [], + ) { if ('' === $name || null === $name) { throw new InvalidArgumentException('Buttons cannot have empty names.'); } $this->name = $name; - $this->options = $options; FormConfigBuilder::validateName($name); } diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c712cc14..3b1fabfd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,23 @@ CHANGELOG ========= +7.2 +--- + + * Deprecate the `VersionAwareTest` trait, use feature detection instead + * Add support for the `calendar` option in `DateType` + * Add `LazyChoiceLoader` and `choice_lazy` option in `ChoiceType` for loading and rendering choices on demand + * Use `form.post_set_data` instead of `form.pre_set_data` in `ResizeFormListener` + * Change the priority of `DataCollectorListener` from 255 to -255 + +7.1 +--- + + * Add option `separator` to `ChoiceType` to use a custom separator after preferred choices (use the new `separator_html` option to display the separator text as HTML) + * Deprecate not configuring the `default_protocol` option of the `UrlType`, it will default to `null` in 8.0 (the current default is `'http'`) + * Add a `keep_as_list` option to `CollectionType` + * Add an `input` option to `MoneyType`, to be able to cast the transformed value to `integer` + 7.0 --- diff --git a/ChoiceList/Factory/CachingFactoryDecorator.php b/ChoiceList/Factory/CachingFactoryDecorator.php index 687fcec1e..1f373228b 100644 --- a/ChoiceList/Factory/CachingFactoryDecorator.php +++ b/ChoiceList/Factory/CachingFactoryDecorator.php @@ -27,8 +27,6 @@ */ class CachingFactoryDecorator implements ChoiceListFactoryInterface, ResetInterface { - private ChoiceListFactoryInterface $decoratedFactory; - /** * @var ChoiceListInterface[] */ @@ -64,9 +62,9 @@ public static function generateHash(mixed $value, string $namespace = ''): strin return hash('sha256', $namespace.':'.serialize($value)); } - public function __construct(ChoiceListFactoryInterface $decoratedFactory) - { - $this->decoratedFactory = $decoratedFactory; + public function __construct( + private ChoiceListFactoryInterface $decoratedFactory, + ) { } /** diff --git a/ChoiceList/Factory/PropertyAccessDecorator.php b/ChoiceList/Factory/PropertyAccessDecorator.php index c83ef17e9..f73a8fc2a 100644 --- a/ChoiceList/Factory/PropertyAccessDecorator.php +++ b/ChoiceList/Factory/PropertyAccessDecorator.php @@ -38,12 +38,12 @@ */ class PropertyAccessDecorator implements ChoiceListFactoryInterface { - private ChoiceListFactoryInterface $decoratedFactory; private PropertyAccessorInterface $propertyAccessor; - public function __construct(ChoiceListFactoryInterface $decoratedFactory, ?PropertyAccessorInterface $propertyAccessor = null) - { - $this->decoratedFactory = $decoratedFactory; + public function __construct( + private ChoiceListFactoryInterface $decoratedFactory, + ?PropertyAccessorInterface $propertyAccessor = null, + ) { $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); } diff --git a/ChoiceList/LazyChoiceList.php b/ChoiceList/LazyChoiceList.php index 2f7918926..8e0eaaa8f 100644 --- a/ChoiceList/LazyChoiceList.php +++ b/ChoiceList/LazyChoiceList.php @@ -27,8 +27,6 @@ */ class LazyChoiceList implements ChoiceListInterface { - private ChoiceLoaderInterface $loader; - /** * The callable creating string values for each choice. * @@ -43,11 +41,13 @@ class LazyChoiceList implements ChoiceListInterface * The callable receives the choice as first and the array key as the second * argument. * - * @param callable|null $value The callable generating the choice values + * @param callable|null $value The callable creating string values for each choice. + * If null, choices are cast to strings. */ - public function __construct(ChoiceLoaderInterface $loader, ?callable $value = null) - { - $this->loader = $loader; + public function __construct( + private ChoiceLoaderInterface $loader, + ?callable $value = null, + ) { $this->value = null === $value ? null : $value(...); } diff --git a/ChoiceList/Loader/LazyChoiceLoader.php b/ChoiceList/Loader/LazyChoiceLoader.php new file mode 100644 index 000000000..03451be36 --- /dev/null +++ b/ChoiceList/Loader/LazyChoiceLoader.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Loader; + +use Symfony\Component\Form\ChoiceList\ArrayChoiceList; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; + +/** + * A choice loader that loads its choices and values lazily, only when necessary. + * + * @author Yonel Ceruto + */ +class LazyChoiceLoader implements ChoiceLoaderInterface +{ + private ?ChoiceListInterface $choiceList = null; + + public function __construct( + private readonly ChoiceLoaderInterface $loader, + ) { + } + + public function loadChoiceList(?callable $value = null): ChoiceListInterface + { + return $this->choiceList ??= new ArrayChoiceList([], $value); + } + + public function loadChoicesForValues(array $values, ?callable $value = null): array + { + $choices = $this->loader->loadChoicesForValues($values, $value); + $this->choiceList = new ArrayChoiceList($choices, $value); + + return $choices; + } + + public function loadValuesForChoices(array $choices, ?callable $value = null): array + { + $values = $this->loader->loadValuesForChoices($choices, $value); + + if ($this->choiceList?->getValuesForChoices($choices) !== $values) { + $this->loadChoicesForValues($values, $value); + } + + return $values; + } +} diff --git a/ChoiceList/View/ChoiceGroupView.php b/ChoiceList/View/ChoiceGroupView.php index 562515012..923fab224 100644 --- a/ChoiceList/View/ChoiceGroupView.php +++ b/ChoiceList/View/ChoiceGroupView.php @@ -20,18 +20,15 @@ */ class ChoiceGroupView implements \IteratorAggregate { - public string $label; - public array $choices; - /** * Creates a new choice group view. * * @param array $choices the choice views in the group */ - public function __construct(string $label, array $choices = []) - { - $this->label = $label; - $this->choices = $choices; + public function __construct( + public string $label, + public array $choices = [], + ) { } /** diff --git a/ChoiceList/View/ChoiceListView.php b/ChoiceList/View/ChoiceListView.php index 15afc4a8a..f64d10e76 100644 --- a/ChoiceList/View/ChoiceListView.php +++ b/ChoiceList/View/ChoiceListView.php @@ -22,19 +22,16 @@ */ class ChoiceListView { - public array $choices; - public array $preferredChoices; - /** * Creates a new choice list view. * * @param array $choices The choice views * @param array $preferredChoices the preferred choice views */ - public function __construct(array $choices = [], array $preferredChoices = []) - { - $this->choices = $choices; - $this->preferredChoices = $preferredChoices; + public function __construct( + public array $choices = [], + public array $preferredChoices = [], + ) { } /** diff --git a/ChoiceList/View/ChoiceView.php b/ChoiceList/View/ChoiceView.php index 52587fd36..d59a7c947 100644 --- a/ChoiceList/View/ChoiceView.php +++ b/ChoiceList/View/ChoiceView.php @@ -20,20 +20,6 @@ */ class ChoiceView { - public string|TranslatableInterface|false $label; - public string $value; - public mixed $data; - - /** - * Additional attributes for the HTML tag. - */ - public array $attr; - - /** - * Additional parameters used to translate the label. - */ - public array $labelTranslationParameters; - /** * Creates a new choice view. * @@ -43,12 +29,12 @@ class ChoiceView * @param array $attr Additional attributes for the HTML tag * @param array $labelTranslationParameters Additional parameters used to translate the label */ - public function __construct(mixed $data, string $value, string|TranslatableInterface|false $label, array $attr = [], array $labelTranslationParameters = []) - { - $this->data = $data; - $this->value = $value; - $this->label = $label; - $this->attr = $attr; - $this->labelTranslationParameters = $labelTranslationParameters; + public function __construct( + public mixed $data, + public string $value, + public string|TranslatableInterface|false $label, + public array $attr = [], + public array $labelTranslationParameters = [], + ) { } } diff --git a/Command/DebugCommand.php b/Command/DebugCommand.php index b1cabc799..91db6f1a9 100644 --- a/Command/DebugCommand.php +++ b/Command/DebugCommand.php @@ -35,23 +35,15 @@ #[AsCommand(name: 'debug:form', description: 'Display form type information')] class DebugCommand extends Command { - private FormRegistryInterface $formRegistry; - private array $namespaces; - private array $types; - private array $extensions; - private array $guessers; - private ?FileLinkFormatter $fileLinkFormatter; - - public function __construct(FormRegistryInterface $formRegistry, array $namespaces = ['Symfony\Component\Form\Extension\Core\Type'], array $types = [], array $extensions = [], array $guessers = [], ?FileLinkFormatter $fileLinkFormatter = null) - { + public function __construct( + private FormRegistryInterface $formRegistry, + private array $namespaces = ['Symfony\Component\Form\Extension\Core\Type'], + private array $types = [], + private array $extensions = [], + private array $guessers = [], + private ?FileLinkFormatter $fileLinkFormatter = null, + ) { parent::__construct(); - - $this->formRegistry = $formRegistry; - $this->namespaces = $namespaces; - $this->types = $types; - $this->extensions = $extensions; - $this->guessers = $guessers; - $this->fileLinkFormatter = $fileLinkFormatter; } protected function configure(): void @@ -61,7 +53,7 @@ protected function configure(): void new InputArgument('class', InputArgument::OPTIONAL, 'The form type class'), new InputArgument('option', InputArgument::OPTIONAL, 'The form type option'), new InputOption('show-deprecated', null, InputOption::VALUE_NONE, 'Display deprecated options in form types'), - new InputOption('format', null, InputOption::VALUE_REQUIRED, sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions())), 'txt'), + new InputOption('format', null, InputOption::VALUE_REQUIRED, \sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions())), 'txt'), ]) ->setHelp(<<<'EOF' The %command.name% command displays information about form types. @@ -122,7 +114,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $object = $resolvedType->getOptionsResolver(); if (!$object->isDefined($option)) { - $message = sprintf('Option "%s" is not defined in "%s".', $option, $resolvedType->getInnerType()::class); + $message = \sprintf('Option "%s" is not defined in "%s".', $option, $resolvedType->getInnerType()::class); if ($alternatives = $this->findAlternatives($option, $object->getDefinedOptions())) { if (1 === \count($alternatives)) { @@ -156,7 +148,7 @@ private function getFqcnTypeClass(InputInterface $input, SymfonyStyle $io, strin $classes = $this->getFqcnTypeClasses($shortClassName); if (0 === $count = \count($classes)) { - $message = sprintf("Could not find type \"%s\" into the following namespaces:\n %s", $shortClassName, implode("\n ", $this->namespaces)); + $message = \sprintf("Could not find type \"%s\" into the following namespaces:\n %s", $shortClassName, implode("\n ", $this->namespaces)); $allTypes = array_merge($this->getCoreTypes(), $this->types); if ($alternatives = $this->findAlternatives($shortClassName, $allTypes)) { @@ -174,10 +166,10 @@ private function getFqcnTypeClass(InputInterface $input, SymfonyStyle $io, strin return $classes[0]; } if (!$input->isInteractive()) { - throw new InvalidArgumentException(sprintf("The type \"%s\" is ambiguous.\n\nDid you mean one of these?\n %s.", $shortClassName, implode("\n ", $classes))); + throw new InvalidArgumentException(\sprintf("The type \"%s\" is ambiguous.\n\nDid you mean one of these?\n %s.", $shortClassName, implode("\n ", $classes))); } - return $io->choice(sprintf("The type \"%s\" is ambiguous.\n\nSelect one of the following form types to display its information:", $shortClassName), $classes, $classes[0]); + return $io->choice(\sprintf("The type \"%s\" is ambiguous.\n\nSelect one of the following form types to display its information:", $shortClassName), $classes, $classes[0]); } private function getFqcnTypeClasses(string $shortClassName): array @@ -280,6 +272,7 @@ private function completeOptions(string $class, CompletionSuggestions $suggestio $suggestions->suggestValues($resolvedType->getOptionsResolver()->getDefinedOptions()); } + /** @return string[] */ private function getAvailableFormatOptions(): array { return (new DescriptorHelper())->getFormats(); diff --git a/Console/Descriptor/Descriptor.php b/Console/Descriptor/Descriptor.php index b8d0399ee..c910acdf4 100644 --- a/Console/Descriptor/Descriptor.php +++ b/Console/Descriptor/Descriptor.php @@ -46,7 +46,7 @@ public function describe(OutputInterface $output, ?object $object, array $option null === $object => $this->describeDefaults($options), $object instanceof ResolvedFormTypeInterface => $this->describeResolvedFormType($object, $options), $object instanceof OptionsResolver => $this->describeOption($object, $options), - default => throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_debug_type($object))), + default => throw new \InvalidArgumentException(\sprintf('Object of type "%s" is not describable.', get_debug_type($object))), }; } @@ -128,7 +128,7 @@ protected function getOptionDefinition(OptionsResolver $optionsResolver, string } } - if (isset($definition['deprecation']) && isset($definition['deprecation']['message']) && \is_string($definition['deprecation']['message'])) { + if (isset($definition['deprecation']['message']) && \is_string($definition['deprecation']['message'])) { $definition['deprecationMessage'] = strtr($definition['deprecation']['message'], ['%name%' => $option]); $definition['deprecationPackage'] = $definition['deprecation']['package']; $definition['deprecationVersion'] = $definition['deprecation']['version']; diff --git a/Console/Descriptor/TextDescriptor.php b/Console/Descriptor/TextDescriptor.php index 7b723a0af..630b87253 100644 --- a/Console/Descriptor/TextDescriptor.php +++ b/Console/Descriptor/TextDescriptor.php @@ -80,7 +80,7 @@ protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedF 'extension' => 'Extension options', ], $formOptions); - $this->output->title(sprintf('%s (Block prefix: "%s")', $resolvedFormType->getInnerType()::class, $resolvedFormType->getInnerType()->getBlockPrefix())); + $this->output->title(\sprintf('%s (Block prefix: "%s")', $resolvedFormType->getInnerType()::class, $resolvedFormType->getInnerType()->getBlockPrefix())); if ($formOptions) { $this->output->table($tableHeaders, $this->buildTableRows($tableHeaders, $formOptions)); @@ -131,7 +131,7 @@ protected function describeOption(OptionsResolver $optionsResolver, array $optio } array_pop($rows); - $this->output->title(sprintf('%s (%s)', $options['type']::class, $options['option'])); + $this->output->title(\sprintf('%s (%s)', $options['type']::class, $options['option'])); $this->output->table([], $rows); } @@ -172,7 +172,7 @@ private function normalizeAndSortOptionsColumns(array $options): array } else { $options[$group][] = null; } - $options[$group][] = sprintf('%s', (new \ReflectionClass($class))->getShortName()); + $options[$group][] = \sprintf('%s', (new \ReflectionClass($class))->getShortName()); $options[$group][] = new TableSeparator(); sort($opt); @@ -196,7 +196,7 @@ private function formatClassLink(string $class, ?string $text = null): string return $text; } - return sprintf('%s', $fileLink, $text); + return \sprintf('%s', $fileLink, $text); } private function getFileLink(string $class): string diff --git a/DependencyInjection/FormPass.php b/DependencyInjection/FormPass.php index 408731163..bec1782d4 100644 --- a/DependencyInjection/FormPass.php +++ b/DependencyInjection/FormPass.php @@ -47,6 +47,7 @@ private function processFormTypes(ContainerBuilder $container): Reference // Get service locator argument $servicesMap = []; $namespaces = ['Symfony\Component\Form\Extension\Core\Type' => true]; + $csrfTokenIds = []; // Builds an array with fully-qualified type class names as keys and service IDs as values foreach ($container->findTaggedServiceIds('form.type', true) as $serviceId => $tag) { @@ -54,6 +55,10 @@ private function processFormTypes(ContainerBuilder $container): Reference $serviceDefinition = $container->getDefinition($serviceId); $servicesMap[$formType = $serviceDefinition->getClass()] = new Reference($serviceId); $namespaces[substr($formType, 0, strrpos($formType, '\\'))] = true; + + if (isset($tag[0]['csrf_token_id'])) { + $csrfTokenIds[$formType] = $tag[0]['csrf_token_id']; + } } if ($container->hasDefinition('console.command.form_debug')) { @@ -62,6 +67,14 @@ private function processFormTypes(ContainerBuilder $container): Reference $commandDefinition->setArgument(2, array_keys($servicesMap)); } + if ($csrfTokenIds && $container->hasDefinition('form.type_extension.csrf')) { + $csrfExtension = $container->getDefinition('form.type_extension.csrf'); + + if (8 <= \count($csrfExtension->getArguments())) { + $csrfExtension->replaceArgument(7, $csrfTokenIds); + } + } + return ServiceLocatorTagPass::register($container, $servicesMap); } @@ -89,7 +102,7 @@ private function processFormTypeExtensions(ContainerBuilder $container): array } if (!$extendsTypes) { - throw new InvalidArgumentException(sprintf('The getExtendedTypes() method for service "%s" does not return any extended types.', $serviceId)); + throw new InvalidArgumentException(\sprintf('The getExtendedTypes() method for service "%s" does not return any extended types.', $serviceId)); } } } diff --git a/Exception/UnexpectedTypeException.php b/Exception/UnexpectedTypeException.php index 7a4dc295c..223061b77 100644 --- a/Exception/UnexpectedTypeException.php +++ b/Exception/UnexpectedTypeException.php @@ -15,6 +15,6 @@ class UnexpectedTypeException extends InvalidArgumentException { public function __construct(mixed $value, string $expectedType) { - parent::__construct(sprintf('Expected argument of type "%s", "%s" given', $expectedType, get_debug_type($value))); + parent::__construct(\sprintf('Expected argument of type "%s", "%s" given', $expectedType, get_debug_type($value))); } } diff --git a/Extension/Core/CoreExtension.php b/Extension/Core/CoreExtension.php index d6c3ff080..1640ed052 100644 --- a/Extension/Core/CoreExtension.php +++ b/Extension/Core/CoreExtension.php @@ -30,13 +30,14 @@ class CoreExtension extends AbstractExtension { private PropertyAccessorInterface $propertyAccessor; private ChoiceListFactoryInterface $choiceListFactory; - private ?TranslatorInterface $translator; - public function __construct(?PropertyAccessorInterface $propertyAccessor = null, ?ChoiceListFactoryInterface $choiceListFactory = null, ?TranslatorInterface $translator = null) - { + public function __construct( + ?PropertyAccessorInterface $propertyAccessor = null, + ?ChoiceListFactoryInterface $choiceListFactory = null, + private ?TranslatorInterface $translator = null, + ) { $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); $this->choiceListFactory = $choiceListFactory ?? new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor)); - $this->translator = $translator; } protected function loadTypes(): array diff --git a/Extension/Core/DataAccessor/ChainAccessor.php b/Extension/Core/DataAccessor/ChainAccessor.php index ac600f16f..e7b9da09e 100644 --- a/Extension/Core/DataAccessor/ChainAccessor.php +++ b/Extension/Core/DataAccessor/ChainAccessor.php @@ -20,14 +20,12 @@ */ class ChainAccessor implements DataAccessorInterface { - private iterable $accessors; - /** * @param DataAccessorInterface[]|iterable $accessors */ - public function __construct(iterable $accessors) - { - $this->accessors = $accessors; + public function __construct( + private iterable $accessors, + ) { } public function getValue(object|array $data, FormInterface $form): mixed diff --git a/Extension/Core/DataAccessor/PropertyPathAccessor.php b/Extension/Core/DataAccessor/PropertyPathAccessor.php index f5c25dfc1..33d01fd4f 100644 --- a/Extension/Core/DataAccessor/PropertyPathAccessor.php +++ b/Extension/Core/DataAccessor/PropertyPathAccessor.php @@ -18,6 +18,7 @@ use Symfony\Component\Form\FormInterface; use Symfony\Component\PropertyAccess\Exception\AccessException as PropertyAccessException; use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; +use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; @@ -72,7 +73,11 @@ public function setValue(object|array &$data, mixed $value, FormInterface $form) // If the data is identical to the value in $data, we are // dealing with a reference if (!\is_object($data) || !$form->getConfig()->getByReference() || $value !== $getValue()) { - $this->propertyAccessor->setValue($data, $propertyPath, $value); + try { + $this->propertyAccessor->setValue($data, $propertyPath, $value); + } catch (NoSuchPropertyException $e) { + throw new NoSuchPropertyException($e->getMessage().' Make the property public, add a setter, or set the "mapped" field option in the form type to be false.', 0, $e); + } } } diff --git a/Extension/Core/DataTransformer/ArrayToPartsTransformer.php b/Extension/Core/DataTransformer/ArrayToPartsTransformer.php index 9256c0a09..92f59c906 100644 --- a/Extension/Core/DataTransformer/ArrayToPartsTransformer.php +++ b/Extension/Core/DataTransformer/ArrayToPartsTransformer.php @@ -21,11 +21,9 @@ */ class ArrayToPartsTransformer implements DataTransformerInterface { - private array $partMapping; - - public function __construct(array $partMapping) - { - $this->partMapping = $partMapping; + public function __construct( + private array $partMapping, + ) { } public function transform(mixed $array): mixed @@ -74,7 +72,7 @@ public function reverseTransform(mixed $array): mixed return null; } - throw new TransformationFailedException(sprintf('The keys "%s" should not be empty.', implode('", "', $emptyKeys))); + throw new TransformationFailedException(\sprintf('The keys "%s" should not be empty.', implode('", "', $emptyKeys))); } return $result; diff --git a/Extension/Core/DataTransformer/BaseDateTimeTransformer.php b/Extension/Core/DataTransformer/BaseDateTimeTransformer.php index 8d311b3f8..ca85fd44d 100644 --- a/Extension/Core/DataTransformer/BaseDateTimeTransformer.php +++ b/Extension/Core/DataTransformer/BaseDateTimeTransformer.php @@ -47,13 +47,13 @@ public function __construct(?string $inputTimezone = null, ?string $outputTimezo try { new \DateTimeZone($this->inputTimezone); } catch (\Exception $e) { - throw new InvalidArgumentException(sprintf('Input timezone is invalid: "%s".', $this->inputTimezone), $e->getCode(), $e); + throw new InvalidArgumentException(\sprintf('Input timezone is invalid: "%s".', $this->inputTimezone), $e->getCode(), $e); } try { new \DateTimeZone($this->outputTimezone); } catch (\Exception $e) { - throw new InvalidArgumentException(sprintf('Output timezone is invalid: "%s".', $this->outputTimezone), $e->getCode(), $e); + throw new InvalidArgumentException(\sprintf('Output timezone is invalid: "%s".', $this->outputTimezone), $e->getCode(), $e); } } } diff --git a/Extension/Core/DataTransformer/BooleanToStringTransformer.php b/Extension/Core/DataTransformer/BooleanToStringTransformer.php index e91bdb4db..7ef84bb44 100644 --- a/Extension/Core/DataTransformer/BooleanToStringTransformer.php +++ b/Extension/Core/DataTransformer/BooleanToStringTransformer.php @@ -25,17 +25,13 @@ */ class BooleanToStringTransformer implements DataTransformerInterface { - private string $trueValue; - - private array $falseValues; - /** * @param string $trueValue The value emitted upon transform if the input is true */ - public function __construct(string $trueValue, array $falseValues = [null]) - { - $this->trueValue = $trueValue; - $this->falseValues = $falseValues; + public function __construct( + private string $trueValue, + private array $falseValues = [null], + ) { if (\in_array($this->trueValue, $this->falseValues, true)) { throw new InvalidArgumentException('The specified "true" value is contained in the false-values.'); } diff --git a/Extension/Core/DataTransformer/ChoiceToValueTransformer.php b/Extension/Core/DataTransformer/ChoiceToValueTransformer.php index daec9d719..e75bc31de 100644 --- a/Extension/Core/DataTransformer/ChoiceToValueTransformer.php +++ b/Extension/Core/DataTransformer/ChoiceToValueTransformer.php @@ -22,11 +22,9 @@ */ class ChoiceToValueTransformer implements DataTransformerInterface { - private ChoiceListInterface $choiceList; - - public function __construct(ChoiceListInterface $choiceList) - { - $this->choiceList = $choiceList; + public function __construct( + private ChoiceListInterface $choiceList, + ) { } public function transform(mixed $choice): mixed @@ -47,7 +45,7 @@ public function reverseTransform(mixed $value): mixed return null; } - throw new TransformationFailedException(sprintf('The choice "%s" does not exist or is not unique.', $value)); + throw new TransformationFailedException(\sprintf('The choice "%s" does not exist or is not unique.', $value)); } return current($choices); diff --git a/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php b/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php index f284ff34f..aa2233854 100644 --- a/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php +++ b/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php @@ -22,11 +22,9 @@ */ class ChoicesToValuesTransformer implements DataTransformerInterface { - private ChoiceListInterface $choiceList; - - public function __construct(ChoiceListInterface $choiceList) - { - $this->choiceList = $choiceList; + public function __construct( + private ChoiceListInterface $choiceList, + ) { } /** diff --git a/Extension/Core/DataTransformer/DataTransformerChain.php b/Extension/Core/DataTransformer/DataTransformerChain.php index ec5def469..e34be7455 100644 --- a/Extension/Core/DataTransformer/DataTransformerChain.php +++ b/Extension/Core/DataTransformer/DataTransformerChain.php @@ -21,16 +21,14 @@ */ class DataTransformerChain implements DataTransformerInterface { - protected array $transformers; - /** * Uses the given value transformers to transform values. * * @param DataTransformerInterface[] $transformers */ - public function __construct(array $transformers) - { - $this->transformers = $transformers; + public function __construct( + protected array $transformers, + ) { } /** diff --git a/Extension/Core/DataTransformer/DateIntervalToArrayTransformer.php b/Extension/Core/DataTransformer/DateIntervalToArrayTransformer.php index 7018749d2..8591db191 100644 --- a/Extension/Core/DataTransformer/DateIntervalToArrayTransformer.php +++ b/Extension/Core/DataTransformer/DateIntervalToArrayTransformer.php @@ -42,16 +42,16 @@ class DateIntervalToArrayTransformer implements DataTransformerInterface self::INVERT => 'r', ]; private array $fields; - private bool $pad; /** * @param string[]|null $fields The date fields * @param bool $pad Whether to use padding */ - public function __construct(?array $fields = null, bool $pad = false) - { + public function __construct( + ?array $fields = null, + private bool $pad = false, + ) { $this->fields = $fields ?? ['years', 'months', 'days', 'hours', 'minutes', 'seconds', 'invert']; - $this->pad = $pad; } /** @@ -93,9 +93,8 @@ public function transform(mixed $dateInterval): array } } $result['invert'] = '-' === $result['invert']; - $result = array_intersect_key($result, array_flip($this->fields)); - return $result; + return array_intersect_key($result, array_flip($this->fields)); } /** @@ -124,29 +123,29 @@ public function reverseTransform(mixed $value): ?\DateInterval } } if (\count($emptyFields) > 0) { - throw new TransformationFailedException(sprintf('The fields "%s" should not be empty.', implode('", "', $emptyFields))); + throw new TransformationFailedException(\sprintf('The fields "%s" should not be empty.', implode('", "', $emptyFields))); } if (isset($value['invert']) && !\is_bool($value['invert'])) { throw new TransformationFailedException('The value of "invert" must be boolean.'); } foreach (self::AVAILABLE_FIELDS as $field => $char) { if ('invert' !== $field && isset($value[$field]) && !ctype_digit((string) $value[$field])) { - throw new TransformationFailedException(sprintf('This amount of "%s" is invalid.', $field)); + throw new TransformationFailedException(\sprintf('This amount of "%s" is invalid.', $field)); } } try { if (!empty($value['weeks'])) { - $interval = sprintf( + $interval = \sprintf( 'P%sY%sM%sWT%sH%sM%sS', empty($value['years']) ? '0' : $value['years'], empty($value['months']) ? '0' : $value['months'], - empty($value['weeks']) ? '0' : $value['weeks'], + $value['weeks'], empty($value['hours']) ? '0' : $value['hours'], empty($value['minutes']) ? '0' : $value['minutes'], empty($value['seconds']) ? '0' : $value['seconds'] ); } else { - $interval = sprintf( + $interval = \sprintf( 'P%sY%sM%sDT%sH%sM%sS', empty($value['years']) ? '0' : $value['years'], empty($value['months']) ? '0' : $value['months'], diff --git a/Extension/Core/DataTransformer/DateIntervalToStringTransformer.php b/Extension/Core/DataTransformer/DateIntervalToStringTransformer.php index 4160f8f34..527836bf5 100644 --- a/Extension/Core/DataTransformer/DateIntervalToStringTransformer.php +++ b/Extension/Core/DataTransformer/DateIntervalToStringTransformer.php @@ -24,8 +24,6 @@ */ class DateIntervalToStringTransformer implements DataTransformerInterface { - private string $format; - /** * Transforms a \DateInterval instance to a string. * @@ -33,9 +31,9 @@ class DateIntervalToStringTransformer implements DataTransformerInterface * * @param string $format The date format */ - public function __construct(string $format = 'P%yY%mM%dDT%hH%iM%sS') - { - $this->format = $format; + public function __construct( + private string $format = 'P%yY%mM%dDT%hH%iM%sS', + ) { } /** @@ -81,7 +79,7 @@ public function reverseTransform(mixed $value): ?\DateInterval } $valuePattern = '/^'.preg_replace('/%([yYmMdDhHiIsSwW])(\w)/', '(?P<$1>\d+)$2', $this->format).'$/'; if (!preg_match($valuePattern, $value)) { - throw new TransformationFailedException(sprintf('Value "%s" contains intervals not accepted by format "%s".', $value, $this->format)); + throw new TransformationFailedException(\sprintf('Value "%s" contains intervals not accepted by format "%s".', $value, $this->format)); } try { $dateInterval = new \DateInterval($value); diff --git a/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php b/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php index c40e176cb..06d24cf8e 100644 --- a/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php +++ b/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php @@ -23,7 +23,6 @@ */ class DateTimeToArrayTransformer extends BaseDateTimeTransformer { - private bool $pad; private array $fields; private \DateTimeInterface $referenceDate; @@ -33,12 +32,16 @@ class DateTimeToArrayTransformer extends BaseDateTimeTransformer * @param string[]|null $fields The date fields * @param bool $pad Whether to use padding */ - public function __construct(?string $inputTimezone = null, ?string $outputTimezone = null, ?array $fields = null, bool $pad = false, ?\DateTimeInterface $referenceDate = null) - { + public function __construct( + ?string $inputTimezone = null, + ?string $outputTimezone = null, + ?array $fields = null, + private bool $pad = false, + ?\DateTimeInterface $referenceDate = null, + ) { parent::__construct($inputTimezone, $outputTimezone); $this->fields = $fields ?? ['year', 'month', 'day', 'hour', 'minute', 'second']; - $this->pad = $pad; $this->referenceDate = $referenceDate ?? new \DateTimeImmutable('1970-01-01 00:00:00'); } @@ -123,7 +126,7 @@ public function reverseTransform(mixed $value): ?\DateTime } if (\count($emptyFields) > 0) { - throw new TransformationFailedException(sprintf('The fields "%s" should not be empty.', implode('", "', $emptyFields))); + throw new TransformationFailedException(\sprintf('The fields "%s" should not be empty.', implode('", "', $emptyFields))); } if (isset($value['month']) && !ctype_digit((string) $value['month'])) { @@ -155,7 +158,7 @@ public function reverseTransform(mixed $value): ?\DateTime } try { - $dateTime = new \DateTime(sprintf( + $dateTime = new \DateTime(\sprintf( '%s-%s-%s %s:%s:%s', empty($value['year']) ? $this->referenceDate->format('Y') : $value['year'], empty($value['month']) ? $this->referenceDate->format('m') : $value['month'], diff --git a/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformer.php b/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformer.php index 855b22a49..dbeed8e49 100644 --- a/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformer.php +++ b/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformer.php @@ -85,7 +85,7 @@ public function reverseTransform(mixed $dateTimeLocal): ?\DateTime // to maintain backwards compatibility we do not strictly validate the submitted date // see https://github.com/symfony/symfony/issues/28699 if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})[T ]\d{2}:\d{2}(?::\d{2})?/', $dateTimeLocal, $matches)) { - throw new TransformationFailedException(sprintf('The date "%s" is not a valid date.', $dateTimeLocal)); + throw new TransformationFailedException(\sprintf('The date "%s" is not a valid date.', $dateTimeLocal)); } try { @@ -99,7 +99,7 @@ public function reverseTransform(mixed $dateTimeLocal): ?\DateTime } if (!checkdate($matches[2], $matches[3], $matches[1])) { - throw new TransformationFailedException(sprintf('The date "%s-%s-%s" is not a valid date.', $matches[1], $matches[2], $matches[3])); + throw new TransformationFailedException(\sprintf('The date "%s-%s-%s" is not a valid date.', $matches[1], $matches[2], $matches[3])); } return $dateTime; diff --git a/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformer.php b/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformer.php index 7bb79f3a1..426c4bf89 100644 --- a/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformer.php +++ b/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformer.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Form\Extension\Core\DataTransformer; +use Symfony\Component\Form\Exception\InvalidArgumentException; use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\Exception\UnexpectedTypeException; @@ -26,23 +27,27 @@ class DateTimeToLocalizedStringTransformer extends BaseDateTimeTransformer { private int $dateFormat; private int $timeFormat; - private ?string $pattern; - private int $calendar; /** * @see BaseDateTimeTransformer::formats for available format options * - * @param string|null $inputTimezone The name of the input timezone - * @param string|null $outputTimezone The name of the output timezone - * @param int|null $dateFormat The date format - * @param int|null $timeFormat The time format - * @param int $calendar One of the \IntlDateFormatter calendar constants - * @param string|null $pattern A pattern to pass to \IntlDateFormatter + * @param string|null $inputTimezone The name of the input timezone + * @param string|null $outputTimezone The name of the output timezone + * @param int|null $dateFormat The date format + * @param int|null $timeFormat The time format + * @param int|\IntlCalendar $calendar One of the \IntlDateFormatter calendar constants or an \IntlCalendar instance + * @param string|null $pattern A pattern to pass to \IntlDateFormatter * * @throws UnexpectedTypeException If a format is not supported or if a timezone is not a string */ - public function __construct(?string $inputTimezone = null, ?string $outputTimezone = null, ?int $dateFormat = null, ?int $timeFormat = null, int $calendar = \IntlDateFormatter::GREGORIAN, ?string $pattern = null) - { + public function __construct( + ?string $inputTimezone = null, + ?string $outputTimezone = null, + ?int $dateFormat = null, + ?int $timeFormat = null, + private int|\IntlCalendar $calendar = \IntlDateFormatter::GREGORIAN, + private ?string $pattern = null, + ) { parent::__construct($inputTimezone, $outputTimezone); $dateFormat ??= \IntlDateFormatter::MEDIUM; @@ -56,10 +61,12 @@ public function __construct(?string $inputTimezone = null, ?string $outputTimezo throw new UnexpectedTypeException($timeFormat, implode('", "', self::$formats)); } + if (\is_int($calendar) && !\in_array($calendar, [\IntlDateFormatter::GREGORIAN, \IntlDateFormatter::TRADITIONAL], true)) { + throw new InvalidArgumentException('The "calendar" option should be either an \IntlDateFormatter constant or an \IntlCalendar instance.'); + } + $this->dateFormat = $dateFormat; $this->timeFormat = $timeFormat; - $this->calendar = $calendar; - $this->pattern = $pattern; } /** @@ -126,7 +133,7 @@ public function reverseTransform(mixed $value): ?\DateTime } elseif (false === $timestamp) { // the value couldn't be parsed but the Intl extension didn't report an error code, this // could be the case when the Intl polyfill is used which always returns 0 as the error code - throw new TransformationFailedException(sprintf('"%s" could not be parsed as a date.', $value)); + throw new TransformationFailedException(\sprintf('"%s" could not be parsed as a date.', $value)); } try { @@ -135,7 +142,7 @@ public function reverseTransform(mixed $value): ?\DateTime $dateTime = new \DateTime(gmdate('Y-m-d', $timestamp), new \DateTimeZone($this->outputTimezone)); } else { // read timestamp into DateTime object - the formatter delivers a timestamp - $dateTime = new \DateTime(sprintf('@%s', $timestamp)); + $dateTime = new \DateTime(\sprintf('@%s', $timestamp)); } // set timezone separately, as it would be ignored if set via the constructor, // see https://php.net/datetime.construct @@ -155,8 +162,6 @@ public function reverseTransform(mixed $value): ?\DateTime * Returns a preconfigured IntlDateFormatter instance. * * @param bool $ignoreTimezone Use UTC regardless of the configured timezone - * - * @throws TransformationFailedException in case the date formatter cannot be constructed */ protected function getIntlDateFormatter(bool $ignoreTimezone = false): \IntlDateFormatter { @@ -168,12 +173,6 @@ protected function getIntlDateFormatter(bool $ignoreTimezone = false): \IntlDate $pattern = $this->pattern; $intlDateFormatter = new \IntlDateFormatter(\Locale::getDefault(), $dateFormat, $timeFormat, $timezone, $calendar, $pattern ?? ''); - - // new \intlDateFormatter may return null instead of false in case of failure, see https://bugs.php.net/66323 - if (!$intlDateFormatter) { - throw new TransformationFailedException(intl_get_error_message(), intl_get_error_code()); - } - $intlDateFormatter->setLenient(false); return $intlDateFormatter; diff --git a/Extension/Core/DataTransformer/DateTimeToRfc3339Transformer.php b/Extension/Core/DataTransformer/DateTimeToRfc3339Transformer.php index 41e63e57c..d32b3eae2 100644 --- a/Extension/Core/DataTransformer/DateTimeToRfc3339Transformer.php +++ b/Extension/Core/DataTransformer/DateTimeToRfc3339Transformer.php @@ -64,7 +64,7 @@ public function reverseTransform(mixed $rfc3339): ?\DateTime } if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})T\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|(?:(?:\+|-)\d{2}:\d{2}))$/', $rfc3339, $matches)) { - throw new TransformationFailedException(sprintf('The date "%s" is not a valid date.', $rfc3339)); + throw new TransformationFailedException(\sprintf('The date "%s" is not a valid date.', $rfc3339)); } try { @@ -78,7 +78,7 @@ public function reverseTransform(mixed $rfc3339): ?\DateTime } if (!checkdate($matches[2], $matches[3], $matches[1])) { - throw new TransformationFailedException(sprintf('The date "%s-%s-%s" is not a valid date.', $matches[1], $matches[2], $matches[3])); + throw new TransformationFailedException(\sprintf('The date "%s-%s-%s" is not a valid date.', $matches[1], $matches[2], $matches[3])); } return $dateTime; diff --git a/Extension/Core/DataTransformer/DateTimeToStringTransformer.php b/Extension/Core/DataTransformer/DateTimeToStringTransformer.php index 96bdc7c0d..135361420 100644 --- a/Extension/Core/DataTransformer/DateTimeToStringTransformer.php +++ b/Extension/Core/DataTransformer/DateTimeToStringTransformer.php @@ -101,7 +101,7 @@ public function transform(mixed $dateTime): string */ public function reverseTransform(mixed $value): ?\DateTime { - if (empty($value)) { + if (!$value) { return null; } diff --git a/Extension/Core/DataTransformer/DateTimeZoneToStringTransformer.php b/Extension/Core/DataTransformer/DateTimeZoneToStringTransformer.php index f7bda1751..50767f7a0 100644 --- a/Extension/Core/DataTransformer/DateTimeZoneToStringTransformer.php +++ b/Extension/Core/DataTransformer/DateTimeZoneToStringTransformer.php @@ -23,11 +23,9 @@ */ class DateTimeZoneToStringTransformer implements DataTransformerInterface { - private bool $multiple; - - public function __construct(bool $multiple = false) - { - $this->multiple = $multiple; + public function __construct( + private bool $multiple = false, + ) { } public function transform(mixed $dateTimeZone): mixed diff --git a/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php b/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php index eb5a2d6ff..d83e31b42 100644 --- a/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php +++ b/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php @@ -38,7 +38,7 @@ public function reverseTransform(mixed $value): int|float|null $decimalSeparator = $this->getNumberFormatter()->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL); if (\is_string($value) && str_contains($value, $decimalSeparator)) { - throw new TransformationFailedException(sprintf('The value "%s" is not a valid integer.', $value)); + throw new TransformationFailedException(\sprintf('The value "%s" is not a valid integer.', $value)); } $result = parent::reverseTransform($value); diff --git a/Extension/Core/DataTransformer/IntlTimeZoneToStringTransformer.php b/Extension/Core/DataTransformer/IntlTimeZoneToStringTransformer.php index d379164a7..19626e49c 100644 --- a/Extension/Core/DataTransformer/IntlTimeZoneToStringTransformer.php +++ b/Extension/Core/DataTransformer/IntlTimeZoneToStringTransformer.php @@ -23,11 +23,9 @@ */ class IntlTimeZoneToStringTransformer implements DataTransformerInterface { - private bool $multiple; - - public function __construct(bool $multiple = false) - { - $this->multiple = $multiple; + public function __construct( + private bool $multiple = false, + ) { } public function transform(mixed $intlTimeZone): mixed @@ -72,7 +70,7 @@ public function reverseTransform(mixed $value): mixed $intlTimeZone = \IntlTimeZone::createTimeZone($value); if ('Etc/Unknown' === $intlTimeZone->getID()) { - throw new TransformationFailedException(sprintf('Unknown timezone identifier "%s".', $value)); + throw new TransformationFailedException(\sprintf('Unknown timezone identifier "%s".', $value)); } return $intlTimeZone; diff --git a/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php b/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php index 7a8aacac6..b03f8da44 100644 --- a/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php +++ b/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php @@ -23,8 +23,14 @@ class MoneyToLocalizedStringTransformer extends NumberToLocalizedStringTransform { private int $divisor; - public function __construct(?int $scale = 2, ?bool $grouping = true, ?int $roundingMode = \NumberFormatter::ROUND_HALFUP, ?int $divisor = 1, ?string $locale = null) - { + public function __construct( + ?int $scale = 2, + ?bool $grouping = true, + ?int $roundingMode = \NumberFormatter::ROUND_HALFUP, + ?int $divisor = 1, + ?string $locale = null, + private readonly string $input = 'float', + ) { parent::__construct($scale ?? 2, $grouping ?? true, $roundingMode, $locale); $this->divisor = $divisor ?? 1; @@ -61,8 +67,18 @@ public function transform(mixed $value): string public function reverseTransform(mixed $value): int|float|null { $value = parent::reverseTransform($value); - if (null !== $value && 1 !== $this->divisor) { - $value = (float) (string) ($value * $this->divisor); + if (null !== $value) { + $value = (string) ($value * $this->divisor); + + if ('float' === $this->input) { + return (float) $value; + } + + if ($value > \PHP_INT_MAX || $value < \PHP_INT_MIN) { + throw new TransformationFailedException(\sprintf('Cannot cast "%s" to an integer. Try setting the input to "float" instead.', $value)); + } + + $value = (int) $value; } return $value; diff --git a/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php b/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php index a3c9bbdf8..3020dd148 100644 --- a/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php +++ b/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php @@ -28,15 +28,14 @@ class NumberToLocalizedStringTransformer implements DataTransformerInterface protected bool $grouping; protected int $roundingMode; - private ?int $scale; - private ?string $locale; - - public function __construct(?int $scale = null, ?bool $grouping = false, ?int $roundingMode = \NumberFormatter::ROUND_HALFUP, ?string $locale = null) - { - $this->scale = $scale; + public function __construct( + private ?int $scale = null, + ?bool $grouping = false, + ?int $roundingMode = \NumberFormatter::ROUND_HALFUP, + private ?string $locale = null, + ) { $this->grouping = $grouping ?? false; $this->roundingMode = $roundingMode ?? \NumberFormatter::ROUND_HALFUP; - $this->locale = $locale; } /** @@ -65,9 +64,7 @@ public function transform(mixed $value): string } // Convert non-breaking and narrow non-breaking spaces to normal ones - $value = str_replace(["\xc2\xa0", "\xe2\x80\xaf"], ' ', $value); - - return $value; + return str_replace(["\xc2\xa0", "\xe2\x80\xaf"], ' ', $value); } /** @@ -105,7 +102,8 @@ public function reverseTransform(mixed $value): int|float|null $value = str_replace(',', $decSep, $value); } - if (str_contains($value, $decSep)) { + // If the value is in exponential notation with a negative exponent, we end up with a float value too + if (str_contains($value, $decSep) || false !== stripos($value, 'e-')) { $type = \NumberFormatter::TYPE_DOUBLE; } else { $type = \PHP_INT_SIZE === 8 @@ -113,10 +111,14 @@ public function reverseTransform(mixed $value): int|float|null : \NumberFormatter::TYPE_INT32; } - $result = $formatter->parse($value, $type, $position); + try { + $result = @$formatter->parse($value, $type, $position); + } catch (\IntlException $e) { + throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e); + } if (intl_is_failure($formatter->getErrorCode())) { - throw new TransformationFailedException($formatter->getErrorMessage()); + throw new TransformationFailedException($formatter->getErrorMessage(), $formatter->getErrorCode()); } if ($result >= \PHP_INT_MAX || $result <= -\PHP_INT_MAX) { @@ -141,7 +143,7 @@ public function reverseTransform(mixed $value): int|float|null $remainder = trim($remainder, " \t\n\r\0\x0b\xc2\xa0"); if ('' !== $remainder) { - throw new TransformationFailedException(sprintf('The number contains unrecognized characters: "%s".', $remainder)); + throw new TransformationFailedException(\sprintf('The number contains unrecognized characters: "%s".', $remainder)); } } @@ -183,35 +185,21 @@ protected function castParsedValue(int|float $value): int|float */ private function round(int|float $number): int|float { - if (null !== $this->scale && null !== $this->roundingMode) { + if (null !== $this->scale) { // shift number to maintain the correct scale during rounding $roundingCoef = 10 ** $this->scale; // string representation to avoid rounding errors, similar to bcmul() $number = (string) ($number * $roundingCoef); - switch ($this->roundingMode) { - case \NumberFormatter::ROUND_CEILING: - $number = ceil($number); - break; - case \NumberFormatter::ROUND_FLOOR: - $number = floor($number); - break; - case \NumberFormatter::ROUND_UP: - $number = $number > 0 ? ceil($number) : floor($number); - break; - case \NumberFormatter::ROUND_DOWN: - $number = $number > 0 ? floor($number) : ceil($number); - break; - case \NumberFormatter::ROUND_HALFEVEN: - $number = round($number, 0, \PHP_ROUND_HALF_EVEN); - break; - case \NumberFormatter::ROUND_HALFUP: - $number = round($number, 0, \PHP_ROUND_HALF_UP); - break; - case \NumberFormatter::ROUND_HALFDOWN: - $number = round($number, 0, \PHP_ROUND_HALF_DOWN); - break; - } + $number = match ($this->roundingMode) { + \NumberFormatter::ROUND_CEILING => ceil($number), + \NumberFormatter::ROUND_FLOOR => floor($number), + \NumberFormatter::ROUND_UP => $number > 0 ? ceil($number) : floor($number), + \NumberFormatter::ROUND_DOWN => $number > 0 ? floor($number) : ceil($number), + \NumberFormatter::ROUND_HALFEVEN => round($number, 0, \PHP_ROUND_HALF_EVEN), + \NumberFormatter::ROUND_HALFUP => round($number, 0, \PHP_ROUND_HALF_UP), + \NumberFormatter::ROUND_HALFDOWN => round($number, 0, \PHP_ROUND_HALF_DOWN), + }; $number = 1 === $roundingCoef ? (int) $number : $number / $roundingCoef; } diff --git a/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php b/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php index 753e56063..093c3ebe2 100644 --- a/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php +++ b/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php @@ -33,10 +33,8 @@ class PercentToLocalizedStringTransformer implements DataTransformerInterface self::INTEGER, ]; - private int $roundingMode; private string $type; private int $scale; - private bool $html5Format; /** * @see self::$types for a list of supported types @@ -46,8 +44,12 @@ class PercentToLocalizedStringTransformer implements DataTransformerInterface * * @throws UnexpectedTypeException if the given value of type is unknown */ - public function __construct(?int $scale = null, ?string $type = null, int $roundingMode = \NumberFormatter::ROUND_HALFUP, bool $html5Format = false) - { + public function __construct( + ?int $scale = null, + ?string $type = null, + private int $roundingMode = \NumberFormatter::ROUND_HALFUP, + private bool $html5Format = false, + ) { $type ??= self::FRACTIONAL; if (!\in_array($type, self::$types, true)) { @@ -56,8 +58,6 @@ public function __construct(?int $scale = null, ?string $type = null, int $round $this->type = $type; $this->scale = $scale ?? 0; - $this->roundingMode = $roundingMode; - $this->html5Format = $html5Format; } /** @@ -131,11 +131,15 @@ public function reverseTransform(mixed $value): int|float|null $type = \PHP_INT_SIZE === 8 ? \NumberFormatter::TYPE_INT64 : \NumberFormatter::TYPE_INT32; } - // replace normal spaces so that the formatter can read them - $result = $formatter->parse(str_replace(' ', "\xc2\xa0", $value), $type, $position); + try { + // replace normal spaces so that the formatter can read them + $result = @$formatter->parse(str_replace(' ', "\xc2\xa0", $value), $type, $position); + } catch (\IntlException $e) { + throw new TransformationFailedException($e->getMessage(), 0, $e); + } if (intl_is_failure($formatter->getErrorCode())) { - throw new TransformationFailedException($formatter->getErrorMessage()); + throw new TransformationFailedException($formatter->getErrorMessage(), $formatter->getErrorCode()); } if (self::FRACTIONAL == $this->type) { @@ -158,7 +162,7 @@ public function reverseTransform(mixed $value): int|float|null $remainder = trim($remainder, " \t\n\r\0\x0b\xc2\xa0"); if ('' !== $remainder) { - throw new TransformationFailedException(sprintf('The number contains unrecognized characters: "%s".', $remainder)); + throw new TransformationFailedException(\sprintf('The number contains unrecognized characters: "%s".', $remainder)); } } @@ -180,9 +184,7 @@ protected function getNumberFormatter(): \NumberFormatter $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->scale); - if (null !== $this->roundingMode) { - $formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode); - } + $formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode); return $formatter; } @@ -192,44 +194,26 @@ protected function getNumberFormatter(): \NumberFormatter */ private function round(int|float $number): int|float { - if (null !== $this->scale && null !== $this->roundingMode) { - // shift number to maintain the correct scale during rounding - $roundingCoef = 10 ** $this->scale; + // shift number to maintain the correct scale during rounding + $roundingCoef = 10 ** $this->scale; - if (self::FRACTIONAL == $this->type) { - $roundingCoef *= 100; - } + if (self::FRACTIONAL === $this->type) { + $roundingCoef *= 100; + } - // string representation to avoid rounding errors, similar to bcmul() - $number = (string) ($number * $roundingCoef); - - switch ($this->roundingMode) { - case \NumberFormatter::ROUND_CEILING: - $number = ceil($number); - break; - case \NumberFormatter::ROUND_FLOOR: - $number = floor($number); - break; - case \NumberFormatter::ROUND_UP: - $number = $number > 0 ? ceil($number) : floor($number); - break; - case \NumberFormatter::ROUND_DOWN: - $number = $number > 0 ? floor($number) : ceil($number); - break; - case \NumberFormatter::ROUND_HALFEVEN: - $number = round($number, 0, \PHP_ROUND_HALF_EVEN); - break; - case \NumberFormatter::ROUND_HALFUP: - $number = round($number, 0, \PHP_ROUND_HALF_UP); - break; - case \NumberFormatter::ROUND_HALFDOWN: - $number = round($number, 0, \PHP_ROUND_HALF_DOWN); - break; - } + // string representation to avoid rounding errors, similar to bcmul() + $number = (string) ($number * $roundingCoef); - $number = 1 === $roundingCoef ? (int) $number : $number / $roundingCoef; - } + $number = match ($this->roundingMode) { + \NumberFormatter::ROUND_CEILING => ceil($number), + \NumberFormatter::ROUND_FLOOR => floor($number), + \NumberFormatter::ROUND_UP => $number > 0 ? ceil($number) : floor($number), + \NumberFormatter::ROUND_DOWN => $number > 0 ? floor($number) : ceil($number), + \NumberFormatter::ROUND_HALFEVEN => round($number, 0, \PHP_ROUND_HALF_EVEN), + \NumberFormatter::ROUND_HALFUP => round($number, 0, \PHP_ROUND_HALF_UP), + \NumberFormatter::ROUND_HALFDOWN => round($number, 0, \PHP_ROUND_HALF_DOWN), + }; - return $number; + return 1 === $roundingCoef ? (int) $number : $number / $roundingCoef; } } diff --git a/Extension/Core/DataTransformer/StringToFloatTransformer.php b/Extension/Core/DataTransformer/StringToFloatTransformer.php index 49b4ea98a..f47fcb625 100644 --- a/Extension/Core/DataTransformer/StringToFloatTransformer.php +++ b/Extension/Core/DataTransformer/StringToFloatTransformer.php @@ -19,11 +19,9 @@ */ class StringToFloatTransformer implements DataTransformerInterface { - private ?int $scale; - - public function __construct(?int $scale = null) - { - $this->scale = $scale; + public function __construct( + private ?int $scale = null, + ) { } public function transform(mixed $value): ?float diff --git a/Extension/Core/DataTransformer/UlidToStringTransformer.php b/Extension/Core/DataTransformer/UlidToStringTransformer.php index 7ace73ad0..9365cab15 100644 --- a/Extension/Core/DataTransformer/UlidToStringTransformer.php +++ b/Extension/Core/DataTransformer/UlidToStringTransformer.php @@ -65,7 +65,7 @@ public function reverseTransform(mixed $value): ?Ulid try { $ulid = new Ulid($value); } catch (\InvalidArgumentException $e) { - throw new TransformationFailedException(sprintf('The value "%s" is not a valid ULID.', $value), $e->getCode(), $e); + throw new TransformationFailedException(\sprintf('The value "%s" is not a valid ULID.', $value), $e->getCode(), $e); } return $ulid; diff --git a/Extension/Core/DataTransformer/UuidToStringTransformer.php b/Extension/Core/DataTransformer/UuidToStringTransformer.php index cc794a024..43326eb64 100644 --- a/Extension/Core/DataTransformer/UuidToStringTransformer.php +++ b/Extension/Core/DataTransformer/UuidToStringTransformer.php @@ -63,13 +63,13 @@ public function reverseTransform(mixed $value): ?Uuid } if (!Uuid::isValid($value)) { - throw new TransformationFailedException(sprintf('The value "%s" is not a valid UUID.', $value)); + throw new TransformationFailedException(\sprintf('The value "%s" is not a valid UUID.', $value)); } try { return Uuid::fromString($value); } catch (\InvalidArgumentException $e) { - throw new TransformationFailedException(sprintf('The value "%s" is not a valid UUID.', $value), $e->getCode(), $e); + throw new TransformationFailedException(\sprintf('The value "%s" is not a valid UUID.', $value), $e->getCode(), $e); } } } diff --git a/Extension/Core/DataTransformer/ValueToDuplicatesTransformer.php b/Extension/Core/DataTransformer/ValueToDuplicatesTransformer.php index 2399abf73..6f8ea4fbe 100644 --- a/Extension/Core/DataTransformer/ValueToDuplicatesTransformer.php +++ b/Extension/Core/DataTransformer/ValueToDuplicatesTransformer.php @@ -21,11 +21,9 @@ */ class ValueToDuplicatesTransformer implements DataTransformerInterface { - private array $keys; - - public function __construct(array $keys) - { - $this->keys = $keys; + public function __construct( + private array $keys, + ) { } /** @@ -58,7 +56,7 @@ public function reverseTransform(mixed $array): mixed $emptyKeys = []; foreach ($this->keys as $key) { - if (isset($array[$key]) && '' !== $array[$key] && false !== $array[$key] && [] !== $array[$key]) { + if (isset($array[$key]) && false !== $array[$key] && [] !== $array[$key]) { if ($array[$key] !== $result) { throw new TransformationFailedException('All values in the array should be the same.'); } @@ -73,7 +71,7 @@ public function reverseTransform(mixed $array): mixed return null; } - throw new TransformationFailedException(sprintf('The keys "%s" should not be empty.', implode('", "', $emptyKeys))); + throw new TransformationFailedException(\sprintf('The keys "%s" should not be empty.', implode('", "', $emptyKeys))); } return $result; diff --git a/Extension/Core/DataTransformer/WeekToArrayTransformer.php b/Extension/Core/DataTransformer/WeekToArrayTransformer.php index c10bc735f..448ae4278 100644 --- a/Extension/Core/DataTransformer/WeekToArrayTransformer.php +++ b/Extension/Core/DataTransformer/WeekToArrayTransformer.php @@ -40,7 +40,7 @@ public function transform(mixed $value): array } if (!\is_string($value)) { - throw new TransformationFailedException(sprintf('Value is expected to be a string but was "%s".', get_debug_type($value))); + throw new TransformationFailedException(\sprintf('Value is expected to be a string but was "%s".', get_debug_type($value))); } if (0 === preg_match('/^(?P\d{4})-W(?P\d{2})$/', $value, $matches)) { @@ -70,7 +70,7 @@ public function reverseTransform(mixed $value): ?string } if (!\is_array($value)) { - throw new TransformationFailedException(sprintf('Value is expected to be an array, but was "%s".', get_debug_type($value))); + throw new TransformationFailedException(\sprintf('Value is expected to be an array, but was "%s".', get_debug_type($value))); } if (!\array_key_exists('year', $value)) { @@ -82,7 +82,7 @@ public function reverseTransform(mixed $value): ?string } if ($additionalKeys = array_diff(array_keys($value), ['year', 'week'])) { - throw new TransformationFailedException(sprintf('Expected only keys "year" and "week" to be present, but also got ["%s"].', implode('", "', $additionalKeys))); + throw new TransformationFailedException(\sprintf('Expected only keys "year" and "week" to be present, but also got ["%s"].', implode('", "', $additionalKeys))); } if (null === $value['year'] && null === $value['week']) { @@ -90,18 +90,18 @@ public function reverseTransform(mixed $value): ?string } if (!\is_int($value['year'])) { - throw new TransformationFailedException(sprintf('Year is expected to be an integer, but was "%s".', get_debug_type($value['year']))); + throw new TransformationFailedException(\sprintf('Year is expected to be an integer, but was "%s".', get_debug_type($value['year']))); } if (!\is_int($value['week'])) { - throw new TransformationFailedException(sprintf('Week is expected to be an integer, but was "%s".', get_debug_type($value['week']))); + throw new TransformationFailedException(\sprintf('Week is expected to be an integer, but was "%s".', get_debug_type($value['week']))); } // The 28th December is always in the last week of the year if (date('W', strtotime('28th December '.$value['year'])) < $value['week']) { - throw new TransformationFailedException(sprintf('Week "%d" does not exist for year "%d".', $value['week'], $value['year'])); + throw new TransformationFailedException(\sprintf('Week "%d" does not exist for year "%d".', $value['week'], $value['year'])); } - return sprintf('%d-W%02d', $value['year'], $value['week']); + return \sprintf('%d-W%02d', $value['year'], $value['week']); } } diff --git a/Extension/Core/EventListener/FixUrlProtocolListener.php b/Extension/Core/EventListener/FixUrlProtocolListener.php index d648d6f50..97cd3ad47 100644 --- a/Extension/Core/EventListener/FixUrlProtocolListener.php +++ b/Extension/Core/EventListener/FixUrlProtocolListener.php @@ -22,14 +22,12 @@ */ class FixUrlProtocolListener implements EventSubscriberInterface { - private ?string $defaultProtocol; - /** * @param string|null $defaultProtocol The URL scheme to add when there is none or null to not modify the data */ - public function __construct(?string $defaultProtocol = 'http') - { - $this->defaultProtocol = $defaultProtocol; + public function __construct( + private ?string $defaultProtocol = 'http', + ) { } public function onSubmit(FormEvent $event): void diff --git a/Extension/Core/EventListener/MergeCollectionListener.php b/Extension/Core/EventListener/MergeCollectionListener.php index 23ee47bd2..61428d5df 100644 --- a/Extension/Core/EventListener/MergeCollectionListener.php +++ b/Extension/Core/EventListener/MergeCollectionListener.php @@ -21,17 +21,14 @@ */ class MergeCollectionListener implements EventSubscriberInterface { - private bool $allowAdd; - private bool $allowDelete; - /** * @param bool $allowAdd Whether values might be added to the collection * @param bool $allowDelete Whether values might be removed from the collection */ - public function __construct(bool $allowAdd = false, bool $allowDelete = false) - { - $this->allowAdd = $allowAdd; - $this->allowDelete = $allowDelete; + public function __construct( + private bool $allowAdd = false, + private bool $allowDelete = false, + ) { } public static function getSubscribedEvents(): array diff --git a/Extension/Core/EventListener/ResizeFormListener.php b/Extension/Core/EventListener/ResizeFormListener.php index 1558c2127..299f91937 100644 --- a/Extension/Core/EventListener/ResizeFormListener.php +++ b/Extension/Core/EventListener/ResizeFormListener.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Form\Extension\Core\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\Event\PostSetDataEvent; use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; @@ -24,20 +25,22 @@ */ class ResizeFormListener implements EventSubscriberInterface { - protected string $type; - protected array $options; protected array $prototypeOptions; - protected bool $allowAdd; - protected bool $allowDelete; private \Closure|bool $deleteEmpty; - - public function __construct(string $type, array $options = [], bool $allowAdd = false, bool $allowDelete = false, bool|callable $deleteEmpty = false, ?array $prototypeOptions = null) - { - $this->type = $type; - $this->allowAdd = $allowAdd; - $this->allowDelete = $allowDelete; - $this->options = $options; + // BC, to be removed in 8.0 + private bool $overridden = true; + private bool $usePreSetData = false; + + public function __construct( + private string $type, + private array $options = [], + private bool $allowAdd = false, + private bool $allowDelete = false, + bool|callable $deleteEmpty = false, + ?array $prototypeOptions = null, + private bool $keepAsList = false, + ) { $this->deleteEmpty = \is_bool($deleteEmpty) ? $deleteEmpty : $deleteEmpty(...); $this->prototypeOptions = $prototypeOptions ?? $options; } @@ -45,15 +48,57 @@ public function __construct(string $type, array $options = [], bool $allowAdd = public static function getSubscribedEvents(): array { return [ - FormEvents::PRE_SET_DATA => 'preSetData', + FormEvents::PRE_SET_DATA => 'preSetData', // deprecated + FormEvents::POST_SET_DATA => ['postSetData', 255], // as early as possible FormEvents::PRE_SUBMIT => 'preSubmit', // (MergeCollectionListener, MergeDoctrineCollectionListener) FormEvents::SUBMIT => ['onSubmit', 50], ]; } + /** + * @deprecated Since Symfony 7.2, use {@see postSetData()} instead. + */ public function preSetData(FormEvent $event): void { + if (__CLASS__ === static::class + || __CLASS__ === (new \ReflectionClass($this))->getMethod('preSetData')->getDeclaringClass()->name + ) { + // not a child class, or child class does not overload PRE_SET_DATA + return; + } + + trigger_deprecation('symfony/form', '7.2', 'Calling "%s()" is deprecated, use "%s::postSetData()" instead.', __METHOD__, __CLASS__); + // parent::preSetData() has been called + $this->overridden = false; + try { + $this->postSetData($event); + } finally { + $this->usePreSetData = true; + } + } + + /** + * Remove FormEvent type hint in 8.0. + * + * @final since Symfony 7.2 + */ + public function postSetData(FormEvent|PostSetDataEvent $event): void + { + if (__CLASS__ !== static::class) { + if ($this->overridden) { + trigger_deprecation('symfony/form', '7.2', 'Calling "%s::preSetData()" is deprecated, use "%s::postSetData()" instead.', static::class, __CLASS__); + // parent::preSetData() has not been called, noop + + return; + } + + if ($this->usePreSetData) { + // nothing else to do + return; + } + } + $form = $event->getForm(); $data = $event->getData() ?? []; @@ -153,6 +198,20 @@ public function onSubmit(FormEvent $event): void } } + if ($this->keepAsList) { + $formReindex = []; + foreach ($form as $name => $child) { + $formReindex[] = $child; + $form->remove($name); + } + foreach ($formReindex as $index => $child) { + $form->add($index, $this->type, array_replace([ + 'property_path' => '['.$index.']', + ], $this->options)); + } + $data = array_values($data); + } + $event->setData($data); } } diff --git a/Extension/Core/EventListener/TransformationFailureListener.php b/Extension/Core/EventListener/TransformationFailureListener.php index ee4b9fae6..48f0b9ac9 100644 --- a/Extension/Core/EventListener/TransformationFailureListener.php +++ b/Extension/Core/EventListener/TransformationFailureListener.php @@ -22,11 +22,9 @@ */ class TransformationFailureListener implements EventSubscriberInterface { - private ?TranslatorInterface $translator; - - public function __construct(?TranslatorInterface $translator = null) - { - $this->translator = $translator; + public function __construct( + private ?TranslatorInterface $translator = null, + ) { } public static function getSubscribedEvents(): array diff --git a/Extension/Core/Type/BaseType.php b/Extension/Core/Type/BaseType.php index 68190c742..a7f745d78 100644 --- a/Extension/Core/Type/BaseType.php +++ b/Extension/Core/Type/BaseType.php @@ -46,9 +46,9 @@ public function buildView(FormView $view, FormInterface $form, array $options): if ($view->parent) { if ('' !== ($parentFullName = $view->parent->vars['full_name'])) { - $id = sprintf('%s_%s', $view->parent->vars['id'], $name); - $fullName = sprintf('%s[%s]', $parentFullName, $name); - $uniqueBlockPrefix = sprintf('%s_%s', $view->parent->vars['unique_block_prefix'], $blockName); + $id = \sprintf('%s_%s', $view->parent->vars['id'], $name); + $fullName = \sprintf('%s[%s]', $parentFullName, $name); + $uniqueBlockPrefix = \sprintf('%s_%s', $view->parent->vars['unique_block_prefix'], $blockName); } else { $id = $name; $fullName = $name; diff --git a/Extension/Core/Type/ChoiceType.php b/Extension/Core/Type/ChoiceType.php index 1ff763ac8..fc083ee40 100644 --- a/Extension/Core/Type/ChoiceType.php +++ b/Extension/Core/Type/ChoiceType.php @@ -27,10 +27,12 @@ use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\Loader\LazyChoiceLoader; use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; use Symfony\Component\Form\ChoiceList\View\ChoiceListView; use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\Event\PreSubmitEvent; +use Symfony\Component\Form\Exception\LogicException; use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\Extension\Core\DataMapper\CheckboxListMapper; use Symfony\Component\Form\Extension\Core\DataMapper\RadioListMapper; @@ -51,16 +53,16 @@ class ChoiceType extends AbstractType { private ChoiceListFactoryInterface $choiceListFactory; - private ?TranslatorInterface $translator; - public function __construct(?ChoiceListFactoryInterface $choiceListFactory = null, ?TranslatorInterface $translator = null) - { + public function __construct( + ?ChoiceListFactoryInterface $choiceListFactory = null, + private ?TranslatorInterface $translator = null, + ) { $this->choiceListFactory = $choiceListFactory ?? new CachingFactoryDecorator( new PropertyAccessDecorator( new DefaultChoiceListFactory() ) ); - $this->translator = $translator; } public function buildForm(FormBuilderInterface $builder, array $options): void @@ -141,9 +143,9 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $knownValues[$child->getName()] = $value; unset($unknownValues[$value]); continue; - } else { - $knownValues[$child->getName()] = null; } + + $knownValues[$child->getName()] = null; } } else { foreach ($choiceList->getChoicesForValues($data) as $key => $choice) { @@ -158,7 +160,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void // 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)))); + throw new TransformationFailedException(\sprintf('The choices "%s" do not exist in the choice list.', implode('", "', array_keys($unknownValues)))); } $event->setData($knownValues); @@ -182,7 +184,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $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.', $clientDataAsString)))); + $form->addError(new FormError($message, $messageTemplate, ['{{ value }}' => $clientDataAsString], null, new TransformationFailedException(\sprintf('The choices "%s" do not exist in the choice list.', $clientDataAsString)))); } }); @@ -236,7 +238,8 @@ public function buildView(FormView $view, FormInterface $form, array $options): 'expanded' => $options['expanded'], 'preferred_choices' => $choiceListView->preferredChoices, 'choices' => $choiceListView->choices, - 'separator' => '-------------------', + 'separator' => $options['separator'], + 'separator_html' => $options['separator_html'], 'placeholder' => null, 'placeholder_attr' => [], 'choice_translation_domain' => $choiceTranslationDomain, @@ -272,6 +275,8 @@ public function buildView(FormView $view, FormInterface $form, array $options): public function finishView(FormView $view, FormInterface $form, array $options): void { + $view->vars['duplicate_preferred_choices'] = $options['duplicate_preferred_choices']; + if ($options['expanded']) { // Radio buttons should have the same name as the parent $childName = $view->vars['full_name']; @@ -332,11 +337,24 @@ public function configureOptions(OptionsResolver $resolver): void return $choiceTranslationDomain; }; + $choiceLoaderNormalizer = static function (Options $options, ?ChoiceLoaderInterface $choiceLoader) { + if (!$options['choice_lazy']) { + return $choiceLoader; + } + + if (null === $choiceLoader) { + throw new LogicException('The "choice_lazy" option can only be used if the "choice_loader" option is set.'); + } + + return new LazyChoiceLoader($choiceLoader); + }; + $resolver->setDefaults([ 'multiple' => false, 'expanded' => false, 'choices' => [], 'choice_filter' => null, + 'choice_lazy' => false, 'choice_loader' => null, 'choice_label' => null, 'choice_name' => null, @@ -344,6 +362,8 @@ public function configureOptions(OptionsResolver $resolver): void 'choice_attr' => null, 'choice_translation_parameters' => [], 'preferred_choices' => [], + 'separator' => '-------------------', + 'separator_html' => false, 'duplicate_preferred_choices' => true, 'group_by' => null, 'empty_data' => $emptyData, @@ -362,9 +382,11 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->setNormalizer('placeholder', $placeholderNormalizer); $resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer); + $resolver->setNormalizer('choice_loader', $choiceLoaderNormalizer); $resolver->setAllowedTypes('choices', ['null', 'array', \Traversable::class]); $resolver->setAllowedTypes('choice_translation_domain', ['null', 'bool', 'string']); + $resolver->setAllowedTypes('choice_lazy', 'bool'); $resolver->setAllowedTypes('choice_loader', ['null', ChoiceLoaderInterface::class, ChoiceLoader::class]); $resolver->setAllowedTypes('choice_filter', ['null', 'callable', 'string', PropertyPath::class, ChoiceFilter::class]); $resolver->setAllowedTypes('choice_label', ['null', 'bool', 'callable', 'string', PropertyPath::class, ChoiceLabel::class]); @@ -374,8 +396,12 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('choice_translation_parameters', ['null', 'array', 'callable', ChoiceTranslationParameters::class]); $resolver->setAllowedTypes('placeholder_attr', ['array']); $resolver->setAllowedTypes('preferred_choices', ['array', \Traversable::class, 'callable', 'string', PropertyPath::class, PreferredChoice::class]); + $resolver->setAllowedTypes('separator', ['string']); + $resolver->setAllowedTypes('separator_html', ['bool']); $resolver->setAllowedTypes('duplicate_preferred_choices', 'bool'); $resolver->setAllowedTypes('group_by', ['null', 'callable', 'string', PropertyPath::class, GroupBy::class]); + + $resolver->setInfo('choice_lazy', 'Load choices on demand. When set to true, only the selected choices are loaded and rendered.'); } public function getBlockPrefix(): string @@ -439,7 +465,7 @@ private function createChoiceList(array $options): ChoiceListInterface } // Harden against NULL values (like in EntityType and ModelType) - $choices = null !== $options['choices'] ? $options['choices'] : []; + $choices = $options['choices'] ?? []; return $this->choiceListFactory->createListFromChoices( $choices, diff --git a/Extension/Core/Type/CollectionType.php b/Extension/Core/Type/CollectionType.php index c9d3ec5b7..3cef93152 100644 --- a/Extension/Core/Type/CollectionType.php +++ b/Extension/Core/Type/CollectionType.php @@ -45,7 +45,8 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $options['allow_add'], $options['allow_delete'], $options['delete_empty'], - $resizePrototypeOptions + $resizePrototypeOptions, + $options['keep_as_list'] ); $builder->addEventSubscriber($resizeListener); @@ -114,12 +115,14 @@ public function configureOptions(OptionsResolver $resolver): void 'prototype_options' => [], 'delete_empty' => false, 'invalid_message' => 'The collection is invalid.', + 'keep_as_list' => false, ]); $resolver->setNormalizer('entry_options', $entryOptionsNormalizer); $resolver->setAllowedTypes('delete_empty', ['bool', 'callable']); $resolver->setAllowedTypes('prototype_options', 'array'); + $resolver->setAllowedTypes('keep_as_list', ['bool']); } public function getBlockPrefix(): string diff --git a/Extension/Core/Type/ColorType.php b/Extension/Core/Type/ColorType.php index c7257b33a..d67d6dc73 100644 --- a/Extension/Core/Type/ColorType.php +++ b/Extension/Core/Type/ColorType.php @@ -26,11 +26,9 @@ class ColorType extends AbstractType */ private const HTML5_PATTERN = '/^#[0-9a-f]{6}$/i'; - private ?TranslatorInterface $translator; - - public function __construct(?TranslatorInterface $translator = null) - { - $this->translator = $translator; + public function __construct( + private ?TranslatorInterface $translator = null, + ) { } public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/Extension/Core/Type/CountryType.php b/Extension/Core/Type/CountryType.php index adef745c5..503bc8327 100644 --- a/Extension/Core/Type/CountryType.php +++ b/Extension/Core/Type/CountryType.php @@ -27,7 +27,7 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->setDefaults([ 'choice_loader' => function (Options $options) { if (!class_exists(Intl::class)) { - throw new LogicException(sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class)); + throw new LogicException(\sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class)); } $choiceTranslationLocale = $options['choice_translation_locale']; diff --git a/Extension/Core/Type/CurrencyType.php b/Extension/Core/Type/CurrencyType.php index 3581a77d9..abfa39729 100644 --- a/Extension/Core/Type/CurrencyType.php +++ b/Extension/Core/Type/CurrencyType.php @@ -27,7 +27,7 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->setDefaults([ 'choice_loader' => function (Options $options) { if (!class_exists(Intl::class)) { - throw new LogicException(sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class)); + throw new LogicException(\sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class)); } $choiceTranslationLocale = $options['choice_translation_locale']; diff --git a/Extension/Core/Type/DateTimeType.php b/Extension/Core/Type/DateTimeType.php index 1c67eeb15..cf4c2b741 100644 --- a/Extension/Core/Type/DateTimeType.php +++ b/Extension/Core/Type/DateTimeType.php @@ -203,7 +203,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void } if ($date->getTimezone()->getName() !== $options['model_timezone']) { - throw new LogicException(sprintf('Using a "%s" instance with a timezone ("%s") not matching the configured model timezone "%s" is not supported.', get_debug_type($date), $date->getTimezone()->getName(), $options['model_timezone'])); + throw new LogicException(\sprintf('Using a "%s" instance with a timezone ("%s") not matching the configured model timezone "%s" is not supported.', get_debug_type($date), $date->getTimezone()->getName(), $options['model_timezone'])); } }); } @@ -311,7 +311,7 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->setNormalizer('date_format', static function (Options $options, $dateFormat) { if (null !== $dateFormat && 'single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) { - throw new LogicException(sprintf('Cannot use the "date_format" option of the "%s" with an HTML5 date.', self::class)); + throw new LogicException(\sprintf('Cannot use the "date_format" option of the "%s" with an HTML5 date.', self::class)); } return $dateFormat; @@ -319,10 +319,10 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->setNormalizer('widget', static function (Options $options, $widget) { if ('single_text' === $widget) { if (null !== $options['date_widget']) { - throw new LogicException(sprintf('Cannot use the "date_widget" option of the "%s" when the "widget" option is set to "single_text".', self::class)); + throw new LogicException(\sprintf('Cannot use the "date_widget" option of the "%s" when the "widget" option is set to "single_text".', self::class)); } if (null !== $options['time_widget']) { - throw new LogicException(sprintf('Cannot use the "time_widget" option of the "%s" when the "widget" option is set to "single_text".', self::class)); + throw new LogicException(\sprintf('Cannot use the "time_widget" option of the "%s" when the "widget" option is set to "single_text".', self::class)); } } elseif (null === $widget && null === $options['date_widget'] && null === $options['time_widget']) { return 'single_text'; @@ -332,7 +332,7 @@ public function configureOptions(OptionsResolver $resolver): void }); $resolver->setNormalizer('html5', static function (Options $options, $html5) { if ($html5 && self::HTML5_FORMAT !== $options['format']) { - throw new LogicException(sprintf('Cannot use the "format" option of "%s" when the "html5" option is enabled.', self::class)); + throw new LogicException(\sprintf('Cannot use the "format" option of "%s" when the "html5" option is enabled.', self::class)); } return $html5; diff --git a/Extension/Core/Type/DateType.php b/Extension/Core/Type/DateType.php index cfbbaf4c0..36b430e14 100644 --- a/Extension/Core/Type/DateType.php +++ b/Extension/Core/Type/DateType.php @@ -49,7 +49,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void { $dateFormat = \is_int($options['format']) ? $options['format'] : self::DEFAULT_FORMAT; $timeFormat = \IntlDateFormatter::NONE; - $calendar = \IntlDateFormatter::GREGORIAN; + $calendar = $options['calendar'] ?? \IntlDateFormatter::GREGORIAN; $pattern = \is_string($options['format']) ? $options['format'] : ''; if (!\in_array($dateFormat, self::ACCEPTED_FORMATS, true)) { @@ -58,7 +58,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void if ('single_text' === $options['widget']) { if ('' !== $pattern && !str_contains($pattern, 'y') && !str_contains($pattern, 'M') && !str_contains($pattern, 'd')) { - throw new InvalidOptionsException(sprintf('The "format" option should contain the letters "y", "M" or "d". Its current value is "%s".', $pattern)); + throw new InvalidOptionsException(\sprintf('The "format" option should contain the letters "y", "M" or "d". Its current value is "%s".', $pattern)); } $builder->addViewTransformer(new DateTimeToLocalizedStringTransformer( @@ -71,7 +71,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void )); } else { if ('' !== $pattern && (!str_contains($pattern, 'y') || !str_contains($pattern, 'M') || !str_contains($pattern, 'd'))) { - throw new InvalidOptionsException(sprintf('The "format" option should contain the letters "y", "M" and "d". Its current value is "%s".', $pattern)); + throw new InvalidOptionsException(\sprintf('The "format" option should contain the letters "y", "M" and "d". Its current value is "%s".', $pattern)); } $yearOptions = $monthOptions = $dayOptions = [ @@ -120,17 +120,11 @@ public function buildForm(FormBuilderInterface $builder, array $options): void \Locale::getDefault(), $dateFormat, $timeFormat, - // see https://bugs.php.net/66323 - class_exists(\IntlTimeZone::class, false) ? \IntlTimeZone::createDefault() : null, + null, $calendar, $pattern ); - // new \IntlDateFormatter may return null instead of false in case of failure, see https://bugs.php.net/66323 - if (!$formatter) { - throw new InvalidOptionsException(intl_get_error_message(), intl_get_error_code()); - } - $formatter->setLenient(false); if ('choice' === $options['widget']) { @@ -187,7 +181,7 @@ class_exists(\IntlTimeZone::class, false) ? \IntlTimeZone::createDefault() : nul } if ($date->getTimezone()->getName() !== $options['model_timezone']) { - throw new LogicException(sprintf('Using a "%s" instance with a timezone ("%s") not matching the configured model timezone "%s" is not supported.', get_debug_type($date), $date->getTimezone()->getName(), $options['model_timezone'])); + throw new LogicException(\sprintf('Using a "%s" instance with a timezone ("%s") not matching the configured model timezone "%s" is not supported.', get_debug_type($date), $date->getTimezone()->getName(), $options['model_timezone'])); } }); } @@ -255,10 +249,8 @@ public function configureOptions(OptionsResolver $resolver): void $choiceTranslationDomainNormalizer = static function (Options $options, $choiceTranslationDomain) { if (\is_array($choiceTranslationDomain)) { - $default = false; - return array_replace( - ['year' => $default, 'month' => $default, 'day' => $default], + ['year' => false, 'month' => false, 'day' => false], $choiceTranslationDomain ); } @@ -281,6 +273,7 @@ public function configureOptions(OptionsResolver $resolver): void 'format' => $format, 'model_timezone' => null, 'view_timezone' => null, + 'calendar' => null, 'placeholder' => $placeholderDefault, 'html5' => true, // Don't modify \DateTime classes by reference, we treat @@ -320,10 +313,13 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('months', 'array'); $resolver->setAllowedTypes('days', 'array'); $resolver->setAllowedTypes('input_format', 'string'); + $resolver->setAllowedTypes('calendar', ['null', 'int', \IntlCalendar::class]); + + $resolver->setInfo('calendar', 'The calendar to use for formatting and parsing the date. The value should be an instance of \IntlCalendar. By default, the Gregorian calendar with the default locale is used.'); $resolver->setNormalizer('html5', static function (Options $options, $html5) { if ($html5 && 'single_text' === $options['widget'] && self::HTML5_FORMAT !== $options['format']) { - throw new LogicException(sprintf('Cannot use the "format" option of "%s" when the "html5" option is enabled.', self::class)); + throw new LogicException(\sprintf('Cannot use the "format" option of "%s" when the "html5" option is enabled.', self::class)); } return $html5; diff --git a/Extension/Core/Type/FileType.php b/Extension/Core/Type/FileType.php index ed10b2b54..b4b4d86d9 100644 --- a/Extension/Core/Type/FileType.php +++ b/Extension/Core/Type/FileType.php @@ -35,11 +35,9 @@ class FileType extends AbstractType self::MIB_BYTES => 'MiB', ]; - private ?TranslatorInterface $translator; - - public function __construct(?TranslatorInterface $translator = null) - { - $this->translator = $translator; + public function __construct( + private ?TranslatorInterface $translator = null, + ) { } public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/Extension/Core/Type/LanguageType.php b/Extension/Core/Type/LanguageType.php index e81571f8c..638976a8c 100644 --- a/Extension/Core/Type/LanguageType.php +++ b/Extension/Core/Type/LanguageType.php @@ -28,7 +28,7 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->setDefaults([ 'choice_loader' => function (Options $options) { if (!class_exists(Intl::class)) { - throw new LogicException(sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class)); + throw new LogicException(\sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class)); } $choiceTranslationLocale = $options['choice_translation_locale']; $useAlpha3Codes = $options['alpha3']; diff --git a/Extension/Core/Type/LocaleType.php b/Extension/Core/Type/LocaleType.php index d0124e600..1529ca83f 100644 --- a/Extension/Core/Type/LocaleType.php +++ b/Extension/Core/Type/LocaleType.php @@ -27,7 +27,7 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->setDefaults([ 'choice_loader' => function (Options $options) { if (!class_exists(Intl::class)) { - throw new LogicException(sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class)); + throw new LogicException(\sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class)); } $choiceTranslationLocale = $options['choice_translation_locale']; diff --git a/Extension/Core/Type/MoneyType.php b/Extension/Core/Type/MoneyType.php index c82911a21..2657b03af 100644 --- a/Extension/Core/Type/MoneyType.php +++ b/Extension/Core/Type/MoneyType.php @@ -34,7 +34,8 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $options['grouping'], $options['rounding_mode'], $options['divisor'], - $options['html5'] ? 'en' : null + $options['html5'] ? 'en' : null, + $options['input'], )) ; } @@ -59,6 +60,7 @@ public function configureOptions(OptionsResolver $resolver): void 'compound' => false, 'html5' => false, 'invalid_message' => 'Please enter a valid money amount.', + 'input' => 'float', ]); $resolver->setAllowedValues('rounding_mode', [ @@ -75,6 +77,8 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('html5', 'bool'); + $resolver->setAllowedValues('input', ['float', 'integer']); + $resolver->setNormalizer('grouping', static function (Options $options, $value) { if ($value && $options['html5']) { throw new LogicException('Cannot use the "grouping" option when the "html5" option is enabled.'); diff --git a/Extension/Core/Type/TimeType.php b/Extension/Core/Type/TimeType.php index cf46b76ac..92cf42d96 100644 --- a/Extension/Core/Type/TimeType.php +++ b/Extension/Core/Type/TimeType.php @@ -18,7 +18,6 @@ use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer; -use Symfony\Component\Form\Event\PreSubmitEvent; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; @@ -45,7 +44,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void } if (null !== $options['reference_date'] && $options['reference_date']->getTimezone()->getName() !== $options['model_timezone']) { - throw new InvalidConfigurationException(sprintf('The configured "model_timezone" (%s) must match the timezone of the "reference_date" (%s).', $options['model_timezone'], $options['reference_date']->getTimezone()->getName())); + throw new InvalidConfigurationException(\sprintf('The configured "model_timezone" (%s) must match the timezone of the "reference_date" (%s).', $options['model_timezone'], $options['reference_date']->getTimezone()->getName())); } if ($options['with_minutes']) { @@ -60,15 +59,14 @@ public function buildForm(FormBuilderInterface $builder, array $options): void if ('single_text' === $options['widget']) { $builder->addEventListener(FormEvents::PRE_SUBMIT, static function (FormEvent $e) use ($options) { - /** @var PreSubmitEvent $event */ $data = $e->getData(); if ($data && preg_match('/^(?P\d{2}):(?P\d{2})(?::(?P\d{2})(?:\.\d+)?)?$/', $data, $matches)) { if ($options['with_seconds']) { // handle seconds ignored by user's browser when with_seconds enabled // https://codereview.chromium.org/450533009/ - $e->setData(sprintf('%s:%s:%s', $matches['hours'], $matches['minutes'], $matches['seconds'] ?? '00')); + $e->setData(\sprintf('%s:%s:%s', $matches['hours'], $matches['minutes'], $matches['seconds'] ?? '00')); } else { - $e->setData(sprintf('%s:%s', $matches['hours'], $matches['minutes'])); + $e->setData(\sprintf('%s:%s', $matches['hours'], $matches['minutes'])); } } }); @@ -217,7 +215,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void } if ($date->getTimezone()->getName() !== $options['model_timezone']) { - throw new LogicException(sprintf('Using a "%s" instance with a timezone ("%s") not matching the configured model timezone "%s" is not supported.', get_debug_type($date), $date->getTimezone()->getName(), $options['model_timezone'])); + throw new LogicException(\sprintf('Using a "%s" instance with a timezone ("%s") not matching the configured model timezone "%s" is not supported.', get_debug_type($date), $date->getTimezone()->getName(), $options['model_timezone'])); } }); } @@ -276,10 +274,8 @@ public function configureOptions(OptionsResolver $resolver): void $choiceTranslationDomainNormalizer = static function (Options $options, $choiceTranslationDomain) { if (\is_array($choiceTranslationDomain)) { - $default = false; - return array_replace( - ['hour' => $default, 'minute' => $default, 'second' => $default], + ['hour' => false, 'minute' => false, 'second' => false], $choiceTranslationDomain ); } diff --git a/Extension/Core/Type/TimezoneType.php b/Extension/Core/Type/TimezoneType.php index 01ce68ce3..2316d666b 100644 --- a/Extension/Core/Type/TimezoneType.php +++ b/Extension/Core/Type/TimezoneType.php @@ -43,7 +43,7 @@ public function configureOptions(OptionsResolver $resolver): void if ($options['intl']) { if (!class_exists(Intl::class)) { - throw new LogicException(sprintf('The "symfony/intl" component is required to use "%s" with option "intl=true". Try running "composer require symfony/intl".', static::class)); + throw new LogicException(\sprintf('The "symfony/intl" component is required to use "%s" with option "intl=true". Try running "composer require symfony/intl".', static::class)); } $choiceTranslationLocale = $options['choice_translation_locale']; diff --git a/Extension/Core/Type/TransformationFailureExtension.php b/Extension/Core/Type/TransformationFailureExtension.php index 234b3f36b..e90cd714e 100644 --- a/Extension/Core/Type/TransformationFailureExtension.php +++ b/Extension/Core/Type/TransformationFailureExtension.php @@ -21,11 +21,9 @@ */ class TransformationFailureExtension extends AbstractTypeExtension { - private ?TranslatorInterface $translator; - - public function __construct(?TranslatorInterface $translator = null) - { - $this->translator = $translator; + public function __construct( + private ?TranslatorInterface $translator = null, + ) { } public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/Extension/Core/Type/UrlType.php b/Extension/Core/Type/UrlType.php index d9cd3c6fb..fd6025729 100644 --- a/Extension/Core/Type/UrlType.php +++ b/Extension/Core/Type/UrlType.php @@ -16,6 +16,7 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class UrlType extends AbstractType @@ -38,7 +39,11 @@ public function buildView(FormView $view, FormInterface $form, array $options): public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'default_protocol' => 'http', + 'default_protocol' => static function (Options $options) { + trigger_deprecation('symfony/form', '7.1', 'Not configuring the "default_protocol" option when using the UrlType is deprecated. It will default to "null" in 8.0.'); + + return 'http'; + }, 'invalid_message' => 'Please enter a valid URL.', ]); diff --git a/Extension/Core/Type/WeekType.php b/Extension/Core/Type/WeekType.php index c3ffae061..c4827604e 100644 --- a/Extension/Core/Type/WeekType.php +++ b/Extension/Core/Type/WeekType.php @@ -39,7 +39,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void } else { $yearOptions = $weekOptions = [ 'error_bubbling' => true, - 'empty_data' => '', ]; // when the form is compound the entries of the array are ignored in favor of children data // so we need to handle the cascade setting here @@ -113,10 +112,8 @@ public function configureOptions(OptionsResolver $resolver): void $choiceTranslationDomainNormalizer = static function (Options $options, $choiceTranslationDomain) { if (\is_array($choiceTranslationDomain)) { - $default = false; - return array_replace( - ['year' => $default, 'week' => $default], + ['year' => false, 'week' => false], $choiceTranslationDomain ); } @@ -145,7 +142,7 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer); $resolver->setNormalizer('html5', static function (Options $options, $html5) { if ($html5 && 'single_text' !== $options['widget']) { - throw new LogicException(sprintf('The "widget" option of "%s" must be set to "single_text" when the "html5" option is enabled.', self::class)); + throw new LogicException(\sprintf('The "widget" option of "%s" must be set to "single_text" when the "html5" option is enabled.', self::class)); } return $html5; diff --git a/Extension/Csrf/CsrfExtension.php b/Extension/Csrf/CsrfExtension.php index 0a648f834..33c4616b4 100644 --- a/Extension/Csrf/CsrfExtension.php +++ b/Extension/Csrf/CsrfExtension.php @@ -22,15 +22,11 @@ */ class CsrfExtension extends AbstractExtension { - private CsrfTokenManagerInterface $tokenManager; - private ?TranslatorInterface $translator; - private ?string $translationDomain; - - public function __construct(CsrfTokenManagerInterface $tokenManager, ?TranslatorInterface $translator = null, ?string $translationDomain = null) - { - $this->tokenManager = $tokenManager; - $this->translator = $translator; - $this->translationDomain = $translationDomain; + public function __construct( + private CsrfTokenManagerInterface $tokenManager, + private ?TranslatorInterface $translator = null, + private ?string $translationDomain = null, + ) { } protected function loadTypeExtensions(): array diff --git a/Extension/Csrf/EventListener/CsrfValidationListener.php b/Extension/Csrf/EventListener/CsrfValidationListener.php index 109006106..44922402f 100644 --- a/Extension/Csrf/EventListener/CsrfValidationListener.php +++ b/Extension/Csrf/EventListener/CsrfValidationListener.php @@ -25,12 +25,6 @@ */ class CsrfValidationListener implements EventSubscriberInterface { - private string $fieldName; - private CsrfTokenManagerInterface $tokenManager; - private string $tokenId; - private string $errorMessage; - private ?TranslatorInterface $translator; - private ?string $translationDomain; private ServerParams $serverParams; public static function getSubscribedEvents(): array @@ -40,14 +34,15 @@ public static function getSubscribedEvents(): array ]; } - public function __construct(string $fieldName, CsrfTokenManagerInterface $tokenManager, string $tokenId, string $errorMessage, ?TranslatorInterface $translator = null, ?string $translationDomain = null, ?ServerParams $serverParams = null) - { - $this->fieldName = $fieldName; - $this->tokenManager = $tokenManager; - $this->tokenId = $tokenId; - $this->errorMessage = $errorMessage; - $this->translator = $translator; - $this->translationDomain = $translationDomain; + public function __construct( + private string $fieldName, + private CsrfTokenManagerInterface $tokenManager, + private string $tokenId, + private string $errorMessage, + private ?TranslatorInterface $translator = null, + private ?string $translationDomain = null, + ?ServerParams $serverParams = null, + ) { $this->serverParams = $serverParams ?? new ServerParams(); } diff --git a/Extension/Csrf/Type/FormTypeCsrfExtension.php b/Extension/Csrf/Type/FormTypeCsrfExtension.php index 0e8fa98a3..a12b9a41e 100644 --- a/Extension/Csrf/Type/FormTypeCsrfExtension.php +++ b/Extension/Csrf/Type/FormTypeCsrfExtension.php @@ -19,6 +19,7 @@ use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; use Symfony\Component\Form\Util\ServerParams; +use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Contracts\Translation\TranslatorInterface; @@ -28,21 +29,16 @@ */ class FormTypeCsrfExtension extends AbstractTypeExtension { - private CsrfTokenManagerInterface $defaultTokenManager; - private bool $defaultEnabled; - private string $defaultFieldName; - private ?TranslatorInterface $translator; - private ?string $translationDomain; - private ?ServerParams $serverParams; - - public function __construct(CsrfTokenManagerInterface $defaultTokenManager, bool $defaultEnabled = true, string $defaultFieldName = '_token', ?TranslatorInterface $translator = null, ?string $translationDomain = null, ?ServerParams $serverParams = null) - { - $this->defaultTokenManager = $defaultTokenManager; - $this->defaultEnabled = $defaultEnabled; - $this->defaultFieldName = $defaultFieldName; - $this->translator = $translator; - $this->translationDomain = $translationDomain; - $this->serverParams = $serverParams; + public function __construct( + private CsrfTokenManagerInterface $defaultTokenManager, + private bool $defaultEnabled = true, + private string $defaultFieldName = '_token', + private ?TranslatorInterface $translator = null, + private ?string $translationDomain = null, + private ?ServerParams $serverParams = null, + private array $fieldAttr = [], + private string|array|null $defaultTokenId = null, + ) { } /** @@ -54,11 +50,17 @@ public function buildForm(FormBuilderInterface $builder, array $options): void return; } + $csrfTokenId = $options['csrf_token_id'] + ?: $this->defaultTokenId[$builder->getType()->getInnerType()::class] + ?? $builder->getName() + ?: $builder->getType()->getInnerType()::class; + $builder->setAttribute('csrf_token_id', $csrfTokenId); + $builder ->addEventSubscriber(new CsrfValidationListener( $options['csrf_field_name'], $options['csrf_token_manager'], - $options['csrf_token_id'] ?: ($builder->getName() ?: $builder->getType()->getInnerType()::class), + $csrfTokenId, $options['csrf_message'], $this->translator, $this->translationDomain, @@ -74,12 +76,13 @@ public function finishView(FormView $view, FormInterface $form, array $options): { if ($options['csrf_protection'] && !$view->parent && $options['compound']) { $factory = $form->getConfig()->getFormFactory(); - $tokenId = $options['csrf_token_id'] ?: ($form->getName() ?: $form->getConfig()->getType()->getInnerType()::class); + $tokenId = $form->getConfig()->getAttribute('csrf_token_id'); $data = (string) $options['csrf_token_manager']->getToken($tokenId); $csrfForm = $factory->createNamed($options['csrf_field_name'], HiddenType::class, $data, [ 'block_prefix' => 'csrf_token', 'mapped' => false, + 'attr' => $this->fieldAttr, ]); $view->children[$options['csrf_field_name']] = $csrfForm->createView($view); @@ -88,13 +91,26 @@ public function finishView(FormView $view, FormInterface $form, array $options): public function configureOptions(OptionsResolver $resolver): void { + if (\is_string($defaultTokenId = $this->defaultTokenId) && $defaultTokenId) { + $defaultTokenManager = $this->defaultTokenManager; + $defaultTokenId = static fn (Options $options) => $options['csrf_token_manager'] === $defaultTokenManager ? $defaultTokenId : null; + } else { + $defaultTokenId = null; + } + $resolver->setDefaults([ 'csrf_protection' => $this->defaultEnabled, 'csrf_field_name' => $this->defaultFieldName, 'csrf_message' => 'The CSRF token is invalid. Please try to resubmit the form.', 'csrf_token_manager' => $this->defaultTokenManager, - 'csrf_token_id' => null, + 'csrf_token_id' => $defaultTokenId, ]); + + $resolver->setAllowedTypes('csrf_protection', 'bool'); + $resolver->setAllowedTypes('csrf_field_name', 'string'); + $resolver->setAllowedTypes('csrf_message', 'string'); + $resolver->setAllowedTypes('csrf_token_manager', CsrfTokenManagerInterface::class); + $resolver->setAllowedTypes('csrf_token_id', ['null', 'string']); } public static function getExtendedTypes(): iterable diff --git a/Extension/DataCollector/DataCollectorExtension.php b/Extension/DataCollector/DataCollectorExtension.php index 50b36bd67..9fb842244 100644 --- a/Extension/DataCollector/DataCollectorExtension.php +++ b/Extension/DataCollector/DataCollectorExtension.php @@ -21,11 +21,9 @@ */ class DataCollectorExtension extends AbstractExtension { - private FormDataCollectorInterface $dataCollector; - - public function __construct(FormDataCollectorInterface $dataCollector) - { - $this->dataCollector = $dataCollector; + public function __construct( + private FormDataCollectorInterface $dataCollector, + ) { } protected function loadTypeExtensions(): array diff --git a/Extension/DataCollector/EventListener/DataCollectorListener.php b/Extension/DataCollector/EventListener/DataCollectorListener.php index f32dc9bc7..c541efec2 100644 --- a/Extension/DataCollector/EventListener/DataCollectorListener.php +++ b/Extension/DataCollector/EventListener/DataCollectorListener.php @@ -24,18 +24,16 @@ */ class DataCollectorListener implements EventSubscriberInterface { - private FormDataCollectorInterface $dataCollector; - - public function __construct(FormDataCollectorInterface $dataCollector) - { - $this->dataCollector = $dataCollector; + public function __construct( + private FormDataCollectorInterface $dataCollector, + ) { } public static function getSubscribedEvents(): array { return [ - // High priority in order to be called as soon as possible - FormEvents::POST_SET_DATA => ['postSetData', 255], + // Low priority in order to be called as late as possible + FormEvents::POST_SET_DATA => ['postSetData', -255], // Low priority in order to be called as late as possible FormEvents::POST_SUBMIT => ['postSubmit', -255], ]; diff --git a/Extension/DataCollector/FormDataCollector.php b/Extension/DataCollector/FormDataCollector.php index 1343592b1..e6cae863e 100644 --- a/Extension/DataCollector/FormDataCollector.php +++ b/Extension/DataCollector/FormDataCollector.php @@ -33,8 +33,6 @@ */ class FormDataCollector extends DataCollector implements FormDataCollectorInterface { - private FormDataExtractorInterface $dataExtractor; - /** * Stores the collected data per {@link FormInterface} instance. * @@ -62,14 +60,13 @@ class FormDataCollector extends DataCollector implements FormDataCollectorInterf */ private array $formsByView; - public function __construct(FormDataExtractorInterface $dataExtractor) - { + public function __construct( + private FormDataExtractorInterface $dataExtractor, + ) { if (!class_exists(ClassStub::class)) { - throw new \LogicException(sprintf('The VarDumper component is needed for using the "%s" class. Install symfony/var-dumper version 3.4 or above.', __CLASS__)); + throw new \LogicException(\sprintf('The VarDumper component is needed for using the "%s" class. Install symfony/var-dumper version 3.4 or above.', __CLASS__)); } - $this->dataExtractor = $dataExtractor; - $this->reset(); } diff --git a/Extension/DataCollector/FormDataExtractor.php b/Extension/DataCollector/FormDataExtractor.php index 158cf3210..f56fe911f 100644 --- a/Extension/DataCollector/FormDataExtractor.php +++ b/Extension/DataCollector/FormDataExtractor.php @@ -103,15 +103,13 @@ public function extractSubmittedData(FormInterface $form): array continue; } + $errorData['trace'][] = $cause; if ($cause instanceof \Exception) { - $errorData['trace'][] = $cause; $cause = $cause->getPrevious(); continue; } - $errorData['trace'][] = $cause; - break; } diff --git a/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php b/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php index 7cbc72b47..90e28a61f 100644 --- a/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php +++ b/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php @@ -27,13 +27,10 @@ */ class ResolvedTypeDataCollectorProxy implements ResolvedFormTypeInterface { - private ResolvedFormTypeInterface $proxiedType; - private FormDataCollectorInterface $dataCollector; - - public function __construct(ResolvedFormTypeInterface $proxiedType, FormDataCollectorInterface $dataCollector) - { - $this->proxiedType = $proxiedType; - $this->dataCollector = $dataCollector; + public function __construct( + private ResolvedFormTypeInterface $proxiedType, + private FormDataCollectorInterface $dataCollector, + ) { } public function getBlockPrefix(): string diff --git a/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php b/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php index f93448412..a052a1782 100644 --- a/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php +++ b/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php @@ -24,13 +24,10 @@ */ class ResolvedTypeFactoryDataCollectorProxy implements ResolvedFormTypeFactoryInterface { - private ResolvedFormTypeFactoryInterface $proxiedFactory; - private FormDataCollectorInterface $dataCollector; - - public function __construct(ResolvedFormTypeFactoryInterface $proxiedFactory, FormDataCollectorInterface $dataCollector) - { - $this->proxiedFactory = $proxiedFactory; - $this->dataCollector = $dataCollector; + public function __construct( + private ResolvedFormTypeFactoryInterface $proxiedFactory, + private FormDataCollectorInterface $dataCollector, + ) { } public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ?ResolvedFormTypeInterface $parent = null): ResolvedFormTypeInterface diff --git a/Extension/DependencyInjection/DependencyInjectionExtension.php b/Extension/DependencyInjection/DependencyInjectionExtension.php index 6564bd565..f986bda41 100644 --- a/Extension/DependencyInjection/DependencyInjectionExtension.php +++ b/Extension/DependencyInjection/DependencyInjectionExtension.php @@ -23,24 +23,21 @@ class DependencyInjectionExtension implements FormExtensionInterface { private ?FormTypeGuesserChain $guesser = null; private bool $guesserLoaded = false; - private ContainerInterface $typeContainer; - private array $typeExtensionServices; - private iterable $guesserServices; /** * @param array> $typeExtensionServices */ - public function __construct(ContainerInterface $typeContainer, array $typeExtensionServices, iterable $guesserServices) - { - $this->typeContainer = $typeContainer; - $this->typeExtensionServices = $typeExtensionServices; - $this->guesserServices = $guesserServices; + public function __construct( + private ContainerInterface $typeContainer, + private array $typeExtensionServices, + private iterable $guesserServices, + ) { } public function getType(string $name): FormTypeInterface { if (!$this->typeContainer->has($name)) { - throw new InvalidArgumentException(sprintf('The field type "%s" is not registered in the service container.', $name)); + throw new InvalidArgumentException(\sprintf('The field type "%s" is not registered in the service container.', $name)); } return $this->typeContainer->get($name); @@ -66,7 +63,7 @@ public function getTypeExtensions(string $name): array // validate the result of getExtendedTypes() to ensure it is consistent with the service definition if (!\in_array($name, $extendedTypes, true)) { - throw new InvalidArgumentException(sprintf('The extended type "%s" specified for the type extension class "%s" does not match any of the actual extended types (["%s"]).', $name, $extension::class, implode('", "', $extendedTypes))); + throw new InvalidArgumentException(\sprintf('The extended type "%s" specified for the type extension class "%s" does not match any of the actual extended types (["%s"]).', $name, $extension::class, implode('", "', $extendedTypes))); } } } diff --git a/Extension/PasswordHasher/EventListener/PasswordHasherListener.php b/Extension/PasswordHasher/EventListener/PasswordHasherListener.php index 3ddac5ff3..0144cc3ff 100644 --- a/Extension/PasswordHasher/EventListener/PasswordHasherListener.php +++ b/Extension/PasswordHasher/EventListener/PasswordHasherListener.php @@ -95,7 +95,7 @@ private function getUser(FormInterface $form): PasswordAuthenticatedUserInterfac $parent = $this->getTargetForm($form)->getParent(); if (!($user = $parent?->getData()) || !$user instanceof PasswordAuthenticatedUserInterface) { - throw new InvalidConfigurationException(sprintf('The "hash_property_path" option only supports "%s" objects, "%s" given.', PasswordAuthenticatedUserInterface::class, get_debug_type($user))); + throw new InvalidConfigurationException(\sprintf('The "hash_property_path" option only supports "%s" objects, "%s" given.', PasswordAuthenticatedUserInterface::class, get_debug_type($user))); } return $user; diff --git a/Extension/Validator/Constraints/FormValidator.php b/Extension/Validator/Constraints/FormValidator.php index 4a05981a8..8f4ec60f2 100644 --- a/Extension/Validator/Constraints/FormValidator.php +++ b/Extension/Validator/Constraints/FormValidator.php @@ -56,7 +56,7 @@ public function validate(mixed $form, Constraint $formConstraint): void // Validate the data against its own constraints $validateDataGraph = $form->isRoot() && (\is_object($data) || \is_array($data)) - && (($groups && \is_array($groups)) || ($groups instanceof GroupSequence && $groups->groups)) + && (\is_array($groups) || ($groups instanceof GroupSequence && $groups->groups)) ; // Validate the data against the constraints defined in the form @@ -92,7 +92,7 @@ public function validate(mixed $form, Constraint $formConstraint): void $fieldFormConstraint = new Form(); $fieldFormConstraint->groups = $group; $this->context->setNode($this->context->getValue(), $field, $this->context->getMetadata(), $this->context->getPropertyPath()); - $validator->atPath(sprintf('children[%s]', $field->getName()))->validate($field, $fieldFormConstraint, $group); + $validator->atPath(\sprintf('children[%s]', $field->getName()))->validate($field, $fieldFormConstraint, $group); } } @@ -120,7 +120,7 @@ public function validate(mixed $form, Constraint $formConstraint): void // Otherwise validate a constraint only once for the first // matching group foreach ($groups as $group) { - if (\in_array($group, $constraint->groups)) { + if (\in_array($group, $constraint->groups, true)) { $groupedConstraints[$group][] = $constraint; // Prevent duplicate validation @@ -139,7 +139,7 @@ public function validate(mixed $form, Constraint $formConstraint): void if ($field->isSubmitted()) { $this->resolvedGroups[$field] = $groups; $this->context->setNode($this->context->getValue(), $field, $this->context->getMetadata(), $this->context->getPropertyPath()); - $validator->atPath(sprintf('children[%s]', $field->getName()))->validate($field, $formConstraint); + $validator->atPath(\sprintf('children[%s]', $field->getName()))->validate($field, $formConstraint); } } } @@ -156,7 +156,7 @@ public function validate(mixed $form, Constraint $formConstraint): void if (!$child->isSynchronized()) { $childrenSynchronized = false; $this->context->setNode($this->context->getValue(), $child, $this->context->getMetadata(), $this->context->getPropertyPath()); - $validator->atPath(sprintf('children[%s]', $child->getName()))->validate($child, $formConstraint); + $validator->atPath(\sprintf('children[%s]', $child->getName()))->validate($child, $formConstraint); } } diff --git a/Extension/Validator/EventListener/ValidationListener.php b/Extension/Validator/EventListener/ValidationListener.php index d24264f24..b3c16dbad 100644 --- a/Extension/Validator/EventListener/ValidationListener.php +++ b/Extension/Validator/EventListener/ValidationListener.php @@ -23,18 +23,15 @@ */ class ValidationListener implements EventSubscriberInterface { - private ValidatorInterface $validator; - private ViolationMapperInterface $violationMapper; - public static function getSubscribedEvents(): array { return [FormEvents::POST_SUBMIT => 'validateForm']; } - public function __construct(ValidatorInterface $validator, ViolationMapperInterface $violationMapper) - { - $this->validator = $validator; - $this->violationMapper = $violationMapper; + public function __construct( + private ValidatorInterface $validator, + private ViolationMapperInterface $violationMapper, + ) { } public function validateForm(FormEvent $event): void diff --git a/Extension/Validator/Type/BaseValidatorExtension.php b/Extension/Validator/Type/BaseValidatorExtension.php index 4b3fb4962..6b70df75f 100644 --- a/Extension/Validator/Type/BaseValidatorExtension.php +++ b/Extension/Validator/Type/BaseValidatorExtension.php @@ -32,7 +32,7 @@ public function configureOptions(OptionsResolver $resolver): void return []; } - if (empty($groups)) { + if (!$groups) { return null; } diff --git a/Extension/Validator/Type/FormTypeValidatorExtension.php b/Extension/Validator/Type/FormTypeValidatorExtension.php index 867550714..14029e339 100644 --- a/Extension/Validator/Type/FormTypeValidatorExtension.php +++ b/Extension/Validator/Type/FormTypeValidatorExtension.php @@ -27,13 +27,14 @@ */ class FormTypeValidatorExtension extends BaseValidatorExtension { - private ValidatorInterface $validator; private ViolationMapper $violationMapper; - private bool $legacyErrorMessages; - public function __construct(ValidatorInterface $validator, bool $legacyErrorMessages = true, ?FormRendererInterface $formRenderer = null, ?TranslatorInterface $translator = null) - { - $this->validator = $validator; + public function __construct( + private ValidatorInterface $validator, + private bool $legacyErrorMessages = true, + ?FormRendererInterface $formRenderer = null, + ?TranslatorInterface $translator = null, + ) { $this->violationMapper = new ViolationMapper($formRenderer, $translator); } diff --git a/Extension/Validator/Type/UploadValidatorExtension.php b/Extension/Validator/Type/UploadValidatorExtension.php index 9a26810c6..7c1e965ab 100644 --- a/Extension/Validator/Type/UploadValidatorExtension.php +++ b/Extension/Validator/Type/UploadValidatorExtension.php @@ -23,13 +23,10 @@ */ class UploadValidatorExtension extends AbstractTypeExtension { - private TranslatorInterface $translator; - private ?string $translationDomain; - - public function __construct(TranslatorInterface $translator, ?string $translationDomain = null) - { - $this->translator = $translator; - $this->translationDomain = $translationDomain; + public function __construct( + private TranslatorInterface $translator, + private ?string $translationDomain = null, + ) { } public function configureOptions(OptionsResolver $resolver): void diff --git a/Extension/Validator/ValidatorExtension.php b/Extension/Validator/ValidatorExtension.php index d7745be07..2c534481c 100644 --- a/Extension/Validator/ValidatorExtension.php +++ b/Extension/Validator/ValidatorExtension.php @@ -27,15 +27,12 @@ */ class ValidatorExtension extends AbstractExtension { - private ValidatorInterface $validator; - private ?FormRendererInterface $formRenderer; - private ?TranslatorInterface $translator; - private bool $legacyErrorMessages; - - public function __construct(ValidatorInterface $validator, bool $legacyErrorMessages = true, ?FormRendererInterface $formRenderer = null, ?TranslatorInterface $translator = null) - { - $this->legacyErrorMessages = $legacyErrorMessages; - + public function __construct( + private ValidatorInterface $validator, + private bool $legacyErrorMessages = true, + private ?FormRendererInterface $formRenderer = null, + private ?TranslatorInterface $translator = null, + ) { $metadata = $validator->getMetadataFor(\Symfony\Component\Form\Form::class); // Register the form constraints in the validator programmatically. @@ -43,13 +40,9 @@ public function __construct(ValidatorInterface $validator, bool $legacyErrorMess // the DIC, where the XML file is loaded automatically. Thus the following // code must be kept synchronized with validation.xml - /* @var $metadata ClassMetadata */ + /* @var ClassMetadata $metadata */ $metadata->addConstraint(new Form()); $metadata->addConstraint(new Traverse(false)); - - $this->validator = $validator; - $this->formRenderer = $formRenderer; - $this->translator = $translator; } public function loadTypeGuesser(): ?FormTypeGuesserInterface diff --git a/Extension/Validator/ValidatorTypeGuesser.php b/Extension/Validator/ValidatorTypeGuesser.php index 57bccaa39..08dc6e2d5 100644 --- a/Extension/Validator/ValidatorTypeGuesser.php +++ b/Extension/Validator/ValidatorTypeGuesser.php @@ -57,11 +57,9 @@ class ValidatorTypeGuesser implements FormTypeGuesserInterface { - private MetadataFactoryInterface $metadataFactory; - - public function __construct(MetadataFactoryInterface $metadataFactory) - { - $this->metadataFactory = $metadataFactory; + public function __construct( + private MetadataFactoryInterface $metadataFactory, + ) { } public function guessType(string $class, string $property): ?TypeGuess @@ -232,7 +230,7 @@ public function guessPatternForConstraint(Constraint $constraint): ?ValueGuess switch ($constraint::class) { case Length::class: if (is_numeric($constraint->min)) { - return new ValueGuess(sprintf('.{%s,}', (string) $constraint->min), Guess::LOW_CONFIDENCE); + return new ValueGuess(\sprintf('.{%s,}', (string) $constraint->min), Guess::LOW_CONFIDENCE); } break; @@ -246,7 +244,7 @@ public function guessPatternForConstraint(Constraint $constraint): ?ValueGuess case Range::class: if (is_numeric($constraint->min)) { - return new ValueGuess(sprintf('.{%s,}', \strlen((string) $constraint->min)), Guess::LOW_CONFIDENCE); + return new ValueGuess(\sprintf('.{%s,}', \strlen((string) $constraint->min)), Guess::LOW_CONFIDENCE); } break; diff --git a/Extension/Validator/ViolationMapper/MappingRule.php b/Extension/Validator/ViolationMapper/MappingRule.php index 6e33f2229..3263f66df 100644 --- a/Extension/Validator/ViolationMapper/MappingRule.php +++ b/Extension/Validator/ViolationMapper/MappingRule.php @@ -19,15 +19,11 @@ */ class MappingRule { - private FormInterface $origin; - private string $propertyPath; - private string $targetPath; - - public function __construct(FormInterface $origin, string $propertyPath, string $targetPath) - { - $this->origin = $origin; - $this->propertyPath = $propertyPath; - $this->targetPath = $targetPath; + public function __construct( + private FormInterface $origin, + private string $propertyPath, + private string $targetPath, + ) { } public function getOrigin(): FormInterface @@ -68,7 +64,7 @@ public function getTarget(): FormInterface foreach ($childNames as $childName) { if (!$target->has($childName)) { - throw new ErrorMappingException(sprintf('The child "%s" of "%s" mapped by the rule "%s" in "%s" does not exist.', $childName, $target->getName(), $this->targetPath, $this->origin->getName())); + throw new ErrorMappingException(\sprintf('The child "%s" of "%s" mapped by the rule "%s" in "%s" does not exist.', $childName, $target->getName(), $this->targetPath, $this->origin->getName())); } $target = $target->get($childName); } diff --git a/Extension/Validator/ViolationMapper/RelativePath.php b/Extension/Validator/ViolationMapper/RelativePath.php index 0384edb44..c13993374 100644 --- a/Extension/Validator/ViolationMapper/RelativePath.php +++ b/Extension/Validator/ViolationMapper/RelativePath.php @@ -19,13 +19,11 @@ */ class RelativePath extends PropertyPath { - private FormInterface $root; - - public function __construct(FormInterface $root, string $propertyPath) - { + public function __construct( + private FormInterface $root, + string $propertyPath, + ) { parent::__construct($propertyPath); - - $this->root = $root; } public function getRoot(): FormInterface diff --git a/Extension/Validator/ViolationMapper/ViolationMapper.php b/Extension/Validator/ViolationMapper/ViolationMapper.php index 13e21798a..faca255b5 100644 --- a/Extension/Validator/ViolationMapper/ViolationMapper.php +++ b/Extension/Validator/ViolationMapper/ViolationMapper.php @@ -28,14 +28,12 @@ */ class ViolationMapper implements ViolationMapperInterface { - private ?FormRendererInterface $formRenderer; - private ?TranslatorInterface $translator; private bool $allowNonSynchronized = false; - public function __construct(?FormRendererInterface $formRenderer = null, ?TranslatorInterface $translator = null) - { - $this->formRenderer = $formRenderer; - $this->translator = $translator; + public function __construct( + private ?FormRendererInterface $formRenderer = null, + private ?TranslatorInterface $translator = null, + ) { } public function mapViolation(ConstraintViolation $violation, FormInterface $form, bool $allowNonSynchronized = false): void diff --git a/Extension/Validator/ViolationMapper/ViolationPath.php b/Extension/Validator/ViolationMapper/ViolationPath.php index a9a0f15d6..0c2a130cc 100644 --- a/Extension/Validator/ViolationMapper/ViolationPath.php +++ b/Extension/Validator/ViolationMapper/ViolationPath.php @@ -132,7 +132,7 @@ public function getElements(): array public function getElement(int $index): string { if (!isset($this->elements[$index])) { - throw new OutOfBoundsException(sprintf('The index "%s" is not within the violation path.', $index)); + throw new OutOfBoundsException(\sprintf('The index "%s" is not within the violation path.', $index)); } return $this->elements[$index]; @@ -141,7 +141,7 @@ public function getElement(int $index): string public function isProperty(int $index): bool { if (!isset($this->isIndex[$index])) { - throw new OutOfBoundsException(sprintf('The index "%s" is not within the violation path.', $index)); + throw new OutOfBoundsException(\sprintf('The index "%s" is not within the violation path.', $index)); } return !$this->isIndex[$index]; @@ -150,7 +150,7 @@ public function isProperty(int $index): bool public function isIndex(int $index): bool { if (!isset($this->isIndex[$index])) { - throw new OutOfBoundsException(sprintf('The index "%s" is not within the violation path.', $index)); + throw new OutOfBoundsException(\sprintf('The index "%s" is not within the violation path.', $index)); } return $this->isIndex[$index]; @@ -176,7 +176,7 @@ public function isNullSafe(int $index): bool public function mapsForm(int $index): bool { if (!isset($this->mapsForm[$index])) { - throw new OutOfBoundsException(sprintf('The index "%s" is not within the violation path.', $index)); + throw new OutOfBoundsException(\sprintf('The index "%s" is not within the violation path.', $index)); } return $this->mapsForm[$index]; diff --git a/Form.php b/Form.php index 7d46368c1..72c60ee41 100644 --- a/Form.php +++ b/Form.php @@ -21,7 +21,6 @@ use Symfony\Component\Form\Exception\OutOfBoundsException; use Symfony\Component\Form\Exception\RuntimeException; use Symfony\Component\Form\Exception\TransformationFailedException; -use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Util\FormUtil; use Symfony\Component\Form\Util\InheritDataAwareIterator; @@ -71,7 +70,6 @@ */ class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterface { - private FormConfigInterface $config; private ?FormInterface $parent = null; /** @@ -136,8 +134,9 @@ class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterfac /** * @throws LogicException if a data mapper is not provided for a compound form */ - public function __construct(FormConfigInterface $config) - { + public function __construct( + private FormConfigInterface $config, + ) { // Compound forms always need a data mapper, otherwise calls to // `setData` and `add` will not lead to the correct population of // the child forms. @@ -151,7 +150,6 @@ public function __construct(FormConfigInterface $config) $this->defaultDataSet = true; } - $this->config = $config; $this->children = new OrderedHashMap(); $this->name = $config->getName(); } @@ -615,7 +613,7 @@ public function isSynchronized(): bool return null === $this->transformationFailure; } - public function getTransformationFailure(): ?Exception\TransformationFailedException + public function getTransformationFailure(): ?TransformationFailedException { return $this->transformationFailure; } @@ -727,12 +725,6 @@ public function add(FormInterface|string $child, ?string $type = null, array $op } if (!$child instanceof FormInterface) { - if (!\is_string($child) && !\is_int($child)) { - throw new UnexpectedTypeException($child, 'string or Symfony\Component\Form\FormInterface'); - } - - $child = (string) $child; - // Never initialize child forms automatically $options['auto_initialize'] = false; @@ -746,7 +738,7 @@ public function add(FormInterface|string $child, ?string $type = null, array $op $child = $this->config->getFormFactory()->createNamed($child, $type, null, $options); } } elseif ($child->getConfig()->getAutoInitialize()) { - throw new RuntimeException(sprintf('Automatic initialization is only supported on root forms. You should set the "auto_initialize" option to false on the field "%s".', $child->getName())); + throw new RuntimeException(\sprintf('Automatic initialization is only supported on root forms. You should set the "auto_initialize" option to false on the field "%s".', $child->getName())); } $this->children[$child->getName()] = $child; @@ -808,7 +800,7 @@ public function get(string $name): FormInterface return $this->children[$name]; } - throw new OutOfBoundsException(sprintf('Child "%s" does not exist.', $name)); + throw new OutOfBoundsException(\sprintf('Child "%s" does not exist.', $name)); } /** @@ -941,7 +933,7 @@ private function modelToNorm(mixed $value): mixed $value = $transformer->transform($value); } } catch (TransformationFailedException $exception) { - throw new TransformationFailedException(sprintf('Unable to transform data for property path "%s": ', $this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters()); + throw new TransformationFailedException(\sprintf('Unable to transform data for property path "%s": ', $this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters()); } return $value; @@ -961,7 +953,7 @@ private function normToModel(mixed $value): mixed $value = $transformers[$i]->reverseTransform($value); } } catch (TransformationFailedException $exception) { - throw new TransformationFailedException(sprintf('Unable to reverse value for property path "%s": ', $this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters()); + throw new TransformationFailedException(\sprintf('Unable to reverse value for property path "%s": ', $this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters()); } return $value; @@ -988,7 +980,7 @@ private function normToView(mixed $value): mixed $value = $transformer->transform($value); } } catch (TransformationFailedException $exception) { - throw new TransformationFailedException(sprintf('Unable to transform value for property path "%s": ', $this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters()); + throw new TransformationFailedException(\sprintf('Unable to transform value for property path "%s": ', $this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters()); } return $value; @@ -1010,7 +1002,7 @@ private function viewToNorm(mixed $value): mixed $value = $transformers[$i]->reverseTransform($value); } } catch (TransformationFailedException $exception) { - throw new TransformationFailedException(sprintf('Unable to reverse value for property path "%s": ', $this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters()); + throw new TransformationFailedException(\sprintf('Unable to reverse value for property path "%s": ', $this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters()); } return $value; diff --git a/FormBuilder.php b/FormBuilder.php index 54a2104c4..f34c2f76e 100644 --- a/FormBuilder.php +++ b/FormBuilder.php @@ -14,7 +14,6 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Form\Exception\BadMethodCallException; use Symfony\Component\Form\Exception\InvalidArgumentException; -use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\Extension\Core\Type\TextType; /** @@ -60,10 +59,6 @@ public function add(FormBuilderInterface|string $child, ?string $type = null, ar return $this; } - if (!\is_string($child) && !\is_int($child)) { - throw new UnexpectedTypeException($child, 'string or Symfony\Component\Form\FormBuilderInterface'); - } - // Add to "children" to maintain order $this->children[$child] = null; $this->unresolvedChildren[$child] = [$type, $options]; @@ -102,7 +97,7 @@ public function get(string $name): FormBuilderInterface return $this->children[$name]; } - throw new InvalidArgumentException(sprintf('The child with the name "%s" does not exist.', $name)); + throw new InvalidArgumentException(\sprintf('The child with the name "%s" does not exist.', $name)); } public function remove(string $name): static @@ -147,7 +142,7 @@ public function count(): int public function getFormConfig(): FormConfigInterface { - /** @var $config self */ + /** @var self $config */ $config = parent::getFormConfig(); $config->children = []; diff --git a/FormConfigBuilder.php b/FormConfigBuilder.php index 49aa89ab0..5d6bd653d 100644 --- a/FormConfigBuilder.php +++ b/FormConfigBuilder.php @@ -33,7 +33,6 @@ class FormConfigBuilder implements FormConfigBuilderInterface */ private static NativeRequestHandler $nativeRequestHandler; - private EventDispatcherInterface $dispatcher; private string $name; private ?PropertyPathInterface $propertyPath = null; private bool $mapped = true; @@ -57,7 +56,6 @@ class FormConfigBuilder implements FormConfigBuilderInterface private string $method = 'POST'; private RequestHandlerInterface $requestHandler; private bool $autoInitialize = false; - private array $options; private ?\Closure $isEmptyCallback = null; /** @@ -69,18 +67,20 @@ class FormConfigBuilder implements FormConfigBuilderInterface * @throws InvalidArgumentException if the data class is not a valid class or if * the name contains invalid characters */ - public function __construct(?string $name, ?string $dataClass, EventDispatcherInterface $dispatcher, array $options = []) - { + public function __construct( + ?string $name, + ?string $dataClass, + private EventDispatcherInterface $dispatcher, + private array $options = [], + ) { self::validateName($name); if (null !== $dataClass && !class_exists($dataClass) && !interface_exists($dataClass, false)) { - throw new InvalidArgumentException(sprintf('Class "%s" not found. Is the "data_class" form option set correctly?', $dataClass)); + throw new InvalidArgumentException(\sprintf('Class "%s" not found. Is the "data_class" form option set correctly?', $dataClass)); } $this->name = (string) $name; $this->dataClass = $dataClass; - $this->dispatcher = $dispatcher; - $this->options = $options; } public function addEventListener(string $eventName, callable $listener, int $priority = 0): static @@ -632,7 +632,7 @@ public function setIsEmptyCallback(?callable $isEmptyCallback): static final public static function validateName(?string $name): void { if (!self::isValidName($name)) { - throw new InvalidArgumentException(sprintf('The name "%s" contains illegal characters. Names should start with a letter, digit or underscore and only contain letters, digits, numbers, underscores ("_"), hyphens ("-") and colons (":").', $name)); + throw new InvalidArgumentException(\sprintf('The name "%s" contains illegal characters. Names should start with a letter, digit or underscore and only contain letters, digits, numbers, underscores ("_"), hyphens ("-") and colons (":").', $name)); } } diff --git a/FormError.php b/FormError.php index f73a4b2c6..335d9e21d 100644 --- a/FormError.php +++ b/FormError.php @@ -21,11 +21,6 @@ class FormError { protected string $messageTemplate; - protected array $messageParameters; - protected ?int $messagePluralization; - - private string $message; - private mixed $cause; /** * The form that spawned this error. @@ -45,13 +40,14 @@ class FormError * * @see \Symfony\Component\Translation\Translator */ - public function __construct(string $message, ?string $messageTemplate = null, array $messageParameters = [], ?int $messagePluralization = null, mixed $cause = null) - { - $this->message = $message; + public function __construct( + private string $message, + ?string $messageTemplate = null, + protected array $messageParameters = [], + protected ?int $messagePluralization = null, + private mixed $cause = null, + ) { $this->messageTemplate = $messageTemplate ?: $message; - $this->messageParameters = $messageParameters; - $this->messagePluralization = $messagePluralization; - $this->cause = $cause; } /** diff --git a/FormErrorIterator.php b/FormErrorIterator.php index 42de0c835..4f4a2574d 100644 --- a/FormErrorIterator.php +++ b/FormErrorIterator.php @@ -42,8 +42,6 @@ class FormErrorIterator implements \RecursiveIterator, \SeekableIterator, \Array */ public const INDENTATION = ' '; - private FormInterface $form; - /** * @var list */ @@ -54,15 +52,16 @@ class FormErrorIterator implements \RecursiveIterator, \SeekableIterator, \Array * * @throws InvalidArgumentException If the errors are invalid */ - public function __construct(FormInterface $form, array $errors) - { + public function __construct( + private FormInterface $form, + array $errors, + ) { foreach ($errors as $error) { if (!($error instanceof FormError || $error instanceof self)) { - throw new InvalidArgumentException(sprintf('The errors must be instances of "Symfony\Component\Form\FormError" or "%s". Got: "%s".', __CLASS__, get_debug_type($error))); + throw new InvalidArgumentException(\sprintf('The errors must be instances of "Symfony\Component\Form\FormError" or "%s". Got: "%s".', __CLASS__, get_debug_type($error))); } } - $this->form = $form; $this->errors = $errors; } @@ -199,7 +198,7 @@ public function hasChildren(): bool public function getChildren(): self { if (!$this->hasChildren()) { - throw new LogicException(sprintf('The current element is not iterable. Use "%s" to get the current element.', self::class.'::current()')); + throw new LogicException(\sprintf('The current element is not iterable. Use "%s" to get the current element.', self::class.'::current()')); } /** @var self $children */ diff --git a/FormEvent.php b/FormEvent.php index e46fbb6a6..e6a3878f6 100644 --- a/FormEvent.php +++ b/FormEvent.php @@ -18,14 +18,10 @@ */ class FormEvent extends Event { - protected mixed $data; - - private FormInterface $form; - - public function __construct(FormInterface $form, mixed $data) - { - $this->form = $form; - $this->data = $data; + public function __construct( + private FormInterface $form, + protected mixed $data, + ) { } /** diff --git a/FormFactory.php b/FormFactory.php index 9e1234f83..dcf7b36f2 100644 --- a/FormFactory.php +++ b/FormFactory.php @@ -16,11 +16,9 @@ class FormFactory implements FormFactoryInterface { - private FormRegistryInterface $registry; - - public function __construct(FormRegistryInterface $registry) - { - $this->registry = $registry; + public function __construct( + private FormRegistryInterface $registry, + ) { } public function create(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface diff --git a/FormFactoryBuilder.php b/FormFactoryBuilder.php index 42b8dec9f..90e3bf20c 100644 --- a/FormFactoryBuilder.php +++ b/FormFactoryBuilder.php @@ -20,8 +20,6 @@ */ class FormFactoryBuilder implements FormFactoryBuilderInterface { - private bool $forceCoreExtension; - private ResolvedFormTypeFactoryInterface $resolvedTypeFactory; /** @@ -44,9 +42,9 @@ class FormFactoryBuilder implements FormFactoryBuilderInterface */ private array $typeGuessers = []; - public function __construct(bool $forceCoreExtension = false) - { - $this->forceCoreExtension = $forceCoreExtension; + public function __construct( + private bool $forceCoreExtension = false, + ) { } public function setResolvedTypeFactory(ResolvedFormTypeFactoryInterface $resolvedTypeFactory): static diff --git a/FormRegistry.php b/FormRegistry.php index ab3f55f9f..ecf654a2a 100644 --- a/FormRegistry.php +++ b/FormRegistry.php @@ -33,8 +33,7 @@ class FormRegistry implements FormRegistryInterface */ private array $types = []; - private FormTypeGuesserInterface|null|false $guesser = false; - private ResolvedFormTypeFactoryInterface $resolvedTypeFactory; + private FormTypeGuesserInterface|false|null $guesser = false; private array $checkedTypes = []; /** @@ -42,8 +41,10 @@ class FormRegistry implements FormRegistryInterface * * @throws UnexpectedTypeException if any extension does not implement FormExtensionInterface */ - public function __construct(array $extensions, ResolvedFormTypeFactoryInterface $resolvedTypeFactory) - { + public function __construct( + array $extensions, + private ResolvedFormTypeFactoryInterface $resolvedTypeFactory, + ) { foreach ($extensions as $extension) { if (!$extension instanceof FormExtensionInterface) { throw new UnexpectedTypeException($extension, FormExtensionInterface::class); @@ -51,7 +52,6 @@ public function __construct(array $extensions, ResolvedFormTypeFactoryInterface } $this->extensions = $extensions; - $this->resolvedTypeFactory = $resolvedTypeFactory; } public function getType(string $name): ResolvedFormTypeInterface @@ -69,10 +69,10 @@ public function getType(string $name): ResolvedFormTypeInterface if (!$type) { // Support fully-qualified class names if (!class_exists($name)) { - throw new InvalidArgumentException(sprintf('Could not load type "%s": class does not exist.', $name)); + throw new InvalidArgumentException(\sprintf('Could not load type "%s": class does not exist.', $name)); } if (!is_subclass_of($name, FormTypeInterface::class)) { - throw new InvalidArgumentException(sprintf('Could not load type "%s": class does not implement "Symfony\Component\Form\FormTypeInterface".', $name)); + throw new InvalidArgumentException(\sprintf('Could not load type "%s": class does not implement "Symfony\Component\Form\FormTypeInterface".', $name)); } $type = new $name(); @@ -94,7 +94,7 @@ private function resolveType(FormTypeInterface $type): ResolvedFormTypeInterface if (isset($this->checkedTypes[$fqcn])) { $types = implode(' > ', array_merge(array_keys($this->checkedTypes), [$fqcn])); - throw new LogicException(sprintf('Circular reference detected for form type "%s" (%s).', $fqcn, $types)); + throw new LogicException(\sprintf('Circular reference detected for form type "%s" (%s).', $fqcn, $types)); } $this->checkedTypes[$fqcn] = true; diff --git a/FormRegistryInterface.php b/FormRegistryInterface.php index b1e77898e..5c76b5c67 100644 --- a/FormRegistryInterface.php +++ b/FormRegistryInterface.php @@ -21,7 +21,7 @@ interface FormRegistryInterface /** * Returns a form type by name. * - * This methods registers the type extensions from the form extensions. + * This method registers the type extensions from the form extensions. * * @throws Exception\InvalidArgumentException if the type cannot be retrieved from any extension */ diff --git a/FormRenderer.php b/FormRenderer.php index 1fd76636b..4478432b2 100644 --- a/FormRenderer.php +++ b/FormRenderer.php @@ -25,16 +25,14 @@ class FormRenderer implements FormRendererInterface { public const CACHE_KEY_VAR = 'unique_block_prefix'; - private FormRendererEngineInterface $engine; - private ?CsrfTokenManagerInterface $csrfTokenManager; private array $blockNameHierarchyMap = []; private array $hierarchyLevelMap = []; private array $variableStack = []; - public function __construct(FormRendererEngineInterface $engine, ?CsrfTokenManagerInterface $csrfTokenManager = null) - { - $this->engine = $engine; - $this->csrfTokenManager = $csrfTokenManager; + public function __construct( + private FormRendererEngineInterface $engine, + private ?CsrfTokenManagerInterface $csrfTokenManager = null, + ) { } public function getEngine(): FormRendererEngineInterface @@ -61,7 +59,7 @@ public function renderBlock(FormView $view, string $blockName, array $variables $resource = $this->engine->getResourceForBlockName($view, $blockName); if (!$resource) { - throw new LogicException(sprintf('No block "%s" found while rendering the form.', $blockName)); + throw new LogicException(\sprintf('No block "%s" found while rendering the form.', $blockName)); } $viewCacheKey = $view->vars[self::CACHE_KEY_VAR]; @@ -118,7 +116,7 @@ public function searchAndRenderBlock(FormView $view, string $blockNameSuffix, ar if ($renderOnlyOnce && $view->isRendered()) { // This is not allowed, because it would result in rendering same IDs multiple times, which is not valid. - throw new BadMethodCallException(sprintf('Field "%s" has already been rendered, save the result of previous render call to a variable and output that instead.', $view->vars['name'])); + throw new BadMethodCallException(\sprintf('Field "%s" has already been rendered, save the result of previous render call to a variable and output that instead.', $view->vars['name'])); } // The cache key for storing the variables and types @@ -205,10 +203,10 @@ public function searchAndRenderBlock(FormView $view, string $blockNameSuffix, ar // Escape if no resource exists for this block if (!$resource) { if (\count($blockNameHierarchy) !== \count(array_unique($blockNameHierarchy))) { - throw new LogicException(sprintf('Unable to render the form because the block names array contains duplicates: "%s".', implode('", "', array_reverse($blockNameHierarchy)))); + throw new LogicException(\sprintf('Unable to render the form because the block names array contains duplicates: "%s".', implode('", "', array_reverse($blockNameHierarchy)))); } - throw new LogicException(sprintf('Unable to render the form as none of the following blocks exist: "%s".', implode('", "', array_reverse($blockNameHierarchy)))); + throw new LogicException(\sprintf('Unable to render the form as none of the following blocks exist: "%s".', implode('", "', array_reverse($blockNameHierarchy)))); } // Merge the passed with the existing attributes diff --git a/FormTypeExtensionInterface.php b/FormTypeExtensionInterface.php index 3e2d2d03e..838406d2a 100644 --- a/FormTypeExtensionInterface.php +++ b/FormTypeExtensionInterface.php @@ -25,9 +25,6 @@ interface FormTypeExtensionInterface */ public static function getExtendedTypes(): iterable; - /** - * @return void - */ public function configureOptions(OptionsResolver $resolver): void; /** diff --git a/FormView.php b/FormView.php index 4695bcdf6..93804bb87 100644 --- a/FormView.php +++ b/FormView.php @@ -29,11 +29,6 @@ class FormView implements \ArrayAccess, \IteratorAggregate, \Countable 'attr' => [], ]; - /** - * The parent view. - */ - public ?self $parent = null; - /** * The child views. * @@ -52,9 +47,12 @@ class FormView implements \ArrayAccess, \IteratorAggregate, \Countable private bool $methodRendered = false; - public function __construct(?self $parent = null) - { - $this->parent = $parent; + /** + * @param FormView|null $parent The parent view + */ + public function __construct( + public ?self $parent = null, + ) { } /** diff --git a/Guess/TypeGuess.php b/Guess/TypeGuess.php index 8ede78eb8..b62873daa 100644 --- a/Guess/TypeGuess.php +++ b/Guess/TypeGuess.php @@ -19,9 +19,6 @@ */ class TypeGuess extends Guess { - private string $type; - private array $options; - /** * @param string $type The guessed field type * @param array $options The options for creating instances of the @@ -29,12 +26,12 @@ class TypeGuess extends Guess * @param int $confidence The confidence that the guessed class name * is correct */ - public function __construct(string $type, array $options, int $confidence) - { + public function __construct( + private string $type, + private array $options, + int $confidence, + ) { parent::__construct($confidence); - - $this->type = $type; - $this->options = $options; } /** diff --git a/Guess/ValueGuess.php b/Guess/ValueGuess.php index 36abe6602..228328747 100644 --- a/Guess/ValueGuess.php +++ b/Guess/ValueGuess.php @@ -18,16 +18,14 @@ */ class ValueGuess extends Guess { - private string|int|bool|null $value; - /** * @param int $confidence The confidence that the guessed class name is correct */ - public function __construct(string|int|bool|null $value, int $confidence) - { + public function __construct( + private string|int|bool|null $value, + int $confidence, + ) { parent::__construct($confidence); - - $this->value = $value; } /** diff --git a/NativeRequestHandler.php b/NativeRequestHandler.php index aec74133e..bee54fa60 100644 --- a/NativeRequestHandler.php +++ b/NativeRequestHandler.php @@ -29,6 +29,7 @@ class NativeRequestHandler implements RequestHandlerInterface */ private const FILE_KEYS = [ 'error', + 'full_path', 'name', 'size', 'tmp_name', @@ -41,7 +42,7 @@ public function __construct(?ServerParams $params = null) } /** - * @throws Exception\UnexpectedTypeException If the $request is not null + * @throws UnexpectedTypeException If the $request is not null */ public function handleRequest(FormInterface $form, mixed $request = null): void { @@ -186,9 +187,7 @@ private static function fixPhpFilesArray(mixed $data): mixed return $data; } - // Remove extra key added by PHP 8.1. - unset($data['full_path']); - $keys = array_keys($data); + $keys = array_keys($data + ['full_path' => null]); sort($keys); if (self::FILE_KEYS !== $keys || !isset($data['name']) || !\is_array($data['name'])) { @@ -207,7 +206,9 @@ private static function fixPhpFilesArray(mixed $data): mixed 'type' => $data['type'][$key], 'tmp_name' => $data['tmp_name'][$key], 'size' => $data['size'][$key], - ]); + ] + (isset($data['full_path'][$key]) ? [ + 'full_path' => $data['full_path'][$key], + ] : [])); } return $files; @@ -219,7 +220,7 @@ private static function stripEmptyFiles(mixed $data): mixed return $data; } - $keys = array_keys($data); + $keys = array_keys($data + ['full_path' => null]); sort($keys); if (self::FILE_KEYS === $keys) { diff --git a/PreloadedExtension.php b/PreloadedExtension.php index 298186a75..26090e00d 100644 --- a/PreloadedExtension.php +++ b/PreloadedExtension.php @@ -21,8 +21,6 @@ class PreloadedExtension implements FormExtensionInterface { private array $types = []; - private array $typeExtensions = []; - private ?FormTypeGuesserInterface $typeGuesser; /** * Creates a new preloaded extension. @@ -30,11 +28,11 @@ class PreloadedExtension implements FormExtensionInterface * @param FormTypeInterface[] $types The types that the extension should support * @param FormTypeExtensionInterface[][] $typeExtensions The type extensions that the extension should support */ - public function __construct(array $types, array $typeExtensions, ?FormTypeGuesserInterface $typeGuesser = null) - { - $this->typeExtensions = $typeExtensions; - $this->typeGuesser = $typeGuesser; - + public function __construct( + array $types, + private array $typeExtensions, + private ?FormTypeGuesserInterface $typeGuesser = null, + ) { foreach ($types as $type) { $this->types[$type::class] = $type; } @@ -43,7 +41,7 @@ public function __construct(array $types, array $typeExtensions, ?FormTypeGuesse public function getType(string $name): FormTypeInterface { if (!isset($this->types[$name])) { - throw new InvalidArgumentException(sprintf('The type "%s" cannot be loaded by this extension.', $name)); + throw new InvalidArgumentException(\sprintf('The type "%s" cannot be loaded by this extension.', $name)); } return $this->types[$name]; diff --git a/ResolvedFormType.php b/ResolvedFormType.php index f55bfb55a..82065f651 100644 --- a/ResolvedFormType.php +++ b/ResolvedFormType.php @@ -23,31 +23,28 @@ */ class ResolvedFormType implements ResolvedFormTypeInterface { - private FormTypeInterface $innerType; - /** * @var FormTypeExtensionInterface[] */ private array $typeExtensions; - private ?ResolvedFormTypeInterface $parent; - private OptionsResolver $optionsResolver; /** * @param FormTypeExtensionInterface[] $typeExtensions */ - public function __construct(FormTypeInterface $innerType, array $typeExtensions = [], ?ResolvedFormTypeInterface $parent = null) - { + public function __construct( + private FormTypeInterface $innerType, + array $typeExtensions = [], + private ?ResolvedFormTypeInterface $parent = null, + ) { foreach ($typeExtensions as $extension) { if (!$extension instanceof FormTypeExtensionInterface) { throw new UnexpectedTypeException($extension, FormTypeExtensionInterface::class); } } - $this->innerType = $innerType; $this->typeExtensions = $typeExtensions; - $this->parent = $parent; } public function getBlockPrefix(): string @@ -75,7 +72,7 @@ public function createBuilder(FormFactoryInterface $factory, string $name, array try { $options = $this->getOptionsResolver()->resolve($options); } catch (ExceptionInterface $e) { - throw new $e(sprintf('An error has occurred resolving the options of the form "%s": ', get_debug_type($this->getInnerType())).$e->getMessage(), $e->getCode(), $e); + throw new $e(\sprintf('An error has occurred resolving the options of the form "%s": ', get_debug_type($this->getInnerType())).$e->getMessage(), $e->getCode(), $e); } // Should be decoupled from the specific option at some point diff --git a/Resources/translations/validators.es.xlf b/Resources/translations/validators.es.xlf index 301e2b33f..a9989737c 100644 --- a/Resources/translations/validators.es.xlf +++ b/Resources/translations/validators.es.xlf @@ -52,7 +52,7 @@ Please enter a valid date. - Por favor, ingrese una fecha valida. + Por favor, ingrese una fecha válida. Please select a valid file. diff --git a/Resources/translations/validators.pt.xlf b/Resources/translations/validators.pt.xlf index 755108f35..673e79f42 100644 --- a/Resources/translations/validators.pt.xlf +++ b/Resources/translations/validators.pt.xlf @@ -24,7 +24,7 @@ The selected choice is invalid. - A escolha seleccionada é inválida. + A escolha selecionada é inválida. The collection is invalid. diff --git a/Resources/translations/validators.zh_TW.xlf b/Resources/translations/validators.zh_TW.xlf index 831759783..0a76ab7a7 100644 --- a/Resources/translations/validators.zh_TW.xlf +++ b/Resources/translations/validators.zh_TW.xlf @@ -4,31 +4,31 @@ This form should not contain extra fields. - 該表單中不可有額外字段。 + 此表單不應包含其他欄位。 The uploaded file was too large. Please try to upload a smaller file. - 上傳文件太大, 請重新嘗試上傳一個較小的文件。 + 上傳的檔案過大。請嘗試上傳較小的檔案。 The CSRF token is invalid. Please try to resubmit the form. - CSRF 驗證符無效, 請重新提交。 + CSRF token 無效。請重新提交表單。 This value is not a valid HTML5 color. - 該數值不是個有效的 HTML5 顏色。 + 這個數值不是有效的 HTML5 顏色。 Please enter a valid birthdate. - 請輸入有效的生日日期。 + 請輸入有效的出生日期。 The selected choice is invalid. - 所選的選項無效。 + 選取的選項無效。 The collection is invalid. - 集合無效。 + 這個集合無效。 Please select a valid color. @@ -44,11 +44,11 @@ Please choose a valid date interval. - 請選擇有效的日期間隔。 + 請選擇有效的日期區間。 Please enter a valid date and time. - 請輸入有效的日期與時間。 + 請輸入有效的日期和時間。 Please enter a valid date. @@ -56,11 +56,11 @@ Please select a valid file. - 請選擇有效的文件。 + 請選擇有效的檔案。 The hidden field is invalid. - 隱藏字段無效。 + 隱藏欄位無效。 Please enter an integer. @@ -72,11 +72,11 @@ Please select a valid locale. - 請選擇有效的語言環境。 + 請選擇有效的語系。 Please enter a valid money amount. - 請輸入正確的金額。 + 請輸入有效的金額。 Please enter a number. @@ -88,11 +88,11 @@ Please enter a percentage value. - 請輸入百分比值。 + 請輸入百分比數值。 The values do not match. - 數值不匹配。 + 數值不相符。 Please enter a valid time. @@ -104,19 +104,19 @@ Please enter a valid URL. - 請輸入有效的網址。 + 請輸入有效的 URL。 Please enter a valid search term. - 請輸入有效的搜索詞。 + 請輸入有效的搜尋關鍵字。 Please provide a valid phone number. - 請提供有效的手機號碼。 + 請提供有效的電話號碼。 The checkbox has an invalid value. - 無效的選框值。 + 核取方塊上有無效的值。 Please enter a valid email address. diff --git a/ReversedTransformer.php b/ReversedTransformer.php index b68387908..4aa92450a 100644 --- a/ReversedTransformer.php +++ b/ReversedTransformer.php @@ -21,11 +21,9 @@ */ class ReversedTransformer implements DataTransformerInterface { - protected DataTransformerInterface $reversedTransformer; - - public function __construct(DataTransformerInterface $reversedTransformer) - { - $this->reversedTransformer = $reversedTransformer; + public function __construct( + protected DataTransformerInterface $reversedTransformer, + ) { } public function transform(mixed $value): mixed diff --git a/Test/FormIntegrationTestCase.php b/Test/FormIntegrationTestCase.php index 5bf37fd48..8756d9968 100644 --- a/Test/FormIntegrationTestCase.php +++ b/Test/FormIntegrationTestCase.php @@ -12,8 +12,12 @@ namespace Symfony\Component\Form\Test; use PHPUnit\Framework\TestCase; +use Symfony\Component\Form\FormExtensionInterface; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\Forms; +use Symfony\Component\Form\FormTypeExtensionInterface; +use Symfony\Component\Form\FormTypeGuesserInterface; +use Symfony\Component\Form\FormTypeInterface; /** * @author Bernhard Schussek @@ -32,21 +36,33 @@ protected function setUp(): void ->getFormFactory(); } + /** + * @return FormExtensionInterface[] + */ protected function getExtensions() { return []; } + /** + * @return FormTypeExtensionInterface[] + */ protected function getTypeExtensions() { return []; } + /** + * @return FormTypeInterface[] + */ protected function getTypes() { return []; } + /** + * @return FormTypeGuesserInterface[] + */ protected function getTypeGuessers() { return []; diff --git a/Test/FormPerformanceTestCase.php b/Test/FormPerformanceTestCase.php index 54ccc67cf..9247f57e7 100644 --- a/Test/FormPerformanceTestCase.php +++ b/Test/FormPerformanceTestCase.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Form\Test; -use Symfony\Component\Form\Tests\VersionAwareTest; - /** * Base class for performance tests. * @@ -23,21 +21,27 @@ */ abstract class FormPerformanceTestCase extends FormIntegrationTestCase { - use VersionAwareTest; - + private float $startTime; protected int $maxRunningTime = 0; - protected function runTest(): mixed + protected function setUp(): void { - $s = microtime(true); - $result = parent::runTest(); - $time = microtime(true) - $s; + parent::setUp(); + + $this->startTime = microtime(true); + } + + protected function assertPostConditions(): void + { + parent::assertPostConditions(); + + $time = microtime(true) - $this->startTime; if (0 != $this->maxRunningTime && $time > $this->maxRunningTime) { - $this->fail(sprintf('expected running time: <= %s but was: %s', $this->maxRunningTime, $time)); + $this->fail(\sprintf('expected running time: <= %s but was: %s', $this->maxRunningTime, $time)); } - return $result; + $this->expectNotToPerformAssertions(); } /** diff --git a/Test/Traits/ValidatorExtensionTrait.php b/Test/Traits/ValidatorExtensionTrait.php index b89095de7..5d0486e8c 100644 --- a/Test/Traits/ValidatorExtensionTrait.php +++ b/Test/Traits/ValidatorExtensionTrait.php @@ -28,7 +28,7 @@ protected function getValidatorExtension(): ValidatorExtension } if (!$this instanceof TypeTestCase) { - throw new \Exception(sprintf('The trait "ValidatorExtensionTrait" can only be added to a class that extends "%s".', TypeTestCase::class)); + throw new \Exception(\sprintf('The trait "ValidatorExtensionTrait" can only be added to a class that extends "%s".', TypeTestCase::class)); } $this->validator = $this->createMock(ValidatorInterface::class); diff --git a/Test/TypeTestCase.php b/Test/TypeTestCase.php index 5d4c2ba9c..1bbb66d25 100644 --- a/Test/TypeTestCase.php +++ b/Test/TypeTestCase.php @@ -13,6 +13,7 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Form\FormBuilder; +use Symfony\Component\Form\FormExtensionInterface; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; abstract class TypeTestCase extends FormIntegrationTestCase @@ -28,22 +29,31 @@ protected function setUp(): void $this->builder = new FormBuilder('', null, $this->dispatcher, $this->factory); } + /** + * @return FormExtensionInterface[] + */ protected function getExtensions() { $extensions = []; - if (\in_array(ValidatorExtensionTrait::class, class_uses($this))) { + if (\in_array(ValidatorExtensionTrait::class, class_uses($this), true)) { $extensions[] = $this->getValidatorExtension(); } return $extensions; } + /** + * @return void + */ public static function assertDateTimeEquals(\DateTime $expected, \DateTime $actual) { self::assertEquals($expected->format('c'), $actual->format('c')); } + /** + * @return void + */ public static function assertDateIntervalEquals(\DateInterval $expected, \DateInterval $actual) { self::assertEquals($expected->format('%RP%yY%mM%dDT%hH%iM%sS'), $actual->format('%RP%yY%mM%dDT%hH%iM%sS')); diff --git a/Tests/AbstractRequestHandlerTestCase.php b/Tests/AbstractRequestHandlerTestCase.php index d050edb41..f80efffb7 100644 --- a/Tests/AbstractRequestHandlerTestCase.php +++ b/Tests/AbstractRequestHandlerTestCase.php @@ -39,7 +39,7 @@ abstract class AbstractRequestHandlerTestCase extends TestCase protected function setUp(): void { - $this->serverParams = new class() extends ServerParams { + $this->serverParams = new class extends ServerParams { public ?int $contentLength = null; public string $postMaxSize = ''; diff --git a/Tests/ChoiceList/AbstractChoiceListTestCase.php b/Tests/ChoiceList/AbstractChoiceListTestCase.php index 0b0cb8e79..5ddae5614 100644 --- a/Tests/ChoiceList/AbstractChoiceListTestCase.php +++ b/Tests/ChoiceList/AbstractChoiceListTestCase.php @@ -39,8 +39,6 @@ abstract class AbstractChoiceListTestCase extends TestCase protected function setUp(): void { - parent::setUp(); - $this->list = $this->createChoiceList(); $choices = $this->getChoices(); diff --git a/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php b/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php index 2668d72ed..67e86208c 100644 --- a/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php +++ b/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php @@ -577,7 +577,7 @@ public static function provideDistinguishedChoices() ]; } - public function provideSameKeyChoices() + public static function provideSameKeyChoices() { // Only test types here that can be used as array keys return [ @@ -588,7 +588,7 @@ public function provideSameKeyChoices() ]; } - public function provideDistinguishedKeyChoices() + public static function provideDistinguishedKeyChoices() { // Only test types here that can be used as array keys return [ diff --git a/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php b/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php index e7bf26d17..2b1b239e5 100644 --- a/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php +++ b/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php @@ -159,9 +159,9 @@ public function testCreateFromChoicesGroupedTraversable() { $list = $this->factory->createListFromChoices( new \ArrayIterator([ - 'Group 1' => ['A' => $this->obj1, 'B' => $this->obj2], - 'Group 2' => ['C' => $this->obj3, 'D' => $this->obj4], - ]) + 'Group 1' => ['A' => $this->obj1, 'B' => $this->obj2], + 'Group 2' => ['C' => $this->obj3, 'D' => $this->obj4], + ]) ); $this->assertObjectListWithGeneratedValues($list); @@ -728,7 +728,7 @@ public function testPassTranslatableMessageAsLabelDoesntCastItToString() public function testPassTranslatableInterfaceAsLabelDoesntCastItToString() { - $message = new class() implements TranslatableInterface { + $message = new class implements TranslatableInterface { public function trans(TranslatorInterface $translator, ?string $locale = null): string { return 'my_message'; @@ -941,7 +941,7 @@ private function assertFlatViewWithAttr($view) 'C', ['attr2' => 'value2'] ), - ] + ] ), $view); } @@ -987,7 +987,7 @@ private function assertGroupedView($view) 'Group 2', [2 => new ChoiceView($this->obj3, '2', 'C')] ), - ] + ] ), $view); } diff --git a/Tests/ChoiceList/Loader/CallbackChoiceLoaderTest.php b/Tests/ChoiceList/Loader/CallbackChoiceLoaderTest.php index d394196ee..791a6f006 100644 --- a/Tests/ChoiceList/Loader/CallbackChoiceLoaderTest.php +++ b/Tests/ChoiceList/Loader/CallbackChoiceLoaderTest.php @@ -69,8 +69,8 @@ public function testLoadChoicesForValuesLoadsChoiceListOnFirstCall() public function testLoadValuesForChoicesCastsCallbackItemsToString() { $choices = [ - (object) ['id' => 2], - (object) ['id' => 3], + (object) ['id' => 2], + (object) ['id' => 3], ]; $value = fn ($item) => $item->id; diff --git a/Tests/ChoiceList/Loader/LazyChoiceLoaderTest.php b/Tests/ChoiceList/Loader/LazyChoiceLoaderTest.php new file mode 100644 index 000000000..0c1bcf3c2 --- /dev/null +++ b/Tests/ChoiceList/Loader/LazyChoiceLoaderTest.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\ChoiceList\Loader; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Form\ChoiceList\Loader\LazyChoiceLoader; +use Symfony\Component\Form\Tests\Fixtures\ArrayChoiceLoader; + +class LazyChoiceLoaderTest extends TestCase +{ + private LazyChoiceLoader $loader; + + protected function setUp(): void + { + $this->loader = new LazyChoiceLoader(new ArrayChoiceLoader(['A', 'B', 'C'])); + } + + public function testInitialEmptyChoiceListLoading() + { + $this->assertSame([], $this->loader->loadChoiceList()->getChoices()); + } + + public function testOnDemandChoiceListAfterLoadingValuesForChoices() + { + $this->loader->loadValuesForChoices(['A']); + $this->assertSame(['A' => 'A'], $this->loader->loadChoiceList()->getChoices()); + } + + public function testOnDemandChoiceListAfterLoadingChoicesForValues() + { + $this->loader->loadChoicesForValues(['B']); + $this->assertSame(['B' => 'B'], $this->loader->loadChoiceList()->getChoices()); + } + + public function testOnDemandChoiceList() + { + $this->loader->loadValuesForChoices(['A']); + $this->loader->loadChoicesForValues(['B']); + $this->assertSame(['B' => 'B'], $this->loader->loadChoiceList()->getChoices()); + } +} diff --git a/Tests/Console/Descriptor/AbstractDescriptorTestCase.php b/Tests/Console/Descriptor/AbstractDescriptorTestCase.php index fa8745b58..8a99c205c 100644 --- a/Tests/Console/Descriptor/AbstractDescriptorTestCase.php +++ b/Tests/Console/Descriptor/AbstractDescriptorTestCase.php @@ -153,7 +153,7 @@ private function getExpectedDescription($name) private function getFixtureFilename($name) { - return sprintf('%s/../../Fixtures/Descriptor/%s.%s', __DIR__, $name, $this->getFormat()); + return \sprintf('%s/../../Fixtures/Descriptor/%s.%s', __DIR__, $name, $this->getFormat()); } } diff --git a/Tests/DependencyInjection/FormPassTest.php b/Tests/DependencyInjection/FormPassTest.php index e9a7b5034..f0ccd3f09 100644 --- a/Tests/DependencyInjection/FormPassTest.php +++ b/Tests/DependencyInjection/FormPassTest.php @@ -21,6 +21,7 @@ use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\Command\DebugCommand; use Symfony\Component\Form\DependencyInjection\FormPass; +use Symfony\Component\Form\Extension\Csrf\Type\FormTypeCsrfExtension; use Symfony\Component\Form\FormRegistry; /** @@ -95,6 +96,25 @@ public function testAddTaggedTypesToDebugCommand() ); } + public function testAddTaggedTypesToCsrfTypeExtension() + { + $container = $this->createContainerBuilder(); + + $container->register('form.registry', FormRegistry::class); + $container->register('form.type_extension.csrf', FormTypeCsrfExtension::class) + ->setArguments([null, true, '_token', null, 'validator.translation_domain', null, [], null]) + ->setPublic(true); + + $container->setDefinition('form.extension', $this->createExtensionDefinition()); + $container->register('my.type1', __CLASS__.'_Type1')->addTag('form.type', ['csrf_token_id' => 'the_token_id']); + $container->register('my.type2', __CLASS__.'_Type2')->addTag('form.type'); + + $container->compile(); + + $csrfDefinition = $container->getDefinition('form.type_extension.csrf'); + $this->assertSame([__CLASS__.'_Type1' => 'the_token_id'], $csrfDefinition->getArgument(7)); + } + /** * @dataProvider addTaggedTypeExtensionsDataProvider */ diff --git a/Tests/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformerTest.php b/Tests/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformerTest.php index c7918ae8b..b9906c236 100644 --- a/Tests/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformerTest.php +++ b/Tests/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformerTest.php @@ -11,10 +11,12 @@ namespace Symfony\Component\Form\Tests\Extension\Core\DataTransformer; +use Symfony\Component\Form\Exception\InvalidArgumentException; use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\Extension\Core\DataTransformer\BaseDateTimeTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer; use Symfony\Component\Form\Tests\Extension\Core\DataTransformer\Traits\DateTimeEqualsTrait; +use Symfony\Component\Intl\Intl; use Symfony\Component\Intl\Util\IntlTestHelper; class DateTimeToLocalizedStringTransformerTest extends BaseDateTimeTransformerTestCase @@ -30,8 +32,6 @@ class DateTimeToLocalizedStringTransformerTest extends BaseDateTimeTransformerTe protected function setUp(): void { - parent::setUp(); - // Normalize intl. configuration settings. if (\extension_loaded('intl')) { $this->initialTestCaseUseException = ini_set('intl.use_exceptions', 0); @@ -54,7 +54,7 @@ protected function tearDown(): void if (\extension_loaded('intl')) { ini_set('intl.use_exceptions', $this->initialTestCaseUseException); - ini_set('intl.error_level', $this->initialTestCaseUseException); + ini_set('intl.error_level', $this->initialTestCaseErrorLevel); } } @@ -236,6 +236,10 @@ public function testReverseTransformFullTime() public function testReverseTransformFromDifferentLocale() { + if (version_compare(Intl::getIcuVersion(), '71.1', '>')) { + $this->markTestSkipped('ICU version 71.1 or lower is required.'); + } + \Locale::setDefault('en_US'); $transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC'); @@ -339,12 +343,11 @@ public function testReverseTransformFiveDigitYearsWithTimestamp() $transformer->reverseTransform('20107-03-21 12:34:56'); } + /** + * @requires extension intl + */ public function testReverseTransformWrapsIntlErrorsWithErrorLevel() { - if (!\extension_loaded('intl')) { - $this->markTestSkipped('intl extension is not loaded'); - } - $errorLevel = ini_set('intl.error_level', \E_WARNING); try { @@ -356,12 +359,11 @@ public function testReverseTransformWrapsIntlErrorsWithErrorLevel() } } + /** + * @requires extension intl + */ public function testReverseTransformWrapsIntlErrorsWithExceptions() { - if (!\extension_loaded('intl')) { - $this->markTestSkipped('intl extension is not loaded'); - } - $initialUseExceptions = ini_set('intl.use_exceptions', 1); try { @@ -373,12 +375,11 @@ public function testReverseTransformWrapsIntlErrorsWithExceptions() } } + /** + * @requires extension intl + */ public function testReverseTransformWrapsIntlErrorsWithExceptionsAndErrorLevel() { - if (!\extension_loaded('intl')) { - $this->markTestSkipped('intl extension is not loaded'); - } - $initialUseExceptions = ini_set('intl.use_exceptions', 1); $initialErrorLevel = ini_set('intl.error_level', \E_WARNING); @@ -392,6 +393,68 @@ public function testReverseTransformWrapsIntlErrorsWithExceptionsAndErrorLevel() } } + public function testTransformDateTimeWithCustomCalendar() + { + $dateTime = new \DateTimeImmutable('2024-03-31'); + + $weekBeginsOnSunday = \IntlCalendar::createInstance(); + $weekBeginsOnSunday->setFirstDayOfWeek(\IntlCalendar::DOW_SUNDAY); + + $this->assertSame( + '2024-03-31 2024w14', + (new DateTimeToLocalizedStringTransformer(calendar: $weekBeginsOnSunday, pattern: "y-MM-dd y'w'w"))->transform($dateTime), + ); + + $weekBeginsOnMonday = \IntlCalendar::createInstance(); + $weekBeginsOnMonday->setFirstDayOfWeek(\IntlCalendar::DOW_MONDAY); + + $this->assertSame( + '2024-03-31 2024w13', + (new DateTimeToLocalizedStringTransformer(calendar: $weekBeginsOnMonday, pattern: "y-MM-dd y'w'w"))->transform($dateTime), + ); + } + + public function testReverseTransformDateTimeWithCustomCalendar() + { + $weekBeginsOnSunday = \IntlCalendar::createInstance(); + $weekBeginsOnSunday->setFirstDayOfWeek(\IntlCalendar::DOW_SUNDAY); + + $this->assertSame( + '2024-03-31', + (new DateTimeToLocalizedStringTransformer(calendar: $weekBeginsOnSunday, pattern: "y-MM-dd y'w'w")) + ->reverseTransform('2024-03-31 2024w14') + ->format('Y-m-d'), + ); + + $weekBeginsOnMonday = \IntlCalendar::createInstance(); + $weekBeginsOnMonday->setFirstDayOfWeek(\IntlCalendar::DOW_MONDAY); + + $this->assertSame( + '2024-03-31', + (new DateTimeToLocalizedStringTransformer(calendar: $weekBeginsOnMonday, pattern: "y-MM-dd y'w'w")) + ->reverseTransform('2024-03-31 2024w13') + ->format('Y-m-d'), + ); + } + + public function testDefaultCalendarIsGregorian() + { + $now = new \DateTimeImmutable(); + + $this->assertSame( + (new DateTimeToLocalizedStringTransformer(calendar: \IntlDateFormatter::GREGORIAN, pattern: "y-MM-dd y'w'w"))->transform($now), + (new DateTimeToLocalizedStringTransformer(pattern: "y-MM-dd y'w'w"))->transform($now), + ); + } + + public function testInvalidCalendar() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "calendar" option should be either an \IntlDateFormatter constant or an \IntlCalendar instance.'); + + new DateTimeToLocalizedStringTransformer(calendar: 123456); + } + protected function createDateTimeTransformer(?string $inputTimezone = null, ?string $outputTimezone = null): BaseDateTimeTransformer { return new DateTimeToLocalizedStringTransformer($inputTimezone, $outputTimezone); diff --git a/Tests/Extension/Core/DataTransformer/DateTimeToRfc3339TransformerTest.php b/Tests/Extension/Core/DataTransformer/DateTimeToRfc3339TransformerTest.php index 6a4d77039..c69ba31be 100644 --- a/Tests/Extension/Core/DataTransformer/DateTimeToRfc3339TransformerTest.php +++ b/Tests/Extension/Core/DataTransformer/DateTimeToRfc3339TransformerTest.php @@ -25,8 +25,6 @@ class DateTimeToRfc3339TransformerTest extends BaseDateTimeTransformerTestCase protected function setUp(): void { - parent::setUp(); - $this->dateTime = new \DateTime('2010-02-03 04:05:06 UTC'); $this->dateTimeWithoutSeconds = new \DateTime('2010-02-03 04:05:00 UTC'); } diff --git a/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php b/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php index 2d43e9533..e5733ad96 100644 --- a/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php +++ b/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php @@ -118,4 +118,13 @@ public function testValidNumericValuesWithNonDotDecimalPointCharacter() $this->assertSame('0,0035', $transformer->transform(12 / 34)); } + + public function testHighIntNumberConversion() + { + $transformer = new MoneyToLocalizedStringTransformer(4, null, null, 100); + + $this->expectException(TransformationFailedException::class); + + $transformer->reverseTransform(111111111111111110.00); + } } diff --git a/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php b/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php index a1dc724fd..37448db51 100644 --- a/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php +++ b/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php @@ -20,8 +20,17 @@ class NumberToLocalizedStringTransformerTest extends TestCase { private string $defaultLocale; + private $initialTestCaseUseException; + private $initialTestCaseErrorLevel; + protected function setUp(): void { + // Normalize intl. configuration settings. + if (\extension_loaded('intl')) { + $this->initialTestCaseUseException = ini_set('intl.use_exceptions', 0); + $this->initialTestCaseErrorLevel = ini_set('intl.error_level', 0); + } + $this->defaultLocale = \Locale::getDefault(); \Locale::setDefault('en'); } @@ -29,6 +38,11 @@ protected function setUp(): void protected function tearDown(): void { \Locale::setDefault($this->defaultLocale); + + if (\extension_loaded('intl')) { + ini_set('intl.use_exceptions', $this->initialTestCaseUseException); + ini_set('intl.error_level', $this->initialTestCaseErrorLevel); + } } public static function provideTransformations() @@ -632,4 +646,83 @@ public function testReverseTransformSmallInt() $this->assertSame(1.0, $transformer->reverseTransform('1')); } + + /** + * @dataProvider eNotationProvider + */ + public function testReverseTransformENotation($output, $input) + { + IntlTestHelper::requireFullIntl($this); + + \Locale::setDefault('en'); + + $transformer = new NumberToLocalizedStringTransformer(); + + $this->assertSame($output, $transformer->reverseTransform($input)); + } + + /** + * @requires extension intl + */ + public function testReverseTransformWrapsIntlErrorsWithErrorLevel() + { + $errorLevel = ini_set('intl.error_level', \E_WARNING); + + try { + $this->expectException(TransformationFailedException::class); + $transformer = new NumberToLocalizedStringTransformer(); + $transformer->reverseTransform('invalid_number'); + } finally { + ini_set('intl.error_level', $errorLevel); + } + } + + /** + * @requires extension intl + */ + public function testReverseTransformWrapsIntlErrorsWithExceptions() + { + $initialUseExceptions = ini_set('intl.use_exceptions', 1); + + try { + $this->expectException(TransformationFailedException::class); + $transformer = new NumberToLocalizedStringTransformer(); + $transformer->reverseTransform('invalid_number'); + } finally { + ini_set('intl.use_exceptions', $initialUseExceptions); + } + } + + /** + * @requires extension intl + */ + public function testReverseTransformWrapsIntlErrorsWithExceptionsAndErrorLevel() + { + $initialUseExceptions = ini_set('intl.use_exceptions', 1); + $initialErrorLevel = ini_set('intl.error_level', \E_WARNING); + + try { + $this->expectException(TransformationFailedException::class); + $transformer = new NumberToLocalizedStringTransformer(); + $transformer->reverseTransform('invalid_number'); + } finally { + ini_set('intl.use_exceptions', $initialUseExceptions); + ini_set('intl.error_level', $initialErrorLevel); + } + } + + public static function eNotationProvider(): array + { + return [ + [0.001, '1E-3'], + [0.001, '1.0E-3'], + [0.001, '1e-3'], + [0.001, '1.0e-03'], + [1000.0, '1E3'], + [1000.0, '1.0E3'], + [1000.0, '1e3'], + [1000.0, '1.0e3'], + [1232.0, '1.232e3'], + ]; + } } diff --git a/Tests/Extension/Core/DataTransformer/PercentToLocalizedStringTransformerTest.php b/Tests/Extension/Core/DataTransformer/PercentToLocalizedStringTransformerTest.php index 2bc6c5d7b..187017396 100644 --- a/Tests/Extension/Core/DataTransformer/PercentToLocalizedStringTransformerTest.php +++ b/Tests/Extension/Core/DataTransformer/PercentToLocalizedStringTransformerTest.php @@ -20,8 +20,17 @@ class PercentToLocalizedStringTransformerTest extends TestCase { private string $defaultLocale; + private $initialTestCaseUseException; + private $initialTestCaseErrorLevel; + protected function setUp(): void { + // Normalize intl. configuration settings. + if (\extension_loaded('intl')) { + $this->initialTestCaseUseException = ini_set('intl.use_exceptions', 0); + $this->initialTestCaseErrorLevel = ini_set('intl.error_level', 0); + } + $this->defaultLocale = \Locale::getDefault(); \Locale::setDefault('en'); } @@ -29,6 +38,11 @@ protected function setUp(): void protected function tearDown(): void { \Locale::setDefault($this->defaultLocale); + + if (\extension_loaded('intl')) { + ini_set('intl.use_exceptions', $this->initialTestCaseUseException); + ini_set('intl.error_level', $this->initialTestCaseErrorLevel); + } } public function testTransform() @@ -475,6 +489,56 @@ public function testReverseTransformForHtml5FormatWithScale() $this->assertEquals(0.1234, $transformer->reverseTransform('12.34')); } + + /** + * @requires extension intl + */ + public function testReverseTransformWrapsIntlErrorsWithErrorLevel() + { + $errorLevel = ini_set('intl.error_level', \E_WARNING); + + try { + $this->expectException(TransformationFailedException::class); + $transformer = new PercentToLocalizedStringTransformer(null, null, \NumberFormatter::ROUND_HALFUP); + $transformer->reverseTransform('invalid_number'); + } finally { + ini_set('intl.error_level', $errorLevel); + } + } + + /** + * @requires extension intl + */ + public function testReverseTransformWrapsIntlErrorsWithExceptions() + { + $initialUseExceptions = ini_set('intl.use_exceptions', 1); + + try { + $this->expectException(TransformationFailedException::class); + $transformer = new PercentToLocalizedStringTransformer(null, null, \NumberFormatter::ROUND_HALFUP); + $transformer->reverseTransform('invalid_number'); + } finally { + ini_set('intl.use_exceptions', $initialUseExceptions); + } + } + + /** + * @requires extension intl + */ + public function testReverseTransformWrapsIntlErrorsWithExceptionsAndErrorLevel() + { + $initialUseExceptions = ini_set('intl.use_exceptions', 1); + $initialErrorLevel = ini_set('intl.error_level', \E_WARNING); + + try { + $this->expectException(TransformationFailedException::class); + $transformer = new PercentToLocalizedStringTransformer(null, null, \NumberFormatter::ROUND_HALFUP); + $transformer->reverseTransform('invalid_number'); + } finally { + ini_set('intl.use_exceptions', $initialUseExceptions); + ini_set('intl.error_level', $initialErrorLevel); + } + } } class PercentToLocalizedStringTransformerWithoutGrouping extends PercentToLocalizedStringTransformer diff --git a/Tests/Extension/Core/DataTransformer/ValueToDuplicatesTransformerTest.php b/Tests/Extension/Core/DataTransformer/ValueToDuplicatesTransformerTest.php index 5909a51ef..358f21af2 100644 --- a/Tests/Extension/Core/DataTransformer/ValueToDuplicatesTransformerTest.php +++ b/Tests/Extension/Core/DataTransformer/ValueToDuplicatesTransformerTest.php @@ -65,7 +65,7 @@ public function testReverseTransformCompletelyEmpty() 'c' => '', ]; - $this->assertNull($this->transformer->reverseTransform($input)); + $this->assertSame('', $this->transformer->reverseTransform($input)); } public function testReverseTransformCompletelyNull() diff --git a/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php b/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php index f63a5c154..934460c8f 100644 --- a/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php +++ b/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php @@ -14,28 +14,28 @@ use Doctrine\Common\Collections\ArrayCollection; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper; use Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilder; +use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormFactoryBuilder; use Symfony\Component\Form\FormFactoryInterface; -use Symfony\Component\Form\FormInterface; class ResizeFormListenerTest extends TestCase { private FormFactoryInterface $factory; - private FormInterface $form; + private FormBuilderInterface $builder; protected function setUp(): void { $this->factory = (new FormFactoryBuilder())->getFormFactory(); - $this->form = $this->getBuilder() + $this->builder = $this->getBuilder() ->setCompound(true) - ->setDataMapper(new DataMapper()) - ->getForm(); + ->setDataMapper(new DataMapper()); } protected function getBuilder($name = 'name') @@ -43,142 +43,221 @@ protected function getBuilder($name = 'name') return new FormBuilder($name, null, new EventDispatcher(), $this->factory); } - protected function getForm($name = 'name') + /** + * @group legacy + */ + public function testPreSetDataResizesForm() { - return $this->getBuilder($name)->getForm(); + $this->builder->add($this->getBuilder('0')); + $this->builder->add($this->getBuilder('1')); + $this->builder->addEventSubscriber(new class(TextType::class, ['attr' => ['maxlength' => 10]], false, false) extends ResizeFormListener { + public function preSetData(FormEvent $event): void + { + parent::preSetData($event); + } + }); + + $form = $this->builder->getForm(); + + $this->assertTrue($form->has('0')); + + // initialize the form + $form->setData([1 => 'string', 2 => 'string']); + + $this->assertFalse($form->has('0')); + $this->assertTrue($form->has('1')); + $this->assertTrue($form->has('2')); + + $this->assertSame('string', $form->get('1')->getData()); + $this->assertSame('string', $form->get('2')->getData()); } - public function testPreSetDataResizesForm() + public function testPostSetDataResizesForm() { - $this->form->add($this->getForm('0')); - $this->form->add($this->getForm('1')); + $this->builder->add($this->getBuilder('0')); + $this->builder->add($this->getBuilder('1')); + $this->builder->addEventSubscriber(new ResizeFormListener(TextType::class, ['attr' => ['maxlength' => 10]], false, false)); - $data = [1 => 'string', 2 => 'string']; - $event = new FormEvent($this->form, $data); - $listener = new ResizeFormListener(TextType::class, ['attr' => ['maxlength' => 10]], false, false); - $listener->preSetData($event); + $form = $this->builder->getForm(); + + $this->assertTrue($form->has('0')); - $this->assertFalse($this->form->has('0')); - $this->assertTrue($this->form->has('1')); - $this->assertTrue($this->form->has('2')); + // initialize the form + $form->setData([1 => 'string', 2 => 'string']); + + $this->assertFalse($form->has('0')); + $this->assertTrue($form->has('1')); + $this->assertTrue($form->has('2')); + + $this->assertSame('string', $form->get('1')->getData()); + $this->assertSame('string', $form->get('2')->getData()); } + /** + * @group legacy + */ public function testPreSetDataRequiresArrayOrTraversable() { $this->expectException(UnexpectedTypeException::class); $data = 'no array or traversable'; - $event = new FormEvent($this->form, $data); - $listener = new ResizeFormListener('text', [], false, false); + $event = new FormEvent($this->builder->getForm(), $data); + $listener = new class(TextType::class, [], false, false) extends ResizeFormListener { + public function preSetData(FormEvent $event): void + { + parent::preSetData($event); + } + }; $listener->preSetData($event); } + public function testPostSetDataRequiresArrayOrTraversable() + { + $this->expectException(UnexpectedTypeException::class); + $data = 'no array or traversable'; + $event = new FormEvent($this->builder->getForm(), $data); + $listener = new ResizeFormListener(TextType::class, [], false, false); + $listener->postSetData($event); + } + + /** + * @group legacy + */ public function testPreSetDataDealsWithNullData() { $data = null; - $event = new FormEvent($this->form, $data); - $listener = new ResizeFormListener(TextType::class, [], false, false); + $event = new FormEvent($this->builder->getForm(), $data); + $listener = new class(TextType::class, [], false, false) extends ResizeFormListener { + public function preSetData(FormEvent $event): void + { + parent::preSetData($event); + } + }; $listener->preSetData($event); - $this->assertSame(0, $this->form->count()); + $this->assertSame(0, $this->builder->count()); + } + + public function testPostSetDataDealsWithNullData() + { + $data = null; + $event = new FormEvent($this->builder->getForm(), $data); + $listener = new ResizeFormListener(TextType::class, [], false, false); + $listener->postSetData($event); + + $this->assertSame(0, $this->builder->count()); } public function testPreSubmitResizesUpIfAllowAdd() { - $this->form->add($this->getForm('0')); + $this->builder->add($this->getBuilder('0')); + $this->builder->addEventSubscriber(new ResizeFormListener(TextType::class, ['attr' => ['maxlength' => 10]], true, false)); - $data = [0 => 'string', 1 => 'string']; - $event = new FormEvent($this->form, $data); - $listener = new ResizeFormListener(TextType::class, ['attr' => ['maxlength' => 10]], true, false); - $listener->preSubmit($event); + $form = $this->builder->getForm(); + + $this->assertTrue($form->has('0')); + $this->assertFalse($form->has('1')); - $this->assertTrue($this->form->has('0')); - $this->assertTrue($this->form->has('1')); + $form->submit([0 => 'string', 1 => 'string']); + + $this->assertTrue($form->has('0')); + $this->assertTrue($form->has('1')); } public function testPreSubmitResizesDownIfAllowDelete() { - $this->form->add($this->getForm('0')); - $this->form->add($this->getForm('1')); + $this->builder->add($this->getBuilder('0')); + $this->builder->add($this->getBuilder('1')); + $this->builder->addEventSubscriber(new ResizeFormListener(TextType::class, [], false, true)); - $data = [0 => 'string']; - $event = new FormEvent($this->form, $data); - $listener = new ResizeFormListener('text', [], false, true); - $listener->preSubmit($event); + $form = $this->builder->getForm(); + // initialize the form + $form->setData([0 => 'string', 1 => 'string']); + + $this->assertTrue($form->has('0')); + $this->assertTrue($form->has('1')); - $this->assertTrue($this->form->has('0')); - $this->assertFalse($this->form->has('1')); + $form->submit([0 => 'string']); + + $this->assertTrue($form->has('0')); + $this->assertFalse($form->has('1')); } // fix for https://github.com/symfony/symfony/pull/493 public function testPreSubmitRemovesZeroKeys() { - $this->form->add($this->getForm('0')); + $this->builder->add($this->getBuilder('0')); $data = []; - $event = new FormEvent($this->form, $data); - $listener = new ResizeFormListener('text', [], false, true); + $form = $this->builder->getForm(); + $event = new FormEvent($form, $data); + $listener = new ResizeFormListener(TextType::class, [], false, true); $listener->preSubmit($event); - $this->assertFalse($this->form->has('0')); + $this->assertFalse($form->has('0')); } public function testPreSubmitDoesNothingIfNotAllowAddNorAllowDelete() { - $this->form->add($this->getForm('0')); - $this->form->add($this->getForm('1')); + $this->builder->add($this->getBuilder('0')); + $this->builder->add($this->getBuilder('1')); $data = [0 => 'string', 2 => 'string']; - $event = new FormEvent($this->form, $data); - $listener = new ResizeFormListener('text', [], false, false); + $form = $this->builder->getForm(); + $event = new FormEvent($form, $data); + $listener = new ResizeFormListener(TextType::class, [], false, false); $listener->preSubmit($event); - $this->assertTrue($this->form->has('0')); - $this->assertTrue($this->form->has('1')); - $this->assertFalse($this->form->has('2')); + $this->assertTrue($form->has('0')); + $this->assertTrue($form->has('1')); + $this->assertFalse($form->has('2')); } public function testPreSubmitDealsWithNoArrayOrTraversable() { $data = 'no array or traversable'; - $event = new FormEvent($this->form, $data); - $listener = new ResizeFormListener('text', [], false, false); + $form = $this->builder->getForm(); + $event = new FormEvent($form, $data); + $listener = new ResizeFormListener(TextType::class, [], false, false); $listener->preSubmit($event); - $this->assertFalse($this->form->has('1')); + $this->assertFalse($form->has('1')); } public function testPreSubmitDealsWithNullData() { - $this->form->add($this->getForm('1')); + $this->builder->add($this->getBuilder('1')); $data = null; - $event = new FormEvent($this->form, $data); - $listener = new ResizeFormListener('text', [], false, true); + $form = $this->builder->getForm(); + $event = new FormEvent($form, $data); + $listener = new ResizeFormListener(TextType::class, [], false, true); $listener->preSubmit($event); - $this->assertFalse($this->form->has('1')); + $this->assertFalse($form->has('1')); } // fixes https://github.com/symfony/symfony/pull/40 public function testPreSubmitDealsWithEmptyData() { - $this->form->add($this->getForm('1')); + $this->builder->add($this->getBuilder('1')); $data = ''; - $event = new FormEvent($this->form, $data); - $listener = new ResizeFormListener('text', [], false, true); + $form = $this->builder->getForm(); + $event = new FormEvent($form, $data); + $listener = new ResizeFormListener(TextType::class, [], false, true); $listener->preSubmit($event); - $this->assertFalse($this->form->has('1')); + $this->assertFalse($form->has('1')); } public function testOnSubmitNormDataRemovesEntriesMissingInTheFormIfAllowDelete() { - $this->form->add($this->getForm('1')); + $this->builder->add($this->getBuilder('1')); $data = [0 => 'first', 1 => 'second', 2 => 'third']; - $event = new FormEvent($this->form, $data); - $listener = new ResizeFormListener('text', [], false, true); + $form = $this->builder->getForm(); + $event = new FormEvent($form, $data); + $listener = new ResizeFormListener(TextType::class, [], false, true); $listener->onSubmit($event); $this->assertEquals([1 => 'second'], $event->getData()); @@ -186,11 +265,12 @@ public function testOnSubmitNormDataRemovesEntriesMissingInTheFormIfAllowDelete( public function testOnSubmitNormDataDoesNothingIfNotAllowDelete() { - $this->form->add($this->getForm('1')); + $this->builder->add($this->getBuilder('1')); $data = [0 => 'first', 1 => 'second', 2 => 'third']; - $event = new FormEvent($this->form, $data); - $listener = new ResizeFormListener('text', [], false, false); + $form = $this->builder->getForm(); + $event = new FormEvent($form, $data); + $listener = new ResizeFormListener(TextType::class, [], false, false); $listener->onSubmit($event); $this->assertEquals($data, $event->getData()); @@ -200,18 +280,18 @@ public function testOnSubmitNormDataRequiresArrayOrTraversable() { $this->expectException(UnexpectedTypeException::class); $data = 'no array or traversable'; - $event = new FormEvent($this->form, $data); - $listener = new ResizeFormListener('text', [], false, false); + $event = new FormEvent($this->builder->getForm(), $data); + $listener = new ResizeFormListener(TextType::class, [], false, false); $listener->onSubmit($event); } public function testOnSubmitNormDataDealsWithNullData() { - $this->form->add($this->getForm('1')); + $this->builder->add($this->getBuilder('1')); $data = null; - $event = new FormEvent($this->form, $data); - $listener = new ResizeFormListener('text', [], false, true); + $event = new FormEvent($this->builder->getForm(), $data); + $listener = new ResizeFormListener(TextType::class, [], false, true); $listener->onSubmit($event); $this->assertEquals([], $event->getData()); @@ -219,11 +299,11 @@ public function testOnSubmitNormDataDealsWithNullData() public function testOnSubmitDealsWithObjectBackedIteratorAggregate() { - $this->form->add($this->getForm('1')); + $this->builder->add($this->getBuilder('1')); $data = new \ArrayObject([0 => 'first', 1 => 'second', 2 => 'third']); - $event = new FormEvent($this->form, $data); - $listener = new ResizeFormListener('text', [], false, true); + $event = new FormEvent($this->builder->getForm(), $data); + $listener = new ResizeFormListener(TextType::class, [], false, true); $listener->onSubmit($event); $this->assertArrayNotHasKey(0, $event->getData()); @@ -232,11 +312,11 @@ public function testOnSubmitDealsWithObjectBackedIteratorAggregate() public function testOnSubmitDealsWithArrayBackedIteratorAggregate() { - $this->form->add($this->getForm('1')); + $this->builder->add($this->getBuilder('1')); $data = new ArrayCollection([0 => 'first', 1 => 'second', 2 => 'third']); - $event = new FormEvent($this->form, $data); - $listener = new ResizeFormListener('text', [], false, true); + $event = new FormEvent($this->builder->getForm(), $data); + $listener = new ResizeFormListener(TextType::class, [], false, true); $listener->onSubmit($event); $this->assertArrayNotHasKey(0, $event->getData()); @@ -245,46 +325,37 @@ public function testOnSubmitDealsWithArrayBackedIteratorAggregate() public function testOnSubmitDeleteEmptyNotCompoundEntriesIfAllowDelete() { - $this->form->setData(['0' => 'first', '1' => 'second']); - $this->form->add($this->getForm('0')); - $this->form->add($this->getForm('1')); - - $data = [0 => 'first', 1 => '']; - foreach ($data as $child => $dat) { - $this->form->get($child)->submit($dat); - } - $event = new FormEvent($this->form, $data); - $listener = new ResizeFormListener('text', [], false, true, true); - $listener->onSubmit($event); + $this->builder->setData(['0' => 'first', '1' => 'second']); + $this->builder->add($this->getBuilder('0')); + $this->builder->add($this->getBuilder('1')); + $this->builder->addEventSubscriber(new ResizeFormListener(TextType::class, [], false, true, true)); - $this->assertEquals([0 => 'first'], $event->getData()); + $form = $this->builder->getForm(); + + $form->submit([0 => 'first', 1 => '']); + + $this->assertEquals([0 => 'first'], $form->getData()); } public function testOnSubmitDeleteEmptyCompoundEntriesIfAllowDelete() { - $this->form->setData(['0' => ['name' => 'John'], '1' => ['name' => 'Jane']]); - $form1 = $this->getBuilder('0') - ->setCompound(true) - ->setDataMapper(new DataMapper()) - ->getForm(); - $form1->add($this->getForm('name')); - $form2 = $this->getBuilder('1') - ->setCompound(true) - ->setDataMapper(new DataMapper()) - ->getForm(); - $form2->add($this->getForm('name')); - $this->form->add($form1); - $this->form->add($form2); - - $data = ['0' => ['name' => 'John'], '1' => ['name' => '']]; - foreach ($data as $child => $dat) { - $this->form->get($child)->submit($dat); - } - $event = new FormEvent($this->form, $data); - $callback = fn ($data) => null === $data['name']; - $listener = new ResizeFormListener('text', [], false, true, $callback); - $listener->onSubmit($event); + $this->builder->setData(['0' => ['name' => 'John'], '1' => ['name' => 'Jane']]); + $this->builder->add('0', NestedType::class); + $this->builder->add('1', NestedType::class); + $callback = fn ($data) => empty($data['name']); + $this->builder->addEventSubscriber(new ResizeFormListener(NestedType::class, [], false, true, $callback)); + + $form = $this->builder->getForm(); + $form->submit(['0' => ['name' => 'John'], '1' => ['name' => '']]); - $this->assertEquals(['0' => ['name' => 'John']], $event->getData()); + $this->assertEquals(['0' => ['name' => 'John']], $form->getData()); + } +} + +class NestedType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->add('name'); } } diff --git a/Tests/Extension/Core/Type/BaseTypeTestCase.php b/Tests/Extension/Core/Type/BaseTypeTestCase.php index 5238e2fd8..8293ec31b 100644 --- a/Tests/Extension/Core/Type/BaseTypeTestCase.php +++ b/Tests/Extension/Core/Type/BaseTypeTestCase.php @@ -12,15 +12,12 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; use Symfony\Component\Form\Test\TypeTestCase; -use Symfony\Component\Form\Tests\VersionAwareTest; /** * @author Bernhard Schussek */ abstract class BaseTypeTestCase extends TypeTestCase { - use VersionAwareTest; - public const TESTED_TYPE = ''; public function testPassDisabledAsOption() @@ -114,8 +111,6 @@ public function testDefaultTranslationDomain() public function testPassLabelTranslationParametersToView() { - $this->requiresFeatureSet(403); - $view = $this->factory->create($this->getTestedType(), null, array_merge($this->getTestOptions(), [ 'label_translation_parameters' => ['%param%' => 'value'], ])) @@ -126,8 +121,6 @@ public function testPassLabelTranslationParametersToView() public function testPassAttrTranslationParametersToView() { - $this->requiresFeatureSet(403); - $view = $this->factory->create($this->getTestedType(), null, array_merge($this->getTestOptions(), [ 'attr_translation_parameters' => ['%param%' => 'value'], ])) @@ -138,8 +131,6 @@ public function testPassAttrTranslationParametersToView() public function testInheritLabelTranslationParametersFromParent() { - $this->requiresFeatureSet(403); - $view = $this->factory ->createNamedBuilder('parent', FormTypeTest::TESTED_TYPE, null, [ 'label_translation_parameters' => ['%param%' => 'value'], @@ -153,8 +144,6 @@ public function testInheritLabelTranslationParametersFromParent() public function testInheritAttrTranslationParametersFromParent() { - $this->requiresFeatureSet(403); - $view = $this->factory ->createNamedBuilder('parent', FormTypeTest::TESTED_TYPE, null, [ 'attr_translation_parameters' => ['%param%' => 'value'], @@ -168,8 +157,6 @@ public function testInheritAttrTranslationParametersFromParent() public function testPreferOwnLabelTranslationParameters() { - $this->requiresFeatureSet(403); - $view = $this->factory ->createNamedBuilder('parent', FormTypeTest::TESTED_TYPE, null, [ 'label_translation_parameters' => ['%parent_param%' => 'parent_value', '%override_param%' => 'parent_override_value'], @@ -185,8 +172,6 @@ public function testPreferOwnLabelTranslationParameters() public function testPreferOwnAttrTranslationParameters() { - $this->requiresFeatureSet(403); - $view = $this->factory ->createNamedBuilder('parent', FormTypeTest::TESTED_TYPE, null, [ 'attr_translation_parameters' => ['%parent_param%' => 'parent_value', '%override_param%' => 'parent_override_value'], @@ -202,8 +187,6 @@ public function testPreferOwnAttrTranslationParameters() public function testDefaultLabelTranslationParameters() { - $this->requiresFeatureSet(403); - $view = $this->factory->createNamedBuilder('parent', FormTypeTest::TESTED_TYPE) ->add('child', $this->getTestedType(), $this->getTestOptions()) ->getForm() @@ -214,8 +197,6 @@ public function testDefaultLabelTranslationParameters() public function testDefaultAttrTranslationParameters() { - $this->requiresFeatureSet(403); - $view = $this->factory->createNamedBuilder('parent', FormTypeTest::TESTED_TYPE) ->add('child', $this->getTestedType(), $this->getTestOptions()) ->getForm() diff --git a/Tests/Extension/Core/Type/BirthdayTypeTest.php b/Tests/Extension/Core/Type/BirthdayTypeTest.php index 048457141..53e5c959c 100644 --- a/Tests/Extension/Core/Type/BirthdayTypeTest.php +++ b/Tests/Extension/Core/Type/BirthdayTypeTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; +use Symfony\Component\Form\Extension\Core\Type\BirthdayType; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; /** @@ -18,7 +19,7 @@ */ class BirthdayTypeTest extends DateTypeTest { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\BirthdayType'; + public const TESTED_TYPE = BirthdayType::class; public function testSetInvalidYearsOption() { diff --git a/Tests/Extension/Core/Type/ButtonTypeTest.php b/Tests/Extension/Core/Type/ButtonTypeTest.php index 0125631c5..4825015d2 100644 --- a/Tests/Extension/Core/Type/ButtonTypeTest.php +++ b/Tests/Extension/Core/Type/ButtonTypeTest.php @@ -14,6 +14,7 @@ use Symfony\Component\Form\Button; use Symfony\Component\Form\Exception\BadMethodCallException; use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Form\Extension\Core\Type\ButtonType; use Symfony\Component\Form\Extension\Core\Type\FormType; /** @@ -21,7 +22,7 @@ */ class ButtonTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\ButtonType'; + public const TESTED_TYPE = ButtonType::class; public function testCreateButtonInstances() { diff --git a/Tests/Extension/Core/Type/CheckboxTypeTest.php b/Tests/Extension/Core/Type/CheckboxTypeTest.php index 62312e28d..69fd0fd60 100644 --- a/Tests/Extension/Core/Type/CheckboxTypeTest.php +++ b/Tests/Extension/Core/Type/CheckboxTypeTest.php @@ -12,11 +12,12 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; use Symfony\Component\Form\CallbackTransformer; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; class CheckboxTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\CheckboxType'; + public const TESTED_TYPE = CheckboxType::class; public function testDataIsFalseByDefault() { diff --git a/Tests/Extension/Core/Type/ChoiceTypeTest.php b/Tests/Extension/Core/Type/ChoiceTypeTest.php index 8e2372d7e..28810bbc7 100644 --- a/Tests/Extension/Core/Type/ChoiceTypeTest.php +++ b/Tests/Extension/Core/Type/ChoiceTypeTest.php @@ -14,13 +14,15 @@ use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader; use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; use Symfony\Component\Form\ChoiceList\View\ChoiceView; +use Symfony\Component\Form\Exception\LogicException; use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormInterface; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; class ChoiceTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\ChoiceType'; + public const TESTED_TYPE = ChoiceType::class; private array $choices = [ 'Bernhard' => 'a', @@ -1895,8 +1897,8 @@ public function testInitializeWithEmptyChoices() { $this->assertInstanceOf( FormInterface::class, $this->factory->createNamed('name', static::TESTED_TYPE, null, [ - 'choices' => [], - ])); + 'choices' => [], + ])); } public function testInitializeWithDefaultObjectChoice() @@ -2276,4 +2278,111 @@ public function testWithSameLoaderAndDifferentChoiceValueCallbacks() $this->assertSame('20', $view['choice_two']->vars['choices'][1]->value); $this->assertSame('30', $view['choice_two']->vars['choices'][2]->value); } + + public function testChoiceLazyThrowsWhenChoiceLoaderIsNotSet() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('The "choice_lazy" option can only be used if the "choice_loader" option is set.'); + + $this->factory->create(static::TESTED_TYPE, null, [ + 'choice_lazy' => true, + ]); + } + + public function testChoiceLazyLoadsAndRendersNothingWhenNoDataSet() + { + $form = $this->factory->create(static::TESTED_TYPE, null, [ + 'choice_loader' => new CallbackChoiceLoader(fn () => ['a' => 'A', 'b' => 'B']), + 'choice_lazy' => true, + ]); + + $this->assertNull($form->getData()); + + $view = $form->createView(); + $this->assertArrayHasKey('choices', $view->vars); + $this->assertSame([], $view->vars['choices']); + } + + public function testChoiceLazyLoadsAndRendersOnlyDataSetViaDefault() + { + $form = $this->factory->create(static::TESTED_TYPE, 'A', [ + 'choice_loader' => new CallbackChoiceLoader(fn () => ['a' => 'A', 'b' => 'B']), + 'choice_lazy' => true, + ]); + + $this->assertSame('A', $form->getData()); + + $view = $form->createView(); + $this->assertArrayHasKey('choices', $view->vars); + $this->assertCount(1, $view->vars['choices']); + $this->assertSame('A', $view->vars['choices'][0]->value); + } + + public function testChoiceLazyLoadsAndRendersOnlyDataSetViaSubmit() + { + $form = $this->factory->create(static::TESTED_TYPE, null, [ + 'choice_loader' => new CallbackChoiceLoader(fn () => ['a' => 'A', 'b' => 'B']), + 'choice_lazy' => true, + ]); + + $form->submit('B'); + $this->assertSame('B', $form->getData()); + + $view = $form->createView(); + $this->assertArrayHasKey('choices', $view->vars); + $this->assertCount(1, $view->vars['choices']); + $this->assertSame('B', $view->vars['choices'][0]->value); + } + + public function testChoiceLazyErrorWhenInvalidSubmitData() + { + $form = $this->factory->create(static::TESTED_TYPE, null, [ + 'choice_loader' => new CallbackChoiceLoader(fn () => ['a' => 'A', 'b' => 'B']), + 'choice_lazy' => true, + ]); + + $form->submit('invalid'); + $this->assertNull($form->getData()); + + $view = $form->createView(); + $this->assertArrayHasKey('choices', $view->vars); + $this->assertCount(0, $view->vars['choices']); + $this->assertCount(1, $form->getErrors()); + $this->assertSame('ERROR: The selected choice is invalid.', trim((string) $form->getErrors())); + } + + public function testChoiceLazyMultipleWithDefaultData() + { + $form = $this->factory->create(static::TESTED_TYPE, ['A', 'B'], [ + 'choice_loader' => new CallbackChoiceLoader(fn () => ['a' => 'A', 'b' => 'B', 'c' => 'C']), + 'choice_lazy' => true, + 'multiple' => true, + ]); + + $this->assertSame(['A', 'B'], $form->getData()); + + $view = $form->createView(); + $this->assertArrayHasKey('choices', $view->vars); + $this->assertCount(2, $view->vars['choices']); + $this->assertSame('A', $view->vars['choices'][0]->value); + $this->assertSame('B', $view->vars['choices'][1]->value); + } + + public function testChoiceLazyMultipleWithSubmittedData() + { + $form = $this->factory->create(static::TESTED_TYPE, null, [ + 'choice_loader' => new CallbackChoiceLoader(fn () => ['a' => 'A', 'b' => 'B', 'c' => 'C']), + 'choice_lazy' => true, + 'multiple' => true, + ]); + + $form->submit(['B', 'C']); + $this->assertSame(['B', 'C'], $form->getData()); + + $view = $form->createView(); + $this->assertArrayHasKey('choices', $view->vars); + $this->assertCount(2, $view->vars['choices']); + $this->assertSame('B', $view->vars['choices'][0]->value); + $this->assertSame('C', $view->vars['choices'][1]->value); + } } diff --git a/Tests/Extension/Core/Type/ChoiceTypeTranslationTest.php b/Tests/Extension/Core/Type/ChoiceTypeTranslationTest.php index 26055c203..f60c6664c 100644 --- a/Tests/Extension/Core/Type/ChoiceTypeTranslationTest.php +++ b/Tests/Extension/Core/Type/ChoiceTypeTranslationTest.php @@ -12,12 +12,13 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; use Symfony\Component\Form\Extension\Core\CoreExtension; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Test\TypeTestCase; use Symfony\Contracts\Translation\TranslatorInterface; class ChoiceTypeTranslationTest extends TypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\ChoiceType'; + public const TESTED_TYPE = ChoiceType::class; private array $choices = [ 'Bernhard' => 'a', @@ -27,11 +28,11 @@ class ChoiceTypeTranslationTest extends TypeTestCase 'Roman' => 'e', ]; - protected function getExtensions() + protected function getExtensions(): array { $translator = $this->createMock(TranslatorInterface::class); $translator->expects($this->any())->method('trans') - ->willReturnCallback(fn ($key, $params) => strtr(sprintf('Translation of: %s', $key), $params) + ->willReturnCallback(fn ($key, $params) => strtr(\sprintf('Translation of: %s', $key), $params) ); return array_merge(parent::getExtensions(), [new CoreExtension(null, null, $translator)]); diff --git a/Tests/Extension/Core/Type/CollectionTypeTest.php b/Tests/Extension/Core/Type/CollectionTypeTest.php index 08d512caf..95e1d9ca7 100644 --- a/Tests/Extension/Core/Type/CollectionTypeTest.php +++ b/Tests/Extension/Core/Type/CollectionTypeTest.php @@ -13,6 +13,7 @@ use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\CollectionType; use Symfony\Component\Form\Form; use Symfony\Component\Form\Tests\Fixtures\Author; use Symfony\Component\Form\Tests\Fixtures\AuthorType; @@ -20,7 +21,7 @@ class CollectionTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\CollectionType'; + public const TESTED_TYPE = CollectionType::class; public function testContainsNoChildByDefault() { @@ -120,7 +121,7 @@ public function testResizedDownWithDeleteEmptyCallable() $form = $this->factory->create(static::TESTED_TYPE, null, [ 'entry_type' => AuthorType::class, 'allow_delete' => true, - 'delete_empty' => fn (?Author $obj = null) => null === $obj || empty($obj->firstName), + 'delete_empty' => fn (?Author $obj = null) => null === $obj || !$obj->firstName, ]); $form->setData([new Author('Bob'), new Author('Alice')]); diff --git a/Tests/Extension/Core/Type/CountryTypeTest.php b/Tests/Extension/Core/Type/CountryTypeTest.php index 57146e1ec..44073ef7b 100644 --- a/Tests/Extension/Core/Type/CountryTypeTest.php +++ b/Tests/Extension/Core/Type/CountryTypeTest.php @@ -12,11 +12,12 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; use Symfony\Component\Form\ChoiceList\View\ChoiceView; +use Symfony\Component\Form\Extension\Core\Type\CountryType; use Symfony\Component\Intl\Util\IntlTestHelper; class CountryTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\CountryType'; + public const TESTED_TYPE = CountryType::class; protected function setUp(): void { diff --git a/Tests/Extension/Core/Type/CurrencyTypeTest.php b/Tests/Extension/Core/Type/CurrencyTypeTest.php index 51aaf6b37..3e8a53dfb 100644 --- a/Tests/Extension/Core/Type/CurrencyTypeTest.php +++ b/Tests/Extension/Core/Type/CurrencyTypeTest.php @@ -12,11 +12,12 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; use Symfony\Component\Form\ChoiceList\View\ChoiceView; +use Symfony\Component\Form\Extension\Core\Type\CurrencyType; use Symfony\Component\Intl\Util\IntlTestHelper; class CurrencyTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\CurrencyType'; + public const TESTED_TYPE = CurrencyType::class; protected function setUp(): void { diff --git a/Tests/Extension/Core/Type/DateTimeTypeTest.php b/Tests/Extension/Core/Type/DateTimeTypeTest.php index a402a70f9..5067bb05e 100644 --- a/Tests/Extension/Core/Type/DateTimeTypeTest.php +++ b/Tests/Extension/Core/Type/DateTimeTypeTest.php @@ -12,12 +12,13 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormInterface; class DateTimeTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\DateTimeType'; + public const TESTED_TYPE = DateTimeType::class; private string $defaultLocale; diff --git a/Tests/Extension/Core/Type/DateTypeTest.php b/Tests/Extension/Core/Type/DateTypeTest.php index dfbae9330..5f4f896b5 100644 --- a/Tests/Extension/Core/Type/DateTypeTest.php +++ b/Tests/Extension/Core/Type/DateTypeTest.php @@ -13,6 +13,7 @@ use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormInterface; use Symfony\Component\Intl\Intl; @@ -21,7 +22,7 @@ class DateTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\DateType'; + public const TESTED_TYPE = DateType::class; private string $defaultTimezone; private string $defaultLocale; @@ -1156,6 +1157,44 @@ public function testDateTimeImmutableInputTimezoneNotMatchingModelTimezone() ]); } + public function testSubmitWithCustomCalendarOption() + { + IntlTestHelper::requireFullIntl($this); + + // Creates a new form using the "roc" (Republic Of China) calendar. This calendar starts in 1912, the year 2024 in + // the Gregorian calendar is the year 113 in the "roc" calendar. + $form = $this->factory->create(static::TESTED_TYPE, options: [ + 'format' => 'y-MM-dd', + 'html5' => false, + 'input' => 'array', + 'calendar' => \IntlCalendar::createInstance(locale: 'zh_TW@calendar=roc'), + ]); + $form->submit('113-03-31'); + + $this->assertSame('2024', $form->getData()['year'], 'The year should be converted to the default locale (en)'); + $this->assertSame('31', $form->getData()['day']); + $this->assertSame('3', $form->getData()['month']); + + $this->assertSame('113-03-31', $form->getViewData()); + } + + public function testSetDataWithCustomCalendarOption() + { + IntlTestHelper::requireFullIntl($this); + + // Creates a new form using the "roc" (Republic Of China) calendar. This calendar starts in 1912, the year 2024 in + // the Gregorian calendar is the year 113 in the "roc" calendar. + $form = $this->factory->create(static::TESTED_TYPE, options: [ + 'format' => 'y-MM-dd', + 'html5' => false, + 'input' => 'array', + 'calendar' => \IntlCalendar::createInstance(locale: 'zh_TW@calendar=roc'), + ]); + $form->setData(['year' => '2024', 'month' => '3', 'day' => '31']); + + $this->assertSame('113-03-31', $form->getViewData()); + } + protected function getTestOptions(): array { return ['widget' => 'choice']; diff --git a/Tests/Extension/Core/Type/FileTypeTest.php b/Tests/Extension/Core/Type/FileTypeTest.php index b7f3332c1..85907b695 100644 --- a/Tests/Extension/Core/Type/FileTypeTest.php +++ b/Tests/Extension/Core/Type/FileTypeTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; use Symfony\Component\Form\Extension\Core\CoreExtension; +use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler; use Symfony\Component\Form\NativeRequestHandler; use Symfony\Component\Form\RequestHandlerInterface; @@ -21,9 +22,9 @@ class FileTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\FileType'; + public const TESTED_TYPE = FileType::class; - protected function getExtensions() + protected function getExtensions(): array { return array_merge(parent::getExtensions(), [new CoreExtension(null, null, new IdentityTranslator())]); } diff --git a/Tests/Extension/Core/Type/FormTypeTest.php b/Tests/Extension/Core/Type/FormTypeTest.php index be89c559f..fe19f3b12 100644 --- a/Tests/Extension/Core/Type/FormTypeTest.php +++ b/Tests/Extension/Core/Type/FormTypeTest.php @@ -64,7 +64,7 @@ public function setReferenceCopy($reference) class FormTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\FormType'; + public const TESTED_TYPE = FormType::class; public function testCreateFormInstances() { @@ -156,24 +156,24 @@ public function testDataClassMayBeNull() { $this->assertInstanceOf( FormBuilderInterface::class, $this->factory->createBuilder(static::TESTED_TYPE, null, [ - 'data_class' => null, - ])); + 'data_class' => null, + ])); } public function testDataClassMayBeAbstractClass() { $this->assertInstanceOf( FormBuilderInterface::class, $this->factory->createBuilder(static::TESTED_TYPE, null, [ - 'data_class' => 'Symfony\Component\Form\Tests\Fixtures\AbstractAuthor', - ])); + 'data_class' => 'Symfony\Component\Form\Tests\Fixtures\AbstractAuthor', + ])); } public function testDataClassMayBeInterface() { $this->assertInstanceOf( FormBuilderInterface::class, $this->factory->createBuilder(static::TESTED_TYPE, null, [ - 'data_class' => 'Symfony\Component\Form\Tests\Fixtures\AuthorInterface', - ])); + 'data_class' => 'Symfony\Component\Form\Tests\Fixtures\AuthorInterface', + ])); } public function testDataClassMustBeValidClassOrInterface() @@ -402,7 +402,7 @@ public function testSubformCallsSettersIfTheObjectChanged() // referenceCopy has a getter that returns a copy 'referenceCopy' => [ 'firstName' => 'Foo', - ], + ], ]); $this->assertEquals('Foo', $author->getReferenceCopy()->firstName); @@ -680,8 +680,8 @@ public function testDataMapperTransformationFailedExceptionInvalidMessageIsUsed( public function testPassZeroLabelToView() { $view = $this->factory->create(static::TESTED_TYPE, null, [ - 'label' => '0', - ]) + 'label' => '0', + ]) ->createView(); $this->assertSame('0', $view->vars['label']); diff --git a/Tests/Extension/Core/Type/IntegerTypeTest.php b/Tests/Extension/Core/Type/IntegerTypeTest.php index 1e143b342..ff33c17c6 100644 --- a/Tests/Extension/Core/Type/IntegerTypeTest.php +++ b/Tests/Extension/Core/Type/IntegerTypeTest.php @@ -11,11 +11,12 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; +use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Intl\Util\IntlTestHelper; class IntegerTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\IntegerType'; + public const TESTED_TYPE = IntegerType::class; private string $previousLocale; diff --git a/Tests/Extension/Core/Type/LanguageTypeTest.php b/Tests/Extension/Core/Type/LanguageTypeTest.php index e214e0afd..8eb085112 100644 --- a/Tests/Extension/Core/Type/LanguageTypeTest.php +++ b/Tests/Extension/Core/Type/LanguageTypeTest.php @@ -13,11 +13,12 @@ use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Form\Extension\Core\Type\LanguageType; use Symfony\Component\Intl\Util\IntlTestHelper; class LanguageTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\LanguageType'; + public const TESTED_TYPE = LanguageType::class; protected function setUp(): void { diff --git a/Tests/Extension/Core/Type/LocaleTypeTest.php b/Tests/Extension/Core/Type/LocaleTypeTest.php index 8486b6656..a2a820b39 100644 --- a/Tests/Extension/Core/Type/LocaleTypeTest.php +++ b/Tests/Extension/Core/Type/LocaleTypeTest.php @@ -12,11 +12,12 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; use Symfony\Component\Form\ChoiceList\View\ChoiceView; +use Symfony\Component\Form\Extension\Core\Type\LocaleType; use Symfony\Component\Intl\Util\IntlTestHelper; class LocaleTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\LocaleType'; + public const TESTED_TYPE = LocaleType::class; protected function setUp(): void { diff --git a/Tests/Extension/Core/Type/MoneyTypeTest.php b/Tests/Extension/Core/Type/MoneyTypeTest.php index b00439b57..f9112ffca 100644 --- a/Tests/Extension/Core/Type/MoneyTypeTest.php +++ b/Tests/Extension/Core/Type/MoneyTypeTest.php @@ -11,11 +11,12 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; +use Symfony\Component\Form\Extension\Core\Type\MoneyType; use Symfony\Component\Intl\Util\IntlTestHelper; class MoneyTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\MoneyType'; + public const TESTED_TYPE = MoneyType::class; private string $defaultLocale; @@ -32,8 +33,6 @@ protected function setUp(): void protected function tearDown(): void { - parent::tearDown(); - \Locale::setDefault($this->defaultLocale); } @@ -123,4 +122,28 @@ public function testHtml5EnablesSpecificFormatting() $this->assertSame('12345.60', $form->createView()->vars['value']); $this->assertSame('number', $form->createView()->vars['type']); } + + public function testDefaultInput() + { + $form = $this->factory->create(static::TESTED_TYPE, null, ['divisor' => 100]); + $form->submit('12345.67'); + + $this->assertSame(1234567.0, $form->getData()); + } + + public function testIntegerInput() + { + $form = $this->factory->create(static::TESTED_TYPE, null, ['divisor' => 100, 'input' => 'integer']); + $form->submit('12345.67'); + + $this->assertSame(1234567, $form->getData()); + } + + public function testIntegerInputWithoutDivisor() + { + $form = $this->factory->create(static::TESTED_TYPE, null, ['input' => 'integer']); + $form->submit('1234567'); + + $this->assertSame(1234567, $form->getData()); + } } diff --git a/Tests/Extension/Core/Type/NumberTypeTest.php b/Tests/Extension/Core/Type/NumberTypeTest.php index 9efe05221..95ccdfea9 100644 --- a/Tests/Extension/Core/Type/NumberTypeTest.php +++ b/Tests/Extension/Core/Type/NumberTypeTest.php @@ -13,11 +13,12 @@ use Symfony\Component\Form\Exception\LogicException; use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Intl\Util\IntlTestHelper; class NumberTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\NumberType'; + public const TESTED_TYPE = NumberType::class; private string $defaultLocale; @@ -34,8 +35,6 @@ protected function setUp(): void protected function tearDown(): void { - parent::tearDown(); - \Locale::setDefault($this->defaultLocale); } diff --git a/Tests/Extension/Core/Type/PasswordTypeTest.php b/Tests/Extension/Core/Type/PasswordTypeTest.php index 8d428a26a..945437bcb 100644 --- a/Tests/Extension/Core/Type/PasswordTypeTest.php +++ b/Tests/Extension/Core/Type/PasswordTypeTest.php @@ -11,9 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; +use Symfony\Component\Form\Extension\Core\Type\PasswordType; + class PasswordTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\PasswordType'; + public const TESTED_TYPE = PasswordType::class; public function testEmptyIfNotSubmitted() { diff --git a/Tests/Extension/Core/Type/PercentTypeTest.php b/Tests/Extension/Core/Type/PercentTypeTest.php index 76595d79b..120aab2f3 100644 --- a/Tests/Extension/Core/Type/PercentTypeTest.php +++ b/Tests/Extension/Core/Type/PercentTypeTest.php @@ -34,8 +34,6 @@ protected function setUp(): void protected function tearDown(): void { - parent::tearDown(); - \Locale::setDefault($this->defaultLocale); } diff --git a/Tests/Extension/Core/Type/RepeatedTypeTest.php b/Tests/Extension/Core/Type/RepeatedTypeTest.php index 06b9151fb..2d19a0613 100644 --- a/Tests/Extension/Core/Type/RepeatedTypeTest.php +++ b/Tests/Extension/Core/Type/RepeatedTypeTest.php @@ -11,13 +11,15 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; +use Symfony\Component\Form\Extension\Core\Type\RepeatedType; +use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Form; use Symfony\Component\Form\Tests\Fixtures\NotMappedType; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; class RepeatedTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\RepeatedType'; + public const TESTED_TYPE = RepeatedType::class; protected Form $form; @@ -188,6 +190,36 @@ public function testSetOptionsPerChildAndOverwrite() $this->assertTrue($form['second']->isRequired()); } + /** + * @dataProvider emptyDataProvider + */ + public function testSubmitNullForTextTypeWithEmptyDataOptionSetToEmptyString($emptyData, $submittedData, $expected) + { + $form = $this->factory->create(static::TESTED_TYPE, null, [ + 'type' => TextType::class, + 'options' => [ + 'empty_data' => $emptyData, + ], + ]); + $form->submit($submittedData); + + $this->assertSame($expected, $form->getData()); + } + + public static function emptyDataProvider() + { + yield ['', null, '']; + yield ['', ['first' => null, 'second' => null], '']; + yield ['', ['first' => '', 'second' => null], '']; + yield ['', ['first' => null, 'second' => ''], '']; + yield ['', ['first' => '', 'second' => ''], '']; + yield [null, null, null]; + yield [null, ['first' => null, 'second' => null], null]; + yield [null, ['first' => '', 'second' => null], null]; + yield [null, ['first' => null, 'second' => ''], null]; + yield [null, ['first' => '', 'second' => ''], null]; + } + public function testSubmitUnequal() { $input = ['first' => 'foo', 'second' => 'bar']; diff --git a/Tests/Extension/Core/Type/SubmitTypeTest.php b/Tests/Extension/Core/Type/SubmitTypeTest.php index 8a16175d7..af5ab8400 100644 --- a/Tests/Extension/Core/Type/SubmitTypeTest.php +++ b/Tests/Extension/Core/Type/SubmitTypeTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\SubmitButton; /** @@ -18,7 +19,7 @@ */ class SubmitTypeTest extends ButtonTypeTest { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\SubmitType'; + public const TESTED_TYPE = SubmitType::class; public function testCreateSubmitButtonInstances() { diff --git a/Tests/Extension/Core/Type/TextTypeTest.php b/Tests/Extension/Core/Type/TextTypeTest.php index e14a81636..483215168 100644 --- a/Tests/Extension/Core/Type/TextTypeTest.php +++ b/Tests/Extension/Core/Type/TextTypeTest.php @@ -11,9 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; +use Symfony\Component\Form\Extension\Core\Type\TextType; + class TextTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\TextType'; + public const TESTED_TYPE = TextType::class; public function testSubmitNull($expected = null, $norm = null, $view = null) { @@ -22,9 +24,9 @@ public function testSubmitNull($expected = null, $norm = null, $view = null) public function testSubmitNullReturnsNullWithEmptyDataAsString() { - $form = $this->factory->create(static::TESTED_TYPE, 'name', [ + $form = $this->factory->create(static::TESTED_TYPE, 'name', array_merge($this->getTestOptions(), [ 'empty_data' => '', - ]); + ])); $form->submit(null); $this->assertSame('', $form->getData()); @@ -48,9 +50,9 @@ public static function provideZeros(): array */ public function testSetDataThroughParamsWithZero($data, $dataAsString) { - $form = $this->factory->create(static::TESTED_TYPE, null, [ + $form = $this->factory->create(static::TESTED_TYPE, null, array_merge($this->getTestOptions(), [ 'data' => $data, - ]); + ])); $view = $form->createView(); $this->assertFalse($form->isEmpty()); diff --git a/Tests/Extension/Core/Type/TimeTypeTest.php b/Tests/Extension/Core/Type/TimeTypeTest.php index 155657038..8a2baf1b4 100644 --- a/Tests/Extension/Core/Type/TimeTypeTest.php +++ b/Tests/Extension/Core/Type/TimeTypeTest.php @@ -14,13 +14,14 @@ use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\Exception\InvalidConfigurationException; use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Form\Extension\Core\Type\TimeType; use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormInterface; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; class TimeTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\TimeType'; + public const TESTED_TYPE = TimeType::class; public function testSubmitDateTime() { diff --git a/Tests/Extension/Core/Type/TimezoneTypeTest.php b/Tests/Extension/Core/Type/TimezoneTypeTest.php index 9966b4043..4f2343974 100644 --- a/Tests/Extension/Core/Type/TimezoneTypeTest.php +++ b/Tests/Extension/Core/Type/TimezoneTypeTest.php @@ -13,11 +13,12 @@ use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\Exception\LogicException; +use Symfony\Component\Form\Extension\Core\Type\TimezoneType; use Symfony\Component\Intl\Util\IntlTestHelper; class TimezoneTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\TimezoneType'; + public const TESTED_TYPE = TimezoneType::class; public function testTimezonesAreSelectable() { diff --git a/Tests/Extension/Core/Type/UrlTypeTest.php b/Tests/Extension/Core/Type/UrlTypeTest.php index b9387d01a..a0d335647 100644 --- a/Tests/Extension/Core/Type/UrlTypeTest.php +++ b/Tests/Extension/Core/Type/UrlTypeTest.php @@ -11,14 +11,22 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; +use Symfony\Bridge\PhpUnit\ExpectUserDeprecationMessageTrait; +use Symfony\Component\Form\Extension\Core\Type\UrlType; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; class UrlTypeTest extends TextTypeTest { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\UrlType'; + use ExpectUserDeprecationMessageTrait; + public const TESTED_TYPE = UrlType::class; + + /** + * @group legacy + */ public function testSubmitAddsDefaultProtocolIfNoneIsIncluded() { + $this->expectUserDeprecationMessage('Since symfony/form 7.1: Not configuring the "default_protocol" option when using the UrlType is deprecated. It will default to "null" in 8.0.'); $form = $this->factory->create(static::TESTED_TYPE, 'name'); $form->submit('www.domain.com'); @@ -86,6 +94,7 @@ public function testThrowExceptionIfDefaultProtocolIsInvalid() public function testSubmitNullUsesDefaultEmptyData($emptyData = 'empty', $expectedData = 'http://empty') { $form = $this->factory->create(static::TESTED_TYPE, null, [ + 'default_protocol' => 'http', 'empty_data' => $emptyData, ]); $form->submit(null); @@ -95,4 +104,9 @@ public function testSubmitNullUsesDefaultEmptyData($emptyData = 'empty', $expect $this->assertSame($expectedData, $form->getNormData()); $this->assertSame($expectedData, $form->getData()); } + + protected function getTestOptions(): array + { + return ['default_protocol' => 'http']; + } } diff --git a/Tests/Extension/Core/Type/WeekTypeTest.php b/Tests/Extension/Core/Type/WeekTypeTest.php index a69b96a38..b4d58fd95 100644 --- a/Tests/Extension/Core/Type/WeekTypeTest.php +++ b/Tests/Extension/Core/Type/WeekTypeTest.php @@ -11,11 +11,12 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; +use Symfony\Component\Form\Extension\Core\Type\WeekType; use Symfony\Component\Form\FormError; class WeekTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\WeekType'; + public const TESTED_TYPE = WeekType::class; public function testSubmitArray() { diff --git a/Tests/Extension/Csrf/Type/FormTypeCsrfExtensionTest.php b/Tests/Extension/Csrf/Type/FormTypeCsrfExtensionTest.php index bfa302555..d5bce6527 100644 --- a/Tests/Extension/Csrf/Type/FormTypeCsrfExtensionTest.php +++ b/Tests/Extension/Csrf/Type/FormTypeCsrfExtensionTest.php @@ -42,7 +42,7 @@ protected function setUp(): void parent::setUp(); } - protected function getExtensions() + protected function getExtensions(): array { return array_merge(parent::getExtensions(), [ new CsrfExtension($this->tokenManager, new IdentityTranslator()), diff --git a/Tests/Extension/DataCollector/FormDataCollectorTest.php b/Tests/Extension/DataCollector/FormDataCollectorTest.php index 798faa0c5..4090fc97b 100644 --- a/Tests/Extension/DataCollector/FormDataCollectorTest.php +++ b/Tests/Extension/DataCollector/FormDataCollectorTest.php @@ -18,7 +18,6 @@ use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\DataCollector\FormDataCollector; use Symfony\Component\Form\Extension\DataCollector\FormDataExtractor; -use Symfony\Component\Form\Form; use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormFactory; use Symfony\Component\Form\FormInterface; @@ -71,7 +70,7 @@ public function testBuildPreliminaryFormTree() ], 'errors' => [], 'children' => [], - ]; + ]; $formData = [ 'id' => 'name', @@ -87,11 +86,11 @@ public function testBuildPreliminaryFormTree() 'norm' => null, ], 'errors' => [], - 'has_children_error' => false, - 'children' => [ - 'child' => $childFormData, - ], - ]; + 'has_children_error' => false, + 'children' => [ + 'child' => $childFormData, + ], + ]; $this->assertEquals([ 'forms' => [ @@ -102,7 +101,7 @@ public function testBuildPreliminaryFormTree() spl_object_hash($this->childForm) => $childFormData, ], 'nb_errors' => 0, - ], $this->dataCollector->getData()); + ], $this->dataCollector->getData()); } public function testBuildMultiplePreliminaryFormTrees() diff --git a/Tests/Extension/DataCollector/FormDataExtractorTest.php b/Tests/Extension/DataCollector/FormDataExtractorTest.php index b8a1fee37..29f9359df 100644 --- a/Tests/Extension/DataCollector/FormDataExtractorTest.php +++ b/Tests/Extension/DataCollector/FormDataExtractorTest.php @@ -362,8 +362,7 @@ public function testExtractSubmittedDataStoresErrorCause() ] EODUMP; } - $this->assertDumpMatchesFormat($expectedFormat - , + $this->assertDumpMatchesFormat($expectedFormat, $this->dataExtractor->extractSubmittedData($form) ); } diff --git a/Tests/Extension/DependencyInjection/DependencyInjectionExtensionTest.php b/Tests/Extension/DependencyInjection/DependencyInjectionExtensionTest.php index b99240c8c..8f2cbdcd5 100644 --- a/Tests/Extension/DependencyInjection/DependencyInjectionExtensionTest.php +++ b/Tests/Extension/DependencyInjection/DependencyInjectionExtensionTest.php @@ -44,7 +44,7 @@ public function testGetTypeExtensions() public function testThrowExceptionForInvalidExtendedType() { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage(sprintf('The extended type "unmatched" specified for the type extension class "%s" does not match any of the actual extended types (["test"]).', TestTypeExtension::class)); + $this->expectExceptionMessage(\sprintf('The extended type "unmatched" specified for the type extension class "%s" does not match any of the actual extended types (["test"]).', TestTypeExtension::class)); $extensions = [ 'unmatched' => new \ArrayIterator([new TestTypeExtension()]), diff --git a/Tests/Extension/HtmlSanitizer/Type/TextTypeHtmlSanitizerExtensionTest.php b/Tests/Extension/HtmlSanitizer/Type/TextTypeHtmlSanitizerExtensionTest.php index 39b8d0332..6784576b0 100644 --- a/Tests/Extension/HtmlSanitizer/Type/TextTypeHtmlSanitizerExtensionTest.php +++ b/Tests/Extension/HtmlSanitizer/Type/TextTypeHtmlSanitizerExtensionTest.php @@ -20,7 +20,7 @@ class TextTypeHtmlSanitizerExtensionTest extends TypeTestCase { - protected function getExtensions() + protected function getExtensions(): array { $fooSanitizer = $this->createMock(HtmlSanitizerInterface::class); $fooSanitizer->expects($this->once()) diff --git a/Tests/Extension/PasswordHasher/Type/PasswordTypePasswordHasherExtensionTest.php b/Tests/Extension/PasswordHasher/Type/PasswordTypePasswordHasherExtensionTest.php index 4ec91c827..07d1292a3 100644 --- a/Tests/Extension/PasswordHasher/Type/PasswordTypePasswordHasherExtensionTest.php +++ b/Tests/Extension/PasswordHasher/Type/PasswordTypePasswordHasherExtensionTest.php @@ -40,7 +40,7 @@ protected function setUp(): void parent::setUp(); } - protected function getExtensions() + protected function getExtensions(): array { return array_merge(parent::getExtensions(), [ new PasswordHasherExtension(new PasswordHasherListener($this->passwordHasher)), diff --git a/Tests/Extension/Validator/Constraints/FormValidatorFunctionalTest.php b/Tests/Extension/Validator/Constraints/FormValidatorFunctionalTest.php index aa6056c13..14595e8cf 100644 --- a/Tests/Extension/Validator/Constraints/FormValidatorFunctionalTest.php +++ b/Tests/Extension/Validator/Constraints/FormValidatorFunctionalTest.php @@ -88,9 +88,9 @@ public function testFieldConstraintsInvalidateFormIfFieldIsSubmitted() public function testNonCompositeConstraintValidatedOnce() { $form = $this->formFactory->create(TextType::class, null, [ - 'constraints' => [new NotBlank(['groups' => ['foo', 'bar']])], - 'validation_groups' => ['foo', 'bar'], - ]); + 'constraints' => [new NotBlank(['groups' => ['foo', 'bar']])], + 'validation_groups' => ['foo', 'bar'], + ]); $form->submit(''); $violations = $this->validator->validate($form); diff --git a/Tests/Extension/Validator/Constraints/FormValidatorPerformanceTest.php b/Tests/Extension/Validator/Constraints/FormValidatorPerformanceTest.php index e8bfbc64a..b0c7d719a 100644 --- a/Tests/Extension/Validator/Constraints/FormValidatorPerformanceTest.php +++ b/Tests/Extension/Validator/Constraints/FormValidatorPerformanceTest.php @@ -20,7 +20,7 @@ */ class FormValidatorPerformanceTest extends FormPerformanceTestCase { - protected function getExtensions() + protected function getExtensions(): array { return [ new ValidatorExtension(Validation::createValidator(), false), diff --git a/Tests/Extension/Validator/Constraints/FormValidatorTest.php b/Tests/Extension/Validator/Constraints/FormValidatorTest.php index 4e1588a9c..86b53ac3a 100644 --- a/Tests/Extension/Validator/Constraints/FormValidatorTest.php +++ b/Tests/Extension/Validator/Constraints/FormValidatorTest.php @@ -184,8 +184,8 @@ public function testDontValidateIfNoValidationGroups() $object = new \stdClass(); $form = $this->getBuilder('name', '\stdClass', [ - 'validation_groups' => [], - ]) + 'validation_groups' => [], + ]) ->setData($object) ->setCompound(true) ->setDataMapper(new DataMapper()) @@ -256,12 +256,12 @@ public function testDontValidateIfNotSynchronized() $object = new \stdClass(); $form = $this->getBuilder('name', '\stdClass', [ - 'invalid_message' => 'invalid_message_key', - // Invalid message parameters must be supported, because the - // invalid message can be a translation key - // see https://github.com/symfony/symfony/issues/5144 - 'invalid_message_parameters' => ['{{ foo }}' => 'bar'], - ]) + 'invalid_message' => 'invalid_message_key', + // Invalid message parameters must be supported, because the + // invalid message can be a translation key + // see https://github.com/symfony/symfony/issues/5144 + 'invalid_message_parameters' => ['{{ foo }}' => 'bar'], + ]) ->setData($object) ->addViewTransformer(new CallbackTransformer( static fn ($data) => $data, @@ -292,13 +292,13 @@ public function testAddInvalidErrorEvenIfNoValidationGroups() $object = new \stdClass(); $form = $this->getBuilder('name', '\stdClass', [ - 'invalid_message' => 'invalid_message_key', - // Invalid message parameters must be supported, because the - // invalid message can be a translation key - // see https://github.com/symfony/symfony/issues/5144 - 'invalid_message_parameters' => ['{{ foo }}' => 'bar'], - 'validation_groups' => [], - ]) + 'invalid_message' => 'invalid_message_key', + // Invalid message parameters must be supported, because the + // invalid message can be a translation key + // see https://github.com/symfony/symfony/issues/5144 + 'invalid_message_parameters' => ['{{ foo }}' => 'bar'], + 'validation_groups' => [], + ]) ->setData($object) ->addViewTransformer(new CallbackTransformer( static fn ($data) => $data, diff --git a/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php index 3b4cd7739..a1d1a3840 100644 --- a/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php @@ -15,9 +15,12 @@ use Symfony\Component\Form\Form; use Symfony\Component\Form\Forms; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; +use Symfony\Component\Form\Tests\Extension\Core\Type\CollectionTypeTest; use Symfony\Component\Form\Tests\Extension\Core\Type\FormTypeTest; use Symfony\Component\Form\Tests\Extension\Core\Type\TextTypeTest; use Symfony\Component\Form\Tests\Fixtures\Author; +use Symfony\Component\Form\Tests\Fixtures\AuthorType; +use Symfony\Component\Form\Tests\Fixtures\Organization; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\Constraints\Length; @@ -158,4 +161,185 @@ protected function createForm(array $options = []) { return $this->factory->create(FormTypeTest::TESTED_TYPE, null, $options); } + + public function testCollectionTypeKeepAsListOptionFalse() + { + $formMetadata = new ClassMetadata(Form::class); + $authorMetadata = (new ClassMetadata(Author::class)) + ->addPropertyConstraint('firstName', new NotBlank()); + $organizationMetadata = (new ClassMetadata(Organization::class)) + ->addPropertyConstraint('authors', new Valid()); + $metadataFactory = $this->createMock(MetadataFactoryInterface::class); + $metadataFactory->expects($this->any()) + ->method('getMetadataFor') + ->willReturnCallback(static function ($classOrObject) use ($formMetadata, $authorMetadata, $organizationMetadata) { + if (Author::class === $classOrObject || $classOrObject instanceof Author) { + return $authorMetadata; + } + + if (Organization::class === $classOrObject || $classOrObject instanceof Organization) { + return $organizationMetadata; + } + + if (Form::class === $classOrObject || $classOrObject instanceof Form) { + return $formMetadata; + } + + return new ClassMetadata(\is_string($classOrObject) ? $classOrObject : $classOrObject::class); + }); + + $validator = Validation::createValidatorBuilder() + ->setMetadataFactory($metadataFactory) + ->getValidator(); + + $form = Forms::createFormFactoryBuilder() + ->addExtension(new ValidatorExtension($validator)) + ->getFormFactory() + ->create(FormTypeTest::TESTED_TYPE, new Organization([]), [ + 'data_class' => Organization::class, + 'by_reference' => false, + ]) + ->add('authors', CollectionTypeTest::TESTED_TYPE, [ + 'entry_type' => AuthorType::class, + 'allow_add' => true, + 'allow_delete' => true, + 'keep_as_list' => false, + ]) + ; + + $form->submit([ + 'authors' => [ + 0 => [ + 'firstName' => '', // Fires a Not Blank Error + 'lastName' => 'lastName1', + ], + // key "1" could be missing if we add 4 blank form entries and then remove it. + 2 => [ + 'firstName' => '', // Fires a Not Blank Error + 'lastName' => 'lastName3', + ], + 3 => [ + 'firstName' => '', // Fires a Not Blank Error + 'lastName' => 'lastName3', + ], + ], + ]); + + // Form does have 3 not blank errors + $errors = $form->getErrors(true); + $this->assertCount(3, $errors); + + // Form behaves as expected. It has index 0, 2 and 3 (1 has been removed) + // But errors property paths mismatch happening with "keep_as_list" option set to false + $errorPaths = [ + $errors[0]->getCause()->getPropertyPath(), + $errors[1]->getCause()->getPropertyPath(), + $errors[2]->getCause()->getPropertyPath(), + ]; + + $this->assertTrue($form->get('authors')->has('0')); + $this->assertContains('data.authors[0].firstName', $errorPaths); + + $this->assertFalse($form->get('authors')->has('1')); + $this->assertContains('data.authors[1].firstName', $errorPaths); + + $this->assertTrue($form->get('authors')->has('2')); + $this->assertContains('data.authors[2].firstName', $errorPaths); + + $this->assertTrue($form->get('authors')->has('3')); + $this->assertNotContains('data.authors[3].firstName', $errorPaths); + + // As result, root form contain errors + $this->assertCount(1, $form->getErrors(false)); + } + + public function testCollectionTypeKeepAsListOptionTrue() + { + $formMetadata = new ClassMetadata(Form::class); + $authorMetadata = (new ClassMetadata(Author::class)) + ->addPropertyConstraint('firstName', new NotBlank()); + $organizationMetadata = (new ClassMetadata(Organization::class)) + ->addPropertyConstraint('authors', new Valid()); + $metadataFactory = $this->createMock(MetadataFactoryInterface::class); + $metadataFactory->expects($this->any()) + ->method('getMetadataFor') + ->willReturnCallback(static function ($classOrObject) use ($formMetadata, $authorMetadata, $organizationMetadata) { + if (Author::class === $classOrObject || $classOrObject instanceof Author) { + return $authorMetadata; + } + + if (Organization::class === $classOrObject || $classOrObject instanceof Organization) { + return $organizationMetadata; + } + + if (Form::class === $classOrObject || $classOrObject instanceof Form) { + return $formMetadata; + } + + return new ClassMetadata(\is_string($classOrObject) ? $classOrObject : $classOrObject::class); + }); + + $validator = Validation::createValidatorBuilder() + ->setMetadataFactory($metadataFactory) + ->getValidator(); + + $form = Forms::createFormFactoryBuilder() + ->addExtension(new ValidatorExtension($validator)) + ->getFormFactory() + ->create(FormTypeTest::TESTED_TYPE, new Organization([]), [ + 'data_class' => Organization::class, + 'by_reference' => false, + ]) + ->add('authors', CollectionTypeTest::TESTED_TYPE, [ + 'entry_type' => AuthorType::class, + 'allow_add' => true, + 'allow_delete' => true, + 'keep_as_list' => true, + ]) + ; + + $form->submit([ + 'authors' => [ + 0 => [ + 'firstName' => '', // Fires a Not Blank Error + 'lastName' => 'lastName1', + ], + // key "1" could be missing if we add 4 blank form entries and then remove it. + 2 => [ + 'firstName' => '', // Fires a Not Blank Error + 'lastName' => 'lastName3', + ], + 3 => [ + 'firstName' => '', // Fires a Not Blank Error + 'lastName' => 'lastName3', + ], + ], + ]); + + // Form does have 3 not blank errors + $errors = $form->getErrors(true); + $this->assertCount(3, $errors); + + // No property paths mismatch happening with "keep_as_list" option set to true + $errorPaths = [ + $errors[0]->getCause()->getPropertyPath(), + $errors[1]->getCause()->getPropertyPath(), + $errors[2]->getCause()->getPropertyPath(), + ]; + + $this->assertTrue($form->get('authors')->has('0')); + $this->assertContains('data.authors[0].firstName', $errorPaths); + + $this->assertTrue($form->get('authors')->has('1')); + $this->assertContains('data.authors[1].firstName', $errorPaths); + + $this->assertTrue($form->get('authors')->has('2')); + $this->assertContains('data.authors[2].firstName', $errorPaths); + + $this->assertFalse($form->get('authors')->has('3')); + $this->assertNotContains('data.authors[3].firstName', $errorPaths); + + // Root form does NOT contain errors + $this->assertCount(0, $form->getErrors(false)); + } } diff --git a/Tests/Extension/Validator/Type/UrlTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/UrlTypeValidatorExtensionTest.php index e6314a3c5..edb212cbd 100644 --- a/Tests/Extension/Validator/Type/UrlTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/UrlTypeValidatorExtensionTest.php @@ -20,7 +20,7 @@ class UrlTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase protected function createForm(array $options = []) { - return $this->factory->create(UrlType::class, null, $options); + return $this->factory->create(UrlType::class, null, $options + ['default_protocol' => 'http']); } public function testInvalidMessage() diff --git a/Tests/Extension/Validator/ValidatorTypeGuesserTest.php b/Tests/Extension/Validator/ValidatorTypeGuesserTest.php index 0ac6b3914..c561cd76f 100644 --- a/Tests/Extension/Validator/ValidatorTypeGuesserTest.php +++ b/Tests/Extension/Validator/ValidatorTypeGuesserTest.php @@ -50,7 +50,7 @@ class ValidatorTypeGuesserTest extends TestCase /** * @var MetadataFactoryInterface */ - private \Symfony\Component\Validator\Tests\Fixtures\FakeMetadataFactory $metadataFactory; + private FakeMetadataFactory $metadataFactory; protected function setUp(): void { diff --git a/Tests/Fixtures/Descriptor/resolved_form_type_1.json b/Tests/Fixtures/Descriptor/resolved_form_type_1.json index 27371fd6f..5590018c0 100644 --- a/Tests/Fixtures/Descriptor/resolved_form_type_1.json +++ b/Tests/Fixtures/Descriptor/resolved_form_type_1.json @@ -6,6 +6,7 @@ "choice_attr", "choice_filter", "choice_label", + "choice_lazy", "choice_loader", "choice_name", "choice_translation_domain", @@ -18,7 +19,9 @@ "multiple", "placeholder", "placeholder_attr", - "preferred_choices" + "preferred_choices", + "separator", + "separator_html" ], "overridden": { "Symfony\\Component\\Form\\Extension\\Core\\Type\\FormType": [ diff --git a/Tests/Fixtures/Descriptor/resolved_form_type_1.txt b/Tests/Fixtures/Descriptor/resolved_form_type_1.txt index c8aee5e78..93c6b66d9 100644 --- a/Tests/Fixtures/Descriptor/resolved_form_type_1.txt +++ b/Tests/Fixtures/Descriptor/resolved_form_type_1.txt @@ -8,22 +8,22 @@ Symfony\Component\Form\Extension\Core\Type\ChoiceType (Block prefix: "choice") choice_attr FormType FormType FormTypeCsrfExtension choice_filter -------------------- ------------------------------ ----------------------- choice_label compound action csrf_field_name - choice_loader data_class allow_file_upload csrf_message - choice_name empty_data attr csrf_protection - choice_translation_domain error_bubbling attr_translation_parameters csrf_token_id - choice_translation_parameters invalid_message auto_initialize csrf_token_manager - choice_value trim block_name - choices block_prefix - duplicate_preferred_choices by_reference - expanded data - group_by disabled - multiple form_attr - placeholder getter - placeholder_attr help - preferred_choices help_attr - help_html - help_translation_parameters - inherit_data + choice_lazy data_class allow_file_upload csrf_message + choice_loader empty_data attr csrf_protection + choice_name error_bubbling attr_translation_parameters csrf_token_id + choice_translation_domain invalid_message auto_initialize csrf_token_manager + choice_translation_parameters trim block_name + choice_value block_prefix + choices by_reference + duplicate_preferred_choices data + expanded disabled + group_by form_attr + multiple getter + placeholder help + placeholder_attr help_attr + preferred_choices help_html + separator help_translation_parameters + separator_html inherit_data invalid_message_parameters is_empty_callback label diff --git a/Tests/Fixtures/Organization.php b/Tests/Fixtures/Organization.php new file mode 100644 index 000000000..db9cee9f9 --- /dev/null +++ b/Tests/Fixtures/Organization.php @@ -0,0 +1,32 @@ +authors = $authors; + } + + public function getAuthors(): array + { + return $this->authors; + } + + public function addAuthor(Author $author): self + { + $this->authors[] = $author; + return $this; + } + + public function removeAuthor(Author $author): self + { + if (false !== $key = array_search($author, $this->authors, true)) { + array_splice($this->authors, $key, 1); + } + return $this; + } +} diff --git a/Tests/Fixtures/TestExtension.php b/Tests/Fixtures/TestExtension.php index 44725a69c..2704ee530 100644 --- a/Tests/Fixtures/TestExtension.php +++ b/Tests/Fixtures/TestExtension.php @@ -34,7 +34,7 @@ public function addType(FormTypeInterface $type) public function getType($name): FormTypeInterface { - return $this->types[$name] ?? null; + return $this->types[$name]; } public function hasType($name): bool diff --git a/Tests/FormFactoryTest.php b/Tests/FormFactoryTest.php index bb18464c7..0cb3cad73 100644 --- a/Tests/FormFactoryTest.php +++ b/Tests/FormFactoryTest.php @@ -173,10 +173,10 @@ public function testCreateBuilderUsesPatternIfFound() class ConfigurableFormTypeGuesser implements FormTypeGuesserInterface { - private ?\Symfony\Component\Form\Guess\TypeGuess $typeGuess = null; - private ?\Symfony\Component\Form\Guess\ValueGuess $requiredGuess = null; - private ?\Symfony\Component\Form\Guess\ValueGuess $maxLengthGuess = null; - private ?\Symfony\Component\Form\Guess\ValueGuess $patternGuess = null; + private ?TypeGuess $typeGuess = null; + private ?ValueGuess $requiredGuess = null; + private ?ValueGuess $maxLengthGuess = null; + private ?ValueGuess $patternGuess = null; public function guessType($class, $property): ?TypeGuess { diff --git a/Tests/NativeRequestHandlerTest.php b/Tests/NativeRequestHandlerTest.php index bdb0763f9..6ff64bc65 100644 --- a/Tests/NativeRequestHandlerTest.php +++ b/Tests/NativeRequestHandlerTest.php @@ -41,8 +41,6 @@ protected function setUp(): void protected function tearDown(): void { - parent::tearDown(); - $_GET = []; $_POST = []; $_FILES = []; @@ -99,6 +97,9 @@ public function testFixBuggyFilesArray() 'name' => [ 'field' => 'upload.txt', ], + 'full_path' => [ + 'field' => 'path/to/file/upload.txt', + ], 'type' => [ 'field' => 'text/plain', ], @@ -118,6 +119,7 @@ public function testFixBuggyFilesArray() $this->assertTrue($form->isSubmitted()); $this->assertEquals([ 'name' => 'upload.txt', + 'full_path' => 'path/to/file/upload.txt', 'type' => 'text/plain', 'tmp_name' => 'owfdskjasdfsa', 'error' => \UPLOAD_ERR_OK, @@ -168,8 +170,8 @@ public function testMethodOverrideHeaderIgnoredIfNotPost() $form = $this->createForm('param1', 'POST'); $this->setRequestData('GET', [ - 'param1' => 'DATA', - ]); + 'param1' => 'DATA', + ]); $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'PUT'; diff --git a/Tests/ResolvedFormTypeTest.php b/Tests/ResolvedFormTypeTest.php index ba0bf243d..fa28f7547 100644 --- a/Tests/ResolvedFormTypeTest.php +++ b/Tests/ResolvedFormTypeTest.php @@ -100,7 +100,7 @@ public function testCreateBuilderWithDataClassOption() public function testFailsCreateBuilderOnInvalidFormOptionsResolution() { $this->expectException(MissingOptionsException::class); - $this->expectExceptionMessage(sprintf('An error has occurred resolving the options of the form "%s": The required option "foo" is missing.', UsageTrackingFormType::class)); + $this->expectExceptionMessage(\sprintf('An error has occurred resolving the options of the form "%s": The required option "foo" is missing.', UsageTrackingFormType::class)); $this->resolvedType->createBuilder($this->formFactory, 'name'); } diff --git a/Tests/Resources/TranslationFilesTest.php b/Tests/Resources/TranslationFilesTest.php index 3b2fe40f4..157335dc6 100644 --- a/Tests/Resources/TranslationFilesTest.php +++ b/Tests/Resources/TranslationFilesTest.php @@ -26,7 +26,7 @@ public function testTranslationFileIsValid($filePath) $errors = XliffUtils::validateSchema($document); - $this->assertCount(0, $errors, sprintf('"%s" is invalid:%s', $filePath, \PHP_EOL.implode(\PHP_EOL, array_column($errors, 'message')))); + $this->assertCount(0, $errors, \sprintf('"%s" is invalid:%s', $filePath, \PHP_EOL.implode(\PHP_EOL, array_column($errors, 'message')))); } /** @@ -39,7 +39,7 @@ public function testTranslationFileIsValidWithoutEntityLoader($filePath) $errors = XliffUtils::validateSchema($document); - $this->assertCount(0, $errors, sprintf('"%s" is invalid:%s', $filePath, \PHP_EOL.implode(\PHP_EOL, array_column($errors, 'message')))); + $this->assertCount(0, $errors, \sprintf('"%s" is invalid:%s', $filePath, \PHP_EOL.implode(\PHP_EOL, array_column($errors, 'message')))); } public static function provideTranslationFiles() diff --git a/Tests/SimpleFormTest.php b/Tests/SimpleFormTest.php index 7ded9b853..d5d3549d2 100644 --- a/Tests/SimpleFormTest.php +++ b/Tests/SimpleFormTest.php @@ -503,9 +503,9 @@ public function testSetDataConvertsScalarToStringIfOnlyModelTransformer() { $form = $this->getBuilder() ->addModelTransformer(new FixedDataTransformer([ - '' => '', - 1 => 23, - ])) + '' => '', + 1 => 23, + ])) ->getForm(); $form->setData(1); diff --git a/Tests/Util/StringUtilTest.php b/Tests/Util/StringUtilTest.php index 8199d6843..d51481f6c 100644 --- a/Tests/Util/StringUtilTest.php +++ b/Tests/Util/StringUtilTest.php @@ -56,7 +56,7 @@ public static function spaceProvider(): array ['0020'], ['00A0'], ['1680'], -// ['180E'], + // ['180E'], ['2000'], ['2001'], ['2002'], diff --git a/Tests/VersionAwareTest.php b/Tests/VersionAwareTest.php deleted file mode 100644 index 1a35b72fc..000000000 --- a/Tests/VersionAwareTest.php +++ /dev/null @@ -1,24 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form\Tests; - -trait VersionAwareTest -{ - protected static int $supportedFeatureSetVersion = 404; - - protected function requiresFeatureSet(int $requiredFeatureSetVersion) - { - if ($requiredFeatureSetVersion > static::$supportedFeatureSetVersion) { - $this->markTestSkipped(sprintf('Test requires features from symfony/form %.2f but only version %.2f is supported.', $requiredFeatureSetVersion / 100, static::$supportedFeatureSetVersion / 100)); - } - } -} diff --git a/Tests/VersionAwareTestTrait.php b/Tests/VersionAwareTestTrait.php new file mode 100644 index 000000000..62e98934e --- /dev/null +++ b/Tests/VersionAwareTestTrait.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests; + +/** + * @deprecated since Symfony 7.2, use feature detection instead. + */ +trait VersionAwareTestTrait +{ + protected static int $supportedFeatureSetVersion = 404; + + /** + * @deprecated since Symfony 7.2, use feature detection instead. + */ + protected function requiresFeatureSet(int $requiredFeatureSetVersion) + { + trigger_deprecation('symfony/form', '7.2', 'The "%s" trait is deprecated, use feature detection instead.', VersionAwareTestTrait::class); + + if ($requiredFeatureSetVersion > static::$supportedFeatureSetVersion) { + $this->markTestSkipped(\sprintf('Test requires features from symfony/form %.2f but only version %.2f is supported.', $requiredFeatureSetVersion / 100, static::$supportedFeatureSetVersion / 100)); + } + } +} diff --git a/Util/OrderedHashMap.php b/Util/OrderedHashMap.php index 32d08caa8..7f81c0739 100644 --- a/Util/OrderedHashMap.php +++ b/Util/OrderedHashMap.php @@ -71,13 +71,6 @@ */ class OrderedHashMap implements \ArrayAccess, \IteratorAggregate, \Countable { - /** - * The elements of the map, indexed by their keys. - * - * @var TValue[] - */ - private array $elements = []; - /** * The keys of the map in the order in which they were inserted or changed. * @@ -95,11 +88,11 @@ class OrderedHashMap implements \ArrayAccess, \IteratorAggregate, \Countable /** * Creates a new map. * - * @param TValue[] $elements The elements to insert initially + * @param TValue[] $elements The initial elements of the map, indexed by their keys */ - public function __construct(array $elements = []) - { - $this->elements = $elements; + public function __construct( + private array $elements = [], + ) { // the explicit string type-cast is necessary as digit-only keys would be returned as integers otherwise $this->orderedKeys = array_map(strval(...), array_keys($elements)); } @@ -112,7 +105,7 @@ public function offsetExists(mixed $key): bool public function offsetGet(mixed $key): mixed { if (!isset($this->elements[$key])) { - throw new \OutOfBoundsException(sprintf('The offset "%s" does not exist.', $key)); + throw new \OutOfBoundsException(\sprintf('The offset "%s" does not exist.', $key)); } return $this->elements[$key]; diff --git a/Util/OrderedHashMapIterator.php b/Util/OrderedHashMapIterator.php index a0c400e21..927a28c04 100644 --- a/Util/OrderedHashMapIterator.php +++ b/Util/OrderedHashMapIterator.php @@ -24,14 +24,8 @@ */ class OrderedHashMapIterator implements \Iterator { - /** @var TValue[] */ - private array $elements; - /** @var list */ - private array $orderedKeys; private int $cursor = 0; private int $cursorId; - /** @var array */ - private array $managedCursors; private ?string $key = null; /** @var TValue|null */ private mixed $current = null; @@ -47,11 +41,11 @@ class OrderedHashMapIterator implements \Iterator * {@link OrderedHashMap} instance to support * recognizing the deletion of elements. */ - public function __construct(array &$elements, array &$orderedKeys, array &$managedCursors) - { - $this->elements = &$elements; - $this->orderedKeys = &$orderedKeys; - $this->managedCursors = &$managedCursors; + public function __construct( + private array &$elements, + private array &$orderedKeys, + private array &$managedCursors, + ) { $this->cursorId = \count($managedCursors); $this->managedCursors[$this->cursorId] = &$this->cursor; @@ -98,10 +92,6 @@ public function next(): void public function key(): mixed { - if (null === $this->key) { - return null; - } - return $this->key; } diff --git a/Util/ServerParams.php b/Util/ServerParams.php index e53faaa8a..2c23efcc8 100644 --- a/Util/ServerParams.php +++ b/Util/ServerParams.php @@ -18,11 +18,9 @@ */ class ServerParams { - private ?RequestStack $requestStack; - - public function __construct(?RequestStack $requestStack = null) - { - $this->requestStack = $requestStack; + public function __construct( + private ?RequestStack $requestStack = null, + ) { } /** diff --git a/composer.json b/composer.json index b8f161e4a..40c021d91 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ ], "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/event-dispatcher": "^6.4|^7.0", "symfony/options-resolver": "^6.4|^7.0", "symfony/polyfill-ctype": "~1.8",