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:
- Tentar recuperar o post usando a classe WP_Query4
- Se encontrarmos o post, iremos atualizá-lo
- 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é
- REpresentational State Transfer ↩︎
- Application Programming Interface ↩︎
- The ‘Basic’ HTTP Authentication Scheme ↩︎
- https://developer.wordpress.org/reference/classes/wp_query/ ↩︎
- https://developer.wordpress.org/reference/functions/wp_insert_post/ ↩︎
- https://developer.wordpress.org/reference/functions/sanitize_text_field/ ↩︎
- https://developer.wordpress.org/reference/functions/wp_kses_post/ ↩︎
- https://developer.wordpress.org/reference/functions/wp_parse_args/ ↩︎
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Status ↩︎
- https://developer.wordpress.org/reference/classes/wp_rest_response/ ↩︎
- https://developer.wordpress.org/reference/classes/wp_error/ ↩︎