Expondo uma função WordPress via REST API

A REST1 API2 do WordPress foi lançada na versão 4.7.0 e com isso é possível enviar e receber dados dos endpoints (URLs) para recuperar, modificar e criar o conteúdo do seu site.

Por exemplo, podemos consultar o conteúdo de um post com ID 1 enviando uma requisição HTTP GET para o endpoint /wp/v2/posts/1:

GET https://my-site.test/wp-json/wp/v2/posts/1

Apesar do WordPress oferecer endpoints para posts, páginas, taxonomias e outros tipos de dados, em algumas situações esses endpoints padrões não vão atender o que precisa ser feito e/ou você precisará realizar várias requisições para atender um objetivo específico. Dessa forma, o WordPress oferece meios de estender a REST API.

Para criar um novo endpoint, você precisa registrá-lo na action rest_api_init e chamar a função register_rest_route. No exemplo abaixo, estamos inserindo o endpoint my-namespace/v1/insert_or_update e a função my_custom_insert_or_update para responder quando uma requisição HTTP POST for enviada para esse endpoint.

add_action(
	'rest_api_init',
	function () {
		register_rest_route(
			'my-namespace/v1',
			'/insert_or_update',
			array(
				'methods'  => 'POST',
				'callback' => 'my_custom_insert_or_update',
			)
		);
	}
);

function my_custom_insert_or_update() {
	return 'it is working';
}

Dessa forma, ao enviar a requisição abaixo o retorno será it is working.

POST https://blog.test/wp-json/my-namespace/v1/insert_or_update

Suponha que você precise inserir ou atualizar um post com base em um meta dado armazenado na chave my_custom_meta_key. Isto é, se você encontrar um post com o meta dado, o post será atualizado. Caso contrário, você irá inserir um novo post.

Diferente da versão inicial da função my_custom_insert_or_update onde apenas retornamos uma string, no cenário apresentado iremos inserir ou alterar um dado dentro do nosso site com base em uma requisição HTTP enviada. Devemos atualizar o nosso endpoint para processar apenas requisições autenticadas. Para isso iremos passar o parâmetro permission_callback na função register_rest_route. Para simplificar iremos apenas verificar se o usuário tem permissão para editar os posts de outros usuários.

add_action(
	'rest_api_init',
	function () {
		register_rest_route(
			'my-namespace/v1',
			'/insert_or_update',
			array(
				'methods'             => 'POST',
				'callback'            => 'my_custom_insert_or_update',
				'permission_callback' => fn() => current_user_can( 'edit_others_posts' ),
			)
		);
	}
);

Depois de inserir o parâmetro permission_callback, a requisição HTTP POST para o nosso novo endpoint passa a retornar uma mensagem dizendo que não temos autorização.

POST https://blog.test/wp-json/my-namespace/v1/insert_or_update

### return

{
  "code": "rest_forbidden",
  "message": "Sorry, you are not allowed to do that.",
  "data": {
    "status": 401
  }
}

Precisamos enviar uma requisição autenticada. Para isso vamos criar uma senha de aplicação (disponível a partir da versão 5.6) para o nosso usuário no WordPress. A senha de aplicação é criada na página de edição do usuário (wp-admin -> Users -> Edit User).

A requisição passa a ter o cabeçalho Authorization (usando Basic Auth3).

POST https://blog.test/wp-json/my-namespace/v1/insert_or_update
Authorization: Basic admin:AgPJ CdUB IIGH iKAP 6ot7 8syu

### Ou codificado em base64

POST https://blog.test/wp-json/my-namespace/v1/insert_or_update
Authorization: Basic YWRtaW46QWdQSiBDZFVCIElJR0ggaUtBUCA2b3Q3IDhzeXU=

Agora que o endpoint aceita apenas requisições autenticadas, podemos nos concentrar em desenvolver a lógica para inserir ou atualizar um post com base em um meta dado armazenado na chave my_custom_meta_key.

Para fazer isso podemos seguir os seguintes passos:

  1. Tentar recuperar o post usando a classe WP_Query4
  2. Se encontrarmos o post, iremos atualizá-lo
  3. Se não encontrarmos o post, iremos inserir um novo

Em ambos os casos (inserir ou atualizar), iremos utilizar a função wp_insert_post5. Dessa forma, essa função será exposta via REST API no endpoint que criamos. Na requisição HTTP, iremos enviar os mesmos dados esperado pelo parâmetro $postarr.

Vamos implementar o primeiro passo que é tentar recuperar o post usando a classe WP_Query com base em um meta dado armazendo na chave my_custom_meta_key.

