Brain-WP/BrainMonkey

Issue with dynamically created callback method

schlessera opened this issue · 5 comments

BrainMonkey does not seem to correctly identify dynamically created callback methods.

I created a new callback method to be used as an add_filter() callback:

class MyClass() {

    public function register( $arg ) {
        $method_name = "callback_for_${arg}";

        $this->$method_name = function ( $value ) use ( $arg ) {
            return $value;
        }

        add_filter( 'my_filter', [ $this, $method_name ] );
    }
}

This code will cause BrainMonkey to throw an InvalidArgumentException:

InvalidArgumentException: A callback is required to add a filter.

When I var_dump the state inside of the Brain\Monkey\WP\Hooks::args() method, I get the following:

array(5) {
  'args' =>
  array(0) {
  }
  'type' =>
  string(6) "filter"
  'getId' =>
  bool(false)
  'hook' =>
  string(21) "my_filter"
  'callback' =>
  array(2) {
    [0] =>
    class MyClass#29 (2) {
      public $callback_for_test =>
      class Closure#50 (3) {
        ...
      }
    }
    [1] =>
    string(17) "callback_for_test"
  }
}

The dump I did was this:

private function args(array $args, $type, $getId = false) {
    // [...]
    if (($getId && ! is_string($callback)) || (! $getId && ! is_callable($callback))) {
        var_dump( [
            'args' => $args,
            'type' => $type,
            'getId' => $getId,
            'hook' => $hook,
            'callback' => $callback
        ] );
        //throw new InvalidArgumentException("A callback is required to add a {$type}.");
    }
    // [...]
}

According to your code, the callback added to the filter is [ $this, $method_name ], but that's not a valid callback: a valid callback can take the form of an array only if the first array element is an object (class name or instance), and the second element is a method name in a string.

In your code, $method_name is actually a Closure...

This is the proof of I want to say: https://3v4l.org/6Au6U

Is there something wrong in the code you posted as example here?

For example, if you change the code to:

add_filter( 'my_filter', $this->$method_name );

it works, see here: https://3v4l.org/5MgOi

Hmm, I'll have to investigate some more. I know that it is a Closure, but I'm using this exact code in another place as well, without unit tests, and it just works with WP filters...

I'll keep you updated.

Okay, nevermind, I'm just too stupid to read my own code...

In the other class, I did indeed add the closure in the exact same way, but WordPress filtering did not work as is, but only because I had also added the following piece of code to that class as well:

    /**
     * Magic method to allow dynamic callbacks
     *
     * @throws BadMethodCallException An undefined callback was invoked.
     *
     * @since 1.0.0
     *
     * @param  string $method         Method that has been called.
     * @param  array  $args           Arguments that have been passed to the
     *                                method.
     * @return mixed                  Return value of the method being called.
     */
    public function __call( $method, $args ) {
        if ( is_callable( array( $this, $method ) ) ) {
            return call_user_func_array( $this->$method, $args );
        }
        throw new BadMethodCallException( 'An undefined callback was invoked: ' . (string) $method );
    }

As soon as I add this magic method, the tests with BrainMonkey work as well.

So, please just ignore my nonsense above! :)

@schlessera Things like this just happen: this is why we write unit tests. I'm just happy that BrainMonkey did its work: catched an error during unit tests ;)