diff --git a/extension.neon b/extension.neon index e4880077..1c54d774 100644 --- a/extension.neon +++ b/extension.neon @@ -13,6 +13,10 @@ parameters: consoleApplicationLoader: null featureToggles: skipCheckGenericClasses: + - Symfony\Component\Form\AbstractType + - Symfony\Component\Form\FormInterface + - Symfony\Component\Form\FormTypeExtensionInterface + - Symfony\Component\Form\FormTypeInterface - Symfony\Component\OptionsResolver\Options - Symfony\Component\Security\Core\Authorization\Voter\Voter - Symfony\Component\Security\Core\User\PasswordUpgraderInterface @@ -34,6 +38,7 @@ parameters: - stubs/Symfony/Component/EventDispatcher/EventDispatcherInterface.stub - stubs/Symfony/Component/EventDispatcher/EventSubscriberInterface.stub - stubs/Symfony/Component/EventDispatcher/GenericEvent.stub + - stubs/Symfony/Component/Form/AbstractType.stub - stubs/Symfony/Component/Form/ChoiceList/Loader/ChoiceLoaderInterface.stub - stubs/Symfony/Component/Form/Exception/ExceptionInterface.stub - stubs/Symfony/Component/Form/Exception/RuntimeException.stub @@ -41,6 +46,7 @@ parameters: - stubs/Symfony/Component/Form/DataTransformerInterface.stub - stubs/Symfony/Component/Form/FormBuilderInterface.stub - stubs/Symfony/Component/Form/FormInterface.stub + - stubs/Symfony/Component/Form/FormFactoryInterface.stub - stubs/Symfony/Component/Form/FormTypeExtensionInterface.stub - stubs/Symfony/Component/Form/FormTypeInterface.stub - stubs/Symfony/Component/Form/FormView.stub @@ -50,6 +56,7 @@ parameters: - stubs/Symfony/Component/HttpFoundation/Session.stub - stubs/Symfony/Component/Messenger/StampInterface.stub - stubs/Symfony/Component/Messenger/Envelope.stub + - stubs/Symfony/Component/OptionsResolver/Exception/InvalidOptionsException.stub - stubs/Symfony/Component/OptionsResolver/Options.stub - stubs/Symfony/Component/Process/Process.stub - stubs/Symfony/Component/PropertyAccess/PropertyPathInterface.stub diff --git a/stubs/Symfony/Component/Form/AbstractType.stub b/stubs/Symfony/Component/Form/AbstractType.stub new file mode 100644 index 00000000..76170a02 --- /dev/null +++ b/stubs/Symfony/Component/Form/AbstractType.stub @@ -0,0 +1,12 @@ + + */ +abstract class AbstractType implements FormTypeInterface +{ +} diff --git a/stubs/Symfony/Component/Form/FormFactoryInterface.stub b/stubs/Symfony/Component/Form/FormFactoryInterface.stub new file mode 100644 index 00000000..a2221ec0 --- /dev/null +++ b/stubs/Symfony/Component/Form/FormFactoryInterface.stub @@ -0,0 +1,36 @@ + + * @template TData + * + * @param class-string $type + * @param TData $data + * @param array $options + * + * @phpstan-return ($data is null ? FormInterface : FormInterface) + * + * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function create(string $type = FormType::class, $data = null, array $options = []): FormInterface; + + /** + * @template TFormType of FormTypeInterface + * @template TData + * + * @param class-string $type + * @param TData $data + * @param array $options + * + * @phpstan-return ($data is null ? FormInterface : FormInterface) + * + * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function createNamed(string $name, string $type = FormType::class, $data = null, array $options = []): FormInterface; +} diff --git a/stubs/Symfony/Component/Form/FormInterface.stub b/stubs/Symfony/Component/Form/FormInterface.stub index 6960cce5..4bd21229 100644 --- a/stubs/Symfony/Component/Form/FormInterface.stub +++ b/stubs/Symfony/Component/Form/FormInterface.stub @@ -3,10 +3,22 @@ namespace Symfony\Component\Form; /** - * @extends \ArrayAccess - * @extends \Traversable + * @template TData + * + * @extends \ArrayAccess> + * @extends \Traversable> */ interface FormInterface extends \ArrayAccess, \Traversable, \Countable { + /** + * @param TData $modelData + * + * @return $this + */ + public function setData($modelData): FormInterface; + /** + * @return TData + */ + public function getData(); } diff --git a/stubs/Symfony/Component/Form/FormTypeExtensionInterface.stub b/stubs/Symfony/Component/Form/FormTypeExtensionInterface.stub index 25b6f25d..234d1b2e 100644 --- a/stubs/Symfony/Component/Form/FormTypeExtensionInterface.stub +++ b/stubs/Symfony/Component/Form/FormTypeExtensionInterface.stub @@ -2,6 +2,9 @@ namespace Symfony\Component\Form; +/** + * @template TData + */ interface FormTypeExtensionInterface { /** @@ -10,11 +13,13 @@ interface FormTypeExtensionInterface public function buildForm(FormBuilderInterface $builder, array $options): void; /** + * @phpstan-param FormInterface $form * @param array $options */ public function buildView(FormView $view, FormInterface $form, array $options): void; /** + * @phpstan-param FormInterface $form * @param array $options */ public function finishView(FormView $view, FormInterface $form, array $options): void; diff --git a/stubs/Symfony/Component/Form/FormTypeInterface.stub b/stubs/Symfony/Component/Form/FormTypeInterface.stub index cebbc1c2..85bb539c 100644 --- a/stubs/Symfony/Component/Form/FormTypeInterface.stub +++ b/stubs/Symfony/Component/Form/FormTypeInterface.stub @@ -2,6 +2,9 @@ namespace Symfony\Component\Form; +/** + * @template TData + */ interface FormTypeInterface { /** @@ -10,11 +13,13 @@ interface FormTypeInterface public function buildForm(FormBuilderInterface $builder, array $options): void; /** + * @phpstan-param FormInterface $form * @param array $options */ public function buildView(FormView $view, FormInterface $form, array $options): void; /** + * @phpstan-param FormInterface $form * @param array $options */ public function finishView(FormView $view, FormInterface $form, array $options): void; diff --git a/stubs/Symfony/Component/OptionsResolver/Exception/InvalidOptionsException.stub b/stubs/Symfony/Component/OptionsResolver/Exception/InvalidOptionsException.stub new file mode 100644 index 00000000..2856e32b --- /dev/null +++ b/stubs/Symfony/Component/OptionsResolver/Exception/InvalidOptionsException.stub @@ -0,0 +1,7 @@ +gatherAssertTypes(__DIR__ . '/data/FormInterface_getErrors.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/cache.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/form_data_type.php'); } /** diff --git a/tests/Type/Symfony/data/form_data_type.php b/tests/Type/Symfony/data/form_data_type.php new file mode 100644 index 00000000..524a5b7c --- /dev/null +++ b/tests/Type/Symfony/data/form_data_type.php @@ -0,0 +1,72 @@ + + */ +class DataClassType extends AbstractType +{ + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('foo', NumberType::class) + ->add('bar', TextType::class) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setDefaults([ + 'data_class' => DataClass::class, + ]) + ; + } + +} + +class FormFactoryAwareClass +{ + + /** @var FormFactoryInterface */ + private $formFactory; + + public function __construct(FormFactoryInterface $formFactory) + { + $this->formFactory = $formFactory; + } + + public function doSomething(): void + { + $form = $this->formFactory->create(DataClassType::class, new DataClass()); + assertType('GenericFormDataType\DataClass', $form->getData()); + } + + public function doSomethingNullable(): void + { + $form = $this->formFactory->create(DataClassType::class); + assertType('GenericFormDataType\DataClass|null', $form->getData()); + } + +}