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

Refactor AllowList Filter, Add generic template to FilterInterface #118

Merged
merged 7 commits into from
Jun 29, 2024

Conversation

gsteel
Copy link
Member

@gsteel gsteel commented Nov 6, 2023

This patch is indicative of the plans for all the shipped filters where feasible:

  • Removes inheritance
  • Private properties instead of protected
  • Native return types
  • Native parameter types
  • Options can no longer be Traversable

The reason options can no longer be Traversable is because we can rely on static analysis by using the documented array shape. Allowing Traversable will require a lot of type juggling and coercion, and it becomes difficult to know where to stop, for example, given the strict option here, previously, at least bool|int<0,1>|'0'|'1' were acceptable, so why not also 'y'|'n'|'yes'|'no'|'true'|'false' too? We can all agree that native types are an improvement, so we might as well get rid of the idea that Options arrays|Traversables can have mixed values too.

Questions…

Option value setters and getters. Can we get rid of them?

It's unlikely that filters are mutated at runtime. Most consumers will be using them via an InputFilter as part of an array spec such as:

[
  'filters' => [
    [
      'name' => AllowList::class,
      'options' => [
        'strict' => true,
        'list' => ['some', 'list'],
      ],
    ],
  ],
];

Handling the option values in __construct and setOptions seems sufficient, and the getters just feel like a testing convenience.

We'll also still have runtime type safety via native property types.

Inheritance Removal…

… might be invalid in this case. I can think of 2 reasons why a parent abstract might be useful:

  • To provide __invoke(mixed):mixed (without useful type inference) that just proxies to filter()
  • To call setOptions during __construct.

Therefore, might it be better to strip everything else out of the AbstractFilter bar these 2 functions?

Introduce an AcceptsOptionsInterface

something along the lines of

/** @template Options of array<string, mixed> */
interface AcceptsOptionsInterface {
  
  /** @param Options $options */
  public function setOptions(array $options): self; /** preferably void */
  
}

This patch is cut from #117 so it'll be easier to review just fab100b in isolation.

@gsteel gsteel added this to the 3.0.0 milestone Nov 6, 2023
@gsteel gsteel requested a review from a team November 6, 2023 21:51
Copy link
Member

@boesing boesing left a comment

Choose a reason for hiding this comment

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

Overall, having final around works for me pretty well.

I'd remove setOptions and/or all the setter methods at all.

All these mutable classes are painful and when I have a look into our project, no1 ever used those setters.

Having immutable filters makes absolutely sense to me and thus I'd be fine with changing that. But if we want to keep setter methods, lets at least remove setOptions, no?

@gsteel gsteel mentioned this pull request Nov 8, 2023
@gsteel gsteel marked this pull request as draft November 10, 2023 10:41
@gsteel gsteel marked this pull request as ready for review November 12, 2023 20:58
@gsteel gsteel requested review from boesing and Ocramius November 12, 2023 20:58
@gsteel
Copy link
Member Author

gsteel commented Nov 20, 2023

Gentle nudge for review from @boesing and/or @Ocramius 🫣

* }
* @extends AbstractFilter<Options>
* @implements FilterInterface<null>
Copy link
Member

Choose a reason for hiding this comment

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

I am not sure why there is null but yet do not really have the mood to give this a more closer look.
I guess you know why and thus I trust this.

/** @psalm-suppress UnusedClass */
final class AllowListChecks
{
public function filterReturnTypeIsUnionOfInputAndNull(int $value): int|null
Copy link
Member

Choose a reason for hiding this comment

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

since there is null allowed here, I guess null works in the template?
so even if you remove int from return value here, that static analysis will still pass, right?
Not sure if that is how it supposed to be?

Copy link
Member Author

Choose a reason for hiding this comment

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

This test verifies that when input type is known, it will be present in the returned union. Without the int here, the union would be mixed|null

Copy link
Member

@boesing boesing left a comment

Choose a reason for hiding this comment

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

I am not 100% sure if the templating stuff is correct here?
But I havent yet fully understood what the template type is supposed to tell us and thus, I guess thats fine? 🤷🏼‍♂️

@gsteel
Copy link
Member Author

gsteel commented Jun 16, 2024

Thanks for the review @boesing

Generally speaking the template on FilterInterface represents the successfully filtered value.

Most filters (But not AllowList), return either filtered value (T) or the input (mixed), so StringToUpper for example, would @implements FilterInterface<uppercase-string> and by default will return a union of mixed|uppercase-string.

It's only possible to narrow the return type further on filter(mixed): mixed with a conditional return such as @return ($value is string ? T : mixed).

Because filters are not frequently used standalone, the benefit of the template isn't huge, but it does allow implementations to be precise about the return type 🤷‍♂️

I probably shouldn't have started with AllowList because it's different to most other filters - successful filtering returns the input and unsuccessful filtering returns null, so it's reversed in this case, hence @implements TemplateInterface<null>.

@boesing
Copy link
Member

boesing commented Jun 17, 2024

but for the allowlist there is null defined while it is actually the value which is returned when the filtering was not successful? even tho, the return type should be a templated type from constructor, but I guess that is not possible to declare 🤔 since return type would be value-of list option.

imho, this Filter should not exist as it combines validation with filtering to a different value. the value is converted to sth invalid when stuff is not as it should be. I would expect s1 from combining validator rather than depending on filtering. but might be a personal thing.

imho allowlist is not a good example to discuss the generics stuff as that Filter is obv special and imho should not even exist

@gsteel
Copy link
Member Author

gsteel commented Jun 17, 2024

I completely agree with everything you say about AllowList - it goes against the convention of all the other filters. What I've done here is preserve its existing behaviour, and regardless of its weirdness, what I want to do to all the other filters:

  • Drop option setters and getters
  • Only allow array<string, mixed> for defining options in the constructor
  • Remove inheritance
  • Where possible, make filters immutable
  • native types everywhere
  • Useful generics (for most filters except AllowList)

I really wish I'd started with a different filter! But 'A' came first…

@boesing
Copy link
Member

boesing commented Jun 17, 2024

Works for me. I guess the outcome will provide some benefit, even tho the idea of generics is for type "safety" but for filtering and how the filters work, it will be always "mixed" as there is absolutely no way to tell if a filter was applied or not (unless explicit === comparison).
I do not expect filters from being used directly, most of these are used in forms where neither of the generics will provide any benefit.
I still like the idea but I believe it wont provide too much benefit - but maybe u proof me wrong.

So from me, feel free to continue the work. But keep in mind that it still has to work with laminas-form 👍🏻

gsteel added 7 commits June 29, 2024 15:11
- Removes inheritance
- Private properties instead of protected
- Native return types
- Native parameter types
- Options can no longer be Traversable

Signed-off-by: George Steel <[email protected]>
@gsteel gsteel changed the title [RFC] Clean up AllowList Filter as an example of general modernisation of all filters Refactor AllowList Filter, Add generic template to FilterInterface Jun 29, 2024
@gsteel gsteel self-assigned this Jun 29, 2024
@gsteel gsteel merged commit 0e6a88d into laminas:3.0.x Jun 29, 2024
11 of 12 checks passed
@gsteel gsteel deleted the allow-list branch June 29, 2024 14:22
@gsteel gsteel mentioned this pull request Sep 8, 2024
54 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants