WordPress.org

Make WordPress Core

Opened 3 months ago

Last modified 8 weeks ago

#43392 new enhancement

Support associative array type in register_meta()

Reported by: diegoliv Owned by:
Milestone: Awaiting Review Priority: normal
Severity: normal Version: 4.9.4
Component: Options, Meta APIs Keywords: close
Focuses: rest-api Cc:

Description

Currently, register_meta() is used to expose meta data by setting show_in_rest => true. If you want to work with custom metadata on Gutenberg (by setting an attribute with source: 'meta'), you need to use register_meta(). Now, this function also has the parameter type, where you set the metadata object type. Currently, allowed types are simple things like integer or string.

While I was working with Gutenberg, I felt the need to create nested attributes (that would translate into JSON objects). Here is an example:

attributes: {
    navigationStyles: {
        type: 'object',
	default: {
	    fontSize: {
                type: 'number',
                default: 10,
            },
	    fontColor: {
                type: 'string',
                default: '#ccc',
            },
	},
        source: 'meta',
        meta: 'navigation_styles',
    },
},

Now, when Gutenberg tries to save this attribute on the database, the attribute will be a JSON object, which will translate into an associative array to be handled in PHP. But, currently, there's no support for registering a metadata with register_meta() that will be an associative array:

<?php
function blocks_meta_init(){
        register_meta('post', 'navigation_styles', array(
                'show_in_rest' => true,
                'type' => 'array', // not possible at the moment
        ));
}

add_action('init', 'blocks_blocks_meta_init');

If you try to do that, the REST API will fail to save the metadata. My current workaround for this use case is to just register the meta as a string, and handle the formatting of the metadata using sanitize_callback:

<?php
function blocks_meta_init(){
        register_meta('post', 'navigation_styles', array(
                'show_in_rest' => true,
                'type' => 'string',
                'sanitize_callback' => 'blocks_sanitize_attr'
        ));
}

add_action('init', 'blocks_blocks_meta_init');

function blocks_sanitize_attr( $meta_value, $meta_key, $meta_type ){
        return serialize( json_decode( $meta_value ) );
}

This works, but has several limitations:

  • You need to turn your javascript object into a string (JSON.stringify)if you want to store it on the database.
  • You need to handle sanitization yourself
  • If you get the metadata using get_post_meta(), it won't apply maybe_unserialize() before returning the metadata, you need to do it yourself.

I propose that we support the 'type' => 'array' with register_meta(). This way, we'll have a proper way to store arrays or JSON objects as associative arrays. This would make the REST API more flexible, and project Gutenberg would also benefit by making attributes more flexible than they currently are.

Change History (5)

#1 @diegoliv
3 months ago

  • Component changed from General to Options, Meta APIs

#2 @azaozz
3 months ago

  • Keywords close added

Hi @diegoliv thanks for the suggestions.

Imho this works quite well as it is now.

You need to turn your javascript object into a string (JSON.stringify) if you want to store it on the database.

Correct, or you can use another format like CSV if it makes more sense for your particular case.

You need to handle sanitization yourself

Correct, that's the best way to do it. You know what data to expect and when it would be "out of bounds".

If you get the metadata using get_post_meta(), it won't apply maybe_unserialize() before returning the metadata, you need to do it yourself.

maybe_unserialize() is a terrible way to do things. It only exists because of back compat. The less it's used -- the better.

Storing the data in a string format of your choice is the proper way here simply because you know what format to expect and how to sanitize it. I wish we could do that for all of WP :)

Last edited 3 months ago by azaozz (previous) (diff)

#3 follow-up: @diegoliv
3 months ago

Hey @azaozz thank you for the response! I think I agree with you regarding maybe_unserialize() and about handling sanitization yourself if you're returning a more complex object. But what about a simple JSON object? a JSON object is not the same thing as a javascript object. A JS object might have simple properties or functions as properties. This would not be a good object to store in the database, of course. But a plain JSON object is really equivalent as an associative array in PHP. It's just a collection of simple properties like 'key' : 'value'. Returning this kind of object through Ajax, you end up with an associative array anyways (inside the $_POST or $_REQUEST globals).

We can easily save associative arrays already with update_post_meta(). Just throw an array as the value and WP automatically saves it into a proper format (a serialized string) on the database. And using get_post_meta(), your serialized array is automatically converted back into an array. Is there any strong reason not to allow this same behavior for meta data updated through the REST API?

#4 in reply to: ↑ 3 @azaozz
3 months ago

Replying to diegoliv:

We can easily save associative arrays already with update_post_meta(). Just throw an array as the value and WP automatically saves it into a proper format (a serialized string) on the database. And using get_post_meta(), your serialized array is automatically converted back into an array.

This is the exact bit that uses maybe_unserialize().

Is there any strong reason not to allow this same behavior for meta data updated through the REST API?

Well, if we want to avoid stuff like maybe_json_decode() :)

I wish there was an easy way to specify the format of an encoded string in the meta table, but there isn't. The only other option is to "guess" the encoding, which is..... subpar.

#5 @ajfleming
8 weeks ago

This post by Ryan McCue; The (Complex) State of Meta in the WordPress REST API explains why complex values & serialized data are not exposed by register_meta().

The key reason for this is that accepting serialized data is either lossy or unsafe.

Note: See TracTickets for help on using tickets.