Fixing a PHP illegal string offset warning with the default_{$meta_type}_metadata filter

A while back I noticed some warnings appearing on my development site for my Bulk Attachment Download plugin, specifically the Illegal string offset PHP warning, and it seemed to be linked to the retrieval of user meta using get_user_meta().

Long story short – the problem was a change in PHP 7.1. As stated in the PHP documentation on modifying arrays:

As of PHP 7.1.0, applying the empty index operator on a string throws a fatal error. Formerly, the string was silently converted to an array.

In other words, the code below used to work fine, but now it throws a warning.

$some_var = '';
$some_var['a_key'] = $value;

I was running into trouble because of the way I was using user_meta to store an array of admin notices that needed to be displayed to a particular user. My code did something like this:

// Function to add a notice to a user.
function add_notice_to_user( $user_id, $notice_to_add_id, $notice_message_to_add ) {
    $notices = array();
    $notices[ $user_id ] = get_user_meta( $user_id, 'jabd_admin_notices', true );
    $notices[ $user_id ][ $notice_to_add_id] = $notice_message_to_add ;
    update_user_meta( $user_id, 'jabd_admin_notices', $notices );
}

// Example of adding a notice to a user.
$user_id = 57;
add_notice_to_user( $user_id, 'a_specific_notice_id', 'A most helpful and interesting notice for our user...' );

See the problem? The last piece of the puzzle is to remember the syntax for get_user_meta():

get_user_meta( int $user_id, string $key = '', bool $single = false )

The $single parameter is the important one here, because as stated in the codex, the function returns:

An array if $single is false. The value of meta data field if $single is true. False for an invalid $user_id.

And according to the More Information section:

Please note that if the meta value exists but is empty, it will return an empty string (or array) as if the meta value didn’t exist.

So now we can see the problem – I have $single set to true, meaning that when no metadata exists an empty string is being returned, not an empty array. Comments added in the code to explain:

// Function to add a notice to a user.
function add_notice_to_user( $user_id, $notice_to_add_id, $notice_message_to_add ) {
    $notices = array();
    
    // This line is fine, but note that if no notices have been set,
    // then $notices[ $user_id ] is set to an empty string...
    $notices[ $user_id ] = get_user_meta( $user_id, 'jabd_admin_notices', true );
    
    // ...and now I'm trying to give that string a key and value as
    // if it's an array. Cue the PHP warning.
    $notices[ $user_id ][ $notice_to_add_id] = $notice_message_to_add ;

    update_user_meta( $user_id, 'jabd_admin_notices', $notices );
}

“Aha”, I thought, when I realized this. “No problem, I’ll just change the $single parameter in get_user_meta() to false. The codex says that’ll give me an array instead. Job done!” But no. True, no more warnings of illegal string offset, but all sorts of other stuff going wrong. When calling get_user_meta() my code was expecting an array of notices in this form:

Array (
    ['zip-file-size-warning'] => 'Your zip file exceeds the maximum file size for download.'
    ['no-ziparchive'] => 'Your download could not be created. It looks like ZipArchive is not installed on your server.'
)

But instead, the array of notices was coming out as:

Array
(
    [0] => Array
        (
            ['zip-file-size-warning'] => 'Your zip file exceeds the maximum file size for download.',
            ['no-ziparchive'] => 'Your download could not be created. It looks like ZipArchive is not installed on your server.'
        )
)

I hadn’t read the codex carefully enough. The info on the $key parameter read as follows:

(string) (Optional) The meta key to retrieve. By default, returns data for all keys.

Default value: ”

In other words, if a meta key is not specified, get_user_meta() returns all meta values for that user, and of course that means an array. Setting $single to false has the same effect – even though my data was already stored as an array, it was being added to a new array as its first value.

The answer was to keep $single as true and run a check each time get_user_meta() was being called. If the returned value was an empty string, it needed to be converted to an empty array. Now, I could have gone through the code, found all the instances of get_user_meta() and added the check in situ. And in fact that’s probably the best approach and something I will do in the next version of the plugin. But as a quick fix, I used the default_{$meta_type}_metadata filter instead.

The function get_metadata_default() is called when get_user_meta() doesn’t find an entry for the requested key and user combination – returning, as we now know, an empty string when $single is true and an empty array otherwise. However this return value can be filtered with default_{$meta_type}_metadata, so my quick fix ended up looking something like this:

// Filter usermeta so that a null result is returned as an empty array instead of a string.
function convert_empty_usermeta_string_to_array( $value, $object_id, $meta_key, $single, $meta_type ) {
	if ( '' === $value ) {
		if ( in_array( $meta_key, array( 'jabd_admin_notices', 'jabd_opt_out_notice_dismissals' ) ) ) {
			$value = array();
		}
	}
	return $value;
}
add_filter( 'default_user_metadata', 'convert_empty_usermeta_string_to_array', 10, 5 );

And that was that. Like I say, probably not the best approach because now any time get_user_meta() returns a null result – whether that be by any other plugin or WP Core, there’s a bit if extra processing being done. But it’s a quick fix that works till I go through my code and add in the conversion from empty string to empty array only where it’s necessary.

Leave a Reply

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