Em todas as requisições ao nosso endpoint, os argumentos utilizados para tentar recuperar o post serão os mesmos. O que vai mudar é o value (dentro de meta_query). Podemos ler esses argumentos da seguinte forma: queremos achar 1 post (posts_per_page) que seja uma página ou post (post_type) e que tenha o meta dado especificado na chave meta_query. Se encontrarmos esse post, apenas o ID será retornado (fields).

$query = new WP_Query(
	array(
		'fields'                 => 'ids',
		'post_type'              => array( 'post', 'page' ),
		'posts_per_page'         => 1,
		'no_found_rows'          => true,
		'update_post_meta_cache' => false,
		'update_post_term_cache' => false,
		'meta_query'             => array(
			array(
				'key'   => 'my_custom_meta_key',
				'value' => 'meta_value',
			),
		),
	)
);

Precisamos informar no nosso endpoint que esperamos o argumento my_custom_meta_value na requisição HTTP. Para isso, no momento que chamamos a função register_rest_route vamos passar mais um parâmetro chamado args.

add_action(
	'rest_api_init',
	function () {
		$args = array(
			'my_custom_meta_value' => array(
				'description'       => 'My custom meta value to search for',
				'type'              => 'string',
				'required'          => true,
				'validate_callback' => fn( $param ) => is_string( $param ),
				'sanitize_callback' => 'sanitize_text_field',
			),
		);

		register_rest_route(
			'my-namespace/v1',
			'/insert_or_update',
			array(
				'methods'             => 'POST',
				'callback'            => 'my_custom_insert_or_update',
				'permission_callback' => fn() => current_user_can( 'edit_others_posts' ),
				'args'                => $args,
			)
		);
	}
);

Note que adicionamos uma descrição, o tipo, se é um campo obrigatório e uma função para validar o argumento e outra função para sanitizar (remover caracteres que podem ser potencialmente nocivos ao sistema) o valor recebido. Na validação (validate_callback) eu apenas verifico se o valor passado é do tipo string e na sanitização eu uso uma função nativa do WordPress sanitize_text_field6.

Vamos atualizar a função my_custom_insert_or_update para usar o parâmetro my_custom_meta_value na hora de tentar recuperar o post usando a classe WP_Query. Além disso, vamos adicionar a variável $post_id para armazenar o id do post encontrado ou zero (caso não seja encontrado). Como não inserimos nenhum post ainda, o valor será zero.

function my_custom_insert_or_update( $request ) {
	$my_custom_meta_value = $request['my_custom_meta_value'];

	$query = new WP_Query(
		array(
			'fields'                 => 'ids',
			'post_type'              => array( 'post', 'page' ),
			'posts_per_page'         => 1,
			'no_found_rows'          => true,
			'update_post_meta_cache' => false,
			'update_post_term_cache' => false,
			'meta_query'             => array(
				array(
					'key'   => 'my_custom_meta_key',
					'value' => $my_custom_meta_value,
				),
			),
		)
	);
	
	$post_id = $query->posts[0] ?? 0;
}

Para enfim chamar a funçao wp_insert_post precisamos apenas passar o parâmetro $postarr com os dados do post (título, conteúdo, tipo de post, etc). Isso será feito passando mais um argumento na requisição HTTP. Vamos então inserir esse novo argumento na variável $args.

add_action(
	'rest_api_init',
	function () {
		$args = array(
			'my_custom_meta_value' => array( ... ),
			// postarr arg
			'postarr'              => array(
				'description'       => 'The post data',
				'type'              => 'array',
				'required'          => true,
				'validate_callback' => fn( $param ) => is_array( $param ),
				'sanitize_callback' => function( $param ) {
					foreach ( $param as $key => $value ) {
						if ( 'post_content' === $key ) {
							$param[ $key ] = wp_kses_post( $value );
							continue;
						}

						$param[ $key ] = sanitize_text_field( $value );
					}

					return $param;
				},
			),
		);

		register_rest_route( ... );
	}
);

