[wp-trac] [WordPress Trac] #51525: Add new functions apply_filters_single_type() and apply_filters_ref_array_single_type() (was: Add new functions apply_filters_typesafe() and apply_filters_ref_array_typesafe())
WordPress Trac
noreply at wordpress.org
Mon Oct 19 17:13:46 UTC 2020
#51525: Add new functions apply_filters_single_type() and
apply_filters_ref_array_single_type()
------------------------------------------+---------------------
Reporter: jrf | Owner: (none)
Type: feature request | Status: new
Priority: normal | Milestone: 5.6
Component: General | Version: trunk
Severity: normal | Resolution:
Keywords: 2nd-opinion needs-patch php8 | Focuses:
------------------------------------------+---------------------
Old description:
> This is a proposal to add two new functions `apply_filters_typesafe()`
> and `apply_filters_ref_array_typesafe()`.
>
> == Underlying reasons
>
> PHP 8 is a lot more strict about data types and depending on what is done
> with the data will start throwing more notices/warnings, but more
> concerning exceptions, which will result in fatal errors (= white screen
> of death) when uncaught.
>
> == Problem outline
>
> Now, say you have a filter hook `foo` which passes an integer as its
> second argument and expects an integer to be returned.
> {{{#!php
> <?php
> $int = apply_filters( 'foo', $int );
> }}}
>
> Also say that there are two functions hooked into this filter, function
> `plugin_a()` and function `plugin_b()`.
>
> `apply_filters()` will pass the original integer to `plugin_a()` and
> subsequently will pass the returned value from `plugin_a()` as the
> parameter to `plugin_b()`.
>
> Now take a situation where `plugin_a()` returns something other than an
> integer, while `plugin_b()` rightfully expects to receive an integer and
> doesn't do additional type checking (which it shouldn't have to do) and
> this results in an error.
>
> **Plugin B will be identified in the error backtrace as the place where
> the error occurred, so plugin B will be blamed and get a bad reputation,
> while plugin A is actually at fault.**
>
> Code example: https://3v4l.org/uqT9G
>
> == Proposed solution
>
> To combat this situation I propose to add two new functions
> `apply_filters_typesafe()` and `apply_filters_ref_array_typesafe()`.
>
> These functions would basically have the same underlying logic as the
> existing `apply_filters()` and `apply_filters_ref_array()` functions,
> with one big difference:
> * At the start of the function, the variable type of the second parameter
> (the value being filtered) received is checked and remembered.
> * In the loop which calls the hooked in functions, a check is done on the
> variable type received back from each function and if it doesn't match
> the expected variable type, the previous value is used for the call to
> the next hooked-in function and a `__doing_it_wrong()` error is thrown
> identifying the function which returned the incorrectly typed variable.
> This includes doing this check after the last hooked-in function is
> called, before returning the value to the original caller of
> `apply_filters_typesafe()`.
>
> For `apply_filters_ref_array()`, this would apply to the first array item
> in `$args`.
>
> === Practical advantages
> 1. Using these functions will prevent potential fatal errors, diminishing
> the impact of these type of developer errors for end-users.
> 2. The "Doing it wrong" notice will make identifying the
> function/plugin/theme which caused the problem a lot easier.
> 3. Plugins/Themes and WP core using the new functions will be able to
> count on the parameter/return type of the call to these new functions and
> can write their code based on that, without excessive defensive coding to
> defend against the errors made by others.
> 4. Using these functions will help in the efforts to identify more PHP 8
> related incompatibilities, both in WP Core as well as in plugins and
> themes.
>
> === Technical considerations
>
> 1. I considered adding a new `$typesafe` parameter to the existing
> `apply_filters()` function, but as it can be passed a variable number of
> arguments to be passed along to the hooked-in functions, this is not an
> option. This doesn't apply to `apply_filters_ref_array()`, but for
> consistency I think introducing a new sister-function for both would be
> the better choice.
> 2. I've also considered type juggling an incorrectly type value within a
> `try/catch`, using that value to pass on to the next function if the
> juggling succeeded and only using the previous value if the juggling did
> not succeed, but I think that could cause even more problems, so I'd
> rather opt for ignoring the incorrectly typed returned value altogether.
>
> == Future scope
>
> Once these functions are in place, a review should be done of all WP Core
> calls to `apply_filters()` and `apply_filters_ref_array()` and where the
> filtered value is expected to be a singular type, the function call
> should be switched out for a call to one of the new functions.
>
> The WP version in which this review is done will need a good dev-note
> about these changes as they ''could'' be considered a BC-break, but then
> again, they are only a BC_break for hooked-in functions already ''doing
> it wrong'' and likely causing problems already.
>
> == Other
>
> I'm tentatively milestoning this for WP 5.6, even though I realize this
> is late in the game, but as this proposal could help with the PHP 8
> efforts, I believe it would be good to address this sooner rather than
> later.
>
>
> /cc @omarreiss
New description:
This is a proposal to add two new functions ~~`apply_filters_typesafe()`~~
`apply_filters_single_type()` and ~~`apply_filters_ref_array_typesafe()`~~
`apply_filters_ref_array_single_type()`.
== Underlying reasons
PHP 8 is a lot more strict about data types and depending on what is done
with the data will start throwing more notices/warnings, but more
concerning exceptions, which will result in fatal errors (= white screen
of death) when uncaught.
== Problem outline
Now, say you have a filter hook `foo` which passes an integer as its
second argument and expects an integer to be returned.
{{{#!php
<?php
$int = apply_filters( 'foo', $int );
}}}
Also say that there are two functions hooked into this filter, function
`plugin_a()` and function `plugin_b()`.
`apply_filters()` will pass the original integer to `plugin_a()` and
subsequently will pass the returned value from `plugin_a()` as the
parameter to `plugin_b()`.
Now take a situation where `plugin_a()` returns something other than an
integer, while `plugin_b()` rightfully expects to receive an integer and
doesn't do additional type checking (which it shouldn't have to do) and
this results in an error.
**Plugin B will be identified in the error backtrace as the place where
the error occurred, so plugin B will be blamed and get a bad reputation,
while plugin A is actually at fault.**
Code example: https://3v4l.org/uqT9G
== Proposed solution
To combat this situation I propose to add two new functions
`apply_filters_single_type()` and `apply_filters_ref_array_single_type()`.
These functions would basically have the same underlying logic as the
existing `apply_filters()` and `apply_filters_ref_array()` functions, with
one big difference:
* At the start of the function, the variable type of the second parameter
(the value being filtered) received is checked and remembered.
* In the loop which calls the hooked in functions, a check is done on the
variable type received back from each function and if it doesn't match the
expected variable type, the previous value is used for the call to the
next hooked-in function and a `__doing_it_wrong()` error is thrown
identifying the function which returned the incorrectly typed variable.
This includes doing this check after the last hooked-in function is
called, before returning the value to the original caller of
`apply_filters_single_type()`.
For `apply_filters_ref_array()`, this would apply to the first array item
in `$args`.
=== Practical advantages
1. Using these functions will prevent potential fatal errors, diminishing
the impact of these type of developer errors for end-users.
2. The "Doing it wrong" notice will make identifying the
function/plugin/theme which caused the problem a lot easier.
3. Plugins/Themes and WP core using the new functions will be able to
count on the parameter/return type of the call to these new functions and
can write their code based on that, without excessive defensive coding to
defend against the errors made by others.
4. Using these functions will help in the efforts to identify more PHP 8
related incompatibilities, both in WP Core as well as in plugins and
themes.
=== Technical considerations
1. I considered adding a new `$typesafe` parameter to the existing
`apply_filters()` function, but as it can be passed a variable number of
arguments to be passed along to the hooked-in functions, this is not an
option. This doesn't apply to `apply_filters_ref_array()`, but for
consistency I think introducing a new sister-function for both would be
the better choice.
2. I've also considered type juggling an incorrectly type value within a
`try/catch`, using that value to pass on to the next function if the
juggling succeeded and only using the previous value if the juggling did
not succeed, but I think that could cause even more problems, so I'd
rather opt for ignoring the incorrectly typed returned value altogether.
== Future scope
Once these functions are in place, a review should be done of all WP Core
calls to `apply_filters()` and `apply_filters_ref_array()` and where the
filtered value is expected to be a singular type, the function call should
be switched out for a call to one of the new functions.
The WP version in which this review is done will need a good dev-note
about these changes as they ''could'' be considered a BC-break, but then
again, they are only a BC_break for hooked-in functions already ''doing it
wrong'' and likely causing problems already.
== Other
I'm tentatively milestoning this for WP 5.6, even though I realize this is
late in the game, but as this proposal could help with the PHP 8 efforts,
I believe it would be good to address this sooner rather than later.
/cc @omarreiss
--
Comment (by jrf):
Hi @giuseppe.mazzapica, thank you for sharing your thoughts on this. You
bring up some good points, though unfortunately I disagree with nearly
all.
As the title of the issue seems to leave the impression that these
functions should support full type declarations as supported by PHP, I'm
going to change the `_typesafe` in the suggested function names to
`_single_type`.
Let's step through the points you bring up one by one:
== Regarding `null`
> Regarding `null` there are two cases: the first is that null is the
value to be filtered, the second is that null is the value returned by the
filters. In the first case, there's basically nothing to do, passing null
to apply_filters_typesafe would equal to apply_filters. In the second
case, null could be acceptable by the caller.
`null` should never be acceptable. As stated before, passing `null` as the
original value, would mean that running the filters is useless as `null`
would be the only acceptable outcome.
Regarding filter functions returning `null`, that is also not acceptable
as it breaks the principle of this function being typesafe. Filters should
always return a value and in the case of filters called via these
functions, a value of the same type as originally received, so `null`
would never be an acceptable return.
You are basically suggesting to support "nullable types" in this function.
This goes against the principle of '''''one type and one type only'''''
and would significantly reduce the advantages of having these functions in
the first place.
== Regarding `callable`
I can't think of a single reason for a callable being filterable. To be
honest, that just sounds like a security issue waiting to be exploited,
which I don't think is a good case to support.
If people still want to do so, let them use `apply_filters()`, but I'd
really wouldn't want to support it in a function with `safe` in the
function name.
== Regarding `iterable` and `countable`
These I'd need to have a think about, but I'm leaning against a "no", if
for no other reason than that it would still require the code in the
callback functions hooked into the "typesafe" filter to allow for several
different types and several different way to ''add'' or ''remove''
something from the iterable/countable.
Again, that situation can be handled by using `apply_filters()`.
== Regarding `number` and `numeric`
First of all, this violates the single type principle again. Second of
all, too few people actually understand that `is_numeric()` will also
accept `NAN`, `INF`, `'1e1'` (floats in exponent annotation), `' 1'` and
more.
Also see: https://phpcheatsheets.com/test/numeric_tests/
Supporting `number`/`numeric` would also negate some of the advantages
regarding PHP 8, which introduces "saner numeric comparisons" and "saner
numeric strings".
In other words, the behaviour of whatever is passed through would become
unpredicatable PHP cross-version, which goes against the whole point of
these new functions and would make identifying the callbacks ''doing it
wrong'' again more difficult.
== Regarding `mixed`
No. Just plain no. Use `apply_filters()`, that's the behaviour you expect
there. Just like union types, `mixed` should not be supported by this
function.
== Regarding adding an extra argument, let's call it `$type_spec`.
I tried to avoid adding an extra argument to make switching existing calls
from `apply_filters()` to the new functions as easy as possible, but I can
see your point about objects.
''If'' we'd go that way, we would basically need to have a "type" key and
an "instanceof" key. If "instanceof" is set, "type" would not need to be
set, though setting it to `object` would be acceptable.
But then, why have two array keys ? We know what the supported basic types
are and if whatever is passed (as a string) is not one of the basic types,
doing an `instanceof` could be done automatically.
Also, and again ''if'' we'd go the way of adding a "type" argument, then
I'd advocate for the original value being passed to
`apply_filters_single_type()` to also be evaluated for validity before any
callback functions are called, a ''doing it wrong'' being thrown if it is
not of the expected type and the function short circuiting (not calling
any callbacks) if the wrong type was passed.
This function should '''''never''''' fallback to calling `apply_filters()`
as in that case, it negates the whole point of having this function in
place, as callbacks would still not be able to trust the input received
from via "typesafe" hooks.
> plugin.php can't have external dependencies, so before doing any
_doing_it_wrong it is necessary to check that function is defined.
Good point :+1:
> `// Objects are passed by ref, clone to return original unchanged in
case of errors.`
Good point, this should definitely be taken into account.
--
Ticket URL: <https://core.trac.wordpress.org/ticket/51525#comment:7>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform
More information about the wp-trac
mailing list