[wp-trac] [WordPress Trac] #51525: Add new functions apply_filters_typesafe() and apply_filters_ref_array_typesafe()

WordPress Trac noreply at wordpress.org
Mon Oct 19 12:34:13 UTC 2020


#51525: Add new functions apply_filters_typesafe() and
apply_filters_ref_array_typesafe()
------------------------------------------+---------------------
 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:
------------------------------------------+---------------------

Comment (by giuseppe.mazzapica):

 Thanks a lot for this @jrf

 A few notes:

 - `plugin.php` can't have external dependencies, so before doing any
 `_doing_it_wrong` it is necessary to check that function is defined.
 - Automatically determine the expected type might not always possible. For
 example, I can pass a `string` but what I actually want is a `callable`.
 Same with all the other pseudo-types. E.g. an array is passed but any
 `iterable` could do.
 - Things become even more complex with objects. If I pass an object to
 `apply_filters_typesafe` and then I call a method on the result, I want
 the object I retrieve to have that method. So checking the result to be an
 `object` will not be enough, and checking all parent classes can be wrong
 either. Let's imagine an object that extends/implements `A` ''and'' `B`,
 but on the result of the filter, I call a method of `B`. If the filter
 returns an `instance of A` that is no good.
 - 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.
 - Regarding strictness about int and float, I think the default behavior
 should be fine with it. Even PHP core does that and there's no need to be
 stricter than PHP.

 To take into consideration all the points above, I think that
 `apply_filters_typesafe` should accept a set of args, as in the best
 WordPress tradition, to fine-tune its behavior.

 I can think of `accepted_types` and `nullable` options.

 That said, because I'm better at PHP than words, I'll paste here what my
 idea would be.

 Note this is meant to only be a discussion point, and I have not tested
 this at all.

 {{{#!php
 <?php
 function apply_filters_typesafe( $tag, $arguments = array(), $value =
 null, ...$values ) {

     if ( null === $value ) {
         return apply_filters( $tag, $value, ...$values );
     }

     $type = gettype( $value );
     $is_object = false;
     switch ( $type ) {
         case 'boolean':
             $accepted_types = array( 'boolean' );
             break;
         case 'integer':
         case 'double':
             $accepted_types = array( 'numeric' );
             break;
         case 'string':
             $accepted_types = array( 'string' );
             break;
         case 'array':
             $accepted_types = array( 'array' );
             break;
         case 'resource':
         case 'resource (closed)':
             $accepted_types = array( 'resource' );
             break;
         case 'object':
             $is_object = true;
         default:
             $accepted_types = array();
     }

     // Skip calculation of accepted types if they are are explicitly
 passed.
     if ( $is_object && empty ( $arguments['accepted_types'] ) ) {
         $class = get_class( $value );
         $accepted_types = array( $class );
         $parent = get_parent_class( $class );
         while ( $parent ) {
             $accepted_types[] = $parent;
             $parent = get_parent_class( $parent );
         }

         $accepted_types = array_merge( $accepted_types, class_implements(
 $class ) );
     }

     $arguments = wp_parse_args(
         $arguments,
         array(
             'nullable' => false,
             'accepted_types' => $accepted_types
         )
     );

     $original = $value;
     // Objects are passed by ref, clone to return original unchanged in
 case of errors.
     $to_filter = $is_object ? clone $value : $value;

     $filtered = apply_filters( $tag, $to_filter, ...$values );

     // 'mixed' is a valid PHP 8 pseudo-type so we support for consistency.
     // That said, if mixed is fine then just use apply_filters.
     if ( in_array( 'mixed', (array)$arguments['accepted_types'] ) ) {
         return $filtered;
     }

     static $can_do_it_wrong = false;
     if ( ! $can_do_it_wrong && function_exists( '_doing_it_wrong' ) ) {
         $can_do_it_wrong = true;
     }

     if ( ( null === $filtered && !$arguments['nullable'] ) ) {
         if ( $can_do_it_wrong ) {
             _doing_it_wrong(
                 __FUNCTION__,
                 "Filters for $tag where not expected to return null.",
                 '5.6'
             );
         }

         return $original;
     }

     static $functions;
     if ( ! $functions ) {
         $functions = array(
             'integer' => 'is_int',
             'double' => 'is_float',
             'float' => 'is_float',
             'numeric' => 'is_numeric',
             'number' => 'is_numeric',
             'bool' => 'is_bool',
             'boolean' => 'is_boolean',
             'string' => 'is_string',
             'array' => 'is_array',
             'callable' => 'is_callable',
             'function' => 'is_callable',
             'resource' => 'is_resource',
             'iterable' => 'is_iterable',
             'countable' => 'is_countable',
         );
     }

     foreach ( $arguments['accepted_types'] as $type ) {
         if ( isset( $functions[ $type ] ) && call_user_func( $functions[
 $type ], $filtered ) ) {
             return $filtered;
         }

         if ( $is_object && is_string ( $type ) && is_a( $filtered, $type )
 ) {
             return $filtered;
         }
     }

     if ( $can_do_it_wrong ) {
         $expected = implode( ', ', $arguments['accepted_types'] );
         $actual = is_object( $filtered ) ? 'instance of ' .
 get_class($filtered) : gettype( $filtered );
         _doing_it_wrong(
             __FUNCTION__,
             "Filters for $tag where expected to return a value of one of
 types $expected. Got $actual instead.",
             '5.6'
         );
     }

     return $original;
 }

 }}}

 I know it's complex, but there are complex issues to tackle.

 Just to leave here a few examples, you could use the above function like
 this:

 {{{#!php
 <?php
 $callable = apply_filters_typesafe('foo', ['accepted_types' =>
 ['callable']], '__return_true');
 $callable();
 }}}

 Or:

 {{{#!php
 <?php
 $fs = apply_filters_typesafe('foo', ['accepted_types' =>
 ['WP_Filesystem_Base']], $fs);
 $fs->find_folder('bar');
 }}}

 Or even:

 {{{#!php
 <?php
 /** @var numeric|null $num */
 $num = apply_filters_typesafe('foo', ['nullable' => true], 0);
 $int = (int)($num ?? 42);
 }}}

-- 
Ticket URL: <https://core.trac.wordpress.org/ticket/51525#comment:4>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform


More information about the wp-trac mailing list