Tabele de baze de date personalizate Crearea unui API

În prima parte a acestei serii am analizat dezavantajele utilizării unei mese personalizate. Una dintre cele mai importante este lipsa unui API: așa că în acest articol vom examina cum să creați unul. API-ul acționează un strat între manipularea datelor în plug-in-ul dvs. și interacțiunea reală cu tabela de baze de date - și este destinat în primul rând pentru a asigura că astfel de interacțiuni sunt sigure și pentru a furniza un pachet "prietenos pentru om" pentru masa dumneavoastră. Ca atare, vom avea nevoie de funcții de împachetare pentru inserarea, actualizarea, ștergerea și interogarea datelor.


De ce ar trebui să creez un API?

Există mai multe motive pentru care este recomandat un API - dar cele mai multe se referă la două principii conexe: reducerea duplicării codului și separarea preocupărilor.

Este mai sigur

Cu cele patru funcții de ambalare de mai sus, trebuie doar să vă asigurați că interogările dvs. de bază de date sunt în siguranță patru - puteți să uitați complet de igienizare. Odată ce sunteți încrezător că funcțiile de împachetare se ocupă de baza de date în siguranță, atunci nu trebuie să vă faceți griji cu privire la datele pe care le oferiți acestora. Poti de asemenea valida datele - returnează o eroare dacă ceva nu este corect.

Ideea este că, fără această funcție, va trebui să vă asigurați că fiecare instanță de interacțiune cu baza dvs. de date face acest lucru în siguranță. Acest lucru aduce o probabilitate crescută că într-una din aceste situații veți pierde ceva și veți crea o vulnerabilitate în plug-in-ul dvs..

Reduce bug-urile

Acest lucru este legat de primul punct (și ambele sunt legate de duplicarea codului). Prin duplicarea codului există mai multe posibilități pentru accesarea cu crawlere a erorilor. În schimb, prin utilizarea funcțiilor de înfășurare - dacă există un bug cu actualizarea sau interogarea tabelului bazei de date - știți exact unde să căutați.

Este mai ușor de citit

Acest lucru poate părea un motiv "moale" - dar lizibilitatea codului este incredibil de importantă. Citibilitatea înseamnă a face clar logica și acțiunile codului cititorului. Acest lucru nu este important doar atunci când lucrați ca parte a unei echipe sau când cineva vă poate moșteni munca: poate știți ce intenționați să faceți acum codul, dar în șase luni probabil că ați uitat. Și dacă codul dvs. este greu de urmărit, este mai ușor să introduceți un bug.

Funcțiile Wrapper vă curăță codul separând literalmente funcționarea internă a unei anumite operații (să zicem, creând un post) din contextul acelei operații (de exemplu, manipularea unei trimiteri de formular). Imaginați-vă că aveți întregul conținut wp_insert_post () în locul fiecărui exemplu pe care îl utilizați wp_insert_post ().

Adaugă un strat de abstractizare

Adăugarea de straturi de abstractizare nu este întotdeauna un lucru bun - dar aici, fără îndoială, este. Nu numai că aceste împachetări oferă o modalitate umană de actualizare sau interogare a tabelului (imaginați-vă că trebuie să utilizați SQL pentru a interoga posturile, mai degrabă decât să utilizați o structură mult mai concisă WP_Query () - și toate formularea SQL și sanitație care merge cu ea), dar, de asemenea, vă ajută să vă protejeze pe dumneavoastră și pe alți dezvoltatori de la modificările aduse structurii bază de date.

Folosind funcțiile de împachetare, dvs., precum și terțe părți, le puteți folosi fără teamă că acestea nu sunt sigure sau se vor rupe. Dacă decideți să redenumiți o coloană, să mutați o coloană în altă parte sau chiar să o ștergeți, puteți să vă asigurați că restul plug-in-ului nu se va rupe, deoarece faceți doar modificările necesare în funcțiile de împachetare. (De altfel, acesta este un motiv convingător pentru a evita interogările SQL directe ale tabelelor WordPress: dacă se schimbă și se vor schimba voi rupe plug-in-ul.). Pe de altă parte, un API vă ajută să vă extindeți extensia într-un mod stabil.

consecvență

Probabil că sunt vinovat de împărțirea unui punct în două aici - dar consider că acest beneficiu important. Există puțin mai rău decât inconsecvența atunci când dezvoltați plug-in-uri: doar încurajează codul dezordonat. Funcțiile Wrapper oferă o interacțiune consecventă cu baza de date: furnizați date și returnează adevărat (sau un ID) sau false (sau un WP_Error obiect, dacă preferați).


API-ul

Sper că am convins acum că este nevoie de un API pentru masa ta. Dar înainte de a merge mai departe vom defini mai întâi o funcție de ajutor care va face sanitizarea un pic ușor.

Coloanele din tabel

Vom defini o funcție care returnează coloanele tabelului împreună cu formatul de date pe care îl așteaptă. Procedând astfel putem cu ușurință să alcătuiți liste admise și să formalizăm intrarea în mod corespunzător. Mai mult, dacă facem modificări în coloane, trebuie doar să facem schimbările aici

 funcția wptuts_get_log_table_columns () retur array ('log_id' => '% d', 'user_id' => '% d', 'object_type' '=>'% s ',' activity_date '=>'% s ',); 

Introducerea datelor

Funcția de înfășurare "inserați" cel mai de bază va lua doar o serie de perechi de coloane-valoare și le va introduce în baza de date. Acest lucru nu trebuie să fie cazul: puteți decide să furnizați mai multe chei "prietenoase cu oamenii", pe care apoi le veți marca la numele coloanelor. De asemenea, puteți decide că unele valori sunt generate automat sau depășite în funcție de valorile trecute (de exemplu: starea postului în wp_insert_post ()).

Este posibil ca * valorile * să aibă nevoie de cartografiere. Formatul în care sunt stocate cele mai bune date nu este întotdeauna cel mai convenabil format de utilizat. De exemplu, pentru date poate fi mai ușor să se ocupe de un obiect DateTime sau de o marcă de timp - și apoi să se convertească la data dorită.

Funcția de înfășurare poate fi simplă sau complicată - dar minimul pe care trebuie să îl faceți este să dezinstalați intrarea. Aș recomanda, de asemenea, lista albă pentru coloanele recunoscute, deoarece încercarea de a insera datele într-o coloană care nu există poate arunca o eroare.

În acest exemplu, ID-ul de utilizator este în mod prestabilit cel al utilizatorului curent și toate câmpurile sunt date în funcție de numele coloanei - cu excepția datei de activitate care este trecută ca "dată". Data, în acest exemplu, ar trebui să fie o marcă de timp locală, care este convertită înainte de adăugarea acesteia în baza de date.

 / ** * Se inserează un jurnal în baza de date * * @ param $ matrix de date Un array de key => perechi de valori care urmează să fie inserate * @ return int ID-ul jurnal al jurnalului de activitate creat. Sau WP_Error sau false la eșec. * / funcția wptuts_insert_log ($ data = array ()) global $ wpdb; // setați valorile implicite $ data = wp_parse_args ($ data, array ('user_id' => get_current_user_id (), 'date' => current_time ('timestamp'),)); // Verificați validitatea datei dacă (! 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->introduceți ($ wpdb-> wptuts_activity_log, $ date, $ column_formats); retur $ wpdb-> insert_id; 
Bacsis: De asemenea, este o idee bună să verificați validitatea datelor. Ce verificări trebuie să efectuați și cum reacționează API depinde în întregime de contextul dvs.. wp_insert_post (), de exemplu, necesită un anumit grad de unicitate pentru post-slugs - dacă există ciocniri, aceasta auto-generează un singur. wp_insert_term pe de altă parte, returnează o eroare dacă termenul există deja. Acesta este un amestec între modul în care WordPress se ocupă de aceste obiecte și semantică.

Actualizarea datelor

Actualizarea datelor, de obicei, mimează inserarea datelor - cu excepția faptului că un identificator de rând (de obicei doar cheia primară) este furnizat împreună cu datele care trebuie actualizate. În general, argumentele ar trebui să se potrivească cu funcția insert (pentru consecvență) - deci în acest exemplu, se utilizează "date" în loc de "activity_date"

 / ** * Actualizează un jurnal de activități cu datele furnizate * * @ param $ log_id int ID-ul jurnalului de activități care urmează să fie actualizat * @ param $ matrice de date Un array de coloane => perechi de valori care trebuie actualizate * @ return bool a fost actualizată cu succes. * / funcția wptuts_update_log ($ log_id, $ data = array ()) global $ wpdb; // ID-ul jurnalului trebuie să fie întregul pozitiv $ log_id = absint ($ log_id); dacă (gol ($ log_id)) return false; // Convertiți data activității de la timestampul local în formatul GMT mysql dacă (isset ($ data ['activity_date'])) $ data ['activity_date'] = data_i18n ('Ymd H: ], Adevărat ); // Arhivă formatare coloană inițiale $ column_formats = wptuts_get_log_table_columns (); // Câmpurile forțate la literele mici $ data = array_change_key_case ($ data); // coloane din lista albă $ data = array_intersect_key ($ date, $ column_formats); // Reordonați $ column_formats pentru a se potrivi cu ordinea coloanelor date în $ data $ data_keys = array_keys ($ data); $ column_formats = array_merge (array_flip ($ date_keys), $ column_formats); dacă false === $ wpdb-> actualizează ($ wpdb-> wptuts_activity_log, $ data, array ('log_id' => $ log_id), $ column_formats)) return false;  return true; 

Interogarea datelor

O funcție de împachetare pentru interogarea datelor va fi adesea destul de complicată - mai ales că ați putea dori să sprijiniți toate tipurile de interogări care selectează numai anumite câmpuri, restricționați prin instrucțiuni AND sau OR, ordonați după una din mai multe coloane posibile etc WP_Query clasă).

Principalul element principal al funcției de înfășurare pentru interogarea datelor este că ar trebui să ia o "matrice interogare", să o interpreteze și să formeze instrucțiunea SQL corespunzătoare.

 / ** * Returnează jurnalele de activitate din baza de date care se potrivesc cu $ query. * interogarea $ este o matrice care poate conține următoarele chei: * * 'fields' - un șir de coloane care trebuie incluse în rolurile returnate. Sau "conta" pentru a număra rânduri. Implicit: gol (toate câmpurile). * 'orderby' - datetime, user_id sau log_id. Implicit: datatime. * 'order' - asc sau desc * 'user_id' - ID-ul de utilizator pentru a se potrivi sau o serie de ID-uri de utilizator * 'since' - timestamp. Returnează numai activitățile după această dată. Implicit false, fără restricții. * "până" - marcajul temporal. Returnează numai activitățile până la această dată. Implicit false, fără restricții. * * @ param $ query Interogare array * @ array retur Array de loguri de potrivire. Fals pe eroare. * / funcția wptuts_get_logs ($ query = array ()) global $ wpdb; / * Parametrii impliciți * / $ defaults = array ('fields' => array (), 'orderby' => 'datetime', 'order' => false, 'până' => false, 'număr' => 10, 'offset' => 0); $ query = wp_parse_args ($ interogare, implicit $); / * Formați o cheie cache din interogarea * / $ cache_key = 'wptuts_logs:'. Md5 (serialize ($ query)); $ cache = wp_cache_get ($ cache_key); dacă (false! == $ cache) $ cache = apply_filters ('wptuts_get_logs', $ cache, $ query); întoarcere $ cache;  extract ($ query); / * SQL Selectați * / // Lista albă a câmpurilor permisibile $ allowed_fields = wptuts_get_log_table_columns (); if (is_array ($ fields)) // Convertiți câmpurile în litere mici (numele coloanelor sunt mici) - $ fields = array_map ('strtolower', $ fields); // Sanitizează prin listarea albă $ fields = array_intersect ($ fields, $ allowed_fields);  altceva $ fields = strtolower (câmpuri $);  // Reveniți numai câmpurile selectate. Gol este interpretat ca toate dacă (gol (câmpuri $)) $ select_sql = "SELECT * FROM $ wpdb-> wptuts_activity_log";  elseif ('count' == $ fields) $ select_sql = "SELECT COUNT (*) DIN $ wpdb-> wptuts_activity_log";  altceva $ select_sql = "SELECT" .implode (',', câmpuri $) "FROM $ wpdb-> wptuts_activity_log";  / * SQL Join * / // Nu avem nevoie de acest lucru, dar vom permite ca acesta să fie filtrat (vezi 'wptuts_logs_clauses') $ join_sql = "; / * SQL unde * / // Initial WHERE $ where_sql = 'WHERE 1 = 1 '; dacă (! Empty ($ log_id)) $ where_sql. = $ Wpdb-> pregăti (' AND log_id =% d ', $ log_id) user_id pentru a fi un array dacă ($ is_array ($ user_id)) $ user_id = array ($ user_id); $ user_id = array_map ('absint', $ user_id); // Distribuție ca număr întreg pozitiv $ user_id__in = , $ user_id); $ where_sql. = "ȘI user_id IN ($ user_id__in)"; $ since = absint ($ since); $ until = absint ($ to); = $ wpdb-> pregăti ('AND activity_date> =% s', data_i18n ('Ymd H: i: s', $ since, true) > pregăti ('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);  / * Efectuați interogarea * / $ logs = $ wpdb-> get_results ($ sql); / * Adăugați în cache și filtrați * / wp_cache_add ($ cache_key, $ logs, 24 * 60 * 60); $ logs = apply_filters ('wptuts_get_logs', $ logs, $ query); returnează jurnalele de $; 

Există un exemplu echitabil în exemplul de mai sus, deoarece am încercat să includ câteva caracteristici care ar putea fi luate în considerare la dezvoltarea funcțiilor de împachetare, pe care le acoperim în secțiunile următoare.

ascunzătoare

Puteți considera că interogările dvs. sunt suficient de complexe sau repetate în mod regulat, că este logic să cacheți rezultatele. Deoarece interogările diferite vor întoarce rezultate diferite, evident că nu vrem să folosim o cheie cache generică - avem nevoie de una care este unică pentru interogarea respectivă. Acesta este exact ceea ce face următorul lucru. Se ordonează matricea de interogare și apoi se șterge, producând o cheie unică pentru $ interogare:

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

Apoi verificăm dacă avem ceva stocat pentru acea cheie de memorare în memoria cache - dacă este așa, minunat, vom returna conținutul. Dacă nu, generăm SQL, executăm interogarea și apoi adăugăm rezultatele în memoria cache (cel mult 24 de ore) și le returnează. Va trebui să ne amintim că înregistrările pot dura până la 24 de ore pentru a apărea în rezultatele acestei funcții. De obicei, există contexte în care cache-ul este șters automat - dar ar trebui să le implementăm.

Filtre și acțiuni

Cârligele au fost acoperite pe larg de WPTuts + recent de Tom McFarlin și Pippin Williamson. În articolul său, Pippin vorbește despre motivele pentru care ar trebui să vă faceți codul extensibil prin cârlige și ambalaje, cum ar fi wptuts_get_logs () servesc ca exemple excelente de unde pot fi folosite.

Am folosit două filtre în funcția de mai sus:

  • wptuts_get_logs - filtrează rezultatul funcției
  • wptuts_logs_clauses - filtrează o serie de componente SQL

Acest lucru permite dezvoltatorilor terță parte sau chiar noi înșine să construim pe API-ul furnizat. Dacă evităm utilizarea SQL directă în plug-in-ul nostru și folosim doar aceste funcții de împachetare pe care le-am construit, atunci imediat face posibilă extinderea plug-in-ului. wptuts_logs_clauses în special filtrul ar permite dezvoltatorilor să modifice fiecare parte a SQL - și astfel să efectueze interogări complexe. Vom observa că sarcina fiecărui plug-in folosindu-le aceste filtre pentru a vă asigura că ceea ce revin este bine dezinfectat.

Cârligele sunt la fel de utile și la efectuarea celorlalte trei operații principale: inserarea, actualizarea și ștergerea datelor. Acțiunile permit plugin-urilor să afle când se efectuează aceste acțiuni. În contextul nostru, acest lucru ar putea însemna trimiterea unui e-mail către un administrator atunci când un anumit utilizator efectuează o anumită acțiune. Filtrele, în contextul acestor operații, sunt utile pentru modificarea datelor înainte de introducerea acestora.

