diff --git a/AbstractExtension.php b/AbstractExtension.php index ca2790021a..27665727cf 100644 --- a/AbstractExtension.php +++ b/AbstractExtension.php @@ -24,63 +24,50 @@ abstract class AbstractExtension implements FormExtensionInterface * * @var FormTypeInterface[] */ - private $types; + private array $types; /** * The type extensions provided by this extension. * * @var FormTypeExtensionInterface[][] */ - private $typeExtensions; + private array $typeExtensions; /** * The type guesser provided by this extension. - * - * @var FormTypeGuesserInterface|null */ - private $typeGuesser; + private ?FormTypeGuesserInterface $typeGuesser = null; /** * Whether the type guesser has been loaded. - * - * @var bool */ - private $typeGuesserLoaded = false; + private bool $typeGuesserLoaded = false; - /** - * {@inheritdoc} - */ - public function getType(string $name) + public function getType(string $name): FormTypeInterface { - if (null === $this->types) { + if (!isset($this->types)) { $this->initTypes(); } 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]; } - /** - * {@inheritdoc} - */ - public function hasType(string $name) + public function hasType(string $name): bool { - if (null === $this->types) { + if (!isset($this->types)) { $this->initTypes(); } return isset($this->types[$name]); } - /** - * {@inheritdoc} - */ - public function getTypeExtensions(string $name) + public function getTypeExtensions(string $name): array { - if (null === $this->typeExtensions) { + if (!isset($this->typeExtensions)) { $this->initTypeExtensions(); } @@ -88,22 +75,16 @@ public function getTypeExtensions(string $name) ?? []; } - /** - * {@inheritdoc} - */ - public function hasTypeExtensions(string $name) + public function hasTypeExtensions(string $name): bool { - if (null === $this->typeExtensions) { + if (!isset($this->typeExtensions)) { $this->initTypeExtensions(); } return isset($this->typeExtensions[$name]) && \count($this->typeExtensions[$name]) > 0; } - /** - * {@inheritdoc} - */ - public function getTypeGuesser() + public function getTypeGuesser(): ?FormTypeGuesserInterface { if (!$this->typeGuesserLoaded) { $this->initTypeGuesser(); @@ -117,7 +98,7 @@ public function getTypeGuesser() * * @return FormTypeInterface[] */ - protected function loadTypes() + protected function loadTypes(): array { return []; } @@ -127,17 +108,15 @@ protected function loadTypes() * * @return FormTypeExtensionInterface[] */ - protected function loadTypeExtensions() + protected function loadTypeExtensions(): array { return []; } /** * Registers the type guesser. - * - * @return FormTypeGuesserInterface|null */ - protected function loadTypeGuesser() + protected function loadTypeGuesser(): ?FormTypeGuesserInterface { return null; } @@ -147,7 +126,7 @@ protected function loadTypeGuesser() * * @throws UnexpectedTypeException if any registered type is not an instance of FormTypeInterface */ - private function initTypes() + private function initTypes(): void { $this->types = []; @@ -156,7 +135,7 @@ private function initTypes() throw new UnexpectedTypeException($type, FormTypeInterface::class); } - $this->types[\get_class($type)] = $type; + $this->types[$type::class] = $type; } } @@ -166,7 +145,7 @@ private function initTypes() * @throws UnexpectedTypeException if any registered type extension is not * an instance of FormTypeExtensionInterface */ - private function initTypeExtensions() + private function initTypeExtensions(): void { $this->typeExtensions = []; @@ -186,7 +165,7 @@ private function initTypeExtensions() * * @throws UnexpectedTypeException if the type guesser is not an instance of FormTypeGuesserInterface */ - private function initTypeGuesser() + private function initTypeGuesser(): void { $this->typeGuesserLoaded = true; diff --git a/AbstractRendererEngine.php b/AbstractRendererEngine.php index 07bf28c885..1968f5a376 100644 --- a/AbstractRendererEngine.php +++ b/AbstractRendererEngine.php @@ -25,30 +25,25 @@ abstract class AbstractRendererEngine implements FormRendererEngineInterface, Re */ public const CACHE_KEY_VAR = 'cache_key'; - /** - * @var array - */ - protected $defaultThemes; - /** * @var array[] */ - protected $themes = []; + protected array $themes = []; /** * @var bool[] */ - protected $useDefaultThemes = []; + protected array $useDefaultThemes = []; /** * @var array[] */ - protected $resources = []; + protected array $resources = []; /** * @var array> */ - private $resourceHierarchyLevels = []; + private array $resourceHierarchyLevels = []; /** * Creates a new renderer engine. @@ -56,15 +51,12 @@ 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 = [], + ) { } - /** - * {@inheritdoc} - */ - public function setTheme(FormView $view, $themes, bool $useDefaultThemes = true) + public function setTheme(FormView $view, mixed $themes, bool $useDefaultThemes = true): void { $cacheKey = $view->vars[self::CACHE_KEY_VAR]; @@ -78,10 +70,7 @@ public function setTheme(FormView $view, $themes, bool $useDefaultThemes = true) unset($this->resources[$cacheKey], $this->resourceHierarchyLevels[$cacheKey]); } - /** - * {@inheritdoc} - */ - public function getResourceForBlockName(FormView $view, string $blockName) + public function getResourceForBlockName(FormView $view, string $blockName): mixed { $cacheKey = $view->vars[self::CACHE_KEY_VAR]; @@ -92,10 +81,7 @@ public function getResourceForBlockName(FormView $view, string $blockName) return $this->resources[$cacheKey][$blockName]; } - /** - * {@inheritdoc} - */ - public function getResourceForBlockNameHierarchy(FormView $view, array $blockNameHierarchy, int $hierarchyLevel) + public function getResourceForBlockNameHierarchy(FormView $view, array $blockNameHierarchy, int $hierarchyLevel): mixed { $cacheKey = $view->vars[self::CACHE_KEY_VAR]; $blockName = $blockNameHierarchy[$hierarchyLevel]; @@ -107,10 +93,7 @@ public function getResourceForBlockNameHierarchy(FormView $view, array $blockNam return $this->resources[$cacheKey][$blockName]; } - /** - * {@inheritdoc} - */ - public function getResourceHierarchyLevel(FormView $view, array $blockNameHierarchy, int $hierarchyLevel) + public function getResourceHierarchyLevel(FormView $view, array $blockNameHierarchy, int $hierarchyLevel): int|false { $cacheKey = $view->vars[self::CACHE_KEY_VAR]; $blockName = $blockNameHierarchy[$hierarchyLevel]; @@ -133,10 +116,8 @@ public function getResourceHierarchyLevel(FormView $view, array $blockNameHierar * Loads the cache with the resource for a given block name. * * @see getResourceForBlock() - * - * @return bool */ - abstract protected function loadResourceForBlockName(string $cacheKey, FormView $view, string $blockName); + abstract protected function loadResourceForBlockName(string $cacheKey, FormView $view, string $blockName): bool; /** * Loads the cache with the resource for a specific level of a block hierarchy. diff --git a/AbstractType.php b/AbstractType.php index 3325b8bc27..8fffa379d8 100644 --- a/AbstractType.php +++ b/AbstractType.php @@ -21,46 +21,46 @@ abstract class AbstractType implements FormTypeInterface { /** - * {@inheritdoc} + * @return string|null */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function getParent() { + return FormType::class; } /** - * {@inheritdoc} + * @return void */ - public function buildView(FormView $view, FormInterface $form, array $options) + public function configureOptions(OptionsResolver $resolver) { } /** - * {@inheritdoc} + * @return void */ - public function finishView(FormView $view, FormInterface $form, array $options) + public function buildForm(FormBuilderInterface $builder, array $options) { } /** - * {@inheritdoc} + * @return void */ - public function configureOptions(OptionsResolver $resolver) + public function buildView(FormView $view, FormInterface $form, array $options) { } /** - * {@inheritdoc} + * @return void */ - public function getBlockPrefix() + public function finishView(FormView $view, FormInterface $form, array $options) { - return StringUtil::fqcnToBlockPrefix(static::class) ?: ''; } /** - * {@inheritdoc} + * @return string */ - public function getParent() + public function getBlockPrefix() { - return FormType::class; + return StringUtil::fqcnToBlockPrefix(static::class) ?: ''; } } diff --git a/AbstractTypeExtension.php b/AbstractTypeExtension.php index 9d369bf294..b32f3b522f 100644 --- a/AbstractTypeExtension.php +++ b/AbstractTypeExtension.php @@ -18,31 +18,19 @@ */ abstract class AbstractTypeExtension implements FormTypeExtensionInterface { - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function configureOptions(OptionsResolver $resolver): void { } - /** - * {@inheritdoc} - */ - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { } - /** - * {@inheritdoc} - */ - public function finishView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function finishView(FormView $view, FormInterface $form, array $options): void { } } diff --git a/Button.php b/Button.php index 03ae96f073..c35f93d606 100644 --- a/Button.php +++ b/Button.php @@ -13,6 +13,8 @@ use Symfony\Component\Form\Exception\AlreadySubmittedException; use Symfony\Component\Form\Exception\BadMethodCallException; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\PropertyAccess\PropertyPathInterface; /** * A form button. @@ -23,38 +25,21 @@ */ class Button implements \IteratorAggregate, FormInterface { - /** - * @var FormInterface|null - */ - private $parent; - - /** - * @var FormConfigInterface - */ - private $config; - - /** - * @var bool - */ - private $submitted = false; + private ?FormInterface $parent = null; + 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, + ) { } /** * Unsupported method. - * - * @param string $offset - * - * @return bool */ - #[\ReturnTypeWillChange] - public function offsetExists($offset) + public function offsetExists(mixed $offset): bool { return false; } @@ -64,14 +49,9 @@ public function offsetExists($offset) * * This method should not be invoked. * - * @param string $offset - * - * @return FormInterface - * * @throws BadMethodCallException */ - #[\ReturnTypeWillChange] - public function offsetGet($offset) + public function offsetGet(mixed $offset): FormInterface { throw new BadMethodCallException('Buttons cannot have children.'); } @@ -81,15 +61,9 @@ public function offsetGet($offset) * * This method should not be invoked. * - * @param string $offset - * @param FormInterface $value - * - * @return void - * * @throws BadMethodCallException */ - #[\ReturnTypeWillChange] - public function offsetSet($offset, $value) + public function offsetSet(mixed $offset, mixed $value): void { throw new BadMethodCallException('Buttons cannot have children.'); } @@ -99,22 +73,14 @@ public function offsetSet($offset, $value) * * This method should not be invoked. * - * @param string $offset - * - * @return void - * * @throws BadMethodCallException */ - #[\ReturnTypeWillChange] - public function offsetUnset($offset) + public function offsetUnset(mixed $offset): void { throw new BadMethodCallException('Buttons cannot have children.'); } - /** - * {@inheritdoc} - */ - public function setParent(?FormInterface $parent = null) + public function setParent(?FormInterface $parent): static { if ($this->submitted) { throw new AlreadySubmittedException('You cannot set the parent of a submitted button.'); @@ -125,10 +91,7 @@ public function setParent(?FormInterface $parent = null) return $this; } - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?FormInterface { return $this->parent; } @@ -140,7 +103,7 @@ public function getParent() * * @throws BadMethodCallException */ - public function add($child, ?string $type = null, array $options = []) + public function add(string|FormInterface $child, ?string $type = null, array $options = []): static { throw new BadMethodCallException('Buttons cannot have children.'); } @@ -152,17 +115,15 @@ public function add($child, ?string $type = null, array $options = []) * * @throws BadMethodCallException */ - public function get(string $name) + public function get(string $name): FormInterface { throw new BadMethodCallException('Buttons cannot have children.'); } /** * Unsupported method. - * - * @return bool */ - public function has(string $name) + public function has(string $name): bool { return false; } @@ -174,23 +135,17 @@ public function has(string $name) * * @throws BadMethodCallException */ - public function remove(string $name) + public function remove(string $name): static { throw new BadMethodCallException('Buttons cannot have children.'); } - /** - * {@inheritdoc} - */ - public function all() + public function all(): array { return []; } - /** - * {@inheritdoc} - */ - public function getErrors(bool $deep = false, bool $flatten = true) + public function getErrors(bool $deep = false, bool $flatten = true): FormErrorIterator { return new FormErrorIterator($this, []); } @@ -200,11 +155,9 @@ public function getErrors(bool $deep = false, bool $flatten = true) * * This method should not be invoked. * - * @param mixed $modelData - * * @return $this */ - public function setData($modelData) + public function setData(mixed $modelData): static { // no-op, called during initialization of the form tree return $this; @@ -213,7 +166,7 @@ public function setData($modelData) /** * Unsupported method. */ - public function getData() + public function getData(): mixed { return null; } @@ -221,7 +174,7 @@ public function getData() /** * Unsupported method. */ - public function getNormData() + public function getNormData(): mixed { return null; } @@ -229,47 +182,39 @@ public function getNormData() /** * Unsupported method. */ - public function getViewData() + public function getViewData(): mixed { return null; } /** * Unsupported method. - * - * @return array */ - public function getExtraData() + public function getExtraData(): array { return []; } /** * Returns the button's configuration. - * - * @return FormConfigInterface */ - public function getConfig() + public function getConfig(): FormConfigInterface { return $this->config; } /** * Returns whether the button is submitted. - * - * @return bool */ - public function isSubmitted() + public function isSubmitted(): bool { return $this->submitted; } /** * Returns the name by which the button is identified in forms. - * - * @return string */ - public function getName() + public function getName(): string { return $this->config->getName(); } @@ -277,7 +222,7 @@ public function getName() /** * Unsupported method. */ - public function getPropertyPath() + public function getPropertyPath(): ?PropertyPathInterface { return null; } @@ -287,37 +232,30 @@ public function getPropertyPath() * * @throws BadMethodCallException */ - public function addError(FormError $error) + public function addError(FormError $error): static { throw new BadMethodCallException('Buttons cannot have errors.'); } /** * Unsupported method. - * - * @return bool */ - public function isValid() + public function isValid(): bool { return true; } /** * Unsupported method. - * - * @return bool */ - public function isRequired() + public function isRequired(): bool { return false; } - /** - * {@inheritdoc} - */ - public function isDisabled() + public function isDisabled(): bool { - if ($this->parent && $this->parent->isDisabled()) { + if ($this->parent?->isDisabled()) { return true; } @@ -326,20 +264,16 @@ public function isDisabled() /** * Unsupported method. - * - * @return bool */ - public function isEmpty() + public function isEmpty(): bool { return true; } /** * Unsupported method. - * - * @return bool */ - public function isSynchronized() + public function isSynchronized(): bool { return true; } @@ -347,7 +281,7 @@ public function isSynchronized() /** * Unsupported method. */ - public function getTransformationFailure() + public function getTransformationFailure(): ?TransformationFailedException { return null; } @@ -357,7 +291,7 @@ public function getTransformationFailure() * * @throws BadMethodCallException */ - public function initialize() + public function initialize(): static { throw new BadMethodCallException('Buttons cannot be initialized. Call initialize() on the root form instead.'); } @@ -365,11 +299,9 @@ public function initialize() /** * Unsupported method. * - * @param mixed $request - * * @throws BadMethodCallException */ - public function handleRequest($request = null) + public function handleRequest(mixed $request = null): static { throw new BadMethodCallException('Buttons cannot handle requests. Call handleRequest() on the root form instead.'); } @@ -377,14 +309,11 @@ public function handleRequest($request = null) /** * Submits data to the button. * - * @param array|string|null $submittedData Not used - * @param bool $clearMissing Not used - * * @return $this * - * @throws Exception\AlreadySubmittedException if the button has already been submitted + * @throws AlreadySubmittedException if the button has already been submitted */ - public function submit($submittedData, bool $clearMissing = true) + public function submit(array|string|null $submittedData, bool $clearMissing = true): static { if ($this->submitted) { throw new AlreadySubmittedException('A form can only be submitted once.'); @@ -395,26 +324,17 @@ public function submit($submittedData, bool $clearMissing = true) return $this; } - /** - * {@inheritdoc} - */ - public function getRoot() + public function getRoot(): FormInterface { return $this->parent ? $this->parent->getRoot() : $this; } - /** - * {@inheritdoc} - */ - public function isRoot() + public function isRoot(): bool { return null === $this->parent; } - /** - * {@inheritdoc} - */ - public function createView(?FormView $parent = null) + public function createView(?FormView $parent = null): FormView { if (null === $parent && $this->parent) { $parent = $this->parent->createView(); @@ -433,22 +353,16 @@ public function createView(?FormView $parent = null) /** * Unsupported method. - * - * @return int */ - #[\ReturnTypeWillChange] - public function count() + public function count(): int { return 0; } /** * Unsupported method. - * - * @return \EmptyIterator */ - #[\ReturnTypeWillChange] - public function getIterator() + public function getIterator(): \EmptyIterator { return new \EmptyIterator(); } diff --git a/ButtonBuilder.php b/ButtonBuilder.php index 3c6b0be18c..4f7079142e 100644 --- a/ButtonBuilder.php +++ b/ButtonBuilder.php @@ -14,6 +14,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Form\Exception\BadMethodCallException; use Symfony\Component\Form\Exception\InvalidArgumentException; +use Symfony\Component\PropertyAccess\PropertyPathInterface; /** * A builder for {@link Button} instances. @@ -24,44 +25,25 @@ */ class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface { - protected $locked = false; + protected bool $locked = false; - /** - * @var bool - */ - private $disabled = false; - - /** - * @var ResolvedFormTypeInterface - */ - private $type; - - /** - * @var string - */ - private $name; - - /** - * @var array - */ - private $attributes = []; - - /** - * @var array - */ - private $options; + private bool $disabled = false; + private ResolvedFormTypeInterface $type; + private string $name; + private array $attributes = []; /** * @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); } @@ -71,7 +53,7 @@ public function __construct(?string $name, array $options = []) * * @throws BadMethodCallException */ - public function add($child, ?string $type = null, array $options = []) + public function add(string|FormBuilderInterface $child, ?string $type = null, array $options = []): never { throw new BadMethodCallException('Buttons cannot have children.'); } @@ -81,7 +63,7 @@ public function add($child, ?string $type = null, array $options = []) * * @throws BadMethodCallException */ - public function create(string $name, ?string $type = null, array $options = []) + public function create(string $name, ?string $type = null, array $options = []): never { throw new BadMethodCallException('Buttons cannot have children.'); } @@ -91,7 +73,7 @@ public function create(string $name, ?string $type = null, array $options = []) * * @throws BadMethodCallException */ - public function get(string $name) + public function get(string $name): never { throw new BadMethodCallException('Buttons cannot have children.'); } @@ -101,37 +83,31 @@ public function get(string $name) * * @throws BadMethodCallException */ - public function remove(string $name) + public function remove(string $name): never { throw new BadMethodCallException('Buttons cannot have children.'); } /** * Unsupported method. - * - * @return bool */ - public function has(string $name) + public function has(string $name): bool { return false; } /** * Returns the children. - * - * @return array */ - public function all() + public function all(): array { return []; } /** * Creates the button. - * - * @return Button */ - public function getForm() + public function getForm(): Button { return new Button($this->getFormConfig()); } @@ -141,7 +117,7 @@ public function getForm() * * @throws BadMethodCallException */ - public function addEventListener(string $eventName, callable $listener, int $priority = 0) + public function addEventListener(string $eventName, callable $listener, int $priority = 0): never { throw new BadMethodCallException('Buttons do not support event listeners.'); } @@ -151,7 +127,7 @@ public function addEventListener(string $eventName, callable $listener, int $pri * * @throws BadMethodCallException */ - public function addEventSubscriber(EventSubscriberInterface $subscriber) + public function addEventSubscriber(EventSubscriberInterface $subscriber): never { throw new BadMethodCallException('Buttons do not support event subscribers.'); } @@ -161,7 +137,7 @@ public function addEventSubscriber(EventSubscriberInterface $subscriber) * * @throws BadMethodCallException */ - public function addViewTransformer(DataTransformerInterface $viewTransformer, bool $forcePrepend = false) + public function addViewTransformer(DataTransformerInterface $viewTransformer, bool $forcePrepend = false): never { throw new BadMethodCallException('Buttons do not support data transformers.'); } @@ -171,7 +147,7 @@ public function addViewTransformer(DataTransformerInterface $viewTransformer, bo * * @throws BadMethodCallException */ - public function resetViewTransformers() + public function resetViewTransformers(): never { throw new BadMethodCallException('Buttons do not support data transformers.'); } @@ -181,7 +157,7 @@ public function resetViewTransformers() * * @throws BadMethodCallException */ - public function addModelTransformer(DataTransformerInterface $modelTransformer, bool $forceAppend = false) + public function addModelTransformer(DataTransformerInterface $modelTransformer, bool $forceAppend = false): never { throw new BadMethodCallException('Buttons do not support data transformers.'); } @@ -191,15 +167,15 @@ public function addModelTransformer(DataTransformerInterface $modelTransformer, * * @throws BadMethodCallException */ - public function resetModelTransformers() + public function resetModelTransformers(): never { throw new BadMethodCallException('Buttons do not support data transformers.'); } /** - * {@inheritdoc} + * @return $this */ - public function setAttribute(string $name, $value) + public function setAttribute(string $name, mixed $value): static { $this->attributes[$name] = $value; @@ -207,9 +183,9 @@ public function setAttribute(string $name, $value) } /** - * {@inheritdoc} + * @return $this */ - public function setAttributes(array $attributes) + public function setAttributes(array $attributes): static { $this->attributes = $attributes; @@ -221,7 +197,7 @@ public function setAttributes(array $attributes) * * @throws BadMethodCallException */ - public function setDataMapper(?DataMapperInterface $dataMapper = null) + public function setDataMapper(?DataMapperInterface $dataMapper): never { throw new BadMethodCallException('Buttons do not support data mappers.'); } @@ -231,7 +207,7 @@ public function setDataMapper(?DataMapperInterface $dataMapper = null) * * @return $this */ - public function setDisabled(bool $disabled) + public function setDisabled(bool $disabled): static { $this->disabled = $disabled; @@ -243,7 +219,7 @@ public function setDisabled(bool $disabled) * * @throws BadMethodCallException */ - public function setEmptyData($emptyData) + public function setEmptyData(mixed $emptyData): never { throw new BadMethodCallException('Buttons do not support empty data.'); } @@ -253,7 +229,7 @@ public function setEmptyData($emptyData) * * @throws BadMethodCallException */ - public function setErrorBubbling(bool $errorBubbling) + public function setErrorBubbling(bool $errorBubbling): never { throw new BadMethodCallException('Buttons do not support error bubbling.'); } @@ -263,7 +239,7 @@ public function setErrorBubbling(bool $errorBubbling) * * @throws BadMethodCallException */ - public function setRequired(bool $required) + public function setRequired(bool $required): never { throw new BadMethodCallException('Buttons cannot be required.'); } @@ -273,7 +249,7 @@ public function setRequired(bool $required) * * @throws BadMethodCallException */ - public function setPropertyPath($propertyPath) + public function setPropertyPath(string|PropertyPathInterface|null $propertyPath): never { throw new BadMethodCallException('Buttons do not support property paths.'); } @@ -283,7 +259,7 @@ public function setPropertyPath($propertyPath) * * @throws BadMethodCallException */ - public function setMapped(bool $mapped) + public function setMapped(bool $mapped): never { throw new BadMethodCallException('Buttons do not support data mapping.'); } @@ -293,7 +269,7 @@ public function setMapped(bool $mapped) * * @throws BadMethodCallException */ - public function setByReference(bool $byReference) + public function setByReference(bool $byReference): never { throw new BadMethodCallException('Buttons do not support data mapping.'); } @@ -303,7 +279,7 @@ public function setByReference(bool $byReference) * * @throws BadMethodCallException */ - public function setCompound(bool $compound) + public function setCompound(bool $compound): never { throw new BadMethodCallException('Buttons cannot be compound.'); } @@ -313,7 +289,7 @@ public function setCompound(bool $compound) * * @return $this */ - public function setType(ResolvedFormTypeInterface $type) + public function setType(ResolvedFormTypeInterface $type): static { $this->type = $type; @@ -325,7 +301,7 @@ public function setType(ResolvedFormTypeInterface $type) * * @throws BadMethodCallException */ - public function setData($data) + public function setData(mixed $data): never { throw new BadMethodCallException('Buttons do not support data.'); } @@ -335,7 +311,7 @@ public function setData($data) * * @throws BadMethodCallException */ - public function setDataLocked(bool $locked) + public function setDataLocked(bool $locked): never { throw new BadMethodCallException('Buttons do not support data locking.'); } @@ -345,7 +321,7 @@ public function setDataLocked(bool $locked) * * @throws BadMethodCallException */ - public function setFormFactory(FormFactoryInterface $formFactory) + public function setFormFactory(FormFactoryInterface $formFactory): never { throw new BadMethodCallException('Buttons do not support form factories.'); } @@ -355,7 +331,7 @@ public function setFormFactory(FormFactoryInterface $formFactory) * * @throws BadMethodCallException */ - public function setAction(string $action) + public function setAction(string $action): never { throw new BadMethodCallException('Buttons do not support actions.'); } @@ -365,7 +341,7 @@ public function setAction(string $action) * * @throws BadMethodCallException */ - public function setMethod(string $method) + public function setMethod(string $method): never { throw new BadMethodCallException('Buttons do not support methods.'); } @@ -375,7 +351,7 @@ public function setMethod(string $method) * * @throws BadMethodCallException */ - public function setRequestHandler(RequestHandlerInterface $requestHandler) + public function setRequestHandler(RequestHandlerInterface $requestHandler): never { throw new BadMethodCallException('Buttons do not support request handlers.'); } @@ -387,7 +363,7 @@ public function setRequestHandler(RequestHandlerInterface $requestHandler) * * @throws BadMethodCallException */ - public function setAutoInitialize(bool $initialize) + public function setAutoInitialize(bool $initialize): static { if (true === $initialize) { throw new BadMethodCallException('Buttons do not support automatic initialization.'); @@ -401,17 +377,15 @@ public function setAutoInitialize(bool $initialize) * * @throws BadMethodCallException */ - public function setInheritData(bool $inheritData) + public function setInheritData(bool $inheritData): never { throw new BadMethodCallException('Buttons do not support data inheritance.'); } /** * Builds and returns the button configuration. - * - * @return FormConfigInterface */ - public function getFormConfig() + public function getFormConfig(): FormConfigInterface { // This method should be idempotent, so clone the builder $config = clone $this; @@ -425,7 +399,7 @@ public function getFormConfig() * * @throws BadMethodCallException */ - public function setIsEmptyCallback(?callable $isEmptyCallback) + public function setIsEmptyCallback(?callable $isEmptyCallback): never { throw new BadMethodCallException('Buttons do not support "is empty" callback.'); } @@ -435,15 +409,12 @@ public function setIsEmptyCallback(?callable $isEmptyCallback) * * @throws BadMethodCallException */ - public function getEventDispatcher() + public function getEventDispatcher(): never { throw new BadMethodCallException('Buttons do not support event dispatching.'); } - /** - * {@inheritdoc} - */ - public function getName() + public function getName(): string { return $this->name; } @@ -451,67 +422,55 @@ public function getName() /** * Unsupported method. */ - public function getPropertyPath() + public function getPropertyPath(): ?PropertyPathInterface { return null; } /** * Unsupported method. - * - * @return bool */ - public function getMapped() + public function getMapped(): bool { return false; } /** * Unsupported method. - * - * @return bool */ - public function getByReference() + public function getByReference(): bool { return false; } /** * Unsupported method. - * - * @return bool */ - public function getCompound() + public function getCompound(): bool { return false; } /** * Returns the form type used to construct the button. - * - * @return ResolvedFormTypeInterface */ - public function getType() + public function getType(): ResolvedFormTypeInterface { return $this->type; } /** * Unsupported method. - * - * @return array */ - public function getViewTransformers() + public function getViewTransformers(): array { return []; } /** * Unsupported method. - * - * @return array */ - public function getModelTransformers() + public function getModelTransformers(): array { return []; } @@ -519,37 +478,31 @@ public function getModelTransformers() /** * Unsupported method. */ - public function getDataMapper() + public function getDataMapper(): ?DataMapperInterface { return null; } /** * Unsupported method. - * - * @return bool */ - public function getRequired() + public function getRequired(): bool { return false; } /** * Returns whether the button is disabled. - * - * @return bool */ - public function getDisabled() + public function getDisabled(): bool { return $this->disabled; } /** * Unsupported method. - * - * @return bool */ - public function getErrorBubbling() + public function getErrorBubbling(): bool { return false; } @@ -557,39 +510,31 @@ public function getErrorBubbling() /** * Unsupported method. */ - public function getEmptyData() + public function getEmptyData(): mixed { return null; } /** * Returns additional attributes of the button. - * - * @return array */ - public function getAttributes() + public function getAttributes(): array { return $this->attributes; } /** * Returns whether the attribute with the given name exists. - * - * @return bool */ - public function hasAttribute(string $name) + public function hasAttribute(string $name): bool { return \array_key_exists($name, $this->attributes); } /** * Returns the value of the given attribute. - * - * @param mixed $default The value returned if the attribute does not exist - * - * @return mixed */ - public function getAttribute(string $name, $default = null) + public function getAttribute(string $name, mixed $default = null): mixed { return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default; } @@ -597,7 +542,7 @@ public function getAttribute(string $name, $default = null) /** * Unsupported method. */ - public function getData() + public function getData(): mixed { return null; } @@ -605,17 +550,15 @@ public function getData() /** * Unsupported method. */ - public function getDataClass() + public function getDataClass(): ?string { return null; } /** * Unsupported method. - * - * @return bool */ - public function getDataLocked() + public function getDataLocked(): bool { return false; } @@ -623,7 +566,7 @@ public function getDataLocked() /** * Unsupported method. */ - public function getFormFactory() + public function getFormFactory(): never { throw new BadMethodCallException('Buttons do not support adding children.'); } @@ -633,7 +576,7 @@ public function getFormFactory() * * @throws BadMethodCallException */ - public function getAction() + public function getAction(): never { throw new BadMethodCallException('Buttons do not support actions.'); } @@ -643,7 +586,7 @@ public function getAction() * * @throws BadMethodCallException */ - public function getMethod() + public function getMethod(): never { throw new BadMethodCallException('Buttons do not support methods.'); } @@ -653,59 +596,47 @@ public function getMethod() * * @throws BadMethodCallException */ - public function getRequestHandler() + public function getRequestHandler(): never { throw new BadMethodCallException('Buttons do not support request handlers.'); } /** * Unsupported method. - * - * @return bool */ - public function getAutoInitialize() + public function getAutoInitialize(): bool { return false; } /** * Unsupported method. - * - * @return bool */ - public function getInheritData() + public function getInheritData(): bool { return false; } /** * Returns all options passed during the construction of the button. - * - * @return array */ - public function getOptions() + public function getOptions(): array { return $this->options; } /** * Returns whether a specific option exists. - * - * @return bool */ - public function hasOption(string $name) + public function hasOption(string $name): bool { return \array_key_exists($name, $this->options); } /** * Returns the value of a specific option. - * - * @param mixed $default The value returned if the option does not exist - * - * @return mixed */ - public function getOption(string $name, $default = null) + public function getOption(string $name, mixed $default = null): mixed { return \array_key_exists($name, $this->options) ? $this->options[$name] : $default; } @@ -715,29 +646,23 @@ public function getOption(string $name, $default = null) * * @throws BadMethodCallException */ - public function getIsEmptyCallback(): ?callable + public function getIsEmptyCallback(): never { throw new BadMethodCallException('Buttons do not support "is empty" callback.'); } /** * Unsupported method. - * - * @return int */ - #[\ReturnTypeWillChange] - public function count() + public function count(): int { return 0; } /** * Unsupported method. - * - * @return \EmptyIterator */ - #[\ReturnTypeWillChange] - public function getIterator() + public function getIterator(): \EmptyIterator { return new \EmptyIterator(); } diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d7e764455..3b1fabfd17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,80 @@ 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 +--- + + * Throw when using `DateTime` or `DateTimeImmutable` model data with a different timezone than configured with the + `model_timezone` option in `DateType`, `DateTimeType`, and `TimeType` + * Make the "widget" option of date/time form types default to "single_text" + * Require explicit argument when calling `Button/Form::setParent()`, `ButtonBuilder/FormConfigBuilder::setDataMapper()`, `TransformationFailedException::setInvalidMessage()` + +6.4 +--- + + * Deprecate using `DateTime` or `DateTimeImmutable` model data with a different timezone than configured with the + `model_timezone` option in `DateType`, `DateTimeType`, and `TimeType` + * Deprecate `PostSetDataEvent::setData()`, use `PreSetDataEvent::setData()` instead + * Deprecate `PostSubmitEvent::setData()`, use `PreSubmitDataEvent::setData()` or `SubmitDataEvent::setData()` instead + * Add `duplicate_preferred_choices` option in `ChoiceType` + * Add `$duplicatePreferredChoices` parameter to `ChoiceListFactoryInterface::createView()` + +6.3 +--- + + * Don't render seconds for HTML5 date pickers unless "with_seconds" is explicitly set + * Add a `placeholder_attr` option to `ChoiceType` + * Deprecate not configuring the "widget" option of date/time form types, it will default to "single_text" in v7 + +6.2 +--- + + * Allow passing `TranslatableInterface` objects to the `ChoiceView` label + * Allow passing `TranslatableInterface` objects to the `help` option + * Deprecate calling `Button/Form::setParent()`, `ButtonBuilder/FormConfigBuilder::setDataMapper()`, `TransformationFailedException::setInvalidMessage()` without arguments + * Change the signature of `FormConfigBuilderInterface::setDataMapper()` to `setDataMapper(?DataMapperInterface)` + * Change the signature of `FormInterface::setParent()` to `setParent(?self)` + * Add `PasswordHasherExtension` with support for `hash_property_path` option in `PasswordType` + +6.1 +--- + + * Add a `prototype_options` option to `CollectionType` + +6.0 +--- + + * Remove `PropertyPathMaper` + * Remove `Symfony\Component\Form\Extension\Validator\Util\ServerParams` + * Remove `FormPass` configuration + * Remove the `NumberToLocalizedStringTransformer::ROUND_*` constants, use `\NumberFormatter::ROUND_*` instead + * The `rounding_mode` option of the `PercentType` defaults to `\NumberFormatter::ROUND_HALFUP` + * The rounding mode argument of the constructor of `PercentToLocalizedStringTransformer` defaults to `\NumberFormatter::ROUND_HALFUP` + * Add `FormConfigInterface::getIsEmptyCallback()` and `FormConfigBuilderInterface::setIsEmptyCallback()` + * Change `$forms` parameter type of the `DataMapper::mapDataToForms()` method from `iterable` to `\Traversable` + * Change `$forms` parameter type of the `DataMapper::mapFormsToData()` method from `iterable` to `\Traversable` + * Change `$checkboxes` parameter type of the `CheckboxListMapper::mapDataToForms()` method from `iterable` to `\Traversable` + * Change `$checkboxes` parameter type of the `CheckboxListMapper::mapFormsToData()` method from `iterable` to `\Traversable` + * Change `$radios` parameter type of the `RadioListMapper::mapDataToForms()` method from `iterable` to `\Traversable` + * Change `$radios` parameter type of the `RadioListMapper::mapFormsToData()` method from `iterable` to `\Traversable` + 5.4 --- diff --git a/CallbackTransformer.php b/CallbackTransformer.php index 5125214ff9..2a79b5b365 100644 --- a/CallbackTransformer.php +++ b/CallbackTransformer.php @@ -13,31 +13,21 @@ class CallbackTransformer implements DataTransformerInterface { - private $transform; - private $reverseTransform; + private \Closure $transform; + private \Closure $reverseTransform; - /** - * @param callable $transform The forward transform callback - * @param callable $reverseTransform The reverse transform callback - */ public function __construct(callable $transform, callable $reverseTransform) { - $this->transform = $transform; - $this->reverseTransform = $reverseTransform; + $this->transform = $transform(...); + $this->reverseTransform = $reverseTransform(...); } - /** - * {@inheritdoc} - */ - public function transform($data) + public function transform(mixed $data): mixed { return ($this->transform)($data); } - /** - * {@inheritdoc} - */ - public function reverseTransform($data) + public function reverseTransform(mixed $data): mixed { return ($this->reverseTransform)($data); } diff --git a/ChoiceList/ArrayChoiceList.php b/ChoiceList/ArrayChoiceList.php index c3b38e6e62..9ac4fd6e52 100644 --- a/ChoiceList/ArrayChoiceList.php +++ b/ChoiceList/ArrayChoiceList.php @@ -24,27 +24,18 @@ */ class ArrayChoiceList implements ChoiceListInterface { - /** - * The choices in the list. - * - * @var array - */ - protected $choices; + protected array $choices; /** * The values indexed by the original keys. - * - * @var array */ - protected $structuredValues; + protected array $structuredValues; /** * The original keys of the choices array. - * - * @var int[]|string[] */ - protected $originalKeys; - protected $valueCallback; + protected array $originalKeys; + protected ?\Closure $valueCallback = null; /** * Creates a list with the given choices and values. @@ -64,18 +55,17 @@ public function __construct(iterable $choices, ?callable $value = null) } if (null === $value && $this->castableToString($choices)) { - $value = function ($choice) { - return false === $choice ? '0' : (string) $choice; - }; + $value = static fn ($choice) => false === $choice ? '0' : (string) $choice; } if (null !== $value) { // If a deterministic value generator was passed, use it later - $this->valueCallback = $value; + $this->valueCallback = $value(...); } else { // Otherwise generate incrementing integers as values - $i = 0; - $value = function () use (&$i) { + $value = static function () { + static $i = 0; + return $i++; }; } @@ -90,42 +80,27 @@ public function __construct(iterable $choices, ?callable $value = null) $this->structuredValues = $structuredValues; } - /** - * {@inheritdoc} - */ - public function getChoices() + public function getChoices(): array { return $this->choices; } - /** - * {@inheritdoc} - */ - public function getValues() + public function getValues(): array { return array_map('strval', array_keys($this->choices)); } - /** - * {@inheritdoc} - */ - public function getStructuredValues() + public function getStructuredValues(): array { return $this->structuredValues; } - /** - * {@inheritdoc} - */ - public function getOriginalKeys() + public function getOriginalKeys(): array { return $this->originalKeys; } - /** - * {@inheritdoc} - */ - public function getChoicesForValues(array $values) + public function getChoicesForValues(array $values): array { $choices = []; @@ -138,10 +113,7 @@ public function getChoicesForValues(array $values) return $choices; } - /** - * {@inheritdoc} - */ - public function getValuesForChoices(array $choices) + public function getValuesForChoices(array $choices): array { $values = []; @@ -182,7 +154,7 @@ public function getValuesForChoices(array $choices) * * @internal */ - protected function flatten(array $choices, callable $value, ?array &$choicesByValues, ?array &$keysByValues, ?array &$structuredValues) + protected function flatten(array $choices, callable $value, ?array &$choicesByValues, ?array &$keysByValues, ?array &$structuredValues): void { if (null === $choicesByValues) { $choicesByValues = []; diff --git a/ChoiceList/ChoiceList.php b/ChoiceList/ChoiceList.php index df63c83d89..31166c1bdf 100644 --- a/ChoiceList/ChoiceList.php +++ b/ChoiceList/ChoiceList.php @@ -35,11 +35,10 @@ final class ChoiceList /** * Creates a cacheable loader from any callable providing iterable choices. * - * @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list - * @param callable $choices A callable that must return iterable choices or grouped choices - * @param mixed|null $vary Dynamic data used to compute a unique hash when caching the loader + * @param callable $choices A callable that must return iterable choices or grouped choices + * @param mixed $vary Dynamic data used to compute a unique hash when caching the loader */ - public static function lazy($formType, callable $choices, $vary = null): ChoiceLoader + public static function lazy(FormTypeInterface|FormTypeExtensionInterface $formType, callable $choices, mixed $vary = null): ChoiceLoader { return self::loader($formType, new CallbackChoiceLoader($choices), $vary); } @@ -47,11 +46,10 @@ public static function lazy($formType, callable $choices, $vary = null): ChoiceL /** * Decorates a loader to make it cacheable. * - * @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list - * @param ChoiceLoaderInterface $loader A loader responsible for creating loading choices or grouped choices - * @param mixed|null $vary Dynamic data used to compute a unique hash when caching the loader + * @param ChoiceLoaderInterface $loader A loader responsible for creating loading choices or grouped choices + * @param mixed $vary Dynamic data used to compute a unique hash when caching the loader */ - public static function loader($formType, ChoiceLoaderInterface $loader, $vary = null): ChoiceLoader + public static function loader(FormTypeInterface|FormTypeExtensionInterface $formType, ChoiceLoaderInterface $loader, mixed $vary = null): ChoiceLoader { return new ChoiceLoader($formType, $loader, $vary); } @@ -59,21 +57,19 @@ public static function loader($formType, ChoiceLoaderInterface $loader, $vary = /** * Decorates a "choice_value" callback to make it cacheable. * - * @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list - * @param callable $value Any pseudo callable to create a unique string value from a choice - * @param mixed|null $vary Dynamic data used to compute a unique hash when caching the callback + * @param callable|array $value Any pseudo callable to create a unique string value from a choice + * @param mixed $vary Dynamic data used to compute a unique hash when caching the callback */ - public static function value($formType, $value, $vary = null): ChoiceValue + public static function value(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $value, mixed $vary = null): ChoiceValue { return new ChoiceValue($formType, $value, $vary); } /** - * @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list - * @param callable $filter Any pseudo callable to filter a choice list - * @param mixed|null $vary Dynamic data used to compute a unique hash when caching the callback + * @param callable|array $filter Any pseudo callable to filter a choice list + * @param mixed $vary Dynamic data used to compute a unique hash when caching the callback */ - public static function filter($formType, $filter, $vary = null): ChoiceFilter + public static function filter(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $filter, mixed $vary = null): ChoiceFilter { return new ChoiceFilter($formType, $filter, $vary); } @@ -81,11 +77,10 @@ public static function filter($formType, $filter, $vary = null): ChoiceFilter /** * Decorates a "choice_label" option to make it cacheable. * - * @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list - * @param callable|false $label Any pseudo callable to create a label from a choice or false to discard it - * @param mixed|null $vary Dynamic data used to compute a unique hash when caching the option + * @param callable|false $label Any pseudo callable to create a label from a choice or false to discard it + * @param mixed $vary Dynamic data used to compute a unique hash when caching the option */ - public static function label($formType, $label, $vary = null): ChoiceLabel + public static function label(FormTypeInterface|FormTypeExtensionInterface $formType, callable|false $label, mixed $vary = null): ChoiceLabel { return new ChoiceLabel($formType, $label, $vary); } @@ -93,11 +88,10 @@ public static function label($formType, $label, $vary = null): ChoiceLabel /** * Decorates a "choice_name" callback to make it cacheable. * - * @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list - * @param callable $fieldName Any pseudo callable to create a field name from a choice - * @param mixed|null $vary Dynamic data used to compute a unique hash when caching the callback + * @param callable|array $fieldName Any pseudo callable to create a field name from a choice + * @param mixed $vary Dynamic data used to compute a unique hash when caching the callback */ - public static function fieldName($formType, $fieldName, $vary = null): ChoiceFieldName + public static function fieldName(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $fieldName, mixed $vary = null): ChoiceFieldName { return new ChoiceFieldName($formType, $fieldName, $vary); } @@ -105,11 +99,10 @@ public static function fieldName($formType, $fieldName, $vary = null): ChoiceFie /** * Decorates a "choice_attr" option to make it cacheable. * - * @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list - * @param callable|array $attr Any pseudo callable or array to create html attributes from a choice - * @param mixed|null $vary Dynamic data used to compute a unique hash when caching the option + * @param callable|array $attr Any pseudo callable or array to create html attributes from a choice + * @param mixed $vary Dynamic data used to compute a unique hash when caching the option */ - public static function attr($formType, $attr, $vary = null): ChoiceAttr + public static function attr(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $attr, mixed $vary = null): ChoiceAttr { return new ChoiceAttr($formType, $attr, $vary); } @@ -117,11 +110,10 @@ public static function attr($formType, $attr, $vary = null): ChoiceAttr /** * Decorates a "choice_translation_parameters" option to make it cacheable. * - * @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list - * @param callable|array $translationParameters Any pseudo callable or array to create translation parameters from a choice - * @param mixed|null $vary Dynamic data used to compute a unique hash when caching the option + * @param callable|array $translationParameters Any pseudo callable or array to create translation parameters from a choice + * @param mixed $vary Dynamic data used to compute a unique hash when caching the option */ - public static function translationParameters($formType, $translationParameters, $vary = null): ChoiceTranslationParameters + public static function translationParameters(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $translationParameters, mixed $vary = null): ChoiceTranslationParameters { return new ChoiceTranslationParameters($formType, $translationParameters, $vary); } @@ -129,11 +121,10 @@ public static function translationParameters($formType, $translationParameters, /** * Decorates a "group_by" callback to make it cacheable. * - * @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list - * @param callable $groupBy Any pseudo callable to return a group name from a choice - * @param mixed|null $vary Dynamic data used to compute a unique hash when caching the callback + * @param callable|array $groupBy Any pseudo callable to return a group name from a choice + * @param mixed $vary Dynamic data used to compute a unique hash when caching the callback */ - public static function groupBy($formType, $groupBy, $vary = null): GroupBy + public static function groupBy(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $groupBy, mixed $vary = null): GroupBy { return new GroupBy($formType, $groupBy, $vary); } @@ -141,11 +132,10 @@ public static function groupBy($formType, $groupBy, $vary = null): GroupBy /** * Decorates a "preferred_choices" option to make it cacheable. * - * @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list - * @param callable|array $preferred Any pseudo callable or array to return a group name from a choice - * @param mixed|null $vary Dynamic data used to compute a unique hash when caching the option + * @param callable|array $preferred Any pseudo callable or array to return a group name from a choice + * @param mixed $vary Dynamic data used to compute a unique hash when caching the option */ - public static function preferred($formType, $preferred, $vary = null): PreferredChoice + public static function preferred(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $preferred, mixed $vary = null): PreferredChoice { return new PreferredChoice($formType, $preferred, $vary); } diff --git a/ChoiceList/ChoiceListInterface.php b/ChoiceList/ChoiceListInterface.php index 8bf6f95d73..e711a97edc 100644 --- a/ChoiceList/ChoiceListInterface.php +++ b/ChoiceList/ChoiceListInterface.php @@ -30,7 +30,7 @@ interface ChoiceListInterface * * @return array The selectable choices indexed by the corresponding values */ - public function getChoices(); + public function getChoices(): array; /** * Returns the values for the choices. @@ -57,7 +57,7 @@ public function getChoices(); * * @return string[] */ - public function getValues(); + public function getValues(): array; /** * Returns the values in the structure originally passed to the list. @@ -88,7 +88,7 @@ public function getValues(); * * @return string[] */ - public function getStructuredValues(); + public function getStructuredValues(): array; /** * Returns the original keys of the choices. @@ -110,7 +110,7 @@ public function getStructuredValues(); * @return int[]|string[] The original choice keys indexed by the * corresponding choice values */ - public function getOriginalKeys(); + public function getOriginalKeys(): array; /** * Returns the choices corresponding to the given values. @@ -120,10 +120,8 @@ public function getOriginalKeys(); * * @param string[] $values An array of choice values. Non-existing values in * this array are ignored - * - * @return array */ - public function getChoicesForValues(array $values); + public function getChoicesForValues(array $values): array; /** * Returns the values corresponding to the given choices. @@ -136,5 +134,5 @@ public function getChoicesForValues(array $values); * * @return string[] */ - public function getValuesForChoices(array $choices); + public function getValuesForChoices(array $choices): array; } diff --git a/ChoiceList/Factory/Cache/AbstractStaticOption.php b/ChoiceList/Factory/Cache/AbstractStaticOption.php index 42b31e0275..2686017c91 100644 --- a/ChoiceList/Factory/Cache/AbstractStaticOption.php +++ b/ChoiceList/Factory/Cache/AbstractStaticOption.php @@ -28,31 +28,22 @@ */ abstract class AbstractStaticOption { - private static $options = []; + private static array $options = []; - /** @var bool|callable|string|array|\Closure|ChoiceLoaderInterface */ - private $option; + private bool|string|array|\Closure|ChoiceLoaderInterface $option; /** - * @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list - * @param mixed $option Any pseudo callable, array, string or bool to define a choice list option - * @param mixed|null $vary Dynamic data used to compute a unique hash when caching the option + * @param mixed $option Any pseudo callable, array, string or bool to define a choice list option + * @param mixed $vary Dynamic data used to compute a unique hash when caching the option */ - final public function __construct($formType, $option, $vary = null) + final public function __construct(FormTypeInterface|FormTypeExtensionInterface $formType, mixed $option, mixed $vary = null) { - if (!$formType instanceof FormTypeInterface && !$formType instanceof FormTypeExtensionInterface) { - throw new \TypeError(sprintf('Expected an instance of "%s" or "%s", but got "%s".', FormTypeInterface::class, FormTypeExtensionInterface::class, get_debug_type($formType))); - } - $hash = CachingFactoryDecorator::generateHash([static::class, $formType, $vary]); - $this->option = self::$options[$hash] ?? self::$options[$hash] = $option; + $this->option = self::$options[$hash] ??= $option instanceof \Closure || \is_string($option) || \is_bool($option) || $option instanceof ChoiceLoaderInterface || !\is_callable($option) ? $option : $option(...); } - /** - * @return mixed - */ - final public function getOption() + final public function getOption(): mixed { return $this->option; } diff --git a/ChoiceList/Factory/Cache/ChoiceLoader.php b/ChoiceList/Factory/Cache/ChoiceLoader.php index eb137f5021..1d64f101c0 100644 --- a/ChoiceList/Factory/Cache/ChoiceLoader.php +++ b/ChoiceList/Factory/Cache/ChoiceLoader.php @@ -26,25 +26,16 @@ */ final class ChoiceLoader extends AbstractStaticOption implements ChoiceLoaderInterface { - /** - * {@inheritdoc} - */ public function loadChoiceList(?callable $value = null): ChoiceListInterface { return $this->getOption()->loadChoiceList($value); } - /** - * {@inheritdoc} - */ public function loadChoicesForValues(array $values, ?callable $value = null): array { return $this->getOption()->loadChoicesForValues($values, $value); } - /** - * {@inheritdoc} - */ public function loadValuesForChoices(array $choices, ?callable $value = null): array { return $this->getOption()->loadValuesForChoices($choices, $value); diff --git a/ChoiceList/Factory/CachingFactoryDecorator.php b/ChoiceList/Factory/CachingFactoryDecorator.php index 912cab4e27..1f373228b5 100644 --- a/ChoiceList/Factory/CachingFactoryDecorator.php +++ b/ChoiceList/Factory/CachingFactoryDecorator.php @@ -27,17 +27,15 @@ */ class CachingFactoryDecorator implements ChoiceListFactoryInterface, ResetInterface { - private $decoratedFactory; - /** * @var ChoiceListInterface[] */ - private $lists = []; + private array $lists = []; /** * @var ChoiceListView[] */ - private $views = []; + private array $views = []; /** * Generates a SHA-256 hash for the given value. @@ -45,18 +43,16 @@ class CachingFactoryDecorator implements ChoiceListFactoryInterface, ResetInterf * Optionally, a namespace string can be passed. Calling this method will * the same values, but different namespaces, will return different hashes. * - * @param mixed $value The value to hash - * * @return string The SHA-256 hash * * @internal */ - public static function generateHash($value, string $namespace = ''): string + public static function generateHash(mixed $value, string $namespace = ''): string { if (\is_object($value)) { $value = spl_object_hash($value); } elseif (\is_array($value)) { - array_walk_recursive($value, function (&$v) { + array_walk_recursive($value, static function (&$v) { if (\is_object($v)) { $v = spl_object_hash($v); } @@ -66,31 +62,21 @@ public static function generateHash($value, string $namespace = ''): string return hash('sha256', $namespace.':'.serialize($value)); } - public function __construct(ChoiceListFactoryInterface $decoratedFactory) - { - $this->decoratedFactory = $decoratedFactory; + public function __construct( + private ChoiceListFactoryInterface $decoratedFactory, + ) { } /** * Returns the decorated factory. - * - * @return ChoiceListFactoryInterface */ - public function getDecoratedFactory() + public function getDecoratedFactory(): ChoiceListFactoryInterface { return $this->decoratedFactory; } - /** - * {@inheritdoc} - * - * @param mixed $value - * @param mixed $filter - */ - public function createListFromChoices(iterable $choices, $value = null/* , $filter = null */) + public function createListFromChoices(iterable $choices, mixed $value = null, mixed $filter = null): ChoiceListInterface { - $filter = \func_num_args() > 2 ? func_get_arg(2) : null; - if ($choices instanceof \Traversable) { $choices = iterator_to_array($choices); } @@ -122,16 +108,8 @@ public function createListFromChoices(iterable $choices, $value = null/* , $filt return $this->lists[$hash]; } - /** - * {@inheritdoc} - * - * @param mixed $value - * @param mixed $filter - */ - public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null/* , $filter = null */) + public function createListFromLoader(ChoiceLoaderInterface $loader, mixed $value = null, mixed $filter = null): ChoiceListInterface { - $filter = \func_num_args() > 2 ? func_get_arg(2) : null; - $cache = true; if ($loader instanceof Cache\ChoiceLoader) { @@ -165,19 +143,8 @@ public function createListFromLoader(ChoiceLoaderInterface $loader, $value = nul return $this->lists[$hash]; } - /** - * {@inheritdoc} - * - * @param mixed $preferredChoices - * @param mixed $label - * @param mixed $index - * @param mixed $groupBy - * @param mixed $attr - * @param mixed $labelTranslationParameters - */ - public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null/* , $labelTranslationParameters = [] */) + public function createView(ChoiceListInterface $list, mixed $preferredChoices = null, mixed $label = null, mixed $index = null, mixed $groupBy = null, mixed $attr = null, mixed $labelTranslationParameters = [], bool $duplicatePreferredChoices = true): ChoiceListView { - $labelTranslationParameters = \func_num_args() > 6 ? func_get_arg(6) : []; $cache = true; if ($preferredChoices instanceof Cache\PreferredChoice) { @@ -224,11 +191,12 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null, $index, $groupBy, $attr, - $labelTranslationParameters + $labelTranslationParameters, + $duplicatePreferredChoices, ); } - $hash = self::generateHash([$list, $preferredChoices, $label, $index, $groupBy, $attr, $labelTranslationParameters]); + $hash = self::generateHash([$list, $preferredChoices, $label, $index, $groupBy, $attr, $labelTranslationParameters, $duplicatePreferredChoices]); if (!isset($this->views[$hash])) { $this->views[$hash] = $this->decoratedFactory->createView( @@ -238,14 +206,15 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null, $index, $groupBy, $attr, - $labelTranslationParameters + $labelTranslationParameters, + $duplicatePreferredChoices, ); } return $this->views[$hash]; } - public function reset() + public function reset(): void { $this->lists = []; $this->views = []; diff --git a/ChoiceList/Factory/ChoiceListFactoryInterface.php b/ChoiceList/Factory/ChoiceListFactoryInterface.php index c6c654b6f8..a9ca6db20e 100644 --- a/ChoiceList/Factory/ChoiceListFactoryInterface.php +++ b/ChoiceList/Factory/ChoiceListFactoryInterface.php @@ -32,10 +32,8 @@ interface ChoiceListFactoryInterface * Null may be passed when the choice list contains the empty value. * * @param callable|null $filter The callable filtering the choices - * - * @return ChoiceListInterface */ - public function createListFromChoices(iterable $choices, ?callable $value = null/* , callable $filter = null */); + public function createListFromChoices(iterable $choices, ?callable $value = null, ?callable $filter = null): ChoiceListInterface; /** * Creates a choice list that is loaded with the given loader. @@ -45,10 +43,8 @@ public function createListFromChoices(iterable $choices, ?callable $value = null * Null may be passed when the choice list contains the empty value. * * @param callable|null $filter The callable filtering the choices - * - * @return ChoiceListInterface */ - public function createListFromLoader(ChoiceLoaderInterface $loader, ?callable $value = null/* , callable $filter = null */); + public function createListFromLoader(ChoiceLoaderInterface $loader, ?callable $value = null, ?callable $filter = null): ChoiceListInterface; /** * Creates a view for the given choice list. @@ -81,8 +77,9 @@ public function createListFromLoader(ChoiceLoaderInterface $loader, ?callable $v * pass false to discard the label * @param array|callable|null $attr The callable generating the HTML attributes * @param array|callable $labelTranslationParameters The parameters used to translate the choice labels - * - * @return ChoiceListView + * @param bool $duplicatePreferredChoices Whether the preferred choices should be duplicated + * on top of the list and in their original position + * or only in the top of the list */ - public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, ?callable $index = null, ?callable $groupBy = null, $attr = null/* , $labelTranslationParameters = [] */); + public function createView(ChoiceListInterface $list, array|callable|null $preferredChoices = null, callable|false|null $label = null, ?callable $index = null, ?callable $groupBy = null, array|callable|null $attr = null, array|callable $labelTranslationParameters = [], bool $duplicatePreferredChoices = true): ChoiceListView; } diff --git a/ChoiceList/Factory/DefaultChoiceListFactory.php b/ChoiceList/Factory/DefaultChoiceListFactory.php index 9ee423bdf1..586ac822a8 100644 --- a/ChoiceList/Factory/DefaultChoiceListFactory.php +++ b/ChoiceList/Factory/DefaultChoiceListFactory.php @@ -20,7 +20,7 @@ use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; use Symfony\Component\Form\ChoiceList\View\ChoiceListView; use Symfony\Component\Form\ChoiceList\View\ChoiceView; -use Symfony\Component\Translation\TranslatableMessage; +use Symfony\Contracts\Translation\TranslatableInterface; /** * Default implementation of {@link ChoiceListFactoryInterface}. @@ -30,36 +30,21 @@ */ class DefaultChoiceListFactory implements ChoiceListFactoryInterface { - /** - * {@inheritdoc} - * - * @param callable|null $filter - */ - public function createListFromChoices(iterable $choices, ?callable $value = null/* , callable $filter = null */) + public function createListFromChoices(iterable $choices, ?callable $value = null, ?callable $filter = null): ChoiceListInterface { - $filter = \func_num_args() > 2 ? func_get_arg(2) : null; - if ($filter) { // filter the choice list lazily return $this->createListFromLoader(new FilterChoiceLoaderDecorator( - new CallbackChoiceLoader(static function () use ($choices) { - return $choices; - } - ), $filter), $value); + new CallbackChoiceLoader(static fn () => $choices), + $filter + ), $value); } return new ArrayChoiceList($choices, $value); } - /** - * {@inheritdoc} - * - * @param callable|null $filter - */ - public function createListFromLoader(ChoiceLoaderInterface $loader, ?callable $value = null/* , callable $filter = null */) + public function createListFromLoader(ChoiceLoaderInterface $loader, ?callable $value = null, ?callable $filter = null): ChoiceListInterface { - $filter = \func_num_args() > 2 ? func_get_arg(2) : null; - if ($filter) { $loader = new FilterChoiceLoaderDecorator($loader, $filter); } @@ -67,14 +52,8 @@ public function createListFromLoader(ChoiceLoaderInterface $loader, ?callable $v return new LazyChoiceList($loader, $value); } - /** - * {@inheritdoc} - * - * @param array|callable $labelTranslationParameters The parameters used to translate the choice labels - */ - public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, ?callable $index = null, ?callable $groupBy = null, $attr = null/* , $labelTranslationParameters = [] */) + public function createView(ChoiceListInterface $list, array|callable|null $preferredChoices = null, callable|false|null $label = null, ?callable $index = null, ?callable $groupBy = null, array|callable|null $attr = null, array|callable $labelTranslationParameters = [], bool $duplicatePreferredChoices = true): ChoiceListView { - $labelTranslationParameters = \func_num_args() > 6 ? func_get_arg(6) : []; $preferredViews = []; $preferredViewsOrder = []; $otherViews = []; @@ -82,21 +61,17 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null, $keys = $list->getOriginalKeys(); if (!\is_callable($preferredChoices)) { - if (empty($preferredChoices)) { + if (!$preferredChoices) { $preferredChoices = null; } else { // make sure we have keys that reflect order $preferredChoices = array_values($preferredChoices); - $preferredChoices = static function ($choice) use ($preferredChoices) { - return array_search($choice, $preferredChoices, true); - }; + $preferredChoices = static fn ($choice) => array_search($choice, $preferredChoices, true); } } // The names are generated from an incrementing integer by default - if (null === $index) { - $index = 0; - } + $index ??= 0; // If $groupBy is a callable returning a string // choices are added to the group with the name returned by the callable. @@ -117,7 +92,8 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null, $preferredChoices, $preferredViews, $preferredViewsOrder, - $otherViews + $otherViews, + $duplicatePreferredChoices, ); } @@ -155,20 +131,17 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null, $preferredChoices, $preferredViews, $preferredViewsOrder, - $otherViews + $otherViews, + $duplicatePreferredChoices, ); } - uksort($preferredViews, static function ($a, $b) use ($preferredViewsOrder): int { - return isset($preferredViewsOrder[$a], $preferredViewsOrder[$b]) - ? $preferredViewsOrder[$a] <=> $preferredViewsOrder[$b] - : 0; - }); + uksort($preferredViews, static fn ($a, $b) => isset($preferredViewsOrder[$a], $preferredViewsOrder[$b]) ? $preferredViewsOrder[$a] <=> $preferredViewsOrder[$b] : 0); return new ChoiceListView($otherViews, $preferredViews); } - private static function addChoiceView($choice, string $value, $label, array $keys, &$index, $attr, $labelTranslationParameters, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews) + private static function addChoiceView($choice, string $value, $label, array $keys, &$index, $attr, $labelTranslationParameters, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews, bool $duplicatePreferredChoices): void { // $value may be an integer or a string, since it's stored in the array // keys. We want to guarantee it's a string though. @@ -186,7 +159,7 @@ private static function addChoiceView($choice, string $value, $label, array $key if (false === $dynamicLabel) { $label = false; - } elseif ($dynamicLabel instanceof TranslatableMessage) { + } elseif ($dynamicLabel instanceof TranslatableInterface) { $label = $dynamicLabel; } else { $label = (string) $dynamicLabel; @@ -209,12 +182,16 @@ private static function addChoiceView($choice, string $value, $label, array $key if (null !== $isPreferred && false !== $preferredKey = $isPreferred($choice, $key, $value)) { $preferredViews[$nextIndex] = $view; $preferredViewsOrder[$nextIndex] = $preferredKey; - } - $otherViews[$nextIndex] = $view; + if ($duplicatePreferredChoices) { + $otherViews[$nextIndex] = $view; + } + } else { + $otherViews[$nextIndex] = $view; + } } - private static function addChoiceViewsFromStructuredValues(array $values, $label, array $choices, array $keys, &$index, $attr, $labelTranslationParameters, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews) + private static function addChoiceViewsFromStructuredValues(array $values, $label, array $choices, array $keys, &$index, $attr, $labelTranslationParameters, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews, bool $duplicatePreferredChoices): void { foreach ($values as $key => $value) { if (null === $value) { @@ -237,7 +214,8 @@ private static function addChoiceViewsFromStructuredValues(array $values, $label $isPreferred, $preferredViewsForGroup, $preferredViewsOrder, - $otherViewsForGroup + $otherViewsForGroup, + $duplicatePreferredChoices, ); if (\count($preferredViewsForGroup) > 0) { @@ -263,12 +241,13 @@ private static function addChoiceViewsFromStructuredValues(array $values, $label $isPreferred, $preferredViews, $preferredViewsOrder, - $otherViews + $otherViews, + $duplicatePreferredChoices, ); } } - private static function addChoiceViewsGroupedByCallable(callable $groupBy, $choice, string $value, $label, array $keys, &$index, $attr, $labelTranslationParameters, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews) + private static function addChoiceViewsGroupedByCallable(callable $groupBy, $choice, string $value, $label, array $keys, &$index, $attr, $labelTranslationParameters, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews, bool $duplicatePreferredChoices): void { $groupLabels = $groupBy($choice, $keys[$value], $value); @@ -285,7 +264,8 @@ private static function addChoiceViewsGroupedByCallable(callable $groupBy, $choi $isPreferred, $preferredViews, $preferredViewsOrder, - $otherViews + $otherViews, + $duplicatePreferredChoices, ); return; @@ -315,7 +295,8 @@ private static function addChoiceViewsGroupedByCallable(callable $groupBy, $choi $isPreferred, $preferredViews[$groupLabel]->choices, $preferredViewsOrder[$groupLabel], - $otherViews[$groupLabel]->choices + $otherViews[$groupLabel]->choices, + $duplicatePreferredChoices, ); } } diff --git a/ChoiceList/Factory/PropertyAccessDecorator.php b/ChoiceList/Factory/PropertyAccessDecorator.php index abd514783f..f73a8fc2aa 100644 --- a/ChoiceList/Factory/PropertyAccessDecorator.php +++ b/ChoiceList/Factory/PropertyAccessDecorator.php @@ -38,50 +38,36 @@ */ class PropertyAccessDecorator implements ChoiceListFactoryInterface { - private $decoratedFactory; - private $propertyAccessor; + 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(); } /** * Returns the decorated factory. - * - * @return ChoiceListFactoryInterface */ - public function getDecoratedFactory() + public function getDecoratedFactory(): ChoiceListFactoryInterface { return $this->decoratedFactory; } - /** - * {@inheritdoc} - * - * @param mixed $value - * @param mixed $filter - * - * @return ChoiceListInterface - */ - public function createListFromChoices(iterable $choices, $value = null/* , $filter = null */) + public function createListFromChoices(iterable $choices, mixed $value = null, mixed $filter = null): ChoiceListInterface { - $filter = \func_num_args() > 2 ? func_get_arg(2) : null; - if (\is_string($value)) { $value = new PropertyPath($value); } if ($value instanceof PropertyPathInterface) { $accessor = $this->propertyAccessor; - $value = function ($choice) use ($accessor, $value) { - // The callable may be invoked with a non-object/array value - // when such values are passed to - // ChoiceListInterface::getValuesForChoices(). Handle this case - // so that the call to getValue() doesn't break. - return \is_object($choice) || \is_array($choice) ? $accessor->getValue($choice, $value) : null; - }; + // The callable may be invoked with a non-object/array value + // when such values are passed to + // ChoiceListInterface::getValuesForChoices(). Handle this case + // so that the call to getValue() doesn't break. + $value = static fn ($choice) => \is_object($choice) || \is_array($choice) ? $accessor->getValue($choice, $value) : null; } if (\is_string($filter)) { @@ -90,39 +76,25 @@ public function createListFromChoices(iterable $choices, $value = null/* , $filt if ($filter instanceof PropertyPath) { $accessor = $this->propertyAccessor; - $filter = static function ($choice) use ($accessor, $filter) { - return (\is_object($choice) || \is_array($choice)) && $accessor->getValue($choice, $filter); - }; + $filter = static fn ($choice) => (\is_object($choice) || \is_array($choice)) && $accessor->getValue($choice, $filter); } return $this->decoratedFactory->createListFromChoices($choices, $value, $filter); } - /** - * {@inheritdoc} - * - * @param mixed $value - * @param mixed $filter - * - * @return ChoiceListInterface - */ - public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null/* , $filter = null */) + public function createListFromLoader(ChoiceLoaderInterface $loader, mixed $value = null, mixed $filter = null): ChoiceListInterface { - $filter = \func_num_args() > 2 ? func_get_arg(2) : null; - if (\is_string($value)) { $value = new PropertyPath($value); } if ($value instanceof PropertyPathInterface) { $accessor = $this->propertyAccessor; - $value = function ($choice) use ($accessor, $value) { - // The callable may be invoked with a non-object/array value - // when such values are passed to - // ChoiceListInterface::getValuesForChoices(). Handle this case - // so that the call to getValue() doesn't break. - return \is_object($choice) || \is_array($choice) ? $accessor->getValue($choice, $value) : null; - }; + // The callable may be invoked with a non-object/array value + // when such values are passed to + // ChoiceListInterface::getValuesForChoices(). Handle this case + // so that the call to getValue() doesn't break. + $value = static fn ($choice) => \is_object($choice) || \is_array($choice) ? $accessor->getValue($choice, $value) : null; } if (\is_string($filter)) { @@ -131,29 +103,14 @@ public function createListFromLoader(ChoiceLoaderInterface $loader, $value = nul if ($filter instanceof PropertyPath) { $accessor = $this->propertyAccessor; - $filter = static function ($choice) use ($accessor, $filter) { - return (\is_object($choice) || \is_array($choice)) && $accessor->getValue($choice, $filter); - }; + $filter = static fn ($choice) => (\is_object($choice) || \is_array($choice)) && $accessor->getValue($choice, $filter); } return $this->decoratedFactory->createListFromLoader($loader, $value, $filter); } - /** - * {@inheritdoc} - * - * @param mixed $preferredChoices - * @param mixed $label - * @param mixed $index - * @param mixed $groupBy - * @param mixed $attr - * @param mixed $labelTranslationParameters - * - * @return ChoiceListView - */ - public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null/* , $labelTranslationParameters = [] */) + public function createView(ChoiceListInterface $list, mixed $preferredChoices = null, mixed $label = null, mixed $index = null, mixed $groupBy = null, mixed $attr = null, mixed $labelTranslationParameters = [], bool $duplicatePreferredChoices = true): ChoiceListView { - $labelTranslationParameters = \func_num_args() > 6 ? func_get_arg(6) : []; $accessor = $this->propertyAccessor; if (\is_string($label)) { @@ -161,9 +118,7 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null, } if ($label instanceof PropertyPathInterface) { - $label = function ($choice) use ($accessor, $label) { - return $accessor->getValue($choice, $label); - }; + $label = static fn ($choice) => $accessor->getValue($choice, $label); } if (\is_string($preferredChoices)) { @@ -171,10 +126,10 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null, } if ($preferredChoices instanceof PropertyPathInterface) { - $preferredChoices = function ($choice) use ($accessor, $preferredChoices) { + $preferredChoices = static function ($choice) use ($accessor, $preferredChoices) { try { return $accessor->getValue($choice, $preferredChoices); - } catch (UnexpectedTypeException $e) { + } catch (UnexpectedTypeException) { // Assume not preferred if not readable return false; } @@ -186,9 +141,7 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null, } if ($index instanceof PropertyPathInterface) { - $index = function ($choice) use ($accessor, $index) { - return $accessor->getValue($choice, $index); - }; + $index = static fn ($choice) => $accessor->getValue($choice, $index); } if (\is_string($groupBy)) { @@ -196,10 +149,10 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null, } if ($groupBy instanceof PropertyPathInterface) { - $groupBy = function ($choice) use ($accessor, $groupBy) { + $groupBy = static function ($choice) use ($accessor, $groupBy) { try { return $accessor->getValue($choice, $groupBy); - } catch (UnexpectedTypeException $e) { + } catch (UnexpectedTypeException) { // Don't group if path is not readable return null; } @@ -211,9 +164,7 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null, } if ($attr instanceof PropertyPathInterface) { - $attr = function ($choice) use ($accessor, $attr) { - return $accessor->getValue($choice, $attr); - }; + $attr = static fn ($choice) => $accessor->getValue($choice, $attr); } if (\is_string($labelTranslationParameters)) { @@ -221,9 +172,7 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null, } if ($labelTranslationParameters instanceof PropertyPath) { - $labelTranslationParameters = static function ($choice) use ($accessor, $labelTranslationParameters) { - return $accessor->getValue($choice, $labelTranslationParameters); - }; + $labelTranslationParameters = static fn ($choice) => $accessor->getValue($choice, $labelTranslationParameters); } return $this->decoratedFactory->createView( @@ -233,7 +182,8 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null, $index, $groupBy, $attr, - $labelTranslationParameters + $labelTranslationParameters, + $duplicatePreferredChoices, ); } } diff --git a/ChoiceList/LazyChoiceList.php b/ChoiceList/LazyChoiceList.php index f6acd7f6e6..8e0eaaa8fc 100644 --- a/ChoiceList/LazyChoiceList.php +++ b/ChoiceList/LazyChoiceList.php @@ -27,16 +27,12 @@ */ class LazyChoiceList implements ChoiceListInterface { - private $loader; - /** * The callable creating string values for each choice. * * If null, choices are cast to strings. - * - * @var callable|null */ - private $value; + private ?\Closure $value; /** * Creates a lazily-loaded list using the given loader. @@ -45,58 +41,42 @@ 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; - $this->value = $value; + public function __construct( + private ChoiceLoaderInterface $loader, + ?callable $value = null, + ) { + $this->value = null === $value ? null : $value(...); } - /** - * {@inheritdoc} - */ - public function getChoices() + public function getChoices(): array { return $this->loader->loadChoiceList($this->value)->getChoices(); } - /** - * {@inheritdoc} - */ - public function getValues() + public function getValues(): array { return $this->loader->loadChoiceList($this->value)->getValues(); } - /** - * {@inheritdoc} - */ - public function getStructuredValues() + public function getStructuredValues(): array { return $this->loader->loadChoiceList($this->value)->getStructuredValues(); } - /** - * {@inheritdoc} - */ - public function getOriginalKeys() + public function getOriginalKeys(): array { return $this->loader->loadChoiceList($this->value)->getOriginalKeys(); } - /** - * {@inheritdoc} - */ - public function getChoicesForValues(array $values) + public function getChoicesForValues(array $values): array { return $this->loader->loadChoicesForValues($values, $this->value); } - /** - * {@inheritdoc} - */ - public function getValuesForChoices(array $choices) + public function getValuesForChoices(array $choices): array { return $this->loader->loadValuesForChoices($choices, $this->value); } diff --git a/ChoiceList/Loader/AbstractChoiceLoader.php b/ChoiceList/Loader/AbstractChoiceLoader.php index d79d693f47..749e2fbcef 100644 --- a/ChoiceList/Loader/AbstractChoiceLoader.php +++ b/ChoiceList/Loader/AbstractChoiceLoader.php @@ -19,27 +19,17 @@ */ abstract class AbstractChoiceLoader implements ChoiceLoaderInterface { - /** - * The loaded choices. - * - * @var iterable|null - */ - private $choices; + private ?iterable $choices; /** * @final - * - * {@inheritdoc} */ public function loadChoiceList(?callable $value = null): ChoiceListInterface { - return new ArrayChoiceList($this->choices ?? $this->choices = $this->loadChoices(), $value); + return new ArrayChoiceList($this->choices ??= $this->loadChoices(), $value); } - /** - * {@inheritdoc} - */ - public function loadChoicesForValues(array $values, ?callable $value = null) + public function loadChoicesForValues(array $values, ?callable $value = null): array { if (!$values) { return []; @@ -48,10 +38,7 @@ public function loadChoicesForValues(array $values, ?callable $value = null) return $this->doLoadChoicesForValues($values, $value); } - /** - * {@inheritdoc} - */ - public function loadValuesForChoices(array $choices, ?callable $value = null) + public function loadValuesForChoices(array $choices, ?callable $value = null): array { if (!$choices) { return []; @@ -59,7 +46,7 @@ public function loadValuesForChoices(array $choices, ?callable $value = null) if ($value) { // if a value callback exists, use it - return array_map(function ($item) use ($value) { return (string) $value($item); }, $choices); + return array_map(fn ($item) => (string) $value($item), $choices); } return $this->doLoadValuesForChoices($choices); diff --git a/ChoiceList/Loader/CallbackChoiceLoader.php b/ChoiceList/Loader/CallbackChoiceLoader.php index 1811d434e8..088f91dae2 100644 --- a/ChoiceList/Loader/CallbackChoiceLoader.php +++ b/ChoiceList/Loader/CallbackChoiceLoader.php @@ -18,14 +18,14 @@ */ class CallbackChoiceLoader extends AbstractChoiceLoader { - private $callback; + private \Closure $callback; /** * @param callable $callback The callable returning iterable choices */ public function __construct(callable $callback) { - $this->callback = $callback; + $this->callback = $callback(...); } protected function loadChoices(): iterable diff --git a/ChoiceList/Loader/ChoiceLoaderInterface.php b/ChoiceList/Loader/ChoiceLoaderInterface.php index 4bf467f989..d5f803c778 100644 --- a/ChoiceList/Loader/ChoiceLoaderInterface.php +++ b/ChoiceList/Loader/ChoiceLoaderInterface.php @@ -33,10 +33,8 @@ interface ChoiceLoaderInterface * * @param callable|null $value The callable which generates the values * from choices - * - * @return ChoiceListInterface */ - public function loadChoiceList(?callable $value = null); + public function loadChoiceList(?callable $value = null): ChoiceListInterface; /** * Loads the choices corresponding to the given values. @@ -51,10 +49,8 @@ public function loadChoiceList(?callable $value = null); * @param string[] $values An array of choice values. Non-existing * values in this array are ignored * @param callable|null $value The callable generating the choice values - * - * @return array */ - public function loadChoicesForValues(array $values, ?callable $value = null); + public function loadChoicesForValues(array $values, ?callable $value = null): array; /** * Loads the values corresponding to the given choices. @@ -72,5 +68,5 @@ public function loadChoicesForValues(array $values, ?callable $value = null); * * @return string[] */ - public function loadValuesForChoices(array $choices, ?callable $value = null); + public function loadValuesForChoices(array $choices, ?callable $value = null): array; } diff --git a/ChoiceList/Loader/FilterChoiceLoaderDecorator.php b/ChoiceList/Loader/FilterChoiceLoaderDecorator.php index f5ad609815..393c73eba8 100644 --- a/ChoiceList/Loader/FilterChoiceLoaderDecorator.php +++ b/ChoiceList/Loader/FilterChoiceLoaderDecorator.php @@ -18,13 +18,13 @@ */ class FilterChoiceLoaderDecorator extends AbstractChoiceLoader { - private $decoratedLoader; - private $filter; + private ChoiceLoaderInterface $decoratedLoader; + private \Closure $filter; public function __construct(ChoiceLoaderInterface $loader, callable $filter) { $this->decoratedLoader = $loader; - $this->filter = $filter; + $this->filter = $filter(...); } protected function loadChoices(): iterable @@ -52,17 +52,11 @@ protected function loadChoices(): iterable return $choices ?? []; } - /** - * {@inheritdoc} - */ public function loadChoicesForValues(array $values, ?callable $value = null): array { return array_filter($this->decoratedLoader->loadChoicesForValues($values, $value), $this->filter); } - /** - * {@inheritdoc} - */ public function loadValuesForChoices(array $choices, ?callable $value = null): array { return $this->decoratedLoader->loadValuesForChoices(array_filter($choices, $this->filter), $value); diff --git a/ChoiceList/Loader/IntlCallbackChoiceLoader.php b/ChoiceList/Loader/IntlCallbackChoiceLoader.php index 80d73af132..0931d3ef56 100644 --- a/ChoiceList/Loader/IntlCallbackChoiceLoader.php +++ b/ChoiceList/Loader/IntlCallbackChoiceLoader.php @@ -19,18 +19,12 @@ */ class IntlCallbackChoiceLoader extends CallbackChoiceLoader { - /** - * {@inheritdoc} - */ - public function loadChoicesForValues(array $values, ?callable $value = null) + public function loadChoicesForValues(array $values, ?callable $value = null): array { return parent::loadChoicesForValues(array_filter($values), $value); } - /** - * {@inheritdoc} - */ - public function loadValuesForChoices(array $choices, ?callable $value = null) + public function loadValuesForChoices(array $choices, ?callable $value = null): array { $choices = array_filter($choices); diff --git a/ChoiceList/Loader/LazyChoiceLoader.php b/ChoiceList/Loader/LazyChoiceLoader.php new file mode 100644 index 0000000000..03451be365 --- /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 46b7d19e71..923fab2241 100644 --- a/ChoiceList/View/ChoiceGroupView.php +++ b/ChoiceList/View/ChoiceGroupView.php @@ -20,27 +20,21 @@ */ class ChoiceGroupView implements \IteratorAggregate { - public $label; - public $choices; - /** * Creates a new choice group view. * - * @param array $choices the choice views in the group + * @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 = [], + ) { } /** - * {@inheritdoc} - * * @return \Traversable */ - #[\ReturnTypeWillChange] - public function getIterator() + public function getIterator(): \Traversable { return new \ArrayIterator($this->choices); } diff --git a/ChoiceList/View/ChoiceListView.php b/ChoiceList/View/ChoiceListView.php index 586269b516..f64d10e761 100644 --- a/ChoiceList/View/ChoiceListView.php +++ b/ChoiceList/View/ChoiceListView.php @@ -22,29 +22,24 @@ */ class ChoiceListView { - public $choices; - public $preferredChoices; - /** * Creates a new choice list view. * - * @param ChoiceGroupView[]|ChoiceView[] $choices The choice views - * @param ChoiceGroupView[]|ChoiceView[] $preferredChoices the preferred choice views + * @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 = [], + ) { } /** * Returns whether a placeholder is in the choices. * * A placeholder must be the first child element, not be in a group and have an empty value. - * - * @return bool */ - public function hasPlaceholder() + public function hasPlaceholder(): bool { if ($this->preferredChoices) { $firstChoice = reset($this->preferredChoices); diff --git a/ChoiceList/View/ChoiceView.php b/ChoiceList/View/ChoiceView.php index e1d8ac92d8..d59a7c947b 100644 --- a/ChoiceList/View/ChoiceView.php +++ b/ChoiceList/View/ChoiceView.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Form\ChoiceList\View; -use Symfony\Component\Translation\TranslatableMessage; +use Symfony\Contracts\Translation\TranslatableInterface; /** * Represents a choice in templates. @@ -20,35 +20,21 @@ */ class ChoiceView { - public $label; - public $value; - public $data; - - /** - * Additional attributes for the HTML tag. - */ - public $attr; - - /** - * Additional parameters used to translate the label. - */ - public $labelTranslationParameters; - /** * Creates a new choice view. * - * @param mixed $data The original choice - * @param string $value The view representation of the choice - * @param string|TranslatableMessage|false $label The label displayed to humans; pass false to discard the label - * @param array $attr Additional attributes for the HTML tag - * @param array $labelTranslationParameters Additional parameters used to translate the label + * @param mixed $data The original choice + * @param string $value The view representation of the choice + * @param string|TranslatableInterface|false $label The label displayed to humans; pass false to discard the label + * @param array $attr Additional attributes for the HTML tag + * @param array $labelTranslationParameters Additional parameters used to translate the label */ - public function __construct($data, string $value, $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/ClearableErrorsInterface.php b/ClearableErrorsInterface.php index e609bed02f..a05ece05a8 100644 --- a/ClearableErrorsInterface.php +++ b/ClearableErrorsInterface.php @@ -25,5 +25,5 @@ interface ClearableErrorsInterface * * @return $this */ - public function clearErrors(bool $deep = false); + public function clearErrors(bool $deep = false): static; } diff --git a/ClickableInterface.php b/ClickableInterface.php index 8b02d36e9e..9be7de0ce8 100644 --- a/ClickableInterface.php +++ b/ClickableInterface.php @@ -20,8 +20,6 @@ interface ClickableInterface { /** * Returns whether this element was clicked. - * - * @return bool */ - public function isClicked(); + public function isClicked(): bool; } diff --git a/Command/DebugCommand.php b/Command/DebugCommand.php index e408a909b9..91db6f1a91 100644 --- a/Command/DebugCommand.php +++ b/Command/DebugCommand.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Form\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; @@ -20,54 +21,40 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter; use Symfony\Component\Form\Console\Helper\DescriptorHelper; use Symfony\Component\Form\Extension\Core\CoreExtension; use Symfony\Component\Form\FormRegistryInterface; use Symfony\Component\Form\FormTypeInterface; -use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; /** * A console command for retrieving information about form types. * * @author Yonel Ceruto */ +#[AsCommand(name: 'debug:form', description: 'Display form type information')] class DebugCommand extends Command { - protected static $defaultName = 'debug:form'; - protected static $defaultDescription = 'Display form type information'; - - private $formRegistry; - private $namespaces; - private $types; - private $extensions; - private $guessers; - private $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; } - /** - * {@inheritdoc} - */ - protected function configure() + protected function configure(): void { $this ->setDefinition([ 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, 'The output format (txt or json)', 'txt'), + new InputOption('format', null, InputOption::VALUE_REQUIRED, \sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions())), 'txt'), ]) - ->setDescription(self::$defaultDescription) ->setHelp(<<<'EOF' The %command.name% command displays information about form types. @@ -100,10 +87,7 @@ protected function configure() ; } - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); @@ -130,7 +114,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $object = $resolvedType->getOptionsResolver(); if (!$object->isDefined($option)) { - $message = sprintf('Option "%s" is not defined in "%s".', $option, \get_class($resolvedType->getInnerType())); + $message = \sprintf('Option "%s" is not defined in "%s".', $option, $resolvedType->getInnerType()::class); if ($alternatives = $this->findAlternatives($option, $object->getDefinedOptions())) { if (1 === \count($alternatives)) { @@ -164,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)) { @@ -182,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 @@ -211,9 +195,8 @@ private function getCoreTypes(): array { $coreExtension = new CoreExtension(); $loadTypesRefMethod = (new \ReflectionObject($coreExtension))->getMethod('loadTypes'); - $loadTypesRefMethod->setAccessible(true); $coreTypes = $loadTypesRefMethod->invoke($coreExtension); - $coreTypes = array_map(function (FormTypeInterface $type) { return \get_class($type); }, $coreTypes); + $coreTypes = array_map(static fn (FormTypeInterface $type) => $type::class, $coreTypes); sort($coreTypes); return $coreTypes; @@ -246,7 +229,7 @@ private function findAlternatives(string $name, array $collection): array } $threshold = 1e3; - $alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; }); + $alternatives = array_filter($alternatives, static fn ($lev) => $lev < 2 * $threshold); ksort($alternatives, \SORT_NATURAL | \SORT_FLAG_CASE); return array_keys($alternatives); @@ -267,8 +250,7 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti } if ($input->mustSuggestOptionValuesFor('format')) { - $helper = new DescriptorHelper(); - $suggestions->suggestValues($helper->getFormats()); + $suggestions->suggestValues($this->getAvailableFormatOptions()); } } @@ -289,4 +271,10 @@ private function completeOptions(string $class, CompletionSuggestions $suggestio $resolvedType = $this->formRegistry->getType($class); $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 4aceafbe69..c910acdf4d 100644 --- a/Console/Descriptor/Descriptor.php +++ b/Console/Descriptor/Descriptor.php @@ -29,46 +29,34 @@ */ abstract class Descriptor implements DescriptorInterface { - /** @var OutputStyle */ - protected $output; - protected $type; - protected $ownOptions = []; - protected $overriddenOptions = []; - protected $parentOptions = []; - protected $extensionOptions = []; - protected $requiredOptions = []; - protected $parents = []; - protected $extensions = []; - - /** - * {@inheritdoc} - */ - public function describe(OutputInterface $output, $object, array $options = []) + protected OutputStyle $output; + protected array $ownOptions = []; + protected array $overriddenOptions = []; + protected array $parentOptions = []; + protected array $extensionOptions = []; + protected array $requiredOptions = []; + protected array $parents = []; + protected array $extensions = []; + + public function describe(OutputInterface $output, ?object $object, array $options = []): void { $this->output = $output instanceof OutputStyle ? $output : new SymfonyStyle(new ArrayInput([]), $output); - switch (true) { - case null === $object: - $this->describeDefaults($options); - break; - case $object instanceof ResolvedFormTypeInterface: - $this->describeResolvedFormType($object, $options); - break; - case $object instanceof OptionsResolver: - $this->describeOption($object, $options); - break; - default: - throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_debug_type($object))); - } + match (true) { + 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))), + }; } - abstract protected function describeDefaults(array $options); + abstract protected function describeDefaults(array $options): void; - abstract protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = []); + abstract protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = []): void; - abstract protected function describeOption(OptionsResolver $optionsResolver, array $options); + abstract protected function describeOption(OptionsResolver $optionsResolver, array $options): void; - protected function collectOptions(ResolvedFormTypeInterface $type) + protected function collectOptions(ResolvedFormTypeInterface $type): void { $this->parents = []; $this->extensions = []; @@ -106,7 +94,7 @@ protected function collectOptions(ResolvedFormTypeInterface $type) $this->extensions = array_keys($this->extensions); } - protected function getOptionDefinition(OptionsResolver $optionsResolver, string $option) + protected function getOptionDefinition(OptionsResolver $optionsResolver, string $option): array { $definition = []; @@ -135,12 +123,12 @@ protected function getOptionDefinition(OptionsResolver $optionsResolver, string foreach ($map as $key => $method) { try { $definition[$key] = $introspector->{$method}($option); - } catch (NoConfigurationException $e) { + } catch (NoConfigurationException) { // noop } } - 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']; @@ -149,7 +137,7 @@ protected function getOptionDefinition(OptionsResolver $optionsResolver, string return $definition; } - protected function filterOptionsByDeprecated(ResolvedFormTypeInterface $type) + protected function filterOptionsByDeprecated(ResolvedFormTypeInterface $type): void { $deprecatedOptions = []; $resolver = $type->getOptionsResolver(); @@ -159,7 +147,7 @@ protected function filterOptionsByDeprecated(ResolvedFormTypeInterface $type) } } - $filterByDeprecated = function (array $options) use ($deprecatedOptions) { + $filterByDeprecated = static function (array $options) use ($deprecatedOptions) { foreach ($options as $class => $opts) { if ($deprecated = array_intersect($deprecatedOptions, $opts)) { $options[$class] = $deprecated; @@ -179,7 +167,7 @@ protected function filterOptionsByDeprecated(ResolvedFormTypeInterface $type) private function getParentOptionsResolver(ResolvedFormTypeInterface $type): OptionsResolver { - $this->parents[$class = \get_class($type->getInnerType())] = []; + $this->parents[$class = $type->getInnerType()::class] = []; if (null !== $type->getParent()) { $optionsResolver = clone $this->getParentOptionsResolver($type->getParent()); @@ -196,12 +184,12 @@ private function getParentOptionsResolver(ResolvedFormTypeInterface $type): Opti return $optionsResolver; } - private function collectTypeExtensionsOptions(ResolvedFormTypeInterface $type, OptionsResolver $optionsResolver) + private function collectTypeExtensionsOptions(ResolvedFormTypeInterface $type, OptionsResolver $optionsResolver): void { foreach ($type->getTypeExtensions() as $extension) { $inheritedOptions = $optionsResolver->getDefinedOptions(); $extension->configureOptions($optionsResolver); - $this->extensions[\get_class($extension)] = array_diff($optionsResolver->getDefinedOptions(), $inheritedOptions); + $this->extensions[$extension::class] = array_diff($optionsResolver->getDefinedOptions(), $inheritedOptions); } } } diff --git a/Console/Descriptor/JsonDescriptor.php b/Console/Descriptor/JsonDescriptor.php index d40561e468..1f5c7bfa55 100644 --- a/Console/Descriptor/JsonDescriptor.php +++ b/Console/Descriptor/JsonDescriptor.php @@ -21,7 +21,7 @@ */ class JsonDescriptor extends Descriptor { - protected function describeDefaults(array $options) + protected function describeDefaults(array $options): void { $data['builtin_form_types'] = $options['core_types']; $data['service_form_types'] = $options['service_types']; @@ -33,7 +33,7 @@ protected function describeDefaults(array $options) $this->writeData($data, $options); } - protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = []) + protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = []): void { $this->collectOptions($resolvedFormType); @@ -51,7 +51,7 @@ protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedF $this->sortOptions($formOptions); $data = [ - 'class' => \get_class($resolvedFormType->getInnerType()), + 'class' => $resolvedFormType->getInnerType()::class, 'block_prefix' => $resolvedFormType->getInnerType()->getBlockPrefix(), 'options' => $formOptions, 'parent_types' => $this->parents, @@ -61,7 +61,7 @@ protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedF $this->writeData($data, $options); } - protected function describeOption(OptionsResolver $optionsResolver, array $options) + protected function describeOption(OptionsResolver $optionsResolver, array $options): void { $definition = $this->getOptionDefinition($optionsResolver, $options['option']); @@ -93,14 +93,14 @@ protected function describeOption(OptionsResolver $optionsResolver, array $optio $this->writeData($data, $options); } - private function writeData(array $data, array $options) + private function writeData(array $data, array $options): void { $flags = $options['json_encoding'] ?? 0; $this->output->write(json_encode($data, $flags | \JSON_PRETTY_PRINT)."\n"); } - private function sortOptions(array &$options) + private function sortOptions(array &$options): void { foreach ($options as &$opts) { $sorted = false; diff --git a/Console/Descriptor/TextDescriptor.php b/Console/Descriptor/TextDescriptor.php index 1146b35d73..630b872535 100644 --- a/Console/Descriptor/TextDescriptor.php +++ b/Console/Descriptor/TextDescriptor.php @@ -13,8 +13,8 @@ use Symfony\Component\Console\Helper\Dumper; use Symfony\Component\Console\Helper\TableSeparator; +use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter; use Symfony\Component\Form\ResolvedFormTypeInterface; -use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; use Symfony\Component\OptionsResolver\OptionsResolver; /** @@ -24,20 +24,16 @@ */ class TextDescriptor extends Descriptor { - private $fileLinkFormatter; - - public function __construct(?FileLinkFormatter $fileLinkFormatter = null) - { - $this->fileLinkFormatter = $fileLinkFormatter; + public function __construct( + private readonly ?FileLinkFormatter $fileLinkFormatter = null, + ) { } - protected function describeDefaults(array $options) + protected function describeDefaults(array $options): void { if ($options['core_types']) { $this->output->section('Built-in form types (Symfony\Component\Form\Extension\Core\Type)'); - $shortClassNames = array_map(function ($fqcn) { - return $this->formatClassLink($fqcn, \array_slice(explode('\\', $fqcn), -1)[0]); - }, $options['core_types']); + $shortClassNames = array_map(fn ($fqcn) => $this->formatClassLink($fqcn, \array_slice(explode('\\', $fqcn), -1)[0]), $options['core_types']); for ($i = 0, $loopsMax = \count($shortClassNames); $i * 5 < $loopsMax; ++$i) { $this->output->writeln(' '.implode(', ', \array_slice($shortClassNames, $i * 5, 5))); } @@ -45,23 +41,23 @@ protected function describeDefaults(array $options) if ($options['service_types']) { $this->output->section('Service form types'); - $this->output->listing(array_map([$this, 'formatClassLink'], $options['service_types'])); + $this->output->listing(array_map($this->formatClassLink(...), $options['service_types'])); } if (!$options['show_deprecated']) { if ($options['extensions']) { $this->output->section('Type extensions'); - $this->output->listing(array_map([$this, 'formatClassLink'], $options['extensions'])); + $this->output->listing(array_map($this->formatClassLink(...), $options['extensions'])); } if ($options['guessers']) { $this->output->section('Type guessers'); - $this->output->listing(array_map([$this, 'formatClassLink'], $options['guessers'])); + $this->output->listing(array_map($this->formatClassLink(...), $options['guessers'])); } } } - protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = []) + protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = []): void { $this->collectOptions($resolvedFormType); @@ -84,7 +80,7 @@ protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedF 'extension' => 'Extension options', ], $formOptions); - $this->output->title(sprintf('%s (Block prefix: "%s")', \get_class($resolvedFormType->getInnerType()), $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)); @@ -92,16 +88,16 @@ protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedF if ($this->parents) { $this->output->section('Parent types'); - $this->output->listing(array_map([$this, 'formatClassLink'], $this->parents)); + $this->output->listing(array_map($this->formatClassLink(...), $this->parents)); } if ($this->extensions) { $this->output->section('Type extensions'); - $this->output->listing(array_map([$this, 'formatClassLink'], $this->extensions)); + $this->output->listing(array_map($this->formatClassLink(...), $this->extensions)); } } - protected function describeOption(OptionsResolver $optionsResolver, array $options) + protected function describeOption(OptionsResolver $optionsResolver, array $options): void { $definition = $this->getOptionDefinition($optionsResolver, $options['option']); @@ -135,7 +131,7 @@ protected function describeOption(OptionsResolver $optionsResolver, array $optio } array_pop($rows); - $this->output->title(sprintf('%s (%s)', \get_class($options['type']), $options['option'])); + $this->output->title(\sprintf('%s (%s)', $options['type']::class, $options['option'])); $this->output->table([], $rows); } @@ -176,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); @@ -194,15 +190,13 @@ private function normalizeAndSortOptionsColumns(array $options): array private function formatClassLink(string $class, ?string $text = null): string { - if (null === $text) { - $text = $class; - } + $text ??= $class; if ('' === $fileLink = $this->getFileLink($class)) { return $text; } - return sprintf('%s', $fileLink, $text); + return \sprintf('%s', $fileLink, $text); } private function getFileLink(string $class): string @@ -213,7 +207,7 @@ private function getFileLink(string $class): string try { $r = new \ReflectionClass($class); - } catch (\ReflectionException $e) { + } catch (\ReflectionException) { return ''; } diff --git a/Console/Helper/DescriptorHelper.php b/Console/Helper/DescriptorHelper.php index 72bfa6e6a7..776b9eaa1c 100644 --- a/Console/Helper/DescriptorHelper.php +++ b/Console/Helper/DescriptorHelper.php @@ -12,9 +12,9 @@ namespace Symfony\Component\Form\Console\Helper; use Symfony\Component\Console\Helper\DescriptorHelper as BaseDescriptorHelper; +use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter; use Symfony\Component\Form\Console\Descriptor\JsonDescriptor; use Symfony\Component\Form\Console\Descriptor\TextDescriptor; -use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; /** * @author Yonel Ceruto diff --git a/DataAccessorInterface.php b/DataAccessorInterface.php index a5b4bc8179..a0aea7e0ee 100644 --- a/DataAccessorInterface.php +++ b/DataAccessorInterface.php @@ -21,45 +21,30 @@ interface DataAccessorInterface /** * Returns the value at the end of the property of the object graph. * - * @param object|array $viewData The view data of the compound form - * @param FormInterface $form The {@link FormInterface()} instance to check - * - * @return mixed - * * @throws Exception\AccessException If unable to read from the given form data */ - public function getValue($viewData, FormInterface $form); + public function getValue(object|array $viewData, FormInterface $form): mixed; /** * Sets the value at the end of the property of the object graph. * - * @param object|array $viewData The view data of the compound form - * @param mixed $value The value to set at the end of the object graph - * @param FormInterface $form The {@link FormInterface()} instance to check - * * @throws Exception\AccessException If unable to write the given value */ - public function setValue(&$viewData, $value, FormInterface $form): void; + public function setValue(object|array &$viewData, mixed $value, FormInterface $form): void; /** * Returns whether a value can be read from an object graph. * * Whenever this method returns true, {@link getValue()} is guaranteed not * to throw an exception when called with the same arguments. - * - * @param object|array $viewData The view data of the compound form - * @param FormInterface $form The {@link FormInterface()} instance to check */ - public function isReadable($viewData, FormInterface $form): bool; + public function isReadable(object|array $viewData, FormInterface $form): bool; /** * Returns whether a value can be written at a given object graph. * * Whenever this method returns true, {@link setValue()} is guaranteed not * to throw an exception when called with the same arguments. - * - * @param object|array $viewData The view data of the compound form - * @param FormInterface $form The {@link FormInterface()} instance to check */ - public function isWritable($viewData, FormInterface $form): bool; + public function isWritable(object|array $viewData, FormInterface $form): bool; } diff --git a/DataMapperInterface.php b/DataMapperInterface.php index c2cd3601e4..b8ca86bfe0 100644 --- a/DataMapperInterface.php +++ b/DataMapperInterface.php @@ -22,12 +22,12 @@ interface DataMapperInterface * The method is responsible for calling {@link FormInterface::setData()} * on the children of compound forms, defining their underlying model data. * - * @param mixed $viewData View data of the compound form being initialized - * @param FormInterface[]|\Traversable $forms A list of {@link FormInterface} instances + * @param mixed $viewData View data of the compound form being initialized + * @param \Traversable $forms A list of {@link FormInterface} instances * * @throws Exception\UnexpectedTypeException if the type of the data parameter is not supported */ - public function mapDataToForms($viewData, \Traversable $forms); + public function mapDataToForms(mixed $viewData, \Traversable $forms): void; /** * Maps the model data of a list of children forms into the view data of their parent. @@ -52,11 +52,11 @@ public function mapDataToForms($viewData, \Traversable $forms); * The model data can be an array or an object, so this second argument is always passed * by reference. * - * @param FormInterface[]|\Traversable $forms A list of {@link FormInterface} instances - * @param mixed $viewData The compound form's view data that get mapped - * its children model data + * @param \Traversable $forms A list of {@link FormInterface} instances + * @param mixed &$viewData The compound form's view data that get mapped + * its children model data * * @throws Exception\UnexpectedTypeException if the type of the data parameter is not supported */ - public function mapFormsToData(\Traversable $forms, &$viewData); + public function mapFormsToData(\Traversable $forms, mixed &$viewData): void; } diff --git a/DataTransformerInterface.php b/DataTransformerInterface.php index 5f48428473..aeab6f2c34 100644 --- a/DataTransformerInterface.php +++ b/DataTransformerInterface.php @@ -17,6 +17,9 @@ * Transforms a value between different representations. * * @author Bernhard Schussek + * + * @template TValue + * @template TTransformedValue */ interface DataTransformerInterface { @@ -53,13 +56,13 @@ interface DataTransformerInterface * of the first data transformer outputs NULL, the second must be able to * process that value. * - * @param mixed $value The value in the original representation + * @param TValue|null $value The value in the original representation * - * @return mixed + * @return TTransformedValue|null * * @throws TransformationFailedException when the transformation fails */ - public function transform($value); + public function transform(mixed $value): mixed; /** * Transforms a value from the transformed representation to its original @@ -82,11 +85,11 @@ public function transform($value); * By convention, reverseTransform() should return NULL if an empty string * is passed. * - * @param mixed $value The value in the transformed representation + * @param TTransformedValue|null $value The value in the transformed representation * - * @return mixed + * @return TValue|null * * @throws TransformationFailedException when the transformation fails */ - public function reverseTransform($value); + public function reverseTransform(mixed $value): mixed; } diff --git a/DependencyInjection/FormPass.php b/DependencyInjection/FormPass.php index 0ba46117c5..bec1782d40 100644 --- a/DependencyInjection/FormPass.php +++ b/DependencyInjection/FormPass.php @@ -30,32 +30,13 @@ class FormPass implements CompilerPassInterface { use PriorityTaggedServiceTrait; - private $formExtensionService; - private $formTypeTag; - private $formTypeExtensionTag; - private $formTypeGuesserTag; - private $formDebugCommandService; - - public function __construct(string $formExtensionService = 'form.extension', string $formTypeTag = 'form.type', string $formTypeExtensionTag = 'form.type_extension', string $formTypeGuesserTag = 'form.type_guesser', string $formDebugCommandService = 'console.command.form_debug') - { - if (0 < \func_num_args()) { - trigger_deprecation('symfony/http-kernel', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); - } - - $this->formExtensionService = $formExtensionService; - $this->formTypeTag = $formTypeTag; - $this->formTypeExtensionTag = $formTypeExtensionTag; - $this->formTypeGuesserTag = $formTypeGuesserTag; - $this->formDebugCommandService = $formDebugCommandService; - } - - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { - if (!$container->hasDefinition($this->formExtensionService)) { + if (!$container->hasDefinition('form.extension')) { return; } - $definition = $container->getDefinition($this->formExtensionService); + $definition = $container->getDefinition('form.extension'); $definition->replaceArgument(0, $this->processFormTypes($container)); $definition->replaceArgument(1, $this->processFormTypeExtensions($container)); $definition->replaceArgument(2, $this->processFormTypeGuessers($container)); @@ -66,21 +47,34 @@ 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($this->formTypeTag, true) as $serviceId => $tag) { + foreach ($container->findTaggedServiceIds('form.type', true) as $serviceId => $tag) { // Add form type service to the service locator $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($this->formDebugCommandService)) { - $commandDefinition = $container->getDefinition($this->formDebugCommandService); + if ($container->hasDefinition('console.command.form_debug')) { + $commandDefinition = $container->getDefinition('console.command.form_debug'); $commandDefinition->setArgument(1, array_keys($namespaces)); $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); } @@ -88,11 +82,11 @@ private function processFormTypeExtensions(ContainerBuilder $container): array { $typeExtensions = []; $typeExtensionsClasses = []; - foreach ($this->findAndSortTaggedServices($this->formTypeExtensionTag, $container) as $reference) { + foreach ($this->findAndSortTaggedServices('form.type_extension', $container) as $reference) { $serviceId = (string) $reference; $serviceDefinition = $container->getDefinition($serviceId); - $tag = $serviceDefinition->getTag($this->formTypeExtensionTag); + $tag = $serviceDefinition->getTag('form.type_extension'); $typeExtensionClass = $container->getParameterBag()->resolveValue($serviceDefinition->getClass()); if (isset($tag[0]['extended_type'])) { @@ -108,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)); } } } @@ -117,8 +111,8 @@ private function processFormTypeExtensions(ContainerBuilder $container): array $typeExtensions[$extendedType] = new IteratorArgument($extensions); } - if ($container->hasDefinition($this->formDebugCommandService)) { - $commandDefinition = $container->getDefinition($this->formDebugCommandService); + if ($container->hasDefinition('console.command.form_debug')) { + $commandDefinition = $container->getDefinition('console.command.form_debug'); $commandDefinition->setArgument(3, $typeExtensionsClasses); } @@ -129,15 +123,15 @@ private function processFormTypeGuessers(ContainerBuilder $container): ArgumentI { $guessers = []; $guessersClasses = []; - foreach ($container->findTaggedServiceIds($this->formTypeGuesserTag, true) as $serviceId => $tags) { + foreach ($container->findTaggedServiceIds('form.type_guesser', true) as $serviceId => $tags) { $guessers[] = new Reference($serviceId); $serviceDefinition = $container->getDefinition($serviceId); $guessersClasses[] = $serviceDefinition->getClass(); } - if ($container->hasDefinition($this->formDebugCommandService)) { - $commandDefinition = $container->getDefinition($this->formDebugCommandService); + if ($container->hasDefinition('console.command.form_debug')) { + $commandDefinition = $container->getDefinition('console.command.form_debug'); $commandDefinition->setArgument(4, $guessersClasses); } diff --git a/Event/PostSetDataEvent.php b/Event/PostSetDataEvent.php index e759308a1c..5b6430a81d 100644 --- a/Event/PostSetDataEvent.php +++ b/Event/PostSetDataEvent.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Form\Event; +use Symfony\Component\Form\Exception\BadMethodCallException; use Symfony\Component\Form\FormEvent; /** @@ -21,4 +22,8 @@ */ final class PostSetDataEvent extends FormEvent { + public function setData(mixed $data): never + { + throw new BadMethodCallException('Form data cannot be changed during "form.post_set_data", you should use "form.pre_set_data" instead.'); + } } diff --git a/Event/PostSubmitEvent.php b/Event/PostSubmitEvent.php index aa3775f5c1..88cd5c4eb1 100644 --- a/Event/PostSubmitEvent.php +++ b/Event/PostSubmitEvent.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Form\Event; +use Symfony\Component\Form\Exception\BadMethodCallException; use Symfony\Component\Form\FormEvent; /** @@ -21,4 +22,8 @@ */ final class PostSubmitEvent extends FormEvent { + public function setData(mixed $data): never + { + throw new BadMethodCallException('Form data cannot be changed during "form.post_submit", you should use "form.pre_submit" or "form.submit" instead.'); + } } diff --git a/Exception/TransformationFailedException.php b/Exception/TransformationFailedException.php index 4d4fce1962..3973d70b46 100644 --- a/Exception/TransformationFailedException.php +++ b/Exception/TransformationFailedException.php @@ -18,8 +18,8 @@ */ class TransformationFailedException extends RuntimeException { - private $invalidMessage; - private $invalidMessageParameters; + private ?string $invalidMessage; + private array $invalidMessageParameters; public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = null, ?string $invalidMessage = null, array $invalidMessageParameters = []) { @@ -34,7 +34,7 @@ public function __construct(string $message = '', int $code = 0, ?\Throwable $pr * @param string|null $invalidMessage The message or message key * @param array $invalidMessageParameters Data to be passed into the translator */ - public function setInvalidMessage(?string $invalidMessage = null, array $invalidMessageParameters = []): void + public function setInvalidMessage(?string $invalidMessage, array $invalidMessageParameters = []): void { $this->invalidMessage = $invalidMessage; $this->invalidMessageParameters = $invalidMessageParameters; diff --git a/Exception/UnexpectedTypeException.php b/Exception/UnexpectedTypeException.php index d25d4705fa..223061b77b 100644 --- a/Exception/UnexpectedTypeException.php +++ b/Exception/UnexpectedTypeException.php @@ -13,8 +13,8 @@ class UnexpectedTypeException extends InvalidArgumentException { - public function __construct($value, string $expectedType) + 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 717d64633e..1640ed0524 100644 --- a/Extension/Core/CoreExtension.php +++ b/Extension/Core/CoreExtension.php @@ -28,18 +28,19 @@ */ class CoreExtension extends AbstractExtension { - private $propertyAccessor; - private $choiceListFactory; - private $translator; + private PropertyAccessorInterface $propertyAccessor; + private ChoiceListFactoryInterface $choiceListFactory; - 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() + protected function loadTypes(): array { return [ new Type\FormType($this->propertyAccessor), @@ -80,7 +81,7 @@ protected function loadTypes() ]; } - protected function loadTypeExtensions() + protected function loadTypeExtensions(): array { return [ new TransformationFailureExtension($this->translator), diff --git a/Extension/Core/DataAccessor/CallbackAccessor.php b/Extension/Core/DataAccessor/CallbackAccessor.php index fb121450a4..a7d5bb13fe 100644 --- a/Extension/Core/DataAccessor/CallbackAccessor.php +++ b/Extension/Core/DataAccessor/CallbackAccessor.php @@ -22,10 +22,7 @@ */ class CallbackAccessor implements DataAccessorInterface { - /** - * {@inheritdoc} - */ - public function getValue($data, FormInterface $form) + public function getValue(object|array $data, FormInterface $form): mixed { if (null === $getter = $form->getConfig()->getOption('getter')) { throw new AccessException('Unable to read from the given form data as no getter is defined.'); @@ -34,10 +31,7 @@ public function getValue($data, FormInterface $form) return ($getter)($data, $form); } - /** - * {@inheritdoc} - */ - public function setValue(&$data, $value, FormInterface $form): void + public function setValue(object|array &$data, mixed $value, FormInterface $form): void { if (null === $setter = $form->getConfig()->getOption('setter')) { throw new AccessException('Unable to write the given value as no setter is defined.'); @@ -46,18 +40,12 @@ public function setValue(&$data, $value, FormInterface $form): void ($setter)($data, $form->getData(), $form); } - /** - * {@inheritdoc} - */ - public function isReadable($data, FormInterface $form): bool + public function isReadable(object|array $data, FormInterface $form): bool { return null !== $form->getConfig()->getOption('getter'); } - /** - * {@inheritdoc} - */ - public function isWritable($data, FormInterface $form): bool + public function isWritable(object|array $data, FormInterface $form): bool { return null !== $form->getConfig()->getOption('setter'); } diff --git a/Extension/Core/DataAccessor/ChainAccessor.php b/Extension/Core/DataAccessor/ChainAccessor.php index 39e444bb7b..e7b9da09ea 100644 --- a/Extension/Core/DataAccessor/ChainAccessor.php +++ b/Extension/Core/DataAccessor/ChainAccessor.php @@ -20,20 +20,15 @@ */ class ChainAccessor implements DataAccessorInterface { - private $accessors; - /** * @param DataAccessorInterface[]|iterable $accessors */ - public function __construct(iterable $accessors) - { - $this->accessors = $accessors; + public function __construct( + private iterable $accessors, + ) { } - /** - * {@inheritdoc} - */ - public function getValue($data, FormInterface $form) + public function getValue(object|array $data, FormInterface $form): mixed { foreach ($this->accessors as $accessor) { if ($accessor->isReadable($data, $form)) { @@ -44,10 +39,7 @@ public function getValue($data, FormInterface $form) throw new AccessException('Unable to read from the given form data as no accessor in the chain is able to read the data.'); } - /** - * {@inheritdoc} - */ - public function setValue(&$data, $value, FormInterface $form): void + public function setValue(object|array &$data, mixed $value, FormInterface $form): void { foreach ($this->accessors as $accessor) { if ($accessor->isWritable($data, $form)) { @@ -60,10 +52,7 @@ public function setValue(&$data, $value, FormInterface $form): void throw new AccessException('Unable to write the given value as no accessor in the chain is able to set the data.'); } - /** - * {@inheritdoc} - */ - public function isReadable($data, FormInterface $form): bool + public function isReadable(object|array $data, FormInterface $form): bool { foreach ($this->accessors as $accessor) { if ($accessor->isReadable($data, $form)) { @@ -74,10 +63,7 @@ public function isReadable($data, FormInterface $form): bool return false; } - /** - * {@inheritdoc} - */ - public function isWritable($data, FormInterface $form): bool + public function isWritable(object|array $data, FormInterface $form): bool { foreach ($this->accessors as $accessor) { if ($accessor->isWritable($data, $form)) { diff --git a/Extension/Core/DataAccessor/PropertyPathAccessor.php b/Extension/Core/DataAccessor/PropertyPathAccessor.php index 24de33a6b9..33d01fd4f5 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; @@ -31,17 +32,14 @@ */ class PropertyPathAccessor implements DataAccessorInterface { - private $propertyAccessor; + private PropertyAccessorInterface $propertyAccessor; public function __construct(?PropertyAccessorInterface $propertyAccessor = null) { $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); } - /** - * {@inheritdoc} - */ - public function getValue($data, FormInterface $form) + public function getValue(object|array $data, FormInterface $form): mixed { if (null === $propertyPath = $form->getPropertyPath()) { throw new AccessException('Unable to read from the given form data as no property path is defined.'); @@ -50,10 +48,7 @@ public function getValue($data, FormInterface $form) return $this->getPropertyValue($data, $propertyPath); } - /** - * {@inheritdoc} - */ - public function setValue(&$data, $propertyValue, FormInterface $form): void + public function setValue(object|array &$data, mixed $value, FormInterface $form): void { if (null === $propertyPath = $form->getPropertyPath()) { throw new AccessException('Unable to write the given value as no property path is defined.'); @@ -71,34 +66,32 @@ public function setValue(&$data, $propertyValue, FormInterface $form): void // If the field is of type DateTimeInterface and the data is the same skip the update to // keep the original object hash - if ($propertyValue instanceof \DateTimeInterface && $propertyValue == $getValue()) { + if ($value instanceof \DateTimeInterface && $value == $getValue()) { return; } // If the data is identical to the value in $data, we are // dealing with a reference - if (!\is_object($data) || !$form->getConfig()->getByReference() || $propertyValue !== $getValue()) { - $this->propertyAccessor->setValue($data, $propertyPath, $propertyValue); + if (!\is_object($data) || !$form->getConfig()->getByReference() || $value !== $getValue()) { + 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); + } } } - /** - * {@inheritdoc} - */ - public function isReadable($data, FormInterface $form): bool + public function isReadable(object|array $data, FormInterface $form): bool { return null !== $form->getPropertyPath(); } - /** - * {@inheritdoc} - */ - public function isWritable($data, FormInterface $form): bool + public function isWritable(object|array $data, FormInterface $form): bool { return null !== $form->getPropertyPath(); } - private function getPropertyValue($data, PropertyPathInterface $propertyPath) + private function getPropertyValue(object|array $data, PropertyPathInterface $propertyPath): mixed { try { return $this->propertyAccessor->getValue($data, $propertyPath); @@ -109,7 +102,7 @@ private function getPropertyValue($data, PropertyPathInterface $propertyPath) if (!$e instanceof UninitializedPropertyException // For versions without UninitializedPropertyException check the exception message - && (class_exists(UninitializedPropertyException::class) || false === strpos($e->getMessage(), 'You should initialize it')) + && (class_exists(UninitializedPropertyException::class) || !str_contains($e->getMessage(), 'You should initialize it')) ) { throw $e; } diff --git a/Extension/Core/DataMapper/CheckboxListMapper.php b/Extension/Core/DataMapper/CheckboxListMapper.php index ecfd83a066..eff6d166b3 100644 --- a/Extension/Core/DataMapper/CheckboxListMapper.php +++ b/Extension/Core/DataMapper/CheckboxListMapper.php @@ -25,20 +25,9 @@ */ class CheckboxListMapper implements DataMapperInterface { - /** - * {@inheritdoc} - */ - public function mapDataToForms($choices, iterable $checkboxes) + public function mapDataToForms(mixed $choices, \Traversable $checkboxes): void { - if (\is_array($checkboxes)) { - trigger_deprecation('symfony/form', '5.3', 'Passing an array as the second argument of the "%s()" method is deprecated, pass "\Traversable" instead.', __METHOD__); - } - - if (null === $choices) { - $choices = []; - } - - if (!\is_array($choices)) { + if (!\is_array($choices ??= [])) { throw new UnexpectedTypeException($choices, 'array'); } @@ -48,15 +37,8 @@ public function mapDataToForms($choices, iterable $checkboxes) } } - /** - * {@inheritdoc} - */ - public function mapFormsToData(iterable $checkboxes, &$choices) + public function mapFormsToData(\Traversable $checkboxes, mixed &$choices): void { - if (\is_array($checkboxes)) { - trigger_deprecation('symfony/form', '5.3', 'Passing an array as the first argument of the "%s()" method is deprecated, pass "\Traversable" instead.', __METHOD__); - } - if (!\is_array($choices)) { throw new UnexpectedTypeException($choices, 'array'); } diff --git a/Extension/Core/DataMapper/DataMapper.php b/Extension/Core/DataMapper/DataMapper.php index e480f47baa..a7bf980322 100644 --- a/Extension/Core/DataMapper/DataMapper.php +++ b/Extension/Core/DataMapper/DataMapper.php @@ -25,7 +25,7 @@ */ class DataMapper implements DataMapperInterface { - private $dataAccessor; + private DataAccessorInterface $dataAccessor; public function __construct(?DataAccessorInterface $dataAccessor = null) { @@ -35,15 +35,8 @@ public function __construct(?DataAccessorInterface $dataAccessor = null) ]); } - /** - * {@inheritdoc} - */ - public function mapDataToForms($data, iterable $forms): void + public function mapDataToForms(mixed $data, \Traversable $forms): void { - if (\is_array($forms)) { - trigger_deprecation('symfony/form', '5.3', 'Passing an array as the second argument of the "%s()" method is deprecated, pass "\Traversable" instead.', __METHOD__); - } - $empty = null === $data || [] === $data; if (!$empty && !\is_array($data) && !\is_object($data)) { @@ -61,15 +54,8 @@ public function mapDataToForms($data, iterable $forms): void } } - /** - * {@inheritdoc} - */ - public function mapFormsToData(iterable $forms, &$data): void + public function mapFormsToData(\Traversable $forms, mixed &$data): void { - if (\is_array($forms)) { - trigger_deprecation('symfony/form', '5.3', 'Passing an array as the first argument of the "%s()" method is deprecated, pass "\Traversable" instead.', __METHOD__); - } - if (null === $data) { return; } diff --git a/Extension/Core/DataMapper/PropertyPathMapper.php b/Extension/Core/DataMapper/PropertyPathMapper.php deleted file mode 100644 index fe3fe1886b..0000000000 --- a/Extension/Core/DataMapper/PropertyPathMapper.php +++ /dev/null @@ -1,118 +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\Extension\Core\DataMapper; - -use Symfony\Component\Form\DataMapperInterface; -use Symfony\Component\Form\Exception\UnexpectedTypeException; -use Symfony\Component\PropertyAccess\Exception\AccessException; -use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; -use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; -use Symfony\Component\PropertyAccess\PropertyAccess; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; - -trigger_deprecation('symfony/form', '5.2', 'The "%s" class is deprecated. Use "%s" instead.', PropertyPathMapper::class, DataMapper::class); - -/** - * Maps arrays/objects to/from forms using property paths. - * - * @author Bernhard Schussek - * - * @deprecated since symfony/form 5.2. Use {@see DataMapper} instead. - */ -class PropertyPathMapper implements DataMapperInterface -{ - private $propertyAccessor; - - public function __construct(?PropertyAccessorInterface $propertyAccessor = null) - { - $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); - } - - /** - * {@inheritdoc} - */ - public function mapDataToForms($data, iterable $forms) - { - $empty = null === $data || [] === $data; - - if (!$empty && !\is_array($data) && !\is_object($data)) { - throw new UnexpectedTypeException($data, 'object, array or empty'); - } - - foreach ($forms as $form) { - $propertyPath = $form->getPropertyPath(); - $config = $form->getConfig(); - - if (!$empty && null !== $propertyPath && $config->getMapped()) { - $form->setData($this->getPropertyValue($data, $propertyPath)); - } else { - $form->setData($config->getData()); - } - } - } - - /** - * {@inheritdoc} - */ - public function mapFormsToData(iterable $forms, &$data) - { - if (null === $data) { - return; - } - - if (!\is_array($data) && !\is_object($data)) { - throw new UnexpectedTypeException($data, 'object, array or empty'); - } - - foreach ($forms as $form) { - $propertyPath = $form->getPropertyPath(); - $config = $form->getConfig(); - - // Write-back is disabled if the form is not synchronized (transformation failed), - // if the form was not submitted and if the form is disabled (modification not allowed) - if (null !== $propertyPath && $config->getMapped() && $form->isSubmitted() && $form->isSynchronized() && !$form->isDisabled()) { - $propertyValue = $form->getData(); - // If the field is of type DateTimeInterface and the data is the same skip the update to - // keep the original object hash - if ($propertyValue instanceof \DateTimeInterface && $propertyValue == $this->getPropertyValue($data, $propertyPath)) { - continue; - } - - // If the data is identical to the value in $data, we are - // dealing with a reference - if (!\is_object($data) || !$config->getByReference() || $propertyValue !== $this->getPropertyValue($data, $propertyPath)) { - $this->propertyAccessor->setValue($data, $propertyPath, $propertyValue); - } - } - } - } - - private function getPropertyValue($data, $propertyPath) - { - try { - return $this->propertyAccessor->getValue($data, $propertyPath); - } catch (AccessException $e) { - if (\is_array($data) && $e instanceof NoSuchIndexException) { - return null; - } - - if (!$e instanceof UninitializedPropertyException - // For versions without UninitializedPropertyException check the exception message - && (class_exists(UninitializedPropertyException::class) || !str_contains($e->getMessage(), 'You should initialize it')) - ) { - throw $e; - } - - return null; - } - } -} diff --git a/Extension/Core/DataMapper/RadioListMapper.php b/Extension/Core/DataMapper/RadioListMapper.php index b54adfa5d1..5313473cb9 100644 --- a/Extension/Core/DataMapper/RadioListMapper.php +++ b/Extension/Core/DataMapper/RadioListMapper.php @@ -25,15 +25,8 @@ */ class RadioListMapper implements DataMapperInterface { - /** - * {@inheritdoc} - */ - public function mapDataToForms($choice, iterable $radios) + public function mapDataToForms(mixed $choice, \Traversable $radios): void { - if (\is_array($radios)) { - trigger_deprecation('symfony/form', '5.3', 'Passing an array as the second argument of the "%s()" method is deprecated, pass "\Traversable" instead.', __METHOD__); - } - if (!\is_string($choice)) { throw new UnexpectedTypeException($choice, 'string'); } @@ -44,15 +37,8 @@ public function mapDataToForms($choice, iterable $radios) } } - /** - * {@inheritdoc} - */ - public function mapFormsToData(iterable $radios, &$choice) + public function mapFormsToData(\Traversable $radios, mixed &$choice): void { - if (\is_array($radios)) { - trigger_deprecation('symfony/form', '5.3', 'Passing an array as the first argument of the "%s()" method is deprecated, pass "\Traversable" instead.', __METHOD__); - } - if (null !== $choice && !\is_string($choice)) { throw new UnexpectedTypeException($choice, 'null or string'); } diff --git a/Extension/Core/DataTransformer/ArrayToPartsTransformer.php b/Extension/Core/DataTransformer/ArrayToPartsTransformer.php index f79920971c..92f59c906e 100644 --- a/Extension/Core/DataTransformer/ArrayToPartsTransformer.php +++ b/Extension/Core/DataTransformer/ArrayToPartsTransformer.php @@ -16,30 +16,26 @@ /** * @author Bernhard Schussek + * + * @implements DataTransformerInterface */ class ArrayToPartsTransformer implements DataTransformerInterface { - private $partMapping; - - public function __construct(array $partMapping) - { - $this->partMapping = $partMapping; + public function __construct( + private array $partMapping, + ) { } - public function transform($array) + public function transform(mixed $array): mixed { - if (null === $array) { - $array = []; - } - - if (!\is_array($array)) { + if (!\is_array($array ??= [])) { throw new TransformationFailedException('Expected an array.'); } $result = []; foreach ($this->partMapping as $partKey => $originalKeys) { - if (empty($array)) { + if (!$array) { $result[$partKey] = null; } else { $result[$partKey] = array_intersect_key($array, array_flip($originalKeys)); @@ -49,7 +45,7 @@ public function transform($array) return $result; } - public function reverseTransform($array) + public function reverseTransform(mixed $array): mixed { if (!\is_array($array)) { throw new TransformationFailedException('Expected an array.'); @@ -76,7 +72,7 @@ public function reverseTransform($array) 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 1c56d179f9..ca85fd44d0 100644 --- a/Extension/Core/DataTransformer/BaseDateTimeTransformer.php +++ b/Extension/Core/DataTransformer/BaseDateTimeTransformer.php @@ -14,9 +14,14 @@ use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\InvalidArgumentException; +/** + * @template TTransformedValue + * + * @implements DataTransformerInterface<\DateTimeInterface, TTransformedValue> + */ abstract class BaseDateTimeTransformer implements DataTransformerInterface { - protected static $formats = [ + protected static array $formats = [ \IntlDateFormatter::NONE, \IntlDateFormatter::FULL, \IntlDateFormatter::LONG, @@ -24,9 +29,8 @@ abstract class BaseDateTimeTransformer implements DataTransformerInterface \IntlDateFormatter::SHORT, ]; - protected $inputTimezone; - - protected $outputTimezone; + protected string $inputTimezone; + protected string $outputTimezone; /** * @param string|null $inputTimezone The name of the input timezone @@ -43,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 b2d5745992..7ef84bb448 100644 --- a/Extension/Core/DataTransformer/BooleanToStringTransformer.php +++ b/Extension/Core/DataTransformer/BooleanToStringTransformer.php @@ -20,20 +20,18 @@ * * @author Bernhard Schussek * @author Florian Eckerstorfer + * + * @implements DataTransformerInterface */ class BooleanToStringTransformer implements DataTransformerInterface { - private $trueValue; - - private $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.'); } @@ -44,11 +42,9 @@ public function __construct(string $trueValue, array $falseValues = [null]) * * @param bool $value Boolean value * - * @return string|null - * * @throws TransformationFailedException if the given value is not a Boolean */ - public function transform($value) + public function transform(mixed $value): ?string { if (null === $value) { return null; @@ -66,11 +62,9 @@ public function transform($value) * * @param string $value String value * - * @return bool - * * @throws TransformationFailedException if the given value is not a string */ - public function reverseTransform($value) + public function reverseTransform(mixed $value): bool { if (\in_array($value, $this->falseValues, true)) { return false; diff --git a/Extension/Core/DataTransformer/ChoiceToValueTransformer.php b/Extension/Core/DataTransformer/ChoiceToValueTransformer.php index e1feafe62e..e75bc31dee 100644 --- a/Extension/Core/DataTransformer/ChoiceToValueTransformer.php +++ b/Extension/Core/DataTransformer/ChoiceToValueTransformer.php @@ -17,22 +17,22 @@ /** * @author Bernhard Schussek + * + * @implements DataTransformerInterface */ class ChoiceToValueTransformer implements DataTransformerInterface { - private $choiceList; - - public function __construct(ChoiceListInterface $choiceList) - { - $this->choiceList = $choiceList; + public function __construct( + private ChoiceListInterface $choiceList, + ) { } - public function transform($choice) + public function transform(mixed $choice): mixed { return (string) current($this->choiceList->getValuesForChoices([$choice])); } - public function reverseTransform($value) + public function reverseTransform(mixed $value): mixed { if (null !== $value && !\is_string($value)) { throw new TransformationFailedException('Expected a string or null.'); @@ -45,7 +45,7 @@ public function reverseTransform($value) 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 6c9e4fe9dc..aa22338549 100644 --- a/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php +++ b/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php @@ -17,22 +17,20 @@ /** * @author Bernhard Schussek + * + * @implements DataTransformerInterface */ class ChoicesToValuesTransformer implements DataTransformerInterface { - private $choiceList; - - public function __construct(ChoiceListInterface $choiceList) - { - $this->choiceList = $choiceList; + public function __construct( + private ChoiceListInterface $choiceList, + ) { } /** - * @return array - * * @throws TransformationFailedException if the given value is not an array */ - public function transform($array) + public function transform(mixed $array): array { if (null === $array) { return []; @@ -46,13 +44,11 @@ public function transform($array) } /** - * @return array - * * @throws TransformationFailedException if the given value is not an array * or if no matching choice could be * found for some given value */ - public function reverseTransform($array) + public function reverseTransform(mixed $array): array { if (null === $array) { return []; diff --git a/Extension/Core/DataTransformer/DataTransformerChain.php b/Extension/Core/DataTransformer/DataTransformerChain.php index e3107a889c..e34be74556 100644 --- a/Extension/Core/DataTransformer/DataTransformerChain.php +++ b/Extension/Core/DataTransformer/DataTransformerChain.php @@ -21,16 +21,14 @@ */ class DataTransformerChain implements DataTransformerInterface { - protected $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, + ) { } /** @@ -43,11 +41,9 @@ public function __construct(array $transformers) * * @param mixed $value The original value * - * @return mixed - * * @throws TransformationFailedException */ - public function transform($value) + public function transform(mixed $value): mixed { foreach ($this->transformers as $transformer) { $value = $transformer->transform($value); @@ -67,11 +63,9 @@ public function transform($value) * * @param mixed $value The transformed value * - * @return mixed - * * @throws TransformationFailedException */ - public function reverseTransform($value) + public function reverseTransform(mixed $value): mixed { for ($i = \count($this->transformers) - 1; $i >= 0; --$i) { $value = $this->transformers[$i]->reverseTransform($value); @@ -83,7 +77,7 @@ public function reverseTransform($value) /** * @return DataTransformerInterface[] */ - public function getTransformers() + public function getTransformers(): array { return $this->transformers; } diff --git a/Extension/Core/DataTransformer/DateIntervalToArrayTransformer.php b/Extension/Core/DataTransformer/DateIntervalToArrayTransformer.php index f11fd9011f..8591db1910 100644 --- a/Extension/Core/DataTransformer/DateIntervalToArrayTransformer.php +++ b/Extension/Core/DataTransformer/DateIntervalToArrayTransformer.php @@ -19,6 +19,8 @@ * Transforms between a normalized date interval and an interval string/array. * * @author Steffen Roßkamp + * + * @implements DataTransformerInterface<\DateInterval, array> */ class DateIntervalToArrayTransformer implements DataTransformerInterface { @@ -39,17 +41,17 @@ class DateIntervalToArrayTransformer implements DataTransformerInterface self::SECONDS => 's', self::INVERT => 'r', ]; - private $fields; - private $pad; + private array $fields; /** * @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; } /** @@ -57,11 +59,9 @@ public function __construct(?array $fields = null, bool $pad = false) * * @param \DateInterval $dateInterval Normalized date interval * - * @return array - * * @throws UnexpectedTypeException if the given value is not a \DateInterval instance */ - public function transform($dateInterval) + public function transform(mixed $dateInterval): array { if (null === $dateInterval) { return array_intersect_key( @@ -93,9 +93,8 @@ public function transform($dateInterval) } } $result['invert'] = '-' === $result['invert']; - $result = array_intersect_key($result, array_flip($this->fields)); - return $result; + return array_intersect_key($result, array_flip($this->fields)); } /** @@ -103,12 +102,10 @@ public function transform($dateInterval) * * @param array $value Interval array * - * @return \DateInterval|null - * * @throws UnexpectedTypeException if the given value is not an array * @throws TransformationFailedException if the value could not be transformed */ - public function reverseTransform($value) + public function reverseTransform(mixed $value): ?\DateInterval { if (null === $value) { return null; @@ -126,29 +123,29 @@ public function reverseTransform($value) } } 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 723c344f93..527836bf55 100644 --- a/Extension/Core/DataTransformer/DateIntervalToStringTransformer.php +++ b/Extension/Core/DataTransformer/DateIntervalToStringTransformer.php @@ -19,11 +19,11 @@ * Transforms between a date string and a DateInterval object. * * @author Steffen Roßkamp + * + * @implements DataTransformerInterface<\DateInterval, string> */ class DateIntervalToStringTransformer implements DataTransformerInterface { - private $format; - /** * Transforms a \DateInterval instance to a string. * @@ -31,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', + ) { } /** @@ -41,11 +41,9 @@ public function __construct(string $format = 'P%yY%mM%dDT%hH%iM%sS') * * @param \DateInterval|null $value A DateInterval object * - * @return string - * * @throws UnexpectedTypeException if the given value is not a \DateInterval instance */ - public function transform($value) + public function transform(mixed $value): string { if (null === $value) { return ''; @@ -62,12 +60,10 @@ public function transform($value) * * @param string $value An ISO 8601 or date string like date interval presentation * - * @return \DateInterval|null - * * @throws UnexpectedTypeException if the given value is not a string * @throws TransformationFailedException if the date interval could not be parsed */ - public function reverseTransform($value) + public function reverseTransform(mixed $value): ?\DateInterval { if (null === $value) { return null; @@ -83,7 +79,7 @@ public function reverseTransform($value) } $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/DateTimeImmutableToDateTimeTransformer.php b/Extension/Core/DataTransformer/DateTimeImmutableToDateTimeTransformer.php index 6eb40af9d8..3f285b4a3d 100644 --- a/Extension/Core/DataTransformer/DateTimeImmutableToDateTimeTransformer.php +++ b/Extension/Core/DataTransformer/DateTimeImmutableToDateTimeTransformer.php @@ -18,6 +18,8 @@ * Transforms between a DateTimeImmutable object and a DateTime object. * * @author Valentin Udaltsov + * + * @implements DataTransformerInterface<\DateTimeImmutable, \DateTime> */ final class DateTimeImmutableToDateTimeTransformer implements DataTransformerInterface { @@ -28,7 +30,7 @@ final class DateTimeImmutableToDateTimeTransformer implements DataTransformerInt * * @throws TransformationFailedException If the given value is not a \DateTimeImmutable */ - public function transform($value): ?\DateTime + public function transform(mixed $value): ?\DateTime { if (null === $value) { return null; @@ -38,11 +40,7 @@ public function transform($value): ?\DateTime throw new TransformationFailedException('Expected a \DateTimeImmutable.'); } - if (\PHP_VERSION_ID >= 70300) { - return \DateTime::createFromImmutable($value); - } - - return \DateTime::createFromFormat('U.u', $value->format('U.u'))->setTimezone($value->getTimezone()); + return \DateTime::createFromImmutable($value); } /** @@ -52,7 +50,7 @@ public function transform($value): ?\DateTime * * @throws TransformationFailedException If the given value is not a \DateTime */ - public function reverseTransform($value): ?\DateTimeImmutable + public function reverseTransform(mixed $value): ?\DateTimeImmutable { if (null === $value) { return null; diff --git a/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php b/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php index 7cc9b96469..06d24cf8e0 100644 --- a/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php +++ b/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php @@ -18,12 +18,13 @@ * * @author Bernhard Schussek * @author Florian Eckerstorfer + * + * @extends BaseDateTimeTransformer */ class DateTimeToArrayTransformer extends BaseDateTimeTransformer { - private $pad; - private $fields; - private $referenceDate; + private array $fields; + private \DateTimeInterface $referenceDate; /** * @param string|null $inputTimezone The input timezone @@ -31,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'); } @@ -45,11 +50,9 @@ public function __construct(?string $inputTimezone = null, ?string $outputTimezo * * @param \DateTimeInterface $dateTime A DateTimeInterface object * - * @return array - * * @throws TransformationFailedException If the given value is not a \DateTimeInterface */ - public function transform($dateTime) + public function transform(mixed $dateTime): array { if (null === $dateTime) { return array_intersect_key([ @@ -67,10 +70,7 @@ public function transform($dateTime) } if ($this->inputTimezone !== $this->outputTimezone) { - if (!$dateTime instanceof \DateTimeImmutable) { - $dateTime = clone $dateTime; - } - + $dateTime = \DateTimeImmutable::createFromInterface($dateTime); $dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone)); } @@ -100,12 +100,10 @@ public function transform($dateTime) * * @param array $value Localized date * - * @return \DateTime|null - * * @throws TransformationFailedException If the given value is not an array, * if the value could not be transformed */ - public function reverseTransform($value) + public function reverseTransform(mixed $value): ?\DateTime { if (null === $value) { return null; @@ -128,7 +126,7 @@ public function reverseTransform($value) } 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'])) { @@ -160,7 +158,7 @@ public function reverseTransform($value) } 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 ebbc76b718..dbeed8e490 100644 --- a/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformer.php +++ b/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformer.php @@ -17,10 +17,18 @@ * @author Franz Wilding * @author Bernhard Schussek * @author Fred Cox + * + * @extends BaseDateTimeTransformer */ class DateTimeToHtml5LocalDateTimeTransformer extends BaseDateTimeTransformer { public const HTML5_FORMAT = 'Y-m-d\\TH:i:s'; + public const HTML5_FORMAT_NO_SECONDS = 'Y-m-d\\TH:i'; + + public function __construct(?string $inputTimezone = null, ?string $outputTimezone = null, private bool $withSeconds = false) + { + parent::__construct($inputTimezone, $outputTimezone); + } /** * Transforms a \DateTime into a local date and time string. @@ -29,32 +37,27 @@ class DateTimeToHtml5LocalDateTimeTransformer extends BaseDateTimeTransformer * input is an RFC3339 date followed by 'T', followed by an RFC3339 time. * https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-local-date-and-time-string * - * @param \DateTime|\DateTimeInterface $dateTime A DateTime object - * - * @return string + * @param \DateTimeInterface $dateTime * * @throws TransformationFailedException If the given value is not an * instance of \DateTime or \DateTimeInterface */ - public function transform($dateTime) + public function transform(mixed $dateTime): string { if (null === $dateTime) { return ''; } - if (!$dateTime instanceof \DateTime && !$dateTime instanceof \DateTimeInterface) { - throw new TransformationFailedException('Expected a \DateTime or \DateTimeInterface.'); + if (!$dateTime instanceof \DateTimeInterface) { + throw new TransformationFailedException('Expected a \DateTimeInterface.'); } if ($this->inputTimezone !== $this->outputTimezone) { - if (!$dateTime instanceof \DateTimeImmutable) { - $dateTime = clone $dateTime; - } - + $dateTime = \DateTimeImmutable::createFromInterface($dateTime); $dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone)); } - return $dateTime->format(self::HTML5_FORMAT); + return $dateTime->format($this->withSeconds ? self::HTML5_FORMAT : self::HTML5_FORMAT_NO_SECONDS); } /** @@ -66,12 +69,10 @@ public function transform($dateTime) * * @param string $dateTimeLocal Formatted string * - * @return \DateTime|null - * * @throws TransformationFailedException If the given value is not a string, * if the value could not be transformed */ - public function reverseTransform($dateTimeLocal) + public function reverseTransform(mixed $dateTimeLocal): ?\DateTime { if (!\is_string($dateTimeLocal)) { throw new TransformationFailedException('Expected a string.'); @@ -84,7 +85,7 @@ public function reverseTransform($dateTimeLocal) // 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 { @@ -98,7 +99,7 @@ public function reverseTransform($dateTimeLocal) } 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 b0b1f187ce..426c4bf89d 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; @@ -19,37 +20,38 @@ * * @author Bernhard Schussek * @author Florian Eckerstorfer + * + * @extends BaseDateTimeTransformer */ class DateTimeToLocalizedStringTransformer extends BaseDateTimeTransformer { - private $dateFormat; - private $timeFormat; - private $pattern; - private $calendar; + private int $dateFormat; + private int $timeFormat; /** * @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); - if (null === $dateFormat) { - $dateFormat = \IntlDateFormatter::MEDIUM; - } - - if (null === $timeFormat) { - $timeFormat = \IntlDateFormatter::SHORT; - } + $dateFormat ??= \IntlDateFormatter::MEDIUM; + $timeFormat ??= \IntlDateFormatter::SHORT; if (!\in_array($dateFormat, self::$formats, true)) { throw new UnexpectedTypeException($dateFormat, implode('", "', self::$formats)); @@ -59,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; } /** @@ -70,12 +74,10 @@ public function __construct(?string $inputTimezone = null, ?string $outputTimezo * * @param \DateTimeInterface $dateTime A DateTimeInterface object * - * @return string - * * @throws TransformationFailedException if the given value is not a \DateTimeInterface * or if the date could not be transformed */ - public function transform($dateTime) + public function transform(mixed $dateTime): string { if (null === $dateTime) { return ''; @@ -97,14 +99,12 @@ public function transform($dateTime) /** * Transforms a localized date string/array into a normalized date. * - * @param string|array $value Localized date string/array - * - * @return \DateTime|null + * @param string $value Localized date string * * @throws TransformationFailedException if the given value is not a string, * if the date could not be parsed */ - public function reverseTransform($value) + public function reverseTransform(mixed $value): ?\DateTime { if (!\is_string($value)) { throw new TransformationFailedException('Expected a string.'); @@ -133,7 +133,7 @@ public function reverseTransform($value) } 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 { @@ -142,7 +142,7 @@ public function reverseTransform($value) $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 @@ -162,12 +162,8 @@ public function reverseTransform($value) * Returns a preconfigured IntlDateFormatter instance. * * @param bool $ignoreTimezone Use UTC regardless of the configured timezone - * - * @return \IntlDateFormatter - * - * @throws TransformationFailedException in case the date formatter cannot be constructed */ - protected function getIntlDateFormatter(bool $ignoreTimezone = false) + protected function getIntlDateFormatter(bool $ignoreTimezone = false): \IntlDateFormatter { $dateFormat = $this->dateFormat; $timeFormat = $this->timeFormat; @@ -177,12 +173,6 @@ protected function getIntlDateFormatter(bool $ignoreTimezone = false) $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; @@ -190,10 +180,8 @@ protected function getIntlDateFormatter(bool $ignoreTimezone = false) /** * Checks if the pattern contains only a date. - * - * @return bool */ - protected function isPatternDateOnly() + protected function isPatternDateOnly(): bool { if (null === $this->pattern) { return false; diff --git a/Extension/Core/DataTransformer/DateTimeToRfc3339Transformer.php b/Extension/Core/DataTransformer/DateTimeToRfc3339Transformer.php index e0cdbcface..d32b3eae2b 100644 --- a/Extension/Core/DataTransformer/DateTimeToRfc3339Transformer.php +++ b/Extension/Core/DataTransformer/DateTimeToRfc3339Transformer.php @@ -15,6 +15,8 @@ /** * @author Bernhard Schussek + * + * @extends BaseDateTimeTransformer */ class DateTimeToRfc3339Transformer extends BaseDateTimeTransformer { @@ -23,11 +25,9 @@ class DateTimeToRfc3339Transformer extends BaseDateTimeTransformer * * @param \DateTimeInterface $dateTime A DateTimeInterface object * - * @return string - * * @throws TransformationFailedException If the given value is not a \DateTimeInterface */ - public function transform($dateTime) + public function transform(mixed $dateTime): string { if (null === $dateTime) { return ''; @@ -38,10 +38,7 @@ public function transform($dateTime) } if ($this->inputTimezone !== $this->outputTimezone) { - if (!$dateTime instanceof \DateTimeImmutable) { - $dateTime = clone $dateTime; - } - + $dateTime = \DateTimeImmutable::createFromInterface($dateTime); $dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone)); } @@ -53,12 +50,10 @@ public function transform($dateTime) * * @param string $rfc3339 Formatted string * - * @return \DateTime|null - * * @throws TransformationFailedException If the given value is not a string, * if the value could not be transformed */ - public function reverseTransform($rfc3339) + public function reverseTransform(mixed $rfc3339): ?\DateTime { if (!\is_string($rfc3339)) { throw new TransformationFailedException('Expected a string.'); @@ -69,7 +64,7 @@ public function reverseTransform($rfc3339) } 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 { @@ -83,7 +78,7 @@ public function reverseTransform($rfc3339) } 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 b4e13eb8fb..1353614208 100644 --- a/Extension/Core/DataTransformer/DateTimeToStringTransformer.php +++ b/Extension/Core/DataTransformer/DateTimeToStringTransformer.php @@ -18,15 +18,15 @@ * * @author Bernhard Schussek * @author Florian Eckerstorfer + * + * @extends BaseDateTimeTransformer */ class DateTimeToStringTransformer extends BaseDateTimeTransformer { /** * Format used for generating strings. - * - * @var string */ - private $generateFormat; + private string $generateFormat; /** * Format used for parsing strings. @@ -34,10 +34,8 @@ class DateTimeToStringTransformer extends BaseDateTimeTransformer * Different than the {@link $generateFormat} because formats for parsing * support additional characters in PHP that are not supported for * generating strings. - * - * @var string */ - private $parseFormat; + private string $parseFormat; /** * Transforms a \DateTime instance to a string. @@ -75,11 +73,9 @@ public function __construct(?string $inputTimezone = null, ?string $outputTimezo * * @param \DateTimeInterface $dateTime A DateTimeInterface object * - * @return string - * * @throws TransformationFailedException If the given value is not a \DateTimeInterface */ - public function transform($dateTime) + public function transform(mixed $dateTime): string { if (null === $dateTime) { return ''; @@ -89,10 +85,7 @@ public function transform($dateTime) throw new TransformationFailedException('Expected a \DateTimeInterface.'); } - if (!$dateTime instanceof \DateTimeImmutable) { - $dateTime = clone $dateTime; - } - + $dateTime = \DateTimeImmutable::createFromInterface($dateTime); $dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone)); return $dateTime->format($this->generateFormat); @@ -103,14 +96,12 @@ public function transform($dateTime) * * @param string $value A value as produced by PHP's date() function * - * @return \DateTime|null - * * @throws TransformationFailedException If the given value is not a string, * or could not be transformed */ - public function reverseTransform($value) + public function reverseTransform(mixed $value): ?\DateTime { - if (empty($value)) { + if (!$value) { return null; } diff --git a/Extension/Core/DataTransformer/DateTimeToTimestampTransformer.php b/Extension/Core/DataTransformer/DateTimeToTimestampTransformer.php index f6c38ba4de..33c1b1d599 100644 --- a/Extension/Core/DataTransformer/DateTimeToTimestampTransformer.php +++ b/Extension/Core/DataTransformer/DateTimeToTimestampTransformer.php @@ -18,6 +18,8 @@ * * @author Bernhard Schussek * @author Florian Eckerstorfer + * + * @extends BaseDateTimeTransformer */ class DateTimeToTimestampTransformer extends BaseDateTimeTransformer { @@ -26,11 +28,9 @@ class DateTimeToTimestampTransformer extends BaseDateTimeTransformer * * @param \DateTimeInterface $dateTime A DateTimeInterface object * - * @return int|null - * * @throws TransformationFailedException If the given value is not a \DateTimeInterface */ - public function transform($dateTime) + public function transform(mixed $dateTime): ?int { if (null === $dateTime) { return null; @@ -48,12 +48,10 @@ public function transform($dateTime) * * @param string $value A timestamp * - * @return \DateTime|null - * * @throws TransformationFailedException If the given value is not a timestamp * or if the given timestamp is invalid */ - public function reverseTransform($value) + public function reverseTransform(mixed $value): ?\DateTime { if (null === $value) { return null; diff --git a/Extension/Core/DataTransformer/DateTimeZoneToStringTransformer.php b/Extension/Core/DataTransformer/DateTimeZoneToStringTransformer.php index 6dfccdfd3d..50767f7a01 100644 --- a/Extension/Core/DataTransformer/DateTimeZoneToStringTransformer.php +++ b/Extension/Core/DataTransformer/DateTimeZoneToStringTransformer.php @@ -18,20 +18,17 @@ * Transforms between a timezone identifier string and a DateTimeZone object. * * @author Roland Franssen + * + * @implements DataTransformerInterface<\DateTimeZone|array<\DateTimeZone>, string|array> */ class DateTimeZoneToStringTransformer implements DataTransformerInterface { - private $multiple; - - public function __construct(bool $multiple = false) - { - $this->multiple = $multiple; + public function __construct( + private bool $multiple = false, + ) { } - /** - * {@inheritdoc} - */ - public function transform($dateTimeZone) + public function transform(mixed $dateTimeZone): mixed { if (null === $dateTimeZone) { return null; @@ -52,10 +49,7 @@ public function transform($dateTimeZone) return $dateTimeZone->getName(); } - /** - * {@inheritdoc} - */ - public function reverseTransform($value) + public function reverseTransform(mixed $value): mixed { if (null === $value) { return null; diff --git a/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php b/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php index 57e3093e62..d83e31b42d 100644 --- a/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php +++ b/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php @@ -33,15 +33,12 @@ public function __construct(?bool $grouping = false, ?int $roundingMode = \Numbe parent::__construct(0, $grouping, $roundingMode, $locale); } - /** - * {@inheritdoc} - */ - public function reverseTransform($value) + 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); @@ -52,7 +49,7 @@ public function reverseTransform($value) /** * @internal */ - protected function castParsedValue($value) + protected function castParsedValue(int|float $value): int|float { return $value; } diff --git a/Extension/Core/DataTransformer/IntlTimeZoneToStringTransformer.php b/Extension/Core/DataTransformer/IntlTimeZoneToStringTransformer.php index aa4629f2ef..19626e49ca 100644 --- a/Extension/Core/DataTransformer/IntlTimeZoneToStringTransformer.php +++ b/Extension/Core/DataTransformer/IntlTimeZoneToStringTransformer.php @@ -18,20 +18,17 @@ * Transforms between a timezone identifier string and a IntlTimeZone object. * * @author Roland Franssen + * + * @implements DataTransformerInterface<\IntlTimeZone|array<\IntlTimeZone>, string|array> */ class IntlTimeZoneToStringTransformer implements DataTransformerInterface { - private $multiple; - - public function __construct(bool $multiple = false) - { - $this->multiple = $multiple; + public function __construct( + private bool $multiple = false, + ) { } - /** - * {@inheritdoc} - */ - public function transform($intlTimeZone) + public function transform(mixed $intlTimeZone): mixed { if (null === $intlTimeZone) { return null; @@ -52,13 +49,10 @@ public function transform($intlTimeZone) return $intlTimeZone->getID(); } - /** - * {@inheritdoc} - */ - public function reverseTransform($value) + public function reverseTransform(mixed $value): mixed { if (null === $value) { - return; + return null; } if ($this->multiple) { @@ -76,7 +70,7 @@ public function reverseTransform($value) $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 5d013a16b9..b03f8da444 100644 --- a/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php +++ b/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php @@ -21,10 +21,16 @@ */ class MoneyToLocalizedStringTransformer extends NumberToLocalizedStringTransformer { - private $divisor; + 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; @@ -35,12 +41,10 @@ public function __construct(?int $scale = 2, ?bool $grouping = true, ?int $round * * @param int|float|null $value Normalized number * - * @return string - * * @throws TransformationFailedException if the given value is not numeric or * if the value cannot be transformed */ - public function transform($value) + public function transform(mixed $value): string { if (null !== $value && 1 !== $this->divisor) { if (!is_numeric($value)) { @@ -57,16 +61,24 @@ public function transform($value) * * @param string $value Localized money string * - * @return int|float|null - * * @throws TransformationFailedException if the given value is not a string * or if the value cannot be transformed */ - public function reverseTransform($value) + 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 653f1c445f..3020dd1483 100644 --- a/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php +++ b/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php @@ -20,57 +20,22 @@ * * @author Bernhard Schussek * @author Florian Eckerstorfer + * + * @implements DataTransformerInterface */ class NumberToLocalizedStringTransformer implements DataTransformerInterface { - /** - * @deprecated since Symfony 5.1, use \NumberFormatter::ROUND_CEILING instead. - */ - public const ROUND_CEILING = \NumberFormatter::ROUND_CEILING; - - /** - * @deprecated since Symfony 5.1, use \NumberFormatter::ROUND_FLOOR instead. - */ - public const ROUND_FLOOR = \NumberFormatter::ROUND_FLOOR; - - /** - * @deprecated since Symfony 5.1, use \NumberFormatter::ROUND_UP instead. - */ - public const ROUND_UP = \NumberFormatter::ROUND_UP; - - /** - * @deprecated since Symfony 5.1, use \NumberFormatter::ROUND_DOWN instead. - */ - public const ROUND_DOWN = \NumberFormatter::ROUND_DOWN; - - /** - * @deprecated since Symfony 5.1, use \NumberFormatter::ROUND_HALFEVEN instead. - */ - public const ROUND_HALF_EVEN = \NumberFormatter::ROUND_HALFEVEN; - - /** - * @deprecated since Symfony 5.1, use \NumberFormatter::ROUND_HALFUP instead. - */ - public const ROUND_HALF_UP = \NumberFormatter::ROUND_HALFUP; - - /** - * @deprecated since Symfony 5.1, use \NumberFormatter::ROUND_HALFDOWN instead. - */ - public const ROUND_HALF_DOWN = \NumberFormatter::ROUND_HALFDOWN; - - protected $grouping; - - protected $roundingMode; - - private $scale; - private $locale; - - public function __construct(?int $scale = null, ?bool $grouping = false, ?int $roundingMode = \NumberFormatter::ROUND_HALFUP, ?string $locale = null) - { - $this->scale = $scale; + protected bool $grouping; + protected int $roundingMode; + + 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; } /** @@ -78,12 +43,10 @@ public function __construct(?int $scale = null, ?bool $grouping = false, ?int $r * * @param int|float|null $value Number value * - * @return string - * * @throws TransformationFailedException if the given value is not numeric * or if the value cannot be transformed */ - public function transform($value) + public function transform(mixed $value): string { if (null === $value) { return ''; @@ -101,9 +64,7 @@ public function transform($value) } // 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); } /** @@ -111,12 +72,10 @@ public function transform($value) * * @param string $value The localized value * - * @return int|float|null - * * @throws TransformationFailedException if the given value is not a string * or if the value cannot be transformed */ - public function reverseTransform($value) + public function reverseTransform(mixed $value): int|float|null { if (null !== $value && !\is_string($value)) { throw new TransformationFailedException('Expected a string.'); @@ -184,7 +143,7 @@ public function reverseTransform($value) $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)); } } @@ -194,10 +153,8 @@ public function reverseTransform($value) /** * Returns a preconfigured \NumberFormatter instance. - * - * @return \NumberFormatter */ - protected function getNumberFormatter() + protected function getNumberFormatter(): \NumberFormatter { $formatter = new \NumberFormatter($this->locale ?? \Locale::getDefault(), \NumberFormatter::DECIMAL); @@ -214,7 +171,7 @@ protected function getNumberFormatter() /** * @internal */ - protected function castParsedValue($value) + protected function castParsedValue(int|float $value): int|float { if (\is_int($value) && $value === (int) $float = (float) $value) { return $float; @@ -225,42 +182,24 @@ protected function castParsedValue($value) /** * Rounds a number according to the configured scale and rounding mode. - * - * @param int|float $number A number - * - * @return int|float */ - private function round($number) + 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 b716e436ee..093c3ebe2c 100644 --- a/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php +++ b/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php @@ -20,39 +20,37 @@ * * @author Bernhard Schussek * @author Florian Eckerstorfer + * + * @implements DataTransformerInterface */ class PercentToLocalizedStringTransformer implements DataTransformerInterface { public const FRACTIONAL = 'fractional'; public const INTEGER = 'integer'; - protected static $types = [ + protected static array $types = [ self::FRACTIONAL, self::INTEGER, ]; - private $roundingMode; - private $type; - private $scale; - private $html5Format; + private string $type; + private int $scale; /** * @see self::$types for a list of supported types * - * @param int|null $roundingMode A value from \NumberFormatter, such as \NumberFormatter::ROUND_HALFUP - * @param bool $html5Format Use an HTML5 specific format, see https://www.w3.org/TR/html51/sec-forms.html#date-time-and-number-formats + * @param int $roundingMode A value from \NumberFormatter, such as \NumberFormatter::ROUND_HALFUP + * @param bool $html5Format Use an HTML5 specific format, see https://www.w3.org/TR/html51/sec-forms.html#date-time-and-number-formats * * @throws UnexpectedTypeException if the given value of type is unknown */ - public function __construct(?int $scale = null, ?string $type = null, ?int $roundingMode = null, bool $html5Format = false) - { - if (null === $type) { - $type = self::FRACTIONAL; - } - - if (null === $roundingMode && (\func_num_args() < 4 || func_get_arg(3))) { - trigger_deprecation('symfony/form', '5.1', 'Not passing a rounding mode to "%s()" is deprecated. Starting with Symfony 6.0 it will default to "\NumberFormatter::ROUND_HALFUP".', __METHOD__); - } + 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)) { throw new UnexpectedTypeException($type, implode('", "', self::$types)); @@ -60,8 +58,6 @@ public function __construct(?int $scale = null, ?string $type = null, ?int $roun $this->type = $type; $this->scale = $scale ?? 0; - $this->roundingMode = $roundingMode; - $this->html5Format = $html5Format; } /** @@ -69,12 +65,10 @@ public function __construct(?int $scale = null, ?string $type = null, ?int $roun * * @param int|float $value Normalized value * - * @return string - * * @throws TransformationFailedException if the given value is not numeric or * if the value could not be transformed */ - public function transform($value) + public function transform(mixed $value): string { if (null === $value) { return ''; @@ -104,12 +98,10 @@ public function transform($value) * * @param string $value Percentage value * - * @return int|float|null - * * @throws TransformationFailedException if the given value is not a string or * if the value could not be transformed */ - public function reverseTransform($value) + public function reverseTransform(mixed $value): int|float|null { if (!\is_string($value)) { throw new TransformationFailedException('Expected a string.'); @@ -170,7 +162,7 @@ public function reverseTransform($value) $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)); } } @@ -179,10 +171,8 @@ public function reverseTransform($value) /** * Returns a preconfigured \NumberFormatter instance. - * - * @return \NumberFormatter */ - protected function getNumberFormatter() + protected function getNumberFormatter(): \NumberFormatter { // Values used in HTML5 number inputs should be formatted as in "1234.5", ie. 'en' format without grouping, // according to https://www.w3.org/TR/html51/sec-forms.html#date-time-and-number-formats @@ -194,60 +184,36 @@ protected function getNumberFormatter() $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; } /** * Rounds a number according to the configured scale and rounding mode. - * - * @param int|float $number A number - * - * @return int|float */ - private function round($number) + 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 e79b669888..f47fcb6254 100644 --- a/Extension/Core/DataTransformer/StringToFloatTransformer.php +++ b/Extension/Core/DataTransformer/StringToFloatTransformer.php @@ -14,21 +14,17 @@ use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\TransformationFailedException; +/** + * @implements DataTransformerInterface + */ class StringToFloatTransformer implements DataTransformerInterface { - private $scale; - - public function __construct(?int $scale = null) - { - $this->scale = $scale; + public function __construct( + private ?int $scale = null, + ) { } - /** - * @param mixed $value - * - * @return float|null - */ - public function transform($value) + public function transform(mixed $value): ?float { if (null === $value) { return null; @@ -41,12 +37,7 @@ public function transform($value) return (float) $value; } - /** - * @param mixed $value - * - * @return string|null - */ - public function reverseTransform($value) + public function reverseTransform(mixed $value): ?string { if (null === $value) { return null; diff --git a/Extension/Core/DataTransformer/UlidToStringTransformer.php b/Extension/Core/DataTransformer/UlidToStringTransformer.php index 33b57db73f..9365cab15e 100644 --- a/Extension/Core/DataTransformer/UlidToStringTransformer.php +++ b/Extension/Core/DataTransformer/UlidToStringTransformer.php @@ -19,6 +19,8 @@ * Transforms between a ULID string and a Ulid object. * * @author Pavel Dyakonov + * + * @implements DataTransformerInterface */ class UlidToStringTransformer implements DataTransformerInterface { @@ -27,11 +29,9 @@ class UlidToStringTransformer implements DataTransformerInterface * * @param Ulid $value A Ulid object * - * @return string|null - * * @throws TransformationFailedException If the given value is not a Ulid object */ - public function transform($value) + public function transform(mixed $value): ?string { if (null === $value) { return null; @@ -49,12 +49,10 @@ public function transform($value) * * @param string $value A ULID string * - * @return Ulid|null - * * @throws TransformationFailedException If the given value is not a string, * or could not be transformed */ - public function reverseTransform($value) + public function reverseTransform(mixed $value): ?Ulid { if (null === $value || '' === $value) { return null; @@ -67,7 +65,7 @@ public function reverseTransform($value) 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 c4775bb12b..43326eb644 100644 --- a/Extension/Core/DataTransformer/UuidToStringTransformer.php +++ b/Extension/Core/DataTransformer/UuidToStringTransformer.php @@ -19,6 +19,8 @@ * Transforms between a UUID string and a Uuid object. * * @author Pavel Dyakonov + * + * @implements DataTransformerInterface */ class UuidToStringTransformer implements DataTransformerInterface { @@ -27,11 +29,9 @@ class UuidToStringTransformer implements DataTransformerInterface * * @param Uuid $value A Uuid object * - * @return string|null - * * @throws TransformationFailedException If the given value is not a Uuid object */ - public function transform($value) + public function transform(mixed $value): ?string { if (null === $value) { return null; @@ -49,12 +49,10 @@ public function transform($value) * * @param string $value A UUID string * - * @return Uuid|null - * * @throws TransformationFailedException If the given value is not a string, * or could not be transformed */ - public function reverseTransform($value) + public function reverseTransform(mixed $value): ?Uuid { if (null === $value || '' === $value) { return null; @@ -65,13 +63,13 @@ public function reverseTransform($value) } 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 8dd5acb616..6f8ea4fbe6 100644 --- a/Extension/Core/DataTransformer/ValueToDuplicatesTransformer.php +++ b/Extension/Core/DataTransformer/ValueToDuplicatesTransformer.php @@ -16,24 +16,20 @@ /** * @author Bernhard Schussek + * + * @implements DataTransformerInterface */ class ValueToDuplicatesTransformer implements DataTransformerInterface { - private $keys; - - public function __construct(array $keys) - { - $this->keys = $keys; + public function __construct( + private array $keys, + ) { } /** * Duplicates the given value through the array. - * - * @param mixed $value The value - * - * @return array */ - public function transform($value) + public function transform(mixed $value): array { $result = []; @@ -47,12 +43,10 @@ public function transform($value) /** * Extracts the duplicated value from an array. * - * @return mixed - * * @throws TransformationFailedException if the given value is not an array or * if the given array cannot be transformed */ - public function reverseTransform($array) + public function reverseTransform(mixed $array): mixed { if (!\is_array($array)) { throw new TransformationFailedException('Expected an array.'); @@ -77,7 +71,7 @@ public function reverseTransform($array) 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 d7349bbc9d..448ae4278b 100644 --- a/Extension/Core/DataTransformer/WeekToArrayTransformer.php +++ b/Extension/Core/DataTransformer/WeekToArrayTransformer.php @@ -18,6 +18,8 @@ * Transforms between an ISO 8601 week date string and an array. * * @author Damien Fayet + * + * @implements DataTransformerInterface */ class WeekToArrayTransformer implements DataTransformerInterface { @@ -31,14 +33,14 @@ class WeekToArrayTransformer implements DataTransformerInterface * @throws TransformationFailedException If the given value is not a string, * or if the given value does not follow the right format */ - public function transform($value) + public function transform(mixed $value): array { if (null === $value) { return ['year' => null, 'week' => null]; } 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)) { @@ -61,14 +63,14 @@ public function transform($value) * @throws TransformationFailedException If the given value cannot be merged in a valid week date string, * or if the obtained week date does not exists */ - public function reverseTransform($value) + public function reverseTransform(mixed $value): ?string { if (null === $value || [] === $value) { return null; } 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)) { @@ -80,7 +82,7 @@ public function reverseTransform($value) } 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']) { @@ -88,18 +90,18 @@ public function reverseTransform($value) } 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 c497212289..97cd3ad474 100644 --- a/Extension/Core/EventListener/FixUrlProtocolListener.php +++ b/Extension/Core/EventListener/FixUrlProtocolListener.php @@ -22,17 +22,15 @@ */ class FixUrlProtocolListener implements EventSubscriberInterface { - private $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) + public function onSubmit(FormEvent $event): void { $data = $event->getData(); @@ -41,7 +39,7 @@ public function onSubmit(FormEvent $event) } } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [FormEvents::SUBMIT => 'onSubmit']; } diff --git a/Extension/Core/EventListener/MergeCollectionListener.php b/Extension/Core/EventListener/MergeCollectionListener.php index cd4a3b5430..61428d5dfa 100644 --- a/Extension/Core/EventListener/MergeCollectionListener.php +++ b/Extension/Core/EventListener/MergeCollectionListener.php @@ -21,34 +21,27 @@ */ class MergeCollectionListener implements EventSubscriberInterface { - private $allowAdd; - private $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() + public static function getSubscribedEvents(): array { return [ FormEvents::SUBMIT => 'onSubmit', ]; } - public function onSubmit(FormEvent $event) + public function onSubmit(FormEvent $event): void { $dataToMergeInto = $event->getForm()->getNormData(); - $data = $event->getData(); - - if (null === $data) { - $data = []; - } + $data = $event->getData() ?? []; if (!\is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) { throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)'); diff --git a/Extension/Core/EventListener/ResizeFormListener.php b/Extension/Core/EventListener/ResizeFormListener.php index 813456b95e..299f919373 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,46 +25,83 @@ */ class ResizeFormListener implements EventSubscriberInterface { - protected $type; - protected $options; - protected $allowAdd; - protected $allowDelete; - - private $deleteEmpty; - - /** - * @param bool $allowAdd Whether children could be added to the group - * @param bool $allowDelete Whether children could be removed from the group - * @param bool|callable $deleteEmpty - */ - public function __construct(string $type, array $options = [], bool $allowAdd = false, bool $allowDelete = false, $deleteEmpty = false) - { - $this->type = $type; - $this->allowAdd = $allowAdd; - $this->allowDelete = $allowDelete; - $this->options = $options; - $this->deleteEmpty = $deleteEmpty; + protected array $prototypeOptions; + + private \Closure|bool $deleteEmpty; + // 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; } - public static function getSubscribedEvents() + 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], ]; } - public function preSetData(FormEvent $event) + /** + * @deprecated Since Symfony 7.2, use {@see postSetData()} instead. + */ + public function preSetData(FormEvent $event): void { - $form = $event->getForm(); - $data = $event->getData(); + 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; + } - if (null === $data) { - $data = []; + 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() ?? []; + if (!\is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) { throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)'); } @@ -81,7 +119,7 @@ public function preSetData(FormEvent $event) } } - public function preSubmit(FormEvent $event) + public function preSubmit(FormEvent $event): void { $form = $event->getForm(); $data = $event->getData(); @@ -105,25 +143,21 @@ public function preSubmit(FormEvent $event) if (!$form->has($name)) { $form->add($name, $this->type, array_replace([ 'property_path' => '['.$name.']', - ], $this->options)); + ], $this->prototypeOptions)); } } } } - public function onSubmit(FormEvent $event) + public function onSubmit(FormEvent $event): void { $form = $event->getForm(); - $data = $event->getData(); + $data = $event->getData() ?? []; // At this point, $data is an array or an array-like object that already contains the // new entries, which were added by the data mapper. The data mapper ignores existing // entries, so we need to manually unset removed entries in the collection. - if (null === $data) { - $data = []; - } - if (!\is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) { throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)'); } @@ -164,6 +198,20 @@ public function onSubmit(FormEvent $event) } } + 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 fb8f0c0b5f..48f0b9ac9f 100644 --- a/Extension/Core/EventListener/TransformationFailureListener.php +++ b/Extension/Core/EventListener/TransformationFailureListener.php @@ -22,21 +22,19 @@ */ class TransformationFailureListener implements EventSubscriberInterface { - private $translator; - - public function __construct(?TranslatorInterface $translator = null) - { - $this->translator = $translator; + public function __construct( + private ?TranslatorInterface $translator = null, + ) { } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ FormEvents::POST_SUBMIT => ['convertTransformationFailureToFormError', -1024], ]; } - public function convertTransformationFailureToFormError(FormEvent $event) + public function convertTransformationFailureToFormError(FormEvent $event): void { $form = $event->getForm(); diff --git a/Extension/Core/EventListener/TrimListener.php b/Extension/Core/EventListener/TrimListener.php index be8c38a854..11151d3999 100644 --- a/Extension/Core/EventListener/TrimListener.php +++ b/Extension/Core/EventListener/TrimListener.php @@ -23,7 +23,7 @@ */ class TrimListener implements EventSubscriberInterface { - public function preSubmit(FormEvent $event) + public function preSubmit(FormEvent $event): void { $data = $event->getData(); @@ -34,7 +34,7 @@ public function preSubmit(FormEvent $event) $event->setData(StringUtil::trim($data)); } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [FormEvents::PRE_SUBMIT => 'preSubmit']; } diff --git a/Extension/Core/Type/BaseType.php b/Extension/Core/Type/BaseType.php index 55efb652da..a7f745d78a 100644 --- a/Extension/Core/Type/BaseType.php +++ b/Extension/Core/Type/BaseType.php @@ -29,19 +29,13 @@ */ abstract class BaseType extends AbstractType { - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->setDisabled($options['disabled']); $builder->setAutoInitialize($options['auto_initialize']); } - /** - * {@inheritdoc} - */ - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { $name = $form->getName(); $blockName = $options['block_name'] ?: $form->getName(); @@ -52,18 +46,16 @@ 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; $uniqueBlockPrefix = '_'.$blockName; } - if (null === $translationDomain) { - $translationDomain = $view->parent->vars['translation_domain']; - } + $translationDomain ??= $view->parent->vars['translation_domain']; $labelTranslationParameters = array_merge($view->parent->vars['label_translation_parameters'], $labelTranslationParameters); $attrTranslationParameters = array_merge($view->parent->vars['attr_translation_parameters'], $attrTranslationParameters); @@ -127,10 +119,7 @@ public function buildView(FormView $view, FormInterface $form, array $options) ]); } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'block_name' => null, diff --git a/Extension/Core/Type/BirthdayType.php b/Extension/Core/Type/BirthdayType.php index 50d8b1e210..651b880d86 100644 --- a/Extension/Core/Type/BirthdayType.php +++ b/Extension/Core/Type/BirthdayType.php @@ -12,40 +12,26 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; -use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class BirthdayType extends AbstractType { - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'years' => range((int) date('Y') - 120, date('Y')), - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please enter a valid birthdate.'; - }, + 'invalid_message' => 'Please enter a valid birthdate.', ]); $resolver->setAllowedTypes('years', 'array'); } - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?string { return DateType::class; } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'birthday'; } diff --git a/Extension/Core/Type/ButtonType.php b/Extension/Core/Type/ButtonType.php index ba2f8dfe0e..0ac0585797 100644 --- a/Extension/Core/Type/ButtonType.php +++ b/Extension/Core/Type/ButtonType.php @@ -21,26 +21,17 @@ */ class ButtonType extends BaseType implements ButtonTypeInterface { - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?string { return null; } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'button'; } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { parent::configureOptions($resolver); diff --git a/Extension/Core/Type/CheckboxType.php b/Extension/Core/Type/CheckboxType.php index 6de35b9d48..2f21279cad 100644 --- a/Extension/Core/Type/CheckboxType.php +++ b/Extension/Core/Type/CheckboxType.php @@ -16,15 +16,11 @@ 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 CheckboxType extends AbstractType { - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { // Unlike in other types, where the data is NULL by default, it // needs to be a Boolean here. setData(null) is not acceptable @@ -36,10 +32,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) $builder->addViewTransformer(new BooleanToStringTransformer($options['value'], $options['false_values'])); } - /** - * {@inheritdoc} - */ - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { $view->vars = array_replace($view->vars, [ 'value' => $options['value'], @@ -47,37 +40,23 @@ public function buildView(FormView $view, FormInterface $form, array $options) ]); } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { - $emptyData = function (FormInterface $form, $viewData) { - return $viewData; - }; + $emptyData = static fn (FormInterface $form, $viewData) => $viewData; $resolver->setDefaults([ 'value' => '1', 'empty_data' => $emptyData, 'compound' => false, 'false_values' => [null], - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'The checkbox has an invalid value.'; - }, - 'is_empty_callback' => static function ($modelData): bool { - return false === $modelData; - }, + 'invalid_message' => 'The checkbox has an invalid value.', + 'is_empty_callback' => static fn ($modelData): bool => false === $modelData, ]); $resolver->setAllowedTypes('false_values', 'array'); } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'checkbox'; } diff --git a/Extension/Core/Type/ChoiceType.php b/Extension/Core/Type/ChoiceType.php index 4a61c7f1b3..fc083ee40d 100644 --- a/Extension/Core/Type/ChoiceType.php +++ b/Extension/Core/Type/ChoiceType.php @@ -27,9 +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; @@ -49,41 +52,20 @@ class ChoiceType extends AbstractType { - private $choiceListFactory; - private $translator; + private ChoiceListFactoryInterface $choiceListFactory; - /** - * @param TranslatorInterface $translator - */ - public function __construct(?ChoiceListFactoryInterface $choiceListFactory = null, $translator = null) - { + public function __construct( + ?ChoiceListFactoryInterface $choiceListFactory = null, + private ?TranslatorInterface $translator = null, + ) { $this->choiceListFactory = $choiceListFactory ?? new CachingFactoryDecorator( new PropertyAccessDecorator( new DefaultChoiceListFactory() ) ); - - if (null !== $translator && !$translator instanceof TranslatorInterface) { - throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be han instance of "%s", "%s" given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator))); - } - $this->translator = $translator; - - // BC, to be removed in 6.0 - if ($this->choiceListFactory instanceof CachingFactoryDecorator) { - return; - } - - $ref = new \ReflectionMethod($this->choiceListFactory, 'createListFromChoices'); - - if ($ref->getNumberOfParameters() < 3) { - trigger_deprecation('symfony/form', '5.1', 'Not defining a third parameter "callable|null $filter" in "%s::%s()" is deprecated.', $ref->class, $ref->name); - } } - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $unknownValues = []; $choiceList = $this->createChoiceList($options); @@ -105,7 +87,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) // Check if the choices already contain the empty value // Only add the placeholder option if this is not the case if (null !== $options['placeholder'] && 0 === \count($choiceList->getChoicesForValues(['']))) { - $placeholderView = new ChoiceView(null, '', $options['placeholder']); + $placeholderView = new ChoiceView(null, '', $options['placeholder'], $options['placeholder_attr']); // "placeholder" is a reserved name $this->addSubForm($builder, 'placeholder', $placeholderView, $options); @@ -118,7 +100,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) if ($options['expanded'] || $options['multiple']) { // Make sure that scalar, submitted values are converted to arrays // which can be submitted to the checkboxes/radio buttons - $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($choiceList, $options, &$unknownValues) { + $builder->addEventListener(FormEvents::PRE_SUBMIT, static function (FormEvent $event) use ($choiceList, $options, &$unknownValues) { + /** @var PreSubmitEvent $event */ $form = $event->getForm(); $data = $event->getData(); @@ -160,9 +143,9 @@ public function buildForm(FormBuilderInterface $builder, array $options) $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) { @@ -177,7 +160,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) // 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); @@ -186,21 +169,22 @@ public function buildForm(FormBuilderInterface $builder, array $options) if ($options['multiple']) { $messageTemplate = $options['invalid_message'] ?? 'The value {{ value }} is not valid.'; + $translator = $this->translator; - $builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use (&$unknownValues, $messageTemplate) { + $builder->addEventListener(FormEvents::POST_SUBMIT, static function (FormEvent $event) use (&$unknownValues, $messageTemplate, $translator) { // Throw exception if unknown values were submitted if (\count($unknownValues) > 0) { $form = $event->getForm(); $clientDataAsString = \is_scalar($form->getViewData()) ? (string) $form->getViewData() : (\is_array($form->getViewData()) ? implode('", "', array_keys($unknownValues)) : \gettype($form->getViewData())); - if (null !== $this->translator) { - $message = $this->translator->trans($messageTemplate, ['{{ value }}' => $clientDataAsString], 'validators'); + if ($translator) { + $message = $translator->trans($messageTemplate, ['{{ value }}' => $clientDataAsString], 'validators'); } else { $message = strtr($messageTemplate, ['{{ value }}' => $clientDataAsString]); } - $form->addError(new FormError($message, $messageTemplate, ['{{ value }}' => $clientDataAsString], null, new TransformationFailedException(sprintf('The choices "%s" do not exist in the choice list.', $clientDataAsString)))); + $form->addError(new FormError($message, $messageTemplate, ['{{ value }}' => $clientDataAsString], null, new TransformationFailedException(\sprintf('The choices "%s" do not exist in the choice list.', $clientDataAsString)))); } }); @@ -219,7 +203,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) // To avoid issues when the submitted choices are arrays (i.e. array to string conversions), // we have to ensure that all elements of the submitted choice data are NULL, strings or ints. - $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { + $builder->addEventListener(FormEvents::PRE_SUBMIT, static function (FormEvent $event) { $data = $event->getData(); if (!\is_array($data)) { @@ -234,10 +218,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) }, 256); } - /** - * {@inheritdoc} - */ - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { $choiceTranslationDomain = $options['choice_translation_domain']; if ($view->parent && null === $choiceTranslationDomain) { @@ -257,8 +238,10 @@ 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, 'choice_translation_parameters' => $options['choice_translation_parameters'], ]); @@ -268,13 +251,9 @@ public function buildView(FormView $view, FormInterface $form, array $options) // closure here that is optimized for the value of the form, to // avoid making the type check inside the closure. if ($options['multiple']) { - $view->vars['is_selected'] = function ($choice, array $values) { - return \in_array($choice, $values, true); - }; + $view->vars['is_selected'] = static fn ($choice, array $values) => \in_array($choice, $values, true); } else { - $view->vars['is_selected'] = function ($choice, $value) { - return $choice === $value; - }; + $view->vars['is_selected'] = static fn ($choice, $value) => $choice === $value; } // Check if the choices already contain the empty value @@ -283,6 +262,7 @@ public function buildView(FormView $view, FormInterface $form, array $options) // Only add the empty value option if this is not the case if (null !== $options['placeholder'] && !$view->vars['placeholder_in_choices']) { $view->vars['placeholder'] = $options['placeholder']; + $view->vars['placeholder_attr'] = $options['placeholder_attr']; } if ($options['multiple'] && !$options['expanded']) { @@ -293,11 +273,10 @@ public function buildView(FormView $view, FormInterface $form, array $options) } } - /** - * {@inheritdoc} - */ - public function finishView(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']; @@ -313,12 +292,9 @@ public function finishView(FormView $view, FormInterface $form, array $options) } } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { - $emptyData = function (Options $options) { + $emptyData = static function (Options $options) { if ($options['expanded'] && !$options['multiple']) { return null; } @@ -330,11 +306,9 @@ public function configureOptions(OptionsResolver $resolver) return ''; }; - $placeholderDefault = function (Options $options) { - return $options['required'] ? null : ''; - }; + $placeholderDefault = static fn (Options $options) => $options['required'] ? null : ''; - $placeholderNormalizer = function (Options $options, $placeholder) { + $placeholderNormalizer = static function (Options $options, $placeholder) { if ($options['multiple']) { // never use an empty value for this case return null; @@ -353,11 +327,9 @@ public function configureOptions(OptionsResolver $resolver) return $placeholder; }; - $compound = function (Options $options) { - return $options['expanded']; - }; + $compound = static fn (Options $options) => $options['expanded']; - $choiceTranslationDomainNormalizer = function (Options $options, $choiceTranslationDomain) { + $choiceTranslationDomainNormalizer = static function (Options $options, $choiceTranslationDomain) { if (true === $choiceTranslationDomain) { return $options['translation_domain']; } @@ -365,11 +337,24 @@ public function configureOptions(OptionsResolver $resolver) 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, @@ -377,9 +362,13 @@ public function configureOptions(OptionsResolver $resolver) 'choice_attr' => null, 'choice_translation_parameters' => [], 'preferred_choices' => [], + 'separator' => '-------------------', + 'separator_html' => false, + 'duplicate_preferred_choices' => true, 'group_by' => null, 'empty_data' => $emptyData, 'placeholder' => $placeholderDefault, + 'placeholder_attr' => [], 'error_bubbling' => false, 'compound' => $compound, // The view data is always a string or an array of strings, @@ -388,18 +377,16 @@ public function configureOptions(OptionsResolver $resolver) 'data_class' => null, 'choice_translation_domain' => true, 'trim' => false, - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'The selected choice is invalid.'; - }, + 'invalid_message' => 'The selected choice is invalid.', ]); $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]); @@ -407,14 +394,17 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setAllowedTypes('choice_value', ['null', 'callable', 'string', PropertyPath::class, ChoiceValue::class]); $resolver->setAllowedTypes('choice_attr', ['null', 'array', 'callable', 'string', PropertyPath::class, ChoiceAttr::class]); $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.'); } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'choice'; } @@ -422,7 +412,7 @@ public function getBlockPrefix() /** * Adds the sub fields for an expanded choice field. */ - private function addSubForms(FormBuilderInterface $builder, array $choiceViews, array $options) + private function addSubForms(FormBuilderInterface $builder, array $choiceViews, array $options): void { foreach ($choiceViews as $name => $choiceView) { // Flatten groups @@ -440,7 +430,7 @@ private function addSubForms(FormBuilderInterface $builder, array $choiceViews, } } - private function addSubForm(FormBuilderInterface $builder, string $name, ChoiceView $choiceView, array $options) + private function addSubForm(FormBuilderInterface $builder, string $name, ChoiceView $choiceView, array $options): void { $choiceOpts = [ 'value' => $choiceView->value, @@ -464,7 +454,7 @@ private function addSubForm(FormBuilderInterface $builder, string $name, ChoiceV $builder->add($name, $choiceType, $choiceOpts); } - private function createChoiceList(array $options) + private function createChoiceList(array $options): ChoiceListInterface { if (null !== $options['choice_loader']) { return $this->choiceListFactory->createListFromLoader( @@ -475,7 +465,7 @@ private function createChoiceList(array $options) } // Harden against NULL values (like in EntityType and ModelType) - $choices = null !== $options['choices'] ? $options['choices'] : []; + $choices = $options['choices'] ?? []; return $this->choiceListFactory->createListFromChoices( $choices, @@ -484,7 +474,7 @@ private function createChoiceList(array $options) ); } - private function createChoiceListView(ChoiceListInterface $choiceList, array $options) + private function createChoiceListView(ChoiceListInterface $choiceList, array $options): ChoiceListView { return $this->choiceListFactory->createView( $choiceList, @@ -493,7 +483,8 @@ private function createChoiceListView(ChoiceListInterface $choiceList, array $op $options['choice_name'], $options['group_by'], $options['choice_attr'], - $options['choice_translation_parameters'] + $options['choice_translation_parameters'], + $options['duplicate_preferred_choices'], ); } } diff --git a/Extension/Core/Type/CollectionType.php b/Extension/Core/Type/CollectionType.php index 5cabf16658..3cef931526 100644 --- a/Extension/Core/Type/CollectionType.php +++ b/Extension/Core/Type/CollectionType.php @@ -21,16 +21,15 @@ class CollectionType extends AbstractType { - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { + $resizePrototypeOptions = null; if ($options['allow_add'] && $options['prototype']) { + $resizePrototypeOptions = array_replace($options['entry_options'], $options['prototype_options']); $prototypeOptions = array_replace([ 'required' => $options['required'], 'label' => $options['prototype_name'].'label__', - ], $options['entry_options']); + ], $resizePrototypeOptions); if (null !== $options['prototype_data']) { $prototypeOptions['data'] = $options['prototype_data']; @@ -45,16 +44,15 @@ public function buildForm(FormBuilderInterface $builder, array $options) $options['entry_options'], $options['allow_add'], $options['allow_delete'], - $options['delete_empty'] + $options['delete_empty'], + $resizePrototypeOptions, + $options['keep_as_list'] ); $builder->addEventSubscriber($resizeListener); } - /** - * {@inheritdoc} - */ - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { $view->vars = array_replace($view->vars, [ 'allow_add' => $options['allow_add'], @@ -67,10 +65,7 @@ public function buildView(FormView $view, FormInterface $form, array $options) } } - /** - * {@inheritdoc} - */ - public function finishView(FormView $view, FormInterface $form, array $options) + public function finishView(FormView $view, FormInterface $form, array $options): void { $prefixOffset = -2; // check if the entry type also defines a block prefix @@ -101,12 +96,9 @@ public function finishView(FormView $view, FormInterface $form, array $options) } } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { - $entryOptionsNormalizer = function (Options $options, $value) { + $entryOptionsNormalizer = static function (Options $options, $value) { $value['block_name'] = 'entry'; return $value; @@ -120,22 +112,20 @@ public function configureOptions(OptionsResolver $resolver) 'prototype_name' => '__name__', 'entry_type' => TextType::class, 'entry_options' => [], + 'prototype_options' => [], 'delete_empty' => false, - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'The collection is invalid.'; - }, + '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']); } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'collection'; } diff --git a/Extension/Core/Type/ColorType.php b/Extension/Core/Type/ColorType.php index 1dbdfc3173..d67d6dc73b 100644 --- a/Extension/Core/Type/ColorType.php +++ b/Extension/Core/Type/ColorType.php @@ -16,7 +16,6 @@ use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; -use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Contracts\Translation\TranslatorInterface; @@ -27,23 +26,19 @@ class ColorType extends AbstractType */ private const HTML5_PATTERN = '/^#[0-9a-f]{6}$/i'; - private $translator; - - public function __construct(?TranslatorInterface $translator = null) - { - $this->translator = $translator; + public function __construct( + private ?TranslatorInterface $translator = null, + ) { } - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { if (!$options['html5']) { return; } - $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event): void { + $translator = $this->translator; + $builder->addEventListener(FormEvents::PRE_SUBMIT, static function (FormEvent $event) use ($translator): void { $value = $event->getData(); if (null === $value || '' === $value) { return; @@ -57,41 +52,28 @@ public function buildForm(FormBuilderInterface $builder, array $options) $messageParameters = [ '{{ value }}' => \is_scalar($value) ? (string) $value : \gettype($value), ]; - $message = $this->translator ? $this->translator->trans($messageTemplate, $messageParameters, 'validators') : $messageTemplate; + $message = $translator?->trans($messageTemplate, $messageParameters, 'validators') ?? $messageTemplate; $event->getForm()->addError(new FormError($message, $messageTemplate, $messageParameters)); }); } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'html5' => false, - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please select a valid color.'; - }, + 'invalid_message' => 'Please select a valid color.', ]); $resolver->setAllowedTypes('html5', 'bool'); } - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?string { return TextType::class; } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'color'; } diff --git a/Extension/Core/Type/CountryType.php b/Extension/Core/Type/CountryType.php index 85293bc284..503bc8327e 100644 --- a/Extension/Core/Type/CountryType.php +++ b/Extension/Core/Type/CountryType.php @@ -22,50 +22,35 @@ class CountryType extends AbstractType { - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + 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']; $alpha3 = $options['alpha3']; - return ChoiceList::loader($this, new IntlCallbackChoiceLoader(function () use ($choiceTranslationLocale, $alpha3) { - return array_flip($alpha3 ? Countries::getAlpha3Names($choiceTranslationLocale) : Countries::getNames($choiceTranslationLocale)); - }), [$choiceTranslationLocale, $alpha3]); + return ChoiceList::loader($this, new IntlCallbackChoiceLoader(static fn () => array_flip($alpha3 ? Countries::getAlpha3Names($choiceTranslationLocale) : Countries::getNames($choiceTranslationLocale))), [$choiceTranslationLocale, $alpha3]); }, 'choice_translation_domain' => false, 'choice_translation_locale' => null, 'alpha3' => false, - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please select a valid country.'; - }, + 'invalid_message' => 'Please select a valid country.', ]); $resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']); $resolver->setAllowedTypes('alpha3', 'bool'); } - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?string { return ChoiceType::class; } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'country'; } diff --git a/Extension/Core/Type/CurrencyType.php b/Extension/Core/Type/CurrencyType.php index 427b493f7e..abfa39729b 100644 --- a/Extension/Core/Type/CurrencyType.php +++ b/Extension/Core/Type/CurrencyType.php @@ -22,47 +22,32 @@ class CurrencyType extends AbstractType { - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + 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']; - return ChoiceList::loader($this, new IntlCallbackChoiceLoader(function () use ($choiceTranslationLocale) { - return array_flip(Currencies::getNames($choiceTranslationLocale)); - }), $choiceTranslationLocale); + return ChoiceList::loader($this, new IntlCallbackChoiceLoader(static fn () => array_flip(Currencies::getNames($choiceTranslationLocale))), $choiceTranslationLocale); }, 'choice_translation_domain' => false, 'choice_translation_locale' => null, - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please select a valid currency.'; - }, + 'invalid_message' => 'Please select a valid currency.', ]); $resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']); } - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?string { return ChoiceType::class; } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'currency'; } diff --git a/Extension/Core/Type/DateIntervalType.php b/Extension/Core/Type/DateIntervalType.php index 4c05557fa5..e7ebb3d45a 100644 --- a/Extension/Core/Type/DateIntervalType.php +++ b/Extension/Core/Type/DateIntervalType.php @@ -43,10 +43,7 @@ class DateIntervalType extends AbstractType 'choice' => ChoiceType::class, ]; - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { if (!$options['with_years'] && !$options['with_months'] && !$options['with_weeks'] && !$options['with_days'] && !$options['with_hours'] && !$options['with_minutes'] && !$options['with_seconds']) { throw new InvalidConfigurationException('You must enable at least one interval field.'); @@ -148,10 +145,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) } } - /** - * {@inheritdoc} - */ - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { $vars = [ 'widget' => $options['widget'], @@ -163,23 +157,14 @@ public function buildView(FormView $view, FormInterface $form, array $options) $view->vars = array_replace($view->vars, $vars); } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { - $compound = function (Options $options) { - return 'single_text' !== $options['widget']; - }; - $emptyData = function (Options $options) { - return 'single_text' === $options['widget'] ? '' : []; - }; + $compound = static fn (Options $options) => 'single_text' !== $options['widget']; + $emptyData = static fn (Options $options) => 'single_text' === $options['widget'] ? '' : []; - $placeholderDefault = function (Options $options) { - return $options['required'] ? null : ''; - }; + $placeholderDefault = static fn (Options $options) => $options['required'] ? null : ''; - $placeholderNormalizer = function (Options $options, $placeholder) use ($placeholderDefault) { + $placeholderNormalizer = static function (Options $options, $placeholder) use ($placeholderDefault) { if (\is_array($placeholder)) { $default = $placeholderDefault($options); @@ -189,20 +174,16 @@ public function configureOptions(OptionsResolver $resolver) return array_fill_keys(self::TIME_PARTS, $placeholder); }; - $labelsNormalizer = function (Options $options, array $labels) { - return array_replace([ - 'years' => null, - 'months' => null, - 'days' => null, - 'weeks' => null, - 'hours' => null, - 'minutes' => null, - 'seconds' => null, - 'invert' => 'Negative interval', - ], array_filter($labels, function ($label) { - return null !== $label; - })); - }; + $labelsNormalizer = static fn (Options $options, array $labels) => array_replace([ + 'years' => null, + 'months' => null, + 'days' => null, + 'weeks' => null, + 'hours' => null, + 'minutes' => null, + 'seconds' => null, + 'invert' => 'Negative interval', + ], array_filter($labels, static fn ($label) => null !== $label)); $resolver->setDefaults([ 'with_years' => true, @@ -233,11 +214,7 @@ public function configureOptions(OptionsResolver $resolver) 'compound' => $compound, 'empty_data' => $emptyData, 'labels' => [], - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please choose a valid date interval.'; - }, + 'invalid_message' => 'Please choose a valid date interval.', ]); $resolver->setNormalizer('placeholder', $placeholderNormalizer); $resolver->setNormalizer('labels', $labelsNormalizer); @@ -281,10 +258,7 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setAllowedTypes('labels', 'array'); } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'dateinterval'; } diff --git a/Extension/Core/Type/DateTimeType.php b/Extension/Core/Type/DateTimeType.php index 2f397f8ed0..cf4c2b7416 100644 --- a/Extension/Core/Type/DateTimeType.php +++ b/Extension/Core/Type/DateTimeType.php @@ -22,6 +22,8 @@ use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; use Symfony\Component\Form\ReversedTransformer; @@ -47,10 +49,7 @@ class DateTimeType extends AbstractType \IntlDateFormatter::SHORT, ]; - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $parts = ['year', 'month', 'day', 'hour']; $dateParts = ['year', 'month', 'day']; @@ -79,7 +78,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) if (self::HTML5_FORMAT === $pattern) { $builder->addViewTransformer(new DateTimeToHtml5LocalDateTimeTransformer( $options['model_timezone'], - $options['view_timezone'] + $options['view_timezone'], + $options['with_seconds'] )); } else { $builder->addViewTransformer(new DateTimeToLocalizedStringTransformer( @@ -110,12 +110,10 @@ public function buildForm(FormBuilderInterface $builder, array $options) ])); if ($emptyData instanceof \Closure) { - $lazyEmptyData = static function ($option) use ($emptyData) { - return static function (FormInterface $form) use ($emptyData, $option) { - $emptyData = $emptyData($form->getParent()); + $lazyEmptyData = static fn ($option) => static function (FormInterface $form) use ($emptyData, $option) { + $emptyData = $emptyData($form->getParent()); - return $emptyData[$option] ?? ''; - }; + return $emptyData[$option] ?? ''; }; $dateOptions['empty_data'] = $lazyEmptyData('date'); @@ -149,18 +147,13 @@ public function buildForm(FormBuilderInterface $builder, array $options) $timeOptions['label'] = false; } - if (null !== $options['date_widget']) { - $dateOptions['widget'] = $options['date_widget']; - } + $dateOptions['widget'] = $options['date_widget'] ?? $options['widget'] ?? 'choice'; + $timeOptions['widget'] = $options['time_widget'] ?? $options['widget'] ?? 'choice'; if (null !== $options['date_label']) { $dateOptions['label'] = $options['date_label']; } - if (null !== $options['time_widget']) { - $timeOptions['widget'] = $options['time_widget']; - } - if (null !== $options['time_label']) { $timeOptions['label'] = $options['time_label']; } @@ -200,12 +193,23 @@ public function buildForm(FormBuilderInterface $builder, array $options) new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], $parts) )); } + + if (\in_array($options['input'], ['datetime', 'datetime_immutable'], true) && null !== $options['model_timezone']) { + $builder->addEventListener(FormEvents::POST_SET_DATA, static function (FormEvent $event) use ($options): void { + $date = $event->getData(); + + if (!$date instanceof \DateTimeInterface) { + return; + } + + 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'])); + } + }); + } } - /** - * {@inheritdoc} - */ - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { $view->vars['widget'] = $options['widget']; @@ -220,30 +224,19 @@ public function buildView(FormView $view, FormInterface $form, array $options) // adding the HTML attribute step if not already defined. // Otherwise the browser will not display and so not send the seconds // therefore the value will always be considered as invalid. - if ($options['with_seconds'] && !isset($view->vars['attr']['step'])) { - $view->vars['attr']['step'] = 1; + if (!isset($view->vars['attr']['step'])) { + if ($options['with_seconds']) { + $view->vars['attr']['step'] = 1; + } elseif (!$options['with_minutes']) { + $view->vars['attr']['step'] = 3600; + } } } } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { - $compound = function (Options $options) { - return 'single_text' !== $options['widget']; - }; - - // Defaults to the value of "widget" - $dateWidget = function (Options $options) { - return 'single_text' === $options['widget'] ? null : $options['widget']; - }; - - // Defaults to the value of "widget" - $timeWidget = function (Options $options) { - return 'single_text' === $options['widget'] ? null : $options['widget']; - }; + $compound = static fn (Options $options) => 'single_text' !== $options['widget']; $resolver->setDefaults([ 'input' => 'datetime', @@ -252,8 +245,8 @@ public function configureOptions(OptionsResolver $resolver) 'format' => self::HTML5_FORMAT, 'date_format' => null, 'widget' => null, - 'date_widget' => $dateWidget, - 'time_widget' => $timeWidget, + 'date_widget' => null, + 'time_widget' => null, 'with_minutes' => true, 'with_seconds' => false, 'html5' => true, @@ -269,15 +262,9 @@ public function configureOptions(OptionsResolver $resolver) 'compound' => $compound, 'date_label' => null, 'time_label' => null, - 'empty_data' => function (Options $options) { - return $options['compound'] ? [] : ''; - }, + 'empty_data' => static fn (Options $options) => $options['compound'] ? [] : '', 'input_format' => 'Y-m-d H:i:s', - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please enter a valid date and time.'; - }, + 'invalid_message' => 'Please enter a valid date and time.', ]); // Don't add some defaults in order to preserve the defaults @@ -322,40 +309,37 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setAllowedTypes('input_format', 'string'); - $resolver->setNormalizer('date_format', function (Options $options, $dateFormat) { + $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; }); - $resolver->setNormalizer('date_widget', function (Options $options, $dateWidget) { - if (null !== $dateWidget && 'single_text' === $options['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)); + $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)); + } + 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)); + } + } elseif (null === $widget && null === $options['date_widget'] && null === $options['time_widget']) { + return 'single_text'; } - return $dateWidget; + return $widget; }); - $resolver->setNormalizer('time_widget', function (Options $options, $timeWidget) { - if (null !== $timeWidget && 'single_text' === $options['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)); - } - - return $timeWidget; - }); - $resolver->setNormalizer('html5', function (Options $options, $html5) { + $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; }); } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'datetime'; } diff --git a/Extension/Core/Type/DateType.php b/Extension/Core/Type/DateType.php index 1a46a8084d..36b430e144 100644 --- a/Extension/Core/Type/DateType.php +++ b/Extension/Core/Type/DateType.php @@ -19,6 +19,8 @@ use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; use Symfony\Component\Form\ReversedTransformer; @@ -43,14 +45,11 @@ class DateType extends AbstractType 'choice' => ChoiceType::class, ]; - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + 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)) { @@ -59,7 +58,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 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( @@ -72,7 +71,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) )); } 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 = [ @@ -84,12 +83,10 @@ public function buildForm(FormBuilderInterface $builder, array $options) $emptyData = $builder->getEmptyData() ?: []; if ($emptyData instanceof \Closure) { - $lazyEmptyData = static function ($option) use ($emptyData) { - return static function (FormInterface $form) use ($emptyData, $option) { - $emptyData = $emptyData($form->getParent()); + $lazyEmptyData = static fn ($option) => static function (FormInterface $form) use ($emptyData, $option) { + $emptyData = $emptyData($form->getParent()); - return $emptyData[$option] ?? ''; - }; + return $emptyData[$option] ?? ''; }; $yearOptions['empty_data'] = $lazyEmptyData('year'); @@ -123,17 +120,11 @@ public function buildForm(FormBuilderInterface $builder, array $options) \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']) { @@ -180,12 +171,23 @@ class_exists(\IntlTimeZone::class, false) ? \IntlTimeZone::createDefault() : nul new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], ['year', 'month', 'day']) )); } + + if (\in_array($options['input'], ['datetime', 'datetime_immutable'], true) && null !== $options['model_timezone']) { + $builder->addEventListener(FormEvents::POST_SET_DATA, static function (FormEvent $event) use ($options): void { + $date = $event->getData(); + + if (!$date instanceof \DateTimeInterface) { + return; + } + + 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'])); + } + }); + } } - /** - * {@inheritdoc} - */ - public function finishView(FormView $view, FormInterface $form, array $options) + public function finishView(FormView $view, FormInterface $form, array $options): void { $view->vars['widget'] = $options['widget']; @@ -222,20 +224,13 @@ public function finishView(FormView $view, FormInterface $form, array $options) } } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { - $compound = function (Options $options) { - return 'single_text' !== $options['widget']; - }; + $compound = static fn (Options $options) => 'single_text' !== $options['widget']; - $placeholderDefault = function (Options $options) { - return $options['required'] ? null : ''; - }; + $placeholderDefault = static fn (Options $options) => $options['required'] ? null : ''; - $placeholderNormalizer = function (Options $options, $placeholder) use ($placeholderDefault) { + $placeholderNormalizer = static function (Options $options, $placeholder) use ($placeholderDefault) { if (\is_array($placeholder)) { $default = $placeholderDefault($options); @@ -252,12 +247,10 @@ public function configureOptions(OptionsResolver $resolver) ]; }; - $choiceTranslationDomainNormalizer = function (Options $options, $choiceTranslationDomain) { + $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 ); } @@ -269,19 +262,18 @@ public function configureOptions(OptionsResolver $resolver) ]; }; - $format = function (Options $options) { - return 'single_text' === $options['widget'] ? self::HTML5_FORMAT : self::DEFAULT_FORMAT; - }; + $format = static fn (Options $options) => 'single_text' === $options['widget'] ? self::HTML5_FORMAT : self::DEFAULT_FORMAT; $resolver->setDefaults([ 'years' => range((int) date('Y') - 5, (int) date('Y') + 5), 'months' => range(1, 12), 'days' => range(1, 31), - 'widget' => 'choice', + 'widget' => 'single_text', 'input' => 'datetime', 'format' => $format, 'model_timezone' => null, 'view_timezone' => null, + 'calendar' => null, 'placeholder' => $placeholderDefault, 'html5' => true, // Don't modify \DateTime classes by reference, we treat @@ -294,16 +286,10 @@ public function configureOptions(OptionsResolver $resolver) // this option. 'data_class' => null, 'compound' => $compound, - 'empty_data' => function (Options $options) { - return $options['compound'] ? [] : ''; - }, + 'empty_data' => static fn (Options $options) => $options['compound'] ? [] : '', 'choice_translation_domain' => false, 'input_format' => 'Y-m-d', - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please enter a valid date.'; - }, + 'invalid_message' => 'Please enter a valid date.', ]); $resolver->setNormalizer('placeholder', $placeholderNormalizer); @@ -327,25 +313,25 @@ public function configureOptions(OptionsResolver $resolver) $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', function (Options $options, $html5) { + $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; }); } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'date'; } - private function formatTimestamps(\IntlDateFormatter $formatter, string $regex, array $timestamps) + private function formatTimestamps(\IntlDateFormatter $formatter, string $regex, array $timestamps): array { $pattern = $formatter->getPattern(); $timezone = $formatter->getTimeZoneId(); @@ -370,18 +356,18 @@ private function formatTimestamps(\IntlDateFormatter $formatter, string $regex, return $formattedTimestamps; } - private function listYears(array $years) + private function listYears(array $years): array { $result = []; foreach ($years as $year) { - $result[\PHP_INT_SIZE === 4 ? \DateTime::createFromFormat('Y e', $year.' UTC')->format('U') : gmmktime(0, 0, 0, 6, 15, $year)] = $year; + $result[\PHP_INT_SIZE === 4 ? \DateTimeImmutable::createFromFormat('Y e', $year.' UTC')->format('U') : gmmktime(0, 0, 0, 6, 15, $year)] = $year; } return $result; } - private function listMonths(array $months) + private function listMonths(array $months): array { $result = []; @@ -392,7 +378,7 @@ private function listMonths(array $months) return $result; } - private function listDays(array $days) + private function listDays(array $days): array { $result = []; diff --git a/Extension/Core/Type/EmailType.php b/Extension/Core/Type/EmailType.php index 486bc0217f..99a5689afc 100644 --- a/Extension/Core/Type/EmailType.php +++ b/Extension/Core/Type/EmailType.php @@ -12,37 +12,23 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; -use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class EmailType extends AbstractType { - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please enter a valid email address.'; - }, + 'invalid_message' => 'Please enter a valid email address.', ]); } - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?string { return TextType::class; } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'email'; } diff --git a/Extension/Core/Type/EnumType.php b/Extension/Core/Type/EnumType.php index c251cdbd00..bfede9c04d 100644 --- a/Extension/Core/Type/EnumType.php +++ b/Extension/Core/Type/EnumType.php @@ -14,6 +14,7 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\Translation\TranslatableInterface; /** * A choice type for native PHP enums. @@ -27,13 +28,9 @@ public function configureOptions(OptionsResolver $resolver): void $resolver ->setRequired(['class']) ->setAllowedTypes('class', 'string') - ->setAllowedValues('class', \Closure::fromCallable('enum_exists')) - ->setDefault('choices', static function (Options $options): array { - return $options['class']::cases(); - }) - ->setDefault('choice_label', static function (\UnitEnum $choice): string { - return $choice->name; - }) + ->setAllowedValues('class', enum_exists(...)) + ->setDefault('choices', static fn (Options $options): array => $options['class']::cases()) + ->setDefault('choice_label', static fn (\UnitEnum $choice) => $choice instanceof TranslatableInterface ? $choice : $choice->name) ->setDefault('choice_value', static function (Options $options): ?\Closure { if (!is_a($options['class'], \BackedEnum::class, true)) { return null; diff --git a/Extension/Core/Type/FileType.php b/Extension/Core/Type/FileType.php index 67f5992d10..b4b4d86d95 100644 --- a/Extension/Core/Type/FileType.php +++ b/Extension/Core/Type/FileType.php @@ -12,12 +12,14 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Event\PreSubmitEvent; use Symfony\Component\Form\FileUploadError; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; +use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Contracts\Translation\TranslatorInterface; @@ -33,20 +35,16 @@ class FileType extends AbstractType self::MIB_BYTES => 'MiB', ]; - private $translator; - - public function __construct(?TranslatorInterface $translator = null) - { - $this->translator = $translator; + public function __construct( + private ?TranslatorInterface $translator = null, + ) { } - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { // Ensure that submitted data is always an uploaded file or an array of some $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) { + /** @var PreSubmitEvent $event */ $form = $event->getForm(); $requestHandler = $form->getConfig()->getRequestHandler(); @@ -84,10 +82,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) }); } - /** - * {@inheritdoc} - */ - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { if ($options['multiple']) { $view->vars['full_name'] .= '[]'; @@ -100,29 +95,19 @@ public function buildView(FormView $view, FormInterface $form, array $options) ]); } - /** - * {@inheritdoc} - */ - public function finishView(FormView $view, FormInterface $form, array $options) + public function finishView(FormView $view, FormInterface $form, array $options): void { $view->vars['multipart'] = true; } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $dataClass = null; - if (class_exists(\Symfony\Component\HttpFoundation\File\File::class)) { - $dataClass = function (Options $options) { - return $options['multiple'] ? null : 'Symfony\Component\HttpFoundation\File\File'; - }; + if (class_exists(File::class)) { + $dataClass = static fn (Options $options) => $options['multiple'] ? null : File::class; } - $emptyData = function (Options $options) { - return $options['multiple'] ? [] : null; - }; + $emptyData = static fn (Options $options) => $options['multiple'] ? [] : null; $resolver->setDefaults([ 'compound' => false, @@ -130,23 +115,16 @@ public function configureOptions(OptionsResolver $resolver) 'empty_data' => $emptyData, 'multiple' => false, 'allow_file_upload' => true, - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please select a valid file.'; - }, + 'invalid_message' => 'Please select a valid file.', ]); } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'file'; } - private function getFileUploadError(int $errorCode) + private function getFileUploadError(int $errorCode): FileUploadError { $messageParameters = []; @@ -176,10 +154,8 @@ private function getFileUploadError(int $errorCode) * Returns the maximum size of an uploaded file as configured in php.ini. * * This method should be kept in sync with Symfony\Component\HttpFoundation\File\UploadedFile::getMaxFilesize(). - * - * @return int|float The maximum size of an uploaded file in bytes (returns float if size > PHP_INT_MAX) */ - private static function getMaxFilesize() + private static function getMaxFilesize(): int|float { $iniMax = strtolower(\ini_get('upload_max_filesize')); @@ -214,10 +190,8 @@ private static function getMaxFilesize() * (i.e. try "MB", then "kB", then "bytes"). * * This method should be kept in sync with Symfony\Component\Validator\Constraints\FileValidator::factorizeSizes(). - * - * @param int|float $limit */ - private function factorizeSizes(int $size, $limit) + private function factorizeSizes(int $size, int|float $limit): array { $coef = self::MIB_BYTES; $coefFactor = self::KIB_BYTES; diff --git a/Extension/Core/Type/FormType.php b/Extension/Core/Type/FormType.php index f7d2801021..9497bad365 100644 --- a/Extension/Core/Type/FormType.php +++ b/Extension/Core/Type/FormType.php @@ -18,18 +18,17 @@ use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper; use Symfony\Component\Form\Extension\Core\EventListener\TrimListener; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Form\FormConfigBuilderInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Symfony\Component\Translation\TranslatableMessage; +use Symfony\Contracts\Translation\TranslatableInterface; class FormType extends BaseType { - private $dataMapper; + private DataMapper $dataMapper; public function __construct(?PropertyAccessorInterface $propertyAccessor = null) { @@ -39,10 +38,7 @@ public function __construct(?PropertyAccessorInterface $propertyAccessor = null) ])); } - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { parent::buildForm($builder, $options); @@ -67,19 +63,10 @@ public function buildForm(FormBuilderInterface $builder, array $options) $builder->addEventSubscriber(new TrimListener()); } - if (!method_exists($builder, 'setIsEmptyCallback')) { - trigger_deprecation('symfony/form', '5.1', 'Not implementing the "%s::setIsEmptyCallback()" method in "%s" is deprecated.', FormConfigBuilderInterface::class, get_debug_type($builder)); - - return; - } - $builder->setIsEmptyCallback($options['is_empty_callback']); } - /** - * {@inheritdoc} - */ - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { parent::buildView($view, $form, $options); @@ -106,7 +93,6 @@ public function buildView(FormView $view, FormInterface $form, array $options) 'value' => $form->getViewData(), 'data' => $form->getNormData(), 'required' => $form->isRequired(), - 'size' => null, 'label_attr' => $options['label_attr'], 'help' => $options['help'], 'help_attr' => $options['help_attr'], @@ -119,10 +105,7 @@ public function buildView(FormView $view, FormInterface $form, array $options) ]); } - /** - * {@inheritdoc} - */ - public function finishView(FormView $view, FormInterface $form, array $options) + public function finishView(FormView $view, FormInterface $form, array $options): void { $multipart = false; @@ -136,45 +119,30 @@ public function finishView(FormView $view, FormInterface $form, array $options) $view->vars['multipart'] = $multipart; } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { parent::configureOptions($resolver); // Derive "data_class" option from passed "data" object - $dataClass = function (Options $options) { - return isset($options['data']) && \is_object($options['data']) ? \get_class($options['data']) : null; - }; + $dataClass = static fn (Options $options) => isset($options['data']) && \is_object($options['data']) ? $options['data']::class : null; // Derive "empty_data" closure from "data_class" option - $emptyData = function (Options $options) { + $emptyData = static function (Options $options) { $class = $options['data_class']; if (null !== $class) { - return function (FormInterface $form) use ($class) { - return $form->isEmpty() && !$form->isRequired() ? null : new $class(); - }; + return static fn (FormInterface $form) => $form->isEmpty() && !$form->isRequired() ? null : new $class(); } - return function (FormInterface $form) { - return $form->getConfig()->getCompound() ? [] : ''; - }; + return static fn (FormInterface $form) => $form->getConfig()->getCompound() ? [] : ''; }; // Wrap "post_max_size_message" in a closure to translate it lazily - $uploadMaxSizeMessage = function (Options $options) { - return function () use ($options) { - return $options['post_max_size_message']; - }; - }; + $uploadMaxSizeMessage = static fn (Options $options) => static fn () => $options['post_max_size_message']; // For any form that is not represented by a single HTML control, // errors should bubble up by default - $errorBubbling = function (Options $options) { - return $options['compound'] && !$options['inherit_data']; - }; + $errorBubbling = static fn (Options $options) => $options['compound'] && !$options['inherit_data']; // If data is given, the form is locked to that data // (independent of its value) @@ -198,7 +166,6 @@ public function configureOptions(OptionsResolver $resolver) // According to RFC 2396 (http://www.ietf.org/rfc/rfc2396.txt) // section 4.2., empty URIs are considered same-document references 'action' => '', - 'attr' => [], 'post_max_size_message' => 'The uploaded file was too large. Please try to upload a smaller file.', 'upload_max_size_message' => $uploadMaxSizeMessage, // internal 'allow_file_upload' => false, @@ -216,7 +183,7 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setAllowedTypes('label_attr', 'array'); $resolver->setAllowedTypes('action', 'string'); $resolver->setAllowedTypes('upload_max_size_message', ['callable']); - $resolver->setAllowedTypes('help', ['string', 'null', TranslatableMessage::class]); + $resolver->setAllowedTypes('help', ['string', 'null', TranslatableInterface::class]); $resolver->setAllowedTypes('help_attr', 'array'); $resolver->setAllowedTypes('help_html', 'bool'); $resolver->setAllowedTypes('is_empty_callback', ['null', 'callable']); @@ -227,18 +194,12 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setInfo('setter', 'A callable that accepts three arguments (a reference to the view data, the submitted value and the current form field).'); } - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?string { return null; } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'form'; } diff --git a/Extension/Core/Type/HiddenType.php b/Extension/Core/Type/HiddenType.php index f4258ec011..73449a3635 100644 --- a/Extension/Core/Type/HiddenType.php +++ b/Extension/Core/Type/HiddenType.php @@ -12,15 +12,11 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; -use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class HiddenType extends AbstractType { - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ // hidden fields cannot have a required attribute @@ -28,18 +24,11 @@ public function configureOptions(OptionsResolver $resolver) // Pass errors to the parent 'error_bubbling' => true, 'compound' => false, - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'The hidden field is invalid.'; - }, + 'invalid_message' => 'The hidden field is invalid.', ]); } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'hidden'; } diff --git a/Extension/Core/Type/IntegerType.php b/Extension/Core/Type/IntegerType.php index a1cd058a94..365c8a84b0 100644 --- a/Extension/Core/Type/IntegerType.php +++ b/Extension/Core/Type/IntegerType.php @@ -16,44 +16,30 @@ 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 IntegerType extends AbstractType { - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->addViewTransformer(new IntegerToLocalizedStringTransformer($options['grouping'], $options['rounding_mode'], !$options['grouping'] ? 'en' : null)); } - /** - * {@inheritdoc} - */ - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { if ($options['grouping']) { $view->vars['type'] = 'text'; } } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'grouping' => false, // Integer cast rounds towards 0, so do the same when displaying fractions 'rounding_mode' => \NumberFormatter::ROUND_DOWN, 'compound' => false, - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please enter an integer.'; - }, + 'invalid_message' => 'Please enter an integer.', ]); $resolver->setAllowedValues('rounding_mode', [ @@ -67,10 +53,7 @@ public function configureOptions(OptionsResolver $resolver) ]); } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'integer'; } diff --git a/Extension/Core/Type/LanguageType.php b/Extension/Core/Type/LanguageType.php index 7bcbda2077..638976a8cf 100644 --- a/Extension/Core/Type/LanguageType.php +++ b/Extension/Core/Type/LanguageType.php @@ -23,27 +23,24 @@ class LanguageType extends AbstractType { - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + 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']; $choiceSelfTranslation = $options['choice_self_translation']; - return ChoiceList::loader($this, new IntlCallbackChoiceLoader(function () use ($choiceTranslationLocale, $useAlpha3Codes, $choiceSelfTranslation) { + return ChoiceList::loader($this, new IntlCallbackChoiceLoader(static function () use ($choiceTranslationLocale, $useAlpha3Codes, $choiceSelfTranslation) { if (true === $choiceSelfTranslation) { foreach (Languages::getLanguageCodes() as $alpha2Code) { try { $languageCode = $useAlpha3Codes ? Languages::getAlpha3Code($alpha2Code) : $alpha2Code; $languagesList[$languageCode] = Languages::getName($alpha2Code, $alpha2Code); - } catch (MissingResourceException $e) { + } catch (MissingResourceException) { // ignore errors like "Couldn't read the indices for the locale 'meta'" } } @@ -58,18 +55,14 @@ public function configureOptions(OptionsResolver $resolver) 'choice_translation_locale' => null, 'alpha3' => false, 'choice_self_translation' => false, - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please select a valid language.'; - }, + 'invalid_message' => 'Please select a valid language.', ]); $resolver->setAllowedTypes('choice_self_translation', ['bool']); $resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']); $resolver->setAllowedTypes('alpha3', 'bool'); - $resolver->setNormalizer('choice_self_translation', function (Options $options, $value) { + $resolver->setNormalizer('choice_self_translation', static function (Options $options, $value) { if (true === $value && $options['choice_translation_locale']) { throw new LogicException('Cannot use the "choice_self_translation" and "choice_translation_locale" options at the same time. Remove one of them.'); } @@ -78,18 +71,12 @@ public function configureOptions(OptionsResolver $resolver) }); } - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?string { return ChoiceType::class; } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'language'; } diff --git a/Extension/Core/Type/LocaleType.php b/Extension/Core/Type/LocaleType.php index 14113e4ac1..1529ca83f9 100644 --- a/Extension/Core/Type/LocaleType.php +++ b/Extension/Core/Type/LocaleType.php @@ -22,47 +22,32 @@ class LocaleType extends AbstractType { - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + 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']; - return ChoiceList::loader($this, new IntlCallbackChoiceLoader(function () use ($choiceTranslationLocale) { - return array_flip(Locales::getNames($choiceTranslationLocale)); - }), $choiceTranslationLocale); + return ChoiceList::loader($this, new IntlCallbackChoiceLoader(static fn () => array_flip(Locales::getNames($choiceTranslationLocale))), $choiceTranslationLocale); }, 'choice_translation_domain' => false, 'choice_translation_locale' => null, - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please select a valid locale.'; - }, + 'invalid_message' => 'Please select a valid locale.', ]); $resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']); } - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?string { return ChoiceType::class; } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'locale'; } diff --git a/Extension/Core/Type/MoneyType.php b/Extension/Core/Type/MoneyType.php index 6bf7a201f3..2657b03afb 100644 --- a/Extension/Core/Type/MoneyType.php +++ b/Extension/Core/Type/MoneyType.php @@ -22,12 +22,9 @@ class MoneyType extends AbstractType { - protected static $patterns = []; + protected static array $patterns = []; - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { // Values used in HTML5 number inputs should be formatted as in "1234.5", ie. 'en' format without grouping, // according to https://www.w3.org/TR/html51/sec-forms.html#date-time-and-number-formats @@ -37,15 +34,13 @@ public function buildForm(FormBuilderInterface $builder, array $options) $options['grouping'], $options['rounding_mode'], $options['divisor'], - $options['html5'] ? 'en' : null + $options['html5'] ? 'en' : null, + $options['input'], )) ; } - /** - * {@inheritdoc} - */ - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { $view->vars['money_pattern'] = self::getPattern($options['currency']); @@ -54,10 +49,7 @@ public function buildView(FormView $view, FormInterface $form, array $options) } } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'scale' => 2, @@ -67,11 +59,8 @@ public function configureOptions(OptionsResolver $resolver) 'currency' => 'EUR', 'compound' => false, 'html5' => false, - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please enter a valid money amount.'; - }, + 'invalid_message' => 'Please enter a valid money amount.', + 'input' => 'float', ]); $resolver->setAllowedValues('rounding_mode', [ @@ -88,7 +77,9 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setAllowedTypes('html5', 'bool'); - $resolver->setNormalizer('grouping', function (Options $options, $value) { + $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.'); } @@ -97,10 +88,7 @@ public function configureOptions(OptionsResolver $resolver) }); } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'money'; } @@ -111,7 +99,7 @@ public function getBlockPrefix() * The pattern contains the placeholder "{{ widget }}" where the HTML tag should * be inserted */ - protected static function getPattern(?string $currency) + protected static function getPattern(?string $currency): string { if (!$currency) { return '{{ widget }}'; diff --git a/Extension/Core/Type/NumberType.php b/Extension/Core/Type/NumberType.php index f9c8b35c68..ce516a352c 100644 --- a/Extension/Core/Type/NumberType.php +++ b/Extension/Core/Type/NumberType.php @@ -23,10 +23,7 @@ class NumberType extends AbstractType { - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->addViewTransformer(new NumberToLocalizedStringTransformer( $options['scale'], @@ -40,10 +37,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) } } - /** - * {@inheritdoc} - */ - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { if ($options['html5']) { $view->vars['type'] = 'number'; @@ -51,13 +45,12 @@ public function buildView(FormView $view, FormInterface $form, array $options) if (!isset($view->vars['attr']['step'])) { $view->vars['attr']['step'] = 'any'; } + } else { + $view->vars['attr']['inputmode'] = 0 === $options['scale'] ? 'numeric' : 'decimal'; } } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ // default scale is locale specific (usually around 3) @@ -67,11 +60,7 @@ public function configureOptions(OptionsResolver $resolver) 'compound' => false, 'input' => 'number', 'html5' => false, - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please enter a number.'; - }, + 'invalid_message' => 'Please enter a number.', ]); $resolver->setAllowedValues('rounding_mode', [ @@ -87,7 +76,7 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setAllowedTypes('scale', ['null', 'int']); $resolver->setAllowedTypes('html5', 'bool'); - $resolver->setNormalizer('grouping', function (Options $options, $value) { + $resolver->setNormalizer('grouping', static function (Options $options, $value) { if (true === $value && $options['html5']) { throw new LogicException('Cannot use the "grouping" option when the "html5" option is enabled.'); } @@ -96,10 +85,7 @@ public function configureOptions(OptionsResolver $resolver) }); } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'number'; } diff --git a/Extension/Core/Type/PasswordType.php b/Extension/Core/Type/PasswordType.php index 779f94d43b..72cc8ec712 100644 --- a/Extension/Core/Type/PasswordType.php +++ b/Extension/Core/Type/PasswordType.php @@ -14,49 +14,32 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; -use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class PasswordType extends AbstractType { - /** - * {@inheritdoc} - */ - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { if ($options['always_empty'] || !$form->isSubmitted()) { $view->vars['value'] = ''; } } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'always_empty' => true, 'trim' => false, - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'The password is invalid.'; - }, + 'invalid_message' => 'The password is invalid.', ]); } - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?string { return TextType::class; } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'password'; } diff --git a/Extension/Core/Type/PercentType.php b/Extension/Core/Type/PercentType.php index 90e8fa6e14..fae102c31a 100644 --- a/Extension/Core/Type/PercentType.php +++ b/Extension/Core/Type/PercentType.php @@ -16,15 +16,11 @@ 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 PercentType extends AbstractType { - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->addViewTransformer(new PercentToLocalizedStringTransformer( $options['scale'], @@ -34,10 +30,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) )); } - /** - * {@inheritdoc} - */ - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { $view->vars['symbol'] = $options['symbol']; @@ -46,27 +39,16 @@ public function buildView(FormView $view, FormInterface $form, array $options) } } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'scale' => 0, - 'rounding_mode' => function (Options $options) { - trigger_deprecation('symfony/form', '5.1', 'Not configuring the "rounding_mode" option is deprecated. It will default to "\NumberFormatter::ROUND_HALFUP" in Symfony 6.0.'); - - return null; - }, + 'rounding_mode' => \NumberFormatter::ROUND_HALFUP, 'symbol' => '%', 'type' => 'fractional', 'compound' => false, 'html5' => false, - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please enter a percentage value.'; - }, + 'invalid_message' => 'Please enter a percentage value.', ]); $resolver->setAllowedValues('type', [ @@ -74,7 +56,6 @@ public function configureOptions(OptionsResolver $resolver) 'integer', ]); $resolver->setAllowedValues('rounding_mode', [ - null, \NumberFormatter::ROUND_FLOOR, \NumberFormatter::ROUND_DOWN, \NumberFormatter::ROUND_HALFDOWN, @@ -85,20 +66,10 @@ public function configureOptions(OptionsResolver $resolver) ]); $resolver->setAllowedTypes('scale', 'int'); $resolver->setAllowedTypes('symbol', ['bool', 'string']); - $resolver->setDeprecated('rounding_mode', 'symfony/form', '5.1', function (Options $options, $roundingMode) { - if (null === $roundingMode) { - return 'Not configuring the "rounding_mode" option is deprecated. It will default to "\NumberFormatter::ROUND_HALFUP" in Symfony 6.0.'; - } - - return ''; - }); $resolver->setAllowedTypes('html5', 'bool'); } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'percent'; } diff --git a/Extension/Core/Type/RadioType.php b/Extension/Core/Type/RadioType.php index ed999f52b8..ac72a20bae 100644 --- a/Extension/Core/Type/RadioType.php +++ b/Extension/Core/Type/RadioType.php @@ -12,37 +12,23 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; -use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class RadioType extends AbstractType { - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please select a valid option.'; - }, + 'invalid_message' => 'Please select a valid option.', ]); } - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?string { return CheckboxType::class; } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'radio'; } diff --git a/Extension/Core/Type/RangeType.php b/Extension/Core/Type/RangeType.php index 73ec6e1631..edb04b4ca3 100644 --- a/Extension/Core/Type/RangeType.php +++ b/Extension/Core/Type/RangeType.php @@ -12,37 +12,23 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; -use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class RangeType extends AbstractType { - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please choose a valid range.'; - }, + 'invalid_message' => 'Please choose a valid range.', ]); } - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?string { return TextType::class; } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'range'; } diff --git a/Extension/Core/Type/RepeatedType.php b/Extension/Core/Type/RepeatedType.php index 16fa1a7bb7..96d2c07d3e 100644 --- a/Extension/Core/Type/RepeatedType.php +++ b/Extension/Core/Type/RepeatedType.php @@ -14,15 +14,11 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\DataTransformer\ValueToDuplicatesTransformer; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class RepeatedType extends AbstractType { - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { // Overwrite required option for child fields $options['first_options']['required'] = $options['required']; @@ -45,10 +41,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) ; } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'type' => TextType::class, @@ -58,11 +51,7 @@ public function configureOptions(OptionsResolver $resolver) 'first_name' => 'first', 'second_name' => 'second', 'error_bubbling' => false, - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'The values do not match.'; - }, + 'invalid_message' => 'The values do not match.', ]); $resolver->setAllowedTypes('options', 'array'); @@ -70,10 +59,7 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setAllowedTypes('second_options', 'array'); } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'repeated'; } diff --git a/Extension/Core/Type/ResetType.php b/Extension/Core/Type/ResetType.php index ce8013d1b0..9a53a3dc68 100644 --- a/Extension/Core/Type/ResetType.php +++ b/Extension/Core/Type/ResetType.php @@ -21,18 +21,12 @@ */ class ResetType extends AbstractType implements ButtonTypeInterface { - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?string { return ButtonType::class; } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'reset'; } diff --git a/Extension/Core/Type/SearchType.php b/Extension/Core/Type/SearchType.php index 682277e615..f69cf79ee4 100644 --- a/Extension/Core/Type/SearchType.php +++ b/Extension/Core/Type/SearchType.php @@ -12,37 +12,23 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; -use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class SearchType extends AbstractType { - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please enter a valid search term.'; - }, + 'invalid_message' => 'Please enter a valid search term.', ]); } - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?string { return TextType::class; } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'search'; } diff --git a/Extension/Core/Type/SubmitType.php b/Extension/Core/Type/SubmitType.php index 945156efc6..5681060d48 100644 --- a/Extension/Core/Type/SubmitType.php +++ b/Extension/Core/Type/SubmitType.php @@ -24,7 +24,7 @@ */ class SubmitType extends AbstractType implements SubmitButtonTypeInterface { - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { $view->vars['clicked'] = $form->isClicked(); @@ -33,27 +33,18 @@ public function buildView(FormView $view, FormInterface $form, array $options) } } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefault('validate', true); $resolver->setAllowedTypes('validate', 'bool'); } - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?string { return ButtonType::class; } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'submit'; } diff --git a/Extension/Core/Type/TelType.php b/Extension/Core/Type/TelType.php index bc25fd94ca..29a3b5db55 100644 --- a/Extension/Core/Type/TelType.php +++ b/Extension/Core/Type/TelType.php @@ -12,37 +12,23 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; -use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class TelType extends AbstractType { - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please provide a valid phone number.'; - }, + 'invalid_message' => 'Please provide a valid phone number.', ]); } - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?string { return TextType::class; } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'tel'; } diff --git a/Extension/Core/Type/TextType.php b/Extension/Core/Type/TextType.php index 72d21c3dd9..bff23eab48 100644 --- a/Extension/Core/Type/TextType.php +++ b/Extension/Core/Type/TextType.php @@ -18,7 +18,7 @@ class TextType extends AbstractType implements DataTransformerInterface { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { // When empty_data is explicitly set to an empty string, // a string should always be returned when NULL is submitted @@ -30,37 +30,25 @@ public function buildForm(FormBuilderInterface $builder, array $options) } } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'compound' => false, ]); } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'text'; } - /** - * {@inheritdoc} - */ - public function transform($data) + public function transform(mixed $data): mixed { // Model data should not be transformed return $data; } - /** - * {@inheritdoc} - */ - public function reverseTransform($data) + public function reverseTransform(mixed $data): mixed { return $data ?? ''; } diff --git a/Extension/Core/Type/TextareaType.php b/Extension/Core/Type/TextareaType.php index 173b7ef53c..1615964c53 100644 --- a/Extension/Core/Type/TextareaType.php +++ b/Extension/Core/Type/TextareaType.php @@ -17,27 +17,18 @@ class TextareaType extends AbstractType { - /** - * {@inheritdoc} - */ - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { $view->vars['pattern'] = null; unset($view->vars['attr']['pattern']); } - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?string { return TextType::class; } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'textarea'; } diff --git a/Extension/Core/Type/TimeType.php b/Extension/Core/Type/TimeType.php index 6b56a120e7..92cf42d963 100644 --- a/Extension/Core/Type/TimeType.php +++ b/Extension/Core/Type/TimeType.php @@ -34,10 +34,7 @@ class TimeType extends AbstractType 'choice' => ChoiceType::class, ]; - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $parts = ['hour']; $format = 'H'; @@ -47,7 +44,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) } 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']) { @@ -61,15 +58,15 @@ public function buildForm(FormBuilderInterface $builder, array $options) } if ('single_text' === $options['widget']) { - $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $e) use ($options) { + $builder->addEventListener(FormEvents::PRE_SUBMIT, static function (FormEvent $e) use ($options) { $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'])); } } }); @@ -79,7 +76,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) if (null !== $options['reference_date']) { $parseFormat = 'Y-m-d '.$format; - $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) { + $builder->addEventListener(FormEvents::PRE_SUBMIT, static function (FormEvent $event) use ($options) { $data = $event->getData(); if (preg_match('/^\d{2}:\d{2}(:\d{2})?$/', $data)) { @@ -99,12 +96,10 @@ public function buildForm(FormBuilderInterface $builder, array $options) $emptyData = $builder->getEmptyData() ?: []; if ($emptyData instanceof \Closure) { - $lazyEmptyData = static function ($option) use ($emptyData) { - return static function (FormInterface $form) use ($emptyData, $option) { - $emptyData = $emptyData($form->getParent()); + $lazyEmptyData = static fn ($option) => static function (FormInterface $form) use ($emptyData, $option) { + $emptyData = $emptyData($form->getParent()); - return $emptyData[$option] ?? ''; - }; + return $emptyData[$option] ?? ''; }; $hourOptions['empty_data'] = $lazyEmptyData('hour'); @@ -210,12 +205,23 @@ public function buildForm(FormBuilderInterface $builder, array $options) new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], $parts, 'text' === $options['widget'], $options['reference_date']) )); } + + if (\in_array($options['input'], ['datetime', 'datetime_immutable'], true) && null !== $options['model_timezone']) { + $builder->addEventListener(FormEvents::POST_SET_DATA, static function (FormEvent $event) use ($options): void { + $date = $event->getData(); + + if (!$date instanceof \DateTimeInterface) { + return; + } + + 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'])); + } + }); + } } - /** - * {@inheritdoc} - */ - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { $view->vars = array_replace($view->vars, [ 'widget' => $options['widget'], @@ -233,26 +239,23 @@ public function buildView(FormView $view, FormInterface $form, array $options) // adding the HTML attribute step if not already defined. // Otherwise the browser will not display and so not send the seconds // therefore the value will always be considered as invalid. - if ($options['with_seconds'] && !isset($view->vars['attr']['step'])) { - $view->vars['attr']['step'] = 1; + if (!isset($view->vars['attr']['step'])) { + if ($options['with_seconds']) { + $view->vars['attr']['step'] = 1; + } elseif (!$options['with_minutes']) { + $view->vars['attr']['step'] = 3600; + } } } } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { - $compound = function (Options $options) { - return 'single_text' !== $options['widget']; - }; + $compound = static fn (Options $options) => 'single_text' !== $options['widget']; - $placeholderDefault = function (Options $options) { - return $options['required'] ? null : ''; - }; + $placeholderDefault = static fn (Options $options) => $options['required'] ? null : ''; - $placeholderNormalizer = function (Options $options, $placeholder) use ($placeholderDefault) { + $placeholderNormalizer = static function (Options $options, $placeholder) use ($placeholderDefault) { if (\is_array($placeholder)) { $default = $placeholderDefault($options); @@ -269,12 +272,10 @@ public function configureOptions(OptionsResolver $resolver) ]; }; - $choiceTranslationDomainNormalizer = function (Options $options, $choiceTranslationDomain) { + $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 ); } @@ -314,7 +315,7 @@ public function configureOptions(OptionsResolver $resolver) 'hours' => range(0, 23), 'minutes' => range(0, 59), 'seconds' => range(0, 59), - 'widget' => 'choice', + 'widget' => 'single_text', 'input' => 'datetime', 'input_format' => 'H:i:s', 'with_minutes' => true, @@ -333,19 +334,13 @@ public function configureOptions(OptionsResolver $resolver) // representation is not \DateTime, but an array, we need to unset // this option. 'data_class' => null, - 'empty_data' => function (Options $options) { - return $options['compound'] ? [] : ''; - }, + 'empty_data' => static fn (Options $options) => $options['compound'] ? [] : '', 'compound' => $compound, 'choice_translation_domain' => false, - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please enter a valid time.'; - }, + 'invalid_message' => 'Please enter a valid time.', ]); - $resolver->setNormalizer('view_timezone', function (Options $options, $viewTimezone): ?string { + $resolver->setNormalizer('view_timezone', static function (Options $options, $viewTimezone): ?string { if (null !== $options['model_timezone'] && $viewTimezone !== $options['model_timezone'] && null === $options['reference_date']) { throw new LogicException('Using different values for the "model_timezone" and "view_timezone" options without configuring a reference date is not supported.'); } @@ -378,10 +373,7 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setAllowedTypes('reference_date', ['null', \DateTimeInterface::class]); } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'time'; } diff --git a/Extension/Core/Type/TimezoneType.php b/Extension/Core/Type/TimezoneType.php index 6ee109ad6c..2316d666b5 100644 --- a/Extension/Core/Type/TimezoneType.php +++ b/Extension/Core/Type/TimezoneType.php @@ -25,10 +25,7 @@ class TimezoneType extends AbstractType { - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { if ('datetimezone' === $options['input']) { $builder->addModelTransformer(new DateTimeZoneToStringTransformer($options['multiple'])); @@ -37,10 +34,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) } } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'intl' => false, @@ -49,35 +43,27 @@ public function configureOptions(OptionsResolver $resolver) 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']; - return ChoiceList::loader($this, new IntlCallbackChoiceLoader(function () use ($input, $choiceTranslationLocale) { - return self::getIntlTimezones($input, $choiceTranslationLocale); - }), [$input, $choiceTranslationLocale]); + return ChoiceList::loader($this, new IntlCallbackChoiceLoader(static fn () => self::getIntlTimezones($input, $choiceTranslationLocale)), [$input, $choiceTranslationLocale]); } - return ChoiceList::lazy($this, function () use ($input) { - return self::getPhpTimezones($input); - }, $input); + return ChoiceList::lazy($this, static fn () => self::getPhpTimezones($input), $input); }, 'choice_translation_domain' => false, 'choice_translation_locale' => null, 'input' => 'string', - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please select a valid timezone.'; - }, + 'invalid_message' => 'Please select a valid timezone.', 'regions' => \DateTimeZone::ALL, ]); $resolver->setAllowedTypes('intl', ['bool']); $resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']); - $resolver->setNormalizer('choice_translation_locale', function (Options $options, $value) { + $resolver->setNormalizer('choice_translation_locale', static function (Options $options, $value) { if (null !== $value && !$options['intl']) { throw new LogicException('The "choice_translation_locale" option can only be used if the "intl" option is set to true.'); } @@ -86,7 +72,7 @@ public function configureOptions(OptionsResolver $resolver) }); $resolver->setAllowedValues('input', ['string', 'datetimezone', 'intltimezone']); - $resolver->setNormalizer('input', function (Options $options, $value) { + $resolver->setNormalizer('input', static function (Options $options, $value) { if ('intltimezone' === $value && !class_exists(\IntlTimeZone::class)) { throw new LogicException('Cannot use "intltimezone" input because the PHP intl extension is not available.'); } @@ -95,18 +81,12 @@ public function configureOptions(OptionsResolver $resolver) }); } - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?string { return ChoiceType::class; } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'timezone'; } diff --git a/Extension/Core/Type/TransformationFailureExtension.php b/Extension/Core/Type/TransformationFailureExtension.php index 4de50a90af..e90cd714e8 100644 --- a/Extension/Core/Type/TransformationFailureExtension.php +++ b/Extension/Core/Type/TransformationFailureExtension.php @@ -21,23 +21,18 @@ */ class TransformationFailureExtension extends AbstractTypeExtension { - private $translator; - - public function __construct(?TranslatorInterface $translator = null) - { - $this->translator = $translator; + public function __construct( + private ?TranslatorInterface $translator = null, + ) { } - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { if (!isset($options['constraints'])) { $builder->addEventSubscriber(new TransformationFailureListener($this->translator)); } } - /** - * {@inheritdoc} - */ public static function getExtendedTypes(): iterable { return [FormType::class]; diff --git a/Extension/Core/Type/UlidType.php b/Extension/Core/Type/UlidType.php index 640d38ffa9..a17b9d9c1d 100644 --- a/Extension/Core/Type/UlidType.php +++ b/Extension/Core/Type/UlidType.php @@ -14,7 +14,6 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\DataTransformer\UlidToStringTransformer; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; /** @@ -22,28 +21,18 @@ */ class UlidType extends AbstractType { - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->addViewTransformer(new UlidToStringTransformer()) ; } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'compound' => false, - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please enter a valid ULID.'; - }, + 'invalid_message' => 'Please enter a valid ULID.', ]); } } diff --git a/Extension/Core/Type/UrlType.php b/Extension/Core/Type/UrlType.php index f294a10ac2..fd6025729a 100644 --- a/Extension/Core/Type/UrlType.php +++ b/Extension/Core/Type/UrlType.php @@ -21,20 +21,14 @@ class UrlType extends AbstractType { - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { if (null !== $options['default_protocol']) { $builder->addEventSubscriber(new FixUrlProtocolListener($options['default_protocol'])); } } - /** - * {@inheritdoc} - */ - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { if ($options['default_protocol']) { $view->vars['attr']['inputmode'] = 'url'; @@ -42,35 +36,26 @@ public function buildView(FormView $view, FormInterface $form, array $options) } } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'default_protocol' => 'http', - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please enter a valid URL.'; + '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.', ]); $resolver->setAllowedTypes('default_protocol', ['null', 'string']); } - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?string { return TextType::class; } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'url'; } diff --git a/Extension/Core/Type/UuidType.php b/Extension/Core/Type/UuidType.php index 0c27802b37..1035939a12 100644 --- a/Extension/Core/Type/UuidType.php +++ b/Extension/Core/Type/UuidType.php @@ -14,7 +14,6 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\DataTransformer\UuidToStringTransformer; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; /** @@ -22,28 +21,18 @@ */ class UuidType extends AbstractType { - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->addViewTransformer(new UuidToStringTransformer()) ; } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'compound' => false, - 'invalid_message' => function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) - ? $previousValue - : 'Please enter a valid UUID.'; - }, + 'invalid_message' => 'Please enter a valid UUID.', ]); } } diff --git a/Extension/Core/Type/WeekType.php b/Extension/Core/Type/WeekType.php index b7f8887d95..c4827604e5 100644 --- a/Extension/Core/Type/WeekType.php +++ b/Extension/Core/Type/WeekType.php @@ -28,10 +28,7 @@ class WeekType extends AbstractType 'choice' => ChoiceType::class, ]; - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { if ('string' === $options['input']) { $builder->addModelTransformer(new WeekToArrayTransformer()); @@ -42,7 +39,6 @@ public function buildForm(FormBuilderInterface $builder, array $options) } 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 @@ -83,10 +79,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) } } - /** - * {@inheritdoc} - */ - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { $view->vars['widget'] = $options['widget']; @@ -95,20 +88,13 @@ public function buildView(FormView $view, FormInterface $form, array $options) } } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { - $compound = function (Options $options) { - return 'single_text' !== $options['widget']; - }; + $compound = static fn (Options $options) => 'single_text' !== $options['widget']; - $placeholderDefault = function (Options $options) { - return $options['required'] ? null : ''; - }; + $placeholderDefault = static fn (Options $options) => $options['required'] ? null : ''; - $placeholderNormalizer = function (Options $options, $placeholder) use ($placeholderDefault) { + $placeholderNormalizer = static function (Options $options, $placeholder) use ($placeholderDefault) { if (\is_array($placeholder)) { $default = $placeholderDefault($options); @@ -124,12 +110,10 @@ public function configureOptions(OptionsResolver $resolver) ]; }; - $choiceTranslationDomainNormalizer = function (Options $options, $choiceTranslationDomain) { + $choiceTranslationDomainNormalizer = static function (Options $options, $choiceTranslationDomain) { if (\is_array($choiceTranslationDomain)) { - $default = false; - return array_replace( - ['year' => $default, 'week' => $default], + ['year' => false, 'week' => false], $choiceTranslationDomain ); } @@ -146,25 +130,19 @@ public function configureOptions(OptionsResolver $resolver) 'widget' => 'single_text', 'input' => 'array', 'placeholder' => $placeholderDefault, - 'html5' => static function (Options $options) { - return 'single_text' === $options['widget']; - }, + 'html5' => static fn (Options $options) => 'single_text' === $options['widget'], 'error_bubbling' => false, - 'empty_data' => function (Options $options) { - return $options['compound'] ? [] : ''; - }, + 'empty_data' => static fn (Options $options) => $options['compound'] ? [] : '', 'compound' => $compound, 'choice_translation_domain' => false, - 'invalid_message' => static function (Options $options, $previousValue) { - return ($options['legacy_error_messages'] ?? true) ? $previousValue : 'Please enter a valid week.'; - }, + 'invalid_message' => 'Please enter a valid week.', ]); $resolver->setNormalizer('placeholder', $placeholderNormalizer); $resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer); - $resolver->setNormalizer('html5', function (Options $options, $html5) { + $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; @@ -185,10 +163,7 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setAllowedTypes('weeks', 'int[]'); } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'week'; } diff --git a/Extension/Csrf/CsrfExtension.php b/Extension/Csrf/CsrfExtension.php index d86574ed67..33c4616b4c 100644 --- a/Extension/Csrf/CsrfExtension.php +++ b/Extension/Csrf/CsrfExtension.php @@ -22,21 +22,14 @@ */ class CsrfExtension extends AbstractExtension { - private $tokenManager; - private $translator; - private $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, + ) { } - /** - * {@inheritdoc} - */ - protected function loadTypeExtensions() + protected function loadTypeExtensions(): array { return [ new Type\FormTypeCsrfExtension($this->tokenManager, true, '_token', $this->translator, $this->translationDomain), diff --git a/Extension/Csrf/EventListener/CsrfValidationListener.php b/Extension/Csrf/EventListener/CsrfValidationListener.php index 89eb5c4ff8..44922402ff 100644 --- a/Extension/Csrf/EventListener/CsrfValidationListener.php +++ b/Extension/Csrf/EventListener/CsrfValidationListener.php @@ -25,33 +25,28 @@ */ class CsrfValidationListener implements EventSubscriberInterface { - private $fieldName; - private $tokenManager; - private $tokenId; - private $errorMessage; - private $translator; - private $translationDomain; - private $serverParams; + private ServerParams $serverParams; - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ FormEvents::PRE_SUBMIT => 'preSubmit', ]; } - 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(); } - public function preSubmit(FormEvent $event) + public function preSubmit(FormEvent $event): void { $form = $event->getForm(); $postRequestSizeExceeded = 'POST' === $form->getConfig()->getMethod() && $this->serverParams->hasPostMaxSizeBeenExceeded(); diff --git a/Extension/Csrf/Type/FormTypeCsrfExtension.php b/Extension/Csrf/Type/FormTypeCsrfExtension.php index dfb3fec46b..a12b9a41ee 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,37 +29,38 @@ */ class FormTypeCsrfExtension extends AbstractTypeExtension { - private $defaultTokenManager; - private $defaultEnabled; - private $defaultFieldName; - private $translator; - private $translationDomain; - private $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, + ) { } /** * Adds a CSRF field to the form when the CSRF protection is enabled. */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { if (!$options['csrf_protection']) { 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() ?: \get_class($builder->getType()->getInnerType())), + $csrfTokenId, $options['csrf_message'], $this->translator, $this->translationDomain, @@ -70,39 +72,47 @@ public function buildForm(FormBuilderInterface $builder, array $options) /** * Adds a CSRF field to the root form view. */ - public function finishView(FormView $view, FormInterface $form, array $options) + public function finishView(FormView $view, FormInterface $form, array $options): void { if ($options['csrf_protection'] && !$view->parent && $options['compound']) { $factory = $form->getConfig()->getFormFactory(); - $tokenId = $options['csrf_token_id'] ?: ($form->getName() ?: \get_class($form->getConfig()->getType()->getInnerType())); + $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); } } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + 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']); } - /** - * {@inheritdoc} - */ public static function getExtendedTypes(): iterable { return [FormType::class]; diff --git a/Extension/DataCollector/DataCollectorExtension.php b/Extension/DataCollector/DataCollectorExtension.php index 4b23513b7c..9fb8422447 100644 --- a/Extension/DataCollector/DataCollectorExtension.php +++ b/Extension/DataCollector/DataCollectorExtension.php @@ -21,17 +21,12 @@ */ class DataCollectorExtension extends AbstractExtension { - private $dataCollector; - - public function __construct(FormDataCollectorInterface $dataCollector) - { - $this->dataCollector = $dataCollector; + public function __construct( + private FormDataCollectorInterface $dataCollector, + ) { } - /** - * {@inheritdoc} - */ - protected function loadTypeExtensions() + protected function loadTypeExtensions(): array { return [ new Type\DataCollectorTypeExtension($this->dataCollector), diff --git a/Extension/DataCollector/EventListener/DataCollectorListener.php b/Extension/DataCollector/EventListener/DataCollectorListener.php index 77595ab38a..c541efec25 100644 --- a/Extension/DataCollector/EventListener/DataCollectorListener.php +++ b/Extension/DataCollector/EventListener/DataCollectorListener.php @@ -24,21 +24,16 @@ */ class DataCollectorListener implements EventSubscriberInterface { - private $dataCollector; - - public function __construct(FormDataCollectorInterface $dataCollector) - { - $this->dataCollector = $dataCollector; + public function __construct( + private FormDataCollectorInterface $dataCollector, + ) { } - /** - * {@inheritdoc} - */ - public static function getSubscribedEvents() + 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], ]; @@ -47,7 +42,7 @@ public static function getSubscribedEvents() /** * Listener for the {@link FormEvents::POST_SET_DATA} event. */ - public function postSetData(FormEvent $event) + public function postSetData(FormEvent $event): void { if ($event->getForm()->isRoot()) { // Collect basic information about each form @@ -61,7 +56,7 @@ public function postSetData(FormEvent $event) /** * Listener for the {@link FormEvents::POST_SUBMIT} event. */ - public function postSubmit(FormEvent $event) + public function postSubmit(FormEvent $event): void { if ($event->getForm()->isRoot()) { // Collect the submitted data of each form diff --git a/Extension/DataCollector/FormDataCollector.php b/Extension/DataCollector/FormDataCollector.php index 8d4f8e0ea7..e6cae863e8 100644 --- a/Extension/DataCollector/FormDataCollector.php +++ b/Extension/DataCollector/FormDataCollector.php @@ -20,6 +20,7 @@ use Symfony\Component\VarDumper\Caster\Caster; use Symfony\Component\VarDumper\Caster\ClassStub; use Symfony\Component\VarDumper\Caster\StubCaster; +use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Cloner\Stub; /** @@ -32,18 +33,14 @@ */ class FormDataCollector extends DataCollector implements FormDataCollectorInterface { - private $dataExtractor; - /** * Stores the collected data per {@link FormInterface} instance. * * Uses the hashes of the forms as keys. This is preferable over using * {@link \SplObjectStorage}, because in this way no references are kept * to the {@link FormInterface} instances. - * - * @var array */ - private $dataByForm; + private array $dataByForm; /** * Stores the collected data per {@link FormView} instance. @@ -51,10 +48,8 @@ class FormDataCollector extends DataCollector implements FormDataCollectorInterf * Uses the hashes of the views as keys. This is preferable over using * {@link \SplObjectStorage}, because in this way no references are kept * to the {@link FormView} instances. - * - * @var array */ - private $dataByView; + private array $dataByView; /** * Connects {@link FormView} with {@link FormInterface} instances. @@ -62,30 +57,27 @@ class FormDataCollector extends DataCollector implements FormDataCollectorInterf * Uses the hashes of the views as keys and the hashes of the forms as * values. This is preferable over storing the objects directly, because * this way they can safely be discarded by the GC. - * - * @var array */ - private $formsByView; + 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(); } /** * Does nothing. The data is collected during the form event listeners. */ - public function collect(Request $request, Response $response, ?\Throwable $exception = null) + public function collect(Request $request, Response $response, ?\Throwable $exception = null): void { } - public function reset() + public function reset(): void { $this->data = [ 'forms' => [], @@ -94,18 +86,12 @@ public function reset() ]; } - /** - * {@inheritdoc} - */ - public function associateFormWithView(FormInterface $form, FormView $view) + public function associateFormWithView(FormInterface $form, FormView $view): void { $this->formsByView[spl_object_hash($view)] = spl_object_hash($form); } - /** - * {@inheritdoc} - */ - public function collectConfiguration(FormInterface $form) + public function collectConfiguration(FormInterface $form): void { $hash = spl_object_hash($form); @@ -123,10 +109,7 @@ public function collectConfiguration(FormInterface $form) } } - /** - * {@inheritdoc} - */ - public function collectDefaultData(FormInterface $form) + public function collectDefaultData(FormInterface $form): void { $hash = spl_object_hash($form); @@ -145,10 +128,7 @@ public function collectDefaultData(FormInterface $form) } } - /** - * {@inheritdoc} - */ - public function collectSubmittedData(FormInterface $form) + public function collectSubmittedData(FormInterface $form): void { $hash = spl_object_hash($form); @@ -179,10 +159,7 @@ public function collectSubmittedData(FormInterface $form) } } - /** - * {@inheritdoc} - */ - public function collectViewVariables(FormView $view) + public function collectViewVariables(FormView $view): void { $hash = spl_object_hash($view); @@ -200,34 +177,22 @@ public function collectViewVariables(FormView $view) } } - /** - * {@inheritdoc} - */ - public function buildPreliminaryFormTree(FormInterface $form) + public function buildPreliminaryFormTree(FormInterface $form): void { $this->data['forms'][$form->getName()] = &$this->recursiveBuildPreliminaryFormTree($form, $this->data['forms_by_hash']); } - /** - * {@inheritdoc} - */ - public function buildFinalFormTree(FormInterface $form, FormView $view) + public function buildFinalFormTree(FormInterface $form, FormView $view): void { $this->data['forms'][$form->getName()] = &$this->recursiveBuildFinalFormTree($form, $view, $this->data['forms_by_hash']); } - /** - * {@inheritdoc} - */ public function getName(): string { return 'form'; } - /** - * {@inheritdoc} - */ - public function getData() + public function getData(): array|Data { return $this->data; } @@ -248,13 +213,10 @@ public function __sleep(): array return parent::__sleep(); } - /** - * {@inheritdoc} - */ protected function getCasters(): array { return parent::getCasters() + [ - \Exception::class => function (\Exception $e, array $a, Stub $s) { + \Exception::class => static function (\Exception $e, array $a, Stub $s) { foreach (["\0Exception\0previous", "\0Exception\0trace"] as $k) { if (isset($a[$k])) { unset($a[$k]); @@ -264,24 +226,20 @@ protected function getCasters(): array return $a; }, - FormInterface::class => function (FormInterface $f, array $a) { - return [ - Caster::PREFIX_VIRTUAL.'name' => $f->getName(), - Caster::PREFIX_VIRTUAL.'type_class' => new ClassStub(\get_class($f->getConfig()->getType()->getInnerType())), - ]; - }, - FormView::class => [StubCaster::class, 'cutInternals'], - ConstraintViolationInterface::class => function (ConstraintViolationInterface $v, array $a) { - return [ - Caster::PREFIX_VIRTUAL.'root' => $v->getRoot(), - Caster::PREFIX_VIRTUAL.'path' => $v->getPropertyPath(), - Caster::PREFIX_VIRTUAL.'value' => $v->getInvalidValue(), - ]; - }, + FormInterface::class => static fn (FormInterface $f, array $a) => [ + Caster::PREFIX_VIRTUAL.'name' => $f->getName(), + Caster::PREFIX_VIRTUAL.'type_class' => new ClassStub($f->getConfig()->getType()->getInnerType()::class), + ], + FormView::class => StubCaster::cutInternals(...), + ConstraintViolationInterface::class => static fn (ConstraintViolationInterface $v, array $a) => [ + Caster::PREFIX_VIRTUAL.'root' => $v->getRoot(), + Caster::PREFIX_VIRTUAL.'path' => $v->getPropertyPath(), + Caster::PREFIX_VIRTUAL.'value' => $v->getInvalidValue(), + ], ]; } - private function &recursiveBuildPreliminaryFormTree(FormInterface $form, array &$outputByHash) + private function &recursiveBuildPreliminaryFormTree(FormInterface $form, array &$outputByHash): array { $hash = spl_object_hash($form); @@ -298,7 +256,7 @@ private function &recursiveBuildPreliminaryFormTree(FormInterface $form, array & return $output; } - private function &recursiveBuildFinalFormTree(?FormInterface $form, FormView $view, array &$outputByHash) + private function &recursiveBuildFinalFormTree(?FormInterface $form, FormView $view, array &$outputByHash): array { $viewHash = spl_object_hash($view); $formHash = null; @@ -331,9 +289,7 @@ private function &recursiveBuildFinalFormTree(?FormInterface $form, FormView $vi foreach ($view->children as $name => $childView) { // The CSRF token, for example, is never added to the form tree. // It is only present in the view. - $childForm = null !== $form && $form->has($name) - ? $form->get($name) - : null; + $childForm = $form?->has($name) ? $form->get($name) : null; $output['children'][$name] = &$this->recursiveBuildFinalFormTree($childForm, $childView, $outputByHash); } diff --git a/Extension/DataCollector/FormDataCollectorInterface.php b/Extension/DataCollector/FormDataCollectorInterface.php index 64b8f83c4b..7c7903980b 100644 --- a/Extension/DataCollector/FormDataCollectorInterface.php +++ b/Extension/DataCollector/FormDataCollectorInterface.php @@ -26,27 +26,27 @@ interface FormDataCollectorInterface extends DataCollectorInterface /** * Stores configuration data of the given form and its children. */ - public function collectConfiguration(FormInterface $form); + public function collectConfiguration(FormInterface $form): void; /** * Stores the default data of the given form and its children. */ - public function collectDefaultData(FormInterface $form); + public function collectDefaultData(FormInterface $form): void; /** * Stores the submitted data of the given form and its children. */ - public function collectSubmittedData(FormInterface $form); + public function collectSubmittedData(FormInterface $form): void; /** * Stores the view variables of the given form view and its children. */ - public function collectViewVariables(FormView $view); + public function collectViewVariables(FormView $view): void; /** * Specifies that the given objects represent the same conceptual form. */ - public function associateFormWithView(FormInterface $form, FormView $view); + public function associateFormWithView(FormInterface $form, FormView $view): void; /** * Assembles the data collected about the given form and its children as @@ -54,7 +54,7 @@ public function associateFormWithView(FormInterface $form, FormView $view); * * The result can be queried using {@link getData()}. */ - public function buildPreliminaryFormTree(FormInterface $form); + public function buildPreliminaryFormTree(FormInterface $form): void; /** * Assembles the data collected about the given form and its children as @@ -74,12 +74,10 @@ public function buildPreliminaryFormTree(FormInterface $form); * corresponding {@link FormInterface} exists otherwise, call * {@link associateFormWithView()} before calling this method. */ - public function buildFinalFormTree(FormInterface $form, FormView $view); + public function buildFinalFormTree(FormInterface $form, FormView $view): void; /** * Returns all collected data. - * - * @return array|Data */ - public function getData(); + public function getData(): array|Data; } diff --git a/Extension/DataCollector/FormDataExtractor.php b/Extension/DataCollector/FormDataExtractor.php index 9cecc720e3..f56fe911fa 100644 --- a/Extension/DataCollector/FormDataExtractor.php +++ b/Extension/DataCollector/FormDataExtractor.php @@ -22,15 +22,12 @@ */ class FormDataExtractor implements FormDataExtractorInterface { - /** - * {@inheritdoc} - */ - public function extractConfiguration(FormInterface $form) + public function extractConfiguration(FormInterface $form): array { $data = [ 'id' => $this->buildId($form), 'name' => $form->getName(), - 'type_class' => \get_class($form->getConfig()->getType()->getInnerType()), + 'type_class' => $form->getConfig()->getType()->getInnerType()::class, 'synchronized' => $form->isSynchronized(), 'passed_options' => [], 'resolved_options' => [], @@ -50,10 +47,7 @@ public function extractConfiguration(FormInterface $form) return $data; } - /** - * {@inheritdoc} - */ - public function extractDefaultData(FormInterface $form) + public function extractDefaultData(FormInterface $form): array { $data = [ 'default_data' => [ @@ -73,10 +67,7 @@ public function extractDefaultData(FormInterface $form) return $data; } - /** - * {@inheritdoc} - */ - public function extractSubmittedData(FormInterface $form) + public function extractSubmittedData(FormInterface $form): array { $data = [ 'submitted_data' => [ @@ -112,15 +103,13 @@ public function extractSubmittedData(FormInterface $form) continue; } + $errorData['trace'][] = $cause; if ($cause instanceof \Exception) { - $errorData['trace'][] = $cause; $cause = $cause->getPrevious(); continue; } - $errorData['trace'][] = $cause; - break; } @@ -132,10 +121,7 @@ public function extractSubmittedData(FormInterface $form) return $data; } - /** - * {@inheritdoc} - */ - public function extractViewVariables(FormView $view) + public function extractViewVariables(FormView $view): array { $data = [ 'id' => $view->vars['id'] ?? null, diff --git a/Extension/DataCollector/FormDataExtractorInterface.php b/Extension/DataCollector/FormDataExtractorInterface.php index 7e82286adf..d6e46d4670 100644 --- a/Extension/DataCollector/FormDataExtractorInterface.php +++ b/Extension/DataCollector/FormDataExtractorInterface.php @@ -23,29 +23,21 @@ interface FormDataExtractorInterface { /** * Extracts the configuration data of a form. - * - * @return array */ - public function extractConfiguration(FormInterface $form); + public function extractConfiguration(FormInterface $form): array; /** * Extracts the default data of a form. - * - * @return array */ - public function extractDefaultData(FormInterface $form); + public function extractDefaultData(FormInterface $form): array; /** * Extracts the submitted data of a form. - * - * @return array */ - public function extractSubmittedData(FormInterface $form); + public function extractSubmittedData(FormInterface $form): array; /** * Extracts the view variables of a form. - * - * @return array */ - public function extractViewVariables(FormView $view); + public function extractViewVariables(FormView $view): array; } diff --git a/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php b/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php index 89aac42286..90e28a61fb 100644 --- a/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php +++ b/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php @@ -15,8 +15,10 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormTypeInterface; use Symfony\Component\Form\FormView; use Symfony\Component\Form\ResolvedFormTypeInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; /** * Proxy that invokes a data collector when creating a form and its view. @@ -25,51 +27,33 @@ */ class ResolvedTypeDataCollectorProxy implements ResolvedFormTypeInterface { - private $proxiedType; - private $dataCollector; - - public function __construct(ResolvedFormTypeInterface $proxiedType, FormDataCollectorInterface $dataCollector) - { - $this->proxiedType = $proxiedType; - $this->dataCollector = $dataCollector; + public function __construct( + private ResolvedFormTypeInterface $proxiedType, + private FormDataCollectorInterface $dataCollector, + ) { } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return $this->proxiedType->getBlockPrefix(); } - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?ResolvedFormTypeInterface { return $this->proxiedType->getParent(); } - /** - * {@inheritdoc} - */ - public function getInnerType() + public function getInnerType(): FormTypeInterface { return $this->proxiedType->getInnerType(); } - /** - * {@inheritdoc} - */ - public function getTypeExtensions() + public function getTypeExtensions(): array { return $this->proxiedType->getTypeExtensions(); } - /** - * {@inheritdoc} - */ - public function createBuilder(FormFactoryInterface $factory, string $name, array $options = []) + public function createBuilder(FormFactoryInterface $factory, string $name, array $options = []): FormBuilderInterface { $builder = $this->proxiedType->createBuilder($factory, $name, $options); @@ -79,34 +63,22 @@ public function createBuilder(FormFactoryInterface $factory, string $name, array return $builder; } - /** - * {@inheritdoc} - */ - public function createView(FormInterface $form, ?FormView $parent = null) + public function createView(FormInterface $form, ?FormView $parent = null): FormView { return $this->proxiedType->createView($form, $parent); } - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $this->proxiedType->buildForm($builder, $options); } - /** - * {@inheritdoc} - */ - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { $this->proxiedType->buildView($view, $form, $options); } - /** - * {@inheritdoc} - */ - public function finishView(FormView $view, FormInterface $form, array $options) + public function finishView(FormView $view, FormInterface $form, array $options): void { $this->proxiedType->finishView($view, $form, $options); @@ -130,10 +102,7 @@ public function finishView(FormView $view, FormInterface $form, array $options) } } - /** - * {@inheritdoc} - */ - public function getOptionsResolver() + public function getOptionsResolver(): OptionsResolver { return $this->proxiedType->getOptionsResolver(); } diff --git a/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php b/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php index c59271461a..a052a17829 100644 --- a/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php +++ b/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php @@ -24,19 +24,13 @@ */ class ResolvedTypeFactoryDataCollectorProxy implements ResolvedFormTypeFactoryInterface { - private $proxiedFactory; - private $dataCollector; - - public function __construct(ResolvedFormTypeFactoryInterface $proxiedFactory, FormDataCollectorInterface $dataCollector) - { - $this->proxiedFactory = $proxiedFactory; - $this->dataCollector = $dataCollector; + public function __construct( + private ResolvedFormTypeFactoryInterface $proxiedFactory, + private FormDataCollectorInterface $dataCollector, + ) { } - /** - * {@inheritdoc} - */ - public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ?ResolvedFormTypeInterface $parent = null) + public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ?ResolvedFormTypeInterface $parent = null): ResolvedFormTypeInterface { return new ResolvedTypeDataCollectorProxy( $this->proxiedFactory->createResolvedType($type, $typeExtensions, $parent), diff --git a/Extension/DataCollector/Type/DataCollectorTypeExtension.php b/Extension/DataCollector/Type/DataCollectorTypeExtension.php index d25fd132c0..0f40968e44 100644 --- a/Extension/DataCollector/Type/DataCollectorTypeExtension.php +++ b/Extension/DataCollector/Type/DataCollectorTypeExtension.php @@ -25,27 +25,18 @@ */ class DataCollectorTypeExtension extends AbstractTypeExtension { - /** - * @var DataCollectorListener - */ - private $listener; + private DataCollectorListener $listener; public function __construct(FormDataCollectorInterface $dataCollector) { $this->listener = new DataCollectorListener($dataCollector); } - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->addEventSubscriber($this->listener); } - /** - * {@inheritdoc} - */ public static function getExtendedTypes(): iterable { return [FormType::class]; diff --git a/Extension/DependencyInjection/DependencyInjectionExtension.php b/Extension/DependencyInjection/DependencyInjectionExtension.php index 5d61a0693a..f986bda415 100644 --- a/Extension/DependencyInjection/DependencyInjectionExtension.php +++ b/Extension/DependencyInjection/DependencyInjectionExtension.php @@ -14,50 +14,41 @@ use Psr\Container\ContainerInterface; use Symfony\Component\Form\Exception\InvalidArgumentException; use Symfony\Component\Form\FormExtensionInterface; +use Symfony\Component\Form\FormTypeExtensionInterface; use Symfony\Component\Form\FormTypeGuesserChain; +use Symfony\Component\Form\FormTypeGuesserInterface; +use Symfony\Component\Form\FormTypeInterface; class DependencyInjectionExtension implements FormExtensionInterface { - private $guesser; - private $guesserLoaded = false; - private $typeContainer; - private $typeExtensionServices; - private $guesserServices; + private ?FormTypeGuesserChain $guesser = null; + private bool $guesserLoaded = false; /** - * @param iterable[] $typeExtensionServices + * @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, + ) { } - /** - * {@inheritdoc} - */ - public function getType(string $name) + 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); } - /** - * {@inheritdoc} - */ - public function hasType(string $name) + public function hasType(string $name): bool { return $this->typeContainer->has($name); } - /** - * {@inheritdoc} - */ - public function getTypeExtensions(string $name) + public function getTypeExtensions(string $name): array { $extensions = []; @@ -72,7 +63,7 @@ public function getTypeExtensions(string $name) // 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, \get_class($extension), 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))); } } } @@ -80,18 +71,12 @@ public function getTypeExtensions(string $name) return $extensions; } - /** - * {@inheritdoc} - */ - public function hasTypeExtensions(string $name) + public function hasTypeExtensions(string $name): bool { return isset($this->typeExtensionServices[$name]); } - /** - * {@inheritdoc} - */ - public function getTypeGuesser() + public function getTypeGuesser(): ?FormTypeGuesserInterface { if (!$this->guesserLoaded) { $this->guesserLoaded = true; diff --git a/Extension/HtmlSanitizer/HtmlSanitizerExtension.php b/Extension/HtmlSanitizer/HtmlSanitizerExtension.php new file mode 100644 index 0000000000..6c4bf49d6a --- /dev/null +++ b/Extension/HtmlSanitizer/HtmlSanitizerExtension.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\HtmlSanitizer; + +use Psr\Container\ContainerInterface; +use Symfony\Component\Form\AbstractExtension; + +/** + * Integrates the HtmlSanitizer component with the Form library. + * + * @author Nicolas Grekas + */ +class HtmlSanitizerExtension extends AbstractExtension +{ + public function __construct( + private ContainerInterface $sanitizers, + private string $defaultSanitizer = 'default', + ) { + } + + protected function loadTypeExtensions(): array + { + return [ + new Type\TextTypeHtmlSanitizerExtension($this->sanitizers, $this->defaultSanitizer), + ]; + } +} diff --git a/Extension/HtmlSanitizer/Type/TextTypeHtmlSanitizerExtension.php b/Extension/HtmlSanitizer/Type/TextTypeHtmlSanitizerExtension.php new file mode 100644 index 0000000000..8c339a0675 --- /dev/null +++ b/Extension/HtmlSanitizer/Type/TextTypeHtmlSanitizerExtension.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\HtmlSanitizer\Type; + +use Psr\Container\ContainerInterface; +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Titouan Galopin + */ +class TextTypeHtmlSanitizerExtension extends AbstractTypeExtension +{ + public function __construct( + private ContainerInterface $sanitizers, + private string $defaultSanitizer = 'default', + ) { + } + + public static function getExtendedTypes(): iterable + { + return [TextType::class]; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setDefaults(['sanitize_html' => false, 'sanitizer' => null]) + ->setAllowedTypes('sanitize_html', 'bool') + ->setAllowedTypes('sanitizer', ['string', 'null']) + ; + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + if (!$options['sanitize_html']) { + return; + } + + $sanitizers = $this->sanitizers; + $sanitizer = $options['sanitizer'] ?? $this->defaultSanitizer; + + $builder->addEventListener( + FormEvents::PRE_SUBMIT, + static function (FormEvent $event) use ($sanitizers, $sanitizer) { + if (\is_scalar($data = $event->getData()) && '' !== trim($data)) { + $event->setData($sanitizers->get($sanitizer)->sanitize($data)); + } + }, + 10000 /* as soon as possible */ + ); + } +} diff --git a/Extension/HttpFoundation/HttpFoundationExtension.php b/Extension/HttpFoundation/HttpFoundationExtension.php index 27305ada41..85bc4f4720 100644 --- a/Extension/HttpFoundation/HttpFoundationExtension.php +++ b/Extension/HttpFoundation/HttpFoundationExtension.php @@ -20,7 +20,7 @@ */ class HttpFoundationExtension extends AbstractExtension { - protected function loadTypeExtensions() + protected function loadTypeExtensions(): array { return [ new Type\FormTypeHttpFoundationExtension(), diff --git a/Extension/HttpFoundation/HttpFoundationRequestHandler.php b/Extension/HttpFoundation/HttpFoundationRequestHandler.php index a047729fa8..d7875e7b0d 100644 --- a/Extension/HttpFoundation/HttpFoundationRequestHandler.php +++ b/Extension/HttpFoundation/HttpFoundationRequestHandler.php @@ -29,20 +29,17 @@ */ class HttpFoundationRequestHandler implements RequestHandlerInterface { - private $serverParams; + private ServerParams $serverParams; public function __construct(?ServerParams $serverParams = null) { $this->serverParams = $serverParams ?? new ServerParams(); } - /** - * {@inheritdoc} - */ - public function handleRequest(FormInterface $form, $request = null) + public function handleRequest(FormInterface $form, mixed $request = null): void { if (!$request instanceof Request) { - throw new UnexpectedTypeException($request, 'Symfony\Component\HttpFoundation\Request'); + throw new UnexpectedTypeException($request, Request::class); } $name = $form->getName(); @@ -110,18 +107,12 @@ public function handleRequest(FormInterface $form, $request = null) $form->submit($data, 'PATCH' !== $method); } - /** - * {@inheritdoc} - */ - public function isFileUpload($data) + public function isFileUpload(mixed $data): bool { return $data instanceof File; } - /** - * @return int|null - */ - public function getUploadFileError($data) + public function getUploadFileError(mixed $data): ?int { if (!$data instanceof UploadedFile || $data->isValid()) { return null; diff --git a/Extension/HttpFoundation/Type/FormTypeHttpFoundationExtension.php b/Extension/HttpFoundation/Type/FormTypeHttpFoundationExtension.php index b789af2293..5139308bd7 100644 --- a/Extension/HttpFoundation/Type/FormTypeHttpFoundationExtension.php +++ b/Extension/HttpFoundation/Type/FormTypeHttpFoundationExtension.php @@ -22,24 +22,18 @@ */ class FormTypeHttpFoundationExtension extends AbstractTypeExtension { - private $requestHandler; + private RequestHandlerInterface $requestHandler; public function __construct(?RequestHandlerInterface $requestHandler = null) { $this->requestHandler = $requestHandler ?? new HttpFoundationRequestHandler(); } - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->setRequestHandler($this->requestHandler); } - /** - * {@inheritdoc} - */ public static function getExtendedTypes(): iterable { return [FormType::class]; diff --git a/Extension/PasswordHasher/EventListener/PasswordHasherListener.php b/Extension/PasswordHasher/EventListener/PasswordHasherListener.php new file mode 100644 index 0000000000..0144cc3ffa --- /dev/null +++ b/Extension/PasswordHasher/EventListener/PasswordHasherListener.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\PasswordHasher\EventListener; + +use Symfony\Component\Form\Exception\InvalidConfigurationException; +use Symfony\Component\Form\Extension\Core\Type\RepeatedType; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; + +/** + * @author Sébastien Alfaiate + * @author Gábor Egyed + */ +class PasswordHasherListener +{ + private array $passwords = []; + + public function __construct( + private UserPasswordHasherInterface $passwordHasher, + private ?PropertyAccessorInterface $propertyAccessor = null, + ) { + $this->propertyAccessor ??= PropertyAccess::createPropertyAccessor(); + } + + public function registerPassword(FormEvent $event): void + { + if (null === $event->getData() || '' === $event->getData()) { + return; + } + + $this->assertNotMapped($event->getForm()); + + $this->passwords[] = [ + 'form' => $event->getForm(), + 'property_path' => $event->getForm()->getConfig()->getOption('hash_property_path'), + 'password' => $event->getData(), + ]; + } + + public function hashPasswords(FormEvent $event): void + { + $form = $event->getForm(); + + if (!$form->isRoot()) { + return; + } + + if ($form->isValid()) { + foreach ($this->passwords as $password) { + $user = $this->getUser($password['form']); + + $this->propertyAccessor->setValue( + $user, + $password['property_path'], + $this->passwordHasher->hashPassword($user, $password['password']) + ); + } + } + + $this->passwords = []; + } + + private function getTargetForm(FormInterface $form): FormInterface + { + if (!$parentForm = $form->getParent()) { + return $form; + } + + $parentType = $parentForm->getConfig()->getType(); + + do { + if ($parentType->getInnerType() instanceof RepeatedType) { + return $parentForm; + } + } while ($parentType = $parentType->getParent()); + + return $form; + } + + private function getUser(FormInterface $form): PasswordAuthenticatedUserInterface + { + $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))); + } + + return $user; + } + + private function assertNotMapped(FormInterface $form): void + { + if ($this->getTargetForm($form)->getConfig()->getMapped()) { + throw new InvalidConfigurationException('The "hash_property_path" option cannot be used on mapped field.'); + } + } +} diff --git a/Extension/PasswordHasher/PasswordHasherExtension.php b/Extension/PasswordHasher/PasswordHasherExtension.php new file mode 100644 index 0000000000..b9675c2153 --- /dev/null +++ b/Extension/PasswordHasher/PasswordHasherExtension.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\PasswordHasher; + +use Symfony\Component\Form\AbstractExtension; +use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener; + +/** + * Integrates the PasswordHasher component with the Form library. + * + * @author Sébastien Alfaiate + */ +class PasswordHasherExtension extends AbstractExtension +{ + public function __construct( + private PasswordHasherListener $passwordHasherListener, + ) { + } + + protected function loadTypeExtensions(): array + { + return [ + new Type\FormTypePasswordHasherExtension($this->passwordHasherListener), + new Type\PasswordTypePasswordHasherExtension($this->passwordHasherListener), + ]; + } +} diff --git a/Extension/PasswordHasher/Type/FormTypePasswordHasherExtension.php b/Extension/PasswordHasher/Type/FormTypePasswordHasherExtension.php new file mode 100644 index 0000000000..8836f95d93 --- /dev/null +++ b/Extension/PasswordHasher/Type/FormTypePasswordHasherExtension.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\PasswordHasher\Type; + +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvents; + +/** + * @author Sébastien Alfaiate + */ +class FormTypePasswordHasherExtension extends AbstractTypeExtension +{ + public function __construct( + private PasswordHasherListener $passwordHasherListener, + ) { + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->addEventListener(FormEvents::POST_SUBMIT, [$this->passwordHasherListener, 'hashPasswords']); + } + + public static function getExtendedTypes(): iterable + { + return [FormType::class]; + } +} diff --git a/Extension/PasswordHasher/Type/PasswordTypePasswordHasherExtension.php b/Extension/PasswordHasher/Type/PasswordTypePasswordHasherExtension.php new file mode 100644 index 0000000000..1d73e0788b --- /dev/null +++ b/Extension/PasswordHasher/Type/PasswordTypePasswordHasherExtension.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\Extension\PasswordHasher\Type; + +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\Type\PasswordType; +use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\PropertyAccess\PropertyPath; + +/** + * @author Sébastien Alfaiate + */ +class PasswordTypePasswordHasherExtension extends AbstractTypeExtension +{ + public function __construct( + private PasswordHasherListener $passwordHasherListener, + ) { + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + if ($options['hash_property_path']) { + $builder->addEventListener(FormEvents::POST_SUBMIT, [$this->passwordHasherListener, 'registerPassword']); + } + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'hash_property_path' => null, + ]); + + $resolver->setAllowedTypes('hash_property_path', ['null', 'string', PropertyPath::class]); + + $resolver->setInfo('hash_property_path', 'A valid PropertyAccess syntax where the hashed password will be set.'); + } + + public static function getExtendedTypes(): iterable + { + return [PasswordType::class]; + } +} diff --git a/Extension/Validator/Constraints/Form.php b/Extension/Validator/Constraints/Form.php index 49291e6718..8be25c0b8b 100644 --- a/Extension/Validator/Constraints/Form.php +++ b/Extension/Validator/Constraints/Form.php @@ -21,15 +21,12 @@ class Form extends Constraint public const NOT_SYNCHRONIZED_ERROR = '1dafa156-89e1-4736-b832-419c2e501fca'; public const NO_SUCH_FIELD_ERROR = '6e5212ed-a197-4339-99aa-5654798a4854'; - protected static $errorNames = [ + protected const ERROR_NAMES = [ self::NOT_SYNCHRONIZED_ERROR => 'NOT_SYNCHRONIZED_ERROR', self::NO_SUCH_FIELD_ERROR => 'NO_SUCH_FIELD_ERROR', ]; - /** - * {@inheritdoc} - */ - public function getTargets() + public function getTargets(): string|array { return self::CLASS_CONSTRAINT; } diff --git a/Extension/Validator/Constraints/FormValidator.php b/Extension/Validator/Constraints/FormValidator.php index 47982f3fc9..8f4ec60f24 100644 --- a/Extension/Validator/Constraints/FormValidator.php +++ b/Extension/Validator/Constraints/FormValidator.php @@ -27,12 +27,9 @@ class FormValidator extends ConstraintValidator /** * @var \SplObjectStorage> */ - private $resolvedGroups; + private \SplObjectStorage $resolvedGroups; - /** - * {@inheritdoc} - */ - public function validate($form, Constraint $formConstraint) + public function validate(mixed $form, Constraint $formConstraint): void { if (!$formConstraint instanceof Form) { throw new UnexpectedTypeException($formConstraint, Form::class); @@ -59,7 +56,7 @@ public function validate($form, Constraint $formConstraint) // 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 @@ -95,7 +92,7 @@ public function validate($form, Constraint $formConstraint) $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); } } @@ -123,7 +120,7 @@ public function validate($form, Constraint $formConstraint) // 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 @@ -142,7 +139,7 @@ public function validate($form, Constraint $formConstraint) 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); } } } @@ -159,7 +156,7 @@ public function validate($form, Constraint $formConstraint) 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); } } @@ -208,7 +205,7 @@ public function validate($form, Constraint $formConstraint) * * @return string|GroupSequence|array */ - private function getValidationGroups(FormInterface $form) + private function getValidationGroups(FormInterface $form): string|GroupSequence|array { // Determine the clicked button of the complete form tree $clickedButton = null; @@ -249,7 +246,7 @@ private function getValidationGroups(FormInterface $form) * * @return GroupSequence|array */ - private static function resolveValidationGroups($groups, FormInterface $form) + private static function resolveValidationGroups(string|GroupSequence|array|callable $groups, FormInterface $form): GroupSequence|array { if (!\is_string($groups) && \is_callable($groups)) { $groups = $groups($form); @@ -262,7 +259,7 @@ private static function resolveValidationGroups($groups, FormInterface $form) return (array) $groups; } - private static function getConstraintsInGroups($constraints, $group) + private static function getConstraintsInGroups(array $constraints, string|array $group): array { $groups = (array) $group; diff --git a/Extension/Validator/EventListener/ValidationListener.php b/Extension/Validator/EventListener/ValidationListener.php index 867a5768ae..b3c16dbad2 100644 --- a/Extension/Validator/EventListener/ValidationListener.php +++ b/Extension/Validator/EventListener/ValidationListener.php @@ -23,25 +23,18 @@ */ class ValidationListener implements EventSubscriberInterface { - private $validator; - - private $violationMapper; - - /** - * {@inheritdoc} - */ - public static function getSubscribedEvents() + 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) + public function validateForm(FormEvent $event): void { $form = $event->getForm(); diff --git a/Extension/Validator/Type/BaseValidatorExtension.php b/Extension/Validator/Type/BaseValidatorExtension.php index 0e9e2a9d7e..6b70df75fa 100644 --- a/Extension/Validator/Type/BaseValidatorExtension.php +++ b/Extension/Validator/Type/BaseValidatorExtension.php @@ -24,18 +24,15 @@ */ abstract class BaseValidatorExtension extends AbstractTypeExtension { - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // Make sure that validation groups end up as null, closure or array - $validationGroupsNormalizer = function (Options $options, $groups) { + $validationGroupsNormalizer = static function (Options $options, $groups) { if (false === $groups) { return []; } - if (empty($groups)) { + if (!$groups) { return null; } diff --git a/Extension/Validator/Type/FormTypeValidatorExtension.php b/Extension/Validator/Type/FormTypeValidatorExtension.php index 58d1b758a8..14029e339d 100644 --- a/Extension/Validator/Type/FormTypeValidatorExtension.php +++ b/Extension/Validator/Type/FormTypeValidatorExtension.php @@ -27,62 +27,41 @@ */ class FormTypeValidatorExtension extends BaseValidatorExtension { - private $validator; - private $violationMapper; - private $legacyErrorMessages; + private ViolationMapper $violationMapper; - 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); - $this->legacyErrorMessages = $legacyErrorMessages; } - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->addEventSubscriber(new ValidationListener($this->validator, $this->violationMapper)); } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { parent::configureOptions($resolver); // Constraint should always be converted to an array - $constraintsNormalizer = function (Options $options, $constraints) { - return \is_object($constraints) ? [$constraints] : (array) $constraints; - }; + $constraintsNormalizer = static fn (Options $options, $constraints) => \is_object($constraints) ? [$constraints] : (array) $constraints; $resolver->setDefaults([ 'error_mapping' => [], 'constraints' => [], 'invalid_message' => 'This value is not valid.', 'invalid_message_parameters' => [], - 'legacy_error_messages' => $this->legacyErrorMessages, 'allow_extra_fields' => false, 'extra_fields_message' => 'This form should not contain extra fields.', ]); $resolver->setAllowedTypes('constraints', [Constraint::class, Constraint::class.'[]']); - $resolver->setAllowedTypes('legacy_error_messages', 'bool'); - $resolver->setDeprecated('legacy_error_messages', 'symfony/form', '5.2', function (Options $options, $value) { - if (true === $value) { - return 'Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'; - } - - return ''; - }); - $resolver->setNormalizer('constraints', $constraintsNormalizer); } - /** - * {@inheritdoc} - */ public static function getExtendedTypes(): iterable { return [FormType::class]; diff --git a/Extension/Validator/Type/RepeatedTypeValidatorExtension.php b/Extension/Validator/Type/RepeatedTypeValidatorExtension.php index 4bda0b27d1..949dca46a6 100644 --- a/Extension/Validator/Type/RepeatedTypeValidatorExtension.php +++ b/Extension/Validator/Type/RepeatedTypeValidatorExtension.php @@ -21,24 +21,16 @@ */ class RepeatedTypeValidatorExtension extends AbstractTypeExtension { - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { // Map errors to the first field - $errorMapping = function (Options $options) { - return ['.' => $options['first_name']]; - }; + $errorMapping = static fn (Options $options) => ['.' => $options['first_name']]; $resolver->setDefaults([ 'error_mapping' => $errorMapping, ]); } - /** - * {@inheritdoc} - */ public static function getExtendedTypes(): iterable { return [RepeatedType::class]; diff --git a/Extension/Validator/Type/SubmitTypeValidatorExtension.php b/Extension/Validator/Type/SubmitTypeValidatorExtension.php index ea273d0ad9..8efae7d52e 100644 --- a/Extension/Validator/Type/SubmitTypeValidatorExtension.php +++ b/Extension/Validator/Type/SubmitTypeValidatorExtension.php @@ -18,9 +18,6 @@ */ class SubmitTypeValidatorExtension extends BaseValidatorExtension { - /** - * {@inheritdoc} - */ public static function getExtendedTypes(): iterable { return [SubmitType::class]; diff --git a/Extension/Validator/Type/UploadValidatorExtension.php b/Extension/Validator/Type/UploadValidatorExtension.php index 2915d538f0..7c1e965aba 100644 --- a/Extension/Validator/Type/UploadValidatorExtension.php +++ b/Extension/Validator/Type/UploadValidatorExtension.php @@ -23,32 +23,19 @@ */ class UploadValidatorExtension extends AbstractTypeExtension { - private $translator; - private $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, + ) { } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $translator = $this->translator; $translationDomain = $this->translationDomain; - $resolver->setNormalizer('upload_max_size_message', function (Options $options, $message) use ($translator, $translationDomain) { - return function () use ($translator, $translationDomain, $message) { - return $translator->trans($message(), [], $translationDomain); - }; - }); + $resolver->setNormalizer('upload_max_size_message', static fn (Options $options, $message) => static fn () => $translator->trans($message(), [], $translationDomain)); } - /** - * {@inheritdoc} - */ public static function getExtendedTypes(): iterable { return [FormType::class]; diff --git a/Extension/Validator/Util/ServerParams.php b/Extension/Validator/Util/ServerParams.php deleted file mode 100644 index 98e9f0c98a..0000000000 --- a/Extension/Validator/Util/ServerParams.php +++ /dev/null @@ -1,25 +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\Extension\Validator\Util; - -use Symfony\Component\Form\Util\ServerParams as BaseServerParams; - -trigger_deprecation('symfony/form', '5.1', 'The "%s" class is deprecated. Use "%s" instead.', ServerParams::class, BaseServerParams::class); - -/** - * @author Bernhard Schussek - * - * @deprecated since Symfony 5.1. Use {@see BaseServerParams} instead. - */ -class ServerParams extends BaseServerParams -{ -} diff --git a/Extension/Validator/ValidatorExtension.php b/Extension/Validator/ValidatorExtension.php index 16b84eb2c4..2c534481cd 100644 --- a/Extension/Validator/ValidatorExtension.php +++ b/Extension/Validator/ValidatorExtension.php @@ -14,6 +14,7 @@ use Symfony\Component\Form\AbstractExtension; use Symfony\Component\Form\Extension\Validator\Constraints\Form; use Symfony\Component\Form\FormRendererInterface; +use Symfony\Component\Form\FormTypeGuesserInterface; use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -26,37 +27,30 @@ */ class ValidatorExtension extends AbstractExtension { - private $validator; - private $formRenderer; - private $translator; - private $legacyErrorMessages; - - public function __construct(ValidatorInterface $validator, bool $legacyErrorMessages = true, ?FormRendererInterface $formRenderer = null, ?TranslatorInterface $translator = null) - { - $this->legacyErrorMessages = $legacyErrorMessages; - - $metadata = $validator->getMetadataFor('Symfony\Component\Form\Form'); + 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. // This functionality is required when using the Form component without // 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() + public function loadTypeGuesser(): ?FormTypeGuesserInterface { return new ValidatorTypeGuesser($this->validator); } - protected function loadTypeExtensions() + protected function loadTypeExtensions(): array { return [ new Type\FormTypeValidatorExtension($this->validator, $this->legacyErrorMessages, $this->formRenderer, $this->translator), diff --git a/Extension/Validator/ValidatorTypeGuesser.php b/Extension/Validator/ValidatorTypeGuesser.php index 57930aa31b..08dc6e2d58 100644 --- a/Extension/Validator/ValidatorTypeGuesser.php +++ b/Extension/Validator/ValidatorTypeGuesser.php @@ -11,153 +11,171 @@ namespace Symfony\Component\Form\Extension\Validator; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\CollectionType; +use Symfony\Component\Form\Extension\Core\Type\CountryType; +use Symfony\Component\Form\Extension\Core\Type\CurrencyType; +use Symfony\Component\Form\Extension\Core\Type\DateTimeType; +use Symfony\Component\Form\Extension\Core\Type\DateType; +use Symfony\Component\Form\Extension\Core\Type\EmailType; +use Symfony\Component\Form\Extension\Core\Type\FileType; +use Symfony\Component\Form\Extension\Core\Type\IntegerType; +use Symfony\Component\Form\Extension\Core\Type\LanguageType; +use Symfony\Component\Form\Extension\Core\Type\LocaleType; +use Symfony\Component\Form\Extension\Core\Type\NumberType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Extension\Core\Type\TimeType; +use Symfony\Component\Form\Extension\Core\Type\UrlType; use Symfony\Component\Form\FormTypeGuesserInterface; use Symfony\Component\Form\Guess\Guess; use Symfony\Component\Form\Guess\TypeGuess; use Symfony\Component\Form\Guess\ValueGuess; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\Count; +use Symfony\Component\Validator\Constraints\Country; +use Symfony\Component\Validator\Constraints\Currency; +use Symfony\Component\Validator\Constraints\Date; +use Symfony\Component\Validator\Constraints\DateTime; +use Symfony\Component\Validator\Constraints\Email; +use Symfony\Component\Validator\Constraints\File; +use Symfony\Component\Validator\Constraints\Image; +use Symfony\Component\Validator\Constraints\Ip; +use Symfony\Component\Validator\Constraints\IsFalse; +use Symfony\Component\Validator\Constraints\IsTrue; +use Symfony\Component\Validator\Constraints\Language; +use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\Constraints\Locale; +use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Constraints\NotNull; +use Symfony\Component\Validator\Constraints\Range; +use Symfony\Component\Validator\Constraints\Regex; +use Symfony\Component\Validator\Constraints\Time; +use Symfony\Component\Validator\Constraints\Type; +use Symfony\Component\Validator\Constraints\Url; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface; class ValidatorTypeGuesser implements FormTypeGuesserInterface { - private $metadataFactory; - - public function __construct(MetadataFactoryInterface $metadataFactory) - { - $this->metadataFactory = $metadataFactory; + public function __construct( + private MetadataFactoryInterface $metadataFactory, + ) { } - /** - * {@inheritdoc} - */ - public function guessType(string $class, string $property) + public function guessType(string $class, string $property): ?TypeGuess { - return $this->guess($class, $property, function (Constraint $constraint) { - return $this->guessTypeForConstraint($constraint); - }); + return $this->guess($class, $property, $this->guessTypeForConstraint(...)); } - /** - * {@inheritdoc} - */ - public function guessRequired(string $class, string $property) + public function guessRequired(string $class, string $property): ?ValueGuess { - return $this->guess($class, $property, function (Constraint $constraint) { - return $this->guessRequiredForConstraint($constraint); - // If we don't find any constraint telling otherwise, we can assume - // that a field is not required (with LOW_CONFIDENCE) - }, false); + // If we don't find any constraint telling otherwise, we can assume + // that a field is not required (with LOW_CONFIDENCE) + return $this->guess($class, $property, $this->guessRequiredForConstraint(...), false); } - /** - * {@inheritdoc} - */ - public function guessMaxLength(string $class, string $property) + public function guessMaxLength(string $class, string $property): ?ValueGuess { - return $this->guess($class, $property, function (Constraint $constraint) { - return $this->guessMaxLengthForConstraint($constraint); - }); + return $this->guess($class, $property, $this->guessMaxLengthForConstraint(...)); } - /** - * {@inheritdoc} - */ - public function guessPattern(string $class, string $property) + public function guessPattern(string $class, string $property): ?ValueGuess { - return $this->guess($class, $property, function (Constraint $constraint) { - return $this->guessPatternForConstraint($constraint); - }); + return $this->guess($class, $property, $this->guessPatternForConstraint(...)); } /** * Guesses a field class name for a given constraint. - * - * @return TypeGuess|null */ - public function guessTypeForConstraint(Constraint $constraint) + public function guessTypeForConstraint(Constraint $constraint): ?TypeGuess { - switch (\get_class($constraint)) { - case 'Symfony\Component\Validator\Constraints\Type': + switch ($constraint::class) { + case Type::class: switch ($constraint->type) { case 'array': - return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\CollectionType', [], Guess::MEDIUM_CONFIDENCE); + return new TypeGuess(CollectionType::class, [], Guess::MEDIUM_CONFIDENCE); case 'boolean': case 'bool': - return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\CheckboxType', [], Guess::MEDIUM_CONFIDENCE); + return new TypeGuess(CheckboxType::class, [], Guess::MEDIUM_CONFIDENCE); case 'double': case 'float': case 'numeric': case 'real': - return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\NumberType', [], Guess::MEDIUM_CONFIDENCE); + return new TypeGuess(NumberType::class, [], Guess::MEDIUM_CONFIDENCE); case 'integer': case 'int': case 'long': - return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\IntegerType', [], Guess::MEDIUM_CONFIDENCE); + return new TypeGuess(IntegerType::class, [], Guess::MEDIUM_CONFIDENCE); case \DateTime::class: case '\DateTime': - return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateType', [], Guess::MEDIUM_CONFIDENCE); + return new TypeGuess(DateType::class, [], Guess::MEDIUM_CONFIDENCE); + + case \DateTimeImmutable::class: + case '\DateTimeImmutable': + case \DateTimeInterface::class: + case '\DateTimeInterface': + return new TypeGuess(DateType::class, ['input' => 'datetime_immutable'], Guess::MEDIUM_CONFIDENCE); case 'string': - return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TextType', [], Guess::LOW_CONFIDENCE); + return new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE); } break; - case 'Symfony\Component\Validator\Constraints\Country': - return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\CountryType', [], Guess::HIGH_CONFIDENCE); + case Country::class: + return new TypeGuess(CountryType::class, [], Guess::HIGH_CONFIDENCE); - case 'Symfony\Component\Validator\Constraints\Currency': - return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\CurrencyType', [], Guess::HIGH_CONFIDENCE); + case Currency::class: + return new TypeGuess(CurrencyType::class, [], Guess::HIGH_CONFIDENCE); - case 'Symfony\Component\Validator\Constraints\Date': - return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateType', ['input' => 'string'], Guess::HIGH_CONFIDENCE); + case Date::class: + return new TypeGuess(DateType::class, ['input' => 'string'], Guess::HIGH_CONFIDENCE); - case 'Symfony\Component\Validator\Constraints\DateTime': - return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateTimeType', ['input' => 'string'], Guess::HIGH_CONFIDENCE); + case DateTime::class: + return new TypeGuess(DateTimeType::class, ['input' => 'string'], Guess::HIGH_CONFIDENCE); - case 'Symfony\Component\Validator\Constraints\Email': - return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\EmailType', [], Guess::HIGH_CONFIDENCE); + case Email::class: + return new TypeGuess(EmailType::class, [], Guess::HIGH_CONFIDENCE); - case 'Symfony\Component\Validator\Constraints\File': - case 'Symfony\Component\Validator\Constraints\Image': + case File::class: + case Image::class: $options = []; if ($constraint->mimeTypes) { $options = ['attr' => ['accept' => implode(',', (array) $constraint->mimeTypes)]]; } - return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\FileType', $options, Guess::HIGH_CONFIDENCE); + return new TypeGuess(FileType::class, $options, Guess::HIGH_CONFIDENCE); - case 'Symfony\Component\Validator\Constraints\Language': - return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\LanguageType', [], Guess::HIGH_CONFIDENCE); + case Language::class: + return new TypeGuess(LanguageType::class, [], Guess::HIGH_CONFIDENCE); - case 'Symfony\Component\Validator\Constraints\Locale': - return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\LocaleType', [], Guess::HIGH_CONFIDENCE); + case Locale::class: + return new TypeGuess(LocaleType::class, [], Guess::HIGH_CONFIDENCE); - case 'Symfony\Component\Validator\Constraints\Time': - return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TimeType', ['input' => 'string'], Guess::HIGH_CONFIDENCE); + case Time::class: + return new TypeGuess(TimeType::class, ['input' => 'string'], Guess::HIGH_CONFIDENCE); - case 'Symfony\Component\Validator\Constraints\Url': - return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\UrlType', [], Guess::HIGH_CONFIDENCE); + case Url::class: + return new TypeGuess(UrlType::class, [], Guess::HIGH_CONFIDENCE); - case 'Symfony\Component\Validator\Constraints\Ip': - return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TextType', [], Guess::MEDIUM_CONFIDENCE); + case Ip::class: + return new TypeGuess(TextType::class, [], Guess::MEDIUM_CONFIDENCE); - case 'Symfony\Component\Validator\Constraints\Length': - case 'Symfony\Component\Validator\Constraints\Regex': - return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TextType', [], Guess::LOW_CONFIDENCE); + case Length::class: + case Regex::class: + return new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE); - case 'Symfony\Component\Validator\Constraints\Range': - return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\NumberType', [], Guess::LOW_CONFIDENCE); + case Range::class: + return new TypeGuess(NumberType::class, [], Guess::LOW_CONFIDENCE); - case 'Symfony\Component\Validator\Constraints\Count': - return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\CollectionType', [], Guess::LOW_CONFIDENCE); + case Count::class: + return new TypeGuess(CollectionType::class, [], Guess::LOW_CONFIDENCE); - case 'Symfony\Component\Validator\Constraints\IsTrue': - case 'Symfony\Component\Validator\Constraints\IsFalse': - return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\CheckboxType', [], Guess::MEDIUM_CONFIDENCE); + case IsTrue::class: + case IsFalse::class: + return new TypeGuess(CheckboxType::class, [], Guess::MEDIUM_CONFIDENCE); } return null; @@ -165,42 +183,36 @@ public function guessTypeForConstraint(Constraint $constraint) /** * Guesses whether a field is required based on the given constraint. - * - * @return ValueGuess|null */ - public function guessRequiredForConstraint(Constraint $constraint) + public function guessRequiredForConstraint(Constraint $constraint): ?ValueGuess { - switch (\get_class($constraint)) { - case 'Symfony\Component\Validator\Constraints\NotNull': - case 'Symfony\Component\Validator\Constraints\NotBlank': - case 'Symfony\Component\Validator\Constraints\IsTrue': - return new ValueGuess(true, Guess::HIGH_CONFIDENCE); - } - - return null; + return match ($constraint::class) { + NotNull::class, + NotBlank::class, + IsTrue::class => new ValueGuess(true, Guess::HIGH_CONFIDENCE), + default => null, + }; } /** * Guesses a field's maximum length based on the given constraint. - * - * @return ValueGuess|null */ - public function guessMaxLengthForConstraint(Constraint $constraint) + public function guessMaxLengthForConstraint(Constraint $constraint): ?ValueGuess { - switch (\get_class($constraint)) { - case 'Symfony\Component\Validator\Constraints\Length': + switch ($constraint::class) { + case Length::class: if (is_numeric($constraint->max)) { return new ValueGuess($constraint->max, Guess::HIGH_CONFIDENCE); } break; - case 'Symfony\Component\Validator\Constraints\Type': + case Type::class: if (\in_array($constraint->type, ['double', 'float', 'numeric', 'real'])) { return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE); } break; - case 'Symfony\Component\Validator\Constraints\Range': + case Range::class: if (is_numeric($constraint->max)) { return new ValueGuess(\strlen((string) $constraint->max), Guess::LOW_CONFIDENCE); } @@ -212,19 +224,17 @@ public function guessMaxLengthForConstraint(Constraint $constraint) /** * Guesses a field's pattern based on the given constraint. - * - * @return ValueGuess|null */ - public function guessPatternForConstraint(Constraint $constraint) + public function guessPatternForConstraint(Constraint $constraint): ?ValueGuess { - switch (\get_class($constraint)) { - case 'Symfony\Component\Validator\Constraints\Length': + 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; - case 'Symfony\Component\Validator\Constraints\Regex': + case Regex::class: $htmlPattern = $constraint->getHtmlPattern(); if (null !== $htmlPattern) { @@ -232,13 +242,13 @@ public function guessPatternForConstraint(Constraint $constraint) } break; - case 'Symfony\Component\Validator\Constraints\Range': + 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; - case 'Symfony\Component\Validator\Constraints\Type': + case Type::class: if (\in_array($constraint->type, ['double', 'float', 'numeric', 'real'])) { return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE); } @@ -256,10 +266,8 @@ public function guessPatternForConstraint(Constraint $constraint) * for a given constraint * @param mixed $defaultValue The default value assumed if no other value * can be guessed - * - * @return Guess|null */ - protected function guess(string $class, string $property, \Closure $closure, $defaultValue = null) + protected function guess(string $class, string $property, \Closure $closure, mixed $defaultValue = null): ?Guess { $guesses = []; $classMetadata = $this->metadataFactory->getMetadataFor($class); diff --git a/Extension/Validator/ViolationMapper/MappingRule.php b/Extension/Validator/ViolationMapper/MappingRule.php index d9342de6e5..3263f66df2 100644 --- a/Extension/Validator/ViolationMapper/MappingRule.php +++ b/Extension/Validator/ViolationMapper/MappingRule.php @@ -19,21 +19,14 @@ */ class MappingRule { - private $origin; - private $propertyPath; - private $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, + ) { } - /** - * @return FormInterface - */ - public function getOrigin() + public function getOrigin(): FormInterface { return $this->origin; } @@ -43,20 +36,16 @@ public function getOrigin() * * If the rule matches, the form mapped by the rule is returned. * Otherwise this method returns false. - * - * @return FormInterface|null */ - public function match(string $propertyPath) + public function match(string $propertyPath): ?FormInterface { return $propertyPath === $this->propertyPath ? $this->getTarget() : null; } /** * Matches a property path against a prefix of the rule path. - * - * @return bool */ - public function isPrefix(string $propertyPath) + public function isPrefix(string $propertyPath): bool { $length = \strlen($propertyPath); $prefix = substr($this->propertyPath, 0, $length); @@ -66,18 +55,16 @@ public function isPrefix(string $propertyPath) } /** - * @return FormInterface - * * @throws ErrorMappingException */ - public function getTarget() + public function getTarget(): FormInterface { $childNames = explode('.', $this->targetPath); $target = $this->origin; 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 0efd168e3d..c139933743 100644 --- a/Extension/Validator/ViolationMapper/RelativePath.php +++ b/Extension/Validator/ViolationMapper/RelativePath.php @@ -19,19 +19,14 @@ */ class RelativePath extends PropertyPath { - private $root; - - public function __construct(FormInterface $root, string $propertyPath) - { + public function __construct( + private FormInterface $root, + string $propertyPath, + ) { parent::__construct($propertyPath); - - $this->root = $root; } - /** - * @return FormInterface - */ - public function getRoot() + public function getRoot(): FormInterface { return $this->root; } diff --git a/Extension/Validator/ViolationMapper/ViolationMapper.php b/Extension/Validator/ViolationMapper/ViolationMapper.php index 6ecc284c4f..faca255b5d 100644 --- a/Extension/Validator/ViolationMapper/ViolationMapper.php +++ b/Extension/Validator/ViolationMapper/ViolationMapper.php @@ -28,20 +28,15 @@ */ class ViolationMapper implements ViolationMapperInterface { - private $formRenderer; - private $translator; - private $allowNonSynchronized = false; + 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, + ) { } - /** - * {@inheritdoc} - */ - public function mapViolation(ConstraintViolation $violation, FormInterface $form, bool $allowNonSynchronized = false) + public function mapViolation(ConstraintViolation $violation, FormInterface $form, bool $allowNonSynchronized = false): void { $this->allowNonSynchronized = $allowNonSynchronized; @@ -153,7 +148,7 @@ public function mapViolation(ConstraintViolation $violation, FormInterface $form $message = $violation->getMessage(); $messageTemplate = $violation->getMessageTemplate(); - if (false !== strpos($message, '{{ label }}') || false !== strpos($messageTemplate, '{{ label }}')) { + if (str_contains($message, '{{ label }}') || str_contains($messageTemplate, '{{ label }}')) { $form = $scope; do { @@ -179,8 +174,8 @@ public function mapViolation(ConstraintViolation $violation, FormInterface $form if (false !== $label) { if (null === $label && null !== $this->formRenderer) { $label = $this->formRenderer->humanize($scope->getName()); - } elseif (null === $label) { - $label = $scope->getName(); + } else { + $label ??= $scope->getName(); } if (null !== $this->translator) { diff --git a/Extension/Validator/ViolationMapper/ViolationMapperInterface.php b/Extension/Validator/ViolationMapper/ViolationMapperInterface.php index 57ed1c84ce..8d1f242f95 100644 --- a/Extension/Validator/ViolationMapper/ViolationMapperInterface.php +++ b/Extension/Validator/ViolationMapper/ViolationMapperInterface.php @@ -25,5 +25,5 @@ interface ViolationMapperInterface * * @param bool $allowNonSynchronized Whether to allow mapping to non-synchronized forms */ - public function mapViolation(ConstraintViolation $violation, FormInterface $form, bool $allowNonSynchronized = false); + 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 625c0b99ff..0c2a130cc8 100644 --- a/Extension/Validator/ViolationMapper/ViolationPath.php +++ b/Extension/Validator/ViolationMapper/ViolationPath.php @@ -22,30 +22,12 @@ */ class ViolationPath implements \IteratorAggregate, PropertyPathInterface { - /** - * @var list - */ - private $elements = []; - - /** - * @var array - */ - private $isIndex = []; - - /** - * @var array - */ - private $mapsForm = []; - - /** - * @var string - */ - private $pathAsString = ''; - - /** - * @var int - */ - private $length = 0; + /** @var list */ + private array $elements = []; + private array $isIndex = []; + private array $mapsForm = []; + private string $pathAsString = ''; + private int $length = 0; /** * Creates a new violation path from a string. @@ -114,26 +96,17 @@ public function __construct(string $violationPath) $this->buildString(); } - /** - * {@inheritdoc} - */ - public function __toString() + public function __toString(): string { return $this->pathAsString; } - /** - * {@inheritdoc} - */ - public function getLength() + public function getLength(): int { return $this->length; } - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?PropertyPathInterface { if ($this->length <= 1) { return null; @@ -151,50 +124,43 @@ public function getParent() return $parent; } - /** - * {@inheritdoc} - */ - public function getElements() + public function getElements(): array { return $this->elements; } - /** - * {@inheritdoc} - */ - public function getElement(int $index) + 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]; } - /** - * {@inheritdoc} - */ - public function isProperty(int $index) + 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]; } - /** - * {@inheritdoc} - */ - public function isIndex(int $index) + 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]; } + public function isNullSafe(int $index): bool + { + return false; + } + /** * Returns whether an element maps directly to a form. * @@ -205,14 +171,12 @@ public function isIndex(int $index) * In this example, "address" and "office" map to forms, while * "street does not. * - * @return bool - * * @throws OutOfBoundsException if the offset is invalid */ - public function mapsForm(int $index) + 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]; @@ -220,11 +184,8 @@ public function mapsForm(int $index) /** * Returns a new iterator for this path. - * - * @return ViolationPathIterator */ - #[\ReturnTypeWillChange] - public function getIterator() + public function getIterator(): ViolationPathIterator { return new ViolationPathIterator($this); } @@ -232,7 +193,7 @@ public function getIterator() /** * Builds the string representation from the elements. */ - private function buildString() + private function buildString(): void { $this->pathAsString = ''; $data = false; diff --git a/Extension/Validator/ViolationMapper/ViolationPathIterator.php b/Extension/Validator/ViolationMapper/ViolationPathIterator.php index 50baa4533e..06c719dc80 100644 --- a/Extension/Validator/ViolationMapper/ViolationPathIterator.php +++ b/Extension/Validator/ViolationMapper/ViolationPathIterator.php @@ -23,7 +23,7 @@ public function __construct(ViolationPath $violationPath) parent::__construct($violationPath); } - public function mapsForm() + public function mapsForm(): bool { return $this->path->mapsForm($this->key()); } diff --git a/Form.php b/Form.php index 9410051a5c..72c60ee41a 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,68 +70,40 @@ */ class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterface { - /** - * @var FormConfigInterface - */ - private $config; - - /** - * @var FormInterface|null - */ - private $parent; + private ?FormInterface $parent = null; /** * A map of FormInterface instances. * - * @var OrderedHashMap + * @var OrderedHashMap */ - private $children; + private OrderedHashMap $children; /** * @var FormError[] */ - private $errors = []; + private array $errors = []; - /** - * @var bool - */ - private $submitted = false; + private bool $submitted = false; /** * The button that was used to submit the form. - * - * @var FormInterface|ClickableInterface|null - */ - private $clickedButton; - - /** - * @var mixed - */ - private $modelData; - - /** - * @var mixed */ - private $normData; + private FormInterface|ClickableInterface|null $clickedButton = null; - /** - * @var mixed - */ - private $viewData; + private mixed $modelData = null; + private mixed $normData = null; + private mixed $viewData = null; /** * The submitted values that don't belong to any children. - * - * @var array */ - private $extraData = []; + private array $extraData = []; /** * The transformation failure generated during submission, if any. - * - * @var TransformationFailedException|null */ - private $transformationFailure; + private ?TransformationFailedException $transformationFailure = null; /** * Whether the form's data has been initialized. @@ -143,40 +114,29 @@ class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterfac * lazily in order to save performance when {@link setData()} is called * manually, making the initialization with the configured default value * superfluous. - * - * @var bool */ - private $defaultDataSet = false; + private bool $defaultDataSet = false; /** * Whether setData() is currently being called. - * - * @var bool */ - private $lockSetData = false; + private bool $lockSetData = false; - /** - * @var string - */ - private $name = ''; + private string $name = ''; /** * Whether the form inherits its underlying data from its parent. - * - * @var bool */ - private $inheritData; + private bool $inheritData; - /** - * @var PropertyPathInterface|null - */ - private $propertyPath; + private ?PropertyPathInterface $propertyPath = null; /** * @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. @@ -190,7 +150,6 @@ public function __construct(FormConfigInterface $config) $this->defaultDataSet = true; } - $this->config = $config; $this->children = new OrderedHashMap(); $this->name = $config->getName(); } @@ -204,26 +163,17 @@ public function __clone() } } - /** - * {@inheritdoc} - */ - public function getConfig() + public function getConfig(): FormConfigInterface { return $this->config; } - /** - * {@inheritdoc} - */ - public function getName() + public function getName(): string { return $this->name; } - /** - * {@inheritdoc} - */ - public function getPropertyPath() + public function getPropertyPath(): ?PropertyPathInterface { if ($this->propertyPath || $this->propertyPath = $this->config->getPropertyPath()) { return $this->propertyPath; @@ -235,7 +185,7 @@ public function getPropertyPath() $parent = $this->parent; - while ($parent && $parent->getConfig()->getInheritData()) { + while ($parent?->getConfig()->getInheritData()) { $parent = $parent->getParent(); } @@ -248,10 +198,7 @@ public function getPropertyPath() return $this->propertyPath; } - /** - * {@inheritdoc} - */ - public function isRequired() + public function isRequired(): bool { if (null === $this->parent || $this->parent->isRequired()) { return $this->config->getRequired(); @@ -260,10 +207,7 @@ public function isRequired() return false; } - /** - * {@inheritdoc} - */ - public function isDisabled() + public function isDisabled(): bool { if (null === $this->parent || !$this->parent->isDisabled()) { return $this->config->getDisabled(); @@ -272,10 +216,7 @@ public function isDisabled() return true; } - /** - * {@inheritdoc} - */ - public function setParent(?FormInterface $parent = null) + public function setParent(?FormInterface $parent): static { if ($this->submitted) { throw new AlreadySubmittedException('You cannot set the parent of a submitted form.'); @@ -290,34 +231,22 @@ public function setParent(?FormInterface $parent = null) return $this; } - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?FormInterface { return $this->parent; } - /** - * {@inheritdoc} - */ - public function getRoot() + public function getRoot(): FormInterface { return $this->parent ? $this->parent->getRoot() : $this; } - /** - * {@inheritdoc} - */ - public function isRoot() + public function isRoot(): bool { return null === $this->parent; } - /** - * {@inheritdoc} - */ - public function setData($modelData) + public function setData(mixed $modelData): static { // If the form is submitted while disabled, it is set to submitted, but the data is not // changed. In such cases (i.e. when the form is not initialized yet) don't @@ -396,10 +325,7 @@ public function setData($modelData) return $this; } - /** - * {@inheritdoc} - */ - public function getData() + public function getData(): mixed { if ($this->inheritData) { if (!$this->parent) { @@ -420,10 +346,7 @@ public function getData() return $this->modelData; } - /** - * {@inheritdoc} - */ - public function getNormData() + public function getNormData(): mixed { if ($this->inheritData) { if (!$this->parent) { @@ -444,10 +367,7 @@ public function getNormData() return $this->normData; } - /** - * {@inheritdoc} - */ - public function getViewData() + public function getViewData(): mixed { if ($this->inheritData) { if (!$this->parent) { @@ -468,18 +388,12 @@ public function getViewData() return $this->viewData; } - /** - * {@inheritdoc} - */ - public function getExtraData() + public function getExtraData(): array { return $this->extraData; } - /** - * {@inheritdoc} - */ - public function initialize() + public function initialize(): static { if (null !== $this->parent) { throw new RuntimeException('Only root forms should be initialized.'); @@ -495,20 +409,14 @@ public function initialize() return $this; } - /** - * {@inheritdoc} - */ - public function handleRequest($request = null) + public function handleRequest(mixed $request = null): static { $this->config->getRequestHandler()->handleRequest($this, $request); return $this; } - /** - * {@inheritdoc} - */ - public function submit($submittedData, bool $clearMissing = true) + public function submit(mixed $submittedData, bool $clearMissing = true): static { if ($this->submitted) { throw new AlreadySubmittedException('A form can only be submitted once.'); @@ -574,11 +482,7 @@ public function submit($submittedData, bool $clearMissing = true) // since forms without children may also be compound. // (think of empty collection forms) if ($this->config->getCompound()) { - if (null === $submittedData) { - $submittedData = []; - } - - if (!\is_array($submittedData)) { + if (!\is_array($submittedData ??= [])) { throw new TransformationFailedException('Compound forms expect an array or NULL on submission.'); } @@ -684,10 +588,7 @@ public function submit($submittedData, bool $clearMissing = true) return $this; } - /** - * {@inheritdoc} - */ - public function addError(FormError $error) + public function addError(FormError $error): static { if (null === $error->getOrigin()) { $error->setOrigin($this); @@ -702,34 +603,22 @@ public function addError(FormError $error) return $this; } - /** - * {@inheritdoc} - */ - public function isSubmitted() + public function isSubmitted(): bool { return $this->submitted; } - /** - * {@inheritdoc} - */ - public function isSynchronized() + public function isSynchronized(): bool { return null === $this->transformationFailure; } - /** - * {@inheritdoc} - */ - public function getTransformationFailure() + public function getTransformationFailure(): ?TransformationFailedException { return $this->transformationFailure; } - /** - * {@inheritdoc} - */ - public function isEmpty() + public function isEmpty(): bool { foreach ($this->children as $child) { if (!$child->isEmpty()) { @@ -737,32 +626,21 @@ public function isEmpty() } } - if (!method_exists($this->config, 'getIsEmptyCallback')) { - trigger_deprecation('symfony/form', '5.1', 'Not implementing the "%s::getIsEmptyCallback()" method in "%s" is deprecated.', FormConfigInterface::class, \get_class($this->config)); - - $isEmptyCallback = null; - } else { - $isEmptyCallback = $this->config->getIsEmptyCallback(); - } - - if (null !== $isEmptyCallback) { + if (null !== $isEmptyCallback = $this->config->getIsEmptyCallback()) { return $isEmptyCallback($this->modelData); } - return FormUtil::isEmpty($this->modelData) || + return FormUtil::isEmpty($this->modelData) // arrays, countables - ((\is_array($this->modelData) || $this->modelData instanceof \Countable) && 0 === \count($this->modelData)) || + || (is_countable($this->modelData) && 0 === \count($this->modelData)) // traversables that are not countable - ($this->modelData instanceof \Traversable && 0 === iterator_count($this->modelData)); + || ($this->modelData instanceof \Traversable && 0 === iterator_count($this->modelData)); } - /** - * {@inheritdoc} - */ - public function isValid() + public function isValid(): bool { if (!$this->submitted) { - throw new LogicException('Cannot check if an unsubmitted form is valid. Call Form::isSubmitted() before Form::isValid().'); + throw new LogicException('Cannot check if an unsubmitted form is valid. Call Form::isSubmitted() and ensure that it\'s true before calling Form::isValid().'); } if ($this->isDisabled()) { @@ -774,10 +652,8 @@ public function isValid() /** * Returns the button that was used to submit the form. - * - * @return FormInterface|ClickableInterface|null */ - public function getClickedButton() + public function getClickedButton(): FormInterface|ClickableInterface|null { if ($this->clickedButton) { return $this->clickedButton; @@ -786,10 +662,7 @@ public function getClickedButton() return $this->parent && method_exists($this->parent, 'getClickedButton') ? $this->parent->getClickedButton() : null; } - /** - * {@inheritdoc} - */ - public function getErrors(bool $deep = false, bool $flatten = true) + public function getErrors(bool $deep = false, bool $flatten = true): FormErrorIterator { $errors = $this->errors; @@ -820,10 +693,7 @@ public function getErrors(bool $deep = false, bool $flatten = true) return new FormErrorIterator($this, $errors); } - /** - * {@inheritdoc} - */ - public function clearErrors(bool $deep = false): self + public function clearErrors(bool $deep = false): static { $this->errors = []; @@ -839,18 +709,12 @@ public function clearErrors(bool $deep = false): self return $this; } - /** - * {@inheritdoc} - */ - public function all() + public function all(): array { return iterator_to_array($this->children); } - /** - * {@inheritdoc} - */ - public function add($child, ?string $type = null, array $options = []) + public function add(FormInterface|string $child, ?string $type = null, array $options = []): static { if ($this->submitted) { throw new AlreadySubmittedException('You cannot add children to a submitted form.'); @@ -861,16 +725,6 @@ public function add($child, ?string $type = null, array $options = []) } if (!$child instanceof FormInterface) { - if (!\is_string($child) && !\is_int($child)) { - throw new UnexpectedTypeException($child, 'string or Symfony\Component\Form\FormInterface'); - } - - $child = (string) $child; - - if (null !== $type && !\is_string($type)) { - throw new UnexpectedTypeException($type, 'string or null'); - } - // Never initialize child forms automatically $options['auto_initialize'] = false; @@ -884,7 +738,7 @@ public function add($child, ?string $type = null, array $options = []) $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; @@ -918,10 +772,7 @@ public function add($child, ?string $type = null, array $options = []) return $this; } - /** - * {@inheritdoc} - */ - public function remove(string $name) + public function remove(string $name): static { if ($this->submitted) { throw new AlreadySubmittedException('You cannot remove children from a submitted form.'); @@ -938,35 +789,26 @@ public function remove(string $name) return $this; } - /** - * {@inheritdoc} - */ - public function has(string $name) + public function has(string $name): bool { return isset($this->children[$name]); } - /** - * {@inheritdoc} - */ - public function get(string $name) + public function get(string $name): FormInterface { if (isset($this->children[$name])) { return $this->children[$name]; } - throw new OutOfBoundsException(sprintf('Child "%s" does not exist.', $name)); + throw new OutOfBoundsException(\sprintf('Child "%s" does not exist.', $name)); } /** * Returns whether a child with the given name exists (implements the \ArrayAccess interface). * * @param string $name The name of the child - * - * @return bool */ - #[\ReturnTypeWillChange] - public function offsetExists($name) + public function offsetExists(mixed $name): bool { return $this->has($name); } @@ -976,12 +818,9 @@ public function offsetExists($name) * * @param string $name The name of the child * - * @return FormInterface - * * @throws OutOfBoundsException if the named child does not exist */ - #[\ReturnTypeWillChange] - public function offsetGet($name) + public function offsetGet(mixed $name): FormInterface { return $this->get($name); } @@ -992,15 +831,12 @@ public function offsetGet($name) * @param string $name Ignored. The name of the child is used * @param FormInterface $child The child to be added * - * @return void - * * @throws AlreadySubmittedException if the form has already been submitted * @throws LogicException when trying to add a child to a non-compound form * * @see self::add() */ - #[\ReturnTypeWillChange] - public function offsetSet($name, $child) + public function offsetSet(mixed $name, mixed $child): void { $this->add($child); } @@ -1010,12 +846,9 @@ public function offsetSet($name, $child) * * @param string $name The name of the child to remove * - * @return void - * * @throws AlreadySubmittedException if the form has already been submitted */ - #[\ReturnTypeWillChange] - public function offsetUnset($name) + public function offsetUnset(mixed $name): void { $this->remove($name); } @@ -1025,27 +858,20 @@ public function offsetUnset($name) * * @return \Traversable */ - #[\ReturnTypeWillChange] - public function getIterator() + public function getIterator(): \Traversable { return $this->children; } /** * Returns the number of form children (implements the \Countable interface). - * - * @return int */ - #[\ReturnTypeWillChange] - public function count() + public function count(): int { return \count($this->children); } - /** - * {@inheritdoc} - */ - public function createView(?FormView $parent = null) + public function createView(?FormView $parent = null): FormView { if (null === $parent && $this->parent) { $parent = $this->parent->createView(); @@ -1092,26 +918,22 @@ private function sort(array &$children): void return; } - uksort($children, static function ($a, $b) use ($c): int { - return [$c[$b]['p'], $c[$a]['i']] <=> [$c[$a]['p'], $c[$b]['i']]; - }); + uksort($children, static fn ($a, $b): int => [$c[$b]['p'], $c[$a]['i']] <=> [$c[$a]['p'], $c[$b]['i']]); } /** * Normalizes the underlying data if a model transformer is set. * - * @return mixed - * * @throws TransformationFailedException If the underlying data cannot be transformed to "normalized" format */ - private function modelToNorm($value) + private function modelToNorm(mixed $value): mixed { try { foreach ($this->config->getModelTransformers() as $transformer) { $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; @@ -1120,11 +942,9 @@ private function modelToNorm($value) /** * Reverse transforms a value if a model transformer is set. * - * @return mixed - * * @throws TransformationFailedException If the value cannot be transformed to "model" format */ - private function normToModel($value) + private function normToModel(mixed $value): mixed { try { $transformers = $this->config->getModelTransformers(); @@ -1133,7 +953,7 @@ private function normToModel($value) $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; @@ -1142,11 +962,9 @@ private function normToModel($value) /** * Transforms the value if a view transformer is set. * - * @return mixed - * * @throws TransformationFailedException If the normalized value cannot be transformed to "view" format */ - private function normToView($value) + private function normToView(mixed $value): mixed { // Scalar values should be converted to strings to // facilitate differentiation between empty ("") and zero (0). @@ -1162,7 +980,7 @@ private function normToView($value) $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; @@ -1171,11 +989,9 @@ private function normToView($value) /** * Reverse transforms a value if a view transformer is set. * - * @return mixed - * * @throws TransformationFailedException If the submitted value cannot be transformed to "normalized" format */ - private function viewToNorm($value) + private function viewToNorm(mixed $value): mixed { if (!$transformers = $this->config->getViewTransformers()) { return '' === $value ? null : $value; @@ -1186,7 +1002,7 @@ private function viewToNorm($value) $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 3881cc56de..f34c2f76eb 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; /** @@ -31,14 +30,12 @@ class FormBuilder extends FormConfigBuilder implements \IteratorAggregate, FormB * * @var FormBuilderInterface[] */ - private $children = []; + private array $children = []; /** * The data of children who haven't been converted to form builders yet. - * - * @var array */ - private $unresolvedChildren = []; + private array $unresolvedChildren = []; public function __construct(?string $name, ?string $dataClass, EventDispatcherInterface $dispatcher, FormFactoryInterface $factory, array $options = []) { @@ -47,10 +44,7 @@ public function __construct(?string $name, ?string $dataClass, EventDispatcherIn $this->setFormFactory($factory); } - /** - * {@inheritdoc} - */ - public function add($child, ?string $type = null, array $options = []) + public function add(FormBuilderInterface|string $child, ?string $type = null, array $options = []): static { if ($this->locked) { throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -65,14 +59,6 @@ public function add($child, ?string $type = null, array $options = []) return $this; } - if (!\is_string($child) && !\is_int($child)) { - throw new UnexpectedTypeException($child, 'string or Symfony\Component\Form\FormBuilderInterface'); - } - - if (null !== $type && !\is_string($type)) { - throw new UnexpectedTypeException($type, 'string or null'); - } - // Add to "children" to maintain order $this->children[$child] = null; $this->unresolvedChildren[$child] = [$type, $options]; @@ -80,10 +66,7 @@ public function add($child, ?string $type = null, array $options = []) return $this; } - /** - * {@inheritdoc} - */ - public function create($name, ?string $type = null, array $options = []) + public function create(string $name, ?string $type = null, array $options = []): FormBuilderInterface { if ($this->locked) { throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -100,10 +83,7 @@ public function create($name, ?string $type = null, array $options = []) return $this->getFormFactory()->createBuilderForProperty($this->getDataClass(), $name, null, $options); } - /** - * {@inheritdoc} - */ - public function get($name) + public function get(string $name): FormBuilderInterface { if ($this->locked) { throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -117,13 +97,10 @@ public function get($name) 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)); } - /** - * {@inheritdoc} - */ - public function remove($name) + public function remove(string $name): static { if ($this->locked) { throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -134,10 +111,7 @@ public function remove($name) return $this; } - /** - * {@inheritdoc} - */ - public function has($name) + public function has(string $name): bool { if ($this->locked) { throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -146,10 +120,7 @@ public function has($name) return isset($this->unresolvedChildren[$name]) || isset($this->children[$name]); } - /** - * {@inheritdoc} - */ - public function all() + public function all(): array { if ($this->locked) { throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -160,11 +131,7 @@ public function all() return $this->children; } - /** - * @return int - */ - #[\ReturnTypeWillChange] - public function count() + public function count(): int { if ($this->locked) { throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -173,12 +140,9 @@ public function count() return \count($this->children); } - /** - * {@inheritdoc} - */ - public function getFormConfig() + public function getFormConfig(): FormConfigInterface { - /** @var $config self */ + /** @var self $config */ $config = parent::getFormConfig(); $config->children = []; @@ -187,10 +151,7 @@ public function getFormConfig() return $config; } - /** - * {@inheritdoc} - */ - public function getForm() + public function getForm(): FormInterface { if ($this->locked) { throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -214,12 +175,9 @@ public function getForm() } /** - * {@inheritdoc} - * * @return \Traversable */ - #[\ReturnTypeWillChange] - public function getIterator() + public function getIterator(): \Traversable { if ($this->locked) { throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -243,7 +201,7 @@ private function resolveChild(string $name): FormBuilderInterface /** * Converts all unresolved children into {@link FormBuilder} instances. */ - private function resolveChildren() + private function resolveChildren(): void { foreach ($this->unresolvedChildren as $name => $info) { $this->children[$name] = $this->create($name, $info[0], $info[1]); diff --git a/FormBuilderInterface.php b/FormBuilderInterface.php index 014bfbdff2..08d29303c9 100644 --- a/FormBuilderInterface.php +++ b/FormBuilderInterface.php @@ -25,12 +25,9 @@ interface FormBuilderInterface extends \Traversable, \Countable, FormConfigBuild * If you add a nested group, this group should also be represented in the * object hierarchy. * - * @param string|FormBuilderInterface $child - * @param array $options - * - * @return static + * @param array $options */ - public function add($child, ?string $type = null, array $options = []); + public function add(string|self $child, ?string $type = null, array $options = []): static; /** * Creates a form builder. @@ -38,45 +35,35 @@ public function add($child, ?string $type = null, array $options = []); * @param string $name The name of the form or the name of the property * @param string|null $type The type of the form or null if name is a property * @param array $options - * - * @return self */ - public function create(string $name, ?string $type = null, array $options = []); + public function create(string $name, ?string $type = null, array $options = []): self; /** * Returns a child by name. * - * @return self - * * @throws Exception\InvalidArgumentException if the given child does not exist */ - public function get(string $name); + public function get(string $name): self; /** * Removes the field with the given name. - * - * @return static */ - public function remove(string $name); + public function remove(string $name): static; /** * Returns whether a field with the given name exists. - * - * @return bool */ - public function has(string $name); + public function has(string $name): bool; /** * Returns the children. * * @return array */ - public function all(); + public function all(): array; /** * Creates the form. - * - * @return FormInterface */ - public function getForm(); + public function getForm(): FormInterface; } diff --git a/FormConfigBuilder.php b/FormConfigBuilder.php index 1dddd71e30..5d6bd653d4 100644 --- a/FormConfigBuilder.php +++ b/FormConfigBuilder.php @@ -26,79 +26,37 @@ */ class FormConfigBuilder implements FormConfigBuilderInterface { - /** - * Caches a globally unique {@link NativeRequestHandler} instance. - * - * @var NativeRequestHandler - */ - private static $nativeRequestHandler; - - protected $locked = false; - private $dispatcher; - private $name; - - /** - * @var PropertyPathInterface|string|null - */ - private $propertyPath; - - private $mapped = true; - private $byReference = true; - private $inheritData = false; - private $compound = false; - - /** - * @var ResolvedFormTypeInterface - */ - private $type; - - private $viewTransformers = []; - private $modelTransformers = []; - - /** - * @var DataMapperInterface|null - */ - private $dataMapper; - - private $required = true; - private $disabled = false; - private $errorBubbling = false; - - /** - * @var mixed - */ - private $emptyData; - - private $attributes = []; - - /** - * @var mixed - */ - private $data; - - /** - * @var string|null - */ - private $dataClass; - - private $dataLocked = false; + protected bool $locked = false; /** - * @var FormFactoryInterface|null - */ - private $formFactory; - - private $action = ''; - private $method = 'POST'; - - /** - * @var RequestHandlerInterface|null + * Caches a globally unique {@link NativeRequestHandler} instance. */ - private $requestHandler; - - private $autoInitialize = false; - private $options; - private $isEmptyCallback; + private static NativeRequestHandler $nativeRequestHandler; + + private string $name; + private ?PropertyPathInterface $propertyPath = null; + private bool $mapped = true; + private bool $byReference = true; + private bool $inheritData = false; + private bool $compound = false; + private ResolvedFormTypeInterface $type; + private array $viewTransformers = []; + private array $modelTransformers = []; + private ?DataMapperInterface $dataMapper = null; + private bool $required = true; + private bool $disabled = false; + private bool $errorBubbling = false; + private mixed $emptyData = null; + private array $attributes = []; + private mixed $data = null; + private ?string $dataClass; + private bool $dataLocked = false; + private FormFactoryInterface $formFactory; + private string $action = ''; + private string $method = 'POST'; + private RequestHandlerInterface $requestHandler; + private bool $autoInitialize = false; + private ?\Closure $isEmptyCallback = null; /** * Creates an empty form configuration. @@ -109,24 +67,23 @@ 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; } - /** - * {@inheritdoc} - */ - public function addEventListener(string $eventName, callable $listener, int $priority = 0) + public function addEventListener(string $eventName, callable $listener, int $priority = 0): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -137,10 +94,7 @@ public function addEventListener(string $eventName, callable $listener, int $pri return $this; } - /** - * {@inheritdoc} - */ - public function addEventSubscriber(EventSubscriberInterface $subscriber) + public function addEventSubscriber(EventSubscriberInterface $subscriber): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -151,10 +105,7 @@ public function addEventSubscriber(EventSubscriberInterface $subscriber) return $this; } - /** - * {@inheritdoc} - */ - public function addViewTransformer(DataTransformerInterface $viewTransformer, bool $forcePrepend = false) + public function addViewTransformer(DataTransformerInterface $viewTransformer, bool $forcePrepend = false): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -169,10 +120,7 @@ public function addViewTransformer(DataTransformerInterface $viewTransformer, bo return $this; } - /** - * {@inheritdoc} - */ - public function resetViewTransformers() + public function resetViewTransformers(): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -183,10 +131,7 @@ public function resetViewTransformers() return $this; } - /** - * {@inheritdoc} - */ - public function addModelTransformer(DataTransformerInterface $modelTransformer, bool $forceAppend = false) + public function addModelTransformer(DataTransformerInterface $modelTransformer, bool $forceAppend = false): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -201,10 +146,7 @@ public function addModelTransformer(DataTransformerInterface $modelTransformer, return $this; } - /** - * {@inheritdoc} - */ - public function resetModelTransformers() + public function resetModelTransformers(): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -215,10 +157,7 @@ public function resetModelTransformers() return $this; } - /** - * {@inheritdoc} - */ - public function getEventDispatcher() + public function getEventDispatcher(): EventDispatcherInterface { if ($this->locked && !$this->dispatcher instanceof ImmutableEventDispatcher) { $this->dispatcher = new ImmutableEventDispatcher($this->dispatcher); @@ -227,170 +166,107 @@ public function getEventDispatcher() return $this->dispatcher; } - /** - * {@inheritdoc} - */ - public function getName() + public function getName(): string { return $this->name; } - /** - * {@inheritdoc} - */ - public function getPropertyPath() + public function getPropertyPath(): ?PropertyPathInterface { return $this->propertyPath; } - /** - * {@inheritdoc} - */ - public function getMapped() + public function getMapped(): bool { return $this->mapped; } - /** - * {@inheritdoc} - */ - public function getByReference() + public function getByReference(): bool { return $this->byReference; } - /** - * {@inheritdoc} - */ - public function getInheritData() + public function getInheritData(): bool { return $this->inheritData; } - /** - * {@inheritdoc} - */ - public function getCompound() + public function getCompound(): bool { return $this->compound; } - /** - * {@inheritdoc} - */ - public function getType() + public function getType(): ResolvedFormTypeInterface { return $this->type; } - /** - * {@inheritdoc} - */ - public function getViewTransformers() + public function getViewTransformers(): array { return $this->viewTransformers; } - /** - * {@inheritdoc} - */ - public function getModelTransformers() + public function getModelTransformers(): array { return $this->modelTransformers; } - /** - * {@inheritdoc} - */ - public function getDataMapper() + public function getDataMapper(): ?DataMapperInterface { return $this->dataMapper; } - /** - * {@inheritdoc} - */ - public function getRequired() + public function getRequired(): bool { return $this->required; } - /** - * {@inheritdoc} - */ - public function getDisabled() + public function getDisabled(): bool { return $this->disabled; } - /** - * {@inheritdoc} - */ - public function getErrorBubbling() + public function getErrorBubbling(): bool { return $this->errorBubbling; } - /** - * {@inheritdoc} - */ - public function getEmptyData() + public function getEmptyData(): mixed { return $this->emptyData; } - /** - * {@inheritdoc} - */ - public function getAttributes() + public function getAttributes(): array { return $this->attributes; } - /** - * {@inheritdoc} - */ - public function hasAttribute(string $name) + public function hasAttribute(string $name): bool { return \array_key_exists($name, $this->attributes); } - /** - * {@inheritdoc} - */ - public function getAttribute(string $name, $default = null) + public function getAttribute(string $name, mixed $default = null): mixed { return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default; } - /** - * {@inheritdoc} - */ - public function getData() + public function getData(): mixed { return $this->data; } - /** - * {@inheritdoc} - */ - public function getDataClass() + public function getDataClass(): ?string { return $this->dataClass; } - /** - * {@inheritdoc} - */ - public function getDataLocked() + public function getDataLocked(): bool { return $this->dataLocked; } - /** - * {@inheritdoc} - */ - public function getFormFactory() + public function getFormFactory(): FormFactoryInterface { if (!isset($this->formFactory)) { throw new BadMethodCallException('The form factory must be set before retrieving it.'); @@ -399,81 +275,50 @@ public function getFormFactory() return $this->formFactory; } - /** - * {@inheritdoc} - */ - public function getAction() + public function getAction(): string { return $this->action; } - /** - * {@inheritdoc} - */ - public function getMethod() + public function getMethod(): string { return $this->method; } - /** - * {@inheritdoc} - */ - public function getRequestHandler() + public function getRequestHandler(): RequestHandlerInterface { - if (null === $this->requestHandler) { - if (null === self::$nativeRequestHandler) { - self::$nativeRequestHandler = new NativeRequestHandler(); - } - $this->requestHandler = self::$nativeRequestHandler; - } - - return $this->requestHandler; + return $this->requestHandler ??= self::$nativeRequestHandler ??= new NativeRequestHandler(); } - /** - * {@inheritdoc} - */ - public function getAutoInitialize() + public function getAutoInitialize(): bool { return $this->autoInitialize; } - /** - * {@inheritdoc} - */ - public function getOptions() + public function getOptions(): array { return $this->options; } - /** - * {@inheritdoc} - */ - public function hasOption(string $name) + public function hasOption(string $name): bool { return \array_key_exists($name, $this->options); } - /** - * {@inheritdoc} - */ - public function getOption(string $name, $default = null) + public function getOption(string $name, mixed $default = null): mixed { return \array_key_exists($name, $this->options) ? $this->options[$name] : $default; } - /** - * {@inheritdoc} - */ public function getIsEmptyCallback(): ?callable { return $this->isEmptyCallback; } /** - * {@inheritdoc} + * @return $this */ - public function setAttribute(string $name, $value) + public function setAttribute(string $name, mixed $value): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -485,9 +330,9 @@ public function setAttribute(string $name, $value) } /** - * {@inheritdoc} + * @return $this */ - public function setAttributes(array $attributes) + public function setAttributes(array $attributes): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -499,9 +344,9 @@ public function setAttributes(array $attributes) } /** - * {@inheritdoc} + * @return $this */ - public function setDataMapper(?DataMapperInterface $dataMapper = null) + public function setDataMapper(?DataMapperInterface $dataMapper): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -513,9 +358,9 @@ public function setDataMapper(?DataMapperInterface $dataMapper = null) } /** - * {@inheritdoc} + * @return $this */ - public function setDisabled(bool $disabled) + public function setDisabled(bool $disabled): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -527,9 +372,9 @@ public function setDisabled(bool $disabled) } /** - * {@inheritdoc} + * @return $this */ - public function setEmptyData($emptyData) + public function setEmptyData(mixed $emptyData): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -541,9 +386,9 @@ public function setEmptyData($emptyData) } /** - * {@inheritdoc} + * @return $this */ - public function setErrorBubbling(bool $errorBubbling) + public function setErrorBubbling(bool $errorBubbling): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -555,9 +400,9 @@ public function setErrorBubbling(bool $errorBubbling) } /** - * {@inheritdoc} + * @return $this */ - public function setRequired(bool $required) + public function setRequired(bool $required): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -569,9 +414,9 @@ public function setRequired(bool $required) } /** - * {@inheritdoc} + * @return $this */ - public function setPropertyPath($propertyPath) + public function setPropertyPath(string|PropertyPathInterface|null $propertyPath): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -587,9 +432,9 @@ public function setPropertyPath($propertyPath) } /** - * {@inheritdoc} + * @return $this */ - public function setMapped(bool $mapped) + public function setMapped(bool $mapped): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -601,9 +446,9 @@ public function setMapped(bool $mapped) } /** - * {@inheritdoc} + * @return $this */ - public function setByReference(bool $byReference) + public function setByReference(bool $byReference): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -615,9 +460,9 @@ public function setByReference(bool $byReference) } /** - * {@inheritdoc} + * @return $this */ - public function setInheritData(bool $inheritData) + public function setInheritData(bool $inheritData): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -629,9 +474,9 @@ public function setInheritData(bool $inheritData) } /** - * {@inheritdoc} + * @return $this */ - public function setCompound(bool $compound) + public function setCompound(bool $compound): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -643,9 +488,9 @@ public function setCompound(bool $compound) } /** - * {@inheritdoc} + * @return $this */ - public function setType(ResolvedFormTypeInterface $type) + public function setType(ResolvedFormTypeInterface $type): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -657,9 +502,9 @@ public function setType(ResolvedFormTypeInterface $type) } /** - * {@inheritdoc} + * @return $this */ - public function setData($data) + public function setData(mixed $data): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -671,9 +516,9 @@ public function setData($data) } /** - * {@inheritdoc} + * @return $this */ - public function setDataLocked(bool $locked) + public function setDataLocked(bool $locked): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -685,9 +530,9 @@ public function setDataLocked(bool $locked) } /** - * {@inheritdoc} + * @return $this */ - public function setFormFactory(FormFactoryInterface $formFactory) + public function setFormFactory(FormFactoryInterface $formFactory): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -699,9 +544,9 @@ public function setFormFactory(FormFactoryInterface $formFactory) } /** - * {@inheritdoc} + * @return $this */ - public function setAction(string $action) + public function setAction(string $action): static { if ($this->locked) { throw new BadMethodCallException('The config builder cannot be modified anymore.'); @@ -713,9 +558,9 @@ public function setAction(string $action) } /** - * {@inheritdoc} + * @return $this */ - public function setMethod(string $method) + public function setMethod(string $method): static { if ($this->locked) { throw new BadMethodCallException('The config builder cannot be modified anymore.'); @@ -727,9 +572,9 @@ public function setMethod(string $method) } /** - * {@inheritdoc} + * @return $this */ - public function setRequestHandler(RequestHandlerInterface $requestHandler) + public function setRequestHandler(RequestHandlerInterface $requestHandler): static { if ($this->locked) { throw new BadMethodCallException('The config builder cannot be modified anymore.'); @@ -741,9 +586,9 @@ public function setRequestHandler(RequestHandlerInterface $requestHandler) } /** - * {@inheritdoc} + * @return $this */ - public function setAutoInitialize(bool $initialize) + public function setAutoInitialize(bool $initialize): static { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -754,10 +599,7 @@ public function setAutoInitialize(bool $initialize) return $this; } - /** - * {@inheritdoc} - */ - public function getFormConfig() + public function getFormConfig(): FormConfigInterface { if ($this->locked) { throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); @@ -771,11 +613,11 @@ public function getFormConfig() } /** - * {@inheritdoc} + * @return $this */ - public function setIsEmptyCallback(?callable $isEmptyCallback) + public function setIsEmptyCallback(?callable $isEmptyCallback): static { - $this->isEmptyCallback = $isEmptyCallback; + $this->isEmptyCallback = null === $isEmptyCallback ? null : $isEmptyCallback(...); return $this; } @@ -787,10 +629,10 @@ public function setIsEmptyCallback(?callable $isEmptyCallback) * * @internal */ - final public static function validateName(?string $name) + 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/FormConfigBuilderInterface.php b/FormConfigBuilderInterface.php index 86d6171c22..6b22b9f9e8 100644 --- a/FormConfigBuilderInterface.php +++ b/FormConfigBuilderInterface.php @@ -16,8 +16,6 @@ /** * @author Bernhard Schussek - * - * @method $this setIsEmptyCallback(callable|null $isEmptyCallback) Sets the callback that will be called to determine if the model data of the form is empty or not - not implementing it is deprecated since Symfony 5.1 */ interface FormConfigBuilderInterface extends FormConfigInterface { @@ -30,14 +28,14 @@ interface FormConfigBuilderInterface extends FormConfigInterface * * @return $this */ - public function addEventListener(string $eventName, callable $listener, int $priority = 0); + public function addEventListener(string $eventName, callable $listener, int $priority = 0): static; /** * Adds an event subscriber for events on this form. * * @return $this */ - public function addEventSubscriber(EventSubscriberInterface $subscriber); + public function addEventSubscriber(EventSubscriberInterface $subscriber): static; /** * Appends / prepends a transformer to the view transformer chain. @@ -51,14 +49,14 @@ public function addEventSubscriber(EventSubscriberInterface $subscriber); * * @return $this */ - public function addViewTransformer(DataTransformerInterface $viewTransformer, bool $forcePrepend = false); + public function addViewTransformer(DataTransformerInterface $viewTransformer, bool $forcePrepend = false): static; /** * Clears the view transformers. * * @return $this */ - public function resetViewTransformers(); + public function resetViewTransformers(): static; /** * Prepends / appends a transformer to the normalization transformer chain. @@ -72,14 +70,14 @@ public function resetViewTransformers(); * * @return $this */ - public function addModelTransformer(DataTransformerInterface $modelTransformer, bool $forceAppend = false); + public function addModelTransformer(DataTransformerInterface $modelTransformer, bool $forceAppend = false): static; /** * Clears the normalization transformers. * * @return $this */ - public function resetModelTransformers(); + public function resetModelTransformers(): static; /** * Sets the value for an attribute. @@ -88,28 +86,28 @@ public function resetModelTransformers(); * * @return $this */ - public function setAttribute(string $name, $value); + public function setAttribute(string $name, mixed $value): static; /** * Sets the attributes. * * @return $this */ - public function setAttributes(array $attributes); + public function setAttributes(array $attributes): static; /** * Sets the data mapper used by the form. * * @return $this */ - public function setDataMapper(?DataMapperInterface $dataMapper = null); + public function setDataMapper(?DataMapperInterface $dataMapper): static; /** * Sets whether the form is disabled. * * @return $this */ - public function setDisabled(bool $disabled); + public function setDisabled(bool $disabled): static; /** * Sets the data used for the client data when no value is submitted. @@ -118,21 +116,21 @@ public function setDisabled(bool $disabled); * * @return $this */ - public function setEmptyData($emptyData); + public function setEmptyData(mixed $emptyData): static; /** * Sets whether errors bubble up to the parent. * * @return $this */ - public function setErrorBubbling(bool $errorBubbling); + public function setErrorBubbling(bool $errorBubbling): static; /** * Sets whether this field is required to be filled out when submitted. * * @return $this */ - public function setRequired(bool $required); + public function setRequired(bool $required): static; /** * Sets the property path that the form should be mapped to. @@ -142,7 +140,7 @@ public function setRequired(bool $required); * * @return $this */ - public function setPropertyPath($propertyPath); + public function setPropertyPath(string|PropertyPathInterface|null $propertyPath): static; /** * Sets whether the form should be mapped to an element of its @@ -150,21 +148,21 @@ public function setPropertyPath($propertyPath); * * @return $this */ - public function setMapped(bool $mapped); + public function setMapped(bool $mapped): static; /** * Sets whether the form's data should be modified by reference. * * @return $this */ - public function setByReference(bool $byReference); + public function setByReference(bool $byReference): static; /** * Sets whether the form should read and write the data of its parent. * * @return $this */ - public function setInheritData(bool $inheritData); + public function setInheritData(bool $inheritData): static; /** * Sets whether the form should be compound. @@ -173,14 +171,14 @@ public function setInheritData(bool $inheritData); * * @see FormConfigInterface::getCompound() */ - public function setCompound(bool $compound); + public function setCompound(bool $compound): static; /** * Sets the resolved type. * * @return $this */ - public function setType(ResolvedFormTypeInterface $type); + public function setType(ResolvedFormTypeInterface $type): static; /** * Sets the initial data of the form. @@ -189,7 +187,7 @@ public function setType(ResolvedFormTypeInterface $type); * * @return $this */ - public function setData($data); + public function setData(mixed $data): static; /** * Locks the form's data to the data passed in the configuration. @@ -203,33 +201,35 @@ public function setData($data); * * @return $this */ - public function setDataLocked(bool $locked); + public function setDataLocked(bool $locked): static; /** * Sets the form factory used for creating new forms. + * + * @return $this */ - public function setFormFactory(FormFactoryInterface $formFactory); + public function setFormFactory(FormFactoryInterface $formFactory): static; /** * Sets the target URL of the form. * * @return $this */ - public function setAction(string $action); + public function setAction(string $action): static; /** * Sets the HTTP method used by the form. * * @return $this */ - public function setMethod(string $method); + public function setMethod(string $method): static; /** * Sets the request handler used by the form. * * @return $this */ - public function setRequestHandler(RequestHandlerInterface $requestHandler); + public function setRequestHandler(RequestHandlerInterface $requestHandler): static; /** * Sets whether the form should be initialized automatically. @@ -243,12 +243,18 @@ public function setRequestHandler(RequestHandlerInterface $requestHandler); * * @return $this */ - public function setAutoInitialize(bool $initialize); + public function setAutoInitialize(bool $initialize): static; /** * Builds and returns the form configuration. + */ + public function getFormConfig(): FormConfigInterface; + + /** + * Sets the callback that will be called to determine if the model + * data of the form is empty or not. * - * @return FormConfigInterface + * @return $this */ - public function getFormConfig(); + public function setIsEmptyCallback(?callable $isEmptyCallback): static; } diff --git a/FormConfigInterface.php b/FormConfigInterface.php index e332feb1e2..93d1998ec2 100644 --- a/FormConfigInterface.php +++ b/FormConfigInterface.php @@ -18,53 +18,39 @@ * The configuration of a {@link Form} object. * * @author Bernhard Schussek - * - * @method callable|null getIsEmptyCallback() Returns a callable that takes the model data as argument and that returns if it is empty or not - not implementing it is deprecated since Symfony 5.1 */ interface FormConfigInterface { /** * Returns the event dispatcher used to dispatch form events. - * - * @return EventDispatcherInterface */ - public function getEventDispatcher(); + public function getEventDispatcher(): EventDispatcherInterface; /** * Returns the name of the form used as HTTP parameter. - * - * @return string */ - public function getName(); + public function getName(): string; /** * Returns the property path that the form should be mapped to. - * - * @return PropertyPathInterface|null */ - public function getPropertyPath(); + public function getPropertyPath(): ?PropertyPathInterface; /** * Returns whether the form should be mapped to an element of its * parent's data. - * - * @return bool */ - public function getMapped(); + public function getMapped(): bool; /** * Returns whether the form's data should be modified by reference. - * - * @return bool */ - public function getByReference(); + public function getByReference(): bool; /** * Returns whether the form should read and write the data of its parent. - * - * @return bool */ - public function getInheritData(); + public function getInheritData(): bool; /** * Returns whether the form is compound. @@ -74,59 +60,47 @@ public function getInheritData(); * for example an empty collection form. * The contrary is not possible, a form which is not compound * cannot have any children. - * - * @return bool */ - public function getCompound(); + public function getCompound(): bool; /** * Returns the resolved form type used to construct the form. - * - * @return ResolvedFormTypeInterface */ - public function getType(); + public function getType(): ResolvedFormTypeInterface; /** * Returns the view transformers of the form. * * @return DataTransformerInterface[] */ - public function getViewTransformers(); + public function getViewTransformers(): array; /** * Returns the model transformers of the form. * * @return DataTransformerInterface[] */ - public function getModelTransformers(); + public function getModelTransformers(): array; /** * Returns the data mapper of the compound form or null for a simple form. - * - * @return DataMapperInterface|null */ - public function getDataMapper(); + public function getDataMapper(): ?DataMapperInterface; /** * Returns whether the form is required. - * - * @return bool */ - public function getRequired(); + public function getRequired(): bool; /** * Returns whether the form is disabled. - * - * @return bool */ - public function getDisabled(); + public function getDisabled(): bool; /** * Returns whether errors attached to the form will bubble to its parent. - * - * @return bool */ - public function getErrorBubbling(); + public function getErrorBubbling(): bool; /** * Used when the view data is empty on submission. @@ -136,47 +110,33 @@ public function getErrorBubbling(); * * The empty data must match the view format as it will passed to the first view transformer's * "reverseTransform" method. - * - * @return mixed */ - public function getEmptyData(); + public function getEmptyData(): mixed; /** * Returns additional attributes of the form. - * - * @return array */ - public function getAttributes(); + public function getAttributes(): array; /** * Returns whether the attribute with the given name exists. - * - * @return bool */ - public function hasAttribute(string $name); + public function hasAttribute(string $name): bool; /** * Returns the value of the given attribute. - * - * @param mixed $default The value returned if the attribute does not exist - * - * @return mixed */ - public function getAttribute(string $name, $default = null); + public function getAttribute(string $name, mixed $default = null): mixed; /** * Returns the initial data of the form. - * - * @return mixed */ - public function getData(); + public function getData(): mixed; /** * Returns the class of the view data or null if the data is scalar or an array. - * - * @return string|null */ - public function getDataClass(); + public function getDataClass(): ?string; /** * Returns whether the form's data is locked. @@ -184,66 +144,53 @@ public function getDataClass(); * A form with locked data is restricted to the data passed in * this configuration. The data can only be modified then by * submitting the form. - * - * @return bool */ - public function getDataLocked(); + public function getDataLocked(): bool; /** * Returns the form factory used for creating new forms. - * - * @return FormFactoryInterface */ - public function getFormFactory(); + public function getFormFactory(): FormFactoryInterface; /** * Returns the target URL of the form. - * - * @return string */ - public function getAction(); + public function getAction(): string; /** * Returns the HTTP method used by the form. - * - * @return string */ - public function getMethod(); + public function getMethod(): string; /** * Returns the request handler used by the form. - * - * @return RequestHandlerInterface */ - public function getRequestHandler(); + public function getRequestHandler(): RequestHandlerInterface; /** * Returns whether the form should be initialized upon creation. - * - * @return bool */ - public function getAutoInitialize(); + public function getAutoInitialize(): bool; /** * Returns all options passed during the construction of the form. * * @return array The passed options */ - public function getOptions(); + public function getOptions(): array; /** * Returns whether a specific option exists. - * - * @return bool */ - public function hasOption(string $name); + public function hasOption(string $name): bool; /** * Returns the value of a specific option. - * - * @param mixed $default The value returned if the option does not exist - * - * @return mixed */ - public function getOption(string $name, $default = null); + public function getOption(string $name, mixed $default = null): mixed; + + /** + * Returns a callable that takes the model data as argument and that returns if it is empty or not. + */ + public function getIsEmptyCallback(): ?callable; } diff --git a/FormError.php b/FormError.php index 07fbb8c00e..335d9e21d3 100644 --- a/FormError.php +++ b/FormError.php @@ -20,19 +20,12 @@ */ class FormError { - protected $messageTemplate; - protected $messageParameters; - protected $messagePluralization; - - private $message; - private $cause; + protected string $messageTemplate; /** * The form that spawned this error. - * - * @var FormInterface */ - private $origin; + private ?FormInterface $origin = null; /** * Any array key in $messageParameters will be used as a placeholder in @@ -47,61 +40,52 @@ class FormError * * @see \Symfony\Component\Translation\Translator */ - public function __construct(string $message, ?string $messageTemplate = null, array $messageParameters = [], ?int $messagePluralization = null, $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; } /** * Returns the error message. - * - * @return string */ - public function getMessage() + public function getMessage(): string { return $this->message; } /** * Returns the error message template. - * - * @return string */ - public function getMessageTemplate() + public function getMessageTemplate(): string { return $this->messageTemplate; } /** * Returns the parameters to be inserted in the message template. - * - * @return array */ - public function getMessageParameters() + public function getMessageParameters(): array { return $this->messageParameters; } /** * Returns the value for error message pluralization. - * - * @return int|null */ - public function getMessagePluralization() + public function getMessagePluralization(): ?int { return $this->messagePluralization; } /** * Returns the cause of this error. - * - * @return mixed */ - public function getCause() + public function getCause(): mixed { return $this->cause; } @@ -113,7 +97,7 @@ public function getCause() * * @throws BadMethodCallException If the method is called more than once */ - public function setOrigin(FormInterface $origin) + public function setOrigin(FormInterface $origin): void { if (null !== $this->origin) { throw new BadMethodCallException('setOrigin() must only be called once.'); @@ -124,10 +108,8 @@ public function setOrigin(FormInterface $origin) /** * Returns the form that caused this error. - * - * @return FormInterface|null */ - public function getOrigin() + public function getOrigin(): ?FormInterface { return $this->origin; } diff --git a/FormErrorIterator.php b/FormErrorIterator.php index 9ee2f0e8fe..4f4a2574d2 100644 --- a/FormErrorIterator.php +++ b/FormErrorIterator.php @@ -35,43 +35,40 @@ * @implements \RecursiveIterator * @implements \SeekableIterator */ -class FormErrorIterator implements \RecursiveIterator, \SeekableIterator, \ArrayAccess, \Countable +class FormErrorIterator implements \RecursiveIterator, \SeekableIterator, \ArrayAccess, \Countable, \Stringable { /** * The prefix used for indenting nested error messages. */ public const INDENTATION = ' '; - private $form; - /** * @var list */ - private $errors; + private array $errors; /** * @param list $errors * * @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; } /** * Returns all iterated error messages as string. - * - * @return string */ - public function __toString() + public function __toString(): string { $string = ''; @@ -90,10 +87,8 @@ public function __toString() /** * Returns the iterated form. - * - * @return FormInterface */ - public function getForm() + public function getForm(): FormInterface { return $this->form; } @@ -103,8 +98,7 @@ public function getForm() * * @return T An error or an iterator containing nested errors */ - #[\ReturnTypeWillChange] - public function current() + public function current(): FormError|self { return current($this->errors); } @@ -112,30 +106,23 @@ public function current() /** * Advances the iterator to the next position. */ - #[\ReturnTypeWillChange] - public function next() + public function next(): void { next($this->errors); } /** * Returns the current position of the iterator. - * - * @return int */ - #[\ReturnTypeWillChange] - public function key() + public function key(): int { return key($this->errors); } /** * Returns whether the iterator's position is valid. - * - * @return bool */ - #[\ReturnTypeWillChange] - public function valid() + public function valid(): bool { return null !== key($this->errors); } @@ -146,8 +133,7 @@ public function valid() * This method detects if errors have been added to the form since the * construction of the iterator. */ - #[\ReturnTypeWillChange] - public function rewind() + public function rewind(): void { reset($this->errors); } @@ -156,11 +142,8 @@ public function rewind() * Returns whether a position exists in the iterator. * * @param int $position The position - * - * @return bool */ - #[\ReturnTypeWillChange] - public function offsetExists($position) + public function offsetExists(mixed $position): bool { return isset($this->errors[$position]); } @@ -174,8 +157,7 @@ public function offsetExists($position) * * @throws OutOfBoundsException If the given position does not exist */ - #[\ReturnTypeWillChange] - public function offsetGet($position) + public function offsetGet(mixed $position): FormError|self { if (!isset($this->errors[$position])) { throw new OutOfBoundsException('The offset '.$position.' does not exist.'); @@ -187,12 +169,9 @@ public function offsetGet($position) /** * Unsupported method. * - * @return void - * * @throws BadMethodCallException */ - #[\ReturnTypeWillChange] - public function offsetSet($position, $value) + public function offsetSet(mixed $position, mixed $value): void { throw new BadMethodCallException('The iterator doesn\'t support modification of elements.'); } @@ -200,12 +179,9 @@ public function offsetSet($position, $value) /** * Unsupported method. * - * @return void - * * @throws BadMethodCallException */ - #[\ReturnTypeWillChange] - public function offsetUnset($position) + public function offsetUnset(mixed $position): void { throw new BadMethodCallException('The iterator doesn\'t support modification of elements.'); } @@ -213,24 +189,16 @@ public function offsetUnset($position) /** * Returns whether the current element of the iterator can be recursed * into. - * - * @return bool */ - #[\ReturnTypeWillChange] - public function hasChildren() + public function hasChildren(): bool { return current($this->errors) instanceof self; } - /** - * @return self - */ - #[\ReturnTypeWillChange] - public function getChildren() + public function getChildren(): self { if (!$this->hasChildren()) { - trigger_deprecation('symfony/form', '5.4', 'Calling "%s()" if the current element is not iterable is deprecated, call "%s" to get the current element.', __METHOD__, self::class.'::current()'); - // 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 */ @@ -253,11 +221,8 @@ public function getChildren() * Alternatively, set the constructor argument $flatten to true as well. * * $count = count($form->getErrors(true, true)); - * - * @return int */ - #[\ReturnTypeWillChange] - public function count() + public function count(): int { return \count($this->errors); } @@ -265,14 +230,9 @@ public function count() /** * Sets the position of the iterator. * - * @param int $position The new position - * - * @return void - * * @throws OutOfBoundsException If the position is invalid */ - #[\ReturnTypeWillChange] - public function seek($position) + public function seek(int $position): void { if (!isset($this->errors[$position])) { throw new OutOfBoundsException('The offset '.$position.' does not exist.'); @@ -289,10 +249,8 @@ public function seek($position) * Creates iterator for errors with specific codes. * * @param string|string[] $codes The codes to find - * - * @return static */ - public function findByCodes($codes) + public function findByCodes(string|array $codes): static { $codes = (array) $codes; $errors = []; diff --git a/FormEvent.php b/FormEvent.php index c466fafdc6..e6a3878f6d 100644 --- a/FormEvent.php +++ b/FormEvent.php @@ -18,44 +18,32 @@ */ class FormEvent extends Event { - private $form; - protected $data; - - /** - * @param mixed $data The data - */ - public function __construct(FormInterface $form, $data) - { - $this->form = $form; - $this->data = $data; + public function __construct( + private FormInterface $form, + protected mixed $data, + ) { } /** * Returns the form at the source of the event. - * - * @return FormInterface */ - public function getForm() + public function getForm(): FormInterface { return $this->form; } /** * Returns the data associated with this event. - * - * @return mixed */ - public function getData() + public function getData(): mixed { return $this->data; } /** * Allows updating with some filtered data. - * - * @param mixed $data */ - public function setData($data) + public function setData(mixed $data): void { $this->data = $data; } diff --git a/FormExtensionInterface.php b/FormExtensionInterface.php index 8fafcee2ae..e540e18256 100644 --- a/FormExtensionInterface.php +++ b/FormExtensionInterface.php @@ -21,20 +21,16 @@ interface FormExtensionInterface * * @param string $name The name of the type * - * @return FormTypeInterface - * * @throws Exception\InvalidArgumentException if the given type is not supported by this extension */ - public function getType(string $name); + public function getType(string $name): FormTypeInterface; /** * Returns whether the given type is supported. * * @param string $name The name of the type - * - * @return bool */ - public function hasType(string $name); + public function hasType(string $name): bool; /** * Returns the extensions for the given type. @@ -43,21 +39,17 @@ public function hasType(string $name); * * @return FormTypeExtensionInterface[] */ - public function getTypeExtensions(string $name); + public function getTypeExtensions(string $name): array; /** * Returns whether this extension provides type extensions for the given type. * * @param string $name The name of the type - * - * @return bool */ - public function hasTypeExtensions(string $name); + public function hasTypeExtensions(string $name): bool; /** * Returns the type guesser provided by this extension. - * - * @return FormTypeGuesserInterface|null */ - public function getTypeGuesser(); + public function getTypeGuesser(): ?FormTypeGuesserInterface; } diff --git a/FormFactory.php b/FormFactory.php index b3185d1a37..dcf7b36f28 100644 --- a/FormFactory.php +++ b/FormFactory.php @@ -16,49 +16,32 @@ class FormFactory implements FormFactoryInterface { - private $registry; - - public function __construct(FormRegistryInterface $registry) - { - $this->registry = $registry; + public function __construct( + private FormRegistryInterface $registry, + ) { } - /** - * {@inheritdoc} - */ - public function create(string $type = FormType::class, $data = null, array $options = []) + public function create(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface { return $this->createBuilder($type, $data, $options)->getForm(); } - /** - * {@inheritdoc} - */ - public function createNamed(string $name, string $type = FormType::class, $data = null, array $options = []) + public function createNamed(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormInterface { return $this->createNamedBuilder($name, $type, $data, $options)->getForm(); } - /** - * {@inheritdoc} - */ - public function createForProperty(string $class, string $property, $data = null, array $options = []) + public function createForProperty(string $class, string $property, mixed $data = null, array $options = []): FormInterface { return $this->createBuilderForProperty($class, $property, $data, $options)->getForm(); } - /** - * {@inheritdoc} - */ - public function createBuilder(string $type = FormType::class, $data = null, array $options = []) + public function createBuilder(string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface { return $this->createNamedBuilder($this->registry->getType($type)->getBlockPrefix(), $type, $data, $options); } - /** - * {@inheritdoc} - */ - public function createNamedBuilder(string $name, string $type = FormType::class, $data = null, array $options = []) + public function createNamedBuilder(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface { if (null !== $data && !\array_key_exists('data', $options)) { $options['data'] = $data; @@ -75,10 +58,7 @@ public function createNamedBuilder(string $name, string $type = FormType::class, return $builder; } - /** - * {@inheritdoc} - */ - public function createBuilderForProperty(string $class, string $property, $data = null, array $options = []) + public function createBuilderForProperty(string $class, string $property, mixed $data = null, array $options = []): FormBuilderInterface { if (null === $guesser = $this->registry->getTypeGuesser()) { return $this->createNamedBuilder($property, TextType::class, $data, $options); @@ -91,8 +71,8 @@ public function createBuilderForProperty(string $class, string $property, $data $type = $typeGuess ? $typeGuess->getType() : TextType::class; - $maxLength = $maxLengthGuess ? $maxLengthGuess->getValue() : null; - $pattern = $patternGuess ? $patternGuess->getValue() : null; + $maxLength = $maxLengthGuess?->getValue(); + $pattern = $patternGuess?->getValue(); if (null !== $pattern) { $options = array_replace_recursive(['attr' => ['pattern' => $pattern]], $options); diff --git a/FormFactoryBuilder.php b/FormFactoryBuilder.php index 735a17e8e5..90e3bf20ca 100644 --- a/FormFactoryBuilder.php +++ b/FormFactoryBuilder.php @@ -20,82 +20,62 @@ */ class FormFactoryBuilder implements FormFactoryBuilderInterface { - private $forceCoreExtension; - - /** - * @var ResolvedFormTypeFactoryInterface - */ - private $resolvedTypeFactory; + private ResolvedFormTypeFactoryInterface $resolvedTypeFactory; /** * @var FormExtensionInterface[] */ - private $extensions = []; + private array $extensions = []; /** * @var FormTypeInterface[] */ - private $types = []; + private array $types = []; /** * @var FormTypeExtensionInterface[][] */ - private $typeExtensions = []; + private array $typeExtensions = []; /** * @var FormTypeGuesserInterface[] */ - private $typeGuessers = []; + private array $typeGuessers = []; - public function __construct(bool $forceCoreExtension = false) - { - $this->forceCoreExtension = $forceCoreExtension; + public function __construct( + private bool $forceCoreExtension = false, + ) { } - /** - * {@inheritdoc} - */ - public function setResolvedTypeFactory(ResolvedFormTypeFactoryInterface $resolvedTypeFactory) + public function setResolvedTypeFactory(ResolvedFormTypeFactoryInterface $resolvedTypeFactory): static { $this->resolvedTypeFactory = $resolvedTypeFactory; return $this; } - /** - * {@inheritdoc} - */ - public function addExtension(FormExtensionInterface $extension) + public function addExtension(FormExtensionInterface $extension): static { $this->extensions[] = $extension; return $this; } - /** - * {@inheritdoc} - */ - public function addExtensions(array $extensions) + public function addExtensions(array $extensions): static { $this->extensions = array_merge($this->extensions, $extensions); return $this; } - /** - * {@inheritdoc} - */ - public function addType(FormTypeInterface $type) + public function addType(FormTypeInterface $type): static { $this->types[] = $type; return $this; } - /** - * {@inheritdoc} - */ - public function addTypes(array $types) + public function addTypes(array $types): static { foreach ($types as $type) { $this->types[] = $type; @@ -104,10 +84,7 @@ public function addTypes(array $types) return $this; } - /** - * {@inheritdoc} - */ - public function addTypeExtension(FormTypeExtensionInterface $typeExtension) + public function addTypeExtension(FormTypeExtensionInterface $typeExtension): static { foreach ($typeExtension::getExtendedTypes() as $extendedType) { $this->typeExtensions[$extendedType][] = $typeExtension; @@ -116,10 +93,7 @@ public function addTypeExtension(FormTypeExtensionInterface $typeExtension) return $this; } - /** - * {@inheritdoc} - */ - public function addTypeExtensions(array $typeExtensions) + public function addTypeExtensions(array $typeExtensions): static { foreach ($typeExtensions as $typeExtension) { $this->addTypeExtension($typeExtension); @@ -128,30 +102,21 @@ public function addTypeExtensions(array $typeExtensions) return $this; } - /** - * {@inheritdoc} - */ - public function addTypeGuesser(FormTypeGuesserInterface $typeGuesser) + public function addTypeGuesser(FormTypeGuesserInterface $typeGuesser): static { $this->typeGuessers[] = $typeGuesser; return $this; } - /** - * {@inheritdoc} - */ - public function addTypeGuessers(array $typeGuessers) + public function addTypeGuessers(array $typeGuessers): static { $this->typeGuessers = array_merge($this->typeGuessers, $typeGuessers); return $this; } - /** - * {@inheritdoc} - */ - public function getFormFactory() + public function getFormFactory(): FormFactoryInterface { $extensions = $this->extensions; diff --git a/FormFactoryBuilderInterface.php b/FormFactoryBuilderInterface.php index e3b0a7b3ee..70bdf507b3 100644 --- a/FormFactoryBuilderInterface.php +++ b/FormFactoryBuilderInterface.php @@ -23,14 +23,14 @@ interface FormFactoryBuilderInterface * * @return $this */ - public function setResolvedTypeFactory(ResolvedFormTypeFactoryInterface $resolvedTypeFactory); + public function setResolvedTypeFactory(ResolvedFormTypeFactoryInterface $resolvedTypeFactory): static; /** * Adds an extension to be loaded by the factory. * * @return $this */ - public function addExtension(FormExtensionInterface $extension); + public function addExtension(FormExtensionInterface $extension): static; /** * Adds a list of extensions to be loaded by the factory. @@ -39,14 +39,14 @@ public function addExtension(FormExtensionInterface $extension); * * @return $this */ - public function addExtensions(array $extensions); + public function addExtensions(array $extensions): static; /** * Adds a form type to the factory. * * @return $this */ - public function addType(FormTypeInterface $type); + public function addType(FormTypeInterface $type): static; /** * Adds a list of form types to the factory. @@ -55,14 +55,14 @@ public function addType(FormTypeInterface $type); * * @return $this */ - public function addTypes(array $types); + public function addTypes(array $types): static; /** * Adds a form type extension to the factory. * * @return $this */ - public function addTypeExtension(FormTypeExtensionInterface $typeExtension); + public function addTypeExtension(FormTypeExtensionInterface $typeExtension): static; /** * Adds a list of form type extensions to the factory. @@ -71,14 +71,14 @@ public function addTypeExtension(FormTypeExtensionInterface $typeExtension); * * @return $this */ - public function addTypeExtensions(array $typeExtensions); + public function addTypeExtensions(array $typeExtensions): static; /** * Adds a type guesser to the factory. * * @return $this */ - public function addTypeGuesser(FormTypeGuesserInterface $typeGuesser); + public function addTypeGuesser(FormTypeGuesserInterface $typeGuesser): static; /** * Adds a list of type guessers to the factory. @@ -87,12 +87,10 @@ public function addTypeGuesser(FormTypeGuesserInterface $typeGuesser); * * @return $this */ - public function addTypeGuessers(array $typeGuessers); + public function addTypeGuessers(array $typeGuessers): static; /** * Builds and returns the factory. - * - * @return FormFactoryInterface */ - public function getFormFactory(); + public function getFormFactory(): FormFactoryInterface; } diff --git a/FormFactoryInterface.php b/FormFactoryInterface.php index c5f2485fd5..0f311c0e57 100644 --- a/FormFactoryInterface.php +++ b/FormFactoryInterface.php @@ -28,11 +28,9 @@ interface FormFactoryInterface * * @param mixed $data The initial data * - * @return FormInterface - * * @throws InvalidOptionsException if any given option is not applicable to the given type */ - public function create(string $type = FormType::class, $data = null, array $options = []); + public function create(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface; /** * Returns a form. @@ -41,11 +39,9 @@ public function create(string $type = FormType::class, $data = null, array $opti * * @param mixed $data The initial data * - * @return FormInterface - * * @throws InvalidOptionsException if any given option is not applicable to the given type */ - public function createNamed(string $name, string $type = FormType::class, $data = null, array $options = []); + public function createNamed(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormInterface; /** * Returns a form for a property of a class. @@ -56,33 +52,27 @@ public function createNamed(string $name, string $type = FormType::class, $data * @param string $property The name of the property to guess for * @param mixed $data The initial data * - * @return FormInterface - * * @throws InvalidOptionsException if any given option is not applicable to the form type */ - public function createForProperty(string $class, string $property, $data = null, array $options = []); + public function createForProperty(string $class, string $property, mixed $data = null, array $options = []): FormInterface; /** * Returns a form builder. * * @param mixed $data The initial data * - * @return FormBuilderInterface - * * @throws InvalidOptionsException if any given option is not applicable to the given type */ - public function createBuilder(string $type = FormType::class, $data = null, array $options = []); + public function createBuilder(string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface; /** * Returns a form builder. * * @param mixed $data The initial data * - * @return FormBuilderInterface - * * @throws InvalidOptionsException if any given option is not applicable to the given type */ - public function createNamedBuilder(string $name, string $type = FormType::class, $data = null, array $options = []); + public function createNamedBuilder(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface; /** * Returns a form builder for a property of a class. @@ -94,9 +84,7 @@ public function createNamedBuilder(string $name, string $type = FormType::class, * @param string $property The name of the property to guess for * @param mixed $data The initial data * - * @return FormBuilderInterface - * * @throws InvalidOptionsException if any given option is not applicable to the form type */ - public function createBuilderForProperty(string $class, string $property, $data = null, array $options = []); + public function createBuilderForProperty(string $class, string $property, mixed $data = null, array $options = []): FormBuilderInterface; } diff --git a/FormInterface.php b/FormInterface.php index 7ecc9b612a..23392c4931 100644 --- a/FormInterface.php +++ b/FormInterface.php @@ -34,14 +34,12 @@ interface FormInterface extends \ArrayAccess, \Traversable, \Countable * @throws Exception\LogicException when trying to set a parent for a form with * an empty name */ - public function setParent(?self $parent = null); + public function setParent(?self $parent): static; /** * Returns the parent form. - * - * @return self|null */ - public function getParent(); + public function getParent(): ?self; /** * Adds or replaces a child to the form. @@ -56,23 +54,19 @@ public function getParent(); * @throws Exception\LogicException when trying to add a child to a non-compound form * @throws Exception\UnexpectedTypeException if $child or $type has an unexpected type */ - public function add($child, ?string $type = null, array $options = []); + public function add(self|string $child, ?string $type = null, array $options = []): static; /** * Returns the child with the given name. * - * @return self - * * @throws Exception\OutOfBoundsException if the named child does not exist */ - public function get(string $name); + public function get(string $name): self; /** * Returns whether a child with the given name exists. - * - * @return bool */ - public function has(string $name); + public function has(string $name): bool; /** * Removes a child from the form. @@ -81,14 +75,14 @@ public function has(string $name); * * @throws Exception\AlreadySubmittedException if the form has already been submitted */ - public function remove(string $name); + public function remove(string $name): static; /** * Returns all children in this group. * * @return self[] */ - public function all(); + public function all(): array; /** * Returns the errors of this form. @@ -96,10 +90,8 @@ public function all(); * @param bool $deep Whether to include errors of child forms as well * @param bool $flatten Whether to flatten the list of errors in case * $deep is set to true - * - * @return FormErrorIterator */ - public function getErrors(bool $deep = false, bool $flatten = true); + public function getErrors(bool $deep = false, bool $flatten = true): FormErrorIterator; /** * Updates the form with default model data. @@ -115,7 +107,7 @@ public function getErrors(bool $deep = false, bool $flatten = true); * the form inherits data from its parent * @throws Exception\TransformationFailedException if the synchronization failed */ - public function setData($modelData); + public function setData(mixed $modelData): static; /** * Returns the model data in the format needed for the underlying object. @@ -126,7 +118,7 @@ public function setData($modelData); * * @throws Exception\RuntimeException If the form inherits data but has no parent */ - public function getData(); + public function getData(): mixed; /** * Returns the normalized data of the field, used as internal bridge @@ -139,7 +131,7 @@ public function getData(); * * @throws Exception\RuntimeException If the form inherits data but has no parent */ - public function getNormData(); + public function getNormData(): mixed; /** * Returns the view data of the field. @@ -158,64 +150,52 @@ public function getNormData(); * * In both cases the view data is the actual altered data on submission. * - * @return mixed - * * @throws Exception\RuntimeException If the form inherits data but has no parent */ - public function getViewData(); + public function getViewData(): mixed; /** * Returns the extra submitted data. * * @return array The submitted data which do not belong to a child */ - public function getExtraData(); + public function getExtraData(): array; /** * Returns the form's configuration. - * - * @return FormConfigInterface */ - public function getConfig(); + public function getConfig(): FormConfigInterface; /** * Returns whether the form is submitted. - * - * @return bool */ - public function isSubmitted(); + public function isSubmitted(): bool; /** * Returns the name by which the form is identified in forms. * * Only root forms are allowed to have an empty name. - * - * @return string */ - public function getName(); + public function getName(): string; /** * Returns the property path that the form is mapped to. - * - * @return PropertyPathInterface|null */ - public function getPropertyPath(); + public function getPropertyPath(): ?PropertyPathInterface; /** * Adds an error to this form. * * @return $this */ - public function addError(FormError $error); + public function addError(FormError $error): static; /** * Returns whether the form and all children are valid. * - * @return bool - * * @throws Exception\LogicException if the form is not submitted */ - public function isValid(); + public function isValid(): bool; /** * Returns whether the form is required to be filled out. @@ -223,10 +203,8 @@ public function isValid(); * If the form has a parent and the parent is not required, this method * will always return false. Otherwise the value set with setRequired() * is returned. - * - * @return bool */ - public function isRequired(); + public function isRequired(): bool; /** * Returns whether this form is disabled. @@ -236,17 +214,13 @@ public function isRequired(); * * Forms whose parents are disabled are considered disabled regardless of * their own state. - * - * @return bool */ - public function isDisabled(); + public function isDisabled(): bool; /** * Returns whether the form is empty. - * - * @return bool */ - public function isEmpty(); + public function isEmpty(): bool; /** * Returns whether the data in the different formats is synchronized. @@ -255,17 +229,13 @@ public function isEmpty(); * by calling {@link getTransformationFailure()}. * * If the form is not submitted, this method always returns true. - * - * @return bool */ - public function isSynchronized(); + public function isSynchronized(): bool; /** * Returns the data transformation failure, if any, during submission. - * - * @return Exception\TransformationFailedException|null */ - public function getTransformationFailure(); + public function getTransformationFailure(): ?Exception\TransformationFailedException; /** * Initializes the form tree. @@ -276,7 +246,7 @@ public function getTransformationFailure(); * * @throws Exception\RuntimeException If the form is not the root */ - public function initialize(); + public function initialize(): static; /** * Inspects the given request and calls {@link submit()} if the form was @@ -286,11 +256,9 @@ public function initialize(); * {@link RequestHandlerInterface} instance, which determines whether to * submit the form or not. * - * @param mixed $request The request to handle - * * @return $this */ - public function handleRequest($request = null); + public function handleRequest(mixed $request = null): static; /** * Submits data to the form. @@ -305,24 +273,17 @@ public function handleRequest($request = null); * * @throws Exception\AlreadySubmittedException if the form has already been submitted */ - public function submit($submittedData, bool $clearMissing = true); + public function submit(string|array|null $submittedData, bool $clearMissing = true): static; /** * Returns the root of the form tree. - * - * @return self */ - public function getRoot(); + public function getRoot(): self; /** * Returns whether the field is the root of the form tree. - * - * @return bool */ - public function isRoot(); + public function isRoot(): bool; - /** - * @return FormView - */ - public function createView(?FormView $parent = null); + public function createView(?FormView $parent = null): FormView; } diff --git a/FormRegistry.php b/FormRegistry.php index 90087a6a8d..ecf654a2a3 100644 --- a/FormRegistry.php +++ b/FormRegistry.php @@ -26,32 +26,25 @@ class FormRegistry implements FormRegistryInterface /** * @var FormExtensionInterface[] */ - private $extensions = []; + private array $extensions = []; /** * @var ResolvedFormTypeInterface[] */ - private $types = []; + private array $types = []; - /** - * @var FormTypeGuesserInterface|false|null - */ - private $guesser = false; - - /** - * @var ResolvedFormTypeFactoryInterface - */ - private $resolvedTypeFactory; - - private $checkedTypes = []; + private FormTypeGuesserInterface|false|null $guesser = false; + private array $checkedTypes = []; /** * @param FormExtensionInterface[] $extensions * * @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); @@ -59,13 +52,9 @@ public function __construct(array $extensions, ResolvedFormTypeFactoryInterface } $this->extensions = $extensions; - $this->resolvedTypeFactory = $resolvedTypeFactory; } - /** - * {@inheritdoc} - */ - public function getType(string $name) + public function getType(string $name): ResolvedFormTypeInterface { if (!isset($this->types[$name])) { $type = null; @@ -80,10 +69,10 @@ public function getType(string $name) 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(); @@ -101,11 +90,11 @@ public function getType(string $name) private function resolveType(FormTypeInterface $type): ResolvedFormTypeInterface { $parentType = $type->getParent(); - $fqcn = \get_class($type); + $fqcn = $type::class; 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; @@ -126,10 +115,7 @@ private function resolveType(FormTypeInterface $type): ResolvedFormTypeInterface } } - /** - * {@inheritdoc} - */ - public function hasType(string $name) + public function hasType(string $name): bool { if (isset($this->types[$name])) { return true; @@ -137,17 +123,14 @@ public function hasType(string $name) try { $this->getType($name); - } catch (ExceptionInterface $e) { + } catch (ExceptionInterface) { return false; } return true; } - /** - * {@inheritdoc} - */ - public function getTypeGuesser() + public function getTypeGuesser(): ?FormTypeGuesserInterface { if (false === $this->guesser) { $guessers = []; @@ -160,16 +143,13 @@ public function getTypeGuesser() } } - $this->guesser = !empty($guessers) ? new FormTypeGuesserChain($guessers) : null; + $this->guesser = $guessers ? new FormTypeGuesserChain($guessers) : null; } return $this->guesser; } - /** - * {@inheritdoc} - */ - public function getExtensions() + public function getExtensions(): array { return $this->extensions; } diff --git a/FormRegistryInterface.php b/FormRegistryInterface.php index f39174b19f..5c76b5c678 100644 --- a/FormRegistryInterface.php +++ b/FormRegistryInterface.php @@ -21,32 +21,26 @@ interface FormRegistryInterface /** * Returns a form type by name. * - * This methods registers the type extensions from the form extensions. - * - * @return ResolvedFormTypeInterface + * This method registers the type extensions from the form extensions. * * @throws Exception\InvalidArgumentException if the type cannot be retrieved from any extension */ - public function getType(string $name); + public function getType(string $name): ResolvedFormTypeInterface; /** * Returns whether the given form type is supported. - * - * @return bool */ - public function hasType(string $name); + public function hasType(string $name): bool; /** * Returns the guesser responsible for guessing types. - * - * @return FormTypeGuesserInterface|null */ - public function getTypeGuesser(); + public function getTypeGuesser(): ?FormTypeGuesserInterface; /** * Returns the extensions loaded by the framework. * * @return FormExtensionInterface[] */ - public function getExtensions(); + public function getExtensions(): array; } diff --git a/FormRenderer.php b/FormRenderer.php index 0be517e4f7..4478432b26 100644 --- a/FormRenderer.php +++ b/FormRenderer.php @@ -25,38 +25,27 @@ class FormRenderer implements FormRendererInterface { public const CACHE_KEY_VAR = 'unique_block_prefix'; - private $engine; - private $csrfTokenManager; - private $blockNameHierarchyMap = []; - private $hierarchyLevelMap = []; - private $variableStack = []; - - public function __construct(FormRendererEngineInterface $engine, ?CsrfTokenManagerInterface $csrfTokenManager = null) - { - $this->engine = $engine; - $this->csrfTokenManager = $csrfTokenManager; + private array $blockNameHierarchyMap = []; + private array $hierarchyLevelMap = []; + private array $variableStack = []; + + public function __construct( + private FormRendererEngineInterface $engine, + private ?CsrfTokenManagerInterface $csrfTokenManager = null, + ) { } - /** - * {@inheritdoc} - */ - public function getEngine() + public function getEngine(): FormRendererEngineInterface { return $this->engine; } - /** - * {@inheritdoc} - */ - public function setTheme(FormView $view, $themes, bool $useDefaultThemes = true) + public function setTheme(FormView $view, mixed $themes, bool $useDefaultThemes = true): void { $this->engine->setTheme($view, $themes, $useDefaultThemes); } - /** - * {@inheritdoc} - */ - public function renderCsrfToken(string $tokenId) + public function renderCsrfToken(string $tokenId): string { if (null === $this->csrfTokenManager) { throw new BadMethodCallException('CSRF tokens can only be generated if a CsrfTokenManagerInterface is injected in FormRenderer::__construct(). Try running "composer require symfony/security-csrf".'); @@ -65,15 +54,12 @@ public function renderCsrfToken(string $tokenId) return $this->csrfTokenManager->getToken($tokenId)->getValue(); } - /** - * {@inheritdoc} - */ - public function renderBlock(FormView $view, string $blockName, array $variables = []) + public function renderBlock(FormView $view, string $blockName, array $variables = []): string { $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]; @@ -124,16 +110,13 @@ public function renderBlock(FormView $view, string $blockName, array $variables return $html; } - /** - * {@inheritdoc} - */ - public function searchAndRenderBlock(FormView $view, string $blockNameSuffix, array $variables = []) + public function searchAndRenderBlock(FormView $view, string $blockNameSuffix, array $variables = []): string { $renderOnlyOnce = 'row' === $blockNameSuffix || 'widget' === $blockNameSuffix; 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 @@ -220,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 @@ -277,10 +260,7 @@ public function searchAndRenderBlock(FormView $view, string $blockNameSuffix, ar return $html; } - /** - * {@inheritdoc} - */ - public function humanize(string $text) + public function humanize(string $text): string { return ucfirst(strtolower(trim(preg_replace(['/([A-Z])/', '/[_\s]+/'], ['_$1', ' '], $text)))); } diff --git a/FormRendererEngineInterface.php b/FormRendererEngineInterface.php index 67b88c90ce..e3c4ba899a 100644 --- a/FormRendererEngineInterface.php +++ b/FormRendererEngineInterface.php @@ -25,7 +25,7 @@ interface FormRendererEngineInterface * @param mixed $themes The theme(s). The type of these themes * is open to the implementation. */ - public function setTheme(FormView $view, $themes, bool $useDefaultThemes = true); + public function setTheme(FormView $view, mixed $themes, bool $useDefaultThemes = true): void; /** * Returns the resource for a block name. @@ -43,7 +43,7 @@ public function setTheme(FormView $view, $themes, bool $useDefaultThemes = true) * * @return mixed the renderer resource or false, if none was found */ - public function getResourceForBlockName(FormView $view, string $blockName); + public function getResourceForBlockName(FormView $view, string $blockName): mixed; /** * Returns the resource for a block hierarchy. @@ -79,7 +79,7 @@ public function getResourceForBlockName(FormView $view, string $blockName); * * @return mixed The renderer resource or false, if none was found */ - public function getResourceForBlockNameHierarchy(FormView $view, array $blockNameHierarchy, int $hierarchyLevel); + public function getResourceForBlockNameHierarchy(FormView $view, array $blockNameHierarchy, int $hierarchyLevel): mixed; /** * Returns the hierarchy level at which a resource can be found. @@ -114,10 +114,8 @@ public function getResourceForBlockNameHierarchy(FormView $view, array $blockNam * @param int $hierarchyLevel The level in the hierarchy at which to start * looking. Level 0 indicates the root block, i.e. * the first element of $blockNameHierarchy. - * - * @return int|false */ - public function getResourceHierarchyLevel(FormView $view, array $blockNameHierarchy, int $hierarchyLevel); + public function getResourceHierarchyLevel(FormView $view, array $blockNameHierarchy, int $hierarchyLevel): int|false; /** * Renders a block in the given renderer resource. @@ -129,8 +127,6 @@ public function getResourceHierarchyLevel(FormView $view, array $blockNameHierar * @param FormView $view The view to render * @param mixed $resource The renderer resource * @param array $variables The variables to pass to the template - * - * @return string */ - public function renderBlock(FormView $view, $resource, string $blockName, array $variables = []); + public function renderBlock(FormView $view, mixed $resource, string $blockName, array $variables = []): string; } diff --git a/FormRendererInterface.php b/FormRendererInterface.php index 04af58baf9..d57a904b3b 100644 --- a/FormRendererInterface.php +++ b/FormRendererInterface.php @@ -20,10 +20,8 @@ interface FormRendererInterface { /** * Returns the engine used by this renderer. - * - * @return FormRendererEngineInterface */ - public function getEngine(); + public function getEngine(): FormRendererEngineInterface; /** * Sets the theme(s) to be used for rendering a view and its children. @@ -34,17 +32,15 @@ public function getEngine(); * @param bool $useDefaultThemes If true, will use default themes specified * in the renderer */ - public function setTheme(FormView $view, $themes, bool $useDefaultThemes = true); + public function setTheme(FormView $view, mixed $themes, bool $useDefaultThemes = true): void; /** * Renders a named block of the form theme. * * @param FormView $view The view for which to render the block * @param array $variables The variables to pass to the template - * - * @return string */ - public function renderBlock(FormView $view, string $blockName, array $variables = []); + public function renderBlock(FormView $view, string $blockName, array $variables = []): string; /** * Searches and renders a block for a given name suffix. @@ -58,10 +54,8 @@ public function renderBlock(FormView $view, string $blockName, array $variables * * @param FormView $view The view for which to render the block * @param array $variables The variables to pass to the template - * - * @return string */ - public function searchAndRenderBlock(FormView $view, string $blockNameSuffix, array $variables = []); + public function searchAndRenderBlock(FormView $view, string $blockNameSuffix, array $variables = []): string; /** * Renders a CSRF token. @@ -77,10 +71,8 @@ public function searchAndRenderBlock(FormView $view, string $blockNameSuffix, ar * if (!$csrfProvider->isCsrfTokenValid('rm_user_'.$user->getId(), $token)) { * throw new \RuntimeException('CSRF attack detected.'); * } - * - * @return string */ - public function renderCsrfToken(string $tokenId); + public function renderCsrfToken(string $tokenId): string; /** * Makes a technical name human readable. @@ -88,8 +80,6 @@ public function renderCsrfToken(string $tokenId); * Sequences of underscores are replaced by single spaces. The first letter * of the resulting string is capitalized, while all other letters are * turned to lowercase. - * - * @return string */ - public function humanize(string $text); + public function humanize(string $text): string; } diff --git a/FormTypeExtensionInterface.php b/FormTypeExtensionInterface.php index 6810f0ae91..838406d2af 100644 --- a/FormTypeExtensionInterface.php +++ b/FormTypeExtensionInterface.php @@ -18,15 +18,26 @@ */ interface FormTypeExtensionInterface { + /** + * Gets the extended types. + * + * @return string[] + */ + public static function getExtendedTypes(): iterable; + + public function configureOptions(OptionsResolver $resolver): void; + /** * Builds the form. * * This method is called after the extended type has built the form to * further modify it. * + * @param array $options + * * @see FormTypeInterface::buildForm() */ - public function buildForm(FormBuilderInterface $builder, array $options); + public function buildForm(FormBuilderInterface $builder, array $options): void; /** * Builds the view. @@ -34,9 +45,11 @@ public function buildForm(FormBuilderInterface $builder, array $options); * This method is called after the extended type has built the view to * further modify it. * + * @param array $options + * * @see FormTypeInterface::buildView() */ - public function buildView(FormView $view, FormInterface $form, array $options); + public function buildView(FormView $view, FormInterface $form, array $options): void; /** * Finishes the view. @@ -44,16 +57,9 @@ public function buildView(FormView $view, FormInterface $form, array $options); * This method is called after the extended type has finished the view to * further modify it. * - * @see FormTypeInterface::finishView() - */ - public function finishView(FormView $view, FormInterface $form, array $options); - - public function configureOptions(OptionsResolver $resolver); - - /** - * Gets the extended types. + * @param array $options * - * @return string[] + * @see FormTypeInterface::finishView() */ - public static function getExtendedTypes(): iterable; + public function finishView(FormView $view, FormInterface $form, array $options): void; } diff --git a/FormTypeGuesserChain.php b/FormTypeGuesserChain.php index 2763d90810..ed94ece6e9 100644 --- a/FormTypeGuesserChain.php +++ b/FormTypeGuesserChain.php @@ -13,10 +13,12 @@ use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\Guess\Guess; +use Symfony\Component\Form\Guess\TypeGuess; +use Symfony\Component\Form\Guess\ValueGuess; class FormTypeGuesserChain implements FormTypeGuesserInterface { - private $guessers = []; + private array $guessers = []; /** * @param FormTypeGuesserInterface[] $guessers @@ -41,44 +43,24 @@ public function __construct(iterable $guessers) $this->guessers = array_merge([], ...$tmpGuessers); } - /** - * {@inheritdoc} - */ - public function guessType(string $class, string $property) + public function guessType(string $class, string $property): ?TypeGuess { - return $this->guess(function ($guesser) use ($class, $property) { - return $guesser->guessType($class, $property); - }); + return $this->guess(static fn ($guesser) => $guesser->guessType($class, $property)); } - /** - * {@inheritdoc} - */ - public function guessRequired(string $class, string $property) + public function guessRequired(string $class, string $property): ?ValueGuess { - return $this->guess(function ($guesser) use ($class, $property) { - return $guesser->guessRequired($class, $property); - }); + return $this->guess(static fn ($guesser) => $guesser->guessRequired($class, $property)); } - /** - * {@inheritdoc} - */ - public function guessMaxLength(string $class, string $property) + public function guessMaxLength(string $class, string $property): ?ValueGuess { - return $this->guess(function ($guesser) use ($class, $property) { - return $guesser->guessMaxLength($class, $property); - }); + return $this->guess(static fn ($guesser) => $guesser->guessMaxLength($class, $property)); } - /** - * {@inheritdoc} - */ - public function guessPattern(string $class, string $property) + public function guessPattern(string $class, string $property): ?ValueGuess { - return $this->guess(function ($guesser) use ($class, $property) { - return $guesser->guessPattern($class, $property); - }); + return $this->guess(static fn ($guesser) => $guesser->guessPattern($class, $property)); } /** diff --git a/FormTypeGuesserInterface.php b/FormTypeGuesserInterface.php index 54414b9f69..ab43793b2f 100644 --- a/FormTypeGuesserInterface.php +++ b/FormTypeGuesserInterface.php @@ -18,29 +18,21 @@ interface FormTypeGuesserInterface { /** * Returns a field guess for a property name of a class. - * - * @return Guess\TypeGuess|null */ - public function guessType(string $class, string $property); + public function guessType(string $class, string $property): ?Guess\TypeGuess; /** * Returns a guess whether a property of a class is required. - * - * @return Guess\ValueGuess|null */ - public function guessRequired(string $class, string $property); + public function guessRequired(string $class, string $property): ?Guess\ValueGuess; /** * Returns a guess about the field's maximum length. - * - * @return Guess\ValueGuess|null */ - public function guessMaxLength(string $class, string $property); + public function guessMaxLength(string $class, string $property): ?Guess\ValueGuess; /** * Returns a guess about the field's pattern. - * - * @return Guess\ValueGuess|null */ - public function guessPattern(string $class, string $property); + public function guessPattern(string $class, string $property): ?Guess\ValueGuess; } diff --git a/FormTypeInterface.php b/FormTypeInterface.php index 2b9066a511..2bc9f7711e 100644 --- a/FormTypeInterface.php +++ b/FormTypeInterface.php @@ -18,6 +18,23 @@ */ interface FormTypeInterface { + /** + * Returns the name of the parent type. + * + * The parent type and its extensions will configure the form with the + * following methods before the current implementation. + * + * @return string|null + */ + public function getParent(); + + /** + * Configures the options for this type. + * + * @return void + */ + public function configureOptions(OptionsResolver $resolver); + /** * Builds the form. * @@ -26,6 +43,8 @@ interface FormTypeInterface * * @param array $options * + * @return void + * * @see FormTypeExtensionInterface::buildForm() */ public function buildForm(FormBuilderInterface $builder, array $options); @@ -42,6 +61,8 @@ public function buildForm(FormBuilderInterface $builder, array $options); * * @param array $options * + * @return void + * * @see FormTypeExtensionInterface::buildView() */ public function buildView(FormView $view, FormInterface $form, array $options); @@ -59,15 +80,12 @@ public function buildView(FormView $view, FormInterface $form, array $options); * * @param array $options * + * @return void + * * @see FormTypeExtensionInterface::finishView() */ public function finishView(FormView $view, FormInterface $form, array $options); - /** - * Configures the options for this type. - */ - public function configureOptions(OptionsResolver $resolver); - /** * Returns the prefix of the template block name for this type. * @@ -77,11 +95,4 @@ public function configureOptions(OptionsResolver $resolver); * @return string */ public function getBlockPrefix(); - - /** - * Returns the name of the parent type. - * - * @return string|null - */ - public function getParent(); } diff --git a/FormView.php b/FormView.php index 49ab5b9216..93804bb879 100644 --- a/FormView.php +++ b/FormView.php @@ -24,22 +24,17 @@ class FormView implements \ArrayAccess, \IteratorAggregate, \Countable /** * The variables assigned to this view. */ - public $vars = [ + public array $vars = [ 'value' => null, 'attr' => [], ]; - /** - * The parent view. - */ - public $parent; - /** * The child views. * * @var array */ - public $children = []; + public array $children = []; /** * Is the form attached to this renderer rendered? @@ -47,24 +42,23 @@ class FormView implements \ArrayAccess, \IteratorAggregate, \Countable * Rendering happens when either the widget or the row method was called. * Row implicitly includes widget, however certain rendering mechanisms * have to skip widget rendering when a row is rendered. - * - * @var bool */ - private $rendered = false; + private bool $rendered = false; - private $methodRendered = false; + 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, + ) { } /** * Returns whether the view was already rendered. - * - * @return bool */ - public function isRendered() + public function isRendered(): bool { if (true === $this->rendered || 0 === \count($this->children)) { return $this->rendered; @@ -84,22 +78,19 @@ public function isRendered() * * @return $this */ - public function setRendered() + public function setRendered(): static { $this->rendered = true; return $this; } - /** - * @return bool - */ - public function isMethodRendered() + public function isMethodRendered(): bool { return $this->methodRendered; } - public function setMethodRendered() + public function setMethodRendered(): void { $this->methodRendered = true; } @@ -108,11 +99,8 @@ public function setMethodRendered() * Returns a child by name (implements \ArrayAccess). * * @param int|string $name The child name - * - * @return self */ - #[\ReturnTypeWillChange] - public function offsetGet($name) + public function offsetGet(mixed $name): self { return $this->children[$name]; } @@ -121,11 +109,8 @@ public function offsetGet($name) * Returns whether the given child exists (implements \ArrayAccess). * * @param int|string $name The child name - * - * @return bool */ - #[\ReturnTypeWillChange] - public function offsetExists($name) + public function offsetExists(mixed $name): bool { return isset($this->children[$name]); } @@ -133,12 +118,9 @@ public function offsetExists($name) /** * Implements \ArrayAccess. * - * @return void - * * @throws BadMethodCallException always as setting a child by name is not allowed */ - #[\ReturnTypeWillChange] - public function offsetSet($name, $value) + public function offsetSet(mixed $name, mixed $value): void { throw new BadMethodCallException('Not supported.'); } @@ -147,11 +129,8 @@ public function offsetSet($name, $value) * Removes a child (implements \ArrayAccess). * * @param int|string $name The child name - * - * @return void */ - #[\ReturnTypeWillChange] - public function offsetUnset($name) + public function offsetUnset(mixed $name): void { unset($this->children[$name]); } @@ -161,19 +140,12 @@ public function offsetUnset($name) * * @return \ArrayIterator */ - #[\ReturnTypeWillChange] - public function getIterator() + public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->children); } - /** - * Implements \Countable. - * - * @return int - */ - #[\ReturnTypeWillChange] - public function count() + public function count(): int { return \count($this->children); } diff --git a/Guess/Guess.php b/Guess/Guess.php index 935bbfea1c..fc19ed9cee 100644 --- a/Guess/Guess.php +++ b/Guess/Guess.php @@ -49,10 +49,8 @@ abstract class Guess * * One of VERY_HIGH_CONFIDENCE, HIGH_CONFIDENCE, MEDIUM_CONFIDENCE * and LOW_CONFIDENCE. - * - * @var int */ - private $confidence; + private int $confidence; /** * Returns the guess most likely to be correct from a list of guesses. @@ -61,10 +59,8 @@ abstract class Guess * returned guess is any of them. * * @param static[] $guesses An array of guesses - * - * @return static|null */ - public static function getBestGuess(array $guesses) + public static function getBestGuess(array $guesses): ?static { $result = null; $maxConfidence = -1; @@ -84,8 +80,8 @@ public static function getBestGuess(array $guesses) */ public function __construct(int $confidence) { - if (self::VERY_HIGH_CONFIDENCE !== $confidence && self::HIGH_CONFIDENCE !== $confidence && - self::MEDIUM_CONFIDENCE !== $confidence && self::LOW_CONFIDENCE !== $confidence) { + if (self::VERY_HIGH_CONFIDENCE !== $confidence && self::HIGH_CONFIDENCE !== $confidence + && self::MEDIUM_CONFIDENCE !== $confidence && self::LOW_CONFIDENCE !== $confidence) { throw new InvalidArgumentException('The confidence should be one of the constants defined in Guess.'); } @@ -98,7 +94,7 @@ public function __construct(int $confidence) * @return int One of the constants VERY_HIGH_CONFIDENCE, HIGH_CONFIDENCE, * MEDIUM_CONFIDENCE and LOW_CONFIDENCE */ - public function getConfidence() + public function getConfidence(): int { return $this->confidence; } diff --git a/Guess/TypeGuess.php b/Guess/TypeGuess.php index ff0c6a7498..b62873daa4 100644 --- a/Guess/TypeGuess.php +++ b/Guess/TypeGuess.php @@ -19,9 +19,6 @@ */ class TypeGuess extends Guess { - private $type; - private $options; - /** * @param string $type The guessed field type * @param array $options The options for creating instances of the @@ -29,30 +26,26 @@ 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; } /** * Returns the guessed field type. - * - * @return string */ - public function getType() + public function getType(): string { return $this->type; } /** * Returns the guessed options for creating instances of the guessed type. - * - * @return array */ - public function getOptions() + public function getOptions(): array { return $this->options; } diff --git a/Guess/ValueGuess.php b/Guess/ValueGuess.php index fe19dfeb04..2283287472 100644 --- a/Guess/ValueGuess.php +++ b/Guess/ValueGuess.php @@ -18,26 +18,20 @@ */ class ValueGuess extends Guess { - private $value; - /** - * @param string|int|bool|null $value The guessed value - * @param int $confidence The confidence that the guessed class name - * is correct + * @param int $confidence The confidence that the guessed class name is correct */ - public function __construct($value, int $confidence) - { + public function __construct( + private string|int|bool|null $value, + int $confidence, + ) { parent::__construct($confidence); - - $this->value = $value; } /** * Returns the guessed value. - * - * @return string|int|bool|null */ - public function getValue() + public function getValue(): string|int|bool|null { return $this->value; } diff --git a/NativeRequestHandler.php b/NativeRequestHandler.php index d3e3404275..bee54fa60d 100644 --- a/NativeRequestHandler.php +++ b/NativeRequestHandler.php @@ -22,13 +22,14 @@ */ class NativeRequestHandler implements RequestHandlerInterface { - private $serverParams; + private ServerParams $serverParams; /** * The allowed keys of the $_FILES array. */ private const FILE_KEYS = [ 'error', + 'full_path', 'name', 'size', 'tmp_name', @@ -41,11 +42,9 @@ public function __construct(?ServerParams $params = null) } /** - * {@inheritdoc} - * - * @throws Exception\UnexpectedTypeException If the $request is not null + * @throws UnexpectedTypeException If the $request is not null */ - public function handleRequest(FormInterface $form, $request = null) + public function handleRequest(FormInterface $form, mixed $request = null): void { if (null !== $request) { throw new UnexpectedTypeException($request, 'null'); @@ -125,10 +124,7 @@ public function handleRequest(FormInterface $form, $request = null) $form->submit($data, 'PATCH' !== $method); } - /** - * {@inheritdoc} - */ - public function isFileUpload($data) + public function isFileUpload(mixed $data): bool { // POST data will always be strings or arrays of strings. Thus, we can be sure // that the submitted data is a file upload if the "error" value is an integer @@ -136,10 +132,7 @@ public function isFileUpload($data) return \is_array($data) && isset($data['error']) && \is_int($data['error']); } - /** - * @return int|null - */ - public function getUploadFileError($data) + public function getUploadFileError(mixed $data): ?int { if (!\is_array($data)) { return null; @@ -160,9 +153,6 @@ public function getUploadFileError($data) return $data['error']; } - /** - * Returns the method used to submit the request to the server. - */ private static function getRequestMethod(): string { $method = isset($_SERVER['REQUEST_METHOD']) @@ -190,18 +180,14 @@ private static function getRequestMethod(): string * * This method is identical to {@link \Symfony\Component\HttpFoundation\FileBag::fixPhpFilesArray} * and should be kept as such in order to port fixes quickly and easily. - * - * @return mixed */ - private static function fixPhpFilesArray($data) + private static function fixPhpFilesArray(mixed $data): mixed { if (!\is_array($data)) { 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'])) { @@ -220,24 +206,21 @@ private static function fixPhpFilesArray($data) '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; } - /** - * Sets empty uploaded files to NULL in the given uploaded files array. - * - * @return mixed - */ - private static function stripEmptyFiles($data) + private static function stripEmptyFiles(mixed $data): mixed { if (!\is_array($data)) { 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 1e8dd085bb..26090e00df 100644 --- a/PreloadedExtension.php +++ b/PreloadedExtension.php @@ -20,9 +20,7 @@ */ class PreloadedExtension implements FormExtensionInterface { - private $types = []; - private $typeExtensions = []; - private $typeGuesser; + private array $types = []; /** * Creates a new preloaded extension. @@ -30,57 +28,42 @@ 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[\get_class($type)] = $type; + $this->types[$type::class] = $type; } } - /** - * {@inheritdoc} - */ - public function getType(string $name) + 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]; } - /** - * {@inheritdoc} - */ - public function hasType(string $name) + public function hasType(string $name): bool { return isset($this->types[$name]); } - /** - * {@inheritdoc} - */ - public function getTypeExtensions(string $name) + public function getTypeExtensions(string $name): array { return $this->typeExtensions[$name] ?? []; } - /** - * {@inheritdoc} - */ - public function hasTypeExtensions(string $name) + public function hasTypeExtensions(string $name): bool { return !empty($this->typeExtensions[$name]); } - /** - * {@inheritdoc} - */ - public function getTypeGuesser() + public function getTypeGuesser(): ?FormTypeGuesserInterface { return $this->typeGuesser; } diff --git a/RequestHandlerInterface.php b/RequestHandlerInterface.php index 65d86e2246..2a4bccf717 100644 --- a/RequestHandlerInterface.php +++ b/RequestHandlerInterface.php @@ -20,17 +20,11 @@ interface RequestHandlerInterface { /** * Submits a form if it was submitted. - * - * @param mixed $request The current request */ - public function handleRequest(FormInterface $form, $request = null); + public function handleRequest(FormInterface $form, mixed $request = null): void; /** * Returns true if the given data is a file upload. - * - * @param mixed $data The form field data - * - * @return bool */ - public function isFileUpload($data); + public function isFileUpload(mixed $data): bool; } diff --git a/ResolvedFormType.php b/ResolvedFormType.php index d76ce9d6f2..82065f6511 100644 --- a/ResolvedFormType.php +++ b/ResolvedFormType.php @@ -23,83 +23,56 @@ */ class ResolvedFormType implements ResolvedFormTypeInterface { - /** - * @var FormTypeInterface - */ - private $innerType; - /** * @var FormTypeExtensionInterface[] */ - private $typeExtensions; + private array $typeExtensions; - /** - * @var ResolvedFormTypeInterface|null - */ - private $parent; - - /** - * @var OptionsResolver - */ - private $optionsResolver; + 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; } - /** - * {@inheritdoc} - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return $this->innerType->getBlockPrefix(); } - /** - * {@inheritdoc} - */ - public function getParent() + public function getParent(): ?ResolvedFormTypeInterface { return $this->parent; } - /** - * {@inheritdoc} - */ - public function getInnerType() + public function getInnerType(): FormTypeInterface { return $this->innerType; } - /** - * {@inheritdoc} - */ - public function getTypeExtensions() + public function getTypeExtensions(): array { return $this->typeExtensions; } - /** - * {@inheritdoc} - */ - public function createBuilder(FormFactoryInterface $factory, string $name, array $options = []) + public function createBuilder(FormFactoryInterface $factory, string $name, array $options = []): FormBuilderInterface { 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 @@ -111,22 +84,14 @@ public function createBuilder(FormFactoryInterface $factory, string $name, array return $builder; } - /** - * {@inheritdoc} - */ - public function createView(FormInterface $form, ?FormView $parent = null) + public function createView(FormInterface $form, ?FormView $parent = null): FormView { return $this->newView($parent); } - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { - if (null !== $this->parent) { - $this->parent->buildForm($builder, $options); - } + $this->parent?->buildForm($builder, $options); $this->innerType->buildForm($builder, $options); @@ -135,14 +100,9 @@ public function buildForm(FormBuilderInterface $builder, array $options) } } - /** - * {@inheritdoc} - */ - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { - if (null !== $this->parent) { - $this->parent->buildView($view, $form, $options); - } + $this->parent?->buildView($view, $form, $options); $this->innerType->buildView($view, $form, $options); @@ -151,14 +111,9 @@ public function buildView(FormView $view, FormInterface $form, array $options) } } - /** - * {@inheritdoc} - */ - public function finishView(FormView $view, FormInterface $form, array $options) + public function finishView(FormView $view, FormInterface $form, array $options): void { - if (null !== $this->parent) { - $this->parent->finishView($view, $form, $options); - } + $this->parent?->finishView($view, $form, $options); $this->innerType->finishView($view, $form, $options); @@ -168,12 +123,9 @@ public function finishView(FormView $view, FormInterface $form, array $options) } } - /** - * {@inheritdoc} - */ - public function getOptionsResolver() + public function getOptionsResolver(): OptionsResolver { - if (null === $this->optionsResolver) { + if (!isset($this->optionsResolver)) { if (null !== $this->parent) { $this->optionsResolver = clone $this->parent->getOptionsResolver(); } else { @@ -194,10 +146,8 @@ public function getOptionsResolver() * Creates a new builder instance. * * Override this method if you want to customize the builder class. - * - * @return FormBuilderInterface */ - protected function newBuilder(string $name, ?string $dataClass, FormFactoryInterface $factory, array $options) + protected function newBuilder(string $name, ?string $dataClass, FormFactoryInterface $factory, array $options): FormBuilderInterface { if ($this->innerType instanceof ButtonTypeInterface) { return new ButtonBuilder($name, $options); @@ -214,10 +164,8 @@ protected function newBuilder(string $name, ?string $dataClass, FormFactoryInter * Creates a new view instance. * * Override this method if you want to customize the view class. - * - * @return FormView */ - protected function newView(?FormView $parent = null) + protected function newView(?FormView $parent = null): FormView { return new FormView($parent); } diff --git a/ResolvedFormTypeFactory.php b/ResolvedFormTypeFactory.php index b20cde2a10..437f9c553c 100644 --- a/ResolvedFormTypeFactory.php +++ b/ResolvedFormTypeFactory.php @@ -16,10 +16,7 @@ */ class ResolvedFormTypeFactory implements ResolvedFormTypeFactoryInterface { - /** - * {@inheritdoc} - */ - public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ?ResolvedFormTypeInterface $parent = null) + public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ?ResolvedFormTypeInterface $parent = null): ResolvedFormTypeInterface { return new ResolvedFormType($type, $typeExtensions, $parent); } diff --git a/ResolvedFormTypeFactoryInterface.php b/ResolvedFormTypeFactoryInterface.php index 47d2eb2790..9fd39e7fe2 100644 --- a/ResolvedFormTypeFactoryInterface.php +++ b/ResolvedFormTypeFactoryInterface.php @@ -27,10 +27,8 @@ interface ResolvedFormTypeFactoryInterface * * @param FormTypeExtensionInterface[] $typeExtensions * - * @return ResolvedFormTypeInterface - * * @throws Exception\UnexpectedTypeException if the types parent {@link FormTypeInterface::getParent()} is not a string * @throws Exception\InvalidArgumentException if the types parent cannot be retrieved from any extension */ - public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ?ResolvedFormTypeInterface $parent = null); + public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ?ResolvedFormTypeInterface $parent = null): ResolvedFormTypeInterface; } diff --git a/ResolvedFormTypeInterface.php b/ResolvedFormTypeInterface.php index 4d0d674547..690e0d7834 100644 --- a/ResolvedFormTypeInterface.php +++ b/ResolvedFormTypeInterface.php @@ -22,71 +22,59 @@ interface ResolvedFormTypeInterface { /** * Returns the prefix of the template block name for this type. - * - * @return string */ - public function getBlockPrefix(); + public function getBlockPrefix(): string; /** * Returns the parent type. - * - * @return self|null */ - public function getParent(); + public function getParent(): ?self; /** * Returns the wrapped form type. - * - * @return FormTypeInterface */ - public function getInnerType(); + public function getInnerType(): FormTypeInterface; /** * Returns the extensions of the wrapped form type. * * @return FormTypeExtensionInterface[] */ - public function getTypeExtensions(); + public function getTypeExtensions(): array; /** * Creates a new form builder for this type. * * @param string $name The name for the builder - * - * @return FormBuilderInterface */ - public function createBuilder(FormFactoryInterface $factory, string $name, array $options = []); + public function createBuilder(FormFactoryInterface $factory, string $name, array $options = []): FormBuilderInterface; /** * Creates a new form view for a form of this type. - * - * @return FormView */ - public function createView(FormInterface $form, ?FormView $parent = null); + public function createView(FormInterface $form, ?FormView $parent = null): FormView; /** * Configures a form builder for the type hierarchy. */ - public function buildForm(FormBuilderInterface $builder, array $options); + public function buildForm(FormBuilderInterface $builder, array $options): void; /** * Configures a form view for the type hierarchy. * * It is called before the children of the view are built. */ - public function buildView(FormView $view, FormInterface $form, array $options); + public function buildView(FormView $view, FormInterface $form, array $options): void; /** * Finishes a form view for the type hierarchy. * * It is called after the children of the view have been built. */ - public function finishView(FormView $view, FormInterface $form, array $options); + public function finishView(FormView $view, FormInterface $form, array $options): void; /** * Returns the configured options resolver used for this type. - * - * @return OptionsResolver */ - public function getOptionsResolver(); + public function getOptionsResolver(): OptionsResolver; } diff --git a/Resources/translations/validators.es.xlf b/Resources/translations/validators.es.xlf index 301e2b33f7..a9989737c3 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 755108f357..673e79f420 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/ReversedTransformer.php b/ReversedTransformer.php index 8089e47bf0..4aa92450a2 100644 --- a/ReversedTransformer.php +++ b/ReversedTransformer.php @@ -21,25 +21,17 @@ */ class ReversedTransformer implements DataTransformerInterface { - protected $reversedTransformer; - - public function __construct(DataTransformerInterface $reversedTransformer) - { - $this->reversedTransformer = $reversedTransformer; + public function __construct( + protected DataTransformerInterface $reversedTransformer, + ) { } - /** - * {@inheritdoc} - */ - public function transform($value) + public function transform(mixed $value): mixed { return $this->reversedTransformer->reverseTransform($value); } - /** - * {@inheritdoc} - */ - public function reverseTransform($value) + public function reverseTransform(mixed $value): mixed { return $this->reversedTransformer->transform($value); } diff --git a/SubmitButton.php b/SubmitButton.php index 520f223e24..37ce141d27 100644 --- a/SubmitButton.php +++ b/SubmitButton.php @@ -18,12 +18,9 @@ */ class SubmitButton extends Button implements ClickableInterface { - private $clicked = false; + private bool $clicked = false; - /** - * {@inheritdoc} - */ - public function isClicked() + public function isClicked(): bool { return $this->clicked; } @@ -31,14 +28,11 @@ public function isClicked() /** * Submits data to the button. * - * @param array|string|null $submittedData The data - * @param bool $clearMissing Not used - * * @return $this * * @throws Exception\AlreadySubmittedException if the form has already been submitted */ - public function submit($submittedData, bool $clearMissing = true) + public function submit(array|string|null $submittedData, bool $clearMissing = true): static { if ($this->getConfig()->getDisabled()) { $this->clicked = false; diff --git a/SubmitButtonBuilder.php b/SubmitButtonBuilder.php index 3045e0dddd..b98398f90f 100644 --- a/SubmitButtonBuilder.php +++ b/SubmitButtonBuilder.php @@ -20,10 +20,8 @@ class SubmitButtonBuilder extends ButtonBuilder { /** * Creates the button. - * - * @return SubmitButton */ - public function getForm() + public function getForm(): SubmitButton { return new SubmitButton($this->getFormConfig()); } diff --git a/Test/FormIntegrationTestCase.php b/Test/FormIntegrationTestCase.php index 4feb8df5a6..8756d99689 100644 --- a/Test/FormIntegrationTestCase.php +++ b/Test/FormIntegrationTestCase.php @@ -12,18 +12,19 @@ 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 */ abstract class FormIntegrationTestCase extends TestCase { - /** - * @var FormFactoryInterface - */ - protected $factory; + protected FormFactoryInterface $factory; protected function setUp(): void { @@ -35,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 e4329150a2..9247f57e7a 100644 --- a/Test/FormPerformanceTestCase.php +++ b/Test/FormPerformanceTestCase.php @@ -11,9 +11,6 @@ namespace Symfony\Component\Form\Test; -use Symfony\Component\Form\Test\Traits\RunTestTrait; -use Symfony\Component\Form\Tests\VersionAwareTest; - /** * Base class for performance tests. * @@ -24,36 +21,33 @@ */ abstract class FormPerformanceTestCase extends FormIntegrationTestCase { - use RunTestTrait; - use VersionAwareTest; + private float $startTime; + protected int $maxRunningTime = 0; - /** - * @var int - */ - protected $maxRunningTime = 0; + protected function setUp(): void + { + parent::setUp(); - /** - * @return mixed - */ - private function doRunTest() + $this->startTime = microtime(true); + } + + protected function assertPostConditions(): void { - $s = microtime(true); - $result = parent::runTest(); - $time = microtime(true) - $s; + 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)); } $this->expectNotToPerformAssertions(); - - return $result; } /** * @throws \InvalidArgumentException */ - public function setMaxRunningTime(int $maxRunningTime) + public function setMaxRunningTime(int $maxRunningTime): void { if ($maxRunningTime < 0) { throw new \InvalidArgumentException(); @@ -62,10 +56,7 @@ public function setMaxRunningTime(int $maxRunningTime) $this->maxRunningTime = $maxRunningTime; } - /** - * @return int - */ - public function getMaxRunningTime() + public function getMaxRunningTime(): int { return $this->maxRunningTime; } diff --git a/Test/Traits/RunTestTrait.php b/Test/Traits/RunTestTrait.php deleted file mode 100644 index 17204b9670..0000000000 --- a/Test/Traits/RunTestTrait.php +++ /dev/null @@ -1,36 +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\Test\Traits; - -use PHPUnit\Framework\TestCase; - -if ((new \ReflectionMethod(TestCase::class, 'runTest'))->hasReturnType()) { - // PHPUnit 10 - /** @internal */ - trait RunTestTrait - { - protected function runTest(): mixed - { - return $this->doRunTest(); - } - } -} else { - // PHPUnit 9 - /** @internal */ - trait RunTestTrait - { - protected function runTest() - { - return $this->doRunTest(); - } - } -} diff --git a/Test/Traits/ValidatorExtensionTrait.php b/Test/Traits/ValidatorExtensionTrait.php index 70240fc3e4..5d0486e8cf 100644 --- a/Test/Traits/ValidatorExtensionTrait.php +++ b/Test/Traits/ValidatorExtensionTrait.php @@ -19,10 +19,7 @@ trait ValidatorExtensionTrait { - /** - * @var ValidatorInterface|null - */ - protected $validator; + protected ValidatorInterface $validator; protected function getValidatorExtension(): ValidatorExtension { @@ -31,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 a925c555ec..1bbb66d25d 100644 --- a/Test/TypeTestCase.php +++ b/Test/TypeTestCase.php @@ -13,19 +13,13 @@ 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 { - /** - * @var FormBuilder - */ - protected $builder; - - /** - * @var EventDispatcherInterface - */ - protected $dispatcher; + protected FormBuilder $builder; + protected EventDispatcherInterface $dispatcher; protected function setUp(): void { @@ -35,29 +29,31 @@ protected function setUp(): void $this->builder = new FormBuilder('', null, $this->dispatcher, $this->factory); } - protected function tearDown(): void - { - if (\in_array(ValidatorExtensionTrait::class, class_uses($this))) { - $this->validator = null; - } - } - + /** + * @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/AbstractDivLayoutTestCase.php b/Tests/AbstractDivLayoutTestCase.php deleted file mode 100644 index db6054ad85..0000000000 --- a/Tests/AbstractDivLayoutTestCase.php +++ /dev/null @@ -1,934 +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; - -use Symfony\Component\Form\FormError; -use Symfony\Component\Security\Csrf\CsrfToken; - -abstract class AbstractDivLayoutTestCase extends AbstractLayoutTestCase -{ - public function testRow() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); - $form->addError(new FormError('[trans]Error![/trans]')); - $view = $form->createView(); - $html = $this->renderRow($view); - - $this->assertMatchesXpath($html, - '/div - [ - ./label[@for="name"] - /following-sibling::ul - [./li[.="[trans]Error![/trans]"]] - [count(./li)=1] - /following-sibling::input[@id="name"] - ] -' - ); - } - - public function testRowOverrideVariables() - { - $view = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType')->createView(); - $html = $this->renderRow($view, [ - 'attr' => ['class' => 'my&class'], - 'label' => 'foo&bar', - 'label_attr' => ['class' => 'my&label&class'], - ]); - - $this->assertMatchesXpath($html, - '/div - [ - ./label[@for="name"][@class="my&label&class required"][.="[trans]foo&bar[/trans]"] - /following-sibling::input[@id="name"][@class="my&class"] - ] -' - ); - } - - public function testRepeatedRow() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RepeatedType'); - $form->addError(new FormError('[trans]Error![/trans]')); - $view = $form->createView(); - $html = $this->renderRow($view); - - // The errors of the form are not rendered by intention! - // In practice, repeated fields cannot have errors as all errors - // on them are mapped to the first child. - // (see RepeatedTypeValidatorExtension) - - $this->assertMatchesXpath($html, - '/div - [ - ./label[@for="name_first"] - /following-sibling::input[@id="name_first"] - ] -/following-sibling::div - [ - ./label[@for="name_second"] - /following-sibling::input[@id="name_second"] - ] -' - ); - } - - public function testButtonRow() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ButtonType'); - $view = $form->createView(); - $html = $this->renderRow($view); - - $this->assertMatchesXpath($html, - '/div - [ - ./button[@type="button"][@name="name"] - ] - [count(//label)=0] -' - ); - } - - public function testRest() - { - $view = $this->factory->createNamedBuilder('name', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add('field1', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->add('field2', 'Symfony\Component\Form\Extension\Core\Type\RepeatedType') - ->add('field3', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->add('field4', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->getForm() - ->createView(); - - // Render field2 row -> does not implicitly call renderWidget because - // it is a repeated field! - $this->renderRow($view['field2']); - - // Render field3 widget - $this->renderWidget($view['field3']); - - // Rest should only contain field1 and field4 - $html = $this->renderRest($view); - - $this->assertMatchesXpath($html, - '/div - [ - ./label[@for="name_field1"] - /following-sibling::input[@type="text"][@id="name_field1"] - ] -/following-sibling::div - [ - ./label[@for="name_field4"] - /following-sibling::input[@type="text"][@id="name_field4"] - ] - [count(../div)=2] - [count(..//label)=2] - [count(..//input)=3] -/following-sibling::input - [@type="hidden"] - [@id="name__token"] -' - ); - } - - public function testRestWithChildrenForms() - { - $child1 = $this->factory->createNamedBuilder('child1', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add('field1', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->add('field2', 'Symfony\Component\Form\Extension\Core\Type\TextType'); - - $child2 = $this->factory->createNamedBuilder('child2', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add('field1', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->add('field2', 'Symfony\Component\Form\Extension\Core\Type\TextType'); - - $view = $this->factory->createNamedBuilder('parent', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add($child1) - ->add($child2) - ->getForm() - ->createView(); - - // Render child1.field1 row - $this->renderRow($view['child1']['field1']); - - // Render child2.field2 widget (remember that widget don't render label) - $this->renderWidget($view['child2']['field2']); - - // Rest should only contain child1.field2 and child2.field1 - $html = $this->renderRest($view); - - $this->assertMatchesXpath($html, - '/div - [ - ./label[not(@for)] - /following-sibling::div[@id="parent_child1"] - [ - ./div - [ - ./label[@for="parent_child1_field2"] - /following-sibling::input[@id="parent_child1_field2"] - ] - ] - ] - -/following-sibling::div - [ - ./label[not(@for)] - /following-sibling::div[@id="parent_child2"] - [ - ./div - [ - ./label[@for="parent_child2_field1"] - /following-sibling::input[@id="parent_child2_field1"] - ] - ] - ] - [count(//label)=4] - [count(//input[@type="text"])=2] -/following-sibling::input[@type="hidden"][@id="parent__token"] -' - ); - } - - public function testRestAndRepeatedWithRow() - { - $view = $this->factory->createNamedBuilder('name', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add('first', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->add('password', 'Symfony\Component\Form\Extension\Core\Type\RepeatedType') - ->getForm() - ->createView(); - - $this->renderRow($view['password']); - - $html = $this->renderRest($view); - - $this->assertMatchesXpath($html, - '/div - [ - ./label[@for="name_first"] - /following-sibling::input[@type="text"][@id="name_first"] - ] - [count(.//input)=1] -/following-sibling::input - [@type="hidden"] - [@id="name__token"] -' - ); - } - - public function testRestAndRepeatedWithRowPerChild() - { - $view = $this->factory->createNamedBuilder('name', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add('first', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->add('password', 'Symfony\Component\Form\Extension\Core\Type\RepeatedType') - ->getForm() - ->createView(); - - $this->renderRow($view['password']['first']); - $this->renderRow($view['password']['second']); - - $html = $this->renderRest($view); - - $this->assertMatchesXpath($html, - '/div - [ - ./label[@for="name_first"] - /following-sibling::input[@type="text"][@id="name_first"] - ] - [count(.//input)=1] - [count(.//label)=1] -/following-sibling::input - [@type="hidden"] - [@id="name__token"] -' - ); - } - - public function testRestAndRepeatedWithWidgetPerChild() - { - $view = $this->factory->createNamedBuilder('name', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add('first', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->add('password', 'Symfony\Component\Form\Extension\Core\Type\RepeatedType') - ->getForm() - ->createView(); - - // The password form is considered as rendered as all its children - // are rendered - $this->renderWidget($view['password']['first']); - $this->renderWidget($view['password']['second']); - - $html = $this->renderRest($view); - - $this->assertMatchesXpath($html, - '/div - [ - ./label[@for="name_first"] - /following-sibling::input[@type="text"][@id="name_first"] - ] - [count(//input)=2] - [count(//label)=1] -/following-sibling::input - [@type="hidden"] - [@id="name__token"] -' - ); - } - - public function testCollection() - { - $form = $this->factory->createNamed('names', 'Symfony\Component\Form\Extension\Core\Type\CollectionType', ['a', 'b'], [ - 'entry_type' => 'Symfony\Component\Form\Extension\Core\Type\TextType', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./div[./input[@type="text"][@value="a"]] - /following-sibling::div[./input[@type="text"][@value="b"]] - ] - [count(./div[./input])=2] -' - ); - } - - // https://github.com/symfony/symfony/issues/5038 - public function testCollectionWithAlternatingRowTypes() - { - $data = [ - ['title' => 'a'], - ['title' => 'b'], - ]; - $form = $this->factory->createNamed('names', 'Symfony\Component\Form\Extension\Core\Type\CollectionType', $data, [ - 'entry_type' => 'Symfony\Component\Form\Tests\Fixtures\AlternatingRowType', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./div[./div/div/input[@type="text"][@value="a"]] - /following-sibling::div[./div/div/textarea[.="b"]] - ] - [count(./div[./div/div/input])=1] - [count(./div[./div/div/textarea])=1] -' - ); - } - - public function testEmptyCollection() - { - $form = $this->factory->createNamed('names', 'Symfony\Component\Form\Extension\Core\Type\CollectionType', [], [ - 'entry_type' => 'Symfony\Component\Form\Extension\Core\Type\TextType', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [./input[@type="hidden"][@id="names__token"]] - [count(./div)=0] -' - ); - } - - public function testCollectionRow() - { - $collection = $this->factory->createNamedBuilder( - 'collection', - 'Symfony\Component\Form\Extension\Core\Type\CollectionType', - ['a', 'b'], - ['entry_type' => 'Symfony\Component\Form\Extension\Core\Type\TextType'] - ); - - $form = $this->factory->createNamedBuilder('form', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add($collection) - ->getForm(); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./div - [ - ./label[not(@for)] - /following-sibling::div - [ - ./div - [ - ./label[@for="form_collection_0"] - /following-sibling::input[@type="text"][@value="a"] - ] - /following-sibling::div - [ - ./label[@for="form_collection_1"] - /following-sibling::input[@type="text"][@value="b"] - ] - ] - ] - /following-sibling::input[@type="hidden"][@id="form__token"] - ] - [count(.//input)=3] -' - ); - } - - public function testForm() - { - $form = $this->factory->createNamedBuilder('name', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->setMethod('PUT') - ->setAction('http://example.com') - ->add('firstName', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->add('lastName', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->getForm(); - - // include ampersands everywhere to validate escaping - $html = $this->renderForm($form->createView(), [ - 'id' => 'my&id', - 'attr' => ['class' => 'my&class'], - ]); - - $this->assertMatchesXpath($html, - '/form - [ - ./input[@type="hidden"][@name="_method"][@value="PUT"] - /following-sibling::div - [ - ./div - [ - ./label[@for="name_firstName"] - /following-sibling::input[@type="text"][@id="name_firstName"] - ] - /following-sibling::div - [ - ./label[@for="name_lastName"] - /following-sibling::input[@type="text"][@id="name_lastName"] - ] - /following-sibling::input[@type="hidden"][@id="name__token"] - ] - [count(.//input)=3] - [@id="my&id"] - [@class="my&class"] - ] - [@method="post"] - [@action="http://example.com"] - [@class="my&class"] -' - ); - } - - public function testFormWidget() - { - $form = $this->factory->createNamedBuilder('name', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add('firstName', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->add('lastName', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->getForm(); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./div - [ - ./label[@for="name_firstName"] - /following-sibling::input[@type="text"][@id="name_firstName"] - ] - /following-sibling::div - [ - ./label[@for="name_lastName"] - /following-sibling::input[@type="text"][@id="name_lastName"] - ] - /following-sibling::input[@type="hidden"][@id="name__token"] - ] - [count(.//input)=3] -' - ); - } - - // https://github.com/symfony/symfony/issues/2308 - public function testNestedFormError() - { - $form = $this->factory->createNamedBuilder('name', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add($this->factory - ->createNamedBuilder('child', 'Symfony\Component\Form\Extension\Core\Type\FormType', null, ['error_bubbling' => false]) - ->add('grandChild', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ) - ->getForm(); - - $form->get('child')->addError(new FormError('[trans]Error![/trans]')); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./div/label - /following-sibling::ul[./li[.="[trans]Error![/trans]"]] - ] - [count(.//li[.="[trans]Error![/trans]"])=1] -' - ); - } - - public function testCsrf() - { - $this->csrfTokenManager->expects($this->any()) - ->method('getToken') - ->willReturn(new CsrfToken('token_id', 'foo&bar')); - - $form = $this->factory->createNamedBuilder('name', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add($this->factory - // No CSRF protection on nested forms - ->createNamedBuilder('child', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add($this->factory->createNamedBuilder('grandchild', 'Symfony\Component\Form\Extension\Core\Type\TextType')) - ) - ->getForm(); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./div - /following-sibling::input[@type="hidden"][@id="name__token"][@value="foo&bar"] - ] - [count(.//input[@type="hidden"])=1] -' - ); - } - - public function testRepeated() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RepeatedType', 'foobar', [ - 'type' => 'Symfony\Component\Form\Extension\Core\Type\TextType', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./div - [ - ./label[@for="name_first"] - /following-sibling::input[@type="text"][@id="name_first"] - ] - /following-sibling::div - [ - ./label[@for="name_second"] - /following-sibling::input[@type="text"][@id="name_second"] - ] - /following-sibling::input[@type="hidden"][@id="name__token"] - ] - [count(.//input)=3] -' - ); - } - - public function testRepeatedWithCustomOptions() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RepeatedType', null, [ - // the global required value cannot be overridden - 'first_options' => ['label' => 'Test', 'required' => false], - 'second_options' => ['label' => 'Test2'], - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./div - [ - ./label[@for="name_first"][.="[trans]Test[/trans]"] - /following-sibling::input[@type="text"][@id="name_first"][@required="required"] - ] - /following-sibling::div - [ - ./label[@for="name_second"][.="[trans]Test2[/trans]"] - /following-sibling::input[@type="text"][@id="name_second"][@required="required"] - ] - /following-sibling::input[@type="hidden"][@id="name__token"] - ] - [count(.//input)=3] -' - ); - } - - public function testSearchInputName() - { - $form = $this->factory->createNamedBuilder('full', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add('name', 'Symfony\Component\Form\Extension\Core\Type\SearchType') - ->getForm(); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./div - [ - ./label[@for="full_name"] - /following-sibling::input[@type="search"][@id="full_name"][@name="full[name]"] - ] - /following-sibling::input[@type="hidden"][@id="full__token"] - ] - [count(//input)=2] -' - ); - } - - public function testLabelHasNoId() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); - $html = $this->renderRow($form->createView()); - - $this->assertMatchesXpath($html, - '/div - [ - ./label[@for="name"][not(@id)] - /following-sibling::input[@id="name"] - ] -' - ); - } - - public function testLabelIsNotRenderedWhenSetToFalse() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ - 'label' => false, - ]); - $html = $this->renderRow($form->createView()); - - $this->assertMatchesXpath($html, - '/div - [ - ./input[@id="name"] - ] - [count(//label)=0] -' - ); - } - - /** - * @dataProvider themeBlockInheritanceProvider - */ - public function testThemeBlockInheritance($theme) - { - $view = $this->factory - ->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\EmailType') - ->createView() - ; - - $this->setTheme($view, $theme); - - $this->assertMatchesXpath( - $this->renderWidget($view), - '/input[@type="email"][@rel="theme"]' - ); - } - - /** - * @dataProvider themeInheritanceProvider - */ - public function testThemeInheritance($parentTheme, $childTheme) - { - $child = $this->factory->createNamedBuilder('child', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add('field', 'Symfony\Component\Form\Extension\Core\Type\TextType'); - - $view = $this->factory->createNamedBuilder('parent', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add('field', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->add($child) - ->getForm() - ->createView() - ; - - $this->setTheme($view, $parentTheme); - $this->setTheme($view['child'], $childTheme); - - $this->assertWidgetMatchesXpath($view, [], - '/div - [ - ./div - [ - ./label[.="parent"] - /following-sibling::input[@type="text"] - ] - /following-sibling::div - [ - ./label[.="child"] - /following-sibling::div - [ - ./div - [ - ./label[.="child"] - /following-sibling::input[@type="text"] - ] - ] - ] - /following-sibling::input[@type="hidden"] - ] -' - ); - } - - /** - * The block "_name_child_label" should be overridden in the theme of the - * implemented driver. - */ - public function testCollectionRowWithCustomBlock() - { - $collection = ['one', 'two', 'three']; - $form = $this->factory->createNamedBuilder('names', 'Symfony\Component\Form\Extension\Core\Type\CollectionType', $collection) - ->getForm(); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./div[./label[.="Custom label: [trans]0[/trans]"]] - /following-sibling::div[./label[.="Custom label: [trans]1[/trans]"]] - /following-sibling::div[./label[.="Custom label: [trans]2[/trans]"]] - ] -' - ); - } - - /** - * The block "_name_c_entry_label" should be overridden in the theme of the - * implemented driver. - */ - public function testChoiceRowWithCustomBlock() - { - $form = $this->factory->createNamedBuilder('name_c', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', 'a', [ - 'choices' => ['ChoiceA' => 'a', 'ChoiceB' => 'b'], - 'expanded' => true, - ]) - ->getForm(); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./label[.="Custom name label: [trans]ChoiceA[/trans]"] - /following-sibling::label[.="Custom name label: [trans]ChoiceB[/trans]"] - ] -' - ); - } - - public function testSingleChoiceExpandedWithLabelsAsFalse() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'choice_label' => false, - 'multiple' => false, - 'expanded' => true, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] - /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)] - /following-sibling::input[@type="hidden"][@id="name__token"] - ] - [count(./input)=3] - [count(./label)=1] -' - ); - } - - public function testSingleChoiceExpandedWithLabelsSetByCallable() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'], - 'choice_label' => function ($choice, $label, $value) { - if ('&b' === $choice) { - return false; - } - - return 'label.'.$value; - }, - 'multiple' => false, - 'expanded' => true, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] - /following-sibling::label[@for="name_0"][.="[trans]label.&a[/trans]"] - /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)] - /following-sibling::input[@type="radio"][@name="name"][@id="name_2"][@value="&c"][not(@checked)] - /following-sibling::label[@for="name_2"][.="[trans]label.&c[/trans]"] - /following-sibling::input[@type="hidden"][@id="name__token"] - ] - [count(./input)=4] - [count(./label)=3] -' - ); - } - - public function testSingleChoiceExpandedWithLabelsSetFalseByCallable() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'choice_label' => function () { - return false; - }, - 'multiple' => false, - 'expanded' => true, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] - /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)] - /following-sibling::input[@type="hidden"][@id="name__token"] - ] - [count(./input)=3] - [count(./label)=1] -' - ); - } - - public function testMultipleChoiceExpandedWithLabelsAsFalse() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', ['&a'], [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'choice_label' => false, - 'multiple' => true, - 'expanded' => true, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@value="&a"][@checked] - /following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_1"][@value="&b"][not(@checked)] - /following-sibling::input[@type="hidden"][@id="name__token"] - ] - [count(./input)=3] - [count(./label)=1] -' - ); - } - - public function testMultipleChoiceExpandedWithLabelsSetByCallable() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', ['&a'], [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'], - 'choice_label' => function ($choice, $label, $value) { - if ('&b' === $choice) { - return false; - } - - return 'label.'.$value; - }, - 'multiple' => true, - 'expanded' => true, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@value="&a"][@checked] - /following-sibling::label[@for="name_0"][.="[trans]label.&a[/trans]"] - /following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_1"][@value="&b"][not(@checked)] - /following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_2"][@value="&c"][not(@checked)] - /following-sibling::label[@for="name_2"][.="[trans]label.&c[/trans]"] - /following-sibling::input[@type="hidden"][@id="name__token"] - ] - [count(./input)=4] - [count(./label)=3] -' - ); - } - - public function testMultipleChoiceExpandedWithLabelsSetFalseByCallable() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', ['&a'], [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'choice_label' => function () { - return false; - }, - 'multiple' => true, - 'expanded' => true, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@value="&a"][@checked] - /following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_1"][@value="&b"][not(@checked)] - /following-sibling::input[@type="hidden"][@id="name__token"] - ] - [count(./input)=3] - [count(./label)=1] -' - ); - } - - public function testFormEndWithRest() - { - $view = $this->factory->createNamedBuilder('name', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add('field1', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->add('field2', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->getForm() - ->createView(); - - $this->renderWidget($view['field1']); - - // Rest should only contain field2 - $html = $this->renderEnd($view); - - // Insert the start tag, the end tag should be rendered by the helper - $this->assertMatchesXpath('
'.$html, - '/form - [ - ./div - [ - ./label[@for="name_field2"] - /following-sibling::input[@type="text"][@id="name_field2"] - ] - /following-sibling::input - [@type="hidden"] - [@id="name__token"] - ] -' - ); - } - - public function testFormEndWithoutRest() - { - $view = $this->factory->createNamedBuilder('name', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add('field1', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->add('field2', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->getForm() - ->createView(); - - $this->renderWidget($view['field1']); - - // Rest should only contain field2, but isn't rendered - $html = $this->renderEnd($view, ['render_rest' => false]); - - $this->assertEquals('
', $html); - } - - public function testWidgetContainerAttributes() - { - $form = $this->factory->createNamed('form', 'Symfony\Component\Form\Extension\Core\Type\FormType', null, [ - 'attr' => ['class' => 'foobar', 'data-foo' => 'bar'], - ]); - - $form->add('text', 'Symfony\Component\Form\Extension\Core\Type\TextType'); - - $html = $this->renderWidget($form->createView()); - - // compare plain HTML to check the whitespace - $this->assertStringContainsString('
', $html); - } - - public function testWidgetContainerAttributeNameRepeatedIfTrue() - { - $form = $this->factory->createNamed('form', 'Symfony\Component\Form\Extension\Core\Type\FormType', null, [ - 'attr' => ['foo' => true], - ]); - - $html = $this->renderWidget($form->createView()); - - // foo="foo" - $this->assertStringContainsString('
', $html); - } -} diff --git a/Tests/AbstractLayoutTestCase.php b/Tests/AbstractLayoutTestCase.php deleted file mode 100644 index aa55bc7431..0000000000 --- a/Tests/AbstractLayoutTestCase.php +++ /dev/null @@ -1,2891 +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; - -use PHPUnit\Framework\SkippedTestError; -use Symfony\Component\Form\Extension\Core\Type\PercentType; -use Symfony\Component\Form\Extension\Core\Type\SubmitType; -use Symfony\Component\Form\Extension\Csrf\CsrfExtension; -use Symfony\Component\Form\FormError; -use Symfony\Component\Form\FormView; -use Symfony\Component\Form\Test\FormIntegrationTestCase; -use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; -use Symfony\Component\Translation\TranslatableMessage; - -abstract class AbstractLayoutTestCase extends FormIntegrationTestCase -{ - use VersionAwareTest; - - protected $csrfTokenManager; - protected $testableFeatures = []; - private $defaultLocale; - - protected function setUp(): void - { - if (!\extension_loaded('intl')) { - $this->markTestSkipped('Extension intl is required.'); - } - - $this->defaultLocale = \Locale::getDefault(); - \Locale::setDefault('en'); - - $this->csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class); - - parent::setUp(); - } - - protected function getExtensions() - { - return [ - new CsrfExtension($this->csrfTokenManager), - ]; - } - - protected function tearDown(): void - { - $this->csrfTokenManager = null; - \Locale::setDefault($this->defaultLocale); - - parent::tearDown(); - } - - protected function assertXpathNodeValue(\DOMElement $element, $expression, $nodeValue) - { - $xpath = new \DOMXPath($element->ownerDocument); - $nodeList = $xpath->evaluate($expression); - $this->assertEquals(1, $nodeList->length); - $this->assertEquals($nodeValue, $nodeList->item(0)->nodeValue); - } - - protected function assertMatchesXpath($html, $expression, $count = 1) - { - $dom = new \DOMDocument('UTF-8'); - try { - // Wrap in node so we can load HTML with multiple tags at - // the top level - $dom->loadXML(''.$html.''); - } catch (\Exception $e) { - $this->fail(sprintf( - "Failed loading HTML:\n\n%s\n\nError: %s", - $html, - $e->getMessage() - )); - } - $xpath = new \DOMXPath($dom); - $nodeList = $xpath->evaluate('/root'.$expression); - - if ($nodeList->length != $count) { - $dom->formatOutput = true; - $this->fail(sprintf( - "Failed asserting that \n\n%s\n\nmatches exactly %s. Matches %s in \n\n%s", - $expression, - 1 == $count ? 'once' : $count.' times', - 1 == $nodeList->length ? 'once' : $nodeList->length.' times', - // strip away and - substr($dom->saveHTML(), 6, -8) - )); - } else { - $this->addToAssertionCount(1); - } - } - - protected function assertWidgetMatchesXpath(FormView $view, array $vars, $xpath) - { - // include ampersands everywhere to validate escaping - $html = $this->renderWidget($view, array_merge([ - 'id' => 'my&id', - 'attr' => ['class' => 'my&class'], - ], $vars)); - - if (!isset($vars['id'])) { - $xpath = trim($xpath).' - [@id="my&id"]'; - } - - if (!isset($vars['attr']['class'])) { - $xpath .= ' - [@class="my&class"]'; - } - - $this->assertMatchesXpath($html, $xpath); - } - - abstract protected function renderForm(FormView $view, array $vars = []); - - abstract protected function renderLabel(FormView $view, $label = null, array $vars = []); - - protected function renderHelp(FormView $view) - { - $this->markTestSkipped(sprintf('%s::renderHelp() is not implemented.', static::class)); - } - - abstract protected function renderErrors(FormView $view); - - abstract protected function renderWidget(FormView $view, array $vars = []); - - abstract protected function renderRow(FormView $view, array $vars = []); - - abstract protected function renderRest(FormView $view, array $vars = []); - - abstract protected function renderStart(FormView $view, array $vars = []); - - abstract protected function renderEnd(FormView $view, array $vars = []); - - abstract protected function setTheme(FormView $view, array $themes, $useDefaultThemes = true); - - public function testLabel() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); - $view = $form->createView(); - $this->renderWidget($view, ['label' => 'foo']); - $html = $this->renderLabel($view); - - $this->assertMatchesXpath($html, - '/label - [@for="name"] - [.="[trans]Name[/trans]"] -' - ); - } - - public function testLabelWithoutTranslation() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ - 'translation_domain' => false, - ]); - - $this->assertMatchesXpath($this->renderLabel($form->createView()), - '/label - [@for="name"] - [.="Name"] -' - ); - } - - public function testLabelOnForm() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateType'); - $view = $form->createView(); - $this->renderWidget($view, ['label' => 'foo']); - $html = $this->renderLabel($view); - - $this->assertMatchesXpath($html, - '/label - [@class="required"] - [.="[trans]Name[/trans]"] -' - ); - } - - public function testLabelWithCustomTextPassedAsOption() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ - 'label' => 'Custom label', - ]); - $html = $this->renderLabel($form->createView()); - - $this->assertMatchesXpath($html, - '/label - [@for="name"] - [.="[trans]Custom label[/trans]"] -' - ); - } - - public function testLabelWithCustomTextPassedDirectly() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); - $html = $this->renderLabel($form->createView(), 'Custom label'); - - $this->assertMatchesXpath($html, - '/label - [@for="name"] - [.="[trans]Custom label[/trans]"] -' - ); - } - - public function testLabelWithCustomTextPassedAsOptionAndDirectly() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ - 'label' => 'Custom label', - ]); - $html = $this->renderLabel($form->createView(), 'Overridden label'); - - $this->assertMatchesXpath($html, - '/label - [@for="name"] - [.="[trans]Overridden label[/trans]"] -' - ); - } - - public function testLabelDoesNotRenderFieldAttributes() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); - $html = $this->renderLabel($form->createView(), null, [ - 'attr' => [ - 'class' => 'my&class', - ], - ]); - - $this->assertMatchesXpath($html, - '/label - [@for="name"] - [@class="required"] -' - ); - } - - public function testLabelWithCustomAttributesPassedDirectly() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); - $html = $this->renderLabel($form->createView(), null, [ - 'label_attr' => [ - 'class' => 'my&class', - ], - ]); - - $this->assertMatchesXpath($html, - '/label - [@for="name"] - [@class="my&class required"] -' - ); - } - - public function testLabelWithCustomTextAndCustomAttributesPassedDirectly() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); - $html = $this->renderLabel($form->createView(), 'Custom label', [ - 'label_attr' => [ - 'class' => 'my&class', - ], - ]); - - $this->assertMatchesXpath($html, - '/label - [@for="name"] - [@class="my&class required"] - [.="[trans]Custom label[/trans]"] -' - ); - } - - // https://github.com/symfony/symfony/issues/5029 - public function testLabelWithCustomTextAsOptionAndCustomAttributesPassedDirectly() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ - 'label' => 'Custom label', - ]); - $html = $this->renderLabel($form->createView(), null, [ - 'label_attr' => [ - 'class' => 'my&class', - ], - ]); - - $this->assertMatchesXpath($html, - '/label - [@for="name"] - [@class="my&class required"] - [.="[trans]Custom label[/trans]"] -' - ); - } - - public function testLabelFormatName() - { - $form = $this->factory->createNamedBuilder('myform') - ->add('myfield', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->getForm(); - $view = $form->get('myfield')->createView(); - $html = $this->renderLabel($view, null, ['label_format' => 'form.%name%']); - - $this->assertMatchesXpath($html, - '/label - [@for="myform_myfield"] - [.="[trans]form.myfield[/trans]"] -' - ); - } - - public function testLabelFormatId() - { - $form = $this->factory->createNamedBuilder('myform') - ->add('myfield', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->getForm(); - $view = $form->get('myfield')->createView(); - $html = $this->renderLabel($view, null, ['label_format' => 'form.%id%']); - - $this->assertMatchesXpath($html, - '/label - [@for="myform_myfield"] - [.="[trans]form.myform_myfield[/trans]"] -' - ); - } - - public function testLabelFormatAsFormOption() - { - $options = ['label_format' => 'form.%name%']; - - $form = $this->factory->createNamedBuilder('myform', 'Symfony\Component\Form\Extension\Core\Type\FormType', null, $options) - ->add('myfield', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->getForm(); - $view = $form->get('myfield')->createView(); - $html = $this->renderLabel($view); - - $this->assertMatchesXpath($html, - '/label - [@for="myform_myfield"] - [.="[trans]form.myfield[/trans]"] -' - ); - } - - public function testLabelFormatOverriddenOption() - { - $options = ['label_format' => 'form.%name%']; - - $form = $this->factory->createNamedBuilder('myform', 'Symfony\Component\Form\Extension\Core\Type\FormType', null, $options) - ->add('myfield', 'Symfony\Component\Form\Extension\Core\Type\TextType', ['label_format' => 'field.%name%']) - ->getForm(); - $view = $form->get('myfield')->createView(); - $html = $this->renderLabel($view); - - $this->assertMatchesXpath($html, - '/label - [@for="myform_myfield"] - [.="[trans]field.myfield[/trans]"] -' - ); - } - - public function testLabelWithoutTranslationOnButton() - { - $form = $this->factory->createNamedBuilder('myform', 'Symfony\Component\Form\Extension\Core\Type\FormType', null, [ - 'translation_domain' => false, - ]) - ->add('mybutton', 'Symfony\Component\Form\Extension\Core\Type\ButtonType') - ->getForm(); - $view = $form->get('mybutton')->createView(); - $html = $this->renderWidget($view); - - $this->assertMatchesXpath($html, - '/button - [@type="button"] - [@name="myform[mybutton]"] - [.="Mybutton"] -' - ); - } - - public function testLabelFormatOnButton() - { - $form = $this->factory->createNamedBuilder('myform') - ->add('mybutton', 'Symfony\Component\Form\Extension\Core\Type\ButtonType') - ->getForm(); - $view = $form->get('mybutton')->createView(); - $html = $this->renderWidget($view, ['label_format' => 'form.%name%']); - - $this->assertMatchesXpath($html, - '/button - [@type="button"] - [@name="myform[mybutton]"] - [.="[trans]form.mybutton[/trans]"] -' - ); - } - - public function testLabelFormatOnButtonId() - { - $form = $this->factory->createNamedBuilder('myform') - ->add('mybutton', 'Symfony\Component\Form\Extension\Core\Type\ButtonType') - ->getForm(); - $view = $form->get('mybutton')->createView(); - $html = $this->renderWidget($view, ['label_format' => 'form.%id%']); - - $this->assertMatchesXpath($html, - '/button - [@type="button"] - [@name="myform[mybutton]"] - [.="[trans]form.myform_mybutton[/trans]"] -' - ); - } - - public function testHelp() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ - 'help' => 'Help text test!', - ]); - $view = $form->createView(); - $html = $this->renderHelp($view); - - $this->assertMatchesXpath($html, - '/p - [@id="name_help"] - [@class="help-text"] - [.="[trans]Help text test![/trans]"] -' - ); - } - - public function testHelpNotSet() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); - $view = $form->createView(); - $html = $this->renderHelp($view); - - $this->assertMatchesXpath($html, '/p', 0); - } - - public function testHelpSetLinkFromWidget() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ - 'help' => 'Help text test!', - ]); - $view = $form->createView(); - $html = $this->renderRow($view); - - // Test if renderHelp method is implemented (throw SkippedTestError if not) - $this->renderHelp($view); - - $this->assertMatchesXpath($html, - '//input - [@aria-describedby="name_help"] -' - ); - } - - public function testHelpNotSetNotLinkedFromWidget() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); - $view = $form->createView(); - $html = $this->renderRow($view); - - // Test if renderHelp method is implemented (throw SkippedTestError if not) - $this->renderHelp($view); - - $this->assertMatchesXpath($html, - '//input - [not(@aria-describedby)] -' - ); - } - - public function testErrors() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); - $form->addError(new FormError('[trans]Error 1[/trans]')); - $form->addError(new FormError('[trans]Error 2[/trans]')); - $view = $form->createView(); - $html = $this->renderErrors($view); - - $this->assertMatchesXpath($html, - '/ul - [ - ./li[.="[trans]Error 1[/trans]"] - /following-sibling::li[.="[trans]Error 2[/trans]"] - ] - [count(./li)=2] -' - ); - } - - public function testOverrideWidgetBlock() - { - // see custom_widgets.html.twig - $form = $this->factory->createNamed('text_id', 'Symfony\Component\Form\Extension\Core\Type\TextType'); - $html = $this->renderWidget($form->createView()); - - $this->assertMatchesXpath($html, - '/div - [ - ./input - [@type="text"] - [@id="text_id"] - ] - [@id="container"] -' - ); - } - - public function testCheckedCheckbox() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CheckboxType', true); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="checkbox"] - [@name="name"] - [@checked="checked"] - [@value="1"] -' - ); - } - - public function testUncheckedCheckbox() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CheckboxType', false); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="checkbox"] - [@name="name"] - [not(@checked)] -' - ); - } - - public function testCheckboxWithValue() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CheckboxType', false, [ - 'value' => 'foo&bar', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="checkbox"] - [@name="name"] - [@value="foo&bar"] -' - ); - } - - public function testSingleChoice() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'multiple' => false, - 'expanded' => false, - ]); - - // If the field is collapsed, has no "multiple" attribute, is required but - // has *no* empty value, the "required" must not be added, otherwise - // the resulting HTML is invalid. - // https://github.com/symfony/symfony/issues/8942 - - // HTML 5 spec - // http://www.w3.org/html/wg/drafts/html/master/forms.html#placeholder-label-option - - // "If a select element has a required attribute specified, does not - // have a multiple attribute specified, and has a display size of 1, - // then the select element must have a placeholder label option." - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/select - [@name="name"] - [not(@required)] - [ - ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] - ] - [count(./option)=2] -' - ); - } - - public function testSelectWithSizeBiggerThanOneCanBeRequired() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', null, [ - 'choices' => ['a', 'b'], - 'multiple' => false, - 'expanded' => false, - 'attr' => ['size' => 2], - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/select - [@name="name"] - [@required="required"] - [@size="2"] - [count(./option)=2] -' - ); - } - - public function testSingleChoiceWithoutTranslation() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'multiple' => false, - 'expanded' => false, - 'choice_translation_domain' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/select - [@name="name"] - [not(@required)] - [ - ./option[@value="&a"][@selected="selected"][.="Choice&A"] - /following-sibling::option[@value="&b"][not(@selected)][.="Choice&B"] - ] - [count(./option)=2] -' - ); - } - - public function testSingleChoiceWithPlaceholderWithoutTranslation() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'multiple' => false, - 'expanded' => false, - 'required' => false, - 'translation_domain' => false, - 'placeholder' => 'Placeholder&Not&Translated', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/select - [@name="name"] - [not(@required)] - [ - ./option[@value=""][not(@selected)][not(@disabled)][.="Placeholder&Not&Translated"] - /following-sibling::option[@value="&a"][@selected="selected"][.="Choice&A"] - /following-sibling::option[@value="&b"][not(@selected)][.="Choice&B"] - ] - [count(./option)=3] -' - ); - } - - public function testSingleChoiceAttributes() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'choice_attr' => ['Choice&B' => ['class' => 'foo&bar']], - 'multiple' => false, - 'expanded' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/select - [@name="name"] - [not(@required)] - [ - ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][@class="foo&bar"][not(@selected)][.="[trans]Choice&B[/trans]"] - ] - [count(./option)=2] -' - ); - } - - public function testSingleChoiceAttributesWithMainAttributes() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'multiple' => false, - 'expanded' => false, - 'attr' => ['class' => 'bar&baz'], - ]); - - $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'bar&baz']], - '/select - [@name="name"] - [@class="bar&baz"] - [not(@required)] - [ - ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"][not(@id)][not(@name)] - /following-sibling::option[@value="&b"][not(@class)][not(@selected)][.="[trans]Choice&B[/trans]"][not(@id)][not(@name)] - ] - [count(./option)=2] -' - ); - } - - public function testSingleExpandedChoiceAttributesWithMainAttributes() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'multiple' => false, - 'expanded' => true, - 'attr' => ['class' => 'bar&baz'], - ]); - - $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'bar&baz']], - '/div - [@class="bar&baz"] - [ - ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] - /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] - /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)] - /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] - /following-sibling::input[@type="hidden"][@id="name__token"] - ] - [count(./input)=3] -' - ); - } - - public function testSingleChoiceWithPreferred() - { - $this->requiresFeatureSet(404); - - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'preferred_choices' => ['&b'], - 'multiple' => false, - 'expanded' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), ['separator' => '-- sep --'], - '/select - [@name="name"] - [not(@required)] - [ - ./option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] - /following-sibling::option[@disabled="disabled"][not(@selected)][.="-- sep --"] - /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][.="[trans]Choice&B[/trans]"] - ] - [count(./option)=4] -' - ); - } - - public function testSingleChoiceWithPreferredAndNoSeparator() - { - $this->requiresFeatureSet(404); - - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'preferred_choices' => ['&b'], - 'multiple' => false, - 'expanded' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), ['separator' => null], - '/select - [@name="name"] - [not(@required)] - [ - ./option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] - /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][.="[trans]Choice&B[/trans]"] - ] - [count(./option)=3] -' - ); - } - - public function testSingleChoiceWithPreferredAndBlankSeparator() - { - $this->requiresFeatureSet(404); - - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'preferred_choices' => ['&b'], - 'multiple' => false, - 'expanded' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), ['separator' => ''], - '/select - [@name="name"] - [not(@required)] - [ - ./option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] - /following-sibling::option[@disabled="disabled"][not(@selected)][.=""] - /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][.="[trans]Choice&B[/trans]"] - ] - [count(./option)=4] -' - ); - } - - public function testChoiceWithOnlyPreferred() - { - $this->requiresFeatureSet(404); - - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'preferred_choices' => ['&a', '&b'], - 'multiple' => false, - 'expanded' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/select - [count(./option)=5] -' - ); - } - - public function testSingleChoiceNonRequired() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'required' => false, - 'multiple' => false, - 'expanded' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/select - [@name="name"] - [not(@required)] - [ - ./option[@value=""][.=""] - /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] - ] - [count(./option)=3] -' - ); - } - - public function testSingleChoiceNonRequiredNoneSelected() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', null, [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'required' => false, - 'multiple' => false, - 'expanded' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/select - [@name="name"] - [not(@required)] - [ - ./option[@value=""][.=""] - /following-sibling::option[@value="&a"][not(@selected)][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] - ] - [count(./option)=3] -' - ); - } - - public function testSingleChoiceNonRequiredWithPlaceholder() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'multiple' => false, - 'expanded' => false, - 'required' => false, - 'placeholder' => 'Select&Anything&Not&Me', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/select - [@name="name"] - [not(@required)] - [ - ./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Select&Anything&Not&Me[/trans]"] - /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] - ] - [count(./option)=3] -' - ); - } - - public function testSingleChoiceRequiredWithPlaceholder() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'required' => true, - 'multiple' => false, - 'expanded' => false, - 'placeholder' => 'Test&Me', - ]); - - // The "disabled" attribute was removed again due to a bug in the - // BlackBerry 10 browser. - // See https://github.com/symfony/symfony/pull/7678 - $this->assertWidgetMatchesXpath($form->createView(), [], - '/select - [@name="name"] - [@required="required"] - [ - ./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Test&Me[/trans]"] - /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] - ] - [count(./option)=3] -' - ); - } - - public function testSingleChoiceRequiredWithPlaceholderViaView() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'required' => true, - 'multiple' => false, - 'expanded' => false, - ]); - - // The "disabled" attribute was removed again due to a bug in the - // BlackBerry 10 browser. - // See https://github.com/symfony/symfony/pull/7678 - $this->assertWidgetMatchesXpath($form->createView(), ['placeholder' => ''], - '/select - [@name="name"] - [@required="required"] - [ - ./option[@value=""][not(@selected)][not(@disabled)][.=""] - /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] - ] - [count(./option)=3] -' - ); - } - - public function testSingleChoiceGrouped() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => [ - 'Group&1' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'Group&2' => ['Choice&C' => '&c'], - ], - 'multiple' => false, - 'expanded' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/select - [@name="name"] - [./optgroup[@label="[trans]Group&1[/trans]"] - [ - ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] - ] - [count(./option)=2] - ] - [./optgroup[@label="[trans]Group&2[/trans]"] - [./option[@value="&c"][not(@selected)][.="[trans]Choice&C[/trans]"]] - [count(./option)=1] - ] - [count(./optgroup)=2] -' - ); - } - - public function testMultipleChoice() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', ['&a'], [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'required' => true, - 'multiple' => true, - 'expanded' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/select - [@name="name[]"] - [@required="required"] - [@multiple="multiple"] - [ - ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] - ] - [count(./option)=2] -' - ); - } - - public function testMultipleChoiceAttributes() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', ['&a'], [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'choice_attr' => ['Choice&B' => ['class' => 'foo&bar']], - 'required' => true, - 'multiple' => true, - 'expanded' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/select - [@name="name[]"] - [@required="required"] - [@multiple="multiple"] - [ - ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][@class="foo&bar"][not(@selected)][.="[trans]Choice&B[/trans]"] - ] - [count(./option)=2] -' - ); - } - - public function testMultipleChoiceSkipsPlaceholder() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', ['&a'], [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'multiple' => true, - 'expanded' => false, - 'placeholder' => 'Test&Me', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/select - [@name="name[]"] - [@multiple="multiple"] - [ - ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] - ] - [count(./option)=2] -' - ); - } - - public function testMultipleChoiceNonRequired() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', ['&a'], [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'required' => false, - 'multiple' => true, - 'expanded' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/select - [@name="name[]"] - [@multiple="multiple"] - [ - ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] - /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] - ] - [count(./option)=2] -' - ); - } - - public function testSingleChoiceExpanded() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'multiple' => false, - 'expanded' => true, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] - /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] - /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)] - /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] - /following-sibling::input[@type="hidden"][@id="name__token"] - ] - [count(./input)=3] -' - ); - } - - public function testSingleChoiceExpandedWithoutTranslation() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'multiple' => false, - 'expanded' => true, - 'choice_translation_domain' => false, - 'placeholder' => 'Placeholder&Not&Translated', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] - /following-sibling::label[@for="name_0"][.="Choice&A"] - /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)] - /following-sibling::label[@for="name_1"][.="Choice&B"] - /following-sibling::input[@type="hidden"][@id="name__token"] - ] - [count(./input)=3] -' - ); - } - - public function testSingleChoiceExpandedAttributes() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'choice_attr' => ['Choice&B' => ['class' => 'foo&bar']], - 'multiple' => false, - 'expanded' => true, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] - /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] - /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][@class="foo&bar"][not(@checked)] - /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] - /following-sibling::input[@type="hidden"][@id="name__token"] - ] - [count(./input)=3] -' - ); - } - - public function testSingleChoiceExpandedWithPlaceholder() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'multiple' => false, - 'expanded' => true, - 'placeholder' => 'Test&Me', - 'required' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./input[@type="radio"][@name="name"][@id="name_placeholder"][not(@checked)] - /following-sibling::label[@for="name_placeholder"][.="[trans]Test&Me[/trans]"] - /following-sibling::input[@type="radio"][@name="name"][@id="name_0"][@checked] - /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] - /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][not(@checked)] - /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] - /following-sibling::input[@type="hidden"][@id="name__token"] - ] - [count(./input)=4] -' - ); - } - - public function testSingleChoiceExpandedWithPlaceholderWithoutTranslation() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], - 'multiple' => false, - 'expanded' => true, - 'required' => false, - 'choice_translation_domain' => false, - 'placeholder' => 'Placeholder&Not&Translated', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./input[@type="radio"][@name="name"][@id="name_placeholder"][not(@checked)] - /following-sibling::label[@for="name_placeholder"][.="Placeholder&Not&Translated"] - /following-sibling::input[@type="radio"][@name="name"][@id="name_0"][@checked] - /following-sibling::label[@for="name_0"][.="Choice&A"] - /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][not(@checked)] - /following-sibling::label[@for="name_1"][.="Choice&B"] - /following-sibling::input[@type="hidden"][@id="name__token"] - ] - [count(./input)=4] -' - ); - } - - public function testSingleChoiceExpandedWithBooleanValue() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', true, [ - 'choices' => ['Choice&A' => '1', 'Choice&B' => '0'], - 'multiple' => false, - 'expanded' => true, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./input[@type="radio"][@name="name"][@id="name_0"][@checked] - /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] - /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][not(@checked)] - /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] - /following-sibling::input[@type="hidden"][@id="name__token"] - ] - [count(./input)=3] -' - ); - } - - public function testMultipleChoiceExpanded() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', ['&a', '&c'], [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'], - 'multiple' => true, - 'expanded' => true, - 'required' => true, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)] - /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] - /following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_1"][not(@checked)][not(@required)] - /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] - /following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_2"][@checked][not(@required)] - /following-sibling::label[@for="name_2"][.="[trans]Choice&C[/trans]"] - /following-sibling::input[@type="hidden"][@id="name__token"] - ] - [count(./input)=4] -' - ); - } - - public function testMultipleChoiceExpandedWithoutTranslation() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', ['&a', '&c'], [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'], - 'multiple' => true, - 'expanded' => true, - 'required' => true, - 'choice_translation_domain' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)] - /following-sibling::label[@for="name_0"][.="Choice&A"] - /following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_1"][not(@checked)][not(@required)] - /following-sibling::label[@for="name_1"][.="Choice&B"] - /following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_2"][@checked][not(@required)] - /following-sibling::label[@for="name_2"][.="Choice&C"] - /following-sibling::input[@type="hidden"][@id="name__token"] - ] - [count(./input)=4] -' - ); - } - - public function testMultipleChoiceExpandedAttributes() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', ['&a', '&c'], [ - 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'], - 'choice_attr' => ['Choice&B' => ['class' => 'foo&bar']], - 'multiple' => true, - 'expanded' => true, - 'required' => true, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)] - /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] - /following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_1"][@class="foo&bar"][not(@checked)][not(@required)] - /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] - /following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_2"][@checked][not(@required)] - /following-sibling::label[@for="name_2"][.="[trans]Choice&C[/trans]"] - /following-sibling::input[@type="hidden"][@id="name__token"] - ] - [count(./input)=4] -' - ); - } - - public function testCountry() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CountryType', 'AT'); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/select - [@name="name"] - [./option[@value="AT"][@selected="selected"][.="Austria"]] - [count(./option)>200] -' - ); - } - - public function testCountryWithPlaceholder() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CountryType', 'AT', [ - 'placeholder' => 'Select&Country', - 'required' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/select - [@name="name"] - [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Select&Country[/trans]"]] - [./option[@value="AT"][@selected="selected"][.="Austria"]] - [count(./option)>201] -' - ); - } - - public function testDateTime() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateTimeType', date('Y').'-02-03 04:05:06', [ - 'input' => 'string', - 'with_seconds' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./div - [@id="name_date"] - [ - ./select - [@id="name_date_month"] - [./option[@value="2"][@selected="selected"]] - /following-sibling::select - [@id="name_date_day"] - [./option[@value="3"][@selected="selected"]] - /following-sibling::select - [@id="name_date_year"] - [./option[@value="'.date('Y').'"][@selected="selected"]] - ] - /following-sibling::div - [@id="name_time"] - [ - ./select - [@id="name_time_hour"] - [./option[@value="4"][@selected="selected"]] - /following-sibling::select - [@id="name_time_minute"] - [./option[@value="5"][@selected="selected"]] - ] - ] - [count(.//select)=5] -' - ); - } - - public function testDateTimeWithPlaceholderGlobal() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateTimeType', null, [ - 'input' => 'string', - 'placeholder' => 'Change&Me', - 'required' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./div - [@id="name_date"] - [ - ./select - [@id="name_date_month"] - [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]] - /following-sibling::select - [@id="name_date_day"] - [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]] - /following-sibling::select - [@id="name_date_year"] - [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]] - ] - /following-sibling::div - [@id="name_time"] - [ - ./select - [@id="name_time_hour"] - [./option[@value=""][.="[trans]Change&Me[/trans]"]] - /following-sibling::select - [@id="name_time_minute"] - [./option[@value=""][.="[trans]Change&Me[/trans]"]] - ] - ] - [count(.//select)=5] -' - ); - } - - public function testDateTimeWithHourAndMinute() - { - $data = ['year' => date('Y'), 'month' => '2', 'day' => '3', 'hour' => '4', 'minute' => '5']; - - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateTimeType', $data, [ - 'input' => 'array', - 'required' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./div - [@id="name_date"] - [ - ./select - [@id="name_date_month"] - [./option[@value="2"][@selected="selected"]] - /following-sibling::select - [@id="name_date_day"] - [./option[@value="3"][@selected="selected"]] - /following-sibling::select - [@id="name_date_year"] - [./option[@value="'.date('Y').'"][@selected="selected"]] - ] - /following-sibling::div - [@id="name_time"] - [ - ./select - [@id="name_time_hour"] - [./option[@value="4"][@selected="selected"]] - /following-sibling::select - [@id="name_time_minute"] - [./option[@value="5"][@selected="selected"]] - ] - ] - [count(.//select)=5] -' - ); - } - - public function testDateTimeWithSeconds() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateTimeType', date('Y').'-02-03 04:05:06', [ - 'input' => 'string', - 'with_seconds' => true, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./div - [@id="name_date"] - [ - ./select - [@id="name_date_month"] - [./option[@value="2"][@selected="selected"]] - /following-sibling::select - [@id="name_date_day"] - [./option[@value="3"][@selected="selected"]] - /following-sibling::select - [@id="name_date_year"] - [./option[@value="'.date('Y').'"][@selected="selected"]] - ] - /following-sibling::div - [@id="name_time"] - [ - ./select - [@id="name_time_hour"] - [./option[@value="4"][@selected="selected"]] - /following-sibling::select - [@id="name_time_minute"] - [./option[@value="5"][@selected="selected"]] - /following-sibling::select - [@id="name_time_second"] - [./option[@value="6"][@selected="selected"]] - ] - ] - [count(.//select)=6] -' - ); - } - - public function testDateTimeSingleText() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateTimeType', '2011-02-03 04:05:06', [ - 'input' => 'string', - 'date_widget' => 'single_text', - 'time_widget' => 'single_text', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./input - [@type="date"] - [@id="name_date"] - [@name="name[date]"] - [@value="2011-02-03"] - /following-sibling::input - [@type="time"] - [@id="name_time"] - [@name="name[time]"] - [@value="04:05"] - ] -' - ); - } - - public function testDateTimeWithWidgetSingleText() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateTimeType', '2011-02-03 04:05:06', [ - 'input' => 'string', - 'widget' => 'single_text', - 'model_timezone' => 'UTC', - 'view_timezone' => 'UTC', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="datetime-local"] - [@name="name"] - [@value="2011-02-03T04:05:06"] -' - ); - } - - public function testDateChoice() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateType', date('Y').'-02-03', [ - 'input' => 'string', - 'widget' => 'choice', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./select - [@id="name_month"] - [./option[@value="2"][@selected="selected"]] - /following-sibling::select - [@id="name_day"] - [./option[@value="3"][@selected="selected"]] - /following-sibling::select - [@id="name_year"] - [./option[@value="'.date('Y').'"][@selected="selected"]] - ] - [count(./select)=3] -' - ); - } - - public function testDateChoiceWithPlaceholderGlobal() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateType', null, [ - 'input' => 'string', - 'widget' => 'choice', - 'placeholder' => 'Change&Me', - 'required' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./select - [@id="name_month"] - [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]] - /following-sibling::select - [@id="name_day"] - [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]] - /following-sibling::select - [@id="name_year"] - [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]] - ] - [count(./select)=3] -' - ); - } - - public function testDateChoiceWithPlaceholderOnYear() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateType', null, [ - 'input' => 'string', - 'widget' => 'choice', - 'required' => false, - 'placeholder' => ['year' => 'Change&Me'], - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./select - [@id="name_month"] - [./option[@value="1"]] - /following-sibling::select - [@id="name_day"] - [./option[@value="1"]] - /following-sibling::select - [@id="name_year"] - [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]] - ] - [count(./select)=3] -' - ); - } - - public function testDateText() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateType', '2011-02-03', [ - 'input' => 'string', - 'widget' => 'text', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./input - [@id="name_month"] - [@type="text"] - [@value="2"] - /following-sibling::input - [@id="name_day"] - [@type="text"] - [@value="3"] - /following-sibling::input - [@id="name_year"] - [@type="text"] - [@value="2011"] - ] - [count(./input)=3] -' - ); - } - - public function testDateSingleText() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateType', '2011-02-03', [ - 'input' => 'string', - 'widget' => 'single_text', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="date"] - [@name="name"] - [@value="2011-02-03"] -' - ); - } - - public function testDateErrorBubbling() - { - $form = $this->factory->createNamedBuilder('form', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add('date', 'Symfony\Component\Form\Extension\Core\Type\DateType') - ->getForm(); - $form->get('date')->addError(new FormError('[trans]Error![/trans]')); - $view = $form->createView(); - - $this->assertEmpty($this->renderErrors($view)); - $this->assertNotEmpty($this->renderErrors($view['date'])); - } - - public function testBirthDay() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\BirthdayType', '2000-02-03', [ - 'input' => 'string', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./select - [@id="name_month"] - [./option[@value="2"][@selected="selected"]] - /following-sibling::select - [@id="name_day"] - [./option[@value="3"][@selected="selected"]] - /following-sibling::select - [@id="name_year"] - [./option[@value="2000"][@selected="selected"]] - ] - [count(./select)=3] -' - ); - } - - public function testBirthDayWithPlaceholder() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\BirthdayType', '1950-01-01', [ - 'input' => 'string', - 'placeholder' => '', - 'required' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./select - [@id="name_month"] - [./option[@value=""][not(@selected)][not(@disabled)][.=""]] - [./option[@value="1"][@selected="selected"]] - /following-sibling::select - [@id="name_day"] - [./option[@value=""][not(@selected)][not(@disabled)][.=""]] - [./option[@value="1"][@selected="selected"]] - /following-sibling::select - [@id="name_year"] - [./option[@value=""][not(@selected)][not(@disabled)][.=""]] - [./option[@value="1950"][@selected="selected"]] - ] - [count(./select)=3] -' - ); - } - - public function testEmail() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\EmailType', 'foo&bar'); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="email"] - [@name="name"] - [@value="foo&bar"] - [not(@maxlength)] -' - ); - } - - public function testEmailWithMaxLength() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\EmailType', 'foo&bar', [ - 'attr' => ['maxlength' => 123], - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="email"] - [@name="name"] - [@value="foo&bar"] - [@maxlength="123"] -' - ); - } - - public function testFile() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\FileType'); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="file"] -' - ); - } - - public function testHidden() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\HiddenType', 'foo&bar'); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="hidden"] - [@name="name"] - [@value="foo&bar"] -' - ); - } - - public function testDisabled() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ - 'disabled' => true, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="text"] - [@name="name"] - [@disabled="disabled"] -' - ); - } - - public function testInteger() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\IntegerType', 123); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="number"] - [@name="name"] - [@value="123"] -' - ); - } - - public function testIntegerTypeWithGroupingRendersAsTextInput() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\IntegerType', 123, [ - 'grouping' => true, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="text"] - [@name="name"] - [@value="123"] -' - ); - } - - public function testLanguage() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\LanguageType', 'de'); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/select - [@name="name"] - [./option[@value="de"][@selected="selected"][.="German"]] - [count(./option)>200] -' - ); - } - - public function testLocale() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\LocaleType', 'de_AT'); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/select - [@name="name"] - [./option[@value="de_AT"][@selected="selected"][.="German (Austria)"]] - [count(./option)>200] -' - ); - } - - public function testMoney() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\MoneyType', 1234.56, [ - 'currency' => 'EUR', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="text"] - [@name="name"] - [@value="1234.56"] - [contains(.., "€")] -' - ); - } - - public function testNumber() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\NumberType', 1234.56); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="text"] - [@name="name"] - [@value="1234.56"] -' - ); - } - - public function testRenderNumberWithHtml5NumberType() - { - $this->requiresFeatureSet(403); - - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\NumberType', 1234.56, [ - 'html5' => true, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="number"] - [@step="any"] - [@name="name"] - [@value="1234.56"] -' - ); - } - - public function testRenderNumberWithHtml5NumberTypeAndStepAttribute() - { - $this->requiresFeatureSet(403); - - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\NumberType', 1234.56, [ - 'html5' => true, - 'attr' => ['step' => '0.1'], - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="number"] - [@step="0.1"] - [@name="name"] - [@value="1234.56"] -' - ); - } - - public function testPassword() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\PasswordType', 'foo&bar'); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="password"] - [@name="name"] -' - ); - } - - public function testPasswordSubmittedWithNotAlwaysEmpty() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\PasswordType', null, [ - 'always_empty' => false, - ]); - $form->submit('foo&bar'); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="password"] - [@name="name"] - [@value="foo&bar"] -' - ); - } - - public function testPasswordWithMaxLength() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\PasswordType', 'foo&bar', [ - 'attr' => ['maxlength' => 123], - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="password"] - [@name="name"] - [@maxlength="123"] -' - ); - } - - public function testPercent() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\PercentType', 0.1, ['rounding_mode' => \NumberFormatter::ROUND_CEILING]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="text"] - [@name="name"] - [@value="10"] - [contains(.., "%")] -' - ); - } - - public function testPercentNoSymbol() - { - $this->requiresFeatureSet(403); - - $form = $this->factory->createNamed('name', PercentType::class, 0.1, ['symbol' => false, 'rounding_mode' => \NumberFormatter::ROUND_CEILING]); - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="text"] - [@name="name"] - [@value="10"] - [not(contains(.., "%"))] -' - ); - } - - public function testPercentCustomSymbol() - { - $this->requiresFeatureSet(403); - - $form = $this->factory->createNamed('name', PercentType::class, 0.1, ['symbol' => '‱', 'rounding_mode' => \NumberFormatter::ROUND_CEILING]); - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="text"] - [@name="name"] - [@value="10"] - [contains(.., "‱")] -' - ); - } - - public function testCheckedRadio() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RadioType', true); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="radio"] - [@name="name"] - [@checked="checked"] - [@value="1"] -' - ); - } - - public function testUncheckedRadio() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RadioType', false); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="radio"] - [@name="name"] - [not(@checked)] -' - ); - } - - public function testRadioWithValue() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RadioType', false, [ - 'value' => 'foo&bar', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="radio"] - [@name="name"] - [@value="foo&bar"] -' - ); - } - - public function testRange() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RangeType', 42, ['attr' => ['min' => 5]]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="range"] - [@name="name"] - [@value="42"] - [@min="5"] -' - ); - } - - public function testRangeWithMinMaxValues() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RangeType', 42, ['attr' => ['min' => 5, 'max' => 57]]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="range"] - [@name="name"] - [@value="42"] - [@min="5"] - [@max="57"] -' - ); - } - - public function testTextarea() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextareaType', 'foo&bar', [ - 'attr' => ['pattern' => 'foo'], - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/textarea - [@name="name"] - [not(@pattern)] - [.="foo&bar"] -' - ); - } - - public function testText() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', 'foo&bar'); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="text"] - [@name="name"] - [@value="foo&bar"] - [not(@maxlength)] -' - ); - } - - public function testTextWithMaxLength() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', 'foo&bar', [ - 'attr' => ['maxlength' => 123], - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="text"] - [@name="name"] - [@value="foo&bar"] - [@maxlength="123"] -' - ); - } - - public function testSearch() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\SearchType', 'foo&bar'); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="search"] - [@name="name"] - [@value="foo&bar"] - [not(@maxlength)] -' - ); - } - - public function testTime() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TimeType', '04:05:06', [ - 'input' => 'string', - 'with_seconds' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./select - [@id="name_hour"] - [not(@size)] - [./option[@value="4"][@selected="selected"]] - /following-sibling::select - [@id="name_minute"] - [not(@size)] - [./option[@value="5"][@selected="selected"]] - ] - [count(./select)=2] -' - ); - } - - public function testTimeWithSeconds() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TimeType', '04:05:06', [ - 'input' => 'string', - 'with_seconds' => true, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./select - [@id="name_hour"] - [not(@size)] - [./option[@value="4"][@selected="selected"]] - [count(./option)>23] - /following-sibling::select - [@id="name_minute"] - [not(@size)] - [./option[@value="5"][@selected="selected"]] - [count(./option)>59] - /following-sibling::select - [@id="name_second"] - [not(@size)] - [./option[@value="6"][@selected="selected"]] - [count(./option)>59] - ] - [count(./select)=3] -' - ); - } - - public function testTimeText() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TimeType', '04:05:06', [ - 'input' => 'string', - 'widget' => 'text', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./input - [@type="text"] - [@id="name_hour"] - [@name="name[hour]"] - [@value="04"] - [@size="1"] - [@required="required"] - /following-sibling::input - [@type="text"] - [@id="name_minute"] - [@name="name[minute]"] - [@value="05"] - [@size="1"] - [@required="required"] - ] - [count(./input)=2] -' - ); - } - - public function testTimeSingleText() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TimeType', '04:05:06', [ - 'input' => 'string', - 'widget' => 'single_text', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="time"] - [@name="name"] - [@value="04:05"] - [not(@size)] -' - ); - } - - public function testTimeWithPlaceholderGlobal() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TimeType', null, [ - 'input' => 'string', - 'placeholder' => 'Change&Me', - 'required' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./select - [@id="name_hour"] - [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]] - [count(./option)>24] - /following-sibling::select - [@id="name_minute"] - [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]] - [count(./option)>60] - ] - [count(./select)=2] -' - ); - } - - public function testTimeWithPlaceholderOnYear() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TimeType', null, [ - 'input' => 'string', - 'required' => false, - 'placeholder' => ['hour' => 'Change&Me'], - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/div - [ - ./select - [@id="name_hour"] - [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]] - [count(./option)>24] - /following-sibling::select - [@id="name_minute"] - [./option[@value="1"]] - [count(./option)>59] - ] - [count(./select)=2] -' - ); - } - - public function testTimeErrorBubbling() - { - $form = $this->factory->createNamedBuilder('form', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add('time', 'Symfony\Component\Form\Extension\Core\Type\TimeType') - ->getForm(); - $form->get('time')->addError(new FormError('[trans]Error![/trans]')); - $view = $form->createView(); - - $this->assertEmpty($this->renderErrors($view)); - $this->assertNotEmpty($this->renderErrors($view['time'])); - } - - public function testTimezone() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TimezoneType', 'Europe/Vienna'); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/select - [@name="name"] - [not(@required)] - [./option[@value="Europe/Vienna"][@selected="selected"][.="Europe / Vienna"]] - [count(./option)>200] -' - ); - } - - public function testTimezoneWithPlaceholder() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TimezoneType', null, [ - 'placeholder' => 'Select&Timezone', - 'required' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/select - [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Select&Timezone[/trans]"]] - [count(./option)>201] -' - ); - } - - public function testUrlWithDefaultProtocol() - { - $url = 'http://www.example.com?foo1=bar1&foo2=bar2'; - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\UrlType', $url, ['default_protocol' => 'http']); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="text"] - [@name="name"] - [@value="http://www.example.com?foo1=bar1&foo2=bar2"] - [@inputmode="url"] -' - ); - } - - public function testUrlWithoutDefaultProtocol() - { - $url = 'http://www.example.com?foo1=bar1&foo2=bar2'; - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\UrlType', $url, ['default_protocol' => null]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="url"] - [@name="name"] - [@value="http://www.example.com?foo1=bar1&foo2=bar2"] -' - ); - } - - public function testCollectionPrototype() - { - $form = $this->factory->createNamedBuilder('name', 'Symfony\Component\Form\Extension\Core\Type\FormType', ['items' => ['one', 'two', 'three']]) - ->add('items', 'Symfony\Component\Form\Extension\Core\Type\CollectionType', ['allow_add' => true]) - ->getForm() - ->createView(); - - $html = $this->renderWidget($form); - - $this->assertMatchesXpath($html, - '//div[@id="name_items"][@data-prototype] - | - //table[@id="name_items"][@data-prototype]' - ); - } - - public function testEmptyRootFormName() - { - $form = $this->factory->createNamedBuilder('', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add('child', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->getForm(); - - $this->assertMatchesXpath($this->renderWidget($form->createView()), - '//input[@type="hidden"][@id="_token"][@name="_token"] - | - //input[@type="text"][@id="child"][@name="child"]', 2); - } - - public function testButton() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ButtonType'); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/button[@type="button"][@name="name"][.="[trans]Name[/trans]"]' - ); - } - - public function testButtonLabelIsEmpty() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ButtonType'); - - $this->assertSame('', $this->renderLabel($form->createView())); - } - - public function testButtonlabelWithoutTranslation() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ButtonType', null, [ - 'translation_domain' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/button[@type="button"][@name="name"][.="Name"]' - ); - } - - public function testSubmit() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\SubmitType'); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/button[@type="submit"][@name="name"]' - ); - } - - public function testReset() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ResetType'); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/button[@type="reset"][@name="name"]' - ); - } - - public function testStartTag() - { - $form = $this->factory->create('Symfony\Component\Form\Extension\Core\Type\FormType', null, [ - 'method' => 'get', - 'action' => 'http://example.com/directory', - ]); - - $html = $this->renderStart($form->createView()); - - $this->assertSame('
', $html); - } - - public function testStartTagForPutRequest() - { - $form = $this->factory->create('Symfony\Component\Form\Extension\Core\Type\FormType', null, [ - 'method' => 'put', - 'action' => 'http://example.com/directory', - ]); - - $html = $this->renderStart($form->createView()); - - $this->assertMatchesXpath($html.'
', - '/form - [./input[@type="hidden"][@name="_method"][@value="PUT"]] - [@method="post"] - [@action="http://example.com/directory"]' - ); - } - - public function testStartTagWithOverriddenVars() - { - $form = $this->factory->create('Symfony\Component\Form\Extension\Core\Type\FormType', null, [ - 'method' => 'put', - 'action' => 'http://example.com/directory', - ]); - - $html = $this->renderStart($form->createView(), [ - 'method' => 'post', - 'action' => 'http://foo.com/directory', - ]); - - $this->assertSame('
', $html); - } - - public function testStartTagForMultipartForm() - { - $form = $this->factory->createBuilder('Symfony\Component\Form\Extension\Core\Type\FormType', null, [ - 'method' => 'get', - 'action' => 'http://example.com/directory', - ]) - ->add('file', 'Symfony\Component\Form\Extension\Core\Type\FileType') - ->getForm(); - - $html = $this->renderStart($form->createView()); - - $this->assertSame('', $html); - } - - public function testStartTagWithExtraAttributes() - { - $form = $this->factory->create('Symfony\Component\Form\Extension\Core\Type\FormType', null, [ - 'method' => 'get', - 'action' => 'http://example.com/directory', - ]); - - $html = $this->renderStart($form->createView(), [ - 'attr' => ['class' => 'foobar'], - ]); - - $this->assertSame('', $html); - } - - public function testWidgetAttributes() - { - $form = $this->factory->createNamed('text', 'Symfony\Component\Form\Extension\Core\Type\TextType', 'value', [ - 'required' => true, - 'disabled' => true, - 'attr' => ['readonly' => true, 'maxlength' => 10, 'pattern' => '\d+', 'class' => 'foobar', 'data-foo' => 'bar'], - ]); - - $html = $this->renderWidget($form->createView()); - - // compare plain HTML to check the whitespace - $this->assertSame('', $html); - } - - public function testWidgetAttributeNameRepeatedIfTrue() - { - $form = $this->factory->createNamed('text', 'Symfony\Component\Form\Extension\Core\Type\TextType', 'value', [ - 'attr' => ['foo' => true], - ]); - - $html = $this->renderWidget($form->createView()); - - // foo="foo" - $this->assertSame('', $html); - } - - public function testWidgetAttributeHiddenIfFalse() - { - $form = $this->factory->createNamed('text', 'Symfony\Component\Form\Extension\Core\Type\TextType', 'value', [ - 'attr' => ['foo' => false], - ]); - - $html = $this->renderWidget($form->createView()); - - $this->assertStringNotContainsString('foo="', $html); - } - - public function testButtonAttributes() - { - $form = $this->factory->createNamed('button', 'Symfony\Component\Form\Extension\Core\Type\ButtonType', null, [ - 'disabled' => true, - 'attr' => ['class' => 'foobar', 'data-foo' => 'bar'], - ]); - - $html = $this->renderWidget($form->createView()); - - // compare plain HTML to check the whitespace - $this->assertSame('', $html); - } - - public function testButtonAttributeNameRepeatedIfTrue() - { - $form = $this->factory->createNamed('button', 'Symfony\Component\Form\Extension\Core\Type\ButtonType', null, [ - 'attr' => ['foo' => true], - ]); - - $html = $this->renderWidget($form->createView()); - - // foo="foo" - $this->assertSame('', $html); - } - - public function testButtonAttributeHiddenIfFalse() - { - $form = $this->factory->createNamed('button', 'Symfony\Component\Form\Extension\Core\Type\ButtonType', null, [ - 'attr' => ['foo' => false], - ]); - - $html = $this->renderWidget($form->createView()); - - $this->assertStringNotContainsString('foo="', $html); - } - - public function testTextareaWithWhitespaceOnlyContentRetainsValue() - { - $form = $this->factory->createNamed('textarea', 'Symfony\Component\Form\Extension\Core\Type\TextareaType', ' '); - - $html = $this->renderWidget($form->createView()); - - $this->assertStringContainsString('> ', $html); - } - - public function testTextareaWithWhitespaceOnlyContentRetainsValueWhenRenderingForm() - { - $form = $this->factory->createBuilder('Symfony\Component\Form\Extension\Core\Type\FormType', ['textarea' => ' ']) - ->add('textarea', 'Symfony\Component\Form\Extension\Core\Type\TextareaType') - ->getForm(); - - $html = $this->renderForm($form->createView()); - - $this->assertStringContainsString('> ', $html); - } - - public function testWidgetContainerAttributeHiddenIfFalse() - { - $form = $this->factory->createNamed('form', 'Symfony\Component\Form\Extension\Core\Type\FormType', null, [ - 'attr' => ['foo' => false], - ]); - - $html = $this->renderWidget($form->createView()); - - // no foo - $this->assertStringNotContainsString('foo="', $html); - } - - public function testTranslatedAttributes() - { - $view = $this->factory->createNamedBuilder('name', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add('firstName', 'Symfony\Component\Form\Extension\Core\Type\TextType', ['attr' => ['title' => 'Foo']]) - ->add('lastName', 'Symfony\Component\Form\Extension\Core\Type\TextType', ['attr' => ['placeholder' => 'Bar']]) - ->getForm() - ->createView(); - - $html = $this->renderForm($view); - - $this->assertMatchesXpath($html, '/form//input[@title="[trans]Foo[/trans]"]'); - $this->assertMatchesXpath($html, '/form//input[@placeholder="[trans]Bar[/trans]"]'); - } - - public function testAttributesNotTranslatedWhenTranslationDomainIsFalse() - { - $view = $this->factory->createNamedBuilder('name', 'Symfony\Component\Form\Extension\Core\Type\FormType', null, [ - 'translation_domain' => false, - ]) - ->add('firstName', 'Symfony\Component\Form\Extension\Core\Type\TextType', ['attr' => ['title' => 'Foo']]) - ->add('lastName', 'Symfony\Component\Form\Extension\Core\Type\TextType', ['attr' => ['placeholder' => 'Bar']]) - ->getForm() - ->createView(); - - $html = $this->renderForm($view); - - $this->assertMatchesXpath($html, '/form//input[@title="Foo"]'); - $this->assertMatchesXpath($html, '/form//input[@placeholder="Bar"]'); - } - - public function testTel() - { - $tel = '0102030405'; - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TelType', $tel); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="tel"] - [@name="name"] - [@value="0102030405"] -' - ); - } - - public function testColor() - { - $color = '#0000ff'; - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ColorType', $color); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/input - [@type="color"] - [@name="name"] - [@value="#0000ff"] -' - ); - } - - public function testLabelWithTranslationParameters() - { - $this->requiresFeatureSet(403); - - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); - $html = $this->renderLabel($form->createView(), 'Address is %address%', [ - 'label_translation_parameters' => [ - '%address%' => 'Paris, rue de la Paix', - ], - ]); - - $this->assertMatchesXpath($html, - '/label - [@for="name"] - [.="[trans]Address is Paris, rue de la Paix[/trans]"] -' - ); - } - - public function testHelpWithTranslationParameters() - { - $this->requiresFeatureSet(403); - - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ - 'help' => 'for company %company%', - 'help_translation_parameters' => [ - '%company%' => 'ACME Ltd.', - ], - ]); - $html = $this->renderHelp($form->createView()); - - $this->assertMatchesXpath($html, - '/* - [@id="name_help"] - [.="[trans]for company ACME Ltd.[/trans]"] -' - ); - } - - public function testLabelWithTranslatableMessage() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ - 'label' => new TranslatableMessage('foo'), - ]); - $html = $this->renderLabel($form->createView()); - - $this->assertMatchesXpath($html, - '/label - [@for="name"] - [.="[trans]foo[/trans]"] -' - ); - } - - public function testHelpWithTranslatableMessage() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ - 'help' => new TranslatableMessage('foo'), - ]); - $html = $this->renderHelp($form->createView()); - - $this->assertMatchesXpath($html, - '/* - [@id="name_help"] - [.="[trans]foo[/trans]"] -' - ); - } - - public function testAttributesWithTranslationParameters() - { - $this->requiresFeatureSet(403); - - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ - 'attr' => [ - 'title' => 'Message to %company%', - 'placeholder' => 'Enter a message to %company%', - ], - 'attr_translation_parameters' => [ - '%company%' => 'ACME Ltd.', - ], - ]); - $html = $this->renderWidget($form->createView()); - - $this->assertMatchesXpath($html, - '/input - [@title="[trans]Message to ACME Ltd.[/trans]"] - [@placeholder="[trans]Enter a message to ACME Ltd.[/trans]"] -' - ); - } - - public function testButtonWithTranslationParameters() - { - $this->requiresFeatureSet(403); - - $form = $this->factory->createNamedBuilder('myform') - ->add('mybutton', 'Symfony\Component\Form\Extension\Core\Type\ButtonType', [ - 'label' => 'Submit to %company%', - 'label_translation_parameters' => [ - '%company%' => 'ACME Ltd.', - ], - ]) - ->getForm(); - $view = $form->get('mybutton')->createView(); - $html = $this->renderWidget($view, ['label_format' => 'form.%name%']); - - $this->assertMatchesXpath($html, - '/button - [.="[trans]Submit to ACME Ltd.[/trans]"] -' - ); - } - - /** - * @dataProvider submitFormNoValidateProvider - */ - public function testSubmitFormNoValidate(bool $validate) - { - $this->requiresFeatureSet(404); - - $form = $this->factory->create(SubmitType::class, null, [ - 'validate' => $validate, - ]); - - $html = $this->renderWidget($form->createView()); - - $xpath = '/button - [@type="submit"] - '; - - if (!$validate) { - $xpath .= '[@formnovalidate="formnovalidate"]'; - } else { - $xpath .= '[not(@formnovalidate="formnovalidate")]'; - } - - $this->assertMatchesXpath($html, $xpath); - } - - public static function submitFormNoValidateProvider() - { - return [ - [false], - [true], - ]; - } - - public function testWeekSingleText() - { - $this->requiresFeatureSet(404); - - $form = $this->factory->createNamed('holidays', 'Symfony\Component\Form\Extension\Core\Type\WeekType', '1970-W01', [ - 'input' => 'string', - 'widget' => 'single_text', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], - '/input - [@type="week"] - [@name="holidays"] - [@class="my&class"] - [@value="1970-W01"] -' - ); - } - - public function testWeekSingleTextNoHtml5() - { - $this->requiresFeatureSet(404); - - $form = $this->factory->createNamed('holidays', 'Symfony\Component\Form\Extension\Core\Type\WeekType', '1970-W01', [ - 'input' => 'string', - 'widget' => 'single_text', - 'html5' => false, - ]); - - $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], - '/input - [@type="text"] - [@name="holidays"] - [@class="my&class"] - [@value="1970-W01"] -' - ); - } - - public function testWeekChoices() - { - $this->requiresFeatureSet(404); - - $data = ['year' => (int) date('Y'), 'week' => 1]; - - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\WeekType', $data, [ - 'input' => 'array', - 'widget' => 'choice', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], - '/div - [@class="my&class"] - [ - ./select - [@id="name_year"] - [./option[@value="'.$data['year'].'"][@selected="selected"]] - /following-sibling::select - [@id="name_week"] - [./option[@value="'.$data['week'].'"][@selected="selected"]] - ] - [count(.//select)=2]' - ); - } - - public function testWeekText() - { - $this->requiresFeatureSet(404); - - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\WeekType', '2000-W01', [ - 'input' => 'string', - 'widget' => 'text', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], - '/div - [@class="my&class"] - [ - ./input - [@id="name_year"] - [@type="number"] - [@value="2000"] - /following-sibling::input - [@id="name_week"] - [@type="number"] - [@value="1"] - ] - [count(./input)=2]' - ); - } -} diff --git a/Tests/AbstractRequestHandlerTestCase.php b/Tests/AbstractRequestHandlerTestCase.php index d9ca3ef109..f80efffb71 100644 --- a/Tests/AbstractRequestHandlerTestCase.php +++ b/Tests/AbstractRequestHandlerTestCase.php @@ -32,25 +32,16 @@ */ abstract class AbstractRequestHandlerTestCase extends TestCase { - /** - * @var RequestHandlerInterface - */ - protected $requestHandler; - - /** - * @var FormFactory - */ - protected $factory; - - protected $request; - - protected $serverParams; + protected RequestHandlerInterface $requestHandler; + protected FormFactory $factory; + protected mixed $request = null; + protected ServerParams $serverParams; protected function setUp(): void { - $this->serverParams = new class() extends ServerParams { - public $contentLength; - public $postMaxSize = ''; + $this->serverParams = new class extends ServerParams { + public ?int $contentLength = null; + public string $postMaxSize = ''; public function getContentLength(): ?int { diff --git a/Tests/AbstractTableLayoutTestCase.php b/Tests/AbstractTableLayoutTestCase.php deleted file mode 100644 index 09d669129f..0000000000 --- a/Tests/AbstractTableLayoutTestCase.php +++ /dev/null @@ -1,536 +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; - -use Symfony\Component\Form\FormError; -use Symfony\Component\Security\Csrf\CsrfToken; - -abstract class AbstractTableLayoutTestCase extends AbstractLayoutTestCase -{ - public function testRow() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType'); - $form->addError(new FormError('[trans]Error![/trans]')); - $view = $form->createView(); - $html = $this->renderRow($view); - - $this->assertMatchesXpath($html, - '/tr - [ - ./td - [./label[@for="name"]] - /following-sibling::td - [ - ./ul - [./li[.="[trans]Error![/trans]"]] - [count(./li)=1] - /following-sibling::input[@id="name"] - ] - ] -' - ); - } - - public function testLabelIsNotRenderedWhenSetToFalse() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ - 'label' => false, - ]); - $html = $this->renderRow($form->createView()); - - $this->assertMatchesXpath($html, - '/tr - [ - ./td - [count(//label)=0] - /following-sibling::td - [./input[@id="name"]] - ] -' - ); - } - - public function testRepeatedRow() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RepeatedType'); - $html = $this->renderRow($form->createView()); - - $this->assertMatchesXpath($html, - '/tr - [ - ./td - [./label[@for="name_first"]] - /following-sibling::td - [./input[@id="name_first"]] - ] -/following-sibling::tr - [ - ./td - [./label[@for="name_second"]] - /following-sibling::td - [./input[@id="name_second"]] - ] -/following-sibling::tr[@style="display: none"] - [./td[@colspan="2"]/input - [@type="hidden"] - [@id="name__token"] - ] - [count(../tr)=3] -' - ); - } - - public function testRepeatedRowWithErrors() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RepeatedType'); - $form->addError(new FormError('[trans]Error![/trans]')); - $view = $form->createView(); - $html = $this->renderRow($view); - - // The errors of the form are not rendered by intention! - // In practice, repeated fields cannot have errors as all errors - // on them are mapped to the first child. - // (see RepeatedTypeValidatorExtension) - - $this->assertMatchesXpath($html, - '/tr - [ - ./td - [./label[@for="name_first"]] - /following-sibling::td - [./input[@id="name_first"]] - ] -/following-sibling::tr - [ - ./td - [./label[@for="name_second"]] - /following-sibling::td - [./input[@id="name_second"]] - ] -/following-sibling::tr[@style="display: none"] - [./td[@colspan="2"]/input - [@type="hidden"] - [@id="name__token"] - ] - [count(../tr)=3] -' - ); - } - - public function testButtonRow() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ButtonType'); - $view = $form->createView(); - $html = $this->renderRow($view); - - $this->assertMatchesXpath($html, - '/tr - [ - ./td - [.=""] - /following-sibling::td - [./button[@type="button"][@name="name"]] - ] - [count(//label)=0] -' - ); - } - - public function testRest() - { - $view = $this->factory->createNamedBuilder('name', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add('field1', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->add('field2', 'Symfony\Component\Form\Extension\Core\Type\RepeatedType') - ->add('field3', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->add('field4', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->getForm() - ->createView(); - - // Render field2 row -> does not implicitly call renderWidget because - // it is a repeated field! - $this->renderRow($view['field2']); - - // Render field3 widget - $this->renderWidget($view['field3']); - - // Rest should only contain field1 and field4 - $html = $this->renderRest($view); - - $this->assertMatchesXpath($html, - '/tr - [ - ./td - [./label[@for="name_field1"]] - /following-sibling::td - [./input[@id="name_field1"]] - ] -/following-sibling::tr - [ - ./td - [./label[@for="name_field4"]] - /following-sibling::td - [./input[@id="name_field4"]] - ] - [count(../tr)=3] - [count(..//label)=2] - [count(..//input)=3] -/following-sibling::tr[@style="display: none"] - [./td[@colspan="2"]/input - [@type="hidden"] - [@id="name__token"] - ] -' - ); - } - - public function testCollection() - { - $form = $this->factory->createNamed('names', 'Symfony\Component\Form\Extension\Core\Type\CollectionType', ['a', 'b'], [ - 'entry_type' => 'Symfony\Component\Form\Extension\Core\Type\TextType', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/table - [ - ./tr[./td/input[@type="text"][@value="a"]] - /following-sibling::tr[./td/input[@type="text"][@value="b"]] - /following-sibling::tr[@style="display: none"][./td[@colspan="2"]/input[@type="hidden"][@id="names__token"]] - ] - [count(./tr[./td/input])=3] -' - ); - } - - public function testEmptyCollection() - { - $form = $this->factory->createNamed('names', 'Symfony\Component\Form\Extension\Core\Type\CollectionType', [], [ - 'entry_type' => 'Symfony\Component\Form\Extension\Core\Type\TextType', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/table - [./tr[@style="display: none"][./td[@colspan="2"]/input[@type="hidden"][@id="names__token"]]] - [count(./tr[./td/input])=1] -' - ); - } - - public function testForm() - { - $view = $this->factory->createNamedBuilder('name', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->setMethod('PUT') - ->setAction('http://example.com') - ->add('firstName', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->add('lastName', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->getForm() - ->createView(); - - $html = $this->renderForm($view, [ - 'id' => 'my&id', - 'attr' => ['class' => 'my&class'], - ]); - - $this->assertMatchesXpath($html, - '/form - [ - ./input[@type="hidden"][@name="_method"][@value="PUT"] - /following-sibling::table - [ - ./tr - [ - ./td - [./label[@for="name_firstName"]] - /following-sibling::td - [./input[@id="name_firstName"]] - ] - /following-sibling::tr - [ - ./td - [./label[@for="name_lastName"]] - /following-sibling::td - [./input[@id="name_lastName"]] - ] - /following-sibling::tr[@style="display: none"] - [./td[@colspan="2"]/input - [@type="hidden"] - [@id="name__token"] - ] - ] - [count(.//input)=3] - [@id="my&id"] - [@class="my&class"] - ] - [@method="post"] - [@action="http://example.com"] - [@class="my&class"] -' - ); - } - - public function testFormWidget() - { - $view = $this->factory->createNamedBuilder('name', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add('firstName', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->add('lastName', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->getForm() - ->createView(); - - $this->assertWidgetMatchesXpath($view, [], - '/table - [ - ./tr - [ - ./td - [./label[@for="name_firstName"]] - /following-sibling::td - [./input[@id="name_firstName"]] - ] - /following-sibling::tr - [ - ./td - [./label[@for="name_lastName"]] - /following-sibling::td - [./input[@id="name_lastName"]] - ] - /following-sibling::tr[@style="display: none"] - [./td[@colspan="2"]/input - [@type="hidden"] - [@id="name__token"] - ] - ] - [count(.//input)=3] -' - ); - } - - // https://github.com/symfony/symfony/issues/2308 - public function testNestedFormError() - { - $form = $this->factory->createNamedBuilder('name', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add($this->factory - ->createNamedBuilder('child', 'Symfony\Component\Form\Extension\Core\Type\FormType', null, ['error_bubbling' => false]) - ->add('grandChild', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ) - ->getForm(); - - $form->get('child')->addError(new FormError('[trans]Error![/trans]')); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/table - [ - ./tr/td/ul[./li[.="[trans]Error![/trans]"]] - /following-sibling::table[@id="name_child"] - ] - [count(.//li[.="[trans]Error![/trans]"])=1] -' - ); - } - - public function testCsrf() - { - $this->csrfTokenManager->expects($this->any()) - ->method('getToken') - ->willReturn(new CsrfToken('token_id', 'foo&bar')); - - $form = $this->factory->createNamedBuilder('name', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add($this->factory - // No CSRF protection on nested forms - ->createNamedBuilder('child', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add($this->factory->createNamedBuilder('grandchild', 'Symfony\Component\Form\Extension\Core\Type\TextType')) - ) - ->getForm(); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/table - [ - ./tr[@style="display: none"] - [./td[@colspan="2"]/input - [@type="hidden"] - [@id="name__token"] - ] - ] - [count(.//input[@type="hidden"])=1] -' - ); - } - - public function testRepeated() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RepeatedType', 'foobar', [ - 'type' => 'Symfony\Component\Form\Extension\Core\Type\TextType', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/table - [ - ./tr - [ - ./td - [./label[@for="name_first"]] - /following-sibling::td - [./input[@type="text"][@id="name_first"]] - ] - /following-sibling::tr - [ - ./td - [./label[@for="name_second"]] - /following-sibling::td - [./input[@type="text"][@id="name_second"]] - ] - /following-sibling::tr[@style="display: none"] - [./td[@colspan="2"]/input - [@type="hidden"] - [@id="name__token"] - ] - ] - [count(.//input)=3] -' - ); - } - - public function testRepeatedWithCustomOptions() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RepeatedType', 'foobar', [ - 'type' => 'Symfony\Component\Form\Extension\Core\Type\PasswordType', - 'first_options' => ['label' => 'Test', 'required' => false], - 'second_options' => ['label' => 'Test2'], - ]); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/table - [ - ./tr - [ - ./td - [./label[@for="name_first"][.="[trans]Test[/trans]"]] - /following-sibling::td - [./input[@type="password"][@id="name_first"][@required="required"]] - ] - /following-sibling::tr - [ - ./td - [./label[@for="name_second"][.="[trans]Test2[/trans]"]] - /following-sibling::td - [./input[@type="password"][@id="name_second"][@required="required"]] - ] - /following-sibling::tr[@style="display: none"] - [./td[@colspan="2"]/input - [@type="hidden"] - [@id="name__token"] - ] - ] - [count(.//input)=3] -' - ); - } - - /** - * The block "_name_child_label" should be overridden in the theme of the - * implemented driver. - */ - public function testCollectionRowWithCustomBlock() - { - $collection = ['one', 'two', 'three']; - $form = $this->factory->createNamedBuilder('names', 'Symfony\Component\Form\Extension\Core\Type\CollectionType', $collection) - ->getForm(); - - $this->assertWidgetMatchesXpath($form->createView(), [], - '/table - [ - ./tr[./td/label[.="Custom label: [trans]0[/trans]"]] - /following-sibling::tr[./td/label[.="Custom label: [trans]1[/trans]"]] - /following-sibling::tr[./td/label[.="Custom label: [trans]2[/trans]"]] - ] -' - ); - } - - public function testFormEndWithRest() - { - $view = $this->factory->createNamedBuilder('name', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add('field1', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->add('field2', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->getForm() - ->createView(); - - $this->renderWidget($view['field1']); - - // Rest should only contain field2 - $html = $this->renderEnd($view); - - // Insert the start tag, the end tag should be rendered by the helper - // Unfortunately this is not valid HTML, because the surrounding table - // tag is missing. If someone renders a form with table layout - // manually, they should call form_rest() explicitly within the - // tag. - $this->assertMatchesXpath(''.$html, - '/form - [ - ./tr - [ - ./td - [./label[@for="name_field2"]] - /following-sibling::td - [./input[@id="name_field2"]] - ] - /following-sibling::tr[@style="display: none"] - [./td[@colspan="2"]/input - [@type="hidden"] - [@id="name__token"] - ] - ] -' - ); - } - - public function testFormEndWithoutRest() - { - $view = $this->factory->createNamedBuilder('name', 'Symfony\Component\Form\Extension\Core\Type\FormType') - ->add('field1', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->add('field2', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->getForm() - ->createView(); - - $this->renderWidget($view['field1']); - - // Rest should only contain field2, but isn't rendered - $html = $this->renderEnd($view, ['render_rest' => false]); - - $this->assertEquals('', $html); - } - - public function testWidgetContainerAttributes() - { - $form = $this->factory->createNamed('form', 'Symfony\Component\Form\Extension\Core\Type\FormType', null, [ - 'attr' => ['class' => 'foobar', 'data-foo' => 'bar'], - ]); - - $form->add('text', 'Symfony\Component\Form\Extension\Core\Type\TextType'); - - $html = $this->renderWidget($form->createView()); - - // compare plain HTML to check the whitespace - $this->assertStringContainsString('
', $html); - } - - public function testWidgetContainerAttributeNameRepeatedIfTrue() - { - $form = $this->factory->createNamed('form', 'Symfony\Component\Form\Extension\Core\Type\FormType', null, [ - 'attr' => ['foo' => true], - ]); - - $html = $this->renderWidget($form->createView()); - - // foo="foo" - $this->assertStringContainsString('
', $html); - } -} diff --git a/Tests/CallbackTransformerTest.php b/Tests/CallbackTransformerTest.php index 9142e1fa3c..1fb6134dd9 100644 --- a/Tests/CallbackTransformerTest.php +++ b/Tests/CallbackTransformerTest.php @@ -19,8 +19,8 @@ class CallbackTransformerTest extends TestCase public function testTransform() { $transformer = new CallbackTransformer( - function ($value) { return $value.' has been transformed'; }, - function ($value) { return $value.' has reversely been transformed'; } + fn ($value) => $value.' has been transformed', + fn ($value) => $value.' has reversely been transformed' ); $this->assertEquals('foo has been transformed', $transformer->transform('foo')); diff --git a/Tests/ChoiceList/AbstractChoiceListTestCase.php b/Tests/ChoiceList/AbstractChoiceListTestCase.php index dd55bfcff0..5ddae56141 100644 --- a/Tests/ChoiceList/AbstractChoiceListTestCase.php +++ b/Tests/ChoiceList/AbstractChoiceListTestCase.php @@ -19,95 +19,26 @@ */ abstract class AbstractChoiceListTestCase extends TestCase { - /** - * @var ChoiceListInterface - */ - protected $list; - - /** - * @var array - */ - protected $choices; - - /** - * @var array - */ - protected $values; - - /** - * @var array - */ - protected $structuredValues; - - /** - * @var array - */ - protected $keys; - - /** - * @var mixed - */ - protected $choice1; - - /** - * @var mixed - */ - protected $choice2; - - /** - * @var mixed - */ - protected $choice3; - - /** - * @var mixed - */ - protected $choice4; - - /** - * @var string - */ - protected $value1; - - /** - * @var string - */ - protected $value2; - - /** - * @var string - */ - protected $value3; - - /** - * @var string - */ - protected $value4; - - /** - * @var string - */ - protected $key1; - - /** - * @var string - */ - protected $key2; - - /** - * @var string - */ - protected $key3; - - /** - * @var string - */ - protected $key4; + protected ChoiceListInterface $list; + protected array $choices; + protected array $values; + protected array $structuredValues; + protected array $keys; + protected mixed $choice1; + protected mixed $choice2; + protected mixed $choice3; + protected mixed $choice4; + protected string $value1; + protected string $value2; + protected string $value3; + protected string $value4; + protected string $key1; + protected string $key2; + protected string $key3; + protected string $key4; protected function setUp(): void { - parent::setUp(); - $this->list = $this->createChoiceList(); $choices = $this->getChoices(); diff --git a/Tests/ChoiceList/ArrayChoiceListTest.php b/Tests/ChoiceList/ArrayChoiceListTest.php index a156ca5b41..f8a6b5b26e 100644 --- a/Tests/ChoiceList/ArrayChoiceListTest.php +++ b/Tests/ChoiceList/ArrayChoiceListTest.php @@ -19,7 +19,7 @@ */ class ArrayChoiceListTest extends AbstractChoiceListTestCase { - private $object; + private \stdClass $object; protected function setUp(): void { @@ -45,9 +45,7 @@ protected function getValues() public function testCreateChoiceListWithValueCallback() { - $callback = function ($choice) { - return ':'.$choice; - }; + $callback = fn ($choice) => ':'.$choice; $choiceList = new ArrayChoiceList([2 => 'foo', 7 => 'bar', 10 => 'baz'], $callback); @@ -112,9 +110,7 @@ public function testCreateChoiceListWithGroupedChoices() public function testCompareChoicesByIdentityByDefault() { - $callback = function ($choice) { - return $choice->value; - }; + $callback = fn ($choice) => $choice->value; $obj1 = (object) ['value' => 'value1']; $obj2 = (object) ['value' => 'value2']; diff --git a/Tests/ChoiceList/Factory/Cache/ChoiceLoaderTest.php b/Tests/ChoiceList/Factory/Cache/ChoiceLoaderTest.php index b2d0d2e6d7..6134160046 100644 --- a/Tests/ChoiceList/Factory/Cache/ChoiceLoaderTest.php +++ b/Tests/ChoiceList/Factory/Cache/ChoiceLoaderTest.php @@ -26,9 +26,7 @@ public function testSameFormTypeUseCachedLoader() $choiceList = new ArrayChoiceList($choices); $type = new FormType(); - $decorated = new CallbackChoiceLoader(static function () use ($choices) { - return $choices; - }); + $decorated = new CallbackChoiceLoader(static fn () => $choices); $loader1 = new ChoiceLoader($type, $decorated); $loader2 = new ChoiceLoader($type, new ArrayChoiceLoader()); diff --git a/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php b/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php index cdad28b5d1..67e86208c5 100644 --- a/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php +++ b/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php @@ -28,10 +28,7 @@ */ class CachingFactoryDecoratorTest extends TestCase { - /** - * @var CachingFactoryDecorator - */ - private $factory; + private CachingFactoryDecorator $factory; protected function setUp(): void { @@ -146,9 +143,7 @@ public function testCreateFromChoicesSameFilterClosure() $filter = function () {}; $list1 = $this->factory->createListFromChoices($choices, null, $filter); $list2 = $this->factory->createListFromChoices($choices, null, $filter); - $lazyChoiceList = new LazyChoiceList(new FilterChoiceLoaderDecorator(new CallbackChoiceLoader(static function () use ($choices) { - return $choices; - }), $filter), null); + $lazyChoiceList = new LazyChoiceList(new FilterChoiceLoaderDecorator(new CallbackChoiceLoader(static fn () => $choices), $filter), null); $this->assertNotSame($list1, $list2); $this->assertEquals($lazyChoiceList, $list1); @@ -162,9 +157,7 @@ public function testCreateFromChoicesSameFilterClosureUseCache() $filterCallback = function () {}; $list1 = $this->factory->createListFromChoices($choices, null, ChoiceList::filter($formType, $filterCallback)); $list2 = $this->factory->createListFromChoices($choices, null, ChoiceList::filter($formType, function () {})); - $lazyChoiceList = new LazyChoiceList(new FilterChoiceLoaderDecorator(new CallbackChoiceLoader(static function () use ($choices) { - return $choices; - }), function () {}), null); + $lazyChoiceList = new LazyChoiceList(new FilterChoiceLoaderDecorator(new CallbackChoiceLoader(static fn () => $choices), function () {}), null); $this->assertSame($list1, $list2); $this->assertEquals($lazyChoiceList, $list1); @@ -178,9 +171,7 @@ public function testCreateFromChoicesDifferentFilterClosure() $closure2 = function () {}; $list1 = $this->factory->createListFromChoices($choices, null, $closure1); $list2 = $this->factory->createListFromChoices($choices, null, $closure2); - $lazyChoiceList = new LazyChoiceList(new FilterChoiceLoaderDecorator(new CallbackChoiceLoader(static function () use ($choices) { - return $choices; - }), function () {}), null); + $lazyChoiceList = new LazyChoiceList(new FilterChoiceLoaderDecorator(new CallbackChoiceLoader(static fn () => $choices), function () {}), null); $this->assertNotSame($list1, $list2); $this->assertEquals($lazyChoiceList, $list1); diff --git a/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php b/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php index f6478367d2..2b1b239e58 100644 --- a/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php +++ b/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php @@ -22,27 +22,17 @@ use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\Tests\Fixtures\ArrayChoiceLoader; use Symfony\Component\Translation\TranslatableMessage; +use Symfony\Contracts\Translation\TranslatableInterface; +use Symfony\Contracts\Translation\TranslatorInterface; class DefaultChoiceListFactoryTest extends TestCase { - private $obj1; - - private $obj2; - - private $obj3; - - private $obj4; - - private $obj5; - - private $obj6; - - private $list; - - /** - * @var DefaultChoiceListFactory - */ - private $factory; + private \stdClass $obj1; + private \stdClass $obj2; + private \stdClass $obj3; + private \stdClass $obj4; + private ArrayChoiceList $list; + private DefaultChoiceListFactory $factory; public function getValue($object) { @@ -139,7 +129,7 @@ public function testCreateFromChoicesFlatValuesAsCallable() { $list = $this->factory->createListFromChoices( ['A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4], - [$this, 'getValue'] + $this->getValue(...) ); $this->assertObjectListWithCustomValues($list); @@ -149,7 +139,7 @@ public function testCreateFromChoicesFlatValuesAsClosure() { $list = $this->factory->createListFromChoices( ['A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4], - function ($object) { return $object->value; } + fn ($object) => $object->value ); $this->assertObjectListWithCustomValues($list); @@ -169,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); @@ -184,7 +174,7 @@ public function testCreateFromChoicesGroupedValuesAsCallable() 'Group 1' => ['A' => $this->obj1, 'B' => $this->obj2], 'Group 2' => ['C' => $this->obj3, 'D' => $this->obj4], ], - [$this, 'getValue'] + $this->getValue(...) ); $this->assertObjectListWithCustomValues($list); @@ -197,7 +187,7 @@ public function testCreateFromChoicesGroupedValuesAsClosure() 'Group 1' => ['A' => $this->obj1, 'B' => $this->obj2], 'Group 2' => ['C' => $this->obj3, 'D' => $this->obj4], ], - function ($object) { return $object->value; } + fn ($object) => $object->value ); $this->assertObjectListWithCustomValues($list); @@ -206,11 +196,9 @@ function ($object) { return $object->value; } public function testCreateFromFilteredChoices() { $list = $this->factory->createListFromChoices( - ['A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4, 'E' => $this->obj5, 'F' => $this->obj6], + ['A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4, 'E' => null, 'F' => null], null, - function ($choice) { - return $choice !== $this->obj5 && $choice !== $this->obj6; - } + fn ($choice) => null !== $choice ); $this->assertObjectListWithGeneratedValues($list); @@ -222,13 +210,11 @@ public function testCreateFromChoicesGroupedAndFiltered() [ 'Group 1' => ['A' => $this->obj1, 'B' => $this->obj2], 'Group 2' => ['C' => $this->obj3, 'D' => $this->obj4], - 'Group 3' => ['E' => $this->obj5, 'F' => $this->obj6], + 'Group 3' => ['E' => null, 'F' => null], 'Group 4' => [/* empty group should be filtered */], ], null, - function ($choice) { - return $choice !== $this->obj5 && $choice !== $this->obj6; - } + fn ($choice) => null !== $choice ); $this->assertObjectListWithGeneratedValues($list); @@ -240,13 +226,11 @@ public function testCreateFromChoicesGroupedAndFilteredTraversable() new \ArrayIterator([ 'Group 1' => ['A' => $this->obj1, 'B' => $this->obj2], 'Group 2' => ['C' => $this->obj3, 'D' => $this->obj4], - 'Group 3' => ['E' => $this->obj5, 'F' => $this->obj6], + 'Group 3' => ['E' => null, 'F' => null], 'Group 4' => [/* empty group should be filtered */], ]), null, - function ($choice) { - return $choice !== $this->obj5 && $choice !== $this->obj6; - } + fn ($choice) => null !== $choice ); $this->assertObjectListWithGeneratedValues($list); @@ -311,9 +295,7 @@ public function testCreateViewFlatPreferredChoicesSameOrder() [$this->obj2, $this->obj1, $this->obj4, $this->obj3] ); - $preferredLabels = array_map(static function (ChoiceView $view): string { - return $view->label; - }, $view->preferredChoices); + $preferredLabels = array_map(static fn (ChoiceView $view): string => $view->label, $view->preferredChoices); $this->assertSame( [ @@ -333,14 +315,10 @@ public function testCreateViewFlatPreferredChoiceGroupsSameOrder() [$this->obj4, $this->obj2, $this->obj1, $this->obj3], null, // label null, // index - [$this, 'getGroup'] + $this->getGroup(...) ); - $preferredLabels = array_map(static function (ChoiceGroupView $groupView): array { - return array_map(static function (ChoiceView $view): string { - return $view->label; - }, $groupView->choices); - }, $view->preferredChoices); + $preferredLabels = array_map(static fn (ChoiceGroupView $groupView): array => array_map(static fn (ChoiceView $view): string => $view->label, $groupView->choices), $view->preferredChoices); $this->assertEquals( [ @@ -378,7 +356,7 @@ public function testCreateViewFlatPreferredChoicesAsCallable() { $view = $this->factory->createView( $this->list, - [$this, 'isPreferred'] + $this->isPreferred(...) ); $this->assertFlatView($view); @@ -391,9 +369,7 @@ public function testCreateViewFlatPreferredChoicesAsClosure() $view = $this->factory->createView( $this->list, - function ($object) use ($obj2, $obj3) { - return $obj2 === $object || $obj3 === $object; - } + fn ($object) => $obj2 === $object || $obj3 === $object ); $this->assertFlatView($view); @@ -403,9 +379,7 @@ public function testCreateViewFlatPreferredChoicesClosureReceivesKey() { $view = $this->factory->createView( $this->list, - function ($object, $key) { - return 'B' === $key || 'C' === $key; - } + fn ($object, $key) => 'B' === $key || 'C' === $key ); $this->assertFlatView($view); @@ -415,9 +389,7 @@ public function testCreateViewFlatPreferredChoicesClosureReceivesValue() { $view = $this->factory->createView( $this->list, - function ($object, $key, $value) { - return '1' === $value || '2' === $value; - } + fn ($object, $key, $value) => '1' === $value || '2' === $value ); $this->assertFlatView($view); @@ -428,7 +400,7 @@ public function testCreateViewFlatLabelAsCallable() $view = $this->factory->createView( $this->list, [$this->obj2, $this->obj3], - [$this, 'getLabel'] + $this->getLabel(...) ); $this->assertFlatView($view); @@ -439,9 +411,7 @@ public function testCreateViewFlatLabelAsClosure() $view = $this->factory->createView( $this->list, [$this->obj2, $this->obj3], - function ($object) { - return $object->label; - } + fn ($object) => $object->label ); $this->assertFlatView($view); @@ -452,9 +422,7 @@ public function testCreateViewFlatLabelClosureReceivesKey() $view = $this->factory->createView( $this->list, [$this->obj2, $this->obj3], - function ($object, $key) { - return $key; - } + fn ($object, $key) => $key ); $this->assertFlatView($view); @@ -484,7 +452,7 @@ public function testCreateViewFlatIndexAsCallable() $this->list, [$this->obj2, $this->obj3], null, // label - [$this, 'getFormIndex'] + $this->getFormIndex(...) ); $this->assertFlatViewWithCustomIndices($view); @@ -496,9 +464,7 @@ public function testCreateViewFlatIndexAsClosure() $this->list, [$this->obj2, $this->obj3], null, // label - function ($object) { - return $object->index; - } + fn ($object) => $object->index ); $this->assertFlatViewWithCustomIndices($view); @@ -578,7 +544,7 @@ public function testCreateViewFlatGroupByAsCallable() [$this->obj2, $this->obj3], null, // label null, // index - [$this, 'getGroup'] + $this->getGroup(...) ); $this->assertGroupedView($view); @@ -591,7 +557,7 @@ public function testCreateViewFlatGroupByAsCallableReturnsArray() [], null, // label null, // index - [$this, 'getGroupArray'] + $this->getGroupArray(...) ); $this->assertGroupedViewWithChoiceDuplication($view); @@ -604,7 +570,7 @@ public function testCreateViewFlatGroupByObjectThatCanBeCastToString() [$this->obj2, $this->obj3], null, // label null, // index - [$this, 'getGroupAsObject'] + $this->getGroupAsObject(...) ); $this->assertGroupedView($view); @@ -620,9 +586,7 @@ public function testCreateViewFlatGroupByAsClosure() [$this->obj2, $this->obj3], null, // label null, // index - function ($object) use ($obj1, $obj2) { - return $obj1 === $object || $obj2 === $object ? 'Group 1' : 'Group 2'; - } + fn ($object) => $obj1 === $object || $obj2 === $object ? 'Group 1' : 'Group 2' ); $this->assertGroupedView($view); @@ -635,9 +599,7 @@ public function testCreateViewFlatGroupByClosureReceivesKey() [$this->obj2, $this->obj3], null, // label null, // index - function ($object, $key) { - return 'A' === $key || 'B' === $key ? 'Group 1' : 'Group 2'; - } + fn ($object, $key) => 'A' === $key || 'B' === $key ? 'Group 1' : 'Group 2' ); $this->assertGroupedView($view); @@ -650,9 +612,7 @@ public function testCreateViewFlatGroupByClosureReceivesValue() [$this->obj2, $this->obj3], null, // label null, // index - function ($object, $key, $value) { - return '0' === $value || '1' === $value ? 'Group 1' : 'Group 2'; - } + fn ($object, $key, $value) => '0' === $value || '1' === $value ? 'Group 1' : 'Group 2' ); $this->assertGroupedView($view); @@ -697,7 +657,7 @@ public function testCreateViewFlatAttrAsCallable() null, // label null, // index null, // group - [$this, 'getAttr'] + $this->getAttr(...) ); $this->assertFlatViewWithAttr($view); @@ -711,9 +671,7 @@ public function testCreateViewFlatAttrAsClosure() null, // label null, // index null, // group - function ($object) { - return $object->attr; - } + fn ($object) => $object->attr ); $this->assertFlatViewWithAttr($view); @@ -727,12 +685,10 @@ public function testCreateViewFlatAttrClosureReceivesKey() null, // label null, // index null, // group - function ($object, $key) { - switch ($key) { - case 'B': return ['attr1' => 'value1']; - case 'C': return ['attr2' => 'value2']; - default: return []; - } + fn ($object, $key) => match ($key) { + 'B' => ['attr1' => 'value1'], + 'C' => ['attr2' => 'value2'], + default => [], } ); @@ -747,29 +703,22 @@ public function testCreateViewFlatAttrClosureReceivesValue() null, // label null, // index null, // group - function ($object, $key, $value) { - switch ($value) { - case '1': return ['attr1' => 'value1']; - case '2': return ['attr2' => 'value2']; - default: return []; - } + fn ($object, $key, $value) => match ($value) { + '1' => ['attr1' => 'value1'], + '2' => ['attr2' => 'value2'], + default => [], } ); $this->assertFlatViewWithAttr($view); } - /** - * @requires function Symfony\Component\Translation\TranslatableMessage::__construct - */ public function testPassTranslatableMessageAsLabelDoesntCastItToString() { $view = $this->factory->createView( $this->list, [$this->obj1], - static function ($choice, $key, $value) { - return new TranslatableMessage('my_message', ['param1' => 'value1']); - } + static fn ($choice, $key, $value) => new TranslatableMessage('my_message', ['param1' => 'value1']) ); $this->assertInstanceOf(TranslatableMessage::class, $view->choices[0]->label); @@ -777,6 +726,24 @@ static function ($choice, $key, $value) { $this->assertArrayHasKey('param1', $view->choices[0]->label->getParameters()); } + public function testPassTranslatableInterfaceAsLabelDoesntCastItToString() + { + $message = new class implements TranslatableInterface { + public function trans(TranslatorInterface $translator, ?string $locale = null): string + { + return 'my_message'; + } + }; + + $view = $this->factory->createView( + $this->list, + [$this->obj1], + static fn () => $message + ); + + $this->assertSame($message, $view->choices[0]->label); + } + public function testCreateViewFlatLabelTranslationParametersAsArray() { $view = $this->factory->createView( @@ -818,7 +785,7 @@ public function testCreateViewFlatlabelTranslationParametersAsCallable() null, // index null, // group null, // attr - [$this, 'getlabelTranslationParameters'] + $this->getlabelTranslationParameters(...) ); $this->assertFlatViewWithlabelTranslationParameters($view); @@ -833,9 +800,7 @@ public function testCreateViewFlatlabelTranslationParametersAsClosure() null, // index null, // group null, // attr - function ($object) { - return $object->labelTranslationParameters; - } + fn ($object) => $object->labelTranslationParameters ); $this->assertFlatViewWithlabelTranslationParameters($view); @@ -850,11 +815,9 @@ public function testCreateViewFlatlabelTranslationParametersClosureReceivesKey() null, // index null, // group null, // attr - function ($object, $key) { - switch ($key) { - case 'D': return ['%placeholder1%' => 'value1']; - default: return []; - } + fn ($object, $key) => match ($key) { + 'D' => ['%placeholder1%' => 'value1'], + default => [], } ); @@ -870,36 +833,15 @@ public function testCreateViewFlatlabelTranslationParametersClosureReceivesValue null, // index null, // group null, // attr - function ($object, $key, $value) { - switch ($value) { - case '3': return ['%placeholder1%' => 'value1']; - default: return []; - } + fn ($object, $key, $value) => match ($value) { + '3' => ['%placeholder1%' => 'value1'], + default => [], } ); $this->assertFlatViewWithlabelTranslationParameters($view); } - private function assertScalarListWithChoiceValues(ChoiceListInterface $list) - { - $this->assertSame(['a', 'b', 'c', 'd'], $list->getValues()); - - $this->assertSame([ - 'a' => 'a', - 'b' => 'b', - 'c' => 'c', - 'd' => 'd', - ], $list->getChoices()); - - $this->assertSame([ - 'a' => 'A', - 'b' => 'B', - 'c' => 'C', - 'd' => 'D', - ], $list->getOriginalKeys()); - } - private function assertObjectListWithGeneratedValues(ChoiceListInterface $list) { $this->assertSame(['0', '1', '2', '3'], $list->getValues()); @@ -919,25 +861,6 @@ private function assertObjectListWithGeneratedValues(ChoiceListInterface $list) ], $list->getOriginalKeys()); } - private function assertScalarListWithCustomValues(ChoiceListInterface $list) - { - $this->assertSame(['a', 'b', '1', '2'], $list->getValues()); - - $this->assertSame([ - 'a' => 'a', - 'b' => 'b', - 1 => 'c', - 2 => 'd', - ], $list->getChoices()); - - $this->assertSame([ - 'a' => 'A', - 'b' => 'B', - 1 => 'C', - 2 => 'D', - ], $list->getOriginalKeys()); - } - private function assertObjectListWithCustomValues(ChoiceListInterface $list) { $this->assertSame(['a', 'b', '1', '2'], $list->getValues()); @@ -1018,7 +941,7 @@ private function assertFlatViewWithAttr($view) 'C', ['attr2' => 'value2'] ), - ] + ] ), $view); } @@ -1064,7 +987,7 @@ private function assertGroupedView($view) 'Group 2', [2 => new ChoiceView($this->obj3, '2', 'C')] ), - ] + ] ), $view); } @@ -1091,7 +1014,7 @@ private function assertGroupedViewWithChoiceDuplication($view) class DefaultChoiceListFactoryTest_Castable { - private $property; + private string $property; public function __construct($property) { diff --git a/Tests/ChoiceList/Factory/PropertyAccessDecoratorTest.php b/Tests/ChoiceList/Factory/PropertyAccessDecoratorTest.php index 09482fda89..afc83f707f 100644 --- a/Tests/ChoiceList/Factory/PropertyAccessDecoratorTest.php +++ b/Tests/ChoiceList/Factory/PropertyAccessDecoratorTest.php @@ -25,10 +25,7 @@ */ class PropertyAccessDecoratorTest extends TestCase { - /** - * @var PropertyAccessDecorator - */ - private $factory; + private PropertyAccessDecorator $factory; protected function setUp(): void { diff --git a/Tests/ChoiceList/LazyChoiceListTest.php b/Tests/ChoiceList/LazyChoiceListTest.php index 94d41cf9e7..501f4377ad 100644 --- a/Tests/ChoiceList/LazyChoiceListTest.php +++ b/Tests/ChoiceList/LazyChoiceListTest.php @@ -108,9 +108,7 @@ public function testGetChoicesForValuesUsesLoadedList() 'b' => 'bar', 'c' => 'baz', ]; - $list = new LazyChoiceList(new ArrayChoiceLoader($choices), function ($choice) use ($choices) { - return array_search($choice, $choices); - }); + $list = new LazyChoiceList(new ArrayChoiceLoader($choices), fn ($choice) => array_search($choice, $choices)); // load choice list $list->getChoices(); @@ -126,9 +124,7 @@ public function testGetValuesForChoicesUsesLoadedList() 'b' => 'bar', 'c' => 'baz', ]; - $list = new LazyChoiceList(new ArrayChoiceLoader($choices), function ($choice) use ($choices) { - return array_search($choice, $choices); - }); + $list = new LazyChoiceList(new ArrayChoiceLoader($choices), fn ($choice) => array_search($choice, $choices)); // load choice list $list->getChoices(); diff --git a/Tests/ChoiceList/Loader/CallbackChoiceLoaderTest.php b/Tests/ChoiceList/Loader/CallbackChoiceLoaderTest.php index 40f098886b..791a6f006e 100644 --- a/Tests/ChoiceList/Loader/CallbackChoiceLoaderTest.php +++ b/Tests/ChoiceList/Loader/CallbackChoiceLoaderTest.php @@ -21,44 +21,20 @@ */ class CallbackChoiceLoaderTest extends TestCase { - /** - * @var \Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader - */ - private static $loader; - - /** - * @var callable - */ - private static $value; - - /** - * @var array - */ - private static $choices; - - /** - * @var string[] - */ - private static $choiceValues; - - /** - * @var \Symfony\Component\Form\ChoiceList\LazyChoiceList - */ - private static $lazyChoiceList; + private static CallbackChoiceLoader $loader; + private static \Closure $value; + private static array $choices; + private static array $choiceValues = ['choice_one', 'choice_two']; + private static LazyChoiceList $lazyChoiceList; public static function setUpBeforeClass(): void { - self::$loader = new CallbackChoiceLoader(function () { - return self::$choices; - }); - self::$value = function ($choice) { - return $choice->value ?? null; - }; + self::$loader = new CallbackChoiceLoader(fn () => self::$choices); + self::$value = fn ($choice) => $choice->value ?? null; self::$choices = [ (object) ['value' => 'choice_one'], (object) ['value' => 'choice_two'], ]; - self::$choiceValues = ['choice_one', 'choice_two']; self::$lazyChoiceList = new LazyChoiceList(self::$loader, self::$value); } @@ -93,11 +69,11 @@ public function testLoadChoicesForValuesLoadsChoiceListOnFirstCall() public function testLoadValuesForChoicesCastsCallbackItemsToString() { $choices = [ - (object) ['id' => 2], - (object) ['id' => 3], + (object) ['id' => 2], + (object) ['id' => 3], ]; - $value = function ($item) { return $item->id; }; + $value = fn ($item) => $item->id; $this->assertSame(['2', '3'], self::$loader->loadValuesForChoices($choices, $value)); } @@ -110,13 +86,4 @@ public function testLoadValuesForChoicesLoadsChoiceListOnFirstCall() 'Choice list should not be reloaded.' ); } - - public static function tearDownAfterClass(): void - { - self::$loader = null; - self::$value = null; - self::$choices = []; - self::$choiceValues = []; - self::$lazyChoiceList = null; - } } diff --git a/Tests/ChoiceList/Loader/FilterChoiceLoaderDecoratorTest.php b/Tests/ChoiceList/Loader/FilterChoiceLoaderDecoratorTest.php index 1f91a47275..5a41e5aff3 100644 --- a/Tests/ChoiceList/Loader/FilterChoiceLoaderDecoratorTest.php +++ b/Tests/ChoiceList/Loader/FilterChoiceLoaderDecoratorTest.php @@ -20,9 +20,7 @@ class FilterChoiceLoaderDecoratorTest extends TestCase { public function testLoadChoiceList() { - $filter = function ($choice) { - return 0 === $choice % 2; - }; + $filter = fn ($choice) => 0 === $choice % 2; $loader = new FilterChoiceLoaderDecorator(new ArrayChoiceLoader(range(1, 4)), $filter); @@ -31,9 +29,7 @@ public function testLoadChoiceList() public function testLoadChoiceListWithGroupedChoices() { - $filter = function ($choice) { - return $choice < 9 && 0 === $choice % 2; - }; + $filter = fn ($choice) => $choice < 9 && 0 === $choice % 2; $loader = new FilterChoiceLoaderDecorator(new ArrayChoiceLoader(['units' => range(1, 9), 'tens' => range(10, 90, 10)]), $filter); @@ -49,9 +45,7 @@ public function testLoadChoiceListWithGroupedChoices() public function testLoadChoiceListMixedWithGroupedAndNonGroupedChoices() { - $filter = function ($choice) { - return 0 === $choice % 2; - }; + $filter = fn ($choice) => 0 === $choice % 2; $choices = array_merge(range(1, 9), ['grouped' => range(10, 40, 5)]); $loader = new FilterChoiceLoaderDecorator(new ArrayChoiceLoader($choices), $filter); @@ -74,9 +68,7 @@ public function testLoadValuesForChoices() { $evenValues = [1 => '2', 3 => '4']; - $filter = function ($choice) { - return 0 === $choice % 2; - }; + $filter = fn ($choice) => 0 === $choice % 2; $loader = new FilterChoiceLoaderDecorator(new ArrayChoiceLoader([range(1, 4)]), $filter); @@ -88,9 +80,7 @@ public function testLoadChoicesForValues() $evenChoices = [1 => 2, 3 => 4]; $values = array_map('strval', range(1, 4)); - $filter = function ($choice) { - return 0 === $choice % 2; - }; + $filter = fn ($choice) => 0 === $choice % 2; $loader = new FilterChoiceLoaderDecorator(new ArrayChoiceLoader(range(1, 4)), $filter); diff --git a/Tests/ChoiceList/Loader/IntlCallbackChoiceLoaderTest.php b/Tests/ChoiceList/Loader/IntlCallbackChoiceLoaderTest.php index e2827b0d91..2c61388d0e 100644 --- a/Tests/ChoiceList/Loader/IntlCallbackChoiceLoaderTest.php +++ b/Tests/ChoiceList/Loader/IntlCallbackChoiceLoaderTest.php @@ -22,44 +22,20 @@ */ class IntlCallbackChoiceLoaderTest extends TestCase { - /** - * @var \Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader - */ - private static $loader; - - /** - * @var callable - */ - private static $value; - - /** - * @var array - */ - private static $choices; - - /** - * @var string[] - */ - private static $choiceValues; - - /** - * @var \Symfony\Component\Form\ChoiceList\LazyChoiceList - */ - private static $lazyChoiceList; + private static IntlCallbackChoiceLoader $loader; + private static \Closure $value; + private static array $choices; + private static array $choiceValues = ['choice_one', 'choice_two']; + private static LazyChoiceList $lazyChoiceList; public static function setUpBeforeClass(): void { - self::$loader = new IntlCallbackChoiceLoader(function () { - return self::$choices; - }); - self::$value = function ($choice) { - return $choice->value ?? null; - }; + self::$loader = new IntlCallbackChoiceLoader(fn () => self::$choices); + self::$value = fn ($choice) => $choice->value ?? null; self::$choices = [ (object) ['value' => 'choice_one'], (object) ['value' => 'choice_two'], ]; - self::$choiceValues = ['choice_one', 'choice_two']; self::$lazyChoiceList = new LazyChoiceList(self::$loader, self::$value); } @@ -105,13 +81,4 @@ public function testLoadValuesForChoicesLoadsChoiceListOnFirstCall() 'Choice list should not be reloaded.' ); } - - public static function tearDownAfterClass(): void - { - self::$loader = null; - self::$value = null; - self::$choices = []; - self::$choiceValues = []; - self::$lazyChoiceList = null; - } } diff --git a/Tests/ChoiceList/Loader/LazyChoiceLoaderTest.php b/Tests/ChoiceList/Loader/LazyChoiceLoaderTest.php new file mode 100644 index 0000000000..0c1bcf3c22 --- /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/Command/DebugCommandTest.php b/Tests/Command/DebugCommandTest.php index b9b2ef03ac..4537099c2d 100644 --- a/Tests/Command/DebugCommandTest.php +++ b/Tests/Command/DebugCommandTest.php @@ -45,11 +45,6 @@ public function testDebugDeprecatedDefaults() $this->assertEquals(0, $ret, 'Returns 0 in case of success'); $this->assertSame(<<markTestSkipped('Test command completion requires symfony/console 5.4+.'); - } - $formRegistry = new FormRegistry([], new ResolvedFormTypeFactory()); $command = new DebugCommand($formRegistry); $application = new Application(); @@ -273,9 +264,8 @@ private static function getCoreTypes(): array { $coreExtension = new CoreExtension(); $loadTypesRefMethod = (new \ReflectionObject($coreExtension))->getMethod('loadTypes'); - $loadTypesRefMethod->setAccessible(true); $coreTypes = $loadTypesRefMethod->invoke($coreExtension); - $coreTypes = array_map(function (FormTypeInterface $type) { return \get_class($type); }, $coreTypes); + $coreTypes = array_map(fn (FormTypeInterface $type) => $type::class, $coreTypes); sort($coreTypes); return $coreTypes; @@ -294,7 +284,7 @@ private function createCommandTester(array $namespaces = ['Symfony\Component\For class FooType extends AbstractType { - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('foo'); $resolver->setDefined('bar'); @@ -302,15 +292,11 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setDefault('empty_data', function (Options $options) { $foo = $options['foo']; - return function (FormInterface $form) use ($foo) { - return $form->getConfig()->getCompound() ? [$foo] : $foo; - }; + return fn (FormInterface $form) => $form->getConfig()->getCompound() ? [$foo] : $foo; }); $resolver->setAllowedTypes('foo', 'string'); $resolver->setAllowedValues('foo', ['bar', 'baz']); - $resolver->setNormalizer('foo', function (Options $options, $value) { - return (string) $value; - }); + $resolver->setNormalizer('foo', fn (Options $options, $value) => (string) $value); $resolver->setInfo('foo', 'Info'); } } diff --git a/Tests/CompoundFormTest.php b/Tests/CompoundFormTest.php index 4d73db8fdf..882e73034c 100644 --- a/Tests/CompoundFormTest.php +++ b/Tests/CompoundFormTest.php @@ -16,7 +16,6 @@ use Symfony\Component\Form\Exception\AlreadySubmittedException; use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor; use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper; -use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -27,7 +26,6 @@ use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormFactory; -use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormRegistry; use Symfony\Component\Form\Forms; @@ -43,15 +41,8 @@ class CompoundFormTest extends TestCase { - /** - * @var FormFactoryInterface - */ - private $factory; - - /** - * @var FormInterface - */ - private $form; + private FormFactory $factory; + private FormInterface $form; protected function setUp(): void { @@ -337,6 +328,16 @@ public function testIterator() $this->assertSame($this->form->all(), iterator_to_array($this->form)); } + public function testIteratorKeys() + { + $this->form->add($this->getBuilder('0')->getForm()); + $this->form->add($this->getBuilder('1')->getForm()); + + foreach ($this->form as $key => $value) { + $this->assertIsString($key); + } + } + public function testAddMapsViewDataToFormIfInitialized() { $form = $this->getBuilder() @@ -946,10 +947,8 @@ public function testCreateViewWithChildren() $this->form->add($field1); $this->form->add($field2); - $assertChildViewsEqual = function (array $childViews) { - return function (FormView $view) use ($childViews) { - $this->assertSame($childViews, $view->children); - }; + $assertChildViewsEqual = fn (array $childViews) => function (FormView $view) use ($childViews) { + $this->assertSame($childViews, $view->children); }; // First create the view @@ -1080,33 +1079,6 @@ public function testFileUpload() $this->assertNull($this->form->get('bar')->getData()); } - /** - * @group legacy - */ - public function testMapDateTimeObjectsWithEmptyArrayDataUsingPropertyPathMapper() - { - $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() - ->enableExceptionOnInvalidIndex() - ->getPropertyAccessor(); - $form = $this->factory->createBuilder() - ->setDataMapper(new PropertyPathMapper($propertyAccessor)) - ->add('date', DateType::class, [ - 'auto_initialize' => false, - 'format' => 'dd/MM/yyyy', - 'html5' => false, - 'model_timezone' => 'UTC', - 'view_timezone' => 'UTC', - 'widget' => 'single_text', - ]) - ->getForm(); - - $form->submit([ - 'date' => '04/08/2022', - ]); - - $this->assertEquals(['date' => new \DateTime('2022-08-04', new \DateTimeZone('UTC'))], $form->getData()); - } - public function testMapDateTimeObjectsWithEmptyArrayDataUsingDataMapper() { $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() diff --git a/Tests/Console/Descriptor/AbstractDescriptorTestCase.php b/Tests/Console/Descriptor/AbstractDescriptorTestCase.php index f11cb8c505..8a99c205cb 100644 --- a/Tests/Console/Descriptor/AbstractDescriptorTestCase.php +++ b/Tests/Console/Descriptor/AbstractDescriptorTestCase.php @@ -28,7 +28,7 @@ abstract class AbstractDescriptorTestCase extends TestCase { - private $colSize; + private string|false $colSize; protected function setUp(): void { @@ -153,13 +153,13 @@ 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()); } } class FooType extends AbstractType { - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('foo'); $resolver->setDefined('bar'); @@ -167,14 +167,10 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setDefault('empty_data', function (Options $options, $value) { $foo = $options['foo']; - return function (FormInterface $form) use ($foo) { - return $form->getConfig()->getCompound() ? [$foo] : $foo; - }; + return fn (FormInterface $form) => $form->getConfig()->getCompound() ? [$foo] : $foo; }); $resolver->setAllowedTypes('foo', 'string'); $resolver->setAllowedValues('foo', ['bar', 'baz']); - $resolver->setNormalizer('foo', function (Options $options, $value) { - return (string) $value; - }); + $resolver->setNormalizer('foo', fn (Options $options, $value) => (string) $value); } } diff --git a/Tests/DependencyInjection/FormPassTest.php b/Tests/DependencyInjection/FormPassTest.php index c2beee8747..f0ccd3f095 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; /** @@ -64,8 +65,8 @@ public function testAddTaggedTypes() (new Definition(ServiceLocator::class, [[ __CLASS__.'_Type1' => new ServiceClosureArgument(new Reference('my.type1')), __CLASS__.'_Type2' => new ServiceClosureArgument(new Reference('my.type2')), - ]]))->addTag('container.service_locator')->setPublic(false), - $locator->setPublic(false) + ]]))->addTag('container.service_locator'), + $locator ); } @@ -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/CoreExtensionTest.php b/Tests/Extension/Core/CoreExtensionTest.php index ff85149e21..91b1e75086 100644 --- a/Tests/Extension/Core/CoreExtensionTest.php +++ b/Tests/Extension/Core/CoreExtensionTest.php @@ -24,7 +24,7 @@ public function testTransformationFailuresAreConvertedIntoFormErrors() ->getFormFactory(); $form = $formFactory->createBuilder() - ->add('foo', 'Symfony\Component\Form\Extension\Core\Type\DateType') + ->add('foo', 'Symfony\Component\Form\Extension\Core\Type\DateType', ['widget' => 'choice']) ->getForm(); $form->submit('foo'); diff --git a/Tests/Extension/Core/DataMapper/DataMapperTest.php b/Tests/Extension/Core/DataMapper/DataMapperTest.php index c4a271cd03..1a8f2678dd 100644 --- a/Tests/Extension/Core/DataMapper/DataMapperTest.php +++ b/Tests/Extension/Core/DataMapper/DataMapperTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor; use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper; use Symfony\Component\Form\Extension\Core\Type\DateType; @@ -28,15 +27,8 @@ class DataMapperTest extends TestCase { - /** - * @var DataMapper - */ - private $mapper; - - /** - * @var EventDispatcherInterface - */ - private $dispatcher; + private DataMapper $mapper; + private EventDispatcher $dispatcher; protected function setUp(): void { @@ -112,9 +104,6 @@ public function testMapDataToFormsIgnoresUnmapped() self::assertNull($form->getData()); } - /** - * @requires PHP 7.4 - */ public function testMapDataToFormsIgnoresUninitializedProperties() { $engineForm = new Form(new FormConfigBuilder('engine', null, $this->dispatcher)); @@ -316,9 +305,6 @@ public function testMapFormsToDataIgnoresDisabled() self::assertSame($initialEngine, $car->engine); } - /** - * @requires PHP 7.4 - */ public function testMapFormsToUninitializedProperties() { $car = new TypehintedPropertiesCar(); @@ -342,7 +328,7 @@ public function testMapFormsToDataDoesNotChangeEqualDateTimeInstance($date) $article['publishedAt'] = $publishedAtValue; $propertyPath = new PropertyPath('[publishedAt]'); - $config = new FormConfigBuilder('publishedAt', \get_class($publishedAt), $this->dispatcher); + $config = new FormConfigBuilder('publishedAt', $publishedAt::class, $this->dispatcher); $config->setByReference(false); $config->setPropertyPath($propertyPath); $config->setData($publishedAt); @@ -367,9 +353,7 @@ public function testMapDataToFormsUsingGetCallbackOption() $person = new DummyPerson($initialName); $config = new FormConfigBuilder('name', null, $this->dispatcher, [ - 'getter' => static function (DummyPerson $person) { - return $person->myName(); - }, + 'getter' => static fn (DummyPerson $person) => $person->myName(), ]); $form = new Form($config); @@ -460,7 +444,7 @@ public function isSynchronized(): bool class DummyPerson { - private $name; + private string $name; public function __construct(string $name) { diff --git a/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php b/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php deleted file mode 100644 index 3b2ce0015b..0000000000 --- a/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php +++ /dev/null @@ -1,394 +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\Extension\Core\DataMapper; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\EventDispatcher\EventDispatcher; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper; -use Symfony\Component\Form\Extension\Core\Type\DateType; -use Symfony\Component\Form\Form; -use Symfony\Component\Form\FormConfigBuilder; -use Symfony\Component\Form\FormFactoryBuilder; -use Symfony\Component\Form\Tests\Fixtures\TypehintedPropertiesCar; -use Symfony\Component\PropertyAccess\PropertyAccess; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Symfony\Component\PropertyAccess\PropertyPath; - -/** - * @group legacy - */ -class PropertyPathMapperTest extends TestCase -{ - /** - * @var PropertyPathMapper - */ - private $mapper; - - /** - * @var EventDispatcherInterface - */ - private $dispatcher; - - /** - * @var PropertyAccessorInterface - */ - private $propertyAccessor; - - protected function setUp(): void - { - $this->dispatcher = new EventDispatcher(); - $this->propertyAccessor = PropertyAccess::createPropertyAccessor(); - $this->mapper = new PropertyPathMapper($this->propertyAccessor); - } - - public function testMapDataToFormsPassesObjectRefIfByReference() - { - $car = new \stdClass(); - $engine = new \stdClass(); - $car->engine = $engine; - $propertyPath = new PropertyPath('engine'); - - $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); - $config->setByReference(true); - $config->setPropertyPath($propertyPath); - $form = new Form($config); - - $this->mapper->mapDataToForms($car, [$form]); - - $this->assertSame($engine, $form->getData()); - } - - public function testMapDataToFormsPassesObjectCloneIfNotByReference() - { - $car = new \stdClass(); - $engine = new \stdClass(); - $engine->brand = 'Rolls-Royce'; - $car->engine = $engine; - $propertyPath = new PropertyPath('engine'); - - $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); - $config->setByReference(false); - $config->setPropertyPath($propertyPath); - $form = new Form($config); - - $this->mapper->mapDataToForms($car, [$form]); - - $this->assertNotSame($engine, $form->getData()); - $this->assertEquals($engine, $form->getData()); - } - - public function testMapDataToFormsIgnoresEmptyPropertyPath() - { - $car = new \stdClass(); - - $config = new FormConfigBuilder(null, '\stdClass', $this->dispatcher); - $config->setByReference(true); - $form = new Form($config); - - $this->assertNull($form->getPropertyPath()); - - $this->mapper->mapDataToForms($car, [$form]); - - $this->assertNull($form->getData()); - } - - public function testMapDataToFormsIgnoresUnmapped() - { - $car = new \stdClass(); - $car->engine = new \stdClass(); - $propertyPath = new PropertyPath('engine'); - - $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); - $config->setByReference(true); - $config->setMapped(false); - $config->setPropertyPath($propertyPath); - $form = new Form($config); - - $this->mapper->mapDataToForms($car, [$form]); - - $this->assertNull($form->getData()); - } - - /** - * @requires PHP 7.4 - */ - public function testMapDataToFormsIgnoresUninitializedProperties() - { - $engineForm = new Form(new FormConfigBuilder('engine', null, $this->dispatcher)); - $colorForm = new Form(new FormConfigBuilder('color', null, $this->dispatcher)); - - $car = new TypehintedPropertiesCar(); - $car->engine = 'BMW'; - - $this->mapper->mapDataToForms($car, [$engineForm, $colorForm]); - - $this->assertSame($car->engine, $engineForm->getData()); - $this->assertNull($colorForm->getData()); - } - - public function testMapDataToFormsSetsDefaultDataIfPassedDataIsNull() - { - $default = new \stdClass(); - $propertyPath = new PropertyPath('engine'); - - $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); - $config->setByReference(true); - $config->setPropertyPath($propertyPath); - $config->setData($default); - - $form = new Form($config); - - $this->mapper->mapDataToForms(null, [$form]); - - $this->assertSame($default, $form->getData()); - } - - public function testMapDataToFormsSetsDefaultDataIfPassedDataIsEmptyArray() - { - $default = new \stdClass(); - $propertyPath = new PropertyPath('engine'); - - $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); - $config->setByReference(true); - $config->setPropertyPath($propertyPath); - $config->setData($default); - - $form = new Form($config); - - $this->mapper->mapDataToForms([], [$form]); - - $this->assertSame($default, $form->getData()); - } - - public function testMapFormsToDataWritesBackIfNotByReference() - { - $car = new \stdClass(); - $car->engine = new \stdClass(); - $engine = new \stdClass(); - $engine->brand = 'Rolls-Royce'; - $propertyPath = new PropertyPath('engine'); - - $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); - $config->setByReference(false); - $config->setPropertyPath($propertyPath); - $config->setData($engine); - $form = new SubmittedForm($config); - - $this->mapper->mapFormsToData([$form], $car); - - $this->assertEquals($engine, $car->engine); - $this->assertNotSame($engine, $car->engine); - } - - public function testMapFormsToDataWritesBackIfByReferenceButNoReference() - { - $car = new \stdClass(); - $car->engine = new \stdClass(); - $engine = new \stdClass(); - $propertyPath = new PropertyPath('engine'); - - $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); - $config->setByReference(true); - $config->setPropertyPath($propertyPath); - $config->setData($engine); - $form = new SubmittedForm($config); - - $this->mapper->mapFormsToData([$form], $car); - - $this->assertSame($engine, $car->engine); - } - - public function testMapFormsToDataWritesBackIfByReferenceAndReference() - { - $car = new \stdClass(); - $car->engine = 'BMW'; - $propertyPath = new PropertyPath('engine'); - - $config = new FormConfigBuilder('engine', null, $this->dispatcher); - $config->setByReference(true); - $config->setPropertyPath($propertyPath); - $config->setData('Rolls-Royce'); - $form = new SubmittedForm($config); - - $car->engine = 'Rolls-Royce'; - - $this->mapper->mapFormsToData([$form], $car); - - $this->assertSame('Rolls-Royce', $car->engine); - } - - public function testMapFormsToDataIgnoresUnmapped() - { - $initialEngine = new \stdClass(); - $car = new \stdClass(); - $car->engine = $initialEngine; - $engine = new \stdClass(); - $propertyPath = new PropertyPath('engine'); - - $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); - $config->setByReference(true); - $config->setPropertyPath($propertyPath); - $config->setData($engine); - $config->setMapped(false); - $form = new SubmittedForm($config); - - $this->mapper->mapFormsToData([$form], $car); - - $this->assertSame($initialEngine, $car->engine); - } - - public function testMapFormsToDataIgnoresUnsubmittedForms() - { - $initialEngine = new \stdClass(); - $car = new \stdClass(); - $car->engine = $initialEngine; - $engine = new \stdClass(); - $propertyPath = new PropertyPath('engine'); - - $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); - $config->setByReference(true); - $config->setPropertyPath($propertyPath); - $config->setData($engine); - $form = new Form($config); - - $this->mapper->mapFormsToData([$form], $car); - - $this->assertSame($initialEngine, $car->engine); - } - - public function testMapFormsToDataIgnoresEmptyData() - { - $initialEngine = new \stdClass(); - $car = new \stdClass(); - $car->engine = $initialEngine; - $propertyPath = new PropertyPath('engine'); - - $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); - $config->setByReference(true); - $config->setPropertyPath($propertyPath); - $config->setData(null); - $form = new Form($config); - - $this->mapper->mapFormsToData([$form], $car); - - $this->assertSame($initialEngine, $car->engine); - } - - public function testMapFormsToDataIgnoresUnsynchronized() - { - $initialEngine = new \stdClass(); - $car = new \stdClass(); - $car->engine = $initialEngine; - $engine = new \stdClass(); - $propertyPath = new PropertyPath('engine'); - - $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); - $config->setByReference(true); - $config->setPropertyPath($propertyPath); - $config->setData($engine); - $form = new NotSynchronizedForm($config); - - $this->mapper->mapFormsToData([$form], $car); - - $this->assertSame($initialEngine, $car->engine); - } - - public function testMapFormsToDataIgnoresDisabled() - { - $initialEngine = new \stdClass(); - $car = new \stdClass(); - $car->engine = $initialEngine; - $engine = new \stdClass(); - $propertyPath = new PropertyPath('engine'); - - $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); - $config->setByReference(true); - $config->setPropertyPath($propertyPath); - $config->setData($engine); - $config->setDisabled(true); - $form = new SubmittedForm($config); - - $this->mapper->mapFormsToData([$form], $car); - - $this->assertSame($initialEngine, $car->engine); - } - - /** - * @requires PHP 7.4 - */ - public function testMapFormsToUninitializedProperties() - { - $car = new TypehintedPropertiesCar(); - $config = new FormConfigBuilder('engine', null, $this->dispatcher); - $config->setData('BMW'); - $form = new SubmittedForm($config); - - $this->mapper->mapFormsToData([$form], $car); - - $this->assertSame('BMW', $car->engine); - } - - /** - * @dataProvider provideDate - */ - public function testMapFormsToDataDoesNotChangeEqualDateTimeInstance($date) - { - $article = []; - $publishedAt = $date; - $publishedAtValue = clone $publishedAt; - $article['publishedAt'] = $publishedAtValue; - $propertyPath = new PropertyPath('[publishedAt]'); - - $config = new FormConfigBuilder('publishedAt', \get_class($publishedAt), $this->dispatcher); - $config->setByReference(false); - $config->setPropertyPath($propertyPath); - $config->setData($publishedAt); - $form = new SubmittedForm($config); - - $this->mapper->mapFormsToData([$form], $article); - - $this->assertSame($publishedAtValue, $article['publishedAt']); - } - - public static function provideDate() - { - return [ - [new \DateTime()], - [new \DateTimeImmutable()], - ]; - } - - public function testMapFormsToDataMapsDateTimeInstanceToArrayIfNotSetBefore() - { - $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() - ->enableExceptionOnInvalidIndex() - ->getPropertyAccessor(); - $form = (new FormFactoryBuilder())->getFormFactory()->createBuilder() - ->setDataMapper(new PropertyPathMapper($propertyAccessor)) - ->add('date', DateType::class, [ - 'auto_initialize' => false, - 'format' => 'dd/MM/yyyy', - 'html5' => false, - 'model_timezone' => 'UTC', - 'view_timezone' => 'UTC', - 'widget' => 'single_text', - ]) - ->getForm(); - - $form->submit([ - 'date' => '04/08/2022', - ]); - - $this->assertEquals(['date' => new \DateTime('2022-08-04', new \DateTimeZone('UTC'))], $form->getData()); - } -} diff --git a/Tests/Extension/Core/DataTransformer/ArrayToPartsTransformerTest.php b/Tests/Extension/Core/DataTransformer/ArrayToPartsTransformerTest.php index b82a3a3802..6040792240 100644 --- a/Tests/Extension/Core/DataTransformer/ArrayToPartsTransformerTest.php +++ b/Tests/Extension/Core/DataTransformer/ArrayToPartsTransformerTest.php @@ -17,7 +17,7 @@ class ArrayToPartsTransformerTest extends TestCase { - private $transformer; + private ArrayToPartsTransformer $transformer; protected function setUp(): void { @@ -27,11 +27,6 @@ protected function setUp(): void ]); } - protected function tearDown(): void - { - $this->transformer = null; - } - public function testTransform() { $input = [ diff --git a/Tests/Extension/Core/DataTransformer/BooleanToStringTransformerTest.php b/Tests/Extension/Core/DataTransformer/BooleanToStringTransformerTest.php index 98d58c612a..a40a26e54b 100644 --- a/Tests/Extension/Core/DataTransformer/BooleanToStringTransformerTest.php +++ b/Tests/Extension/Core/DataTransformer/BooleanToStringTransformerTest.php @@ -20,21 +20,13 @@ class BooleanToStringTransformerTest extends TestCase { private const TRUE_VALUE = '1'; - /** - * @var BooleanToStringTransformer - */ - protected $transformer; + protected BooleanToStringTransformer $transformer; protected function setUp(): void { $this->transformer = new BooleanToStringTransformer(self::TRUE_VALUE); } - protected function tearDown(): void - { - $this->transformer = null; - } - public function testTransform() { $this->assertEquals(self::TRUE_VALUE, $this->transformer->transform(true)); diff --git a/Tests/Extension/Core/DataTransformer/ChoiceToValueTransformerTest.php b/Tests/Extension/Core/DataTransformer/ChoiceToValueTransformerTest.php index a7a3d1c845..4c6f74925d 100644 --- a/Tests/Extension/Core/DataTransformer/ChoiceToValueTransformerTest.php +++ b/Tests/Extension/Core/DataTransformer/ChoiceToValueTransformerTest.php @@ -18,8 +18,8 @@ class ChoiceToValueTransformerTest extends TestCase { - protected $transformer; - protected $transformerWithNull; + protected ChoiceToValueTransformer $transformer; + protected ChoiceToValueTransformer $transformerWithNull; protected function setUp(): void { diff --git a/Tests/Extension/Core/DataTransformer/ChoicesToValuesTransformerTest.php b/Tests/Extension/Core/DataTransformer/ChoicesToValuesTransformerTest.php index d0911673dd..f7233463bd 100644 --- a/Tests/Extension/Core/DataTransformer/ChoicesToValuesTransformerTest.php +++ b/Tests/Extension/Core/DataTransformer/ChoicesToValuesTransformerTest.php @@ -18,8 +18,8 @@ class ChoicesToValuesTransformerTest extends TestCase { - protected $transformer; - protected $transformerWithNull; + protected ChoicesToValuesTransformer $transformer; + protected ChoicesToValuesTransformer $transformerWithNull; protected function setUp(): void { @@ -30,12 +30,6 @@ protected function setUp(): void $this->transformerWithNull = new ChoicesToValuesTransformer($listWithNull); } - protected function tearDown(): void - { - $this->transformer = null; - $this->transformerWithNull = null; - } - public function testTransform() { $in = ['', false, 'X']; diff --git a/Tests/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformerTest.php b/Tests/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformerTest.php index 18fbf7da67..f2fb15cf0b 100644 --- a/Tests/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformerTest.php +++ b/Tests/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformerTest.php @@ -23,12 +23,16 @@ class DateTimeToHtml5LocalDateTimeTransformerTest extends BaseDateTimeTransforme public static function transformProvider(): array { return [ - ['UTC', 'UTC', '2010-02-03 04:05:06 UTC', '2010-02-03T04:05:06'], - ['UTC', 'UTC', null, ''], - ['America/New_York', 'Asia/Hong_Kong', '2010-02-03 04:05:06 America/New_York', '2010-02-03T17:05:06'], - ['America/New_York', 'Asia/Hong_Kong', null, ''], - ['UTC', 'Asia/Hong_Kong', '2010-02-03 04:05:06 UTC', '2010-02-03T12:05:06'], - ['America/New_York', 'UTC', '2010-02-03 04:05:06 America/New_York', '2010-02-03T09:05:06'], + ['UTC', 'UTC', '2010-02-03 04:05:06 UTC', '2010-02-03T04:05:06', true], + ['UTC', 'UTC', null, '', true], + ['America/New_York', 'Asia/Hong_Kong', '2010-02-03 04:05:06 America/New_York', '2010-02-03T17:05:06', true], + ['America/New_York', 'Asia/Hong_Kong', null, '', true], + ['UTC', 'Asia/Hong_Kong', '2010-02-03 04:05:06 UTC', '2010-02-03T12:05:06', true], + ['America/New_York', 'UTC', '2010-02-03 04:05:06 America/New_York', '2010-02-03T09:05:06', true], + ['UTC', 'UTC', '2010-02-03 04:05:06 UTC', '2010-02-03T04:05', false], + ['America/New_York', 'Asia/Hong_Kong', '2010-02-03 04:05:06 America/New_York', '2010-02-03T17:05', false], + ['UTC', 'Asia/Hong_Kong', '2010-02-03 04:05:06 UTC', '2010-02-03T12:05', false], + ['America/New_York', 'UTC', '2010-02-03 04:05:06 America/New_York', '2010-02-03T09:05', false], ]; } @@ -55,9 +59,9 @@ public static function reverseTransformProvider(): array /** * @dataProvider transformProvider */ - public function testTransform($fromTz, $toTz, $from, $to) + public function testTransform($fromTz, $toTz, $from, $to, bool $withSeconds) { - $transformer = new DateTimeToHtml5LocalDateTimeTransformer($fromTz, $toTz); + $transformer = new DateTimeToHtml5LocalDateTimeTransformer($fromTz, $toTz, $withSeconds); $this->assertSame($to, $transformer->transform(null !== $from ? new \DateTime($from) : null)); } @@ -65,9 +69,9 @@ public function testTransform($fromTz, $toTz, $from, $to) /** * @dataProvider transformProvider */ - public function testTransformDateTimeImmutable($fromTz, $toTz, $from, $to) + public function testTransformDateTimeImmutable($fromTz, $toTz, $from, $to, bool $withSeconds) { - $transformer = new DateTimeToHtml5LocalDateTimeTransformer($fromTz, $toTz); + $transformer = new DateTimeToHtml5LocalDateTimeTransformer($fromTz, $toTz, $withSeconds); $this->assertSame($to, $transformer->transform(null !== $from ? new \DateTimeImmutable($from) : null)); } diff --git a/Tests/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformerTest.php b/Tests/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformerTest.php index 21f5c5079c..b9906c236a 100644 --- a/Tests/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformerTest.php +++ b/Tests/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformerTest.php @@ -11,27 +11,27 @@ 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 { use DateTimeEqualsTrait; - protected $dateTime; - protected $dateTimeWithoutSeconds; - private $defaultLocale; + protected \DateTime $dateTime; + protected \DateTime $dateTimeWithoutSeconds; + private string $defaultLocale; private $initialTestCaseUseException; private $initialTestCaseErrorLevel; protected function setUp(): void { - parent::setUp(); - // Normalize intl. configuration settings. if (\extension_loaded('intl')) { $this->initialTestCaseUseException = ini_set('intl.use_exceptions', 0); @@ -50,8 +50,6 @@ protected function setUp(): void protected function tearDown(): void { - $this->dateTime = null; - $this->dateTimeWithoutSeconds = null; \Locale::setDefault($this->defaultLocale); if (\extension_loaded('intl')) { @@ -238,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'); @@ -391,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 f3dcf1dd7d..c69ba31be9 100644 --- a/Tests/Extension/Core/DataTransformer/DateTimeToRfc3339TransformerTest.php +++ b/Tests/Extension/Core/DataTransformer/DateTimeToRfc3339TransformerTest.php @@ -20,13 +20,11 @@ class DateTimeToRfc3339TransformerTest extends BaseDateTimeTransformerTestCase { use DateTimeEqualsTrait; - protected $dateTime; - protected $dateTimeWithoutSeconds; + protected \DateTime $dateTime; + protected \DateTime $dateTimeWithoutSeconds; 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/IntegerToLocalizedStringTransformerTest.php b/Tests/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformerTest.php index 837717670a..513224574a 100644 --- a/Tests/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformerTest.php +++ b/Tests/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformerTest.php @@ -18,7 +18,7 @@ class IntegerToLocalizedStringTransformerTest extends TestCase { - private $defaultLocale; + private string $defaultLocale; protected function setUp(): void { diff --git a/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php b/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php index 7f9d436679..e5733ad96a 100644 --- a/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php +++ b/Tests/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformerTest.php @@ -18,8 +18,8 @@ class MoneyToLocalizedStringTransformerTest extends TestCase { - private $previousLocale; - private $defaultLocale; + private string|false $previousLocale; + private string $defaultLocale; protected function setUp(): void { @@ -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 f40d447f44..37448db510 100644 --- a/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php +++ b/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php @@ -18,7 +18,7 @@ class NumberToLocalizedStringTransformerTest extends TestCase { - private $defaultLocale; + private string $defaultLocale; private $initialTestCaseUseException; private $initialTestCaseErrorLevel; diff --git a/Tests/Extension/Core/DataTransformer/PercentToLocalizedStringTransformerTest.php b/Tests/Extension/Core/DataTransformer/PercentToLocalizedStringTransformerTest.php index 161aa81caf..1870173960 100644 --- a/Tests/Extension/Core/DataTransformer/PercentToLocalizedStringTransformerTest.php +++ b/Tests/Extension/Core/DataTransformer/PercentToLocalizedStringTransformerTest.php @@ -12,16 +12,13 @@ namespace Symfony\Component\Form\Tests\Extension\Core\DataTransformer; use PHPUnit\Framework\TestCase; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\Extension\Core\DataTransformer\PercentToLocalizedStringTransformer; use Symfony\Component\Intl\Util\IntlTestHelper; class PercentToLocalizedStringTransformerTest extends TestCase { - use ExpectDeprecationTrait; - - private $defaultLocale; + private string $defaultLocale; private $initialTestCaseUseException; private $initialTestCaseErrorLevel; @@ -87,16 +84,11 @@ public function testTransformWithScale() $this->assertEquals('12,34', $transformer->transform(0.1234)); } - /** - * @group legacy - */ - public function testReverseTransformWithScaleAndRoundingDisabled() + public function testReverseTransformWithScaleAndImplicitRounding() { - $this->expectDeprecation('Since symfony/form 5.1: Not passing a rounding mode to "Symfony\Component\Form\Extension\Core\DataTransformer\PercentToLocalizedStringTransformer::__construct()" is deprecated. Starting with Symfony 6.0 it will default to "\NumberFormatter::ROUND_HALFUP".'); - $transformer = new PercentToLocalizedStringTransformer(2, PercentToLocalizedStringTransformer::FRACTIONAL); - $this->assertEqualsWithDelta(0.0123456, $transformer->reverseTransform('1.23456'), \PHP_FLOAT_EPSILON); + $this->assertEquals(0.0123, $transformer->reverseTransform('1.23456')); } public function testReverseTransform() diff --git a/Tests/Extension/Core/DataTransformer/Traits/DateTimeEqualsTrait.php b/Tests/Extension/Core/DataTransformer/Traits/DateTimeEqualsTrait.php index 738c4d96b0..7b582df7ac 100644 --- a/Tests/Extension/Core/DataTransformer/Traits/DateTimeEqualsTrait.php +++ b/Tests/Extension/Core/DataTransformer/Traits/DateTimeEqualsTrait.php @@ -18,7 +18,7 @@ trait DateTimeEqualsTrait { public static function assertDateTimeEquals($expected, $actual) { - if ($expected instanceof \DateTime && $actual instanceof \DateTime) { + if ($expected instanceof \DateTimeInterface && $actual instanceof \DateTimeInterface) { $expected = $expected->format('c'); $actual = $actual->format('c'); } diff --git a/Tests/Extension/Core/DataTransformer/ValueToDuplicatesTransformerTest.php b/Tests/Extension/Core/DataTransformer/ValueToDuplicatesTransformerTest.php index 462472da98..358f21af2e 100644 --- a/Tests/Extension/Core/DataTransformer/ValueToDuplicatesTransformerTest.php +++ b/Tests/Extension/Core/DataTransformer/ValueToDuplicatesTransformerTest.php @@ -17,18 +17,13 @@ class ValueToDuplicatesTransformerTest extends TestCase { - private $transformer; + private ValueToDuplicatesTransformer $transformer; protected function setUp(): void { $this->transformer = new ValueToDuplicatesTransformer(['a', 'b', 'c']); } - protected function tearDown(): void - { - $this->transformer = null; - } - public function testTransform() { $output = [ diff --git a/Tests/Extension/Core/EventListener/MergeCollectionListenerArrayObjectTest.php b/Tests/Extension/Core/EventListener/MergeCollectionListenerArrayObjectTest.php index 47db5a0acd..80dfa22bd4 100644 --- a/Tests/Extension/Core/EventListener/MergeCollectionListenerArrayObjectTest.php +++ b/Tests/Extension/Core/EventListener/MergeCollectionListenerArrayObjectTest.php @@ -13,6 +13,7 @@ use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\Form\FormBuilder; +use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormFactoryBuilder; class MergeCollectionListenerArrayObjectTest extends MergeCollectionListenerTestCase @@ -22,7 +23,7 @@ protected function getData(array $data) return new \ArrayObject($data); } - protected function getBuilder($name = 'name') + protected function getBuilder($name = 'name'): FormBuilderInterface { return new FormBuilder($name, \ArrayObject::class, new EventDispatcher(), (new FormFactoryBuilder())->getFormFactory()); } diff --git a/Tests/Extension/Core/EventListener/MergeCollectionListenerArrayTest.php b/Tests/Extension/Core/EventListener/MergeCollectionListenerArrayTest.php index df382a0b50..9095951748 100644 --- a/Tests/Extension/Core/EventListener/MergeCollectionListenerArrayTest.php +++ b/Tests/Extension/Core/EventListener/MergeCollectionListenerArrayTest.php @@ -13,6 +13,7 @@ use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\Form\FormBuilder; +use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormFactoryBuilder; class MergeCollectionListenerArrayTest extends MergeCollectionListenerTestCase @@ -22,7 +23,7 @@ protected function getData(array $data) return $data; } - protected function getBuilder($name = 'name') + protected function getBuilder($name = 'name'): FormBuilderInterface { return new FormBuilder($name, null, new EventDispatcher(), (new FormFactoryBuilder())->getFormFactory()); } diff --git a/Tests/Extension/Core/EventListener/MergeCollectionListenerCustomArrayObjectTest.php b/Tests/Extension/Core/EventListener/MergeCollectionListenerCustomArrayObjectTest.php index a13a6c071a..b57eabc0bb 100644 --- a/Tests/Extension/Core/EventListener/MergeCollectionListenerCustomArrayObjectTest.php +++ b/Tests/Extension/Core/EventListener/MergeCollectionListenerCustomArrayObjectTest.php @@ -13,6 +13,7 @@ use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\Form\FormBuilder; +use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormFactoryBuilder; use Symfony\Component\Form\Tests\Fixtures\CustomArrayObject; @@ -23,7 +24,7 @@ protected function getData(array $data) return new CustomArrayObject($data); } - protected function getBuilder($name = 'name') + protected function getBuilder($name = 'name'): FormBuilderInterface { return new FormBuilder($name, 'Symfony\Component\Form\Tests\Fixtures\CustomArrayObject', new EventDispatcher(), (new FormFactoryBuilder())->getFormFactory()); } diff --git a/Tests/Extension/Core/EventListener/MergeCollectionListenerTestCase.php b/Tests/Extension/Core/EventListener/MergeCollectionListenerTestCase.php index 039108f4eb..7070db995b 100644 --- a/Tests/Extension/Core/EventListener/MergeCollectionListenerTestCase.php +++ b/Tests/Extension/Core/EventListener/MergeCollectionListenerTestCase.php @@ -14,27 +14,24 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener; +use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormInterface; abstract class MergeCollectionListenerTestCase extends TestCase { - protected $form; + protected FormInterface $form; protected function setUp(): void { $this->form = $this->getForm('axes'); } - protected function tearDown(): void - { - $this->form = null; - } - - abstract protected function getBuilder($name = 'name'); + abstract protected function getBuilder($name = 'name'): FormBuilderInterface; protected function getForm($name = 'name', $propertyPath = null) { - $propertyPath = $propertyPath ?: $name; + $propertyPath ??= $name; return $this->getBuilder($name)->setAttribute('property_path', $propertyPath)->getForm(); } diff --git a/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php b/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php index 9bcb22efe0..934460c8f9 100644 --- a/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php +++ b/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php @@ -14,32 +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; class ResizeFormListenerTest extends TestCase { - private $factory; - private $form; + private FormFactoryInterface $factory; + 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(); - } - - protected function tearDown(): void - { - $this->factory = null; - $this->form = null; + ->setDataMapper(new DataMapper()); } protected function getBuilder($name = 'name') @@ -47,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')); + + // initialize the form + $form->setData([1 => 'string', 2 => 'string']); - $this->assertFalse($this->form->has('0')); - $this->assertTrue($this->form->has('1')); - $this->assertTrue($this->form->has('2')); + $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($this->form->has('0')); - $this->assertTrue($this->form->has('1')); + $this->assertTrue($form->has('0')); + $this->assertFalse($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($this->form->has('0')); - $this->assertFalse($this->form->has('1')); + $this->assertTrue($form->has('0')); + $this->assertTrue($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()); @@ -190,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()); @@ -204,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()); @@ -223,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()); @@ -236,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()); @@ -249,48 +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)); + + $form = $this->builder->getForm(); - $this->assertEquals([0 => 'first'], $event->getData()); + $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 = function ($data) { - return 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)); - $this->assertEquals(['0' => ['name' => 'John']], $event->getData()); + $form = $this->builder->getForm(); + $form->submit(['0' => ['name' => 'John'], '1' => ['name' => '']]); + + $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 5238e2fd88..8293ec31bd 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 47028ac014..53e5c959ce 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,13 +19,14 @@ */ class BirthdayTypeTest extends DateTypeTest { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\BirthdayType'; + public const TESTED_TYPE = BirthdayType::class; public function testSetInvalidYearsOption() { $this->expectException(InvalidOptionsException::class); $this->factory->create(static::TESTED_TYPE, null, [ 'years' => 'bad value', + 'widget' => 'choice', ]); } } diff --git a/Tests/Extension/Core/Type/ButtonTypeTest.php b/Tests/Extension/Core/Type/ButtonTypeTest.php index 0125631c58..4825015d27 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 4682c40e39..69fd0fd601 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() { @@ -146,12 +147,8 @@ public function testCustomModelTransformer($data, $checked) { // present a binary status field as a checkbox $transformer = new CallbackTransformer( - function ($value) { - return 'checked' == $value; - }, - function ($value) { - return $value ? 'checked' : 'unchecked'; - } + fn ($value) => 'checked' == $value, + fn ($value) => $value ? 'checked' : 'unchecked' ); $form = $this->factory->createBuilder(static::TESTED_TYPE) diff --git a/Tests/Extension/Core/Type/ChoiceTypeTest.php b/Tests/Extension/Core/Type/ChoiceTypeTest.php index ccb6b78222..28810bbc7e 100644 --- a/Tests/Extension/Core/Type/ChoiceTypeTest.php +++ b/Tests/Extension/Core/Type/ChoiceTypeTest.php @@ -11,23 +11,20 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; 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\Form\Tests\Fixtures\ChoiceList\DeprecatedChoiceListFactory; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; class ChoiceTypeTest extends BaseTypeTestCase { - use ExpectDeprecationTrait; + public const TESTED_TYPE = ChoiceType::class; - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\ChoiceType'; - - private $choices = [ + private array $choices = [ 'Bernhard' => 'a', 'Fabien' => 'b', 'Kris' => 'c', @@ -35,19 +32,19 @@ class ChoiceTypeTest extends BaseTypeTestCase 'Roman' => 'e', ]; - private $scalarChoices = [ + private array $scalarChoices = [ 'Yes' => true, 'No' => false, 'n/a' => '', ]; - private $booleanChoicesWithNull = [ + private array $booleanChoicesWithNull = [ 'Yes' => true, 'No' => false, 'n/a' => null, ]; - private $numericChoicesFlipped = [ + private array $numericChoicesFlipped = [ 0 => 'Bernhard', 1 => 'Fabien', 2 => 'Kris', @@ -55,9 +52,9 @@ class ChoiceTypeTest extends BaseTypeTestCase 4 => 'Roman', ]; - private $objectChoices; + private array $objectChoices; - protected $groupedChoices = [ + protected array $groupedChoices = [ 'Symfony' => [ 'Bernhard' => 'a', 'Fabien' => 'b', @@ -82,13 +79,6 @@ protected function setUp(): void ]; } - protected function tearDown(): void - { - parent::tearDown(); - - $this->objectChoices = null; - } - public function testChoicesOptionExpectsArrayOrTraversable() { $this->expectException(InvalidOptionsException::class); @@ -105,6 +95,14 @@ public function testChoiceLoaderOptionExpectsChoiceLoaderInterface() ]); } + public function testPlaceholderAttrOptionExpectsArray() + { + $this->expectException(InvalidOptionsException::class); + $this->factory->create(static::TESTED_TYPE, null, [ + 'placeholder_attr' => new \stdClass(), + ]); + } + public function testChoiceListAndChoicesCanBeEmpty() { $this->assertInstanceOf(FormInterface::class, $this->factory->create(static::TESTED_TYPE, null, [])); @@ -194,15 +192,19 @@ public function testExpandedChoiceListWithBooleanAndNullValuesAndFalseAsPreSetDa public function testPlaceholderPresentOnNonRequiredExpandedSingleChoice() { + $placeholderAttr = ['attr' => 'value']; + $form = $this->factory->create(static::TESTED_TYPE, null, [ 'multiple' => false, 'expanded' => true, 'required' => false, 'choices' => $this->choices, + 'placeholder_attr' => $placeholderAttr, ]); $this->assertArrayHasKey('placeholder', $form); $this->assertCount(\count($this->choices) + 1, $form, 'Each choice should become a new field'); + $this->assertSame($placeholderAttr, $form->createView()->children['placeholder']->vars['attr']); } public function testPlaceholderNotPresentIfRequired() @@ -536,9 +538,7 @@ public function testSubmitSingleNonExpandedEmptyExplicitEmptyChoice() 'choices' => [ 'Empty' => 'EMPTY_CHOICE', ], - 'choice_value' => function () { - return ''; - }, + 'choice_value' => fn () => '', ]); $form->submit(''); @@ -1674,36 +1674,40 @@ public function testPlaceholderIsEmptyStringByDefaultIfNotRequired() /** * @dataProvider getOptionsWithPlaceholder */ - public function testPassPlaceholderToView($multiple, $expanded, $required, $placeholder, $viewValue) + public function testPassPlaceholderToView($multiple, $expanded, $required, $placeholder, $placeholderViewValue, $placeholderAttr, $placeholderAttrViewValue) { $view = $this->factory->create(static::TESTED_TYPE, null, [ 'multiple' => $multiple, 'expanded' => $expanded, 'required' => $required, 'placeholder' => $placeholder, + 'placeholder_attr' => $placeholderAttr, 'choices' => $this->choices, ]) ->createView(); - $this->assertSame($viewValue, $view->vars['placeholder']); + $this->assertSame($placeholderViewValue, $view->vars['placeholder']); + $this->assertSame($placeholderAttrViewValue, $view->vars['placeholder_attr']); $this->assertFalse($view->vars['placeholder_in_choices']); } /** * @dataProvider getOptionsWithPlaceholder */ - public function testDontPassPlaceholderIfContainedInChoices($multiple, $expanded, $required, $placeholder, $viewValue) + public function testDontPassPlaceholderIfContainedInChoices($multiple, $expanded, $required, $placeholder, $placeholderViewValue, $placeholderAttr, $placeholderAttrViewValue) { $view = $this->factory->create(static::TESTED_TYPE, null, [ 'multiple' => $multiple, 'expanded' => $expanded, 'required' => $required, 'placeholder' => $placeholder, + 'placeholder_attr' => $placeholderAttr, 'choices' => ['Empty' => '', 'A' => 'a'], ]) ->createView(); $this->assertNull($view->vars['placeholder']); + $this->assertSame([], $view->vars['placeholder_attr']); $this->assertTrue($view->vars['placeholder_in_choices']); } @@ -1711,43 +1715,43 @@ public static function getOptionsWithPlaceholder() { return [ // single non-expanded - [false, false, false, 'foobar', 'foobar'], - [false, false, false, '', ''], - [false, false, false, null, null], - [false, false, false, false, null], - [false, false, true, 'foobar', 'foobar'], - [false, false, true, '', ''], - [false, false, true, null, null], - [false, false, true, false, null], + [false, false, false, 'foobar', 'foobar', ['attr' => 'value'], ['attr' => 'value']], + [false, false, false, '', '', ['attr' => 'value'], ['attr' => 'value']], + [false, false, false, null, null, ['attr' => 'value'], []], + [false, false, false, false, null, ['attr' => 'value'], []], + [false, false, true, 'foobar', 'foobar', ['attr' => 'value'], ['attr' => 'value']], + [false, false, true, '', '', ['attr' => 'value'], ['attr' => 'value']], + [false, false, true, null, null, ['attr' => 'value'], []], + [false, false, true, false, null, ['attr' => 'value'], []], // single expanded - [false, true, false, 'foobar', 'foobar'], + [false, true, false, 'foobar', 'foobar', ['attr' => 'value'], ['attr' => 'value']], // radios should never have an empty label - [false, true, false, '', 'None'], - [false, true, false, null, null], - [false, true, false, false, null], + [false, true, false, '', 'None', ['attr' => 'value'], ['attr' => 'value']], + [false, true, false, null, null, ['attr' => 'value'], []], + [false, true, false, false, null, ['attr' => 'value'], []], // required radios should never have a placeholder - [false, true, true, 'foobar', null], - [false, true, true, '', null], - [false, true, true, null, null], - [false, true, true, false, null], + [false, true, true, 'foobar', null, ['attr' => 'value'], []], + [false, true, true, '', null, ['attr' => 'value'], []], + [false, true, true, null, null, ['attr' => 'value'], []], + [false, true, true, false, null, ['attr' => 'value'], []], // multiple non-expanded - [true, false, false, 'foobar', null], - [true, false, false, '', null], - [true, false, false, null, null], - [true, false, false, false, null], - [true, false, true, 'foobar', null], - [true, false, true, '', null], - [true, false, true, null, null], - [true, false, true, false, null], + [true, false, false, 'foobar', null, ['attr' => 'value'], []], + [true, false, false, '', null, ['attr' => 'value'], []], + [true, false, false, null, null, ['attr' => 'value'], []], + [true, false, false, false, null, ['attr' => 'value'], []], + [true, false, true, 'foobar', null, ['attr' => 'value'], []], + [true, false, true, '', null, ['attr' => 'value'], []], + [true, false, true, null, null, ['attr' => 'value'], []], + [true, false, true, false, null, ['attr' => 'value'], []], // multiple expanded - [true, true, false, 'foobar', null], - [true, true, false, '', null], - [true, true, false, null, null], - [true, true, false, false, null], - [true, true, true, 'foobar', null], - [true, true, true, '', null], - [true, true, true, null, null], - [true, true, true, false, null], + [true, true, false, 'foobar', null, ['attr' => 'value'], []], + [true, true, false, '', null, ['attr' => 'value'], []], + [true, true, false, null, null, ['attr' => 'value'], []], + [true, true, false, false, null, ['attr' => 'value'], []], + [true, true, true, 'foobar', null, ['attr' => 'value'], []], + [true, true, true, '', null, ['attr' => 'value'], []], + [true, true, true, null, null, ['attr' => 'value'], []], + [true, true, true, false, null, ['attr' => 'value'], []], ]; } @@ -1893,8 +1897,8 @@ public function testInitializeWithEmptyChoices() { $this->assertInstanceOf( FormInterface::class, $this->factory->createNamed('name', static::TESTED_TYPE, null, [ - 'choices' => [], - ])); + 'choices' => [], + ])); } public function testInitializeWithDefaultObjectChoice() @@ -2213,9 +2217,7 @@ public function testFilteredChoices() { $form = $this->factory->create(static::TESTED_TYPE, null, [ 'choices' => $this->choices, - 'choice_filter' => function ($choice) { - return \in_array($choice, range('a', 'c'), true); - }, + 'choice_filter' => fn ($choice) => \in_array($choice, range('a', 'c'), true), ]); $this->assertEquals([ @@ -2229,9 +2231,7 @@ public function testFilteredGroupedChoices() { $form = $this->factory->create(static::TESTED_TYPE, null, [ 'choices' => $this->groupedChoices, - 'choice_filter' => function ($choice) { - return \in_array($choice, range('a', 'c'), true); - }, + 'choice_filter' => fn ($choice) => \in_array($choice, range('a', 'c'), true), ]); $this->assertEquals(['Symfony' => new ChoiceGroupView('Symfony', [ @@ -2244,12 +2244,8 @@ public function testFilteredGroupedChoices() public function testFilteredChoiceLoader() { $form = $this->factory->create(static::TESTED_TYPE, null, [ - 'choice_loader' => new CallbackChoiceLoader(function () { - return $this->choices; - }), - 'choice_filter' => function ($choice) { - return \in_array($choice, range('a', 'c'), true); - }, + 'choice_loader' => new CallbackChoiceLoader(fn () => $this->choices), + 'choice_filter' => fn ($choice) => \in_array($choice, range('a', 'c'), true), ]); $this->assertEquals([ @@ -2259,23 +2255,9 @@ public function testFilteredChoiceLoader() ], $form->createView()->vars['choices']); } - /** - * @group legacy - */ - public function testUsingDeprecatedChoiceListFactory() - { - $this->expectDeprecation('The "Symfony\Component\Form\Tests\Fixtures\ChoiceList\DeprecatedChoiceListFactory::createListFromChoices()" method will require a new "callable|null $filter" argument in the next major version of its interface "Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface", not defining it is deprecated.'); - $this->expectDeprecation('The "Symfony\Component\Form\Tests\Fixtures\ChoiceList\DeprecatedChoiceListFactory::createListFromLoader()" method will require a new "callable|null $filter" argument in the next major version of its interface "Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface", not defining it is deprecated.'); - $this->expectDeprecation('Since symfony/form 5.1: Not defining a third parameter "callable|null $filter" in "Symfony\Component\Form\Tests\Fixtures\ChoiceList\DeprecatedChoiceListFactory::createListFromChoices()" is deprecated.'); - - new ChoiceType(new DeprecatedChoiceListFactory()); - } - public function testWithSameLoaderAndDifferentChoiceValueCallbacks() { - $choiceLoader = new CallbackChoiceLoader(function () { - return [1, 2, 3]; - }); + $choiceLoader = new CallbackChoiceLoader(fn () => [1, 2, 3]); $view = $this->factory->create(FormTypeTest::TESTED_TYPE) ->add('choice_one', self::TESTED_TYPE, [ @@ -2283,9 +2265,7 @@ public function testWithSameLoaderAndDifferentChoiceValueCallbacks() ]) ->add('choice_two', self::TESTED_TYPE, [ 'choice_loader' => $choiceLoader, - 'choice_value' => function ($choice) { - return $choice ? (string) $choice * 10 : ''; - }, + 'choice_value' => fn ($choice) => $choice ? (string) $choice * 10 : '', ]) ->createView() ; @@ -2298,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 9e14410059..f60c6664c6 100644 --- a/Tests/Extension/Core/Type/ChoiceTypeTranslationTest.php +++ b/Tests/Extension/Core/Type/ChoiceTypeTranslationTest.php @@ -12,14 +12,15 @@ 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 $choices = [ + private array $choices = [ 'Bernhard' => 'a', 'Fabien' => 'b', 'Kris' => 'c', @@ -27,13 +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(function ($key, $params) { - return 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 d0db40fd79..95e1d9ca70 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,9 +121,7 @@ public function testResizedDownWithDeleteEmptyCallable() $form = $this->factory->create(static::TESTED_TYPE, null, [ 'entry_type' => AuthorType::class, 'allow_delete' => true, - 'delete_empty' => function (?Author $obj = null) { - return null === $obj || empty($obj->firstName); - }, + 'delete_empty' => fn (?Author $obj = null) => null === $obj || !$obj->firstName, ]); $form->setData([new Author('Bob'), new Author('Alice')]); @@ -143,9 +142,7 @@ public function testResizedDownIfSubmittedWithCompoundEmptyDataDeleteEmptyAndNoD 'entry_options' => ['data_class' => null], 'allow_add' => true, 'allow_delete' => true, - 'delete_empty' => function ($author) { - return empty($author['firstName']); - }, + 'delete_empty' => fn ($author) => empty($author['firstName']), ]); $form->setData([['firstName' => 'first', 'lastName' => 'last']]); $form->submit([ @@ -430,6 +427,46 @@ public function testPrototypeNotOverrideRequiredByEntryOptionsInFavorOfParent() $this->assertFalse($child->createView()->vars['prototype']->vars['required'], '"Prototype" should not be required'); } + public function testPrototypeOptionsOverrideEntryOptions() + { + $form = $this->factory->create(static::TESTED_TYPE, [], [ + 'allow_add' => true, + 'prototype' => true, + 'entry_type' => TextTypeTest::TESTED_TYPE, + 'entry_options' => [ + 'help' => null, + ], + 'prototype_options' => [ + 'help' => 'foo', + ], + ]); + + $this->assertSame('foo', $form->createView()->vars['prototype']->vars['help']); + } + + public function testPrototypeOptionsAppliedToNewFields() + { + $form = $this->factory->create(static::TESTED_TYPE, ['first'], [ + 'allow_add' => true, + 'prototype' => true, + 'entry_type' => TextTypeTest::TESTED_TYPE, + 'entry_options' => [ + 'disabled' => true, + ], + 'prototype_options' => [ + 'disabled' => false, + ], + ]); + + $form->submit(['first_changed', 'second']); + + $this->assertTrue($form->has('0')); + $this->assertTrue($form->has('1')); + $this->assertSame('first', $form[0]->getData()); + $this->assertSame('second', $form[1]->getData()); + $this->assertSame(['first', 'second'], $form->getData()); + } + public function testEntriesBlockPrefixes() { $collectionView = $this->factory->createNamed('fields', static::TESTED_TYPE, [''], [ diff --git a/Tests/Extension/Core/Type/CountryTypeTest.php b/Tests/Extension/Core/Type/CountryTypeTest.php index 57146e1ecf..44073ef7b9 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 51aaf6b372..3e8a53dfb2 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 64f116313b..5067bb05e7 100644 --- a/Tests/Extension/Core/Type/DateTimeTypeTest.php +++ b/Tests/Extension/Core/Type/DateTimeTypeTest.php @@ -11,14 +11,16 @@ 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 $defaultLocale; + private string $defaultLocale; protected function setUp(): void { @@ -154,7 +156,7 @@ public function testSubmitWithoutMinutes() 'with_minutes' => false, ]); - $form->setData(new \DateTime()); + $form->setData(new \DateTime('now', new \DateTimeZone('UTC'))); $input = [ 'date' => [ @@ -184,7 +186,7 @@ public function testSubmitWithSeconds() 'with_seconds' => true, ]); - $form->setData(new \DateTime()); + $form->setData(new \DateTime('now', new \DateTimeZone('UTC'))); $input = [ 'date' => [ @@ -252,7 +254,7 @@ public function testSubmitDifferentTimezonesDateTime() $outputTime->setTimezone(new \DateTimeZone('America/New_York')); $this->assertEquals($outputTime, $form->getData()); - $this->assertEquals('2010-06-02T03:04:00', $form->getViewData()); + $this->assertEquals('2010-06-02T03:04', $form->getViewData()); } public function testSubmitDifferentTimezonesDateTimeImmutable() @@ -272,7 +274,7 @@ public function testSubmitDifferentTimezonesDateTimeImmutable() $this->assertInstanceOf(\DateTimeImmutable::class, $form->getData()); $this->assertEquals($outputTime, $form->getData()); - $this->assertEquals('2010-06-02T03:04:00', $form->getViewData()); + $this->assertEquals('2010-06-02T03:04', $form->getViewData()); } public function testSubmitStringSingleText() @@ -287,7 +289,7 @@ public function testSubmitStringSingleText() $form->submit('2010-06-02T03:04:00'); $this->assertEquals('2010-06-02 03:04:00', $form->getData()); - $this->assertEquals('2010-06-02T03:04:00', $form->getViewData()); + $this->assertEquals('2010-06-02T03:04', $form->getViewData()); } public function testSubmitStringSingleTextWithSeconds() @@ -330,7 +332,7 @@ public function testInitializeWithDateTime() { // Throws an exception if "data_class" option is not explicitly set // to null in the type - $this->assertInstanceOf(FormInterface::class, $this->factory->create(static::TESTED_TYPE, new \DateTime())); + $this->assertInstanceOf(FormInterface::class, $this->factory->create(static::TESTED_TYPE, new \DateTime(), ['widget' => 'choice'])); } public function testSingleTextWidgetShouldUseTheRightInputType() @@ -348,6 +350,7 @@ public function testPassDefaultPlaceholderToViewIfNotRequired() $view = $this->factory->create(static::TESTED_TYPE, null, [ 'required' => false, 'with_seconds' => true, + 'widget' => 'choice', ]) ->createView(); @@ -364,6 +367,7 @@ public function testPassNoPlaceholderToViewIfRequired() $view = $this->factory->create(static::TESTED_TYPE, null, [ 'required' => true, 'with_seconds' => true, + 'widget' => 'choice', ]) ->createView(); @@ -380,6 +384,7 @@ public function testPassPlaceholderAsString() $view = $this->factory->create(static::TESTED_TYPE, null, [ 'placeholder' => 'Empty', 'with_seconds' => true, + 'widget' => 'choice', ]) ->createView(); @@ -403,6 +408,7 @@ public function testPassPlaceholderAsArray() 'second' => 'Empty second', ], 'with_seconds' => true, + 'widget' => 'choice', ]) ->createView(); @@ -425,6 +431,7 @@ public function testPassPlaceholderAsPartialArrayAddEmptyIfNotRequired() 'second' => 'Empty second', ], 'with_seconds' => true, + 'widget' => 'choice', ]) ->createView(); @@ -447,6 +454,7 @@ public function testPassPlaceholderAsPartialArrayAddNullIfRequired() 'second' => 'Empty second', ], 'with_seconds' => true, + 'widget' => 'choice', ]) ->createView(); @@ -536,7 +544,7 @@ public function testSingleTextWidgetWithCustomNonHtml5Format() public function testDateTypeChoiceErrorsBubbleUp() { $error = new FormError('Invalid!'); - $form = $this->factory->create(static::TESTED_TYPE, null); + $form = $this->factory->create(static::TESTED_TYPE, null, ['widget' => 'choice']); $form['date']->addError($error); @@ -549,6 +557,7 @@ public function testDateTypeSingleTextErrorsBubbleUp() $error = new FormError('Invalid!'); $form = $this->factory->create(static::TESTED_TYPE, null, [ 'date_widget' => 'single_text', + 'time_widget' => 'choice', ]); $form['date']->addError($error); @@ -560,7 +569,7 @@ public function testDateTypeSingleTextErrorsBubbleUp() public function testTimeTypeChoiceErrorsBubbleUp() { $error = new FormError('Invalid!'); - $form = $this->factory->create(static::TESTED_TYPE, null); + $form = $this->factory->create(static::TESTED_TYPE, null, ['widget' => 'choice']); $form['time']->addError($error); @@ -573,6 +582,7 @@ public function testTimeTypeSingleTextErrorsBubbleUp() $error = new FormError('Invalid!'); $form = $this->factory->create(static::TESTED_TYPE, null, [ 'time_widget' => 'single_text', + 'date_widget' => 'choice', ]); $form['time']->addError($error); @@ -585,6 +595,7 @@ public function testPassDefaultChoiceTranslationDomain() { $form = $this->factory->create(static::TESTED_TYPE, null, [ 'with_seconds' => true, + 'widget' => 'choice', ]); $view = $form->createView(); @@ -602,6 +613,7 @@ public function testPassChoiceTranslationDomainAsString() $form = $this->factory->create(static::TESTED_TYPE, null, [ 'choice_translation_domain' => 'messages', 'with_seconds' => true, + 'widget' => 'choice', ]); $view = $form->createView(); @@ -623,6 +635,7 @@ public function testPassChoiceTranslationDomainAsArray() 'second' => 'test', ], 'with_seconds' => true, + 'widget' => 'choice', ]); $view = $form->createView(); @@ -675,6 +688,7 @@ public function testSubmitNullUsesDefaultEmptyData($emptyData = [], $expectedDat { $form = $this->factory->create(static::TESTED_TYPE, null, [ 'empty_data' => $emptyData, + 'widget' => 'choice', ]); $form->submit(null); @@ -710,12 +724,10 @@ public function testSubmitNullUsesDateEmptyData($widget, $emptyData, $expectedDa public static function provideEmptyData() { $expectedData = \DateTime::createFromFormat('Y-m-d H:i', '2018-11-11 21:23'); - $lazyEmptyData = static function (FormInterface $form) { - return $form->getConfig()->getCompound() ? ['date' => ['year' => '2018', 'month' => '11', 'day' => '11'], 'time' => ['hour' => '21', 'minute' => '23']] : '2018-11-11T21:23:00'; - }; + $lazyEmptyData = static fn (FormInterface $form) => $form->getConfig()->getCompound() ? ['date' => ['year' => '2018', 'month' => '11', 'day' => '11'], 'time' => ['hour' => '21', 'minute' => '23']] : '2018-11-11T21:23'; return [ - 'Simple field' => ['single_text', '2018-11-11T21:23:00', $expectedData], + 'Simple field' => ['single_text', '2018-11-11T21:23', $expectedData], 'Compound text field' => ['text', ['date' => ['year' => '2018', 'month' => '11', 'day' => '11'], 'time' => ['hour' => '21', 'minute' => '23']], $expectedData], 'Compound choice field' => ['choice', ['date' => ['year' => '2018', 'month' => '11', 'day' => '11'], 'time' => ['hour' => '21', 'minute' => '23']], $expectedData], 'Simple field lazy' => ['single_text', $lazyEmptyData, $expectedData], @@ -738,4 +750,30 @@ public function testSubmitStringWithCustomInputFormat() $this->assertSame('14/01/2018 21:29:00 +00:00', $form->getData()); } + + public function testDateTimeInputTimezoneNotMatchingModelTimezone() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Using a "DateTime" instance with a timezone ("UTC") not matching the configured model timezone "Europe/Berlin" is not supported.'); + + $this->factory->create(static::TESTED_TYPE, new \DateTime('now', new \DateTimeZone('UTC')), [ + 'model_timezone' => 'Europe/Berlin', + ]); + } + + public function testDateTimeImmutableInputTimezoneNotMatchingModelTimezone() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Using a "DateTimeImmutable" instance with a timezone ("UTC") not matching the configured model timezone "Europe/Berlin" is not supported.'); + + $this->factory->create(static::TESTED_TYPE, new \DateTimeImmutable('now', new \DateTimeZone('UTC')), [ + 'input' => 'datetime_immutable', + 'model_timezone' => 'Europe/Berlin', + ]); + } + + protected function getTestOptions(): array + { + return ['widget' => 'choice']; + } } diff --git a/Tests/Extension/Core/Type/DateTypeTest.php b/Tests/Extension/Core/Type/DateTypeTest.php index ee3ceb9585..5f4f896b5d 100644 --- a/Tests/Extension/Core/Type/DateTypeTest.php +++ b/Tests/Extension/Core/Type/DateTypeTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; 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; @@ -20,10 +22,10 @@ class DateTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\DateType'; + public const TESTED_TYPE = DateType::class; - private $defaultTimezone; - private $defaultLocale; + private string $defaultTimezone; + private string $defaultLocale; protected function setUp(): void { @@ -51,6 +53,7 @@ public function testInvalidInputOption() $this->expectException(InvalidOptionsException::class); $this->factory->create(static::TESTED_TYPE, null, [ 'input' => 'fake_input', + 'widget' => 'choice', ]); } @@ -400,6 +403,7 @@ public function testDatePatternWithFormatOption($format, $pattern) { $view = $this->factory->create(static::TESTED_TYPE, null, [ 'format' => $format, + 'widget' => 'choice', ]) ->createView(); @@ -437,6 +441,7 @@ public function testThrowExceptionIfFormatDoesNotContainYearMonthAndDay() $this->factory->create(static::TESTED_TYPE, null, [ 'months' => [6, 7], 'format' => 'yy', + 'widget' => 'choice', ]); } @@ -456,6 +461,7 @@ public function testThrowExceptionIfFormatIsNoConstant() $this->expectException(InvalidOptionsException::class); $this->factory->create(static::TESTED_TYPE, null, [ 'format' => 105, + 'widget' => 'choice', ]); } @@ -464,6 +470,7 @@ public function testThrowExceptionIfFormatIsInvalid() $this->expectException(InvalidOptionsException::class); $this->factory->create(static::TESTED_TYPE, null, [ 'format' => [], + 'widget' => 'choice', ]); } @@ -472,6 +479,7 @@ public function testThrowExceptionIfYearsIsInvalid() $this->expectException(InvalidOptionsException::class); $this->factory->create(static::TESTED_TYPE, null, [ 'years' => 'bad value', + 'widget' => 'choice', ]); } @@ -480,6 +488,7 @@ public function testThrowExceptionIfMonthsIsInvalid() $this->expectException(InvalidOptionsException::class); $this->factory->create(static::TESTED_TYPE, null, [ 'months' => 'bad value', + 'widget' => 'choice', ]); } @@ -488,6 +497,7 @@ public function testThrowExceptionIfDaysIsInvalid() $this->expectException(InvalidOptionsException::class); $this->factory->create(static::TESTED_TYPE, null, [ 'days' => 'bad value', + 'widget' => 'choice', ]); } @@ -544,6 +554,7 @@ public function testYearsOption() { $form = $this->factory->create(static::TESTED_TYPE, null, [ 'years' => [2010, 2011], + 'widget' => 'choice', ]); $view = $form->createView(); @@ -560,6 +571,7 @@ public function testMonthsOption() $form = $this->factory->create(static::TESTED_TYPE, null, [ 'months' => [6, 7], 'format' => \IntlDateFormatter::SHORT, + 'widget' => 'choice', ]); $view = $form->createView(); @@ -580,6 +592,7 @@ public function testMonthsOptionShortFormat() $form = $this->factory->create(static::TESTED_TYPE, null, [ 'months' => [1, 4], 'format' => 'dd.MMM.yy', + 'widget' => 'choice', ]); $view = $form->createView(); @@ -600,6 +613,7 @@ public function testMonthsOptionLongFormat() $view = $this->factory->create(static::TESTED_TYPE, null, [ 'months' => [1, 4], 'format' => 'dd.MMMM.yy', + 'widget' => 'choice', ]) ->createView(); @@ -619,6 +633,7 @@ public function testMonthsOptionLongFormatWithDifferentTimezone() $view = $this->factory->create(static::TESTED_TYPE, null, [ 'months' => [1, 4], 'format' => 'dd.MMMM.yy', + 'widget' => 'choice', ]) ->createView(); @@ -633,6 +648,7 @@ public function testIsDayWithinRangeReturnsTrueIfWithin() \Locale::setDefault('en'); $view = $this->factory->create(static::TESTED_TYPE, null, [ 'days' => [6, 7], + 'widget' => 'choice', ]) ->createView(); @@ -661,7 +677,7 @@ public function testIsSynchronizedReturnsTrueIfChoiceAndCompletelyEmpty() public function testIsSynchronizedReturnsTrueIfChoiceAndCompletelyFilled() { - $form = $this->factory->create(static::TESTED_TYPE, new \DateTime(), [ + $form = $this->factory->create(static::TESTED_TYPE, new \DateTime('now', new \DateTimeZone('UTC')), [ 'model_timezone' => 'UTC', 'view_timezone' => 'UTC', 'widget' => 'choice', @@ -700,7 +716,7 @@ public function testPassDatePatternToView() \Locale::setDefault('de_AT'); - $view = $this->factory->create(static::TESTED_TYPE) + $view = $this->factory->create(static::TESTED_TYPE, null, ['widget' => 'choice']) ->createView(); $this->assertSame('{{ day }}{{ month }}{{ year }}', $view->vars['date_pattern']); @@ -715,6 +731,7 @@ public function testPassDatePatternToViewDifferentFormat() $view = $this->factory->create(static::TESTED_TYPE, null, [ 'format' => \IntlDateFormatter::LONG, + 'widget' => 'choice', ]) ->createView(); @@ -725,6 +742,7 @@ public function testPassDatePatternToViewDifferentPattern() { $view = $this->factory->create(static::TESTED_TYPE, null, [ 'format' => 'MMyyyydd', + 'widget' => 'choice', ]) ->createView(); @@ -735,6 +753,7 @@ public function testPassDatePatternToViewDifferentPatternWithSeparators() { $view = $this->factory->create(static::TESTED_TYPE, null, [ 'format' => 'MM*yyyy*dd', + 'widget' => 'choice', ]) ->createView(); @@ -761,6 +780,7 @@ public function testDatePatternFormatWithQuotedStrings() $view = $this->factory->create(static::TESTED_TYPE, null, [ // EEEE, d 'de' MMMM 'de' y 'format' => \IntlDateFormatter::FULL, + 'widget' => 'choice', ]) ->createView(); @@ -781,7 +801,7 @@ public function testInitializeWithDateTime() { // Throws an exception if "data_class" option is not explicitly set // to null in the type - $this->assertInstanceOf(FormInterface::class, $this->factory->create(static::TESTED_TYPE, new \DateTime())); + $this->assertInstanceOf(FormInterface::class, $this->factory->create(static::TESTED_TYPE, new \DateTime(), ['widget' => 'choice'])); } public function testSingleTextWidgetShouldUseTheRightInputType() @@ -798,6 +818,7 @@ public function testPassDefaultPlaceholderToViewIfNotRequired() { $view = $this->factory->create(static::TESTED_TYPE, null, [ 'required' => false, + 'widget' => 'choice', ]) ->createView(); @@ -810,6 +831,7 @@ public function testPassNoPlaceholderToViewIfRequired() { $view = $this->factory->create(static::TESTED_TYPE, null, [ 'required' => true, + 'widget' => 'choice', ]) ->createView(); @@ -821,6 +843,7 @@ public function testPassNoPlaceholderToViewIfRequired() public function testPassPlaceholderAsString() { $view = $this->factory->create(static::TESTED_TYPE, null, [ + 'widget' => 'choice', 'placeholder' => 'Empty', ]) ->createView(); @@ -833,6 +856,7 @@ public function testPassPlaceholderAsString() public function testPassPlaceholderAsArray() { $view = $this->factory->create(static::TESTED_TYPE, null, [ + 'widget' => 'choice', 'placeholder' => [ 'year' => 'Empty year', 'month' => 'Empty month', @@ -849,6 +873,7 @@ public function testPassPlaceholderAsArray() public function testPassPlaceholderAsPartialArrayAddEmptyIfNotRequired() { $view = $this->factory->create(static::TESTED_TYPE, null, [ + 'widget' => 'choice', 'required' => false, 'placeholder' => [ 'year' => 'Empty year', @@ -865,6 +890,7 @@ public function testPassPlaceholderAsPartialArrayAddEmptyIfNotRequired() public function testPassPlaceholderAsPartialArrayAddNullIfRequired() { $view = $this->factory->create(static::TESTED_TYPE, null, [ + 'widget' => 'choice', 'required' => true, 'placeholder' => [ 'year' => 'Empty year', @@ -977,6 +1003,7 @@ public function testDayErrorsBubbleUp($widget) public function testYears() { $view = $this->factory->create(static::TESTED_TYPE, null, [ + 'widget' => 'choice', 'years' => [1900, 2000, 2040], ]) ->createView(); @@ -991,7 +1018,7 @@ public function testYears() public function testPassDefaultChoiceTranslationDomain() { - $form = $this->factory->create(static::TESTED_TYPE); + $form = $this->factory->create(static::TESTED_TYPE, null, ['widget' => 'choice']); $view = $form->createView(); $this->assertFalse($view['year']->vars['choice_translation_domain']); @@ -1002,6 +1029,7 @@ public function testPassDefaultChoiceTranslationDomain() public function testPassChoiceTranslationDomainAsString() { $form = $this->factory->create(static::TESTED_TYPE, null, [ + 'widget' => 'choice', 'choice_translation_domain' => 'messages', ]); @@ -1014,6 +1042,7 @@ public function testPassChoiceTranslationDomainAsString() public function testPassChoiceTranslationDomainAsArray() { $form = $this->factory->create(static::TESTED_TYPE, null, [ + 'widget' => 'choice', 'choice_translation_domain' => [ 'year' => 'foo', 'day' => 'test', @@ -1046,6 +1075,7 @@ public function testSubmitNullWithSingleText() public function testSubmitNullUsesDefaultEmptyData($emptyData = [], $expectedData = null) { $form = $this->factory->create(static::TESTED_TYPE, null, [ + 'widget' => 'choice', 'empty_data' => $emptyData, ]); $form->submit(null); @@ -1079,9 +1109,7 @@ public function testSubmitNullUsesDateEmptyData($widget, $emptyData, $expectedDa public static function provideEmptyData() { $expectedData = \DateTime::createFromFormat('Y-m-d H:i:s', '2018-11-11 00:00:00'); - $lazyEmptyData = static function (FormInterface $form) { - return $form->getConfig()->getCompound() ? ['year' => '2018', 'month' => '11', 'day' => '11'] : '2018-11-11'; - }; + $lazyEmptyData = static fn (FormInterface $form) => $form->getConfig()->getCompound() ? ['year' => '2018', 'month' => '11', 'day' => '11'] : '2018-11-11'; return [ 'Simple field' => ['single_text', '2018-11-11', $expectedData], @@ -1107,4 +1135,68 @@ public function testSubmitStringWithCustomInputFormat() $this->assertSame('14/01/2018', $form->getData()); } + + public function testDateTimeInputTimezoneNotMatchingModelTimezone() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Using a "DateTime" instance with a timezone ("UTC") not matching the configured model timezone "Europe/Berlin" is not supported.'); + + $this->factory->create(static::TESTED_TYPE, new \DateTime('now', new \DateTimeZone('UTC')), [ + 'model_timezone' => 'Europe/Berlin', + ]); + } + + public function testDateTimeImmutableInputTimezoneNotMatchingModelTimezone() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Using a "DateTimeImmutable" instance with a timezone ("UTC") not matching the configured model timezone "Europe/Berlin" is not supported.'); + + $this->factory->create(static::TESTED_TYPE, new \DateTimeImmutable('now', new \DateTimeZone('UTC')), [ + 'input' => 'datetime_immutable', + 'model_timezone' => 'Europe/Berlin', + ]); + } + + 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/EnumTypeTest.php b/Tests/Extension/Core/Type/EnumTypeTest.php index 77c1c62b04..0458720691 100644 --- a/Tests/Extension/Core/Type/EnumTypeTest.php +++ b/Tests/Extension/Core/Type/EnumTypeTest.php @@ -16,12 +16,12 @@ use Symfony\Component\Form\Tests\Fixtures\Answer; use Symfony\Component\Form\Tests\Fixtures\Number; use Symfony\Component\Form\Tests\Fixtures\Suit; +use Symfony\Component\Form\Tests\Fixtures\TranslatableTextAlign; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; +use Symfony\Component\Translation\IdentityTranslator; +use Symfony\Contracts\Translation\TranslatableInterface; -/** - * @requires PHP 8.1 - */ class EnumTypeTest extends BaseTypeTestCase { public const TESTED_TYPE = EnumType::class; @@ -94,13 +94,13 @@ public static function provideSingleSubmitData(): iterable yield 'string backed' => [ Suit::class, - (Suit::Spades)->value, + Suit::Spades->value, Suit::Spades, ]; yield 'integer backed' => [ Number::class, - (string) (Number::Two)->value, + (string) Number::Two->value, Number::Two, ]; } @@ -134,7 +134,7 @@ public function testSubmitNull($expected = null, $norm = null, $view = null) public function testSubmitNullUsesDefaultEmptyData($emptyData = 'empty', $expectedData = null) { - $emptyData = (Suit::Hearts)->value; + $emptyData = Suit::Hearts->value; $form = $this->factory->create($this->getTestedType(), null, [ 'class' => Suit::class, @@ -154,7 +154,7 @@ public function testSubmitMultipleChoiceWithEmptyData() 'multiple' => true, 'expanded' => false, 'class' => Suit::class, - 'empty_data' => [(Suit::Diamonds)->value], + 'empty_data' => [Suit::Diamonds->value], ]); $form->submit(null); @@ -168,7 +168,7 @@ public function testSubmitSingleChoiceExpandedWithEmptyData() 'multiple' => false, 'expanded' => true, 'class' => Suit::class, - 'empty_data' => (Suit::Hearts)->value, + 'empty_data' => Suit::Hearts->value, ]); $form->submit(null); @@ -182,7 +182,7 @@ public function testSubmitMultipleChoiceExpandedWithEmptyData() 'multiple' => true, 'expanded' => true, 'class' => Suit::class, - 'empty_data' => [(Suit::Spades)->value], + 'empty_data' => [Suit::Spades->value], ]); $form->submit(null); @@ -236,13 +236,13 @@ public static function provideMultiSubmitData(): iterable yield 'string backed' => [ Suit::class, - [(Suit::Hearts)->value, (Suit::Spades)->value], + [Suit::Hearts->value, Suit::Spades->value], [Suit::Hearts, Suit::Spades], ]; yield 'integer backed' => [ Number::class, - [(string) (Number::Two)->value, (string) (Number::Three)->value], + [(string) Number::Two->value, (string) Number::Three->value], [Number::Two, Number::Three], ]; } @@ -260,6 +260,20 @@ public function testChoiceLabel() $this->assertSame('Yes', $view->children[0]->vars['label']); } + public function testChoiceLabelTranslatable() + { + $form = $this->factory->create($this->getTestedType(), null, [ + 'multiple' => false, + 'expanded' => true, + 'class' => TranslatableTextAlign::class, + ]); + + $view = $form->createView(); + + $this->assertInstanceOf(TranslatableInterface::class, $view->children[0]->vars['label']); + $this->assertEquals('Left', $view->children[0]->vars['label']->trans(new IdentityTranslator())); + } + protected function getTestOptions(): array { return ['class' => Suit::class]; diff --git a/Tests/Extension/Core/Type/FileTypeTest.php b/Tests/Extension/Core/Type/FileTypeTest.php index b7f3332c1e..85907b6959 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 1d2ff4ff12..fe19f3b120 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); @@ -439,9 +439,8 @@ public function testSubformCallsSettersIfReferenceIsScalar() $builder->add('referenceCopy', static::TESTED_TYPE); $builder->get('referenceCopy')->addViewTransformer(new CallbackTransformer( function () {}, - function ($value) { // reverseTransform - return 'foobar'; - } + fn ($value) => // reverseTransform +'foobar' )); $form = $builder->getForm(); @@ -464,9 +463,8 @@ public function testSubformAlwaysInsertsIntoArrays() $builder->add('referenceCopy', static::TESTED_TYPE); $builder->get('referenceCopy')->addViewTransformer(new CallbackTransformer( function () {}, - function ($value) use ($ref2) { // reverseTransform - return $ref2; - } + fn ($value) => // reverseTransform +$ref2 )); $form = $builder->getForm(); @@ -682,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']); @@ -885,14 +883,14 @@ public function getCurrency() class MoneyDataMapper implements DataMapperInterface { - public function mapDataToForms($data, $forms) + public function mapDataToForms(mixed $viewData, \Traversable $forms): void { $forms = iterator_to_array($forms); - $forms['amount']->setData($data ? $data->getAmount() : 0); - $forms['currency']->setData($data ? $data->getCurrency() : 'EUR'); + $forms['amount']->setData($viewData ? $viewData->getAmount() : 0); + $forms['currency']->setData($viewData ? $viewData->getCurrency() : 'EUR'); } - public function mapFormsToData($forms, &$data) + public function mapFormsToData(\Traversable $forms, mixed &$viewData): void { $forms = iterator_to_array($forms); @@ -904,7 +902,7 @@ public function mapFormsToData($forms, &$data) throw $failure; } - $data = new Money( + $viewData = new Money( $forms['amount']->getData(), $forms['currency']->getData() ); diff --git a/Tests/Extension/Core/Type/IntegerTypeTest.php b/Tests/Extension/Core/Type/IntegerTypeTest.php index 6d03ebf6cf..ff33c17c63 100644 --- a/Tests/Extension/Core/Type/IntegerTypeTest.php +++ b/Tests/Extension/Core/Type/IntegerTypeTest.php @@ -11,13 +11,14 @@ 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 $previousLocale; + private string $previousLocale; protected function setUp(): void { diff --git a/Tests/Extension/Core/Type/LanguageTypeTest.php b/Tests/Extension/Core/Type/LanguageTypeTest.php index e214e0afd4..8eb085112f 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 8486b6656b..a2a820b390 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 c65629d946..f9112ffcac 100644 --- a/Tests/Extension/Core/Type/MoneyTypeTest.php +++ b/Tests/Extension/Core/Type/MoneyTypeTest.php @@ -11,13 +11,14 @@ 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 $defaultLocale; + private string $defaultLocale; protected function setUp(): void { @@ -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 d3a5420847..95ccdfea9f 100644 --- a/Tests/Extension/Core/Type/NumberTypeTest.php +++ b/Tests/Extension/Core/Type/NumberTypeTest.php @@ -13,13 +13,14 @@ 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 $defaultLocale; + private string $defaultLocale; protected function setUp(): void { @@ -34,8 +35,6 @@ protected function setUp(): void protected function tearDown(): void { - parent::tearDown(); - \Locale::setDefault($this->defaultLocale); } @@ -202,4 +201,36 @@ public function testGroupingNotAllowedWithHtml5Widget() 'html5' => true, ]); } + + public function testNumericInputmode() + { + $form = $this->factory->create(static::TESTED_TYPE, null, [ + 'scale' => 0, + 'html5' => false, + ]); + $form->setData(12345.54321); + + $this->assertSame('numeric', $form->createView()->vars['attr']['inputmode']); + } + + public function testDecimalInputmode() + { + $form = $this->factory->create(static::TESTED_TYPE, null, [ + 'scale' => 2, + 'html5' => false, + ]); + $form->setData(12345.54321); + + $this->assertSame('decimal', $form->createView()->vars['attr']['inputmode']); + } + + public function testNoInputmodeWithHtml5Widget() + { + $form = $this->factory->create(static::TESTED_TYPE, null, [ + 'html5' => true, + ]); + $form->setData(12345.54321); + + $this->assertArrayNotHasKey('inputmode', $form->createView()->vars['attr']); + } } diff --git a/Tests/Extension/Core/Type/PasswordTypeTest.php b/Tests/Extension/Core/Type/PasswordTypeTest.php index 8d428a26a5..945437bcbd 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 9641d529f3..120aab2f31 100644 --- a/Tests/Extension/Core/Type/PercentTypeTest.php +++ b/Tests/Extension/Core/Type/PercentTypeTest.php @@ -11,18 +11,15 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\PercentType; use Symfony\Component\Form\Test\TypeTestCase; use Symfony\Component\Intl\Util\IntlTestHelper; class PercentTypeTest extends TypeTestCase { - use ExpectDeprecationTrait; - public const TESTED_TYPE = PercentType::class; - private $defaultLocale; + private string $defaultLocale; protected function setUp(): void { @@ -37,8 +34,6 @@ protected function setUp(): void protected function tearDown(): void { - parent::tearDown(); - \Locale::setDefault($this->defaultLocale); } @@ -83,36 +78,14 @@ public function testHtml5EnablesSpecificFormatting() $this->assertSame('number', $form->createView()->vars['type']); } - /** - * @group legacy - */ public function testSubmitWithoutRoundingMode() { - $this->expectDeprecation('Since symfony/form 5.1: Not configuring the "rounding_mode" option is deprecated. It will default to "\NumberFormatter::ROUND_HALFUP" in Symfony 6.0.'); - - $form = $this->factory->create(self::TESTED_TYPE, null, [ - 'scale' => 2, - ]); - - $form->submit('1.23456'); - - $this->assertEqualsWithDelta(0.0123456, $form->getData(), \PHP_FLOAT_EPSILON); - } - - /** - * @group legacy - */ - public function testSubmitWithNullRoundingMode() - { - $this->expectDeprecation('Since symfony/form 5.1: Not configuring the "rounding_mode" option is deprecated. It will default to "\NumberFormatter::ROUND_HALFUP" in Symfony 6.0.'); - $form = $this->factory->create(self::TESTED_TYPE, null, [ - 'rounding_mode' => null, 'scale' => 2, ]); $form->submit('1.23456'); - $this->assertEqualsWithDelta(0.0123456, $form->getData(), \PHP_FLOAT_EPSILON); + $this->assertEquals(0.0123, $form->getData()); } } diff --git a/Tests/Extension/Core/Type/RepeatedTypeTest.php b/Tests/Extension/Core/Type/RepeatedTypeTest.php index ca0de12233..2d19a0613d 100644 --- a/Tests/Extension/Core/Type/RepeatedTypeTest.php +++ b/Tests/Extension/Core/Type/RepeatedTypeTest.php @@ -11,6 +11,7 @@ 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; @@ -18,12 +19,9 @@ class RepeatedTypeTest extends BaseTypeTestCase { - public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\RepeatedType'; + public const TESTED_TYPE = RepeatedType::class; - /** - * @var Form - */ - protected $form; + protected Form $form; protected function setUp(): void { @@ -201,7 +199,7 @@ public function testSubmitNullForTextTypeWithEmptyDataOptionSetToEmptyString($em 'type' => TextType::class, 'options' => [ 'empty_data' => $emptyData, - ] + ], ]); $form->submit($submittedData); diff --git a/Tests/Extension/Core/Type/SubmitTypeTest.php b/Tests/Extension/Core/Type/SubmitTypeTest.php index 8a16175d76..af5ab8400b 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 e14a816362..4832151684 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 9b3bbbfd00..8a2baf1b4c 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() { @@ -28,6 +29,7 @@ public function testSubmitDateTime() 'model_timezone' => 'UTC', 'view_timezone' => 'UTC', 'input' => 'datetime', + 'widget' => 'choice', ]); $input = [ @@ -49,6 +51,7 @@ public function testSubmitDateTimeImmutable() 'model_timezone' => 'UTC', 'view_timezone' => 'UTC', 'input' => 'datetime_immutable', + 'widget' => 'choice', ]); $input = [ @@ -71,6 +74,7 @@ public function testSubmitString() 'model_timezone' => 'UTC', 'view_timezone' => 'UTC', 'input' => 'string', + 'widget' => 'choice', ]); $input = [ @@ -106,6 +110,7 @@ public function testSubmitTimestamp() 'model_timezone' => 'UTC', 'view_timezone' => 'UTC', 'input' => 'timestamp', + 'widget' => 'choice', ]); $input = [ @@ -127,6 +132,7 @@ public function testSubmitArray() 'model_timezone' => 'UTC', 'view_timezone' => 'UTC', 'input' => 'array', + 'widget' => 'choice', ]); $input = [ @@ -288,6 +294,7 @@ public function testPreSetDataDifferentTimezones() 'input' => 'datetime', 'with_seconds' => true, 'reference_date' => new \DateTimeImmutable('2019-01-01', new \DateTimeZone('UTC')), + 'widget' => 'choice', ]); $form->setData(new \DateTime('2022-01-01 15:09:10', new \DateTimeZone('UTC'))); @@ -307,6 +314,7 @@ public function testPreSetDataDifferentTimezonesDuringDaylightSavingTime() 'input' => 'datetime', 'with_seconds' => true, 'reference_date' => new \DateTimeImmutable('2019-07-12', new \DateTimeZone('UTC')), + 'widget' => 'choice', ]); $form->setData(new \DateTime('2022-04-29 15:09:10', new \DateTimeZone('UTC'))); @@ -358,6 +366,7 @@ public function testSubmitDifferentTimezones() 'input' => 'datetime', 'with_seconds' => true, 'reference_date' => new \DateTimeImmutable('2019-01-01', new \DateTimeZone('UTC')), + 'widget' => 'choice', ]); $form->submit([ 'hour' => '16', @@ -376,6 +385,7 @@ public function testSubmitDifferentTimezonesDuringDaylightSavingTime() 'input' => 'datetime', 'with_seconds' => true, 'reference_date' => new \DateTimeImmutable('2019-07-12', new \DateTimeZone('UTC')), + 'widget' => 'choice', ]); $form->submit([ 'hour' => '16', @@ -456,6 +466,7 @@ public function testSetDataWithoutMinutes() 'view_timezone' => 'UTC', 'input' => 'datetime', 'with_minutes' => false, + 'widget' => 'choice', ]); $form->setData(new \DateTime('03:04:05 UTC')); @@ -470,6 +481,7 @@ public function testSetDataWithSeconds() 'view_timezone' => 'UTC', 'input' => 'datetime', 'with_seconds' => true, + 'widget' => 'choice', ]); $form->setData(new \DateTime('03:04:05 UTC')); @@ -485,6 +497,7 @@ public function testSetDataDifferentTimezones() 'input' => 'string', 'with_seconds' => true, 'reference_date' => new \DateTimeImmutable('2013-01-01 00:00:00', new \DateTimeZone('America/New_York')), + 'widget' => 'choice', ]); $dateTime = new \DateTime('2013-01-01 12:04:05'); @@ -512,6 +525,7 @@ public function testSetDataDifferentTimezonesDateTime() 'input' => 'datetime', 'with_seconds' => true, 'reference_date' => new \DateTimeImmutable('now', new \DateTimeZone('America/New_York')), + 'widget' => 'choice', ]); $dateTime = new \DateTime('12:04:05'); @@ -540,6 +554,7 @@ public function testSetDataDifferentTimezonesDuringDaylightSavingTime() 'input' => 'datetime', 'with_seconds' => true, 'reference_date' => new \DateTimeImmutable('2019-07-12', new \DateTimeZone('UTC')), + 'widget' => 'choice', ]); $form->setData(new \DateTime('2019-07-24 14:09:10', new \DateTimeZone('UTC'))); @@ -557,6 +572,7 @@ public function testSetDataDifferentTimezonesWithoutReferenceDate() 'view_timezone' => 'Europe/Berlin', 'input' => 'datetime', 'with_seconds' => true, + 'widget' => 'choice', ]); $form->setData(new \DateTime('2019-07-24 14:09:10', new \DateTimeZone('UTC'))); @@ -568,6 +584,7 @@ public function testHoursOption() { $form = $this->factory->create(static::TESTED_TYPE, null, [ 'hours' => [6, 7], + 'widget' => 'choice', ]); $view = $form->createView(); @@ -582,6 +599,7 @@ public function testIsMinuteWithinRangeReturnsTrueIfWithin() { $form = $this->factory->create(static::TESTED_TYPE, null, [ 'minutes' => [6, 7], + 'widget' => 'choice', ]); $view = $form->createView(); @@ -597,6 +615,7 @@ public function testIsSecondWithinRangeReturnsTrueIfWithin() $form = $this->factory->create(static::TESTED_TYPE, null, [ 'seconds' => [6, 7], 'with_seconds' => true, + 'widget' => 'choice', ]); $view = $form->createView(); @@ -733,7 +752,7 @@ public function testInitializeWithDateTime() { // Throws an exception if "data_class" option is not explicitly set // to null in the type - $this->assertInstanceOf(FormInterface::class, $this->factory->create(static::TESTED_TYPE, new \DateTime())); + $this->assertInstanceOf(FormInterface::class, $this->factory->create(static::TESTED_TYPE, new \DateTime(), ['widget' => 'choice'])); } public function testSingleTextWidgetShouldUseTheRightInputType() @@ -789,6 +808,7 @@ public function testPassDefaultPlaceholderToViewIfNotRequired() $form = $this->factory->create(static::TESTED_TYPE, null, [ 'required' => false, 'with_seconds' => true, + 'widget' => 'choice', ]); $view = $form->createView(); @@ -802,6 +822,7 @@ public function testPassNoPlaceholderToViewIfRequired() $form = $this->factory->create(static::TESTED_TYPE, null, [ 'required' => true, 'with_seconds' => true, + 'widget' => 'choice', ]); $view = $form->createView(); @@ -815,6 +836,7 @@ public function testPassPlaceholderAsString() $form = $this->factory->create(static::TESTED_TYPE, null, [ 'placeholder' => 'Empty', 'with_seconds' => true, + 'widget' => 'choice', ]); $view = $form->createView(); @@ -832,6 +854,7 @@ public function testPassPlaceholderAsArray() 'second' => 'Empty second', ], 'with_seconds' => true, + 'widget' => 'choice', ]); $view = $form->createView(); @@ -849,6 +872,7 @@ public function testPassPlaceholderAsPartialArrayAddEmptyIfNotRequired() 'second' => 'Empty second', ], 'with_seconds' => true, + 'widget' => 'choice', ]); $view = $form->createView(); @@ -866,6 +890,7 @@ public function testPassPlaceholderAsPartialArrayAddNullIfRequired() 'second' => 'Empty second', ], 'with_seconds' => true, + 'widget' => 'choice', ]); $view = $form->createView(); @@ -934,6 +959,7 @@ public function testInitializeWithSecondsAndWithoutMinutes() $this->factory->create(static::TESTED_TYPE, null, [ 'with_minutes' => false, 'with_seconds' => true, + 'widget' => 'choice', ]); } @@ -942,6 +968,7 @@ public function testThrowExceptionIfHoursIsInvalid() $this->expectException(InvalidOptionsException::class); $this->factory->create(static::TESTED_TYPE, null, [ 'hours' => 'bad value', + 'widget' => 'choice', ]); } @@ -950,6 +977,7 @@ public function testThrowExceptionIfMinutesIsInvalid() $this->expectException(InvalidOptionsException::class); $this->factory->create(static::TESTED_TYPE, null, [ 'minutes' => 'bad value', + 'widget' => 'choice', ]); } @@ -958,6 +986,7 @@ public function testThrowExceptionIfSecondsIsInvalid() $this->expectException(InvalidOptionsException::class); $this->factory->create(static::TESTED_TYPE, null, [ 'seconds' => 'bad value', + 'widget' => 'choice', ]); } @@ -968,6 +997,7 @@ public function testReferenceDateTimezoneMustMatchModelTimezone() 'model_timezone' => 'UTC', 'view_timezone' => 'Europe/Berlin', 'reference_date' => new \DateTimeImmutable('now', new \DateTimeZone('Europe/Berlin')), + 'widget' => 'choice', ]); } @@ -976,6 +1006,7 @@ public function testModelTimezoneDefaultToReferenceDateTimezoneIfProvided() $form = $this->factory->create(static::TESTED_TYPE, null, [ 'view_timezone' => 'Europe/Berlin', 'reference_date' => new \DateTimeImmutable('now', new \DateTimeZone('Europe/Berlin')), + 'widget' => 'choice', ]); $this->assertSame('Europe/Berlin', $form->getConfig()->getOption('model_timezone')); @@ -985,6 +1016,7 @@ public function testViewTimezoneDefaultsToModelTimezoneIfProvided() { $form = $this->factory->create(static::TESTED_TYPE, null, [ 'model_timezone' => 'Europe/Berlin', + 'widget' => 'choice', ]); $this->assertSame('Europe/Berlin', $form->getConfig()->getOption('view_timezone')); @@ -992,7 +1024,7 @@ public function testViewTimezoneDefaultsToModelTimezoneIfProvided() public function testPassDefaultChoiceTranslationDomain() { - $form = $this->factory->create(static::TESTED_TYPE); + $form = $this->factory->create(static::TESTED_TYPE, null, ['widget' => 'choice']); $view = $form->createView(); $this->assertFalse($view['hour']->vars['choice_translation_domain']); @@ -1004,6 +1036,7 @@ public function testPassChoiceTranslationDomainAsString() $form = $this->factory->create(static::TESTED_TYPE, null, [ 'choice_translation_domain' => 'messages', 'with_seconds' => true, + 'widget' => 'choice', ]); $view = $form->createView(); @@ -1020,6 +1053,7 @@ public function testPassChoiceTranslationDomainAsArray() 'second' => 'test', ], 'with_seconds' => true, + 'widget' => 'choice', ]); $view = $form->createView(); @@ -1039,6 +1073,7 @@ public function testSubmitNullUsesDefaultEmptyData($emptyData = [], $expectedDat { $form = $this->factory->create(static::TESTED_TYPE, null, [ 'empty_data' => $emptyData, + 'widget' => 'choice', ]); $form->submit(null); @@ -1055,6 +1090,7 @@ public function testArrayTimeWithReferenceDoesNotUseReferenceTimeOnZero() 'view_timezone' => 'Europe/Berlin', 'reference_date' => new \DateTimeImmutable('01-01-2021 12:34:56', new \DateTimeZone('UTC')), 'input' => 'array', + 'widget' => 'choice', ]); $input = [ @@ -1082,6 +1118,7 @@ public function testArrayTimeWithReferenceDoesUseReferenceDateOnModelTransform() 'view_timezone' => 'Europe/Berlin', 'reference_date' => new \DateTimeImmutable('01-05-2021 12:34:56', new \DateTimeZone('UTC')), 'input' => 'array', + 'widget' => 'choice', ]); $this->assertSame($input, $form->getData()); @@ -1113,9 +1150,7 @@ public function testSubmitNullUsesDateEmptyData($widget, $emptyData, $expectedDa public static function provideEmptyData() { $expectedData = \DateTime::createFromFormat('Y-m-d H:i', '1970-01-01 21:23'); - $lazyEmptyData = static function (FormInterface $form) { - return $form->getConfig()->getCompound() ? ['hour' => '21', 'minute' => '23'] : '21:23'; - }; + $lazyEmptyData = static fn (FormInterface $form) => $form->getConfig()->getCompound() ? ['hour' => '21', 'minute' => '23'] : '21:23'; return [ 'Simple field' => ['single_text', '21:23', $expectedData], @@ -1126,4 +1161,30 @@ public static function provideEmptyData() 'Compound choice field lazy' => ['choice', $lazyEmptyData, $expectedData], ]; } + + public function testDateTimeInputTimezoneNotMatchingModelTimezone() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Using a "DateTime" instance with a timezone ("UTC") not matching the configured model timezone "Europe/Berlin" is not supported.'); + + $this->factory->create(static::TESTED_TYPE, new \DateTime('now', new \DateTimeZone('UTC')), [ + 'model_timezone' => 'Europe/Berlin', + ]); + } + + public function testDateTimeImmutableInputTimezoneNotMatchingModelTimezone() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Using a "DateTimeImmutable" instance with a timezone ("UTC") not matching the configured model timezone "Europe/Berlin" is not supported.'); + + $this->factory->create(static::TESTED_TYPE, new \DateTimeImmutable('now', new \DateTimeZone('UTC')), [ + 'input' => 'datetime_immutable', + 'model_timezone' => 'Europe/Berlin', + ]); + } + + protected function getTestOptions(): array + { + return ['widget' => 'choice']; + } } diff --git a/Tests/Extension/Core/Type/TimezoneTypeTest.php b/Tests/Extension/Core/Type/TimezoneTypeTest.php index 9966b40438..4f23439746 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 b9387d01a4..a0d335647d 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 a69b96a38a..b4d58fd95c 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/EventListener/CsrfValidationListenerTest.php b/Tests/Extension/Csrf/EventListener/CsrfValidationListenerTest.php index 3d028ac801..6a6a8be9cd 100644 --- a/Tests/Extension/Csrf/EventListener/CsrfValidationListenerTest.php +++ b/Tests/Extension/Csrf/EventListener/CsrfValidationListenerTest.php @@ -18,15 +18,17 @@ use Symfony\Component\Form\FormBuilder; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormFactoryBuilder; +use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\Util\ServerParams; use Symfony\Component\Security\Csrf\CsrfTokenManager; class CsrfValidationListenerTest extends TestCase { - protected $dispatcher; - protected $factory; - protected $tokenManager; - protected $form; + protected EventDispatcher $dispatcher; + protected FormFactoryInterface $factory; + protected CsrfTokenManager $tokenManager; + protected FormInterface $form; protected function setUp(): void { @@ -38,14 +40,6 @@ protected function setUp(): void ->getForm(); } - protected function tearDown(): void - { - $this->dispatcher = null; - $this->factory = null; - $this->tokenManager = null; - $this->form = null; - } - protected function getBuilder() { return new FormBuilder('post', null, $this->dispatcher, $this->factory, ['compound' => true]); diff --git a/Tests/Extension/Csrf/Type/FormTypeCsrfExtensionTest.php b/Tests/Extension/Csrf/Type/FormTypeCsrfExtensionTest.php index 7fcc80fee7..d5bce6527b 100644 --- a/Tests/Extension/Csrf/Type/FormTypeCsrfExtensionTest.php +++ b/Tests/Extension/Csrf/Type/FormTypeCsrfExtensionTest.php @@ -23,7 +23,7 @@ class FormTypeCsrfExtensionTest_ChildType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { // The form needs a child in order to trigger CSRF protection by // default @@ -33,10 +33,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) class FormTypeCsrfExtensionTest extends TypeTestCase { - /** - * @var MockObject&CsrfTokenManagerInterface - */ - protected $tokenManager; + protected MockObject&CsrfTokenManagerInterface $tokenManager; protected function setUp(): void { @@ -45,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/DataCollectorExtensionTest.php b/Tests/Extension/DataCollector/DataCollectorExtensionTest.php index 0ae127d077..99e6215cbb 100644 --- a/Tests/Extension/DataCollector/DataCollectorExtensionTest.php +++ b/Tests/Extension/DataCollector/DataCollectorExtensionTest.php @@ -19,10 +19,7 @@ class DataCollectorExtensionTest extends TestCase { - /** - * @var DataCollectorExtension - */ - private $extension; + private DataCollectorExtension $extension; protected function setUp(): void { diff --git a/Tests/Extension/DataCollector/FormDataCollectorTest.php b/Tests/Extension/DataCollector/FormDataCollectorTest.php index 39009b598c..4090fc97bf 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; @@ -28,35 +27,12 @@ class FormDataCollectorTest extends TestCase { - /** - * @var FormDataCollector - */ - private $dataCollector; - - /** - * @var FormFactory - */ - private $factory; - - /** - * @var Form - */ - private $form; - - /** - * @var Form - */ - private $childForm; - - /** - * @var FormView - */ - private $view; - - /** - * @var FormView - */ - private $childView; + private FormDataCollector $dataCollector; + private FormFactory $factory; + private FormInterface $form; + private FormInterface $childForm; + private FormView $view; + private FormView $childView; protected function setUp(): void { @@ -94,7 +70,7 @@ public function testBuildPreliminaryFormTree() ], 'errors' => [], 'children' => [], - ]; + ]; $formData = [ 'id' => 'name', @@ -110,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' => [ @@ -125,7 +101,7 @@ public function testBuildPreliminaryFormTree() spl_object_hash($this->childForm) => $childFormData, ], 'nb_errors' => 0, - ], $this->dataCollector->getData()); + ], $this->dataCollector->getData()); } public function testBuildMultiplePreliminaryFormTrees() @@ -335,9 +311,7 @@ public function testSerializeWithFormAddedMultipleTimes() $form1View = new FormView(); $form2View = new FormView(); $child1View = new FormView(); - $child1View->vars['is_selected'] = function ($choice, array $values) { - return \in_array($choice, $values, true); - }; + $child1View->vars['is_selected'] = fn ($choice, array $values) => \in_array($choice, $values, true); $form1->add($child1); $form2->add($child1); diff --git a/Tests/Extension/DataCollector/FormDataExtractorTest.php b/Tests/Extension/DataCollector/FormDataExtractorTest.php index b496b71fd5..29f9359df8 100644 --- a/Tests/Extension/DataCollector/FormDataExtractorTest.php +++ b/Tests/Extension/DataCollector/FormDataExtractorTest.php @@ -26,6 +26,7 @@ use Symfony\Component\Form\ResolvedFormType; use Symfony\Component\Form\ResolvedFormTypeFactory; use Symfony\Component\Form\Tests\Fixtures\FixedDataTransformer; +use Symfony\Component\Validator\Constraints\WordCount; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\VarDumper\Test\VarDumperTestTrait; @@ -36,10 +37,7 @@ class FormDataExtractorTest extends TestCase { use VarDumperTestTrait; - /** - * @var FormDataExtractor - */ - private $dataExtractor; + private FormDataExtractor $dataExtractor; protected function setUp(): void { @@ -303,36 +301,68 @@ public function testExtractSubmittedDataStoresErrorCause() $form->addError(new FormError('Invalid!', null, [], null, $violation)); $origin = spl_object_hash($form); - $this->assertDumpMatchesFormat(<< array:1 [ - "norm" => "Foobar" - ] - "errors" => array:1 [ - 0 => array:3 [ - "message" => "Invalid!" - "origin" => "$origin" - "trace" => array:2 [ - 0 => Symfony\Component\Validator\ConstraintViolation { - -message: "Foo" - -messageTemplate: "Foo" - -parameters: [] - -plural: null - -root: "Root" - -propertyPath: "property.path" - -invalidValue: "Invalid!" - -constraint: null - -code: null - -cause: Exception {%A} + if (class_exists(WordCount::class)) { + $expectedFormat = <<<"EODUMP" + array:3 [ + "submitted_data" => array:1 [ + "norm" => "Foobar" + ] + "errors" => array:1 [ + 0 => array:3 [ + "message" => "Invalid!" + "origin" => "$origin" + "trace" => array:2 [ + 0 => Symfony\Component\Validator\ConstraintViolation { + -message: "Foo" + -messageTemplate: "Foo" + -parameters: [] + -root: "Root" + -propertyPath: "property.path" + -invalidValue: "Invalid!" + -plural: null + -code: null + -constraint: null + -cause: Exception {%A} + } + 1 => Exception {#1} + ] + ] + ] + "synchronized" => true + ] + EODUMP; + } else { + $expectedFormat = <<<"EODUMP" + array:3 [ + "submitted_data" => array:1 [ + "norm" => "Foobar" + ] + "errors" => array:1 [ + 0 => array:3 [ + "message" => "Invalid!" + "origin" => "$origin" + "trace" => array:2 [ + 0 => Symfony\Component\Validator\ConstraintViolation { + -message: "Foo" + -messageTemplate: "Foo" + -parameters: [] + -plural: null + -root: "Root" + -propertyPath: "property.path" + -invalidValue: "Invalid!" + -constraint: null + -code: null + -cause: Exception {%A} + } + 1 => Exception {#1} + ] + ] + ] + "synchronized" => true + ] + EODUMP; } - 1 => Exception {#1} - ] - ] - ] - "synchronized" => true -] -EODUMP - , + $this->assertDumpMatchesFormat($expectedFormat, $this->dataExtractor->extractSubmittedData($form) ); } diff --git a/Tests/Extension/DataCollector/Type/DataCollectorTypeExtensionTest.php b/Tests/Extension/DataCollector/Type/DataCollectorTypeExtensionTest.php index 1f7d6a817d..7442d181b8 100644 --- a/Tests/Extension/DataCollector/Type/DataCollectorTypeExtensionTest.php +++ b/Tests/Extension/DataCollector/Type/DataCollectorTypeExtensionTest.php @@ -24,10 +24,7 @@ class DataCollectorTypeExtensionTest extends TestCase { - /** - * @var DataCollectorTypeExtension - */ - private $extension; + private DataCollectorTypeExtension $extension; protected function setUp(): void { diff --git a/Tests/Extension/DependencyInjection/DependencyInjectionExtensionTest.php b/Tests/Extension/DependencyInjection/DependencyInjectionExtensionTest.php index b99240c8cb..8f2cbdcd5d 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 new file mode 100644 index 0000000000..6784576b05 --- /dev/null +++ b/Tests/Extension/HtmlSanitizer/Type/TextTypeHtmlSanitizerExtensionTest.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\Csrf\Type; + +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Extension\HtmlSanitizer\HtmlSanitizerExtension; +use Symfony\Component\Form\Test\TypeTestCase; +use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface; + +class TextTypeHtmlSanitizerExtensionTest extends TypeTestCase +{ + protected function getExtensions(): array + { + $fooSanitizer = $this->createMock(HtmlSanitizerInterface::class); + $fooSanitizer->expects($this->once()) + ->method('sanitize') + ->with('foobar') + ->willReturn('foo'); + + $barSanitizer = $this->createMock(HtmlSanitizerInterface::class); + $barSanitizer->expects($this->once()) + ->method('sanitize') + ->with('foobar') + ->willReturn('bar'); + + return array_merge(parent::getExtensions(), [ + new HtmlSanitizerExtension(new ServiceLocator([ + 'foo' => fn () => $fooSanitizer, + 'bar' => fn () => $barSanitizer, + ]), 'foo'), + ]); + } + + public function testSanitizer() + { + $form = $this->factory->createBuilder(FormType::class, ['data' => null]) + ->add('data', TextType::class, ['sanitize_html' => true]) + ->getForm() + ; + $form->submit(['data' => 'foobar']); + + $this->assertSame(['data' => 'foo'], $form->getData()); + + $form = $this->factory->createBuilder(FormType::class, ['data' => null]) + ->add('data', TextType::class, ['sanitize_html' => true, 'sanitizer' => 'bar']) + ->getForm() + ; + $form->submit(['data' => 'foobar']); + + $this->assertSame(['data' => 'bar'], $form->getData()); + } +} diff --git a/Tests/Extension/PasswordHasher/Type/PasswordTypePasswordHasherExtensionTest.php b/Tests/Extension/PasswordHasher/Type/PasswordTypePasswordHasherExtensionTest.php new file mode 100644 index 0000000000..07d1292a3f --- /dev/null +++ b/Tests/Extension/PasswordHasher/Type/PasswordTypePasswordHasherExtensionTest.php @@ -0,0 +1,259 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\PasswordHasher\Type; + +use PHPUnit\Framework\MockObject\MockObject; +use Symfony\Component\Form\Exception\InvalidConfigurationException; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\Core\Type\PasswordType; +use Symfony\Component\Form\Extension\Core\Type\RepeatedType; +use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener; +use Symfony\Component\Form\Extension\PasswordHasher\PasswordHasherExtension; +use Symfony\Component\Form\Test\TypeTestCase; +use Symfony\Component\Form\Tests\Fixtures\RepeatedPasswordField; +use Symfony\Component\Form\Tests\Fixtures\User; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; + +class PasswordTypePasswordHasherExtensionTest extends TypeTestCase +{ + protected MockObject&UserPasswordHasherInterface $passwordHasher; + + protected function setUp(): void + { + if (!interface_exists(PasswordAuthenticatedUserInterface::class)) { + $this->markTestSkipped('PasswordAuthenticatedUserInterface not available.'); + } + + $this->passwordHasher = $this->createMock(UserPasswordHasher::class); + + parent::setUp(); + } + + protected function getExtensions(): array + { + return array_merge(parent::getExtensions(), [ + new PasswordHasherExtension(new PasswordHasherListener($this->passwordHasher)), + ]); + } + + public function testPasswordHashSuccess() + { + $user = new User(); + + $plainPassword = 'PlainPassword'; + $hashedPassword = 'HashedPassword'; + + $this->passwordHasher + ->expects($this->once()) + ->method('hashPassword') + ->with($user, $plainPassword) + ->willReturn($hashedPassword) + ; + + $this->assertNull($user->getPassword()); + + $form = $this->factory + ->createBuilder('Symfony\Component\Form\Extension\Core\Type\FormType', $user) + ->add('plainPassword', 'Symfony\Component\Form\Extension\Core\Type\PasswordType', [ + 'hash_property_path' => 'password', + 'mapped' => false, + ]) + ->getForm() + ; + + $form->submit(['plainPassword' => $plainPassword]); + + $this->assertTrue($form->isValid()); + $this->assertSame($user->getPassword(), $hashedPassword); + } + + public function testPasswordHashSkippedWithEmptyPassword() + { + $oldHashedPassword = 'PreviousHashedPassword'; + + $user = new User(); + $user->setPassword($oldHashedPassword); + + $this->passwordHasher + ->expects($this->never()) + ->method('hashPassword') + ; + + $this->assertEquals($user->getPassword(), $oldHashedPassword); + + $form = $this->factory + ->createBuilder(FormType::class, $user) + ->add('plainPassword', PasswordType::class, [ + 'hash_property_path' => 'password', + 'mapped' => false, + 'required' => false, + ]) + ->getForm() + ; + + $form->submit(['plainPassword' => '']); + + $this->assertTrue($form->isValid()); + $this->assertSame($user->getPassword(), $oldHashedPassword); + } + + public function testPasswordHashSuccessWithEmptyData() + { + $user = new User(); + + $plainPassword = 'PlainPassword'; + $hashedPassword = 'HashedPassword'; + + $this->passwordHasher + ->expects($this->once()) + ->method('hashPassword') + ->with($user, $plainPassword) + ->willReturn($hashedPassword) + ; + + $this->assertNull($user->getPassword()); + + $form = $this->factory + ->createBuilder('Symfony\Component\Form\Extension\Core\Type\FormType', null, [ + 'data_class' => User::class, + 'empty_data' => function () use ($user) { + return $user; + }, + ]) + ->add('plainPassword', 'Symfony\Component\Form\Extension\Core\Type\PasswordType', [ + 'hash_property_path' => 'password', + 'mapped' => false, + ]) + ->getForm() + ; + + $form->submit(['plainPassword' => $plainPassword]); + + $this->assertTrue($form->isValid()); + $this->assertSame($user->getPassword(), $hashedPassword); + } + + /** + * @dataProvider provideRepeatedPasswordField + */ + public function testRepeatedPasswordField(string $type, array $options = []) + { + $user = new User(); + + $plainPassword = 'PlainPassword'; + $hashedPassword = 'HashedPassword'; + + $this->passwordHasher + ->expects($this->once()) + ->method('hashPassword') + ->with($user, $plainPassword) + ->willReturn($hashedPassword) + ; + + $this->assertNull($user->getPassword()); + + $form = $this->factory + ->createBuilder(data: $user) + ->add('plainPassword', $type, $options) + ->getForm() + ; + + $form->submit(['plainPassword' => ['first' => $plainPassword, 'second' => $plainPassword]]); + + $this->assertTrue($form->isValid()); + $this->assertSame($user->getPassword(), $hashedPassword); + } + + public static function provideRepeatedPasswordField(): iterable + { + yield 'RepeatedType' => [ + RepeatedType::class, + [ + 'type' => PasswordType::class, + 'first_options' => [ + 'hash_property_path' => 'password', + ], + 'mapped' => false, + ], + ]; + + yield 'RepeatedType child' => [RepeatedPasswordField::class]; + } + + public function testPasswordHashOnInvalidForm() + { + $user = new User(); + + $this->passwordHasher + ->expects($this->never()) + ->method('hashPassword') + ; + + $this->assertNull($user->getPassword()); + + $form = $this->factory + ->createBuilder('Symfony\Component\Form\Extension\Core\Type\FormType', $user) + ->add('plainPassword', 'Symfony\Component\Form\Extension\Core\Type\PasswordType', [ + 'hash_property_path' => 'password', + 'mapped' => false, + ]) + ->add('integer', 'Symfony\Component\Form\Extension\Core\Type\IntegerType', [ + 'mapped' => false, + ]) + ->getForm() + ; + + $form->submit([ + 'plainPassword' => 'PlainPassword', + 'integer' => 'text', + ]); + + $this->assertFalse($form->isValid()); + $this->assertNull($user->getPassword()); + } + + public function testPasswordHashOnInvalidData() + { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('The "hash_property_path" option only supports "Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface" objects, "array" given.'); + + $form = $this->factory + ->createBuilder('Symfony\Component\Form\Extension\Core\Type\FormType', []) + ->add('plainPassword', 'Symfony\Component\Form\Extension\Core\Type\PasswordType', [ + 'hash_property_path' => 'password', + 'mapped' => false, + ]) + ->getForm() + ; + + $form->submit(['plainPassword' => 'PlainPassword']); + } + + public function testPasswordHashOnMappedFieldForbidden() + { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('The "hash_property_path" option cannot be used on mapped field.'); + + $form = $this->factory + ->createBuilder('Symfony\Component\Form\Extension\Core\Type\FormType', new User()) + ->add('password', 'Symfony\Component\Form\Extension\Core\Type\PasswordType', [ + 'hash_property_path' => 'password', + 'mapped' => true, + ]) + ->getForm() + ; + + $form->submit(['password' => 'PlainPassword']); + } +} diff --git a/Tests/Extension/Validator/Constraints/FormValidatorFunctionalTest.php b/Tests/Extension/Validator/Constraints/FormValidatorFunctionalTest.php index de423cbcf3..14595e8cf5 100644 --- a/Tests/Extension/Validator/Constraints/FormValidatorFunctionalTest.php +++ b/Tests/Extension/Validator/Constraints/FormValidatorFunctionalTest.php @@ -23,6 +23,7 @@ use Symfony\Component\Form\Extension\Validator\ValidatorExtension; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormFactoryBuilder; +use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints\Collection; use Symfony\Component\Validator\Constraints\Expression; @@ -34,11 +35,12 @@ use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory; use Symfony\Component\Validator\Mapping\Loader\StaticMethodLoader; use Symfony\Component\Validator\Validation; +use Symfony\Component\Validator\Validator\ValidatorInterface; class FormValidatorFunctionalTest extends TestCase { - private $validator; - private $formFactory; + private ValidatorInterface $validator; + private FormFactoryInterface $formFactory; protected function setUp(): void { @@ -86,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); @@ -439,9 +441,9 @@ public function testSubmitFormChoiceInvalid() $this->assertTrue($form->isSubmitted()); $this->assertFalse($form->isValid()); $this->assertCount(2, $form->getErrors()); - $this->assertSame('This value is not valid.', $form->getErrors()[0]->getMessage()); + $this->assertSame('Please enter a valid date.', $form->getErrors()[0]->getMessage()); $this->assertSame($form->get('year'), $form->getErrors()[0]->getOrigin()); - $this->assertSame('This value is not valid.', $form->getErrors()[1]->getMessage()); + $this->assertSame('Please enter a valid date.', $form->getErrors()[1]->getMessage()); $this->assertSame($form->get('month'), $form->getErrors()[1]->getOrigin()); } @@ -486,7 +488,7 @@ class Foo public $bar; public $baz; - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('bar', new NotBlank()); } @@ -494,7 +496,7 @@ public static function loadValidatorMetadata(ClassMetadata $metadata) class FooType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('bar') @@ -504,7 +506,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) ; } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefault('data_class', Foo::class); } @@ -516,7 +518,7 @@ class Review public $title; public $author; - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('title', new NotBlank()); $metadata->addPropertyConstraint('rating', new NotBlank()); @@ -525,7 +527,7 @@ public static function loadValidatorMetadata(ClassMetadata $metadata) class ReviewType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('rating', IntegerType::class, [ @@ -538,7 +540,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) ; } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefault('data_class', Review::class); } @@ -548,7 +550,7 @@ class Customer { public $email; - public static function loadValidatorMetadata(ClassMetadata $metadata) + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('email', new NotBlank()); } @@ -556,14 +558,14 @@ public static function loadValidatorMetadata(ClassMetadata $metadata) class CustomerType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('email') ; } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefault('data_class', Customer::class); } diff --git a/Tests/Extension/Validator/Constraints/FormValidatorPerformanceTest.php b/Tests/Extension/Validator/Constraints/FormValidatorPerformanceTest.php index e8bfbc64ae..b0c7d719ae 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 67b5abd0a4..86b53ac3ad 100644 --- a/Tests/Extension/Validator/Constraints/FormValidatorTest.php +++ b/Tests/Extension/Validator/Constraints/FormValidatorTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Constraints; use Symfony\Component\EventDispatcher\EventDispatcher; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper; @@ -39,15 +38,8 @@ */ class FormValidatorTest extends ConstraintValidatorTestCase { - /** - * @var EventDispatcherInterface - */ - private $dispatcher; - - /** - * @var FormFactoryInterface - */ - private $factory; + private EventDispatcher $dispatcher; + private FormFactoryInterface $factory; protected function setUp(): void { @@ -192,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()) @@ -264,16 +256,16 @@ 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( - function ($data) { return $data; }, - function () { throw new TransformationFailedException(); } + static fn ($data) => $data, + static fn () => throw new TransformationFailedException() )) ->getForm(); @@ -300,17 +292,17 @@ 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( - function ($data) { return $data; }, - function () { throw new TransformationFailedException(); } + static fn ($data) => $data, + static fn () => throw new TransformationFailedException() )) ->getForm(); @@ -344,8 +336,8 @@ public function testDontValidateConstraintsIfNotSynchronized() $form = $this->getBuilder('name', '\stdClass', $options) ->setData($object) ->addViewTransformer(new CallbackTransformer( - function ($data) { return $data; }, - function () { throw new TransformationFailedException(); } + static fn ($data) => $data, + static fn () => throw new TransformationFailedException() )) ->getForm(); @@ -375,8 +367,8 @@ public function testTransformationFailedExceptionInvalidMessageIsUsed() ]) ->setData($object) ->addViewTransformer(new CallbackTransformer( - function ($data) { return $data; }, - function () { + static fn ($data) => $data, + static function () { $failure = new TransformationFailedException(); $failure->setInvalidMessage('safe message to be used', ['{{ bar }}' => 'bar']); @@ -423,7 +415,7 @@ public function testHandleGroupSequenceValidationGroups() public function testHandleCallbackValidationGroups() { $object = new \stdClass(); - $options = ['validation_groups' => [$this, 'getValidationGroups']]; + $options = ['validation_groups' => $this->getValidationGroups(...)]; $form = $this->getCompoundForm($object, $options); $form->submit([]); @@ -451,9 +443,7 @@ public function testDontExecuteFunctionNames() public function testHandleClosureValidationGroups() { $object = new \stdClass(); - $options = ['validation_groups' => function (FormInterface $form) { - return ['group1', 'group2']; - }]; + $options = ['validation_groups' => fn (FormInterface $form) => ['group1', 'group2']]; $form = $this->getCompoundForm($object, $options); $form->submit([]); @@ -543,7 +533,7 @@ public function testUseInheritedCallbackValidationGroup() { $object = new \stdClass(); - $parentOptions = ['validation_groups' => [$this, 'getValidationGroups']]; + $parentOptions = ['validation_groups' => $this->getValidationGroups(...)]; $parent = $this->getBuilder('parent', null, $parentOptions) ->setCompound(true) ->setDataMapper(new DataMapper()) @@ -565,9 +555,7 @@ public function testUseInheritedClosureValidationGroup() $object = new \stdClass(); $parentOptions = [ - 'validation_groups' => function () { - return ['group1', 'group2']; - }, + 'validation_groups' => fn () => ['group1', 'group2'], ]; $parent = $this->getBuilder('parent', null, $parentOptions) ->setCompound(true) @@ -714,7 +702,7 @@ public function testCauseForNotAllowedExtraFieldsIsTheFormConstraint() $this->assertSame($constraint, $context->getViolations()->get(0)->getConstraint()); } - protected function createValidator() + protected function createValidator(): FormValidator { return new FormValidator(); } @@ -736,7 +724,7 @@ private function getForm($name = 'name', $dataClass = null, array $options = []) private function getCompoundForm($data, array $options = []) { - return $this->getBuilder('name', \is_object($data) ? \get_class($data) : null, $options) + return $this->getBuilder('name', \is_object($data) ? $data::class : null, $options) ->setData($data) ->setCompound(true) ->setDataMapper(new DataMapper()) diff --git a/Tests/Extension/Validator/EventListener/ValidationListenerTest.php b/Tests/Extension/Validator/EventListener/ValidationListenerTest.php index ba01183915..b3f900e87e 100644 --- a/Tests/Extension/Validator/EventListener/ValidationListenerTest.php +++ b/Tests/Extension/Validator/EventListener/ValidationListenerTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper; use Symfony\Component\Form\Extension\Validator\Constraints\Form as FormConstraint; use Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener; @@ -23,7 +22,6 @@ use Symfony\Component\Form\FormConfigBuilder; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormFactoryBuilder; -use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\ConstraintViolationList; @@ -36,36 +34,14 @@ class ValidationListenerTest extends TestCase { - /** - * @var EventDispatcherInterface - */ - private $dispatcher; - - /** - * @var FormFactoryInterface - */ - private $factory; - - /** - * @var ValidatorInterface - */ - private $validator; - - /** - * @var ValidationListener - */ - private $listener; - - private $message; - - private $messageTemplate; - - private $params; + private ValidatorInterface $validator; + private ValidationListener $listener; + private string $message; + private string $messageTemplate; + private array $params; protected function setUp(): void { - $this->dispatcher = new EventDispatcher(); - $this->factory = (new FormFactoryBuilder())->getFormFactory(); $this->validator = Validation::createValidator(); $this->listener = new ValidationListener($this->validator, new ViolationMapper()); $this->message = 'Message'; @@ -153,7 +129,7 @@ public function isSynchronized(): bool class DummyValidator implements ValidatorInterface { - private $violation; + private ConstraintViolationInterface $violation; public function __construct(ConstraintViolationInterface $violation) { diff --git a/Tests/Extension/Validator/Type/BaseValidatorExtensionTestCase.php b/Tests/Extension/Validator/Type/BaseValidatorExtensionTestCase.php index 47fc264fc7..2ade3b844b 100644 --- a/Tests/Extension/Validator/Type/BaseValidatorExtensionTestCase.php +++ b/Tests/Extension/Validator/Type/BaseValidatorExtensionTestCase.php @@ -57,7 +57,7 @@ public function testValidationGroupsCanBeSetToFalse() public function testValidationGroupsCanBeSetToCallback() { $form = $this->createForm([ - 'validation_groups' => [$this, 'testValidationGroupsCanBeSetToCallback'], + 'validation_groups' => $this->testValidationGroupsCanBeSetToCallback(...), ]); $this->assertIsCallable($form->getConfig()->getOption('validation_groups')); diff --git a/Tests/Extension/Validator/Type/BirthdayTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/BirthdayTypeValidatorExtensionTest.php index 5d4a204376..9fb1e24219 100644 --- a/Tests/Extension/Validator/Type/BirthdayTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/BirthdayTypeValidatorExtensionTest.php @@ -11,18 +11,16 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\BirthdayType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class BirthdayTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) { - return $this->factory->create(BirthdayType::class, null, $options); + return $this->factory->create(BirthdayType::class, null, $options + ['widget' => 'choice']); } public function testInvalidMessage() @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('Please enter a valid birthdate.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/CheckboxTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/CheckboxTypeValidatorExtensionTest.php index 7cbf318cc8..361c0539c3 100644 --- a/Tests/Extension/Validator/Type/CheckboxTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/CheckboxTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class CheckboxTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('The checkbox has an invalid value.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/ChoiceTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/ChoiceTypeValidatorExtensionTest.php index ee75e73292..3f4db70fd5 100644 --- a/Tests/Extension/Validator/Type/ChoiceTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/ChoiceTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class ChoiceTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('The selected choice is invalid.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/CollectionTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/CollectionTypeValidatorExtensionTest.php index 89a573d53b..c6f11c24aa 100644 --- a/Tests/Extension/Validator/Type/CollectionTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/CollectionTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\CollectionType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class CollectionTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('The collection is invalid.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/ColorTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/ColorTypeValidatorExtensionTest.php index 64d0480868..766beeec05 100644 --- a/Tests/Extension/Validator/Type/ColorTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/ColorTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\ColorType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class ColorTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('Please select a valid color.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/CountryTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/CountryTypeValidatorExtensionTest.php index 5b62d95044..c8b2bd3424 100644 --- a/Tests/Extension/Validator/Type/CountryTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/CountryTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\CountryType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class CountryTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('Please select a valid country.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/CurrencyTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/CurrencyTypeValidatorExtensionTest.php index 1c7660052a..134be31154 100644 --- a/Tests/Extension/Validator/Type/CurrencyTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/CurrencyTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\CurrencyType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class CurrencyTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('Please select a valid currency.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/DateIntervalTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/DateIntervalTypeValidatorExtensionTest.php index a0f3580e2d..ea2f4998c0 100644 --- a/Tests/Extension/Validator/Type/DateIntervalTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/DateIntervalTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\DateIntervalType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class DateIntervalTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('Please choose a valid date interval.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/DateTimeTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/DateTimeTypeValidatorExtensionTest.php index 67312e0a57..df601ab870 100644 --- a/Tests/Extension/Validator/Type/DateTimeTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/DateTimeTypeValidatorExtensionTest.php @@ -11,18 +11,16 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class DateTimeTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) { - return $this->factory->create(DateTimeType::class, null, $options); + return $this->factory->create(DateTimeType::class, null, $options + ['widget' => 'choice']); } public function testInvalidMessage() @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('Please enter a valid date and time.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/DateTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/DateTypeValidatorExtensionTest.php index aa6336ca79..163fe91fd0 100644 --- a/Tests/Extension/Validator/Type/DateTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/DateTypeValidatorExtensionTest.php @@ -11,18 +11,16 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class DateTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) { - return $this->factory->create(DateType::class, null, $options); + return $this->factory->create(DateType::class, null, $options + ['widget' => 'choice']); } public function testInvalidMessage() @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('Please enter a valid date.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/EmailTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/EmailTypeValidatorExtensionTest.php index 29fdf24965..89e69749c3 100644 --- a/Tests/Extension/Validator/Type/EmailTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/EmailTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class EmailTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('Please enter a valid email address.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/FileTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/FileTypeValidatorExtensionTest.php index 5d7130f299..84045d6ade 100644 --- a/Tests/Extension/Validator/Type/FileTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/FileTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class FileTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('Please select a valid file.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php index 87de70e3c5..a1d1a38402 100644 --- a/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php @@ -11,14 +11,16 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Validator\ValidatorExtension; 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; @@ -31,7 +33,6 @@ class FormTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; public function testSubmitValidatesData() @@ -115,7 +116,7 @@ public function testManyFieldsGroupSequenceWithConstraintsOption() return $formMetadata; } - return new ClassMetadata(\is_string($classOrObject) ? $classOrObject : \get_class($classOrObject)); + return new ClassMetadata(\is_string($classOrObject) ? $classOrObject : $classOrObject::class); }) ; @@ -156,20 +157,189 @@ public function testInvalidMessage() $this->assertEquals('This value is not valid.', $form->getConfig()->getOption('invalid_message')); } - /** - * @group legacy - */ - public function testLegacyInvalidMessage() + protected function createForm(array $options = []) + { + return $this->factory->create(FormTypeTest::TESTED_TYPE, null, $options); + } + + public function testCollectionTypeKeepAsListOptionFalse() { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); + $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; + } - $form = $this->createForm(['legacy_error_messages' => true]); + if (Organization::class === $classOrObject || $classOrObject instanceof Organization) { + return $organizationMetadata; + } - $this->assertEquals('This value is not valid.', $form->getConfig()->getOption('invalid_message')); + 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)); } - protected function createForm(array $options = []) + public function testCollectionTypeKeepAsListOptionTrue() { - return $this->factory->create(FormTypeTest::TESTED_TYPE, null, $options); + $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/HiddenTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/HiddenTypeValidatorExtensionTest.php index 090d940e77..83de73f996 100644 --- a/Tests/Extension/Validator/Type/HiddenTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/HiddenTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class HiddenTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('The hidden field is invalid.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/IntegerTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/IntegerTypeValidatorExtensionTest.php index 0402a234b8..0621d6d8d0 100644 --- a/Tests/Extension/Validator/Type/IntegerTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/IntegerTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class IntegerTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('Please enter an integer.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/LanguageTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/LanguageTypeValidatorExtensionTest.php index e3cf797d17..9343a9a123 100644 --- a/Tests/Extension/Validator/Type/LanguageTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/LanguageTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\LanguageType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class LanguageTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('Please select a valid language.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/LocaleTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/LocaleTypeValidatorExtensionTest.php index 39dfa1f1a8..e2e948b5db 100644 --- a/Tests/Extension/Validator/Type/LocaleTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/LocaleTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\LocaleType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class LocaleTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('Please select a valid locale.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/MoneyTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/MoneyTypeValidatorExtensionTest.php index e6dadefcd3..e457a4b2a7 100644 --- a/Tests/Extension/Validator/Type/MoneyTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/MoneyTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\MoneyType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class MoneyTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('Please enter a valid money amount.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/NumberTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/NumberTypeValidatorExtensionTest.php index 262c9c1d52..6e886cecdc 100644 --- a/Tests/Extension/Validator/Type/NumberTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/NumberTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class NumberTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('Please enter a number.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/PasswordTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/PasswordTypeValidatorExtensionTest.php index 3bc242f8c3..5ddc0d4cb0 100644 --- a/Tests/Extension/Validator/Type/PasswordTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/PasswordTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class PasswordTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('The password is invalid.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/PercentTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/PercentTypeValidatorExtensionTest.php index 2be9abe3c6..d4317eb17a 100644 --- a/Tests/Extension/Validator/Type/PercentTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/PercentTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\PercentType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class PercentTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('Please enter a percentage value.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/RadioTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/RadioTypeValidatorExtensionTest.php index 074c336c22..c6ac73544c 100644 --- a/Tests/Extension/Validator/Type/RadioTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/RadioTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\RadioType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class RadioTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('Please select a valid option.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/RangeTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/RangeTypeValidatorExtensionTest.php index 4f8555a132..0ace710977 100644 --- a/Tests/Extension/Validator/Type/RangeTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/RangeTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\RangeType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class RangeTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('Please choose a valid range.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/RepeatedTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/RepeatedTypeValidatorExtensionTest.php index 43e2aa4da3..99c521682b 100644 --- a/Tests/Extension/Validator/Type/RepeatedTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/RepeatedTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\RepeatedType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class RepeatedTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('The values do not match.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/SearchTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/SearchTypeValidatorExtensionTest.php index b5429d8931..14d4350e5a 100644 --- a/Tests/Extension/Validator/Type/SearchTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/SearchTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\SearchType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class SearchTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('Please enter a valid search term.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/TelTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/TelTypeValidatorExtensionTest.php index b1eb1b1ffc..ff6ec95c83 100644 --- a/Tests/Extension/Validator/Type/TelTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/TelTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\TelType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class TelTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('Please provide a valid phone number.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/TimeTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/TimeTypeValidatorExtensionTest.php index c7213603d3..ab85366e17 100644 --- a/Tests/Extension/Validator/Type/TimeTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/TimeTypeValidatorExtensionTest.php @@ -11,18 +11,16 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\TimeType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class TimeTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) { - return $this->factory->create(TimeType::class, null, $options); + return $this->factory->create(TimeType::class, null, $options + ['widget' => 'choice']); } public function testInvalidMessage() @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('Please enter a valid time.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/TimezoneTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/TimezoneTypeValidatorExtensionTest.php index 0c013f053c..3ee0f5d202 100644 --- a/Tests/Extension/Validator/Type/TimezoneTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/TimezoneTypeValidatorExtensionTest.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\TimezoneType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class TimezoneTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; protected function createForm(array $options = []) @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('Please select a valid timezone.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Type/UploadValidatorExtensionTest.php b/Tests/Extension/Validator/Type/UploadValidatorExtensionTest.php index 34c911f52a..96c39c5cae 100644 --- a/Tests/Extension/Validator/Type/UploadValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/UploadValidatorExtensionTest.php @@ -26,11 +26,7 @@ public function testPostMaxSizeTranslation() $resolver = new OptionsResolver(); $resolver->setDefault('post_max_size_message', 'old max {{ max }}!'); - $resolver->setDefault('upload_max_size_message', function (Options $options) { - return function () use ($options) { - return $options['post_max_size_message']; - }; - }); + $resolver->setDefault('upload_max_size_message', fn (Options $options) => fn () => $options['post_max_size_message']); $extension->configureOptions($resolver); $options = $resolver->resolve(); @@ -46,7 +42,7 @@ public function trans($id, array $parameters = [], $domain = null, $locale = nul return 'translated max {{ max }}!'; } - public function setLocale($locale) + public function setLocale($locale): void { } diff --git a/Tests/Extension/Validator/Type/UrlTypeValidatorExtensionTest.php b/Tests/Extension/Validator/Type/UrlTypeValidatorExtensionTest.php index 574cb1d62e..edb212cbd4 100644 --- a/Tests/Extension/Validator/Type/UrlTypeValidatorExtensionTest.php +++ b/Tests/Extension/Validator/Type/UrlTypeValidatorExtensionTest.php @@ -11,18 +11,16 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\Extension\Core\Type\UrlType; use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait; class UrlTypeValidatorExtensionTest extends BaseValidatorExtensionTestCase { - use ExpectDeprecationTrait; use ValidatorExtensionTrait; 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() @@ -31,18 +29,4 @@ public function testInvalidMessage() $this->assertSame('Please enter a valid URL.', $form->getConfig()->getOption('invalid_message')); } - - /** - * @group legacy - */ - public function testLegacyInvalidMessage() - { - $this->expectDeprecation('Since symfony/form 5.2: Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.'); - - $form = $this->createForm([ - 'legacy_error_messages' => true, - ]); - - $this->assertSame('This value is not valid.', $form->getConfig()->getOption('invalid_message')); - } } diff --git a/Tests/Extension/Validator/Util/LegacyServerParamsTest.php b/Tests/Extension/Validator/Util/LegacyServerParamsTest.php deleted file mode 100644 index 8b0b0d7ecf..0000000000 --- a/Tests/Extension/Validator/Util/LegacyServerParamsTest.php +++ /dev/null @@ -1,31 +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\Extension\Validator\Util; - -use PHPUnit\Framework\TestCase; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; -use Symfony\Component\Form\Extension\Validator\Util\ServerParams; - -class LegacyServerParamsTest extends TestCase -{ - use ExpectDeprecationTrait; - - /** - * @group legacy - */ - public function testClassIsDeprecated() - { - $this->expectDeprecation('Since symfony/form 5.1: The "Symfony\Component\Form\Extension\Validator\Util\ServerParams" class is deprecated. Use "Symfony\Component\Form\Util\ServerParams" instead.'); - - new ServerParams(); - } -} diff --git a/Tests/Extension/Validator/ValidatorTypeGuesserTest.php b/Tests/Extension/Validator/ValidatorTypeGuesserTest.php index 8648dc3d49..c561cd76f2 100644 --- a/Tests/Extension/Validator/ValidatorTypeGuesserTest.php +++ b/Tests/Extension/Validator/ValidatorTypeGuesserTest.php @@ -42,23 +42,15 @@ class ValidatorTypeGuesserTest extends TestCase { public const TEST_CLASS = 'Symfony\Component\Form\Tests\Extension\Validator\ValidatorTypeGuesserTest_TestClass'; - public const TEST_PROPERTY = 'property'; - /** - * @var ValidatorTypeGuesser - */ - private $guesser; - - /** - * @var ClassMetadata - */ - private $metadata; + private ValidatorTypeGuesser $guesser; + private ClassMetadata $metadata; /** * @var MetadataFactoryInterface */ - private $metadataFactory; + private FakeMetadataFactory $metadataFactory; protected function setUp(): void { @@ -93,6 +85,7 @@ public static function guessTypeProvider() [new Type('long'), new TypeGuess(IntegerType::class, [], Guess::MEDIUM_CONFIDENCE)], [new Type('string'), new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE)], [new Type(\DateTime::class), new TypeGuess(DateType::class, [], Guess::MEDIUM_CONFIDENCE)], + [new Type(\DateTimeImmutable::class), new TypeGuess(DateType::class, ['input' => 'datetime_immutable'], Guess::MEDIUM_CONFIDENCE)], [new Type('\DateTime'), new TypeGuess(DateType::class, [], Guess::MEDIUM_CONFIDENCE)], ]; } diff --git a/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php b/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php index 5a9658a74c..0aeb35adcc 100644 --- a/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php +++ b/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper; @@ -43,30 +42,11 @@ class ViolationMapperTest extends TestCase private const LEVEL_1B = 2; private const LEVEL_2 = 3; - /** - * @var EventDispatcherInterface - */ - private $dispatcher; - - /** - * @var ViolationMapper - */ - private $mapper; - - /** - * @var string - */ - private $message; - - /** - * @var string - */ - private $messageTemplate; - - /** - * @var array - */ - private $params; + private EventDispatcher $dispatcher; + private ViolationMapper $mapper; + private string $message; + private string $messageTemplate; + private array $params; protected function setUp(): void { @@ -91,8 +71,8 @@ protected function getForm($name = 'name', $propertyPath = null, $dataClass = nu if (!$synchronized) { $config->addViewTransformer(new CallbackTransformer( - function ($normData) { return $normData; }, - function () { throw new TransformationFailedException(); } + static fn ($normData) => $normData, + static fn () => throw new TransformationFailedException() )); } diff --git a/Tests/Extension/Validator/ViolationMapper/ViolationPathTest.php b/Tests/Extension/Validator/ViolationMapper/ViolationPathTest.php index eba3210abd..ebae56eb42 100644 --- a/Tests/Extension/Validator/ViolationMapper/ViolationPathTest.php +++ b/Tests/Extension/Validator/ViolationMapper/ViolationPathTest.php @@ -89,9 +89,7 @@ public static function providePaths() */ public function testCreatePath($string, $entries, $slicedPath = null) { - if (null === $slicedPath) { - $slicedPath = $string; - } + $slicedPath ??= $string; $path = new ViolationPath($string); diff --git a/Tests/Fixtures/AlternatingRowType.php b/Tests/Fixtures/AlternatingRowType.php index 556166f554..3e0bb40c2b 100644 --- a/Tests/Fixtures/AlternatingRowType.php +++ b/Tests/Fixtures/AlternatingRowType.php @@ -9,7 +9,7 @@ class AlternatingRowType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) { $form = $event->getForm(); diff --git a/Tests/Fixtures/ArrayChoiceLoader.php b/Tests/Fixtures/ArrayChoiceLoader.php index 7224d0e936..8c5a3a4733 100644 --- a/Tests/Fixtures/ArrayChoiceLoader.php +++ b/Tests/Fixtures/ArrayChoiceLoader.php @@ -8,8 +8,6 @@ class ArrayChoiceLoader extends CallbackChoiceLoader { public function __construct(array $choices = []) { - parent::__construct(static function () use ($choices): array { - return $choices; - }); + parent::__construct(static fn (): array => $choices); } } diff --git a/Tests/Fixtures/AuthorType.php b/Tests/Fixtures/AuthorType.php index 84c988984f..a55dbfeacc 100644 --- a/Tests/Fixtures/AuthorType.php +++ b/Tests/Fixtures/AuthorType.php @@ -8,7 +8,7 @@ class AuthorType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('firstName') @@ -16,7 +16,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) ; } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => 'Symfony\Component\Form\Tests\Fixtures\Author', diff --git a/Tests/Fixtures/BlockPrefixedFooTextType.php b/Tests/Fixtures/BlockPrefixedFooTextType.php index 3fda7a55dd..fa145a49c2 100644 --- a/Tests/Fixtures/BlockPrefixedFooTextType.php +++ b/Tests/Fixtures/BlockPrefixedFooTextType.php @@ -16,7 +16,7 @@ class BlockPrefixedFooTextType extends AbstractType { - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefault('block_prefix', 'foo'); } diff --git a/Tests/Fixtures/ChoiceList/DeprecatedChoiceListFactory.php b/Tests/Fixtures/ChoiceList/DeprecatedChoiceListFactory.php deleted file mode 100644 index 731262a84d..0000000000 --- a/Tests/Fixtures/ChoiceList/DeprecatedChoiceListFactory.php +++ /dev/null @@ -1,23 +0,0 @@ -setDefaults(['expanded' => true]); - $resolver->setNormalizer('choices', function () { - return [ - 'attr1' => 'Attribute 1', - 'attr2' => 'Attribute 2', - ]; - }); + $resolver->setNormalizer('choices', fn () => [ + 'attr1' => 'Attribute 1', + 'attr2' => 'Attribute 2', + ]); } - /** - * {@inheritdoc} - */ public function getParent(): ?string { return ChoiceType::class; diff --git a/Tests/Fixtures/ChoiceTypeExtension.php b/Tests/Fixtures/ChoiceTypeExtension.php index 34242c5b69..3549763f16 100644 --- a/Tests/Fixtures/ChoiceTypeExtension.php +++ b/Tests/Fixtures/ChoiceTypeExtension.php @@ -18,10 +18,7 @@ class ChoiceTypeExtension extends AbstractTypeExtension { public static $extendedType; - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefault('choices', [ 'A' => 'a', @@ -29,9 +26,6 @@ public function configureOptions(OptionsResolver $resolver) ]); } - /** - * {@inheritdoc} - */ public static function getExtendedTypes(): iterable { return [self::$extendedType]; diff --git a/Tests/Fixtures/CustomArrayObject.php b/Tests/Fixtures/CustomArrayObject.php index 7a4db4fe28..8be0323ae1 100644 --- a/Tests/Fixtures/CustomArrayObject.php +++ b/Tests/Fixtures/CustomArrayObject.php @@ -15,9 +15,9 @@ * This class is a hand written simplified version of PHP native `ArrayObject` * class, to show that it behaves differently than the PHP native implementation. */ -class CustomArrayObject implements \ArrayAccess, \IteratorAggregate, \Countable, \Serializable +class CustomArrayObject implements \ArrayAccess, \IteratorAggregate, \Countable { - private $array; + private array $array; public function __construct(?array $array = null) { @@ -29,13 +29,7 @@ public function offsetExists($offset): bool return \array_key_exists($offset, $this->array); } - /** - * @param mixed $offset - * - * @return mixed - */ - #[\ReturnTypeWillChange] - public function offsetGet($offset) + public function offsetGet($offset): mixed { return $this->array[$offset]; } @@ -69,18 +63,8 @@ public function __serialize(): array return $this->array; } - public function serialize(): string - { - return serialize($this->__serialize()); - } - public function __unserialize(array $data): void { $this->array = $data; } - - public function unserialize($serialized): void - { - $this->__unserialize((array) unserialize((string) $serialized)); - } } diff --git a/Tests/Fixtures/Descriptor/default_option_with_normalizer.txt b/Tests/Fixtures/Descriptor/default_option_with_normalizer.txt index 4a226f576d..0f6f1f40e7 100644 --- a/Tests/Fixtures/Descriptor/default_option_with_normalizer.txt +++ b/Tests/Fixtures/Descriptor/default_option_with_normalizer.txt @@ -20,7 +20,6 @@ Symfony\Component\Form\Extension\Core\Type\ChoiceType (choice_translation_domain Normalizers [ %s Closure(%s class:%s - this: %s file: %s line: %s } %s diff --git a/Tests/Fixtures/Descriptor/resolved_form_type_1.json b/Tests/Fixtures/Descriptor/resolved_form_type_1.json index c9f453c8d6..5590018c09 100644 --- a/Tests/Fixtures/Descriptor/resolved_form_type_1.json +++ b/Tests/Fixtures/Descriptor/resolved_form_type_1.json @@ -6,17 +6,22 @@ "choice_attr", "choice_filter", "choice_label", + "choice_lazy", "choice_loader", "choice_name", "choice_translation_domain", "choice_translation_parameters", "choice_value", "choices", + "duplicate_preferred_choices", "expanded", "group_by", "multiple", "placeholder", - "preferred_choices" + "placeholder_attr", + "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 c4c4e90180..93c6b66d98 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 - expanded by_reference - group_by data - multiple disabled - placeholder form_attr - preferred_choices getter - help - 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/FixedDataTransformer.php b/Tests/Fixtures/FixedDataTransformer.php index 2939116096..f3121fc103 100644 --- a/Tests/Fixtures/FixedDataTransformer.php +++ b/Tests/Fixtures/FixedDataTransformer.php @@ -16,14 +16,14 @@ class FixedDataTransformer implements DataTransformerInterface { - private $mapping; + private array $mapping; public function __construct(array $mapping) { $this->mapping = $mapping; } - public function transform($value) + public function transform($value): mixed { if (!\array_key_exists($value, $this->mapping)) { throw new TransformationFailedException(sprintf('No mapping for value "%s"', $value)); @@ -32,7 +32,7 @@ public function transform($value) return $this->mapping[$value]; } - public function reverseTransform($value) + public function reverseTransform($value): mixed { $result = array_search($value, $this->mapping, true); diff --git a/Tests/Fixtures/FixedTranslator.php b/Tests/Fixtures/FixedTranslator.php index 098ef83e4a..432f2ab12d 100644 --- a/Tests/Fixtures/FixedTranslator.php +++ b/Tests/Fixtures/FixedTranslator.php @@ -15,7 +15,7 @@ class FixedTranslator implements TranslatorInterface { - private $translations; + private array $translations; public function __construct(array $translations) { diff --git a/Tests/Fixtures/FooTypeBarExtension.php b/Tests/Fixtures/FooTypeBarExtension.php index 70c710e922..477f36148e 100644 --- a/Tests/Fixtures/FooTypeBarExtension.php +++ b/Tests/Fixtures/FooTypeBarExtension.php @@ -16,12 +16,12 @@ class FooTypeBarExtension extends AbstractTypeExtension { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->setAttribute('bar', 'x'); } - public function getAllowedOptionValues() + public function getAllowedOptionValues(): array { return [ 'a_or_b' => ['c'], diff --git a/Tests/Fixtures/FooTypeBazExtension.php b/Tests/Fixtures/FooTypeBazExtension.php index a11f158844..9720439eb1 100644 --- a/Tests/Fixtures/FooTypeBazExtension.php +++ b/Tests/Fixtures/FooTypeBazExtension.php @@ -16,7 +16,7 @@ class FooTypeBazExtension extends AbstractTypeExtension { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->setAttribute('baz', 'x'); } diff --git a/Tests/Fixtures/LazyChoiceTypeExtension.php b/Tests/Fixtures/LazyChoiceTypeExtension.php index 20fe789cd7..0c7e294144 100644 --- a/Tests/Fixtures/LazyChoiceTypeExtension.php +++ b/Tests/Fixtures/LazyChoiceTypeExtension.php @@ -19,22 +19,14 @@ class LazyChoiceTypeExtension extends AbstractTypeExtension { public static $extendedType; - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { - $resolver->setDefault('choice_loader', ChoiceList::lazy($this, function () { - return [ - 'Lazy A' => 'lazy_a', - 'Lazy B' => 'lazy_b', - ]; - })); + $resolver->setDefault('choice_loader', ChoiceList::lazy($this, fn () => [ + 'Lazy A' => 'lazy_a', + 'Lazy B' => 'lazy_b', + ])); } - /** - * {@inheritdoc} - */ public static function getExtendedTypes(): iterable { return [self::$extendedType]; diff --git a/Tests/Fixtures/Map.php b/Tests/Fixtures/Map.php index 93409aceee..ffe89e0875 100644 --- a/Tests/Fixtures/Map.php +++ b/Tests/Fixtures/Map.php @@ -13,18 +13,14 @@ class Map implements \ArrayAccess { - private $data = []; + private array $data = []; public function offsetExists($offset): bool { return isset($this->data[$offset]); } - /** - * @return mixed - */ - #[\ReturnTypeWillChange] - public function offsetGet($offset) + public function offsetGet($offset): mixed { return $this->data[$offset]; } diff --git a/Tests/Fixtures/NotMappedType.php b/Tests/Fixtures/NotMappedType.php index 14c340b891..3d55122e79 100644 --- a/Tests/Fixtures/NotMappedType.php +++ b/Tests/Fixtures/NotMappedType.php @@ -16,7 +16,7 @@ class NotMappedType extends AbstractType { - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefault('mapped', false); } diff --git a/Tests/Fixtures/Organization.php b/Tests/Fixtures/Organization.php new file mode 100644 index 0000000000..db9cee9f96 --- /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/RepeatedPasswordField.php b/Tests/Fixtures/RepeatedPasswordField.php new file mode 100644 index 0000000000..605c81c4c7 --- /dev/null +++ b/Tests/Fixtures/RepeatedPasswordField.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Fixtures; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\PasswordType; +use Symfony\Component\Form\Extension\Core\Type\RepeatedType; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class RepeatedPasswordField extends AbstractType +{ + public function getParent(): ?string + { + return RepeatedType::class; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'mapped' => false, + 'type' => PasswordType::class, + 'first_options' => [ + 'hash_property_path' => 'password', + ], + ]); + } +} diff --git a/Tests/Fixtures/TestExtension.php b/Tests/Fixtures/TestExtension.php index 3f7c4005fe..2704ee5303 100644 --- a/Tests/Fixtures/TestExtension.php +++ b/Tests/Fixtures/TestExtension.php @@ -18,11 +18,9 @@ class TestExtension implements FormExtensionInterface { - private $types = []; - - private $extensions = []; - - private $guesser; + private array $types = []; + private array $extensions = []; + private FormTypeGuesserInterface $guesser; public function __construct(FormTypeGuesserInterface $guesser) { @@ -31,12 +29,12 @@ public function __construct(FormTypeGuesserInterface $guesser) public function addType(FormTypeInterface $type) { - $this->types[\get_class($type)] = $type; + $this->types[$type::class] = $type; } public function getType($name): FormTypeInterface { - return $this->types[$name] ?? null; + return $this->types[$name]; } public function hasType($name): bool diff --git a/Tests/Fixtures/TranslatableTextAlign.php b/Tests/Fixtures/TranslatableTextAlign.php new file mode 100644 index 0000000000..4464088c78 --- /dev/null +++ b/Tests/Fixtures/TranslatableTextAlign.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Fixtures; + +use Symfony\Contracts\Translation\TranslatableInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +enum TranslatableTextAlign implements TranslatableInterface +{ + case Left; + case Center; + case Right; + + public function trans(TranslatorInterface $translator, ?string $locale = null): string + { + return $translator->trans($this->name, locale: $locale); + } +} diff --git a/Tests/Fixtures/User.php b/Tests/Fixtures/User.php new file mode 100644 index 0000000000..a637a4e1fe --- /dev/null +++ b/Tests/Fixtures/User.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Fixtures; + +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; + +class User implements PasswordAuthenticatedUserInterface +{ + private ?string $password = null; + + public function getPassword(): ?string + { + return $this->password; + } + + public function setPassword(string $password): self + { + $this->password = $password; + + return $this; + } +} diff --git a/Tests/FormBuilderTest.php b/Tests/FormBuilderTest.php index dfa2d61ac7..023cf77d60 100644 --- a/Tests/FormBuilderTest.php +++ b/Tests/FormBuilderTest.php @@ -15,7 +15,6 @@ use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\Form\ButtonBuilder; use Symfony\Component\Form\Exception\InvalidArgumentException; -use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Form; @@ -28,8 +27,8 @@ class FormBuilderTest extends TestCase { - private $factory; - private $builder; + private FormFactory $factory; + private FormBuilder $builder; protected function setUp(): void { @@ -48,12 +47,6 @@ public function testNoSetName() $this->assertFalse(method_exists($this->builder, 'setName')); } - public function testAddNameNoStringAndNoInteger() - { - $this->expectException(UnexpectedTypeException::class); - $this->builder->add(true); - } - public function testAddWithGuessFluent() { $rootFormBuilder = new FormBuilder('name', 'stdClass', new EventDispatcher(), $this->factory); @@ -180,9 +173,6 @@ public function testGetFormConfigErasesReferences() $children = $reflClass->getProperty('children'); $unresolvedChildren = $reflClass->getProperty('unresolvedChildren'); - $children->setAccessible(true); - $unresolvedChildren->setAccessible(true); - $this->assertEmpty($children->getValue($config)); $this->assertEmpty($unresolvedChildren->getValue($config)); } diff --git a/Tests/FormErrorIteratorTest.php b/Tests/FormErrorIteratorTest.php index 8e9ecac51b..56472c82e9 100644 --- a/Tests/FormErrorIteratorTest.php +++ b/Tests/FormErrorIteratorTest.php @@ -28,10 +28,6 @@ class FormErrorIteratorTest extends TestCase */ public function testFindByCodes($code, $violationsCount) { - if (!class_exists(ConstraintViolation::class)) { - $this->markTestSkipped('Validator component required.'); - } - $formBuilder = new FormBuilder( 'form', null, diff --git a/Tests/FormFactoryBuilderTest.php b/Tests/FormFactoryBuilderTest.php index fbabec1dd0..13e6e30a16 100644 --- a/Tests/FormFactoryBuilderTest.php +++ b/Tests/FormFactoryBuilderTest.php @@ -19,14 +19,13 @@ class FormFactoryBuilderTest extends TestCase { - private $registry; - private $type; + private \ReflectionProperty $registry; + private FooType $type; protected function setUp(): void { $factory = new \ReflectionClass(FormFactory::class); $this->registry = $factory->getProperty('registry'); - $this->registry->setAccessible(true); $this->type = new FooType(); } @@ -41,7 +40,7 @@ public function testAddType() $extensions = $registry->getExtensions(); $this->assertCount(1, $extensions); - $this->assertTrue($extensions[0]->hasType(\get_class($this->type))); + $this->assertTrue($extensions[0]->hasType($this->type::class)); $this->assertNull($extensions[0]->getTypeGuesser()); } diff --git a/Tests/FormFactoryTest.php b/Tests/FormFactoryTest.php index ab13c67775..0cb3cad730 100644 --- a/Tests/FormFactoryTest.php +++ b/Tests/FormFactoryTest.php @@ -16,7 +16,6 @@ use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormFactory; use Symfony\Component\Form\FormRegistry; -use Symfony\Component\Form\FormRegistryInterface; use Symfony\Component\Form\FormTypeGuesserChain; use Symfony\Component\Form\FormTypeGuesserInterface; use Symfony\Component\Form\Guess\Guess; @@ -31,25 +30,10 @@ */ class FormFactoryTest extends TestCase { - /** - * @var ConfigurableFormTypeGuesser - */ - private $guesser1; - - /** - * @var ConfigurableFormTypeGuesser - */ - private $guesser2; - - /** - * @var FormRegistryInterface - */ - private $registry; - - /** - * @var FormFactory - */ - private $factory; + private ConfigurableFormTypeGuesser $guesser1; + private ConfigurableFormTypeGuesser $guesser2; + private FormRegistry $registry; + private FormFactory $factory; protected function setUp(): void { @@ -189,10 +173,10 @@ public function testCreateBuilderUsesPatternIfFound() class ConfigurableFormTypeGuesser implements FormTypeGuesserInterface { - private $typeGuess; - private $requiredGuess; - private $maxLengthGuess; - private $patternGuess; + 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/FormRegistryTest.php b/Tests/FormRegistryTest.php index 8cee7282a4..e2f226924c 100644 --- a/Tests/FormRegistryTest.php +++ b/Tests/FormRegistryTest.php @@ -35,20 +35,9 @@ */ class FormRegistryTest extends TestCase { - /** - * @var FormRegistry - */ - private $registry; - - /** - * @var TestExtension - */ - private $extension1; - - /** - * @var TestExtension - */ - private $extension2; + private FormRegistry $registry; + private TestExtension $extension1; + private TestExtension $extension2; protected function setUp(): void { diff --git a/Tests/NativeRequestHandlerTest.php b/Tests/NativeRequestHandlerTest.php index e698138376..6ff64bc657 100644 --- a/Tests/NativeRequestHandlerTest.php +++ b/Tests/NativeRequestHandlerTest.php @@ -19,7 +19,7 @@ */ class NativeRequestHandlerTest extends AbstractRequestHandlerTestCase { - private static $serverBackup; + private static array $serverBackup; public static function setUpBeforeClass(): void { @@ -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 4d4aa5f30b..fa28f75472 100644 --- a/Tests/ResolvedFormTypeTest.php +++ b/Tests/ResolvedFormTypeTest.php @@ -19,11 +19,8 @@ use Symfony\Component\Form\FormBuilder; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormFactory; -use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormRegistry; -use Symfony\Component\Form\FormTypeExtensionInterface; -use Symfony\Component\Form\FormTypeInterface; use Symfony\Component\Form\FormView; use Symfony\Component\Form\ResolvedFormType; use Symfony\Component\Form\ResolvedFormTypeFactory; @@ -36,42 +33,14 @@ */ class ResolvedFormTypeTest extends TestCase { - private $calls; - - /** - * @var FormTypeInterface - */ - private $parentType; - - /** - * @var FormTypeInterface - */ - private $type; - - /** - * @var FormTypeExtensionInterface - */ - private $extension1; - - /** - * @var FormTypeExtensionInterface - */ - private $extension2; - - /** - * @var ResolvedFormType - */ - private $parentResolvedType; - - /** - * @var ResolvedFormType - */ - private $resolvedType; - - /** - * @var FormFactoryInterface - */ - private $formFactory; + private array $calls; + private UsageTrackingParentFormType $parentType; + private UsageTrackingFormType $type; + private UsageTrackingFormTypeExtension $extension1; + private UsageTrackingFormTypeExtension $extension2; + private ResolvedFormType $parentResolvedType; + private ResolvedFormType $resolvedType; + private FormFactory $formFactory; protected function setUp(): void { @@ -131,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'); } @@ -246,7 +215,7 @@ class UsageTrackingFormTypeExtension extends AbstractTypeExtension { use UsageTrackingTrait; - private $defaultOptions; + private array $defaultOptions; public function __construct(array &$calls, array $defaultOptions) { @@ -267,7 +236,7 @@ public static function getExtendedTypes(): iterable trait UsageTrackingTrait { - private $calls; + private array $calls; public function buildForm(FormBuilderInterface $builder, array $options): void { diff --git a/Tests/Resources/TranslationFilesTest.php b/Tests/Resources/TranslationFilesTest.php index f732ba9e00..157335dc6d 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')))); } /** @@ -36,19 +36,16 @@ public function testTranslationFileIsValidWithoutEntityLoader($filePath) { $document = new \DOMDocument(); $document->loadXML(file_get_contents($filePath)); - if (\LIBXML_VERSION < 20900) { - libxml_disable_entity_loader(true); - } $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() { return array_map( - function ($filePath) { return (array) $filePath; }, + fn ($filePath) => (array) $filePath, glob(\dirname(__DIR__, 2).'/Resources/translations/*.xlf') ); } diff --git a/Tests/SimpleFormTest.php b/Tests/SimpleFormTest.php index af091b49c9..d5d3549d2a 100644 --- a/Tests/SimpleFormTest.php +++ b/Tests/SimpleFormTest.php @@ -38,7 +38,7 @@ class SimpleFormTest_Countable implements \Countable { - private $count; + private int $count; public function __construct($count) { @@ -53,7 +53,7 @@ public function count(): int class SimpleFormTest_Traversable implements \IteratorAggregate { - private $iterator; + private \ArrayIterator $iterator; public function __construct($count) { @@ -68,7 +68,7 @@ public function getIterator(): \Traversable class SimpleFormTest extends TestCase { - private $form; + private Form $form; protected function setUp(): void { @@ -503,9 +503,9 @@ public function testSetDataConvertsScalarToStringIfOnlyModelTransformer() { $form = $this->getBuilder() ->addModelTransformer(new FixedDataTransformer([ - '' => '', - 1 => 23, - ])) + '' => '', + 1 => 23, + ])) ->getForm(); $form->setData(1); @@ -1112,12 +1112,12 @@ public function testIsEmptyCallback() { $config = new FormConfigBuilder('foo', null, new EventDispatcher()); - $config->setIsEmptyCallback(function ($modelData): bool { return 'ccc' === $modelData; }); + $config->setIsEmptyCallback(fn ($modelData): bool => 'ccc' === $modelData); $form = new Form($config); $form->setData('ccc'); $this->assertTrue($form->isEmpty()); - $config->setIsEmptyCallback(function (): bool { return false; }); + $config->setIsEmptyCallback(fn (): bool => false); $form = new Form($config); $form->setData(null); $this->assertFalse($form->isEmpty()); diff --git a/Tests/Util/OrderedHashMapTest.php b/Tests/Util/OrderedHashMapTest.php index 894da681d5..0cafaafad7 100644 --- a/Tests/Util/OrderedHashMapTest.php +++ b/Tests/Util/OrderedHashMapTest.php @@ -51,7 +51,7 @@ public function testInsertNullKeys() $map['foo'] = 2; $map[] = 3; - $this->assertSame([0 => 1, 'foo' => 2, 1 => 3], iterator_to_array($map)); + $this->assertSame(['0' => 1, 'foo' => 2, '1' => 3], iterator_to_array($map)); } public function testInsertLooselyEqualKeys() @@ -496,12 +496,30 @@ public function testParallelIteration() $this->assertNull($it1->current()); } + public function testKeysAreString() + { + $map = new OrderedHashMap(['1' => 1]); + $map['2'] = 2; + + $it = $map->getIterator(); + + $it->rewind(); + $this->assertTrue($it->valid()); + $this->assertSame('1', $it->key()); + $this->assertSame(1, $it->current()); + + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('2', $it->key()); + $this->assertSame(2, $it->current()); + } + public function testCount() { $map = new OrderedHashMap(); $map[] = 1; $map['foo'] = 2; - unset($map[0]); + unset($map['0']); $map[] = 3; $this->assertCount(2, $map); diff --git a/Tests/Util/ServerParamsTest.php b/Tests/Util/ServerParamsTest.php index ebe680e710..1904812d81 100644 --- a/Tests/Util/ServerParamsTest.php +++ b/Tests/Util/ServerParamsTest.php @@ -70,7 +70,7 @@ public static function getGetPostMaxSizeTestData() class DummyServerParams extends ServerParams { - private $size; + private string $size; public function __construct($size) { diff --git a/Tests/Util/StringUtilTest.php b/Tests/Util/StringUtilTest.php index 8199d6843e..d51481f6c1 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 9ab4797b30..0000000000 --- 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 $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 0000000000..62e98934e3 --- /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/FormUtil.php b/Util/FormUtil.php index 3485d800af..1a5cd3b15e 100644 --- a/Util/FormUtil.php +++ b/Util/FormUtil.php @@ -29,12 +29,8 @@ private function __construct() * This logic is reused multiple times throughout the processing of * a form and needs to be consistent. PHP keyword `empty` cannot * be used as it also considers 0 and "0" to be empty. - * - * @param mixed $data - * - * @return bool */ - public static function isEmpty($data) + public static function isEmpty(mixed $data): bool { // Should not do a check for [] === $data!!! // This method is used in occurrences where arrays are diff --git a/Util/InheritDataAwareIterator.php b/Util/InheritDataAwareIterator.php index 7cba17dbeb..26a2135223 100644 --- a/Util/InheritDataAwareIterator.php +++ b/Util/InheritDataAwareIterator.php @@ -25,22 +25,12 @@ */ class InheritDataAwareIterator extends \IteratorIterator implements \RecursiveIterator { - /** - * {@inheritdoc} - * - * @return static - */ - #[\ReturnTypeWillChange] - public function getChildren() + public function getChildren(): static { return new static($this->current()); } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function hasChildren() + public function hasChildren(): bool { return (bool) $this->current()->getConfig()->getInheritData(); } diff --git a/Util/OptionsResolverWrapper.php b/Util/OptionsResolverWrapper.php index 078f93dd9b..51cba4e08f 100644 --- a/Util/OptionsResolverWrapper.php +++ b/Util/OptionsResolverWrapper.php @@ -22,16 +22,16 @@ */ class OptionsResolverWrapper extends OptionsResolver { - private $undefined = []; + private array $undefined = []; /** * @return $this */ - public function setNormalizer(string $option, \Closure $normalizer): self + public function setNormalizer(string $option, \Closure $normalizer): static { try { parent::setNormalizer($option, $normalizer); - } catch (UndefinedOptionsException $e) { + } catch (UndefinedOptionsException) { $this->undefined[$option] = true; } @@ -41,11 +41,11 @@ public function setNormalizer(string $option, \Closure $normalizer): self /** * @return $this */ - public function setAllowedValues(string $option, $allowedValues): self + public function setAllowedValues(string $option, mixed $allowedValues): static { try { parent::setAllowedValues($option, $allowedValues); - } catch (UndefinedOptionsException $e) { + } catch (UndefinedOptionsException) { $this->undefined[$option] = true; } @@ -55,11 +55,11 @@ public function setAllowedValues(string $option, $allowedValues): self /** * @return $this */ - public function addAllowedValues(string $option, $allowedValues): self + public function addAllowedValues(string $option, mixed $allowedValues): static { try { parent::addAllowedValues($option, $allowedValues); - } catch (UndefinedOptionsException $e) { + } catch (UndefinedOptionsException) { $this->undefined[$option] = true; } @@ -71,11 +71,11 @@ public function addAllowedValues(string $option, $allowedValues): self * * @return $this */ - public function setAllowedTypes(string $option, $allowedTypes): self + public function setAllowedTypes(string $option, $allowedTypes): static { try { parent::setAllowedTypes($option, $allowedTypes); - } catch (UndefinedOptionsException $e) { + } catch (UndefinedOptionsException) { $this->undefined[$option] = true; } @@ -87,11 +87,11 @@ public function setAllowedTypes(string $option, $allowedTypes): self * * @return $this */ - public function addAllowedTypes(string $option, $allowedTypes): self + public function addAllowedTypes(string $option, $allowedTypes): static { try { parent::addAllowedTypes($option, $allowedTypes); - } catch (UndefinedOptionsException $e) { + } catch (UndefinedOptionsException) { $this->undefined[$option] = true; } diff --git a/Util/OrderedHashMap.php b/Util/OrderedHashMap.php index 7d84cbe22f..7f81c07390 100644 --- a/Util/OrderedHashMap.php +++ b/Util/OrderedHashMap.php @@ -64,77 +64,54 @@ * * @author Bernhard Schussek * - * @template TKey of array-key * @template TValue * - * @implements \ArrayAccess - * @implements \IteratorAggregate + * @implements \ArrayAccess + * @implements \IteratorAggregate */ class OrderedHashMap implements \ArrayAccess, \IteratorAggregate, \Countable { - /** - * The elements of the map, indexed by their keys. - * - * @var array - */ - private $elements = []; - /** * The keys of the map in the order in which they were inserted or changed. * - * @var list + * @var list */ - private $orderedKeys = []; + private array $orderedKeys = []; /** * References to the cursors of all open iterators. * * @var array */ - private $managedCursors = []; + private array $managedCursors = []; /** * Creates a new map. * - * @param array $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; - $this->orderedKeys = array_keys($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)); } - /** - * @return bool - */ - #[\ReturnTypeWillChange] - public function offsetExists($key) + public function offsetExists(mixed $key): bool { return isset($this->elements[$key]); } - /** - * {@inheritdoc} - * - * @return TValue - */ - #[\ReturnTypeWillChange] - public function offsetGet($key) + 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]; } - /** - * {@inheritdoc} - * - * @return void - */ - #[\ReturnTypeWillChange] - public function offsetSet($key, $value) + public function offsetSet(mixed $key, mixed $value): void { if (null === $key || !isset($this->elements[$key])) { if (null === $key) { @@ -152,13 +129,7 @@ public function offsetSet($key, $value) $this->elements[$key] = $value; } - /** - * {@inheritdoc} - * - * @return void - */ - #[\ReturnTypeWillChange] - public function offsetUnset($key) + public function offsetUnset(mixed $key): void { if (false !== ($position = array_search((string) $key, $this->orderedKeys))) { array_splice($this->orderedKeys, $position, 1); @@ -172,20 +143,12 @@ public function offsetUnset($key) } } - /** - * @return \Traversable - */ - #[\ReturnTypeWillChange] - public function getIterator() + public function getIterator(): \Traversable { return new OrderedHashMapIterator($this->elements, $this->orderedKeys, $this->managedCursors); } - /** - * @return int - */ - #[\ReturnTypeWillChange] - public function count() + public function count(): int { return \count($this->elements); } diff --git a/Util/OrderedHashMapIterator.php b/Util/OrderedHashMapIterator.php index e91e33301b..927a28c04a 100644 --- a/Util/OrderedHashMapIterator.php +++ b/Util/OrderedHashMapIterator.php @@ -18,64 +18,34 @@ * * @internal * - * @template-covariant TKey of array-key * @template-covariant TValue * - * @implements \Iterator + * @implements \Iterator */ class OrderedHashMapIterator implements \Iterator { - /** - * @var array - */ - private $elements; - - /** - * @var list - */ - private $orderedKeys; - - /** - * @var int - */ - private $cursor = 0; - - /** - * @var int - */ - private $cursorId; - - /** - * @var array - */ - private $managedCursors; - - /** - * @var TKey|null - */ - private $key; + private int $cursor = 0; + private int $cursorId; + private ?string $key = null; + /** @var TValue|null */ + private mixed $current = null; /** - * @var TValue|null + * @param TValue[] $elements The elements of the map, indexed by their + * keys + * @param list $orderedKeys The keys of the map in the order in which + * they should be iterated + * @param array $managedCursors An array from which to reference the + * iterator's cursor as long as it is alive. + * This array is managed by the corresponding + * {@link OrderedHashMap} instance to support + * recognizing the deletion of elements. */ - private $current; - - /** - * @param array $elements The elements of the map, indexed by their - * keys - * @param list $orderedKeys The keys of the map in the order in which - * they should be iterated - * @param array $managedCursors An array from which to reference the - * iterator's cursor as long as it is alive. - * This array is managed by the corresponding - * {@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; @@ -86,7 +56,7 @@ public function __sleep(): array throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } - public function __wakeup() + public function __wakeup(): void { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } @@ -102,20 +72,11 @@ public function __destruct() array_splice($this->managedCursors, $this->cursorId, 1); } - /** - * {@inheritdoc} - * - * @return mixed - */ - #[\ReturnTypeWillChange] - public function current() + public function current(): mixed { return $this->current; } - /** - * {@inheritdoc} - */ public function next(): void { ++$this->cursor; @@ -129,34 +90,16 @@ public function next(): void } } - /** - * {@inheritdoc} - * - * @return mixed - */ - #[\ReturnTypeWillChange] - public function key() + public function key(): mixed { - if (null === $this->key) { - return null; - } - - $array = [$this->key => null]; - - return key($array); + return $this->key; } - /** - * {@inheritdoc} - */ public function valid(): bool { return null !== $this->key; } - /** - * {@inheritdoc} - */ public function rewind(): void { $this->cursor = 0; diff --git a/Util/ServerParams.php b/Util/ServerParams.php index 168471a252..2c23efcc88 100644 --- a/Util/ServerParams.php +++ b/Util/ServerParams.php @@ -18,19 +18,15 @@ */ class ServerParams { - private $requestStack; - - public function __construct(?RequestStack $requestStack = null) - { - $this->requestStack = $requestStack; + public function __construct( + private ?RequestStack $requestStack = null, + ) { } /** * Returns true if the POST max size has been exceeded in the request. - * - * @return bool */ - public function hasPostMaxSizeBeenExceeded() + public function hasPostMaxSizeBeenExceeded(): bool { $contentLength = $this->getContentLength(); $maxContentLength = $this->getPostMaxSize(); @@ -40,10 +36,8 @@ public function hasPostMaxSizeBeenExceeded() /** * Returns maximum post size in bytes. - * - * @return int|float|null */ - public function getPostMaxSize() + public function getPostMaxSize(): int|float|null { $iniMax = strtolower($this->getNormalizedIniPostMaxSize()); @@ -75,20 +69,16 @@ public function getPostMaxSize() /** * Returns the normalized "post_max_size" ini setting. - * - * @return string */ - public function getNormalizedIniPostMaxSize() + public function getNormalizedIniPostMaxSize(): string { return strtoupper(trim(\ini_get('post_max_size'))); } /** * Returns the content length of the request. - * - * @return mixed */ - public function getContentLength() + public function getContentLength(): mixed { if (null !== $this->requestStack && null !== $request = $this->requestStack->getCurrentRequest()) { return $request->server->get('CONTENT_LENGTH'); diff --git a/Util/StringUtil.php b/Util/StringUtil.php index db60f95d19..45a50c1adc 100644 --- a/Util/StringUtil.php +++ b/Util/StringUtil.php @@ -26,10 +26,8 @@ private function __construct() /** * Returns the trimmed data. - * - * @return string */ - public static function trim(string $string) + public static function trim(string $string): string { if (null !== $result = @preg_replace('/^[\pZ\p{Cc}\p{Cf}]+|[\pZ\p{Cc}\p{Cf}]+$/u', '', $string)) { return $result; @@ -42,10 +40,8 @@ public static function trim(string $string) * Converts a fully-qualified class name to a block prefix. * * @param string $fqcn The fully-qualified class name - * - * @return string|null */ - public static function fqcnToBlockPrefix(string $fqcn) + public static function fqcnToBlockPrefix(string $fqcn): ?string { // Non-greedy ("+?") to match "type" suffix, if present if (preg_match('~([^\\\\]+?)(type)?$~i', $fqcn, $matches)) { diff --git a/composer.json b/composer.json index 0a97b6e62f..40c021d915 100644 --- a/composer.json +++ b/composer.json @@ -16,48 +16,43 @@ } ], "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/event-dispatcher": "^4.4|^5.0|^6.0", - "symfony/options-resolver": "^5.1|^6.0", + "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", "symfony/polyfill-intl-icu": "^1.21", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "^1.16", - "symfony/polyfill-php81": "^1.23", - "symfony/property-access": "^5.0.8|^6.0", - "symfony/service-contracts": "^1.1|^2|^3" + "symfony/property-access": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3" }, "require-dev": { "doctrine/collections": "^1.0|^2.0", - "symfony/validator": "^4.4.17|^5.1.9|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/console": "^5.4|^6.0", - "symfony/http-foundation": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^4.4|^5.0|^6.0", - "symfony/intl": "^4.4|^5.0|^6.0", - "symfony/security-csrf": "^4.4|^5.0|^6.0", - "symfony/translation": "^5.4.35|~6.3.12|^6.4.3", - "symfony/var-dumper": "^4.4|^5.0|^6.0", - "symfony/uid": "^5.1|^6.0" + "symfony/validator": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/html-sanitizer": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/security-csrf": "^6.4|^7.0", + "symfony/translation": "^6.4.3|^7.0.3", + "symfony/var-dumper": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0" }, "conflict": { - "symfony/console": "<4.4", - "symfony/dependency-injection": "<4.4", - "symfony/doctrine-bridge": "<5.4.21|>=6,<6.2.7", - "symfony/error-handler": "<4.4.5", - "symfony/framework-bundle": "<4.4", - "symfony/http-kernel": "<4.4", - "symfony/translation": "<5.4.35|>=6.0,<6.3.12|>=6.4,<6.4.3", - "symfony/translation-contracts": "<1.1.7", - "symfony/twig-bridge": "<5.4.21|>=6,<6.2.7" - }, - "suggest": { - "symfony/validator": "For form validation.", - "symfony/security-csrf": "For protecting forms against CSRF attacks.", - "symfony/twig-bridge": "For templating with Twig." + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/error-handler": "<6.4", + "symfony/framework-bundle": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/translation": "<6.4.3|>=7.0,<7.0.3", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4" }, "autoload": { "psr-4": { "Symfony\\Component\\Form\\": "" }, diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ede79e207d..148f8f58dd 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,7 @@ - - + + ./ - - ./Resources - ./Tests - ./vendor - - - + + + ./Resources + ./Tests + ./vendor + +