Tilpassede databasetabeller Opprette en API

I den første delen av denne serien så vi på ulemper ved å bruke et tilpasset bord. En av de store er mangelen på en API: så i denne artikkelen ser vi på hvordan du lager en. API-en virker et lag mellom håndtering av data i plugin-modulen og den faktiske interaksjonen med databasetabellen - og er primært ment å sikre at slike interaksjoner er sikre og å gi et "menneskelig vennlig" innpakning for bordet ditt. Som sådan vil vi kreve innpakningsfunksjoner for å sette inn, oppdatere, slette og spørre data.


Hvorfor skal jeg opprette en API?

Det er flere grunner til at en API anbefales - men de fleste koker ned til to relaterte prinsipper: redusere kodedublisering og separasjon av bekymringer.

Det er tryggere

Med de ovennevnte fire nevnte wrapperfunksjonene trenger du bare å sikre at databasespørsmålene dine er trygge fire steder - du kan da glemme sanitering helt. Når du er sikker på at wrapperfunksjonene dine håndterer databasen på en sikker måte, trenger du ikke å bekymre deg for dataene du gir dem. Du kan også validere dataene - returnerer en feil hvis noe ikke er riktig.

Tanken er at uten denne funksjonen må du sørge for at hver forekomst av interaksjon med databasen din gjør det trygt. Dette gir bare en økt sannsynlighet for at i en av disse tilfellene vil du savne noe og skape et sikkerhetsproblem i plugin-modulen din.

Reduserer feil

Dette er relatert til det første punktet (og begge er relatert til kod duplisering). Ved å duplisere kode er det større mulighet for bugs å krype inn. Omvendt ved å bruke en wrapper-funksjoner - hvis det er en feil med å oppdatere eller spørre databasetabellen - vet du nøyaktig hvor du skal se.

Det er enklere å lese

Dette kan virke som en "myk" grunn - men lesbarhet av kode er utrolig viktig. Lesbarhet handler om å gjøre logikk og handlinger av koden klar til leseren. Dette er ikke bare viktig når du arbeider som en del av et lag, eller når noen kan arve arbeidet ditt: Du kan kanskje vite hva koden din er ment å gjøre nå, men på seks måneder vil du sannsynligvis ha glemt. Og hvis koden din er vanskelig å følge, er det lettere å introdusere en feil.

Wrapper-funksjonene rydder opp koden ved å bokstavelig talt separere de interne operasjonene av en operasjon (si å lage et innlegg) fra konteksten til den operasjonen (si å håndtere et skjemainnlegg). Tenk deg å ha hele innholdet i wp_insert_post () i stedet for alle forekomster du bruker wp_insert_post ().

Legger til et lag av abstraksjon

Å legge til lag av abstraksjon er ikke alltid en god ting - men her er det utvilsomt. Ikke bare gir disse wrappers en menneskelig vennlig måte å oppdatere eller spørre bordet på (forestille seg å måtte bruke SQL til å søke innlegg i stedet for å bruke den langt mer konsise WP_Query () - og alle SQL formuleringen og sanitering som følger med den), men hjelper også med å beskytte deg og andre utviklere mot endringer i den underliggende databasestrukturen.

Ved å bruke wrapperfunksjoner kan du, så vel som tredjeparter, bruke disse uten frykt for at de er usikre eller vil bryte. Hvis du bestemmer deg for å gi nytt navn til en kolonne, flytter du en kolonne andre steder eller sletter det, men du kan være sikker på at resten av plugin-modulen ikke bryter, fordi du bare foretar de nødvendige endringene i innpakningsfunksjonene dine. (For øvrig er dette en overbevisende grunn til å unngå direkte SQL-spørringer av WordPress-tabeller: hvis de endres, og de vil det vil bryte plugin-modulen din.). På den annen side kan en API hjelpe plugin-modulen til å bli utvidet på en stabil måte.

Konsistens

Jeg er kanskje skyldig i å splitte et poeng i to her - men jeg føler at dette er en viktig fordel. Det er lite verre enn inkonsekvens når du utvikler plugin-moduler: det oppfordrer bare rotete koden. Wrapper-funksjoner gir en konsistent samhandling med databasen: du oppgir data og returnerer sann (eller en ID) eller falsk (eller en WP_Error objekt, hvis du foretrekker det).


