Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add stub for form options #419

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -24,3 +24,8 @@ parameters:
message: "#^Accessing PHPStan\\\\Rules\\\\Comparison\\\\ImpossibleCheckTypeMethodCallRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#"
count: 1
path: tests/Type/Symfony/ImpossibleCheckTypeMethodCallRuleTest.php

-
message: "#^Accessing PHPStan\\\\Rules\\\\Methods\\\\CallMethodsRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#"
count: 1
path: tests/Type/Symfony/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
@@ -9,12 +9,13 @@ use Symfony\Contracts\Service\ServiceSubscriberInterface;
abstract class AbstractController implements ServiceSubscriberInterface
{
/**
* @template TFormType of FormTypeInterface<TData>
* @template TFormType of FormTypeInterface<TData, TOptions>
* @template TData
* @template TOptions of array<string, mixed>
*
* @param class-string<TFormType> $type
* @param TData $data
* @param array<string, mixed> $options
* @param TOptions $options
*
* @phpstan-return ($data is null ? FormInterface<null|TData> : FormInterface<TData>)
*/
9 changes: 5 additions & 4 deletions stubs/Symfony/Component/Form/AbstractType.stub
Original file line number Diff line number Diff line change
@@ -4,27 +4,28 @@ namespace Symfony\Component\Form;

/**
* @template TData
* @template TOptions of array<string, mixed>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has to be optional, otherwise it's a BC break. See phpstan/phpstan-src#3457

*
* @implements FormTypeInterface<TData>
* @implements FormTypeInterface<TData, TOptions>
*/
abstract class AbstractType implements FormTypeInterface
{

/**
* @param FormBuilderInterface<TData|null> $builder
* @param array<string, mixed> $options
* @param TOptions $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void;

/**
* @param FormInterface<TData> $form
* @param array<string, mixed> $options
* @param TOptions $options
*/
public function buildView(FormView $view, FormInterface $form, array $options): void;

/**
* @param FormInterface<TData> $form
* @param array<string, mixed> $options
* @param TOptions $options
*/
public function finishView(FormView $view, FormInterface $form, array $options): void;

12 changes: 7 additions & 5 deletions stubs/Symfony/Component/Form/FormFactoryInterface.stub
Original file line number Diff line number Diff line change
@@ -7,12 +7,13 @@ use Symfony\Component\Form\Extension\Core\Type\FormType;
interface FormFactoryInterface
{
/**
* @template TFormType of FormTypeInterface<TData>
* @template TFormType of FormTypeInterface<TData, TOptions>
* @template TData
* @template TOptions of array<string, mixed>
*
* @param class-string<TFormType> $type
* @param TData $data
* @param array<string, mixed> $options
* @param TOptions $options
*
* @phpstan-return ($data is null ? FormInterface<null|TData> : FormInterface<TData>)
*
@@ -21,12 +22,13 @@ interface FormFactoryInterface
public function create(string $type = FormType::class, $data = null, array $options = []): FormInterface;

/**
* @template TFormType of FormTypeInterface<TData>
* @template TFormType of FormTypeInterface<TData, TOptions>
* @template TData
* @template TOptions of array<string, mixed>
*
* @param class-string<TFormType> $type
* @param class-string<TFormType> $type
* @param TData $data
* @param array<string, mixed> $options
* @param TOptions $options
*
* @phpstan-return ($data is null ? FormInterface<null|TData> : FormInterface<TData>)
*
11 changes: 6 additions & 5 deletions stubs/Symfony/Component/Form/FormTypeExtensionInterface.stub
Original file line number Diff line number Diff line change
@@ -4,24 +4,25 @@ namespace Symfony\Component\Form;

/**
* @template TData
* @template TOptions of array<string, mixed>
*/
interface FormTypeExtensionInterface
{
/**
* @param FormBuilderInterface<TData|null> $builder
* @param array<string, mixed> $options
* @param TOptions $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void;

/**
* @phpstan-param FormInterface<TData> $form
* @param array<string, mixed> $options
* @param FormInterface<TData> $form
* @param TOptions $options
*/
public function buildView(FormView $view, FormInterface $form, array $options): void;

/**
* @phpstan-param FormInterface<TData> $form
* @param array<string, mixed> $options
* @param FormInterface<TData> $form
* @param TOptions $options
*/
public function finishView(FormView $view, FormInterface $form, array $options): void;
}
7 changes: 4 additions & 3 deletions stubs/Symfony/Component/Form/FormTypeInterface.stub
Original file line number Diff line number Diff line change
@@ -4,24 +4,25 @@ namespace Symfony\Component\Form;

/**
* @template TData
* @template TOptions of array<string, mixed>
*/
interface FormTypeInterface
{
/**
* @param FormBuilderInterface<TData|null> $builder
* @param array<string, mixed> $options
* @param TOptions $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void;

/**
* @param FormInterface<TData> $form
* @param array<string, mixed> $options
* @param TOptions $options
*/
public function buildView(FormView $view, FormInterface $form, array $options): void;

/**
* @param FormInterface<TData> $form
* @param array<string, mixed> $options
* @param TOptions $options
*/
public function finishView(FormView $view, FormInterface $form, array $options): void;
}
33 changes: 33 additions & 0 deletions tests/Type/Symfony/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Symfony;

use PHPStan\Rules\Methods\CallMethodsRule;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<CallMethodsRule>
*/
class CallMethodsRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return self::getContainer()->getByType(CallMethodsRule::class);
}

public function testExtension(): void
{
$this->analyse([__DIR__ . '/data/form_options.php'], []);
}

public static function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/../../../extension.neon',
__DIR__ . '/../../../vendor/phpstan/phpstan-strict-rules/rules.neon',
];
}

}
1 change: 1 addition & 0 deletions tests/Type/Symfony/ExtensionTest.php
Original file line number Diff line number Diff line change
@@ -58,6 +58,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');
yield from $this->gatherAssertTypes(__DIR__ . '/data/form_options.php');

yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/with-configuration/WithConfigurationExtension.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/without-configuration/WithoutConfigurationExtension.php');
102 changes: 102 additions & 0 deletions tests/Type/Symfony/data/form_options.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php declare(strict_types = 1);

namespace GenericFormOptionsType;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use function PHPStan\Testing\assertType;

class DataClass
{
}

/**
* @extends AbstractType<DataClass, array{required: string, optional: int}>
*/
class DataClassType extends AbstractType
{

public function buildForm(FormBuilderInterface $builder, array $options): void
{
assertType('string', $options['required']);
assertType('int', $options['optional']);

$builder
->add('foo', NumberType::class)
->add('bar', TextType::class)
;
}

public function configureOptions(OptionsResolver $resolver): void
{
$resolver
->setDefaults([
'data_class' => DataClass::class,
'optional' => 0,
])
->setRequired('required')
->setAllowedTypes('required', 'string')
->setAllowedTypes('optional', 'int')
;
}

}

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('Symfony\Component\Form\FormInterface<GenericFormOptionsType\DataClass>', $form);
}

public function doSomethingWithOption(): void
{
$form = $this->formFactory->create(DataClassType::class, new DataClass(), ['required' => 'foo']);
assertType('Symfony\Component\Form\FormInterface<GenericFormOptionsType\DataClass>', $form);
}

public function doSomethingWithInvalidOption(): void
{
$form = $this->formFactory->create(DataClassType::class, new DataClass(), ['required' => 42]);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to get an error here saying I pass an integer when a string is expected because DataClassType is

AbstractType<DataClass, array{required: string, optional: int}>

I thought it would have been reported by the CallMethodsRuleTest.

What do I miss @ondrejmirtes ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please recreate the scenario in a minimal way on phpstan.org/try so I don't have to hold all the interfaces and Symfony classes in my head.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure.
You already helped me with phpstan/phpstan#12372 (comment).

But I discovered that in Symfony, that the required options in the Factory::create methods and the certains options in the buildForm $options are not exactly the same.

So this would give something like
https://phpstan.org/r/2fc552e7-506e-46a5-aba0-69287a5b8d3e

I dunno if it's possible to have something better than

@implements FormTypeInterface<
 *   object,
 *   array{required: string, optional?: int, defined?: bool},
 *   array{required: string, optional: int, defined?: bool}
 * >

assertType('Symfony\Component\Form\FormInterface<GenericFormOptionsType\DataClass>', $form);
}

}

class FormController extends AbstractController
{

public function doSomething(): void
{
$form = $this->createForm(DataClassType::class, new DataClass());
assertType('Symfony\Component\Form\FormInterface<GenericFormOptionsType\DataClass>', $form);
}

public function doSomethingWithOption(): void
{
$form = $this->createForm(DataClassType::class, new DataClass(), ['required' => 'foo']);
assertType('Symfony\Component\Form\FormInterface<GenericFormOptionsType\DataClass>', $form);
}

public function doSomethingWithInvalidOption(): void
{
$form = $this->createForm(DataClassType::class, new DataClass(), ['required' => 42]);
assertType('Symfony\Component\Form\FormInterface<GenericFormOptionsType\DataClass>', $form);
}

}