In un sito WordPress possono esserci funzionalità che fanno richieste ajax alla nostra installazione del CMS. Ho già trattato questo tema nell’articolo Fare richieste ajax in WordPress con wp_ajax_ e wp_ajax_nopriv_.
In questo articolo parliamo ancora di WordPress e ajax ma, questa volta, sfrutteremo le potenti Rest API.
Cosa sono le Rest API
Per capire cosa sono le Rest API analizziamo, anzitutto, il termine.
Con API, applicato a WordPress, intendiamo una serie di variabili, funzioni, classi e quant’altro che ci permetta di effettuare operazioni, anche complesse, senza preoccuparsi di cosa c’è sotto la scocca.
È il caso, ad esempio, della funzione get_posts() che restituisce gli articoli pubblicati (o altri tipi di contenuti se specificati) in un array di oggetti WP_Post. Non dobbiamo scrivere delle query per il database, non ci interessa sapere in che tabella vengono salvati, quali siano i campi, ecc….
Per definire il concetto di Rest prendo a prestito la definizione presente su Wikipedia al momento che scrivo l’articolo, cioè “L’architettura REST si basa su HTTP. Il funzionamento prevede una struttura degli URL ben definita che identifica univocamente una risorsa o un insieme di risorse e l’utilizzo dei metodi HTTP specifici per il recupero di informazioni (GET), per la modifica (POST, PUT, PATCH, DELETE) e per altri scopi (OPTIONS, ecc.).”
Si tratta di uri che non forniscono pagine HTML, bensì restituiscono dati, spesse volte in formato JSON come nel caso di WordPress. Ad esempio, il core di WordPress imposta una di queste url, miosito/wp-json/wp/v2/posts: richiamata in GET restituisce il medesimo risultato di get_posts() ma in formato JSON. Mentre get_posts() può essere usata solo all’interno di un tema o un plugin la url può essere chiamata da qualunque applicazione che supporti le connessioni http e il risultato può essere manipolato da qualunque software scritto in un linguaggio che supporta JSON (praticamente qualsiasi).
Perché usare le Rest API per le richieste ajax
Non è certo necessario conoscere e saper usare le Rest API per fare richieste ajax; tuttavia approfondirle e utilizzarle a tale scopo ha dei vantaggi che vale la pena sfruttare:
- ogni richiesta viene fatta con una uri specifica; in questo modo è molto chiaro cosa otterremo e/o che tipo di operazioni eseguiremo.
- grazie agli endpoint è possibile limitare ogni richiesta al metodo GET o POST in modo semplice, oppure effettuare, con la stessa uri, operazioni differenziate per metodo di chiamata;
- le Rest API prevedono un sistema di gestione dei permessi, per ogni richiesta, che consentono, ad esempio, di limitare l’accesso solo a utenti sottoscrittori, oppure di rendere disponibile l’API solo per chiamate via web e tanto altro ancora;
- Lo stessa API possiamo renderla disponibile anche ad applicazioni esterne; questo può tornare utile, ad esempio, quando una realtà commerciale sviluppa un’app che prende contenuti dal sito.
Alcuni concetti fondamentali
In seguito vediamo un esempio di come funziona una richiesta ajax elaborata tramite Rest API; prima, però, dobbiamo parlare di alcuni concetti chiave: qui non li citerò tutti ma solo quelli che ci interessano per fare una semplice richiesta ajax.
Rotte e endpoint
Abbiamo detto che ogni richesta Rest API viene fatta tramite una uri specifica. Attraverso una rotta (route) possiamo ottenere dei dati oppure compiere specifiche operazioni: ad esempio /wp-json/wp/v2/posts è una rotta.
Ogni richiesta web può essere fatta in GET oppure POST (ci sarebbero anche i metodi PUTCH, OPTIONS e DELETE ma il browser non li prevede). La stessa rotta può essere mappata in GET o in POST: ogni mappatura è un endpoint.
Ad esempio la rotta /wp-json/wp/v2/posts, che fa parte delle Rest API definite nel core di WordPress, ha 2 endpoint:
- uno per il metodo GET che fornisce un JSON di tutti gli articoli pubblicati;
- uno per il metodo POST che consente la creazione di un nuovo articolo a un utente loggato che dispone dei permessi per farlo.
Vediamo come è strutturata una uri Rest API di WordPress, partendo dallo stesso esempio:
- /wp-json/ chiama una Rest API;
- /wp/ è il namespace, in questo caso identifica il core di WordPress. Normalmente si riferisce al tema o al plugin che ha impostato la Rest API;
- /v2/ è la versione della API utilizzata;
- /posts/ è la url di base specifica di questa richiesta.
Come registrare delle rotte Rest API in WordPress
Per creare delle nostre Rest API dobbiamo anzitutto registrarle. Per farlo bisogna chiamare la funzione register_rest_route passandole 3 parametri:
- una stringa formato namespace/versione_restapi, ad esempio mioplugin/v2;
- base url, ossia la parte della rotta dopo namespace/versione_restapi;
- opzioni di endpoint.
Oltre a questi tre argomenti ci sarebbe override, un booleano che consente di sovrascrivere un’eventuale Rest API già esistente per la stessa rotta: personalmente non sono molto a favore di questa pratica perché potrebbe creare conflitti con altri eventuali plugin o con il tema.
Tra le opzioni per l’endpoint citiamo:
- methods, i metodi con cui può essere soddisfatta una richiesta. Da notare che register_rest_route registra un endpoint; se si desidera creare un altro endpoint per la medesima rotta è bene fare un’altra chiamata a register_rest_route.
- permission_callback, una funzione che consente di implementare una logica volta a verificare i permessi per soddisfare la richiesta; ovviamente deve restituire true o false.
- callback, la funzione da eseguire per soddisfare la richiesta, una volta verificati i permessi, . Un valore deve essere ritornato: questo verrà preso da WordPress e codificato in JSON prima di inviarlo al client. Ad, esempio, se la callback restituisce un valore tipo array(‘success’ => true), il client riceverà {“success”:true}.
Esempio di form con richiesta ajax gestita da WordPress con una Rest API
Ora vediamo un caso dove è stato messo in pratica tutto quanto spiegato fin qua.
Dobbiamo preparare un sondaggio di gradimento rivolto ai partecipanti di un evento. Questo form non sarà accessibile a tutti ma solo ai partecipanti, registrati come utenti sottoscrittori nella nostra installazione di WordPress. La Rest API che riceverà le risposte e le registrerà in un’apposita tabella nel database, dovrà verificare che l’invio sia opera di un utente loggato col ruolo di sottoscrittore e non avente ruoli superiori; va da sé che la richiesta non è pubblicamente accessibile.
Per comodità non c’è la validazione dei dati ma, normalmente, andrebbe fatta.
Il form
Cominciamo a vedere l’html del form, che ho inserito tramite shortcode per poter stampare alcuni valori dinamici con PHP. Della creazione di uno shortcode ne ho parlato nell’articolo Come creare uno shortcode personalizzato.
<!-- method is not specified because an ajax request will be done and POST method will be setted by JavaScript. Action attribute value will be useful to provide the Rest API url outputted by PHP --> <form id="satisfaction-form" action="<?php echo esc_url_raw( rest_url() ) .'satisfaction-survey/v2/send/'; ?>" > <div> <p>1. Complessivamente, quanto ti ritieni soddisfatto dell'evento?</p> <ul> <li><input type="radio" name="satisfaction_level" id="satisfaction_level_5" value="5"> <label for="satisfaction_level_5">Molto soddisfatto</label></li> <li><input type="radio" name="satisfaction_level" id="satisfaction_level_4" value="4"> <label for="satisfaction_level_4">Soddisfatto</label></li> <li><input type="radio" name="satisfaction_level" id="satisfaction_level_3" value="3"> <label for="satisfaction_level_3">Né soddisfatto né insoddisfatto</label></li> <li><input type="radio" name="satisfaction_level" id="satisfaction_level_2" value="2"> <label for="satisfaction_level_2">Insoddisfatto</label></li> <li><input type="radio" name="satisfaction_level" id="satisfaction_level_1" value="1"> <label for="satisfaction_level_1">Molto insoddisfatto</label></li> </ul> </div> <div> <p>2. Esprimi il tuo livello di soddisfazione sui seguenti aspetti dell'evento.</p> <table> <tr> <th></th> <th>Molto soddisfatto</th> <th>Soddisfatto</th> <th>Né soddisfatto né insoddisfatto</th> <th>Insoddisfatto</th> <th>Molto insoddisfatto</th> </tr> <tr> <th>Data e ora</th> <td><input type="radio" name="satisfaction_datetime_level" value="5"></td> <td><input type="radio" name="satisfaction_datetime_level" value="4"></td> <td><input type="radio" name="satisfaction_datetime_level" value="3"></td> <td><input type="radio" name="satisfaction_datetime_level" value="2"></td> <td><input type="radio" name="satisfaction_datetime_level" value="1"></td> </tr> <tr> <th>Sede</th> <td><input type="radio" name="satisfaction_location_level" value="5"></td> <td><input type="radio" name="satisfaction_location_level" value="4"></td> <td><input type="radio" name="satisfaction_location_level" value="3"></td> <td><input type="radio" name="satisfaction_location_level" value="2"></td> <td><input type="radio" name="satisfaction_location_level" value="1"></td> </tr> <tr> <th>Sessioni</th> <td><input type="radio" name="satisfaction_sessions_level" value="5"></td> <td><input type="radio" name="satisfaction_sessions_level" value="4"></td> <td><input type="radio" name="satisfaction_sessions_level" value="3"></td> <td><input type="radio" name="satisfaction_sessions_level" value="2"></td> <td><input type="radio" name="satisfaction_sessions_level" value="1"></td> </tr> <tr> <th>Speaker</th> <td><input type="radio" name="satisfaction_speaker_level" value="5"></td> <td><input type="radio" name="satisfaction_speaker_level" value="4"></td> <td><input type="radio" name="satisfaction_speaker_level" value="3"></td> <td><input type="radio" name="satisfaction_speaker_level" value="2"></td> <td><input type="radio" name="satisfaction_speaker_level" value="1"></td> </tr> <tr> <th>Cibo e bevande</th> <td><input type="radio" name="satisfaction_food_beverage_level" value="5"></td> <td><input type="radio" name="satisfaction_food_beverage_level" value="4"></td> <td><input type="radio" name="satisfaction_food_beverage_level" value="3"></td> <td><input type="radio" name="satisfaction_food_beverage_level" value="2"></td> <td><input type="radio" name="satisfaction_food_beverage_level" value="1"></td> </tr> </table> </div> <div> <p>3. È la prima volta che partecipi a uno dei nostri eventi?</p> <ul> <li><input type="radio" name="first_partecipation" id="first_partecipation_yes" value="1"> <label for="first_partecipation_yes">Si</label></li> <li><input type="radio" name="first_partecipation" id="first_partecipation_no" value="0"> <label for="first_partecipation_no">No</label></li> </ul> </div> <div> <p>4. Secondo la tua esperienza a questo evento, quanto è probabile che parteciperai a un altro evento in futuro? (1 Decisamente improbabile, 10 Estremamente probabile)</p> <p> <select name="next_partecipation_probality"> <option value="0">Seleziona</option> <?php //numeric value options from 10 to 1 for($i = 10; $i > 0; $i--){ echo '<option value="' . $i . '">' . $i . '</option>'; } ?> </select> </p> </div> <div> <p>5. Con quale probabilità consiglierai questo evento a un amico o collega?</p> <p> <select name="recommend_probability"> <option value="0">Seleziona</option> <?php //numeric value options from 10 to 1 for($i = 10; $i > 0; $i--){ echo '<option value="' . $i . '">' . $i . '</option>'; } ?> </select> </p> </div> <div> <p>6. Hai qualche suggerimento su come migliorare gli eventi futuri?</p> <div> <textarea name="suggestions"></textarea> </div> </div> <div id="wrap-result"> </div> <div> <!-- the input#wp-rest-nonce has no name because its value will be take by JavaScript which send it as a header --> <input type="hidden" id="wp-rest-nonce" value="<?php echo wp_create_nonce('wp_rest'); ?>"> <button type="submit" id="button-submit" >Invia</button> </div> </form>
Su questo codice segnalo anzitutto che l’attributo action ha come valore una stringa, di cui una parte è stata ottenuta con esc_url_raw(rest_url()): questa restituisce url-del-sito/wp-json/, la url base di tutte le Rest API.
Successivamente, in fondo al form, prima del tasto di invio, c’è un input hidden con id wp-rest-nonce: questo è valorizzato con un codice, creato da WordPress, che dovrà essere inviato come header nella richiesta ajax. È necessario per verificare che l’utente sia loggato e la sua “identità”. Non c’è l’attributo name perché sarà compito di JavaScript trattare questo valore in modo separato da tutti gli altri.
Il codice JavaScript
Ora veniamo al codice JavaScript che si occuperà della richiesta ajax.
(function(){ //form element let form = document.getElementById('satisfaction-form'); //element where message request response is printed let wrap_result = document.getElementById('wrap-result'); //button to submit. It will be removed when request is done and response is successfully let button_submit = document.getElementById('button-submit'); //intercepting form submitting form.onsubmit = function(event){ //avoid the normal browser http request event.preventDefault(); //telling to user answer send is going wrap_result.innerHTML = 'Invio risposte in corso...'; //object of data inserted let formdata = new FormData(form); //ajax request const xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function () { if (this.readyState == 4){ //Verify server status code let serverStatus = this.status; //response is a json object, so it will be parsified let response = JSON.parse(this.responseText); if (serverStatus == 200) { //if answers are correctly sended a human readable message response is printed and button submit is removed to prevent a new send if (response.answers_sent) { wrap_result.innerHTML = 'Risposte inviate correttamente. Grazie per aver partecipato al sondaggio'; button_submit.parentNode.removeChild(button_submit); } //otherwise human readable message response is printed to advise user the send is gone wrong else{ wrap_result.innerHTML = 'Risposte non inviate: riprovare o contattare l’assistenza'; } return; } //the request is not ok else{ //user have no permission to send to partecipate to survey if (serverStatus == 403) { wrap_result.innerHTML = 'Non disponi dei permessi per partecipare al sondaggio.'; } //generic tecnical issues else{ wrap_result.innerHTML = 'Invio non andato a buon fine: riprovare o contattare l’assistenza.'; } } } }; //request is open in post method. The url is defined in form action attribute xhttp.open('POST', form.getAttribute('action') ); //this header is necessary to WordPress to verify user is logged and who is it xhttp.setRequestHeader('X-WP-Nonce', document.getElementById('wp-rest-nonce').value); //request is sent xhttp.send(formdata); }; })();
Non mi dilungherò sulla spiegazione dell’intero codice; mi soffermerò solo su alcunu aspetti.
I codici di risposta che ci si aspetta sono 200 o 403. Il codice 200 sarà restituito se non ci sono problemi coi permessi e se non si verificano problemi tecnici sul server, il 403 se la Rest API non viene eseguita per mancanza di permessi. Si tenga presente che il codice 200 viene restituito anche se l’inserimento nel database non dovesse andare a buon fine: in questo caso bisogna verificare il valore di answer_sent, dell’oggetto di risposta, per conoscerne l’esito.
Subito prima di inviare la richiesta ajax viene inviato un header dal nome X-WP-Nonce; dal momento che le Rest API non utilizzano i cookie dobbiamo settare manualmente un valore che wp_ajax_ e wp_ajax_nopriv_ inviano come cookie. Si tratta di un codice da cui WordPress deduce se chi invia la richiesta è un utente loggato e chi è. Senza questo header la permission_callback della Rest API restituirà false e la richiesta non potrà essere soddisfatta.
Nel backend
Il primo passaggio consiste nella registrazione della Rest API in questo modo
add_action( 'rest_api_init', function(){ register_rest_route( 'satisfaction-survey/v2','/send/', array( 'methods' => ['POST'], 'callback' => 'satisfaction_survey_send', 'permission_callback' => function(){ //a subscriber can read but not edit_posts, so we are verifing current user is a subscriber and not else return current_user_can( 'read' ) && !current_user_can( 'edit_posts' ); } ) ); });
Le Rest API si registrano con register_rest_route all’interno di una funzione agganciata a rest_api_init.
Nel caso di esempio abbiamo impostato solo un endpoint per il metodo POST, la callback è la funzione satisfaction_survey_send e la permission_callback verifica che l’utente possa leggere i post e non editarli: questi sono i permessi di un sottoscrittore. Se l’utente non è loggato current_user_can restituisce false e la permission_callback fa altrettanto di conseguenza. Se la permission_callback restituisce false il server produce il codice di risposta 403.
Ora passiamo alla funzione che esegue la Rest API
function satisfaction_survey_send(){ $user_answers = [ 'satisfaction_level' => ( isset($_POST['satisfaction_level']) ? $_POST['satisfaction_level'] : '' ), 'satisfaction_datetime_level' => ( isset($_POST['satisfaction_datetime_level']) ? $_POST['satisfaction_datetime_level'] : '' ), 'satisfaction_location_level' => ( isset($_POST['satisfaction_location_level']) ? $_POST['satisfaction_location_level'] : '' ), 'satisfaction_sessions_level' => ( isset($_POST['satisfaction_sessions_level']) ? $_POST['satisfaction_sessions_level'] : '' ), 'satisfaction_speaker_level' => ( isset($_POST['satisfaction_speaker_level']) ? $_POST['satisfaction_speaker_level'] : '' ), 'satisfaction_food_beverage_level' => ( isset($_POST['satisfaction_food_beverage_level']) ? $_POST['satisfaction_food_beverage_level'] : '' ), 'first_partecipation' => ( isset($_POST['first_partecipation']) ? $_POST['first_partecipation'] : '' ), 'next_partecipation_probability' => $_POST['next_partecipation_probability'], 'recommend_probability' => $_POST['recommend_probability'], 'suggestions' => sanitize_text_field( trim($_POST['suggestions']) ) ]; //TODO if some fields are required, validation data have to be done and eventually some message errors have to be returned //storing data in database global $wpdb; $insert = $wpdb->insert( $wpdb->prefix . 'satisfaction_survey', $user_answers, [ 'satisfaction_level' => '%d', 'satisfaction_datetime_level' => '%d', 'satisfaction_location_level' => '%d', 'satisfaction_sessions_level' => '%d', 'satisfaction_speaker_level' => '%d', 'satisfaction_food_beverage_level' => '%d', 'first_partecipation' => '%d', 'next_partecipation_probability' => '%d', 'recommend_probability' => '%d', 'suggestions' => '%s' ] ); //response of insert query, done by $wpdb->insert(), is returned. WordPress encode this array in json and send the obtained output to client return ['answers_sent' => $insert]; }
Questa funzione è semplice; anzitutto vengono sanificati e preparati i dati per l’inserimento nel database. Ci sarebbe da fare la validazione ma per brevità è stata omessa.
Successivamente viene tentata la registrazione nel database e l’esito viene restituito in un array. WordPress prenderà questo risultato, lo codificherà in JSON e lo invierà al client.
Conclusioni
Ci sarebbe da menzionare altri aspetti sulle Rest API ma per fare una semplice richiesta queste nozioni possono bastare. Naturalmente ti invito ad approfondire l’argomento leggendo la documentazione ufficiale di WordPress sulle Rest API: se hai provato a riprodurre questo piccolo plugin, in una tua installazione di WordPress, potrai ottimizzare ulteriormente l’algoritmo sfruttando ancora meglio le Rest API.