API

Forhåpentligvis har jeg nå overbevist om behovet for en API for bordet ditt. Men før vi går videre, definerer vi først en hjelperfunksjon som gjør sanitering litt lett.

Tabellkolonnene

Vi definerer en funksjon som returnerer tabellens kolonner sammen med dataformatet de forventer. Ved å gjøre dette kan vi enkelt hviteliste tillatte kolonner og formatere inngangen tilsvarende. Videre hvis vi gjør noen endringer i kolonnene, trenger vi bare å gjøre endringene her

 funksjon wptuts_get_log_table_columns () return array ('log_id' => '% d', 'user_id' => '% d', 'aktivitet' => '% s', 'object_id' => '% d', 'object_type '=>'% s ',' activity_date '=>'% s ',); 

Sette inn data

Den mest grunnleggende "insert" wrapper-funksjonen vil bare ta et utvalg av kolonneverdierpar og sette disse inn i databasen. Dette trenger ikke være tilfelle: Du kan bestemme deg for å gi flere "menneskelige vennlig" nøkler som du deretter kartlegger til kolonnens navn. Du kan også bestemme at noen verdier er automatisk generert eller overstyrt basert på de bestått verdiene (for eksempel: Legg inn status i wp_insert_post ()).

Det er kanskje * verdiene * som trenger kartlegging. Formatet som dataene er best lagret i, er ikke alltid det mest praktiske formatet som skal brukes. For eksempel kan det være lettere å håndtere en DateTime-objekt eller en tidsstempel for datoer, og konverter deretter til ønsket datoformat.

Innpakningsfunksjonen kan være enkel eller komplisert - men det minste det bør gjøre er å sanitere inngangen. Jeg vil også anbefale whitelisting for de anerkjente kolonnene, da du prøver å sette inn data i en kolonne som ikke eksisterer kan kaste en feil.

I dette eksemplet er bruker-ID som standard av den gjeldende brukeren, og alle feltene er oppgitt med deres kolonnenavn - som unntak av aktivitetsdatoen som er bestått som "dato". Datoen, i dette eksemplet, bør være en lokal tidsstempel, som konverteres før du legger den til databasen.

 / ** * Setter inn en logg inn i databasen * * @ param $ data array En rekke nøkkel => verdipar som skal settes inn * @ return int Loggen ID for den opprettede aktivitetsloggen. Eller WP_Error eller feil på feil. * / funksjon wptuts_insert_log ($ data = array ()) global $ wpdb; // Angi standardverdier $ data = wp_parse_args ($ data, array ('user_id' => get_current_user_id (), 'date' => current_time ('tidsstempel'))); // Sjekk dato gyldighet hvis (! Is_flate ($ 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->sett inn ($ wpdb-> wptuts_activity_log, $ data, $ column_formats); returner $ wpdb-> insert_id; 
Tips: Det er også en god ide å kontrollere gyldigheten av dataene. Hva sjekker du skal utføre, og hvordan API-en reagerer, avhenger helt av konteksten din. wp_insert_post (), for eksempel krever en viss grad av unikt å legge inn sluger - hvis det er sammenstøt, genererer det automatisk en unik en. wp_insert_term På den annen side returneres en feil hvis begrepet allerede eksisterer. Dette er ned til en blanding mellom hvordan WordPress håndterer disse objektene og semantikken.

Oppdaterer data

Oppdatering av data følger vanligvis nøye med å sette inn data - med unntak av at en radidentifikator (vanligvis bare primærnøkkelen) leveres sammen med data som må oppdateres. Generelt bør argumentene samsvare med innsatsfunksjonen (for konsistens) - så i dette eksemplet brukes "dato" i stedet for "activity_date"

 / ** * Oppdaterer en aktivitetslogg med medfølgende data * * @ param $ log_id int ID av aktivitetsloggen skal oppdateres * @ param $ data array En rekke kolonne => verdipar som skal oppdateres * @ return bool Om loggen ble oppdatert. * / funksjon wptuts_update_log ($ log_id, $ data = array ()) global $ wpdb; // Log ID må være positivt heltall $ log_id = absint ($ log_id); hvis (tomt ($ log_id)) returnerer falsk; // Konverter aktivitetsdato fra lokal tidsstempel til GMT mysql format hvis (isset ($ data ['activity_date'])) $ data ['activity_date'] = date_i18n ('Ymd H: i: s', $ data ['date' ], sant); // Initialiser kolonneformat array $ column_formats = wptuts_get_log_table_columns (); // Force felt til små bokstaver $ data = array_change_key_case ($ data); // Hvite liste kolonner $ data = array_intersect_key ($ data, $ column_formats); // Reorder $ column_formats for å matche rekkefølgen på kolonner gitt i $ data $ data_keys = array_keys ($ data); $ column_formats = array_merge (array_flip ($ data_keys), $ column_formats); hvis (false === $ wpdb-> oppdatering ($ wpdb-> wptuts_activity_log, $ data, array ('log_id' => $ log_id), $ column_formats)) return false;  returnere sann; 

Spørringsdata

En wrapper-funksjon for spørring av data vil ofte være ganske komplisert - spesielt siden du kanskje vil støtte alle typer søk, som bare velger bestemte felt, begrenser med AND eller OR-setninger, rekkefølge av en av flere mulige kolonner osv. (Se bare WP_Query klasse).

Den grunnleggende prinsipper for wrapper-funksjonen for å spørre dataene er at hvis den skulle ta en "spørringsgruppe", tolke den og danne den tilsvarende SQL-setningen.

 / ** * Henter aktivitetslogger fra databasen som matcher $ spørring. * $ spørring er en matrise som kan inneholde følgende nøkler: * * 'felt' - en rekke kolonner som skal inkluderes i returnerte roller. Eller "telle" for å telle rader. Standard: tomt (alle felt). * 'orderby' - datetime, user_id eller log_id. Standard: datetime. * 'bestilling' - asc eller desc * 'user_id' - bruker ID for å matche, eller en rekke bruker-IDer * 'siden' - tidsstempel. Returner kun aktiviteter etter denne datoen. Standard false, ingen begrensninger. * 'til' - tidsstempel. Returner kun aktiviteter opp til denne datoen. Standard false, ingen begrensninger. * * @ param $ spørring Query array * @ return array Array av matchende logger. False on error. * / funksjon wptuts_get_logs ($ query = array ()) global $ wpdb; / * Parse defaults * / $ defaults = array ('fields' => array (), 'orderby' => 'datetime', 'order' => 'desc', 'user_id' => false, 'since' => false, 'until' => false, 'number' => 10, 'offset' => 0); $ query = wp_parse_args ($ query, $ default); / * Lag en cache-nøkkel fra spørringen * / $ cache_key = 'wptuts_logs:'. Md5 (serialize ($ query)); $ cache = wp_cache_get ($ cache_key); hvis (false! == $ cache) $ cache = apply_filters ('wptuts_get_logs', $ cache, $ query); returner $ cache;  ekstrakt ($ spørring); / * SQL Velg * / // Whitelist of allowed fields $ allowed_fields = wptuts_get_log_table_columns (); if (is_array ($ fields)) // Konverter felt til små bokstaver (som våre kolonneavn er alle små bokstaver - se del 1) $ felt = array_map ('strtolower', $ felt); // Sanitize ved hvit notering $ felt = array_intersect ($ felt, $ allowed_fields);  ellers $ felt = strtolower ($ felt);  // Returner bare markerte felt. Tom tolkes som alle om (tomt ($ felt)) $ select_sql = "SELECT * FRA $ wpdb-> wptuts_activity_log";  elseif ('count' == $ felt) $ select_sql = "VELG COUNT (*) FRA $ wpdb-> wptuts_activity_log";  else $ select_sql = "SELECT" .implode (',', $ felt). "FRA $ wpdb-> wptuts_activity_log";  / * SQL Bli med * / // Vi trenger ikke dette, men vi tillater det å bli filtrert (se 'wptuts_logs_clauses') $ join_sql = "; / * SQL Hvor * / // Initialise WHERE $ where_sql = 'WHERE 1 = 1 ', hvis (! Tom ($ log_id)) $ where_sql. = $ Wpdb-> klargjør (' OG log_id =% d ', $ log_id) user_id for å være en matrise hvis (! is_array ($ user_id)) $ user_id = array ($ user_id); $ user_id = array_map ('absint', $ user_id); // Cast som positive heltall $ user_id__in = implode (',' , $ user_id); $ where_sql. = "OG user_id IN ($ user_id__in)"; $ siden = absint ($ siden); $ til = absint ($ til); = $ wpdb-> klargjør ('OG activity_date> =% s', date_i18n ('Ymd H: i: s', $ siden, sant)); > forberede ('og 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);  / * Utfør spørring * / $ logs = $ wpdb-> get_results ($ sql); / * Legg til i cache og filter * / wp_cache_add ($ cache_key, $ logs, 24 * 60 * 60); $ logs = apply_filters ('wptuts_get_logs', $ logs, $ query); returner $ logger; 

