WordPress is going through somewhat a transition lately with JSON REST API increasingly becoming a thing. And a good thing at that, because it’s finally catching up with the rest of data-driven, JSON-powered world.
What is JSON
If you’re unfamiliar with JSON (abbreviation for JavaScript Object Notation), simply put it’s a standard format in which we represent data, or more importantly, a representation that many devices, languages and protocols understand. Let’s say, for example, that you have a person that has a number of attributes. The JSON object of that person would look something like this:
{
"first-name": "Tomaz",
"last-name": "Zaman",
"age": 32,
"address": {
"country": "Slovenia"
},
"hobbies": [
"skydiving",
"movies"
]
}
In many ways JSON became the successor of the XML, primarily because it’s simpler and faster to write and read.
No plugins please
Unfortunately, without the WP-REST-API plugin, WordPress doesn’t handle JSON data well, or at all. That’s because all forms submit data type of x-www-form-urlencoded
which means when you POST something (like submitting a new article), the data gets escaped and encoded in the URL. This is actually the default behaviour of how regular HTML forms behave, but since we’re covering importing data in this article we don’t have any forms in the first place, just data.
But we need some URL to call from our external service in order to import data, right? Well, let’s use what WordPress already provides: wp_ajax. Since it’s an internal service, it has no notion of where the ajax endpoint
(the URL to call) is, so we needed to hardcode the URL with the appropriate action, which is usually something like http://example.com/wp-admin/admin-ajax.php?action=your_import_action_name
.
When it comes to tutorials, I think it’s always better to show a real life example so here I’m going to show you how we import Codeable developers from our custom application (built in Ruby on Rails) into WordPress, as a custom post type (called developer
).
Register hooks
In order to leverage the power of WordPress AJAX, we need to create two hooks/actions that will trigger the importing function (put this — and all the code below — in functions.php, or any other file that is included in your theme or plugin):
add_action( 'wp_ajax_import_developer', 'import_developer' );
add_action( 'wp_ajax_nopriv_import_developer', 'import_developer' );
Now we need to define the function that these two actions hook into:
function import_developer() {
$developer_data = json_decode( file_get_contents( ‘php://input’ ) );
if ( compare_keys() ) {
insert_or_update( $developer_data );
}
wp_die();
}
The important parts of this function:
- line 3: the script reads the request body, decodes the format and assigns it to $developer_data
- line 5: security check is performed (more on that later)
- line 6: data is passed to another function that either creates a new developer record or updates an existing one
- line 9: making sure nothing is being processed after we import the developer
The most relevant part is importing the developer, so let’s look at that next.
Inserting or updating a record
Before going any further, it’s worth noting what our JSON representation of a developer looks like (how our custom application sends the data to WordPress):
{
"id": "1",
"full_name": "Tomaz Zaman",
"bio": "Codeable founder",
"tags": [
"CSS",
"HTML",
"PHP",
"WordPress"
]
}
When importing, full_name
becomes the post title, bio
becomes the post content and tags, well, tags 🙂
In order to determine which of the two actions to perform we need to check whether the record already exists and for that we’ll use a custom field developer_id
in our custom post type. Apart from that I’ll also use another custom field to store whole JSON in it, as it is. This may not be very useful to you but we use some of the data just for displaying it (like reviews), so we can do that in our theme, no need for additional fields if some attributes are never going to be modified in WordPress. I’ll name this custom field json
. Feel free to use as much custom fields as needed if your problem requires that though.
Here’s the full code (with explanation below):
function insert_or_update($developer_data) {
if ( ! $developer_data)
return false;
$args = array(
‘meta_query’ => array(
array(
‘key’ => ‘developer_id’,
‘value’ => $developer_data->id
)
),
‘post_type’ => ‘developer’,
‘post_status’ => array(‘publish’, ‘pending’, ‘draft’, ‘auto-draft’, ‘future’, ‘private’, ‘inherit’),
‘posts_per_page’ => 1
);
$developer = get_posts( $args );
$developer_id = ”;
if ( $developer )
$developer_id = $developer[0]->ID;
$developer_post = array(
‘ID’ => $developer_id,
‘post_title’ => $developer_data->full_name,
‘post_content’ => $developer_data->bio,
‘post_type’ => ‘developer’,
‘post_status’ => ( $developer ) ? $developer[0]->post_status : ‘publish’
);
$developer_id = wp_insert_post( $developer_post );
if ( $developer_id ) {
update_post_meta( $developer_id, ‘developer_id’, $developer_data->id );
update_post_meta( $developer_id, ‘json’, addslashes( file_get_contents( ‘php://input’ ) ) );
wp_set_object_terms( $developer_id, $developer_data->tags, ‘developer_tag’ );
}
print_r( $developer_id );
}
Before we do anything, we check that $developer_data
actually contains data (line 3) and then prepare the arguments for our query that will either return one record, or no records at all (lines 6-18). If it does find an existing record, then it assign’s its ID to $developer_id
variable (line 23), which is important for WordPress. When we use wp_insert_post
it decides whether to create a new record or update an existing one based on whether the ID exists or not. In our case we need to prevent duplicates but your mileage may vary so modify the code accordingly.
Once the record is created/updated, we need to update our custom fields (via update_post_meta
). As I mentioned earlier, we save raw JSON data into a field for convenience.
Because we use custom taxonomy for our developers, we also need to update the developer_tag
which also automatically updates or creates tags as necessary.
As the last step, to let our external application know everything went smoothly, we just print out the WordPress ID of the record.
What about security?
As we’re accessing WordPress from outside, we can’t use its default AJAX security measures (wp_nonce), so I had to come up with a custom solution. Truth be told, I didn’t come up with it on my own, I just copied what GitHub does – if it’s good for them, it’s good for us as well.
Here’s the function:
function compare_keys() {
if ( ! isset( $_SERVER[‘HTTP_X_CODEABLE_SIGNATURE’] ) ) {
throw new Exception( “HTTP header ‘X-Codeable-Signature’ is missing.” );
}
list( $algo, $hash ) = explode( ‘=’, $_SERVER[‘HTTP_X_CODEABLE_SIGNATURE’], 2 ) + array( ”, ” );
$raw_post = file_get_contents( ‘php://input’ );
if ( $hash !== hash_hmac( $algo, $raw_post, CODEABLE_KEY ) ) {
throw new Exception( ‘Secret hash does not match.’ );
}
return true;
}
When we import developer data into WordPress, a special header is sent with the request that looks like this: X-Codeable-Signature: sha1=246d2e58593645b1f261b1bbc867fe2a9fc1a682
.
Without complicating it too much, this signature is generated from the request body (our JSON developer object) and signed with a private key (defined both in WordPress as CODEABLE_KEY
and in our custom application) and then this signature is compared with the same signature, this time generated by WordPress.
Conclusion
I understand this tutorial may be a bit more on the advanced side of things, but the truth is, JSON is a widely used and supported format that an increasing number of online services use (yes, Facebook too, which is why you see empty boxes when you log in — your browser is waiting for JSON to be returned behind the scenes) – so I urge you to learn it, it’ll pay dividends in the future, especially when it becomes part of WordPress core. Which it totally will.
To thank you for your time spent on following this tutorial, I prepared the code from above in a gist (packaged into a class, to boot) you’re free to use.
I’d also like to thank Zoran Ugrina and fine folks at Advanced WordPress for helping me with the code.