• Resolved DanielAGW

    (@danielagw)


    Hello,
    I was recently updating my plugin when I realized that I don’t know how to use PHP closures properly within filters/hooks in a class-based plugin.

    Basically this is what I wanted to do:

    add_action('admin_bar_menu', function(){
    	add_filter('get_avatar', 'some_function');
    }, 0);

    So we have two typical WP hooks, one inside another – we’re using anonymous function with admin_bar_menu action and we’re filtering get_avatar with the output of function named some_function.

    That’s easy and it would have worked… if only my plugin wasn’t an object. And because I realised I don’t know how to use anonymous functions with WP hooks in class-based plugins, I ended up doing this:

    class Some_plugin {
    
    	public function __construct(){
    		add_action('admin_bar_menu', array($this, 'some_unnecessary_function'), 0);
    	}
    
    	public function some_unnecessary_function(){
    		add_filter('get_avatar', array($this, 'some_function'), 10);
    	}
    
    	public function some_function(){
    		//do something
    	}
    
    }
    
    $some_plugin = new Some_plugin();

    This solution works, of course, but I am not very happy with the way it is written. Declaring another method just to make it work, instead of using perfectly-suited closures which were born to serve exactly this purpose, made me look for the solution, but I just couldn’t find anything. I know how to use closures with WP hooks, I know how to write object-oriented plugins, but I don’t know how to use closures with WP hooks in an object-oriented plugin. Any ideas?

    Thank you for your time.

    Cheers,
    Daniel

Viewing 9 replies - 1 through 9 (of 9 total)
  • Have you tried this (inside the class)?

    add_action('admin_bar_menu', function(){
    	add_filter('get_avatar', array($this, 'some_function'));
    }, 0);

    Or this outside the class:

    add_action('admin_bar_menu', function(){
    	add_filter('get_avatar', 'Some_plugin::some_function');
    }, 0);

    I’m not a fan of using closures with WP hooks, because they can’t be unhooked then. This limits the ability of anyone else using your code and trying to modify its behavior.

    Nonetheless the closure shouldn’t be a problem. Have you tested the hook using __CLASS__ instead of $this in the inner function call? If you hook something and then use the hook again it will not be seen as a part of the object of the class created.

    For instance:

    add_action( 'admin_bar_menu' , function(){
    	add_filter( 'get_avatar', array( __CLASS__, 'some_function' ) );
    }, 0 );'

    If this won’t work make the function static and use the class name instead of the class.

    The problem is connected with the fact that the inner function is called from within the place where first action is called (which means outside of the class), so there’s no object to apply the call to.

    Thread Starter DanielAGW

    (@danielagw)

    @anastis Sourgoutsidis: Thanks, but it’s the first thing I tried. Arguments are not passed to some_function when using the first method.

    @gicolek: __CLASS__ gives the same result as $this – arguments are not passed to the function (missing arguments warning). I kind of understand why it happens – when I apply action/filter to array($this, 'foo'), the hook will execute foo() from within the class. When I use closure instead, there is no way to execute instructions (entered in a form of anomyous function) within my class. This construction would make sense in this case:

    array($this, function(){
    	add_filter('get_avatar', array($this, 'some_function'));
    })

    So that closure would be part of the array along with $this, just like we do it when we use an array with $this and a method name. Only in this case, instead of function name, a full function in a form of closure would be used.

    Yes Daniel, that’s exactly what I had written. Actually I shouldn’t have proposed the __CLASS__ as per your description, as it contradicts to what I wrote later on ?? The problem is the scope here. It’s not only caused by the closure, it’s caused by the fact that filters / actions are run outside the class; to be precise in the place where corresponding apply_filters, do_action calls have been specified.

    You can also declare your function static and reference it directly via

    array( CLASS_NAME, method_name);

    Does the method described above by you work at all?

    Thread Starter DanielAGW

    (@danielagw)

    it’s caused by the fact that filters / actions are run outside the class; to be precise in the place where corresponding apply_filters, do_action calls have been specified.

    But it does work perfectly when I use normal function call instead of closure. I apply action callback to function_x() which applies filter callback to function_y() and it works. It only doesn’t work when I don’t use callback to function_x() and apply filter callback to function_y() in action callback with a closure. That’s what I have written in the first post:

    class Some_plugin {
    
    	public function __construct(){
    		add_action('admin_bar_menu', array($this, 'some_unnecessary_function'), 0);
    	}
    
    	public function some_unnecessary_function(){
    		add_filter('get_avatar', array($this, 'some_function'), 10);
    	}
    
    	public function some_function(){
    		//do something
    	}
    
    }
    
    $some_plugin = new Some_plugin();

    some_unnecessary_function() represents aforementioned function_x(), and some_function() is function_y(). This solution works perfectly. The problem is that I don’t want to define some_unnecessary_function() just for the sake of defining it so I can callback to it later on. I want to use closure in __construct() in add_action(). And I can’t do that, because I can’t create array with $this and anonymous function, just like I can create array with $this and a function name.

    So the scope is not a direct problem here. The problem is that I cannot correctly specify the scope when using closures to WP hooks. I can do it easily when I use callback to another method, but I cannot do it when I use closure.

    Does the method described above by you work at all?

    Not at all, it was more of a pseudo-code. Just how I would imagine it working. Just like we use array($this, 'function_name'), it would make sense to use array($this, function(){}) to represent the same thing, only without the need to define function_name() just for the sake of defining it to callback to it later.

    EDIT:
    Okay, after finishing this post I realised that you were right all along and it makes sense. And it IS all about the scope and closures are not to blame here, it’s all because of the place where this closure is located. It’s still very, very blurry to me, though. I’ve used to think of closures as a String, for example, whereas the function definition and its content as a variable with this String (or any other data type) assigned to it. So we can do this:
    some_random_function('text');
    But we can also do this:

    $some_string = 'text';
    some_random_function($some_string);

    Technically, it does not matter which option we choose, the effect will be the same (except for minor performance difference). This is how I used to think about using WP hooks with function names or with closures – we can define the function and callback to it in the hook, or we can callback directly to a closure, which would result in the same thing, just without having the function defined separately. But I begin to understand that this is not the same, right? So when we use hook, like: add_action('some_hook', array($this, 'foo'), we are actually passing the object we want to use the method with (hence $this – cause we want to use it with current object) and running its foo() method? But I still can’t get my head around why it is not possible with closures! If anonymous function was used instead of ‘foo’, we should also be able to pass current object and use anonymous function within the scope of this object. I don’t get it ??

    Sorry for such a long post, the more I think about it the less I know…

    Hey Daniel,

    So when we use hook, like: add_action(‘some_hook’, array($this, ‘foo’), we are actually passing the object we want to use the method with (hence $this – cause we want to use it with current object) and running its foo() method?

    That’s correct!

    But I still can’t get my head around why it is not possible with closures!

    Educating myself a bit: perhaps using the alternate syntax for closures would help again?

    Anyway the best way to identify the issue is to actually look at the source of the apply_filters, do_action, add_filter and add_action methods. Dwelling in there you’ll see that do_action is a tricky wrapper to apply_filters, and apply_filters is using call_user_func_array twice – this is why our hooked function is the executed.

    However this function is executed from the place where one of the hooks has been defined. Which means from OUTSIDE of the class. So we actually need to make the function public and then it will be called automatically via PHP.

    The above is bull*hit actually ?? I was wrong all the way. I’m correct up to a point. I think that you can’t use the object from the closure call, because the object will not be a part of the corresponding scope in that case.

    And the reason for that is that the following code…

    	`add_action( 'init', function() {
    			add_filter('the_content', function(){
    				return 'aabbbb';
    			}, 9999);
    		}, 99999 );`
    

    … works from within the constructor of a class properly. I have just tested it. I think that the problem with your code lies in lack of priority of the hook. Have you tried altering its pririty like I did?

    By the way look on a comment about closures with filters.

    Thread Starter DanielAGW

    (@danielagw)

    Thank you so much gicolek for your help. I just realized that… everything works just fine. The missing arguments warning I was getting was present only when I didn’t include $accepted_args in the hook. So the first solution proposed by Anastis Sourgoutsidis caused missing arguments error in my function. At first I have written it the same way he did and that’s why I couldn’t get it right. But doing it this way:

    add_action('admin_bar_menu', function(){
    	add_filter('get_avatar', array($this, 'some_function'), 10, 5);
    }, 0);

    works just as it should. I thought that the warning is caused by not passing arguments to my method because of scope, so I tried looking for solution to run closure in correct scope. And it turned that the scope is okay, it’s just the arguments weren’t passed because I haven’t specified them…

    I think that you can’t use the object from the closure call, because the object will not be a part of the corresponding scope in that case.

    Well, actually we can do it. PHP has no problem with that and the reason why I had problems was only because I wasn’t clever enough to think that “missing arguments” warning may actually mean… well, missing arguments in function. Not a problem with closure, scope or anything else. Just missing arguments because I haven’t specified $accepted_args in the hook.

    So sorry for misguiding you, I wish I examined it more thoroughly beforehand. Good thing is that I learned much more about closures, WP and PHP in general, so thank you for your time and willingness to help.

    After reading a lot about closures I realized that they are very weird. Not as weird as they are in JS, but in JS at least they make much more sense, because the language is much more event-driven. Closures in PHP seem very unnatural to me.

    EDIT:
    I think this will only work with PHP 5.4+. See Changelog of anonymous functions – https://php.net/manual/en/functions.anonymous.php
    I wonder why $this couldn’t be used in anonymous functions before PHP 5.4? Anonymous functions are great for callbacks, and today, when 90% of programming is OOP, we want to use this object also in anonymous functions… Well, fortunately it works in 5.4+.

    Thread Starter DanielAGW

    (@danielagw)

    I just checked it on my local environment (with PHP 5.3) and that’s correct, it doesn’t work. So the problem wasn’t only with callback arguments, it was $this in closure as well. I did first tests on local environment with PHP 5.3 and it didn’t work, even though I was doing everything right. It didn’t work because I was using $this in closure which gave me call_user_func_array() expects parameter 1 to be a valid callback, first array member is not a valid class name or object warning, which I didn’t quite understand. Then I switched to my remote environment to do further tests (with PHP 5.4), but in this case I forgot to specify the arguments param in the hook and it didn’t work because of my function construction (function was executed properly). So there were actually two problems happening due to different reasons.

    I’m glad to have this finally sorted out. Great learning experience, thanks gicolek and Anastis Sourgoutsidis.

    Hey Daniel,

    It was a learning experience for me either. I’ve mixed both closures and objects in my mind somehow giving my answers ??

    Good luck with your code!

    And please bear in mind inability to unhook stuff with closures.

Viewing 9 replies - 1 through 9 (of 9 total)
  • The topic ‘Using PHP closures in class-based plugin’ is closed to new replies.