How many times will this code run? (or, how rich is grandma?)

The question:

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).

The Solutions:

Below are the methods you can try. The first solution is probably the best. Try others if the first one doesn’t work. Senior developers aren’t just copying/pasting – they read the methods carefully & apply them wisely to each case.

Method 1

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 😉

Method 2

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.

Method 3

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).


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Comment