Aveți grijă când denumiți cârligele. Un nume bun cârlig face mai multe lucruri:

  • Comunică când este apelat cârligul sau ce face (de exemplu, puteți ghici ce pre_get_posts și user_has_cap ar putea face.
  • Fi unic. Este recomandat să prefixați cârligele cu numele plug-in-ului. Spre deosebire de funcții, nu va exista o eroare în cazul în care există o ciocnire între numele cârligului - în schimb, probabil va fi doar "tăcut" rupe unul sau mai multe plug-in-uri.
  • Expune un fel de structură. Fă-ți cârligele predicabili, și evitați să numim cârlige "în zbor", deoarece acest lucru poate duce, uneori, la nume de cârlig aparent aleatorii. În loc să planificați cât mai departe posibil cârligele pe care le veți folosi și să veniți cu o convenție de numire adecvată - și să vă lipiți de ea.
Bacsis: În general, o idee bună de a imita aceleași convenții ca WordPress - în calitate de dezvoltatori, va înțelege mai repede ce face acest cârlig. În ceea ce privește utilizarea prefixului cu numele plug-in-ului: dacă numele dvs. de plug-in este generic, este posibil ca acesta să nu fie suficient pentru a asigura unicitatea. În cele din urmă, nu dați o acțiune și un filtru cu același nume.

Ștergerea datelor

Ștergerea datelor este de cele mai multe ori cea mai simplă dintre ambalaje - deși poate fi necesară efectuarea unor operațiuni de "curățare" precum și eliminarea pur și simplu a datelor. wp_delete_post () de exemplu, nu numai că șterge postul din * _posts tabel dar, de asemenea, șterge post meta corespunzătoare, relații de taxonomie, comentarii și revizii etc.

În concordanță cu comentariile din secțiunea anterioară, vom include două două acțiuni: una declanșată înainte și cealaltă după ce un jurnal a fost șters din tabel. Urmând convenția de numire a WordPress pentru astfel de acțiuni:

  • _șterge_ este declanșată înainte de ștergere
  • _deleted_ este declanșată după ștergere
 / ** * Șterge un jurnal de activități din baza de date * * @ param $ log_id int ID al jurnalului de activități care urmează să fie șters * @ return bool Dacă jurnalul a fost șters cu succes. * / funcția wptuts_delete_log ($ log_id) global $ wpdb; // ID-ul jurnalului trebuie să fie întregul pozitiv $ log_id = absint ($ log_id); dacă (gol ($ log_id)) return false; do_action ( 'wptuts_delete_log', $ log_id); $ sql = $ wpdb-> pregăti ("DELETE din $ wpdb-> wptuts_activity_log WHERE log_id =% d", $ log_id); dacă (! $ wpdb-> interogare ($ sql)) return false; do_action ( 'wptuts_deleted_log', $ log_id); return true; 

Documentație

Am fost puțin leneș cu documentația în sursă a API-ului de mai sus. În această serie Tom McFarlin explică de ce nu ar trebui să fii. Este posibil să fi petrecut mult timp dezvoltând funcțiile API, dar dacă alți dezvoltatori nu știu cum să le folosească - nu vor. De asemenea, vă veți ajuta singuri, când după 6 luni ați uitat cum trebuie date datele sau ce ar trebui să vă așteptați să vi se returneze.


rezumat

Împacheturile pentru tabelul de bază de date pot varia de la relativ simplu (de ex. get_terms ()) la extrem de complex (de ex WP_Query clasă). În mod colectiv, aceștia ar trebui să încerce să servească drept poartă de acces la masa dvs., permițându-vă să vă concentrați asupra contextului în care sunt folosiți și, în mod esențial, să uitați ceea ce fac ei în realitate. API-ul pe care îl creați este doar un mic exemplu al noțiunii de "separare a preocupărilor", adesea atribuit lui Edsger W. Dijkstra în lucrarea sa despre rolul gândirii științifice:

Este ceea ce uneori am numit "separarea preocupărilor", care, chiar dacă nu este perfect posibil, este singura tehnică disponibilă pentru ordonarea eficientă a gândurilor, despre care știu. Aceasta este ceea ce vreau să spun prin "focalizarea atenției asupra unui anumit aspect": nu înseamnă ignorarea celorlalte aspecte, ci doar justificarea faptului că, din punctul de vedere al acestui aspect, celălalt este irelevant. Ea este una și mai multe piste minte simultan.

Puteți găsi codul utilizat în această serie, în întregime, pe GitHub. În următoarea parte a acestei serii vom analiza modul în care vă puteți menține baza de date și puteți face upgrade-uri.

Cod