Det er en god del å gå i eksemplet ovenfor som jeg har forsøkt å inkludere flere funksjoner som kan vurderes når du utvikler wrapper-funksjonene, som vi dekker i de påfølgende avsnittene.

cache

Du kan vurdere at spørringene dine er tilstrekkelig komplekse, eller gjentatte ganger, at det er fornuftig å cache resultatene. Siden forskjellige søk returnerer forskjellige resultater, ønsker vi åpenbart ikke å bruke en generisk hurtigbuffert nøkkel - vi trenger en som er unik for den forespørselen. Dette er akkurat det som gjør følgende. Det serialiserer spørringen array, og deretter hashes den, produserer en nøkkel unik for $ query:

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

Deretter sjekker vi om vi har noe lagret for den cachenøkkelen. Hvis ja, flott, vi returnerer bare innholdet. Hvis ikke, genererer vi SQL, utfører spørringen og legger deretter resultatene til hurtigbufferen (i høyst 24 timer) og returnerer dem. Vi må huske at det kan ta opptil 24 timer å registrere i resultatene fra denne funksjonen. Vanligvis er det sammenhenger der hurtigbufferen slettes automatisk - men vi må implementere disse.

Filtre og handlinger

Kroker har blitt dekket omfattende på WPTuts + nylig av Tom McFarlin og Pippin Williamson. I sin artikkel snakker Pippin om årsakene til at du bør gjøre koden tilstrekkelig gjennom kroker og innpakninger som wptuts_get_logs () tjene som gode eksempler på hvor de kan brukes.

Vi har brukt to filtre i funksjonen ovenfor:

  • wptuts_get_logs - filtrerer resultatet av funksjonen
  • wptuts_logs_clauses - filtrerer en rekke SQL-komponenter

Dette tillater tredjepartsutviklere, eller til og med oss ​​selv, å bygge videre på den angitte APIen. Hvis vi unngår å bruke direkte SQL i plugin-modulen, og bare bruker disse innpakningsfunksjonene vi har bygget, gjør det umiddelbart mulig å utvide plugin-modulen. De wptuts_logs_clauses Spesielt filter ville tillate utviklere å endre hver del av SQL - og dermed utføre komplekse spørringer. Vi merker at det er jobben med en plugin-modul som bruker disse filtrene for å sikre at det de returnerer, er riktig sanitert.

Kroker er like nyttig når du utfører de tre andre viktigste operasjonene: innsetting, oppdatering og sletting av data. Handlinger tillater plugin-moduler å vite når disse blir utført - så de gjør noe. I vår sammenheng kan dette bety at du sender en e-post til en administrator når en bestemt bruker utfører en bestemt handling. Filtre, i sammenheng med disse operasjonene, er nyttige for å endre data før den er satt inn.

