Tabelle database personalizzate creazione di un'API

Nella prima parte di questa serie abbiamo esaminato gli svantaggi dell'utilizzo di una tabella personalizzata. Uno dei principali è la mancanza di un'API: quindi in questo articolo vedremo come crearne uno. L'API agisce su un livello tra la gestione dei dati nel plug-in e l'interazione effettiva con la tabella del database e ha lo scopo principale di garantire che tali interazioni siano sicure e di fornire un wrapper "human friendly" per la tua tabella. Come tale, avremo bisogno delle funzioni wrapper per l'inserimento, l'aggiornamento, l'eliminazione e l'interrogazione dei dati.


Perché dovrei creare un'API?

Esistono diversi motivi per cui è consigliabile utilizzare un'API, ma la maggior parte si riduce a due principi correlati: riduzione della duplicazione del codice e separazione delle preoccupazioni.

È più sicuro

Con le quattro funzioni wrapper menzionate sopra, devi solo assicurarti che le tue query sul database siano sicure quattro luoghi - è quindi possibile dimenticare completamente la sanificazione. Una volta che sei sicuro che le funzioni del wrapper gestiscono il database in modo sicuro, non devi preoccuparti dei dati che stai dando loro. Puoi anche convalidare i dati - restituire un errore se qualcosa non va bene.

L'idea è che senza queste funzioni dovrai assicurarti che ogni istanza di interazione con il tuo database funzioni in modo sicuro. Questo porta solo una maggiore probabilità che in uno di questi casi ti manchi qualcosa e crei una vulnerabilità nel tuo plug-in.

Riduce i bug

Questo è legato al primo punto (ed entrambi sono legati alla duplicazione del codice). Duplicando il codice vi è un maggior margine di errore per la scansione all'interno. Al contrario, usando le funzioni di un wrapper - se c'è un bug con l'aggiornamento o l'interrogazione della tabella del database - sai esattamente dove cercare.

È più facile da leggere

Questo può sembrare un motivo "soft", ma la leggibilità del codice è incredibilmente importante. La leggibilità consiste nel rendere chiara la logica e le azioni del codice al lettore. Questo non è importante solo quando lavori come parte di una squadra, o quando qualcuno potrebbe ereditare il tuo lavoro: potresti sapere che cosa il tuo codice è destinato a fare ora, ma in sei mesi probabilmente lo avrai dimenticato. E se il tuo codice è difficile da seguire, è più facile introdurre un bug.

Le funzioni wrapper puliscono il codice separando letteralmente il funzionamento interno di alcune operazioni (ad esempio creando un post) dal contesto di tale operazione (ad esempio, gestendo l'invio di un modulo). Immagina di avere l'intero contenuto di wp_insert_post () al posto di ogni istanza che usi wp_insert_post ().

Aggiunge uno strato di astrazione

Aggiungere strati di astrazione non è sempre una buona cosa, ma qui è indubbiamente. Non solo questi wrapper forniscono un modo umano di aggiornare o interrogare la tabella (immagina di dover usare SQL per interrogare i post piuttosto che usare il molto più conciso WP_Query () - e tutta la formulazione SQL e la sanificazione che ne consegue), ma aiuta anche a proteggere te e altri sviluppatori dalle modifiche alla struttura del database sottostante.

Usando le funzioni di wrapper, voi e terze parti potete usarli senza temere che siano insicuri o si rompano. Se decidi di rinominare una colonna, spostare una colonna altrove o addirittura eliminarla, puoi essere certo che il resto del plug-in non si interromperà, perché apporta le modifiche necessarie alle funzioni del wrapper. (Incidentalmente questo è un motivo convincente per evitare le query SQL dirette delle tabelle di WordPress: se cambiano, e lo faranno, volontà rompere il plug-in.). D'altra parte una API aiuta a estendere il plug-in in modo stabile.

Consistenza

Sono forse colpevole di spaccare un punto in due qui - ma ritengo che questo sia un vantaggio importante. Quando si sviluppano i plug-in, c'è poco più di incoerenza: incoraggia solo il codice disordinato. Le funzioni wrapper forniscono un'interazione coerente con il database: fornite dati e restituisce true (o un ID) o false (o un file WP_Error oggetto, se preferisci).


L'API

Spero di essermi ormai convinto della necessità di un'API per il tuo tavolo. Ma prima di andare avanti definiremo innanzitutto una funzione di supporto che renderà la sanitizzazione un po 'facile.

Le colonne della tabella

Definiremo una funzione che restituisce le colonne della tabella insieme al formato dei dati che si aspettano. Facendo ciò possiamo facilmente autorizzare le colonne consentite e formattare di conseguenza l'input. Inoltre, se apportiamo modifiche alle colonne, dobbiamo solo apportare le modifiche qui

 function wptuts_get_log_table_columns () return array ('log_id' => '% d', 'user_id' => '% d', 'activity' => '% s', 'object_id' => '% d', 'tipo_oggetto '=>'% s ',' activity_date '=>'% s ',); 

Inserimento di dati

La funzione di wrapper 'insert' più semplice prenderà semplicemente una serie di coppie valore-colonna e le inserirà nel database. Questo non deve essere il caso: puoi decidere di fornire più chiavi 'human friendly' che poi mapperai ai nomi delle colonne. È inoltre possibile decidere che alcuni valori siano generati automaticamente o sovradimensionati in base ai valori passati (ad esempio: stato post in wp_insert_post ()).

Forse i * valori * che hanno bisogno di mappatura. Il formato in cui è meglio archiviare i dati non è sempre il formato più conveniente da utilizzare. Ad esempio, per le date è forse più semplice gestire un oggetto DateTime o un timestamp, quindi convertirlo nel formato data desiderato.

La funzione wrapper può essere semplice o complicata, ma il minimo che dovrebbe fare è sanificare l'input. Raccomando anche la whitelisting per le colonne riconosciute, poiché provare a inserire dati in una colonna che non esiste può generare un errore.

In questo esempio l'ID utente è per impostazione predefinita quello dell'utente corrente e tutti i campi sono dati dal loro nome di colonna, che è l'eccezione della data dell'attività che viene passata come 'data'. La data, in questo esempio, dovrebbe essere un timestamp locale, che viene convertito prima di aggiungerlo al database.

 / ** * Inserisce un log nel database * * @ param $ array di dati Un array di chiavi => coppie di valori da inserire * @ return int L'ID di registro del log di attività creato. O WP_Error o false in caso di fallimento. * / function wptuts_insert_log ($ data = array ()) global $ wpdb; // Imposta valori predefiniti $ data = wp_parse_args ($ data, array ('user_id' => get_current_user_id (), 'date' => current_time ('timestamp'),)); // Verifica la validità della data se (! Is_float ($ data ['date']) || $ data ['date'] <= 0 ) return 0; //Convert activity date from local timestamp to GMT mysql format $data['activity_date'] = date_i18n( 'Y-m-d H:i:s', $data['date'], true ); //Initialise column format array $column_formats = wptuts_get_log_table_columns(); //Force fields to lower case $data = array_change_key_case ( $data ); //White list columns $data = array_intersect_key($data, $column_formats); //Reorder $column_formats to match the order of columns given in $data $data_keys = array_keys($data); $column_formats = array_merge(array_flip($data_keys), $column_formats); $wpdb->insert ($ wpdb-> wptuts_activity_log, $ data, $ column_formats); return $ wpdb-> insert_id; 
Mancia: È anche una buona idea controllare la validità dei dati. Quali controlli dovresti eseguire e come reagisce l'API dipende interamente dal tuo contesto. wp_insert_post (), per esempio richiede un certo grado di unicità per pubblicare le lumache - se ci sono scontri, ne genera automaticamente uno unico. wp_insert_term d'altra parte restituisce un errore se il termine esiste già. Si tratta di un mix tra il modo in cui WordPress gestisce questi oggetti e la semantica.

Aggiornamento dei dati

L'aggiornamento dei dati di solito imita da vicino l'inserimento dei dati, con l'eccezione che viene fornito un identificatore di riga (di solito solo la chiave primaria) insieme ai dati che devono essere aggiornati. In generale gli argomenti devono corrispondere alla funzione di inserimento (per coerenza), quindi in questo esempio viene utilizzata la "data" anziché "data_attività"

 / ** * Aggiorna un registro attività con i dati forniti * * @ param $ log_id int ID del registro attività da aggiornare * @ param $ matrice dati Un array di colonne => coppie valore da aggiornare * @ return bool Se il registro è stato aggiornato con successo. * / function wptuts_update_log ($ log_id, $ data = array ()) globale $ wpdb; // Il log ID deve essere intero positivo $ log_id = absint ($ log_id); if (vuoto ($ log_id)) restituisce falso; // Converti la data dell'attività dal timestamp locale al formato mysql GMT se (isset ($ data ['activity_date'])) $ data ['activity_date'] = date_i18n ('Ymd H: i: s', $ data ['date' ], vero ); // Inizializza il formato della colonna array $ column_formats = wptuts_get_log_table_columns (); // Forza i campi in minuscolo $ data = array_change_key_case ($ data); // colonne della lista bianca $ data = array_intersect_key ($ data, $ column_formats); // Riordina $ column_formats per abbinare l'ordine delle colonne dati in $ data $ data_keys = array_keys ($ data); $ column_formats = array_merge (array_flip ($ data_keys), $ column_formats); if (false === $ wpdb-> update ($ wpdb-> wptuts_activity_log, $ data, array ('log_id' => $ log_id), $ column_formats)) return false;  return true; 

Dati di query

Una funzione wrapper per interrogare i dati sarà spesso abbastanza complicata, in particolare perché potresti voler supportare tutti i tipi di query che selezionano solo determinati campi, limitati dalle istruzioni AND o OR, ordinati da una delle varie colonne possibili ecc. WP_Query classe).

Il principio di base della funzione wrapper per l'interrogazione dei dati è che dovrebbe richiedere un 'array di query', interpretarlo e formare l'istruzione SQL corrispondente.

 / ** * Recupera i registri delle attività dal database che corrisponde alla query $. * $ query è una matrice che può contenere le seguenti chiavi: * * 'campi': una matrice di colonne da includere nei ruoli restituiti. Oppure "conta" per contare le righe. Predefinito: vuoto (tutti i campi). * 'orderby' - datetime, user_id o log_id. Predefinito: datetime. * 'ordine' - asc o desc * 'user_id' - ID utente da abbinare, o un array di user ID * 'since' - timestamp. Restituisci solo le attività dopo questa data. Predefinito falso, nessuna restrizione. * 'until' - timestamp. Restituisci solo attività fino a questa data. Predefinito falso, nessuna restrizione. * * @ param $ query Matrice di query * @ return array Matrice di log di corrispondenza. Falso in caso di errore. * / function wptuts_get_logs ($ query = array ()) global $ wpdb; / * Impostazioni di default * / $ defaults = array ('fields' => array (), 'orderby' => 'datetime', 'order' => 'desc', 'user_id' => false, 'since' => false, 'until' => false, 'number' => 10, 'offset' => 0); $ query = wp_parse_args ($ query, $ defaults); / * Forma una chiave di cache dalla query * / $ cache_key = 'wptuts_logs:'. Md5 (serialize ($ query)); $ cache = wp_cache_get ($ cache_key); if (false! == $ cache) $ cache = apply_filters ('wptuts_get_logs', $ cache, $ query); restituire $ cache;  extract ($ query); / * SQL Select * / // Whitelist dei campi consentiti $ allowed_fields = wptuts_get_log_table_columns (); if (is_array ($ fields)) // Converti i campi in minuscolo (poiché i nostri nomi di colonna sono tutti in minuscolo - vedi parte 1) $ fields = array_map ('strtolower', $ fields); // Disinfetta per elenco bianco $ fields = array_intersect ($ fields, $ allowed_fields);  else $ fields = strtolower ($ fields);  // Restituisce solo i campi selezionati. Empty è interpretato come tutto if (vuoto ($ campi)) $ select_sql = "SELECT * FROM $ wpdb-> wptuts_activity_log";  elseif ('count' == $ fields) $ select_sql = "SELECT COUNT (*) FROM $ wpdb-> wptuts_activity_log";  else $ select_sql = "SELECT" .implode (',', $ fields). "FROM $ wpdb-> wptuts_activity_log";  / * SQL Join * / // Non abbiamo bisogno di questo, ma consentiremo che venga filtrato (vedi 'wptuts_logs_clauses') $ join_sql = "; / * SQL Dove * / // Inizializza WHERE $ where_sql = 'WHERE 1 = 1 '; if (! Empty ($ log_id)) $ where_sql. = $ Wpdb-> prepare (' AND log_id =% d ', $ log_id); if (! Empty ($ user_id)) // Force $ user_id per essere un array se (! is_array ($ user_id)) $ user_id = array ($ user_id); $ user_id = array_map ('absint', $ user_id); // Cast come numeri interi positivi $ user_id__in = implode (',' , $ user_id); $ where_sql. = "AND user_id IN ($ user_id__in)"; $ since = absint ($ since); $ until = absint ($ until); if (! empty ($ since)) $ where_sql. = $ wpdb-> prepare ('AND activity_date> =% s', date_i18n ('Ymd H: i: s', $ since, true)); if (! empty ($ until)) $ where_sql. = $ wpdb- > preparare ('AND activity_date <= %s', date_i18n( 'Y-m-d H:i:s', $until, true)); /* SQL Order */ //Whitelist order $order = strtoupper($order); $order = ( 'ASC' == $order ? 'ASC' : 'DESC' ); switch( $orderby ) case 'log_id': $order_sql = "ORDER BY log_id $order"; break; case 'user_id': $order_sql = "ORDER BY user_id $order"; break; case 'datetime': $order_sql = "ORDER BY activity_date $order"; default: break;  /* SQL Limit */ $offset = absint($offset); //Positive integer if( $number == -1 ) $limit_sql = ""; else $number = absint($number); //Positive integer $limit_sql = "LIMIT $offset, $number";  /* Filter SQL */ $pieces = array( 'select_sql', 'join_sql', 'where_sql', 'order_sql', 'limit_sql' ); $clauses = apply_filters( 'wptuts_logs_clauses', compact( $pieces ), $query ); foreach ( $pieces as $piece ) $$piece = isset( $clauses[ $piece ] ) ? $clauses[ $piece ] :"; /* Form SQL statement */ $sql = "$select_sql $where_sql $order_sql $limit_sql"; if( 'count' == $fields ) return $wpdb->get_var ($ sql);  / * Esegui query * / $ logs = $ wpdb-> get_results ($ sql); / * Aggiungi alla cache e filtra * / wp_cache_add ($ cache_key, $ logs, 24 * 60 * 60); $ logs = apply_filters ('wptuts_get_logs', $ logs, $ query); restituire i log $; 

