phpstan / phpstan-symfony

Symfony extension for PHPStan
MIT License
698 stars 89 forks source link

Type refinement for options in form types #400

Open stof opened 2 months ago

stof commented 2 months ago

the $options argument of the various methods in FormTypeInterface and FormTypeExtensionInterface are typed as array<string, mixed> for now, because there is no other way to type them in phpdoc. However, the actual type is a configurable array shape, depending on the configuration of the OptionsResolver. Having a more precise type would make it easier to write form types when using the level 9 of phpstan (where mixed requires explicit type checking to use them) and would allow more precise analysis at lower levels (as phpstan would report invalid usages of values taken from options instead of allowing anything because they are mixed)

Calls done on the OptionsResolver in the configureOptions method should be analyzed to build the array shape:

For options that are defined without defining allowed types or allowed values (or defining callback-based validation of allowed values that cannot be analyzed), the array shape should used mixed as the type for them.

A basic version of the analysis could report only options defined by a form type for itself. However, this might return false positive errors for some valid code relying on parent options. A full analysis would have to consider the type hierarchy:

Note: the array shape should be an unsealed one (if PHPStan ever implements the distinction between sealed and unsealed shapes that Psalm implements) because all form types of the type hierarchy receive the full array of options (so when calling methods on a parent type, it is likely to contain some extra options known only by the child type or a type extension).

kevinpapst commented 1 month ago

As I am currently struggling with that exact topic, here is a short & simple reproducer:

final class SomeDateHandlerForm extends AbstractType {
  public function buildForm(FormBuilderInterface $builder, array $options): void {
        $date = $options['year'] . '-01-01';
        // ... 
    }

    public function configureOptions(OptionsResolver $resolver): void {
        $resolver->setDefaults(['year' => date('Y')]);
        $resolver->setAllowedTypes('year', ['string']);
    }
}

This will report Binary operation "." between mixed and '-01-01' results in an error. even though the OptionsResolver makes sure that only a string is given in $options['year']. Currently I have to either silence the line or write type checking logic, which is not necessary due to the validation inside OptionsResolver.