From 2f97c6ac6caa4e2c5ee5c73c1974bbe0b9134dc6 Mon Sep 17 00:00:00 2001 From: Naoki Ikeguchi Date: Mon, 20 Feb 2023 12:56:42 +0900 Subject: [PATCH 01/10] feat: Support specifying type of data_class in forms --- extension.neon | 3 ++ .../Symfony/Component/Form/AbstractType.stub | 12 +++++++ .../Component/Form/FormFactoryInterface.stub | 36 +++++++++++++++++++ .../Symfony/Component/Form/FormInterface.stub | 11 ++++-- .../Form/FormTypeExtensionInterface.stub | 5 +++ .../Component/Form/FormTypeInterface.stub | 5 +++ .../Exception/InvalidOptionsException.stub | 7 ++++ .../Symfony/Component/Form/DataClass.php | 18 ++++++++++ .../Symfony/Component/Form/DataClassType.php | 32 +++++++++++++++++ .../Component/Form/FormFactoryAwareClass.php | 30 ++++++++++++++++ 10 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 stubs/Symfony/Component/Form/AbstractType.stub create mode 100644 stubs/Symfony/Component/Form/FormFactoryInterface.stub create mode 100644 stubs/Symfony/Component/OptionsResolver/Exception/InvalidOptionsException.stub create mode 100644 tests/Stubs/Symfony/Component/Form/DataClass.php create mode 100644 tests/Stubs/Symfony/Component/Form/DataClassType.php create mode 100644 tests/Stubs/Symfony/Component/Form/FormFactoryAwareClass.php diff --git a/extension.neon b/extension.neon index e4880077..e2c3afa1 100644 --- a/extension.neon +++ b/extension.neon @@ -34,6 +34,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 +42,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 +52,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..d7b79c37 --- /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 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 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..a9a0f88a 100644 --- a/stubs/Symfony/Component/Form/FormInterface.stub +++ b/stubs/Symfony/Component/Form/FormInterface.stub @@ -3,10 +3,15 @@ namespace Symfony\Component\Form; /** - * @extends \ArrayAccess - * @extends \Traversable + * @template TData + * + * @extends \ArrayAccess> + * @extends \Traversable> */ interface FormInterface extends \ArrayAccess, \Traversable, \Countable { - + /** + * @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 @@ + + */ +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, + ]) + ; + } +} diff --git a/tests/Stubs/Symfony/Component/Form/FormFactoryAwareClass.php b/tests/Stubs/Symfony/Component/Form/FormFactoryAwareClass.php new file mode 100644 index 00000000..b601b48a --- /dev/null +++ b/tests/Stubs/Symfony/Component/Form/FormFactoryAwareClass.php @@ -0,0 +1,30 @@ +formFactory = $formFactory; + } + + public function doSomething(): void + { + $form = $this->formFactory->create(DataClassType::class, new DataClass()); + $data = $form->getData(); + $this->thisOnlyAcceptsDataClass($data); + } + + private function thisOnlyAcceptsDataClass(DataClass $data): void + { + } +} From 7ac48f58ea78e78611c5aa1c418dd1b9b9bedbb5 Mon Sep 17 00:00:00 2001 From: Naoki Ikeguchi Date: Mon, 20 Feb 2023 13:00:47 +0900 Subject: [PATCH 02/10] feat: Use null|TData when the initial data is null --- .../Component/Form/FormFactoryInterface.stub | 6 +++--- .../Component/Form/FormFactoryAwareClass.php | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/stubs/Symfony/Component/Form/FormFactoryInterface.stub b/stubs/Symfony/Component/Form/FormFactoryInterface.stub index d7b79c37..99f704cd 100644 --- a/stubs/Symfony/Component/Form/FormFactoryInterface.stub +++ b/stubs/Symfony/Component/Form/FormFactoryInterface.stub @@ -14,7 +14,7 @@ interface FormFactoryInterface * @param TData $data * @param array $options * - * @phpstan-return FormInterface + * @phpstan-return ($data is null ? FormInterface : FormInterface) * * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException */ @@ -27,8 +27,8 @@ interface FormFactoryInterface * @param class-string $type * @param TData $data * @param array $options - * - * @phpstan-return FormInterface + * + * @phpstan-return ($data is null ? FormInterface : FormInterface) * * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException */ diff --git a/tests/Stubs/Symfony/Component/Form/FormFactoryAwareClass.php b/tests/Stubs/Symfony/Component/Form/FormFactoryAwareClass.php index b601b48a..e490bb20 100644 --- a/tests/Stubs/Symfony/Component/Form/FormFactoryAwareClass.php +++ b/tests/Stubs/Symfony/Component/Form/FormFactoryAwareClass.php @@ -22,9 +22,22 @@ public function doSomething(): void $form = $this->formFactory->create(DataClassType::class, new DataClass()); $data = $form->getData(); $this->thisOnlyAcceptsDataClass($data); + $this->thisOnlyAcceptsDataClassOrNull($data); + } + + public function doSomethingNullable(): void + { + $form = $this->formFactory->create(DataClassType::class); + $data = $form->getData(); + // $this->thisOnlyAcceptsDataClass($data); // ERROR! + $this->thisOnlyAcceptsDataClassOrNull($data); } private function thisOnlyAcceptsDataClass(DataClass $data): void { } + + private function thisOnlyAcceptsDataClassOrNull(?DataClass $data): void + { + } } From a18b756621163b0040a3c3f5a58a2f24b3652f48 Mon Sep 17 00:00:00 2001 From: Naoki Ikeguchi Date: Mon, 20 Feb 2023 13:08:43 +0900 Subject: [PATCH 03/10] style: Fix CS --- tests/Stubs/Symfony/Component/Form/DataClass.php | 16 ++++++---------- .../Symfony/Component/Form/DataClassType.php | 6 ++++-- .../Component/Form/FormFactoryAwareClass.php | 15 +++++++-------- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/tests/Stubs/Symfony/Component/Form/DataClass.php b/tests/Stubs/Symfony/Component/Form/DataClass.php index d31e96e8..d22e3b3e 100644 --- a/tests/Stubs/Symfony/Component/Form/DataClass.php +++ b/tests/Stubs/Symfony/Component/Form/DataClass.php @@ -1,18 +1,14 @@ -formFactory = $formFactory; } @@ -40,4 +38,5 @@ private function thisOnlyAcceptsDataClass(DataClass $data): void private function thisOnlyAcceptsDataClassOrNull(?DataClass $data): void { } + } From 88da905c8a29f791fa9ee78ec5f83f2980f8e809 Mon Sep 17 00:00:00 2001 From: Natsuki Ikeguchi Date: Fri, 14 Apr 2023 20:35:33 +0900 Subject: [PATCH 04/10] style: Fix wrong indentation --- .../Component/Form/FormFactoryInterface.stub | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/stubs/Symfony/Component/Form/FormFactoryInterface.stub b/stubs/Symfony/Component/Form/FormFactoryInterface.stub index 99f704cd..8760c918 100644 --- a/stubs/Symfony/Component/Form/FormFactoryInterface.stub +++ b/stubs/Symfony/Component/Form/FormFactoryInterface.stub @@ -10,10 +10,10 @@ interface FormFactoryInterface * @template TFormType of FormTypeInterface * @template TData * - * @param class-string $type - * @param TData $data - * @param array $options - * + * @param class-string $type + * @param TData $data + * @param array $options + * * @phpstan-return ($data is null ? FormInterface : FormInterface) * * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException @@ -24,9 +24,9 @@ interface FormFactoryInterface * @template TFormType of FormTypeInterface * @template TData * - * @param class-string $type - * @param TData $data - * @param array $options + * @param class-string $type + * @param TData $data + * @param array $options * * @phpstan-return ($data is null ? FormInterface : FormInterface) * From da2cb981eb40944ba69a93595b7f4e3dedd2b40b Mon Sep 17 00:00:00 2001 From: Natsuki Ikeguchi Date: Fri, 14 Apr 2023 20:37:41 +0900 Subject: [PATCH 05/10] fix: Add to skipCheckGenericClasses --- extension.neon | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/extension.neon b/extension.neon index e2c3afa1..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 From dcf9b4ff13233b72e7c6c4f8eb86abfe87601dc5 Mon Sep 17 00:00:00 2001 From: Natsuki Ikeguchi Date: Fri, 14 Apr 2023 20:50:42 +0900 Subject: [PATCH 06/10] test: Migrate to type inference test --- .../Symfony/Component/Form/DataClass.php | 14 ---- .../Symfony/Component/Form/DataClassType.php | 34 --------- .../Component/Form/FormFactoryAwareClass.php | 42 ----------- tests/Type/Symfony/ExtensionTest.php | 1 + tests/Type/Symfony/data/form_data_type.php | 72 +++++++++++++++++++ 5 files changed, 73 insertions(+), 90 deletions(-) delete mode 100644 tests/Stubs/Symfony/Component/Form/DataClass.php delete mode 100644 tests/Stubs/Symfony/Component/Form/DataClassType.php delete mode 100644 tests/Stubs/Symfony/Component/Form/FormFactoryAwareClass.php create mode 100644 tests/Type/Symfony/data/form_data_type.php diff --git a/tests/Stubs/Symfony/Component/Form/DataClass.php b/tests/Stubs/Symfony/Component/Form/DataClass.php deleted file mode 100644 index d22e3b3e..00000000 --- a/tests/Stubs/Symfony/Component/Form/DataClass.php +++ /dev/null @@ -1,14 +0,0 @@ - - */ -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, - ]) - ; - } - -} diff --git a/tests/Stubs/Symfony/Component/Form/FormFactoryAwareClass.php b/tests/Stubs/Symfony/Component/Form/FormFactoryAwareClass.php deleted file mode 100644 index 42642d34..00000000 --- a/tests/Stubs/Symfony/Component/Form/FormFactoryAwareClass.php +++ /dev/null @@ -1,42 +0,0 @@ -formFactory = $formFactory; - } - - public function doSomething(): void - { - $form = $this->formFactory->create(DataClassType::class, new DataClass()); - $data = $form->getData(); - $this->thisOnlyAcceptsDataClass($data); - $this->thisOnlyAcceptsDataClassOrNull($data); - } - - public function doSomethingNullable(): void - { - $form = $this->formFactory->create(DataClassType::class); - $data = $form->getData(); - // $this->thisOnlyAcceptsDataClass($data); // ERROR! - $this->thisOnlyAcceptsDataClassOrNull($data); - } - - private function thisOnlyAcceptsDataClass(DataClass $data): void - { - } - - private function thisOnlyAcceptsDataClassOrNull(?DataClass $data): void - { - } - -} diff --git a/tests/Type/Symfony/ExtensionTest.php b/tests/Type/Symfony/ExtensionTest.php index 8dff057e..879abb5d 100644 --- a/tests/Type/Symfony/ExtensionTest.php +++ b/tests/Type/Symfony/ExtensionTest.php @@ -56,6 +56,7 @@ public function dataFileAsserts(): iterable yield from $this->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..0b66c197 --- /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('PHPStan\Type\Symfony\DataClass', $form->getData()); + } + + public function doSomethingNullable(): void + { + $form = $this->formFactory->create(DataClassType::class); + assertType('PHPStan\Type\Symfony\DataClass|null', $form->getData()); + } + +} From b66084e0f28a725ec3cc9192e60d43c7c7e41a4b Mon Sep 17 00:00:00 2001 From: Natsuki Ikeguchi Date: Fri, 14 Apr 2023 20:58:41 +0900 Subject: [PATCH 07/10] feat: Add stub of setData --- stubs/Symfony/Component/Form/FormInterface.stub | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/stubs/Symfony/Component/Form/FormInterface.stub b/stubs/Symfony/Component/Form/FormInterface.stub index a9a0f88a..4bd21229 100644 --- a/stubs/Symfony/Component/Form/FormInterface.stub +++ b/stubs/Symfony/Component/Form/FormInterface.stub @@ -10,6 +10,13 @@ namespace Symfony\Component\Form; */ interface FormInterface extends \ArrayAccess, \Traversable, \Countable { + /** + * @param TData $modelData + * + * @return $this + */ + public function setData($modelData): FormInterface; + /** * @return TData */ From 1a263bc0c52d8003527879ec879e75b53f80ce15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Fri, 14 Apr 2023 14:15:12 +0200 Subject: [PATCH 08/10] Update form_data_type.php --- tests/Type/Symfony/data/form_data_type.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Type/Symfony/data/form_data_type.php b/tests/Type/Symfony/data/form_data_type.php index 0b66c197..e1a8f814 100644 --- a/tests/Type/Symfony/data/form_data_type.php +++ b/tests/Type/Symfony/data/form_data_type.php @@ -1,6 +1,6 @@ Date: Fri, 14 Apr 2023 14:15:45 +0200 Subject: [PATCH 09/10] Update form_data_type.php --- tests/Type/Symfony/data/form_data_type.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Type/Symfony/data/form_data_type.php b/tests/Type/Symfony/data/form_data_type.php index e1a8f814..524a5b7c 100644 --- a/tests/Type/Symfony/data/form_data_type.php +++ b/tests/Type/Symfony/data/form_data_type.php @@ -60,13 +60,13 @@ public function __construct(FormFactoryInterface $formFactory) public function doSomething(): void { $form = $this->formFactory->create(DataClassType::class, new DataClass()); - assertType('PHPStan\Type\Symfony\DataClass', $form->getData()); + assertType('GenericFormDataType\DataClass', $form->getData()); } public function doSomethingNullable(): void { $form = $this->formFactory->create(DataClassType::class); - assertType('PHPStan\Type\Symfony\DataClass|null', $form->getData()); + assertType('GenericFormDataType\DataClass|null', $form->getData()); } } From 24e94dd751231ca16f342c7a1763d3f474f93653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Fri, 14 Apr 2023 14:16:31 +0200 Subject: [PATCH 10/10] Update FormFactoryInterface.stub --- .../Component/Form/FormFactoryInterface.stub | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/stubs/Symfony/Component/Form/FormFactoryInterface.stub b/stubs/Symfony/Component/Form/FormFactoryInterface.stub index 8760c918..a2221ec0 100644 --- a/stubs/Symfony/Component/Form/FormFactoryInterface.stub +++ b/stubs/Symfony/Component/Form/FormFactoryInterface.stub @@ -10,10 +10,10 @@ interface FormFactoryInterface * @template TFormType of FormTypeInterface * @template TData * - * @param class-string $type - * @param TData $data - * @param array $options - * + * @param class-string $type + * @param TData $data + * @param array $options + * * @phpstan-return ($data is null ? FormInterface : FormInterface) * * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException @@ -24,11 +24,11 @@ interface FormFactoryInterface * @template TFormType of FormTypeInterface * @template TData * - * @param class-string $type - * @param TData $data - * @param array $options - * - * @phpstan-return ($data is null ? FormInterface : FormInterface) + * @param class-string $type + * @param TData $data + * @param array $options + * + * @phpstan-return ($data is null ? FormInterface : FormInterface) * * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException */