Dans la première partie de cette série, nous avons examiné les inconvénients de l’utilisation d’un tableau personnalisé. L’une des principales est l’absence d’API: dans cet article, nous verrons comment en créer une. L'API constitue une couche entre la manipulation des données dans votre plug-in et l'interaction réelle avec la table de la base de données. Elle est principalement destinée à garantir la sécurité de ces interactions et à fournir un wrapper "convivial pour l'homme" pour votre table. En tant que tel, nous aurons besoin de fonctions d'encapsuleur pour insérer, mettre à jour, supprimer et interroger des données..
Une API est recommandée pour plusieurs raisons, mais la plupart se résument à deux principes connexes: la réduction de la duplication de code et la séparation des problèmes..
Avec les quatre fonctions d'encapsulation mentionnées ci-dessus, vous devez uniquement vous assurer que vos requêtes de base de données sont en sécurité. quatre endroits - vous pouvez alors oublier complètement la désinfection. Une fois que vous êtes certain que vos fonctions d'encapsulation gèrent la base de données en toute sécurité, vous n'avez plus à vous soucier des données que vous leur fournissez. Vous pouvez également valider les données - renvoyer une erreur si quelque chose ne va pas.
L'idée est que, sans ces fonctions, vous devrez vous assurer que chaque instance d'interaction avec votre base de données le fait en toute sécurité. Cela accroît simplement la probabilité que dans l'un de ces cas, vous manquiez quelque chose et que vous créiez une vulnérabilité dans votre plug-in..
Ceci est lié au premier point (et les deux sont liés à la duplication de code). En dupliquant le code, les bogues sont plus facilement explorables. Inversement, en utilisant des fonctions d'encapsulation, s'il existe un bogue lors de la mise à jour ou de l'interrogation de la table de la base de données, vous savez exactement où chercher..
Cela peut sembler une raison "logicielle" - mais la lisibilité du code est extrêmement importante. La lisibilité consiste à clarifier la logique et les actions du code pour le lecteur. Ce n’est pas seulement important lorsque vous travaillez en équipe, ou que quelqu'un hérite de votre travail: vous savez peut-être ce que votre code est censé faire maintenant, mais dans six mois, vous aurez probablement oublié. Et si votre code est difficile à suivre, il est plus facile d'introduire un bogue.
Les fonctions d'encapsulation nettoient votre code en séparant littéralement le fonctionnement interne de certaines opérations (par exemple, la création d'une publication) du contexte de cette opération (par exemple, la gestion d'une soumission de formulaire). Imaginez avoir le contenu entier de wp_insert_post ()
à la place de chaque instance que vous utilisez wp_insert_post ()
.
Ajouter des couches d'abstraction n'est pas toujours une bonne chose, mais c'est sans aucun doute le cas. Non seulement ces wrappers fournissent un moyen convivial de mettre à jour ou d'interroger la table (imaginez avoir à utiliser SQL pour interroger des publications plutôt que d'utiliser la méthode beaucoup plus concise WP_Query ()
- ainsi que toute la formulation et la désinfection SQL qui va avec), mais vous aide également à vous protéger, ainsi que les autres développeurs, des modifications apportées à la structure de la base de données sous-jacente..
En utilisant les fonctions d'encapsulation, vous, ainsi que des tiers, pouvez les utiliser sans craindre de ne pas être sûrs ou de ne pas les endommager. Si vous décidez de renommer une colonne, de déplacer une colonne ailleurs ou même de la supprimer, vous pouvez être sûr que le reste de votre plug-in ne se cassera pas, car vous apportez simplement les modifications nécessaires à vos fonctions d'encapsulation. (Incidemment, c’est une raison impérieuse d’éviter les requêtes SQL directes des tables WordPress: si elles changent, et elles le feront, volonté casser votre plug-in). D'autre part, une API aide votre plug-in à être étendu de manière stable.
Je suis peut-être coupable d'avoir scindé un point en deux, mais j'estime qu'il s'agit d'un avantage important. Il y a peu de pire que l'incohérence lors du développement de plug-ins: cela ne fait qu'encourager le code désordonné. Les fonctions wrapper fournissent une interaction cohérente avec la base de données: vous fournissez des données et renvoie true (ou un ID) ou false (ou un WP_Error
objet, si vous préférez).
J'espère que j'ai maintenant convaincu de la nécessité d'une API pour votre table. Mais avant d’aller plus loin, nous allons d’abord définir une fonction d’aide qui facilitera un peu la désinfection..
Nous définirons une fonction qui renvoie les colonnes de la table avec le format de données attendu. Ce faisant, nous pouvons facilement ajouter à la liste blanche les colonnes autorisées et formater l’entrée en conséquence. De plus, si nous apportons des modifications aux colonnes, nous devons simplement les apporter ici.
function wptuts_get_log_table_columns () return array ('log_id' => '% d', 'user_id' => '% d', 'activity' => '% s', 'object_id' => '% d', 'object_type '=>'% s ',' date_activité '=>'% s ',);
La fonction d'encapsuleur 'insert' la plus élémentaire prend simplement un tableau de paires colonne-valeur et les insère dans la base de données. Ce n'est pas nécessairement le cas: vous pouvez décider de fournir davantage de clés "conviviales pour l'homme" que vous mapperez ensuite aux noms des colonnes. Vous pouvez également décider que certaines valeurs sont générées automatiquement ou sont remplacées en fonction des valeurs transmises (par exemple: post status in wp_insert_post ()
).
Ce sont peut-être les * valeurs * qui nécessitent une cartographie. Le format dans lequel les données sont mieux stockées n’est pas toujours le format le plus pratique à utiliser. Par exemple, pour les dates, il peut être plus facile de gérer un objet DateTime ou un horodatage, puis de le convertir au format de date souhaité..
La fonction wrapper peut être simple ou compliquée - mais le minimum qu'elle devrait faire est de désinfecter l'entrée. Je recommanderais également la liste blanche des colonnes reconnues, car essayer d'insérer des données dans une colonne inexistante peut générer une erreur..
Dans cet exemple, l'ID utilisateur est par défaut celui de l'utilisateur actuel et tous les champs sont spécifiés par leur nom de colonne, exception de la date d'activité passée en tant que "date". La date, dans cet exemple, doit être un horodatage local, qui est converti avant de l'ajouter à la base de données..
/ ** * Insère un journal dans la base de données * * @ @ param $ data array Un tableau de paires clé => valeur à insérer * @ @ return int ID de journal du journal d'activité créé. Ou WP_Error ou false en cas d'échec. * / function wptuts_insert_log ($ data = array ()) global $ wpdb; // Définition des valeurs par défaut $ data = wp_parse_args ($ data, array ('user_id' => get_current_user_id (), 'date' => current_time ('timestamp'),)); // Vérifier la validité de la date si (! 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;Pointe: C'est aussi une bonne idée de vérifier la validité des données. Les vérifications que vous devez effectuer et le comportement de l'API dépendent entièrement de votre contexte..
wp_insert_post ()
, par exemple, il faut un certain degré d'unicité pour publier des slugs - en cas de conflit, il en génère automatiquement un unique.. wp_insert_term
d'autre part renvoie une erreur si le terme existe déjà. Ceci est dû à un mélange entre la façon dont WordPress traite ces objets et la sémantique. La mise à jour des données imite généralement l’insertion de données, à l’exception du fait qu’un identifiant de ligne (généralement la clé primaire) est fourni avec les données à mettre à jour. En général, les arguments doivent correspondre à la fonction insert (pour des raisons de cohérence). Dans cet exemple, "date" est utilisé à la place de "date_activité".
/ ** * Met à jour un journal d'activité avec les données fournies * * @ @ param $ log_id int ID du journal d'activité à mettre à jour * @ param $ data array Un tableau de colonnes => à valeur à mettre à jour * @ return bool Indique si le journal a été mis à jour avec succès. * / function wptuts_update_log ($ log_id, $ data = array ()) global $ wpdb; // L'ID de journal doit être un entier positif. $ Log_id = absint ($ log_id); if (empty ($ log_id)) renvoie false; // Convertit la date d'activité de l'horodatage local au format GMT mysql si (isset ($ data ['date_activité'])) $ data ['date_activité'] = date_i18n ('Ymd H: i: s', $ data ['date' ], vrai ); // Initialise le tableau de format de colonne $ column_formats = wptuts_get_log_table_columns (); // Forcer les champs en minuscule $ data = array_change_key_case ($ data); // Colonnes de la liste blanche $ data = array_intersect_key ($ data, $ column_formats); // Réordonne les formats $ column_formats pour qu'ils correspondent à l'ordre des colonnes indiqué dans $ 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;
Une fonction de wrapper pour interroger des données sera souvent assez compliquée - d’autant plus que vous souhaiterez peut-être prendre en charge tous les types de requêtes ne sélectionnant que certains champs, restreignant les instructions AND ou OR, triant l’une des colonnes possibles, etc. WP_Query
classe).
Le principe de base de la fonction d'encapsulation pour interroger les données est le suivant: devrait-il prendre un «tableau d'interrogation», l'interpréter et former l'instruction SQL correspondante.
/ ** * Récupère les journaux d'activité de la base de données correspondant à la requête $. * $ query est un tableau qui peut contenir les clés suivantes: * * 'fields' - un tableau de colonnes à inclure dans les rôles retournés. Ou 'compter' pour compter les lignes. Par défaut: vide (tous les champs). * 'orderby' - date-heure, id_utilisateur ou id_log. Par défaut: date / heure. * 'order' - asc ou desc * 'user_id' - l'ID utilisateur auquel correspondre, ou un tableau d'identifiants * * 'depuis' - horodatage. Ne retourne que les activités après cette date. Par défaut false, aucune restriction. * 'jusqu'à' - horodatage. Ne retourne que les activités jusqu'à cette date. Par défaut false, aucune restriction. * * @ param $ query Tableau de requête * @ tableau de retour Tableau de journaux correspondants. Faux en cas d'erreur. * / function wptuts_get_logs ($ query = array ()) global $ wpdb; / * Analyse par défaut * / $ defaults = array ('champs' => array (), 'orderby' => 'datetime', 'order' => 'desc', 'user_id' => false, 'depuis' => false, 'jusqu'à' => false, 'nombre' => 10, 'offset' => 0); $ query = wp_parse_args ($ query, $ defaults); / * Forme une clé de cache à partir de la requête * / $ 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; extraire ($ requête); / * Sélection SQL * / // Liste blanche des champs autorisés $ allowed_fields = wptuts_get_log_table_columns (); if (is_array ($ fields)) // Convertissez les champs en minuscules (nos noms de colonnes sont tous en minuscules - voir la partie 1) $ fields = array_map ('strtolower', $ fields); // Désinfecte par liste blanche $ fields = array_intersect ($ fields, $ allowed_fields); else $ fields = strtolower ($ fields); // Renvoie uniquement les champs sélectionnés. Vide est interprété comme tout si (vide (champs $)) $ 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 (champs ',', $). "FROM $ wpdb-> wptuts_activity_log"; / * SQL Join * / // Nous n’avons pas besoin de cela, mais nous permettons qu’il soit filtré (voir 'wptuts_logs_clauses') $ join_sql = "; / * SQL Où * / // 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)) // Force $ id_utilisateur devant être un tableau if (! is_array ($ id_utilisateur)) $ id_utilisateur = array ($ id_utilisateur); $ id_utilisateur = array_map ('absint', $ id_utilisateur); // Transforme en entiers positifs $ id_utilisateur__in = implode (',' , $ user_id); $ where_sql. = "AND user_id IN ($ user_id__in)"; $ Since = absint ($ de); $ Until = absint ($ Jusqu'à); if (! empty ($ depuis)) $ Where_sql. = $ wpdb-> prepare ('AND date_activité> =% s', date_i18n ('Ymd H: i: s', $ depuis, vrai)); if (! vide ($ jusqu'à)) $ where_sql. = $ wpdb- > prepare ('AND date_activité <= %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); / * Effectue une requête * / $ logs = $ wpdb-> get_results ($ sql); / * Ajouter au cache et filtrer * / wp_cache_add ($ cache_key, $ logs, 24 * 60 * 60); $ logs = apply_filters ('wptuts_get_logs', $ logs, $ query); retourne $ logs;
L'exemple ci-dessus présente de nombreux problèmes, car j'ai essayé d'inclure plusieurs fonctionnalités pouvant être prises en compte lors du développement de vos fonctions d'encapsulation, que nous aborderons dans les sections suivantes..
Vous pouvez considérer que vos requêtes sont suffisamment complexes ou répétées régulièrement pour qu'il soit logique de mettre en cache les résultats. Étant donné que différentes requêtes donnent des résultats différents, nous ne souhaitons évidemment pas utiliser une clé de cache générique - nous en avons besoin d'une qui est unique pour cette requête. C'est exactement ce que fait la suite. Il sérialise le tableau de requêtes, puis le hache, produisant une clé unique pour $ requête
:
$ cache_key = 'wptuts_logs:'. md5 (serialize ($ query));
Nous vérifions ensuite si nous avons quelque chose de stocké pour cette clé de cache. Si c'est le cas, tant mieux, nous renvoyons simplement son contenu. Sinon, nous générons le code SQL, exécutons la requête, puis ajoutons les résultats au cache (pendant au plus 24 heures) et les renvoyons. Nous devons nous rappeler que les enregistrements peuvent prendre jusqu'à 24 heures pour apparaître dans les résultats de cette fonction. Il existe généralement des contextes dans lesquels le cache est automatiquement effacé - mais nous aurions besoin de les implémenter..
Les crochets ont été largement abordés sur WPTuts + récemment par Tom McFarlin et Pippin Williamson. Dans son article, Pippin explique les raisons pour lesquelles vous devriez rendre votre code extensible via des hooks et des wrappers tels que wptuts_get_logs ()
servir d'excellents exemples de l'endroit où ils peuvent être utilisés.
Nous avons utilisé deux filtres dans la fonction ci-dessus:
wptuts_get_logs
- filtre le résultat de la fonctionwptuts_logs_clauses
- filtre un tableau de composants SQL Cela permet aux développeurs tiers, voire à nous-mêmes, de s'appuyer sur l'API fournie. Si nous évitons d'utiliser le code SQL direct dans notre plug-in et n'utilisons que les fonctions d'encapsuleur que nous avons créées, cela permet immédiatement d'étendre notre plug-in. le wptuts_logs_clauses
Un filtre en particulier permettrait aux développeurs de modifier chaque partie du code SQL - et donc d’exécuter des requêtes complexes. Nous noterons que c'est le travail de tout plug-in utilisant ces filtres pour s'assurer que ce qu'ils retournent est correctement filtré..
Les crochets sont tout aussi utiles lorsque vous effectuez les trois autres «opérations» principales: insérer, mettre à jour et supprimer des données. Les actions permettent aux plug-ins de savoir quand ils sont exécutés, donc ils ont une action à prendre. Dans notre contexte, cela peut vouloir dire envoyer un courrier électronique à un administrateur lorsqu'un utilisateur particulier effectue une action particulière. Les filtres, dans le contexte de ces opérations, sont utiles pour modifier des données avant leur insertion..
Soyez prudent lorsque vous nommez les crochets. Un bon nom de crochet fait plusieurs choses:
pre_get_posts
et user_has_cap
pourrait faire.La suppression de données est souvent le plus simple des wrappers - bien qu'il soit nécessaire d'effectuer certaines opérations de «nettoyage» ainsi que de simplement supprimer les données.. wp_delete_post ()
par exemple, non seulement supprime la publication de la *_des postes
table, mais supprime également les méta-publications, relations de taxonomie, commentaires et révisions appropriés, etc..
Conformément aux commentaires de la section précédente, nous allons inclure deux actions: une déclenchée avant et l’autre après la suppression d’un journal de la table. En suivant la convention de nommage de WordPress pour de telles actions:
_effacer_
est déclenché avant la suppression_deleted_
est déclenché après la suppression / ** * Supprime un journal d'activité de la base de données * * @ param $ log_id int ID du journal d'activité à supprimer * @ return bool Indique si le journal a été supprimé avec succès. * / function wptuts_delete_log ($ log_id) global $ wpdb; // L'ID de journal doit être un entier positif. $ Log_id = absint ($ log_id); if (empty ($ log_id)) renvoie false; do_action ('wptuts_delete_log', $ log_id); $ sql = $ wpdb-> prepare ("DELETE de $ wpdb-> wptuts_activity_log WHERE log_id =% d", $ log_id); if (! $ wpdb-> query ($ sql)) renvoie false; do_action ('wptuts_deleted_log', $ log_id); retourne vrai;
J'ai été un peu paresseux avec la documentation en source de l'API ci-dessus. Dans cette série, Tom McFarlin explique pourquoi vous ne devriez pas l'être. Vous avez peut-être passé beaucoup de temps à développer vos fonctions d'API, mais si d'autres développeurs ne savent pas comment les utiliser, ils ne le sauront pas. Vous vous aiderez également quand, après 6 mois, vous aurez oublié comment vous devez donner les données ou ce à quoi vous devriez vous attendre..
Les wrappers pour votre table de base de données peuvent aller de la relativement simple (par exemple. get_terms ()
) à extrêmement complexe (par exemple le WP_Query
classe). Collectivement, ils devraient chercher à servir de passerelle vers votre table: vous permettre de vous concentrer sur le contexte dans lequel ils sont utilisés et essentiellement d'oublier ce qu'ils font réellement. L'API que vous créez n'est qu'un petit exemple de la notion de «séparation des préoccupations», souvent attribuée à Edsger W. Dijkstra dans son article Sur le rôle de la pensée scientifique:
C’est ce que j’ai parfois appelé "la séparation des préoccupations", qui, même si cela n’est pas parfaitement possible, est pourtant la seule technique disponible pour bien ordonner ses pensées, à ma connaissance. C’est ce que j’entends par "attirer son attention sur un aspect": cela ne signifie pas ignorer les autres aspects, mais rendre justice au fait que, du point de vue de cet aspect, l’autre n’a aucune pertinence. C’est être à la fois sur une et plusieurs pistes.
Vous pouvez trouver le code utilisé dans cette série, dans son intégralité, sur GitHub. Dans la prochaine partie de cette série, nous verrons comment vous pouvez gérer votre base de données et gérer les mises à niveau..