C'è un bel po 'nell'esempio sopra visto che ho cercato di includere diverse funzionalità che potrebbero essere considerate quando si sviluppano le funzioni del wrapper, che copriamo nelle sezioni successive.

nascondiglio

Potresti considerare le tue query sufficientemente complesse, o ripetute regolarmente, che ha senso memorizzare nella cache i risultati. Poiché diverse query restituiranno risultati diversi, ovviamente non vogliamo utilizzare una chiave di cache generica: ne abbiamo bisogno di una che sia unica per quella query. Questo è esattamente ciò che segue. Serializza l'array di query, quindi lo esegue, producendo una chiave esclusiva $ query:

 $ cache_key = 'wptuts_logs:'. md5 (serialize ($ query));

Quindi controlliamo se abbiamo qualcosa memorizzato per quella chiave di cache - in tal caso, ottimo, restituiamo solo il suo contenuto. In caso contrario, generiamo l'SQL, eseguiamo la query e quindi aggiungiamo i risultati alla cache (per un massimo di 24 ore) e li restituiamo. Dovremo ricordare che i record potrebbero richiedere fino a 24 ore per apparire nei risultati di questa funzione. Di solito ci sono contesti in cui la cache viene automaticamente cancellata, ma avremmo bisogno di implementarli.

Filtri e azioni

I ganci sono stati ampiamente trattati su WPTuts + recentemente da Tom McFarlin e Pippin Williamson. Nel suo articolo, Pipino parla dei motivi per cui dovresti rendere il tuo codice estensibile tramite hooks e wrapper come wptuts_get_logs () servire come esempi eccellenti di dove possono essere utilizzati.

Abbiamo usato due filtri nella funzione precedente:

  • wptuts_get_logs - filtra il risultato della funzione
  • wptuts_logs_clauses - filtra una serie di componenti SQL

Ciò consente agli sviluppatori di terze parti o anche a noi stessi di creare l'API fornita. Se evitiamo di usare l'SQL diretto nel nostro plug-in e usiamo solo queste funzioni wrapper che abbiamo creato, allora è immediatamente possibile estendere il nostro plug-in. Il wptuts_logs_clauses filtro in particolare consentirebbe agli sviluppatori di modificare ogni parte dell'SQL - e quindi eseguire query complesse. Noteremo che è il lavoro di qualsiasi plug-in che utilizza questi filtri per assicurarsi che ciò che restituiscono sia adeguatamente igienizzato.

Gli hook sono altrettanto utili quando si eseguono le altre tre "operazioni" principali: inserimento, aggiornamento e cancellazione dei dati. Le azioni consentono ai plug-in di sapere quando vengono eseguite queste operazioni, quindi eseguono un'azione. Nel nostro contesto, ciò potrebbe significare inviare una e-mail a un amministratore quando un particolare utente esegue una determinata azione. I filtri, nel contesto di queste operazioni, sono utili per modificare i dati prima che vengano inseriti.