Note que na validação do parâmetro é apenas verificado se o valor é um array. Já na sanitização do valor, a chave post_content` tem um tratamento especial pois queremos preservar tags HTML que podem ser inseridas no conteúdo de um post e por isso usamos a função wp_kses_post7.

Vamos adicionar a variável $postarr na função my_custom_insert_or_update para armazenar os dados enviados na requisição. A variável $default_data também foi inserida com alguns valores padrão. Isso significa que podemos omitir por exemplo a chave post_status na requisição HTTP e que o valor padrão será “publish”. Usamos a função wp_parse_args8 para mesclar os valores vindos da requisição HTTP com os valores padrão definidos na variável $default_data.

Por fim, adicionamos o meta value que é enviado na requisição. Isso é importante pois cria um vínculo entre o novo post que será criado e os dados enviados. Por exemplo, se enviarmos a mesma requisição com o título diferente, o post será atualizado ao invés de inserido. Imagine que você esteja importando posts de um outro sistema para o WordPress, mas os usuários continuam inserindo dados enquanto a virada de sistemas não acontece. Provavelmente você irá rodar a importação mais de uma vez para inserir os novos posts e atualizar os que já foram inseridos.

function my_custom_insert_or_update( $request ) {
	. . .
	
	$post_id = $query->posts[0] ?? 0;
	
	$default_data = array(
		'ID'          => $post_id,
		'post_status' => 'publish',
		'post_type'   => 'post',
	);

	$postarr = wp_parse_args(
		$request['postarr'],
		$default_data
	);

	$postarr['meta_input']['my_custom_meta_key'] = $my_custom_meta_value;
}

Agora precisamos chamar a função wp_insert_post passando a variável $postarr. Além disso, é preciso retornar um status HTTP9 indicando se a requisição HTTP foi finalizada com sucesso (HTTP 200) ou não (HTTP 500).

Para indicar que a requisição foi finalizada com sucesso iremos retornar uma instância da classe WP_REST_Response10 com o ID do post inserido/atualizado. Já para indicar um erro, iremos retornar o objeto WP_Error11 que é retornado pela função wp_insert_post em caso de erro. O WordPress converte um objeto WP_Error em um WP_REST_Response com o status 500.

function my_custom_insert_or_update( $request ) {
	. . .
	
	$result = wp_insert_post( $postarr, true );

	if ( is_wp_error( $result ) ) {
		return $result;
	}

	return new WP_REST_Response( $result );
}

Vamos testar os seguintes cenários do nosso endpoint:

  • Inserindo um novo post
  • Atualizando um post existente
  • Lidando com um erro ao tentar inserir/atualizar um post

Para testar a inserção de um novo post, podemos enviar a seguinte requisição HTTP:

POST https://blog.test/wp-json/my-namespace/v1/insert_or_update
Authorization: Basic YWRtaW46QWdQSiBDZFVCIElJR0ggaUtBUCA2b3Q3IDhzeXU=
Content-Type: application/json

{
	"my_custom_meta_value": "test_value",
	"postarr": {
		"post_title": "Lorem ipsum",
		"post_content": "<p>Ut enim ad minim veniam, quis nostrud consequat.</p>"
	}
}

// Return
HTTP/1.1 200 OK

213 // post ID

Para verificar se o post realmente foi inserido podemos enviar uma requisição para o endpoint GET /wp/v2/posts/<id>

GET https://blog.test/wp-json/wp/v2/posts/213?_fields=id,title,content

// Return
{
  "id": 213,
  "title": {
    "rendered": "Lorem ipsum"
  },
  "content": {
    "rendered": "<p>Ut enim ad minim veniam, quis nostrud consequat.<\/p>\n",
    "protected": false
  }
}

Para testar a atualização de um post existente, iremos enviar a mesma requisição que usamos para inserir mas com o valor da chave “title” diferente. Lembre-se que o valor armazenado no meta dado my_custom_meta_key que é utilizado como referência para sabermos se um post já foi inserido ou não.

POST https://blog.test/wp-json/my-namespace/v1/insert_or_update
Authorization: Basic YWRtaW46QWdQSiBDZFVCIElJR0ggaUtBUCA2b3Q3IDhzeXU=
Content-Type: application/json

{
	"my_custom_meta_value": "test_value",
	"postarr": {
		"post_title": "Excepteur sint",
		"post_content": "<p>Ut enim ad minim veniam, quis nostrud consequat.</p>"
	}
}

// Return
HTTP/1.1 200 OK

213 // post ID

Verificamos se o post foi atualizado enviando uma requisição para o endpoint GET /wp/v2/posts/<id>

GET https://blog.test/wp-json/wp/v2/posts/213?_fields=id,title

// Return
{
  "id": 213,
  "title": {
    "rendered": "Excepteur sint"
  }
}

Por fim, para testar como nosso endpoint lida com um erro ao inserir/atualizar um post, vamos enviar uma requisição passando um ID que não existe para ser atualizado. Isso faz com que a função wp_insert_post retorne um erro.

POST https://blog.test/wp-json/my-namespace/v1/insert_or_update
Authorization: Basic YWRtaW46QWdQSiBDZFVCIElJR0ggaUtBUCA2b3Q3IDhzeXU=
Content-Type: application/json

{
	"my_custom_meta_value": "test_value",
	"postarr": {
		"ID": "12345",
		"post_title": "Excepteur sint",
		"post_content": "<p>Ut enim ad minim veniam, quis nostrud consequat.</p>"
	}
}

// Return
HTTP/1.1 500 Internal Server Error

{
  "code": "invalid_post",
  "message": "Invalid post ID.",
  "data": null
}

Transformando em um plugin

O código criado nesse post pode ser usado como um plugin. Para isso é preciso copiar o código abaixo no arquivo wp-content/plugins/my-custom-insert-or-update-post-endpoint/my-custom-insert-or-update-post-endpoint.php. Lembre-se de ativar o plugin.

<?php
/**
 * Plugin Name:       My custom insert or update post endpoint.
 * Description:       Add the `/my-namespace/v1/insert_or_update` endpoint to the WP REST API.
 * Version:           1.0.0
 * Requires at least: 4.7.0
 * Requires PHP:      7.4
 * Author:            Ramon Ahnert
 * Author URI:        https://nomar.dev/
 * License:           GPL v2 or later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 */

namespace My_Custom_Insert_Update_Endpoint;

add_action(
	'rest_api_init',
	function () {
		$args = array(
			'my_custom_meta_value' => array(
				'description'       => 'My custom meta value to search for',
				'type'              => 'string',
				'required'          => true,
				'validate_callback' => fn( $param ) => is_string( $param ),
				'sanitize_callback' => 'sanitize_text_field',
			),
			'postarr'              => array(
				'description'       => 'The post data',
				'type'              => 'array',
				'required'          => true,
				'validate_callback' => fn( $param ) => is_array( $param ),
				'sanitize_callback' => function( $param ) {
					foreach ( $param as $key => $value ) {
						if ( 'post_content' === $key ) {
							$param[ $key ] = wp_kses_post( $value );
							continue;
						}

						$param[ $key ] = sanitize_text_field( $value );
					}

					return $param;
				},
			),
		);

		register_rest_route(
			'my-namespace/v1',
			'/insert_or_update',
			array(
				'methods'             => 'POST',
				'callback'            => 'my_custom_insert_or_update',
				'permission_callback' => fn() => current_user_can( 'edit_others_posts' ),
				'args'                => $args,
			)
		);
	}
);

/**
 * Handle `/my-namespace/v1/insert_or_update` endpoint.
 *
 * @param WP_REST_Request $request Full data about the request.
 * @return WP_REST_Response|WP_Error
 */
function my_custom_insert_or_update( $request ) {
	$my_custom_meta_value = $request['my_custom_meta_value'];

	$query = new WP_Query(
		array(
			'fields'                 => 'ids',
			'post_type'              => array( 'post', 'page' ),
			'posts_per_page'         => 1,
			'no_found_rows'          => true,
			'update_post_meta_cache' => false,
			'update_post_term_cache' => false,
			'meta_query'             => array(
				array(
					'key'   => 'my_custom_meta_key',
					'value' => $my_custom_meta_value,
				),
			),
		)
	);

	$post_id = $query->posts[0] ?? 0;

	$default_data = array(
		'ID'          => $post_id,
		'post_status' => 'publish',
		'post_type'   => 'post',
	);

	$postarr = wp_parse_args(
		$request['postarr'],
		$default_data
	);

	$postarr['meta_input']['my_custom_meta_key'] = $my_custom_meta_value;

	$result = wp_insert_post( $postarr, true );

	if ( is_wp_error( $result ) ) {
		return $result;
	}

	return new WP_REST_Response( $result );
}

Notas de rodapé

  1. REpresentational State Transfer ↩︎
  2. Application Programming Interface ↩︎
  3. The ‘Basic’ HTTP Authentication Scheme ↩︎
  4. https://developer.wordpress.org/reference/classes/wp_query/ ↩︎
  5. https://developer.wordpress.org/reference/functions/wp_insert_post/ ↩︎
  6. https://developer.wordpress.org/reference/functions/sanitize_text_field/ ↩︎
  7. https://developer.wordpress.org/reference/functions/wp_kses_post/ ↩︎
  8. https://developer.wordpress.org/reference/functions/wp_parse_args/ ↩︎
  9. https://developer.mozilla.org/en-US/docs/Web/HTTP/Status ↩︎
  10. https://developer.wordpress.org/reference/classes/wp_rest_response/ ↩︎
  11. https://developer.wordpress.org/reference/classes/wp_error/ ↩︎