20

Hypothetical example but real world applicability (for someone learning, like me).

Given this code:

<?php

function send_money_to_grandma() {
     internetofThings("send grandma","$1");
}

add_action('init','send_money_to_grandma');
add_action('init','send_money_to_grandma');

ok, now I bring up my WP site and log in. I traverse a few pages in Admin. The action 'init' fires a total of 100 times before my laptop battery dies.

First questions: How much money did we send to grandma? Is it $1, $2, $100 or $200 (or something else?)

If you could also explain your answer that would be awesome.

Second questions: If we want to make sure we only send grandma $1, what's the best way to do that? Global variable (semaphore) that gets set 'true' the first time we send $1? Or is there some other test to see if an action happened already and prevent it from firing multiple times?

Third question: Is this something that plugin developers worry about? I realize my example is silly but I was thinking of both performance issues and other unexpected side effects (e.g. if the function updates/inserts to the database).

  • 2
    Must admit, this is one the best questions in a long time ;-) – Pieter Goosen Nov 25 '15 at 4:30
21

Here are some random thoughts on this:

Question #1

How much money did we send to grandma?

For 100 page loads, we sent her 100 x $1 = $100.

Here we actually mean 100 x do_action( 'init' ) calls.

It didn't matter that we added it twice with:

add_action( 'init','send_money_to_grandma' );
add_action( 'init','send_money_to_grandma' );

because the callbacks and priorities (default 10) are identical.

We can check how the add_action is just a wrapper for add_filter that constructs the global $wp_filter array:

function add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
        global $wp_filter, $merged_filters;

        $idx = _wp_filter_build_unique_id($tag, $function_to_add, $priority);
        $wp_filter[$tag][$priority][$idx] = array(
            'function'      => $function_to_add, 
            'accepted_args' => $accepted_args
        );
        unset( $merged_filters[ $tag ] );
        return true;
}

If we did however change the priority:

add_action( 'init','send_money_to_grandma', 9 );
add_action( 'init','send_money_to_grandma', 10 );

then we would send her 2 x $1 per page load or $200 for 100 page loads.

Same if the callbacks where different:

add_action( 'init','send_money_to_grandma_1_dollar' );
add_action( 'init','send_money_to_grandma_also_1_dollar' );

Question #2

If we want to make sure we only send grandma $1

If we only want to send it once per page load, then this should do it:

add_action( 'init','send_money_to_grandma' );

because the init hook is only fired once. We might have other hooks that fires up many times per page load.

Let's call:

add_action( 'someaction ','send_money_to_grandma' );

but what happens if someaction fires 10 times per page load?

We could adjust the send_money_to_grandma() function with

function send_money_to_grandma() 
{
    if( ! did_action( 'someaction' ) )
        internetofThings("send grandma","$1");
}

or use a static variable as a counter:

function send_money_to_grandma() 
{
    static $counter = 0;
    if( 0 === $counter++ )
        internetofThings("send grandma","$1");
}

If we only want to run it once (ever!), then we might register an option in the wp_options table via the Options API:

function send_money_to_grandma() 
{
    if( 'no' === get_option( 'sent_grandma_money', 'no' ) )
    {
        update_option( 'sent_grandma_money', 'yes' );
        internetofThings( "send grandma","$1" );
    }
}

If we want to send her money once every day, then we can use the Transient API

function send_money_to_grandma() 
{
    if ( false === get_transient( 'sent_grandma_money' ) ) )
    {
        internetofThings( "send grandma","$1" );
        set_transient( 'sent_grandma_money', 'yes', DAY_IN_SECONDS );
    }
}

or even use the wp-cron.

Note that you might have ajax calls. as well.

There are ways to check for those, e.g. with DOING_AJAX

There might also be redirections, that could interrupt the flow.

Then we might want to restrict to the backend only, is_admin() or not: ! is_admin().

Question #3

Is this something that plugin developers worry about?

yes this is important.

If we want to make our grandma very happy we would do:

add_action( 'all','send_money_to_grandma' );

but this would be very bad for performance ... and our wallet ;-)

  • wow - thank you for such a thorough response; this helps immensely! – C C Nov 24 '15 at 23:05
  • 1
    You're welcome - I really enjoyed how you phrased your question ;-) @CC – birgire Nov 24 '15 at 23:26
  • 2
    At the end of the day, we want to keep our wallets and grandma happy, so it is all about finding the perfect harmony/balance ;-) – Pieter Goosen Nov 25 '15 at 4:32
  • very nice answer +1, but it worth to say that how may times an action is added also depends on the callback id, and when dealing with objects things are a bit more complex... Hard to better explain the concept here, I'll write an answer... – gmazzap Nov 25 '15 at 12:29
  • thanks @gmazzap - yes that would be great, since I didn't cover the third key $idx and _wp_filter_build_unique_id(), just displayed it ;-) – birgire Nov 25 '15 at 12:50
8

This is more a comment to the very good Birgire's answer than a complete answer, but having to write code, comments don't fit.

From the answer it may seems that the only reason why action is added once in OP sample code, even if add_action() is called twice, is the fact that the same priority is used. That's not true.

In the code of add_filter an important part is _wp_filter_build_unique_id() function call, that creates an unique id per callback.

If you use a simple variable, like a string that holds a function name, e.g. "send_money_to_grandma", then the id will be equal to the string itself, so if priority is the same, being id the same as well, the callback is added once.

However, things are not always simple like that. Callbacks may be anything that is callable in PHP:

  • function names
  • static class methods
  • dynamic class methods
  • invokable objects
  • closures (anonymous functions)

The first two are represented, respectively, by a string and an array of 2 strings ('send_money_to_grandma' and array('MoneySender', 'send_to_grandma')) so the id is always the same, and you can be sure that callback is added once if priority is the same.

In all the other 3 cases, the id depends on object instances (an anonymous function is an object in PHP) so the callback is added once only if the object is the same instance, and it is important to note that same instance and same class are two different things.

Take this example:

class MoneySender {

   public function sent_to_grandma( $amount = 1 ) {
     // things happen here
   }

}

$sender1 = new MoneySender();
$sender2 = new MoneySender();

add_action( 'init', array( $sender1, 'sent_to_grandma' ) );
add_action( 'init', array( $sender1, 'sent_to_grandma' ) );
add_action( 'init', array( $sender2, 'sent_to_grandma' ) );

How many dollars are we sending per page load?

Answer is 2, because the id WordPress generates for $sender1 and $sender2 are different.

The same happen in this case:

add_action( 'init', function() {
   sent_to_grandma();
} );

add_action( 'init', function() {
   sent_to_grandma();
} );

Above I used the function sent_to_grandma inside closures, and even if the code is identical, the 2 closures are 2 different instance of \Closure object, so WP will create 2 different ids, which will cause the action be added twice, even if priority is the same.

4

You can't add the same action to the same action hook, with the same priority.

This is done to prevent multiple plugins relying on a third party plugins' action to happen more than once (think woocommerce and all it's third party plugins, like gateway payment integrations, etc). So without specifying priority, Grandma remains poor:

add_action('init','print_a_buck');
add_action('init','print_a_buck');

function print_a_buck() {
    echo '$1</br>';
}
add_action('wp', 'die_hard');
function die_hard() {
    die('hard');
}

However, if you add priority to those actions:

add_action('init','print_a_buck', 1);
add_action('init','print_a_buck', 2);
add_action('init','print_a_buck', 3);

Grandma now dies with $4 in her pocket (1, 2, 3 and the default: 10).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.