Fare attenzione quando si nominano i ganci. Un buon hook name fa diverse cose:

  • Comunica quando viene chiamato l'hook o cosa sta facendo (ad es. Puoi indovinare cosa pre_get_posts e user_has_cap potresti fare.
  • Essere unico. Si consiglia di aggiungere un prefisso agli hook con il nome del plug-in. A differenza delle funzioni, non ci sarà un errore se c'è uno scontro tra i nomi degli hook - invece sarà probabilmente solo 'silenziosamente' rompere uno o più plug-in.
  • Esibisce una sorta di struttura. Crea i tuoi ganci predicable, ed evitare di nominare i ganci "al volo", poiché a volte questo può portare a nomi di aggancio apparentemente casuali. Pianifica, invece, il più avanti possibile i ganci che userai, e crea una convenzione di denominazione appropriata, attenendoti ad essa.
Mancia: Generalmente è una buona idea imitare le stesse convenzioni di WordPress - in quanto gli sviluppatori capiranno più rapidamente cosa sta facendo quel gancio. Per quanto riguarda l'uso del nome del plug-in come prefisso: se il nome del plug-in è generico, questo potrebbe non essere sufficiente per garantire l'unicità. Infine, non dare un'azione e un filtro con lo stesso nome.

Eliminazione dei dati

Eliminare i dati è spesso il più semplice dei wrapper, anche se potrebbe essere necessario eseguire alcune operazioni di "pulizia" e semplicemente rimuovere i dati. wp_delete_post () per esempio, non solo elimina il post dal * _posts tabella, ma elimina anche il post post appropriato, le relazioni tassonomiche, i commenti e le revisioni ecc.

In linea con i commenti della sezione precedente, includeremo due due azioni: una attivata prima e l'altra dopo che un registro è stato eliminato dalla tabella. Seguendo la convenzione di denominazione di WordPress per tali azioni:

  • _Elimina_ viene attivato prima della cancellazione
  • _deleted_ viene attivato dopo la cancellazione
 / ** * Elimina un registro attività dal database * * @ param $ log_id int ID del registro attività da eliminare * @ return bool Indica se il registro è stato cancellato correttamente. * / function wptuts_delete_log ($ log_id) global $ wpdb; // Il log ID deve essere intero positivo $ log_id = absint ($ log_id); if (vuoto ($ log_id)) restituisce falso; do_action ( 'wptuts_delete_log', $ log_id); $ sql = $ wpdb-> prepare ("DELETE da $ wpdb-> wptuts_activity_log WHERE log_id =% d", $ log_id); if (! $ wpdb-> query ($ sql)) restituisce false; do_action ( 'wptuts_deleted_log', $ log_id); ritorna vero; 

Documentazione

Sono stato un po 'pigro con la documentazione in-source dell'API di cui sopra. In questa serie Tom McFarlin spiega perché non dovresti esserlo. Potresti aver speso molto tempo a sviluppare le tue funzioni API, ma se altri sviluppatori non sapessero come usarle, non lo faranno. Aiuterai anche te stesso, quando dopo 6 mesi hai dimenticato come devono essere dati i dati o cosa ti aspetti di ricevere.


Sommario

I wrapper per la tabella del database possono variare dal relativamente semplice (ad es. get_terms ()) a estremamente complesso (ad es WP_Query classe). Collettivamente dovrebbero cercare di fungere da porta di accesso al tuo tavolo: permettendoti di concentrarti sul contesto in cui sono utilizzati, e in sostanza dimentica quello che stanno facendo in realtà. L'API che crei è solo un piccolo esempio della nozione di "separazione delle preoccupazioni", spesso attribuita a Edsger W. Dijkstra nel suo articolo Sul ruolo del pensiero scientifico:

È quello che a volte ho chiamato "la separazione delle preoccupazioni", che, anche se non perfettamente possibile, è ancora l'unica tecnica disponibile per l'ordinamento efficace dei propri pensieri, che io conosca. Questo è ciò che intendo per "focalizzare l'attenzione su qualche aspetto": non significa ignorare gli altri aspetti, è solo rendere giustizia al fatto che dal punto di vista di questo aspetto, l'altro è irrilevante. È simultaneamente a mente singola e multipla.

Puoi trovare il codice utilizzato in questa serie, nella sua interezza, su GitHub. Nella parte successiva di questa serie vedremo come è possibile mantenere il database e gestire gli aggiornamenti.