In het eerste deel van deze serie keken we naar de nadelen van het gebruik van een aangepaste tabel. Een van de belangrijkste is het ontbreken van een API: dus in dit artikel zullen we kijken hoe we er een kunnen maken. De API vormt een laag tussen het verwerken van gegevens in uw plug-in en de daadwerkelijke interactie met de databasetabel - en is primair bedoeld om ervoor te zorgen dat dergelijke interacties veilig zijn en om een 'mensvriendelijke' verpakking voor uw tafel te bieden. Als zodanig hebben we wrapper-functies nodig voor het invoegen, bijwerken, verwijderen en opvragen van gegevens.
Er zijn verschillende redenen waarom een API wordt aanbevolen, maar de meeste komen neer op twee gerelateerde principes: vermindering van codeduplicatie en scheiding van punten van zorg.
Met de bovengenoemde vier genoemde wrapper-functies hoeft u alleen te zorgen dat uw databasequery's veilig zijn vier plaatsen - u kunt de ontsmetting dan volledig vergeten. Als u eenmaal zeker weet dat uw wrapper-functies de database veilig behandelen, hoeft u zich geen zorgen te maken over de gegevens die u ze geeft. Je kan ook bevestigen de gegevens - een foutmelding als iets niet klopt.
Het idee is dat zonder deze functie u ervoor moet zorgen dat elk exemplaar van de interactie met uw database zo veilig werkt. Dit brengt alleen maar een verhoogde kans met zich mee dat u in een van deze gevallen iets mist en een kwetsbaarheid in uw invoegtoepassing creëert.
Dit is gerelateerd aan het eerste punt (en beide hebben betrekking op codeduplicatie). Door code te dupliceren is er meer ruimte voor bugs om in te kruipen. Omgekeerd, door een wrapper-functie te gebruiken - als er een bug is bij het bijwerken of opvragen van de databasetabel - weet je precies waar je moet zoeken.
Dit lijkt misschien een 'zachte' reden, maar de leesbaarheid van code is ongelooflijk belangrijk. Leesbaarheid gaat over het duidelijk maken van de logica en acties van de code voor de lezer. Dit is niet alleen belangrijk als u werkt als onderdeel van een team of als iemand uw werk erven: u weet mogelijk wat uw code nu moet doen, maar na zes maanden bent u het waarschijnlijk vergeten. En als uw code moeilijk te volgen is, is het gemakkelijker om een bug te introduceren.
Wrapper-functies opschonen uw code door de interne werking van een bewerking (bijvoorbeeld een post maken) letterlijk te scheiden van de context van die bewerking (bijvoorbeeld het verwerken van een formulierinzending). Stel je voor dat je de volledige inhoud van hebt wp_insert_post ()
in plaats van elke instantie die u gebruikt wp_insert_post ()
.
Het toevoegen van lagen van abstractie is niet altijd een goede zaak, maar hier is het ongetwijfeld. Deze wrappers bieden niet alleen een mensvriendelijke manier om de tabel bij te werken of te ondervragen (stel je voor dat je SQL moet gebruiken om berichten te zoeken in plaats van het veel beknopter te gebruiken WP_Query ()
- en alle SQL-formuleringen en sanitaties die daarbij horen), maar helpt ook jou en andere ontwikkelaars beschermen tegen wijzigingen in de onderliggende databasestructuur.
Door wrapper-functies te gebruiken, kunt u deze evenals deze gebruiken zonder bang te zijn dat ze onveilig zijn of breken. Als u besluit een kolom een andere naam te geven, een kolom ergens anders te plaatsen of deze te verwijderen, kunt u er zeker van zijn dat de rest van uw plug-in niet kapot gaat, omdat u gewoon de nodige wijzigingen in uw wrapper-functies aanbrengt. (Overigens is dit een dwingende reden om directe SQL-query's van WordPress-tabellen te vermijden: als ze veranderen, en zij zullen zullen verbreek je plug-in.). Aan de andere kant helpt een API je plug-in op een stabiele manier te vergroten.
Ik ben misschien schuldig aan het splitsen van één punt in twee - maar ik denk dat dit een belangrijk voordeel is. Er is weinig erger dan inconsistentie bij het ontwikkelen van plug-ins: het moedigt gewoon rommelige code aan. Wrapper-functies zorgen voor een consistente interactie met de database: u levert gegevens en retourneert true (of een ID) of false (of een WP_Error
object, als je dat liever hebt).
Hopelijk ben ik nu overtuigd van de noodzaak van een API voor je tafel. Maar voordat we verder gaan, zullen we eerst een hulpfunctie definiëren die sanitatie een beetje gemakkelijk zal maken.
We zullen een functie definiëren die de kolommen met tabellen retourneert, samen met het gegevensformaat dat ze verwachten. Door dit te doen kunnen we eenvoudig toegestane kolommen toewijzen en de invoer dienovereenkomstig formatteren. Verder moeten we de wijzigingen hier aanbrengen als we de kolommen aanpassen
functie wptuts_get_log_table_columns () return array ('log_id' => '% d', 'user_id' => '% d', 'activity' => '% s', 'object_id' => '% d', 'object_type '=>'% s ',' activity_date '=>'% s ',);
De meest eenvoudige 'insert'-wrapper-functie neemt gewoon een array van kolomwaardeparen en voegt deze in de database in. Dit hoeft niet het geval te zijn: u kunt besluiten om meer 'mensvriendelijke' sleutels aan te bieden, die u vervolgens toewijst aan de kolomnamen. U kunt ook besluiten dat sommige waarden automatisch worden gegenereerd of overschreven op basis van de doorgegeven waarden (bijvoorbeeld: poststatus in wp_insert_post ()
).
Het zijn misschien de * waarden * die moeten worden toegewezen. Het formaat waarin gegevens het best kunnen worden opgeslagen, is niet altijd het meest geschikte formaat om te gebruiken. Voor datums kan het bijvoorbeeld gemakkelijker zijn om een DateTime-object of een tijdstempel te verwerken - en dit vervolgens converteren naar het gewenste datumnotatie.
De wrapper-functie kan eenvoudig of gecompliceerd zijn, maar het minste wat het moet doen is de invoer zuiveren. Ik zou ook whitelisting aanbevelen voor de herkende kolommen, aangezien het proberen om gegevens in te voegen in een kolom die niet bestaat, een fout kan veroorzaken.
In dit voorbeeld is de gebruikers-ID standaard die van de huidige gebruiker en worden alle velden aangeduid met hun kolomnaam - de uitzondering van de activiteitsdatum die is doorgegeven als 'datum'. De datum, in dit voorbeeld, moet een lokale tijdstempel zijn, die wordt geconverteerd voordat deze aan de database wordt toegevoegd.
/ ** * Voegt een log in de database in * * @ param $ data array Een array van sleutel => waardeparen die moeten worden ingevoegd * @ return int De log-ID van het gemaakte activiteitenlogboek. Of WP_Error of false bij een fout. * / function wptuts_insert_log ($ data = array ()) global $ wpdb; // Stel standaardwaarden in $ data = wp_parse_args ($ data, array ('user_id' => get_current_user_id (), 'date' => current_time ('timestamp'),)); // Controleer de geldigheid van de datum als (! 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;Tip: Het is ook een goed idee om de geldigheid van de gegevens te controleren. Welke controles u moet uitvoeren en hoe de API reageert, hangt volledig af van uw context.
wp_insert_post ()
, vereist bijvoorbeeld een zekere mate van uniciteit om slakken te plaatsen - als er botsingen zijn, genereert deze automatisch een unieke. wp_insert_term
aan de andere kant retourneert een fout als de term al bestaat. Dit komt door een mix tussen hoe WordPress omgaat met deze objecten en semantiek. Het bijwerken van gegevens lijkt meestal nauw samen met het invoegen van gegevens, met de uitzondering dat een rij-ID (meestal alleen de primaire sleutel) wordt verstrekt, samen met gegevens die moeten worden bijgewerkt. Over het algemeen moeten de argumenten overeenkomen met de invoegfunctie (voor consistentie) - dus in dit voorbeeld wordt 'datum' gebruikt in plaats van 'activiteitsdatum'
/ ** * Werkt een activiteitenlogboek bij met de bijgeleverde gegevens * * @ param $ log_id int ID van het activiteitenlogboek dat moet worden bijgewerkt * @ param $ data array Een kolomkolom => waardeparen die moeten worden bijgewerkt * @ return bool Of het logboek wordt weergegeven is succesvol bijgewerkt. * / function wptuts_update_log ($ log_id, $ data = array ()) global $ wpdb; // Log-ID moet positief integer $ log_id = absint ($ log_id) zijn; als (empty ($ log_id)) false retourneert; // Converteer activiteitsdatum van lokale tijdstempel naar GMT mysql-indeling if (isset ($ data ['activity_date'])) $ data ['activity_date'] = date_i18n ('Ymd H: i: s', $ data ['date' ], waar); // Initialiseer kolomindeling array $ column_formats = wptuts_get_log_table_columns (); // Forceer velden naar kleine letters $ data = array_change_key_case ($ data); // Witte lijstkolommen $ data = array_intersect_key ($ data, $ column_formats); // Sorteer $ column_formats opnieuw om overeen te komen met de volgorde van de kolommen gegeven 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;
Een wrapper-functie voor het opvragen van gegevens is vaak vrij ingewikkeld - vooral omdat u mogelijk alle typen query's wilt ondersteunen die alleen bepaalde velden selecteren, beperken door AND- of OR-instructies, bestellen op een van de verschillende mogelijke kolommen, enz. (Zie de WP_Query
klasse).
Het basisprincipe van de wrapper-functie voor het opvragen van gegevens is dat het een 'query-array' moet zijn, deze moet interpreteren en de bijbehorende SQL-instructie moet vormen.
/ ** * Hiermee haalt u activiteitenlogboeken op uit de overeenkomende $ -query van de database. * $ query is een array die de volgende sleutels kan bevatten: * * 'velden' - een reeks kolommen die moeten worden opgenomen in geretourneerde rollen. Of 'tel' om rijen te tellen. Standaard: leeg (alle velden). * 'orderby' - datetime, user_id of log_id. Standaard: datetime. * 'order' - asc of desc * 'user_id' - overeenkomende gebruikers-ID, of een reeks gebruikers-ID's * 'since' - timestamp. Retourneer alleen activiteiten na deze datum. Standaard false, geen beperking. * 'tot' - tijdstempel. Retourneer alleen activiteiten tot deze datum. Standaard false, geen beperking. * * @ param $ query Query array * @ return array Array van overeenkomende logboeken. Fout bij fout. * / functie 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, $ standaard); / * Vorm een cachesleutel uit de 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); return $ cache; extract ($ query); / * SQL Select * / // Witte lijst van toegestane velden $ allowed_fields = wptuts_get_log_table_columns (); if (is_array ($ velden)) // Converteer velden naar kleine letters (onze kolomnamen zijn allemaal kleine letters - zie deel 1) $ fields = array_map ('strtolower', $ fields); // Sanitize by white list $ fields = array_intersect ($ fields, $ allowed_fields); else $ fields = strtolower ($ fields); // Alleen geselecteerde velden retourneren. Leeg wordt geïnterpreteerd als alles als (leeg ($ velden)) $ select_sql = "SELECT * FROM $ wpdb-> wptuts_activity_log"; elseif ('count' == $ velden) $ select_sql = "SELECTEER COUNT (*) VAN $ wpdb-> wptuts_activity_log"; else $ select_sql = "SELECT" .implode (',', $ velden). "FROM $ wpdb-> wptuts_activity_log"; / * SQL Join * / // Dit hebben we niet nodig, maar we laten het filteren (zie 'wptuts_logs_clauses') $ join_sql = "; / * SQL Where * / // Initialise WHERE $ where_sql = 'WHERE 1 = 1 '; if (! Empty ($ log_id)) $ where_sql. = $ Wpdb-> prepare (' AND log_id =% d ', $ log_id); if (! Empty ($ user_id)) // Forceer $ user_id wordt een array als (! is_array ($ user_id)) $ user_id = array ($ user_id); $ user_id = array_map ('absint', $ user_id); // Cast als positieve gehele getallen $ 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- > voorbereiden ('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); / * Voer query * / $ logs = $ wpdb-> get_results ($ sql) uit; / * Toevoegen aan cache en filter * / wp_cache_add ($ cache_key, $ logs, 24 * 60 * 60); $ logs = apply_filters ('wptuts_get_logs', $ logs, $ query); $ logs teruggeven;
In het bovenstaande voorbeeld is nogal wat veranderd, omdat ik heb geprobeerd om verschillende functies te gebruiken die mogelijk worden overwogen bij het ontwikkelen van uw wrapperfuncties, die we in de volgende secties bespreken.
U kunt uw vragen beschouwen als voldoende complex of regelmatig herhaald, dat het zinvol is om de resultaten in de cache op te slaan. Omdat verschillende zoekopdrachten verschillende resultaten opleveren, willen we natuurlijk geen generieke cachesleutel gebruiken - we hebben er een nodig die uniek is voor die zoekopdracht. Dit is precies wat het volgende doet. Het serialiseert de query-array en hashes het, waardoor een unieke sleutel ontstaat $ vraag
:
$ cache_key = 'wptuts_logs:'. md5 (serialize ($ query));
Vervolgens controleren we of we iets hebben opgeslagen voor die cachesleutel - zo ja, goed, we geven gewoon de inhoud ervan terug. Als dat niet het geval is, genereren we de SQL, voeren we de query uit en voegen we de resultaten toe aan de cache (maximaal 24 uur) en retourneren deze. We moeten onthouden dat records tot 24 uur kunnen duren om in de resultaten van deze functie te verschijnen. Meestal zijn er contexten waar de cache automatisch wordt gewist, maar we zouden deze moeten implementeren.
Hooks zijn uitgebreid behandeld op WPTuts + recentelijk door Tom McFarlin en Pippin Williamson. In zijn artikel vertelt Pepijn over de redenen waarom je je code uitbreidt met haken en omslagen zoals wptuts_get_logs ()
dienen als uitstekende voorbeelden van waar ze kunnen worden gebruikt.
We hebben twee filters in de bovenstaande functie gebruikt:
wptuts_get_logs
- filtert het resultaat van de functiewptuts_logs_clauses
- filtert een array van SQL-componenten Hiermee kunnen externe ontwikkelaars, of zelfs wij, voortbouwen op de geleverde API. Als we directe SQL in onze plug-in vermijden en alleen de wrapper-functies gebruiken die we hebben gebouwd, dan is het onmiddellijk mogelijk om onze plug-in uit te breiden. De wptuts_logs_clauses
filter in het bijzonder zou het ontwikkelaars mogelijk maken om elk deel van de SQL te veranderen - en aldus complexe query's uit te voeren. We merken op dat het de taak is van elke plug-in die deze filters gebruikt om ervoor te zorgen dat wat ze retourneren, naar behoren is opgeschoond.
Haken zijn net zo handig bij het uitvoeren van de andere drie 'hoofdoperaties': invoegen, bijwerken en verwijderen van gegevens. Met acties kunnen invoegtoepassingen weten wanneer deze worden uitgevoerd, dus ze hebben wat actie. In onze context kan dit betekenen dat een e-mail naar een beheerder moet worden verzonden wanneer een bepaalde gebruiker een bepaalde actie uitvoert. Filters, in de context van deze bewerkingen, zijn handig voor het wijzigen van gegevens voordat deze worden ingevoegd.
Wees voorzichtig bij het benoemen van haken. Een goede haaknaam doet verschillende dingen:
pre_get_posts
en user_has_cap
zou kunnen doen.Het verwijderen van gegevens is vaak de eenvoudigste van de wrappers - hoewel het misschien nodig is om sommige 'opruimacties' uit te voeren en eenvoudigweg de gegevens te verwijderen. wp_delete_post ()
bijvoorbeeld, verwijdert niet alleen de post van de * _posts
tabel maar verwijdert ook de juiste postmeta, taxonomierelaties, opmerkingen en revisies enz.
In overeenstemming met de opmerkingen van de vorige sectie, zullen we twee twee acties opnemen: de ene geactiveerd voor en de andere nadat een log is verwijderd uit de tabel. Het volgen van de naamgevingsconventie van WordPress voor dergelijke acties:
_delete_
wordt geactiveerd vóór verwijdering_deleted_
wordt geactiveerd na verwijdering / ** * Verwijdert een activiteitenlogboek uit de database * * @ param $ log_id int ID van het activiteitenlogboek dat moet worden verwijderd * @ return bool Of het logboek met succes is verwijderd. * / function wptuts_delete_log ($ log_id) global $ wpdb; // Log-ID moet positief integer $ log_id = absint ($ log_id) zijn; als (empty ($ log_id)) false retourneert; do_action ( 'wptuts_delete_log', $ log_id); $ sql = $ wpdb-> prepare ("DELETE from $ wpdb-> wptuts_activity_log WHERE log_id =% d", $ log_id); als (! $ wpdb-> query ($ sql)) false retourneert; do_action ( 'wptuts_deleted_log', $ log_id); geef waar terug;
Ik ben een beetje lui geweest met de in-source documentatie van de bovenstaande API. In deze serie legt Tom McFarlin uit waarom je dat niet zou moeten zijn. Je hebt misschien veel tijd besteed aan het ontwikkelen van je API-functies, maar als andere ontwikkelaars niet weten hoe ze ze moeten gebruiken, doen ze dat niet. Je zult ook jezelf helpen, wanneer je na 6 maanden bent vergeten hoe de gegevens moeten worden gegeven, of wat je zou verwachten te worden teruggestuurd.
Wrappers voor uw databasetabel kunnen variëren van relatief eenvoudig (bijv. get_terms ()
) tot extreem complex (bijvoorbeeld de WP_Query
klasse). Gezamenlijk zouden ze moeten proberen om als toegangspoort tot je tafel te dienen: zodat je je kunt concentreren op de context waarin ze worden gebruikt, en in wezen vergeet wat ze feitelijk doen. De API die u maakt is slechts een klein voorbeeld van het begrip 'scheiding van punten van zorg', vaak toegeschreven aan Edsger W. Dijkstra in zijn paper over de rol van wetenschappelijk denken:
Het is wat ik soms "de scheiding van zorgen" heb genoemd, wat, hoewel niet perfect mogelijk, de enige beschikbare techniek is voor het effectief ordenen van iemands gedachten, die ik ken. Dit is wat ik bedoel met "de aandacht richten op een bepaald aspect": het betekent niet dat je de andere aspecten negeert, het doet gewoon recht aan het feit dat vanuit het oogpunt van dit aspect, de andere niet relevant is. Het is tegelijkertijd één- en meervoudig gericht denken.
Je kunt de code die in deze serie wordt gebruikt, in zijn geheel, vinden op GitHub. In het volgende deel van deze serie zullen we bekijken hoe u uw database kunt onderhouden en upgrades kunt uitvoeren.