Vær forsiktig når du navngir kroker. Et godt krognavn gjør flere ting:

  • Kommuniserer når kroken kalles eller hva den gjør (for eksempel kan du gjette hva pre_get_posts og user_has_cap kan gjøre.
  • Være unik. Det anbefalte deg prefix kroker med plugin-navnet ditt. I motsetning til funksjoner, vil det ikke være en feil hvis det oppstår et sammenstøt mellom kroknavn - i stedet vil det nok bare "stille" bryte en eller flere plugin-moduler.
  • Viser en slags struktur. Lag krokene dine predicable, og unngå å navngi kroker "på flukt", da dette noen ganger kan føre til tilsynelatende tilfeldige kroknavn. I stedet planlegg så høyt som mulig kroker du vil bruke, og kom opp med en passende navngivningskonvensjon - og hold deg til den.
Tips: Generelt er det en god ide å etterligne de samme konvensjonene som WordPress - som utviklere vil raskere forstå hva den kroken gjør. Når det gjelder bruk av pluginens navn som et prefiks: Hvis plugin-navnet ditt er generisk, kan dette ikke være tilstrekkelig for å sikre unikthet. Til slutt, ikke gi en handling og et filter med samme navn.

Sletting av data

Sletting av data er ofte det enkleste av wrappers - selv om det kanskje kreves å utføre noen "clean up" -operasjoner, så vel som bare å fjerne dataene. wp_delete_post () for eksempel sletter ikke bare innlegget fra * _posts bordet, men også slett det aktuelle innlegget meta, taksonomi relasjoner, kommentarer og revisjoner osv.

I tråd med kommentarene i forrige seksjon, inkluderer vi to to handlinger: en utløst før og den andre etter at en logg er slettet fra tabellen. Følg WordPress 'navnekonvensjon for slike handlinger:

  • _delete_ utløses før sletting
  • _deleted_ utløses etter sletting
 / ** * Sletter en aktivitetslogg fra databasen * * @ param $ log_id int ID av aktivitetsloggen som skal slettes * @ return bool Om loggen ble slettet. * / funksjon wptuts_delete_log ($ log_id) global $ wpdb; // Log ID må være positivt heltall $ log_id = absint ($ log_id); hvis (tomt ($ log_id)) returnerer falsk; do_action ( 'wptuts_delete_log', $ log_id); $ sql = $ wpdb-> forberede ("DELETE fra $ wpdb-> wptuts_activity_log WHERE log_id =% d", $ log_id); hvis (! $ wpdb-> spørring ($ sql)) returnerer false; do_action ( 'wptuts_deleted_log', $ log_id); returnere sant; 

dokumentasjon

Jeg har vært litt lat med dokumentasjonen i kilden til APIen ovenfor. I denne serien forklarer Tom McFarlin hvorfor du ikke burde være. Du har kanskje brukt mye tid på å utvikle API-funksjonene, men hvis andre utviklere ikke vet hvordan de skal brukes, vil de ikke. Du vil også hjelpe deg selv, når du etter 6 måneder har glemt hvordan dataene skal gis, eller hva du bør forvente å bli returnert.


Sammendrag

Wrappers for databasetabellen kan variere fra det relativt enkle (f.eks. get_terms ()) til ekstremt kompleks (f.eks WP_Query klasse). Samlet bør de søke å tjene som inngangsporten til bordet ditt: slik at du kan fokusere på konteksten der de brukes, og i hovedsak glemme hva de egentlig gjør. APIen du oppretter, er bare et lite eksempel på begrepet "adskillelse av bekymringer", ofte tilskrives Edsger W. Dijkstra i hans papir om rollen som vitenskapelig tanke:

Det er det jeg noen ganger har kalt "separasjon av bekymringer", som, selv om det ikke er fullt mulig, er den eneste tilgjengelige teknikken for effektiv bestilling av ens tanker, som jeg vet om. Dette er hva jeg mener ved å "fokusere sin oppmerksomhet på et visst aspekt": det betyr ikke å ignorere de andre aspektene, det gjør bare rettferdighet til det faktum at den andre ikke er relevant for dette aspektet. Det er å være en- og flersporsynt samtidig.

Du kan finne koden som brukes i denne serien, i sin helhet, på GitHub. I neste del av denne serien ser vi på hvordan du kan vedlikeholde databasen, og håndtere oppgraderinger.