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

Draft
wants to merge 1 commit into
base: 2.0.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
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
Expand Up @@ -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
Expand Up @@ -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>)
*/
Expand Down
9 changes: 5 additions & 4 deletions stubs/Symfony/Component/Form/AbstractType.stub
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,28 @@ namespace Symfony\Component\Form;

/**
* @template TData
* @template TOptions of array<string, mixed>
*
* @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;

Expand Down
12 changes: 7 additions & 5 deletions stubs/Symfony/Component/Form/FormFactoryInterface.stub
Original file line number Diff line number Diff line change
Expand Up @@ -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>)
*
Expand All @@ -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>)
*
Expand Down
11 changes: 6 additions & 5 deletions stubs/Symfony/Component/Form/FormTypeExtensionInterface.stub
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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
Expand Up @@ -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');
Expand Down
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 ?

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);
}

}
Loading