WordPress do_action, PHP 5.4, and pass-by-reference

A somewhat common idiom in WordPress is to pass an object by reference 1 into do_action for possible modification: do_action(‘before_frobulation’, $frob_params, &$this); Just out of curiosity, if you’re expecting possible modification, wouldn’t apply_filter be more semantically logical? I’m glad you asked! It is a subtle distinction, but in this case, no. apply_filter gives the opportunity…

Published on by Justin de Vesine

A somewhat common idiom in WordPress is to pass an object by reference 1 into do_action for possible modification:

do_action('before_frobulation', $frob_params, &$this);
Just out of curiosity, if you're expecting possible modification, wouldn't apply_filter be more semantically logical?

I'm glad you asked! It is a subtle distinction, but in this case, no. apply_filter gives the opportunity to replace a value with something else ($foo = apply_filter('filter_foo', $foo);) – but there is no way to do so when the thing in question is the very object that is calling out to the hooks; you can't assign a new object to $this.

In fact, there may be no effective way to do it, even if you're talking about some other object, if there is some chance that some other scope holds a reference an object identifier to that object - changing what object your reference object identifier points to doesn't change the original object.

(Edit: If you have a symbol table alias to another variable that holds an object identifier - a "reference" - and you change that variable to contain another object identifier, you can change what object that other variable is pointing to. Unless you're talking about $this, where you can't. Except in method scope, where you can get $this to lie to you.)

Given those constraints, saying "please replace this thing with a thing of your choosing", by invoking apply_filter, is not what you want to say. Rather, you want to say "here is the state of the world; do some actions if you like", where part of the state of the world is a reference to an object. If the object were available as a global - the global post object, for example - this situation would be slightly more obvious: when you're taking action at this point in time, the global foo object is available to you. This is a way of handling the situation where the object in question is available in private scope instead.

Ok. Back to passing by reference?

Back to passing by reference. In PHP 5.4, that last bit (&$this) is no longer valid - passing a variable by reference must be specified in the function declaration, not at call time. This was deprecated in 5.3, which is what most of our world2 runs on, but removed in 5.4.

The appropriate and slightly more future-safe way to do this is using do_action_ref_array instead:

do_action_ref_array('before_frobulation', array($frob_params, &$this));
But that's practically the same thing!

Yes, but PHP. In particular, PHP's handling of arrays and references allows this to sneak through. Arrays are always copied by value, but copying the value of a reference in an array returns a new reference to the same thing:

$a = "string a";
$b = "string b";
$first_array = array($a, &$b);
$second_array = $first_array;

var_dump($first_array);
var_dump($second_array);

/*
array(2) {
  [0]=>
  string(8) "string a"
  [1]=>
  &string(8) "string b"
}
array(2) {
  [0]=>
  string(8) "string a"
  [1]=>
  &string(8) "string b"
}
*/

$second_array[0] = "new string 0";
$second_array[1] = "new string 1";

var_dump($first_array);
/*
array(2) {
  [0]=>
  string(8) "string a"
  [1]=>
  &string(12) "new string 1"  # Changed by reference
}
*/

var_dump($second_array);
/*
array(2) {
  [0]=>
  string(12) "new string 0"
  [1]=>
  &string(12) "new string 1"
}
*/

And therefore, allows you to pass in references to functions that couldn't otherwise handle them:


$a = "string a";
$b = "string b";
$outer_array = array($a, &$b);

function reassign($somearr) {
	$somearr[0] = "new string 0";
	$somearr[1] = "new string 1";
}

reassign($outer_array);

var_dump($outer_array);
/*
array(2) {
  [0]=>
  string(8) "string a"       # unchanged
  [1]=>
  &string(12) "new string 1" # (reference) changed
}
*/

var_dump($a);
var_dump($b);
/*
($a)
string(8) "string a"         # $a is unchanged...
($b)
string(12) "new string 1"    # $b is changed
*/

This technique is useful - but overuse can easily lead to unreadable, unmaintainable code. Use it with care, and document its use; the you of tomorrow will thank you.

This post was brought to you by the numbers 1.4 and 5.4, and by the letter RAMP (where we ran into this as part of the 1.4 release).

EDIT:

Why not do_action('before_frobulation', $frob_params, $this);?
@nacin

Good point. While (most of) the technical basis for this post is still sound, the justification really isn't - particularly since there's nothing preventing a mix of functions, some of which have a pass-by-reference signature and some of which do not, when some function might want to write to one of its arguments. (The advisability of this feature is really a different discussion.)

1. PHP doesn't have references as one might expect; rather, it has symbol table aliases. They behave in sometimes unexpected ways. 2. PHP 5.3, 52%; PHP 5.2, 35%; PHP 5.4, 10% - Percentages via w3techs.com, as of December 2013

Sorry, we couldn't find any entries.