Fear And Surprise: Improving A Widespread WordPress Pattern

A very common pattern used in WordPress core, plugins and themes is way of dealing with supplying arguments to functions and merging them with default values. This pattern is documented in the codex here and utilizes wp_parse_args() and extract().

What does it do? Well a look at the actual wp_parse_args function shows us that it’s a glorified version of array_merge. What array_merge does is combine two arrays into one, overwriting duplicate keys supplied by the first array. That’s exactly what you’d want right? You see if your function requires a value and none is set by the user, a default would be used. Merging the defaults with the user supplied arguments ensures that you always have your required values set.

As a matter of convenience extract() is often called straight after wp_parse_args(). Extract will take all the variables in an array and import them as individually named local variables based on the array key value. An example:

$my_array_of_settings = array( "my_setting" => "wonderful" );
extract($my_array_of_settings);
echo $my_setting; // prints "wonderful"

The problem with extract() is that it is also a potential vector for insecure behaviors since it will try to import every value that is contained in the supplied array. It also can result in unpredictable results. That’s not a good thing when you are not in full control of how arguments are passed to your functions (which is certainly the case for distributed plugins and themes). The alternative, and most advised practice is to avoid the use of extract altogether and just use other ways of accessing the values.

I’m not one to throw the baby out with the bathwater and what I really wanted was a stricter version of wp_parse_args(), one that will limit the variable set based on those defined in the defaults. Instead of importing all the variables in a supplied array, it will only import those that are also specified in the array of defaults. (This behaviour is actually what I was intuitively expecting from wp_parse_args in the first place.)

function custom_parse_args( $args, $defaults ){
	if ( is_object( $args ) ) {
		$r = get_object_vars( $args );
	} elseif ( is_array( $args ) ) {
		$r =& $args;
	} else {
		wp_parse_str( $args, $r );
	}

	if( is_array( $defaults ) ) {
		foreach( $defaults as $default_key => $default_var ) {
			$vars[ $default_key ] = ( $r[ $default_key ] ) ? $r[ $default_key ] : $default_var;
		}
	}
	return $vars;
}

So what does this do differently? Let’s compare this function with wp_parse_args. Let’s have an array with defaults and an array with our arguments, some of which the function is not expecting.

$defaults = array ( "title" => "parsing arguments test");
$args = array(
	"title" => "I was not expecting some kind of Spanish Inquisition",
	"unexpected_response" => "Nobody expects the Spanish Inquisition",
	"chief_weapon" => "fear and surprise",
	"three_weapons" => "fear, surprise and a ruthless efficiency",
	"ah_four_weapons" => "fear, surprise, a ruthless efficiency and an almost fanatical devotion to the pope",
);

$wp_parse_test = wp_parse_args( $args, $defaults );
$custom_parse_test = custom_parse_args( $args, $defaults );

If we now inspect the resulting variables, you’ll find that the wp_parse_args method leaves us with:

// var_dump( $wp_parse_test ); outputs:
array(5) {
  ["title"]=>
  string(52) "I was not expecting some kind of Spanish Inquisition"
  ["unexpected_response"]=>
  string(38) "Nobody expects the Spanish Inquisition"
  ["chief_weapon"]=>
  string(17) "fear and surprise"
  ["three_weapons"]=>
  string(40) "fear, surprise and a ruthless efficiency"
  ["ah_four_weapons"]=>
  string(82) "fear, surprise, a ruthless efficiency and an almost fanatical devotion to the pope"
}

But our custom function filters out the unwanted variables automatically:

// var_dump( $custom_parse_test ); outputs:
array(1) {
  ["title"]=>
  string(52) "I was not expecting some kind of Spanish Inquisition"
}

Because we better control what variables can be used inside the function, using extract() becomes a whole lot safer.

For even more control, you could even run data sanitization and validation checks on each variable before they are allowed to overwrite a default variable.

Leave a Reply

Your email address will not be published. Required fields are marked *