laminas / laminas-form

Validate and display simple and complex forms, casting forms to business objects and vice versa
https://docs.laminas.dev/laminas-form/
BSD 3-Clause "New" or "Revised" License
80 stars 52 forks source link

[RFC] Improve type inference for `getData()` #238

Closed gsteel closed 11 months ago

gsteel commented 1 year ago

Feature Request

Q A
New Feature yes
RFC yes
BC Break no

Summary

With https://github.com/laminas/laminas-inputfilter/pull/91 landing in input-filter, it'll be easier to annotate forms with something like:

/**
 * @psalm-type ValidPayload = array{
 *     field1: int<1, 365>,
 *     field2: non-empty-string,
 *     field3: list<non-empty-string>,
 * }
 * @extends Form<ValidPayload>
 */
final class MyForm extends Form
{
}

It's been a while since I used form for anything other than regular associative arrays, but given forms can be bound to objects too - it's also feasible that forms could be annotated to return models too, such as:

/** @extends Form<SomeModel> */
final class MyForm extends Form
{
}

The patch in InputFilter helps because users will be able to do something like the following:

/**
 * @psalm-import-type ValidPayload from MyInputFilter
 * @extends Form<ValidPayload>
 */
final class MyForm extends Form
{
    public function __construct(MyInputFilter $inputFilter)
    {
        parent::__construct();
        $this->setInputFilter($inputFilter);
    }
}
Ocramius commented 1 year ago

/** @extends Form<SomeModel> */

Very cool and elegant 💪

gsteel commented 11 months ago

Hopefully the psalm bot is working here. This is what I have in mind:

https://psalm.dev/r/0d6f0ee752

psalm-github-bot[bot] commented 11 months ago

I found these snippets:

https://psalm.dev/r/0d6f0ee752 ```php */ class MyFilter implements Filter { public function getValues(): array { return ['foo' => 'bar']; } } /** @template TFilterShape */ interface Form { /** @return TFilterShape */ public function getData(): array; /** @param Filter $filter **/ public function setFilter(Filter $filter): void; } /** * @template TFilterShape * @implements Form */ abstract class BaseForm implements Form { /** @var Filter */ private Filter|null $filter = null; /** @return TFilterShape */ public function getData(): array { if (! $this->filter) { throw new Exception('Bad News'); } return $this->filter->getValues(); } /** @param Filter $filter **/ public function setFilter(Filter $filter): void { $this->filter = $filter; } } /** * @psalm-import-type MyPayload from MyFilter * @extends BaseForm */ class MyForm extends BaseForm { } $form = new MyForm(); $form->setFilter(new MyFilter()); $foo = $form->getData()['foo']; /** @psalm-trace $foo */ ``` ``` Psalm output (using commit baa6d5d): INFO: Trace - 72:25 - $foo: non-empty-string INFO: UnusedVariable - 70:1 - $foo is never referenced or the value is not used ```