Tablas de base de datos personalizadas Creando una API

En la primera parte de esta serie analizamos las desventajas de usar una tabla personalizada. Uno de los principales es la falta de una API: por lo que en este artículo veremos cómo crear una. La API actúa como una capa entre el manejo de los datos en su complemento y la interacción real con la tabla de la base de datos, y su objetivo principal es garantizar que dichas interacciones sean seguras y proporcionar un envoltorio "amigable" para su mesa. Como tal, necesitaremos funciones de envoltura para insertar, actualizar, eliminar y consultar datos.


¿Por qué debería crear una API??

Hay varias razones por las que se recomienda una API, pero la mayoría se reduce a dos principios relacionados: reducir la duplicación de códigos y la separación de inquietudes.

Es más seguro

Con las cuatro funciones de envoltura mencionadas anteriormente, solo necesita asegurarse de que sus consultas de base de datos estén seguras cuatro Lugares - entonces puedes olvidarte completamente de la desinfección. Una vez que esté seguro de que sus funciones de envoltorio manejan la base de datos de manera segura, entonces no necesita preocuparse por los datos que les está proporcionando. Tú también puedes validar los datos - devolver un error si algo no está bien.

La idea es que sin estas funciones, deberá asegurarse de que cada instancia de interacción con su base de datos lo haga de manera segura. Esto solo aumenta la probabilidad de que en uno de estos casos se pierda algo y se cree una vulnerabilidad en su complemento..

Reduce los errores

Esto está relacionado con el primer punto (y ambos están relacionados con la duplicación de código). Al duplicar el código, hay un mayor margen para que se arrastren los errores. A la inversa, mediante el uso de funciones de envoltorio, si hay un error al actualizar o consultar la tabla de la base de datos, usted sabe exactamente dónde buscar..

Es más fácil de leer

Esto puede parecer una razón 'blanda', pero la legibilidad del código es increíblemente importante. La legibilidad se trata de hacer que la lógica y las acciones del código sean claras para el lector. Esto no solo es importante cuando se trabaja como parte de un equipo, o cuando alguien puede heredar su trabajo: es posible que sepa qué debe hacer su código ahora, pero en seis meses probablemente lo habrá olvidado. Y si su código es difícil de seguir, es más fácil introducir un error..

Las funciones de Wrapper limpian su código al separar literalmente el funcionamiento interno de alguna operación (por ejemplo, crear una publicación) del contexto de esa operación (por ejemplo, manejar el envío de un formulario). Solo imagina tener todo el contenido de wp_insert_post () en lugar de cada instancia que uses wp_insert_post ().

Agrega una capa de abstracción

Agregar capas de abstracción no siempre es algo bueno, pero aquí, sin duda, lo es. No solo estos envoltorios proporcionan una forma amigable para actualizar o consultar la tabla (imagínese tener que usar SQL para consultar publicaciones en lugar de usar el mucho más conciso WP_Query () - y toda la formulación y desinfección de SQL que la acompaña), pero también ayuda a proteger a usted y a otros desarrolladores de los cambios en la estructura de la base de datos subyacente.

Al utilizar las funciones de envoltorio, usted, así como terceros, puede usarlas sin temor a que no sean seguros o se rompan. Si decide cambiar el nombre de una columna, mover una columna a otra parte o incluso eliminarla, puede estar seguro de que el resto de su plug-in no se romperá, porque simplemente hace los cambios necesarios en las funciones de su contenedor. (Por cierto, esta es una razón convincente para evitar consultas SQL directas de tablas de WordPress: si cambian, y lo harán, será romper su plug-in.). Por otro lado, una API ayuda a que su complemento se extienda de manera estable.

Consistencia

Quizás soy culpable de dividir un punto en dos aquí, pero creo que esto es un beneficio importante. Hay poco peor que la inconsistencia cuando se desarrollan complementos: solo alienta el código desordenado. Las funciones de contenedor proporcionan una interacción consistente con la base de datos: usted proporciona datos y devuelve verdadero (o una identificación) o falso (o un WP_Error objeto, si lo prefiere).


La API

Esperemos que ahora esté convencido de la necesidad de una API para su tabla. Pero antes de seguir adelante, primero definiremos una función de ayuda que facilitará un poco la desinfección..

Las columnas de la tabla

Definiremos una función que devuelva las columnas de la tabla junto con el formato de datos que esperan. Al hacer esto, podemos fácilmente agregar a la lista blanca las columnas permitidas y formatear la entrada en consecuencia. Además, si realizamos cambios en las columnas, solo necesitamos hacer los cambios aquí.

 función wptuts_get_log_table_columns () return array ('log_id' => '% d', 'user_id' => '% d', 'activity' => '% s', 'object_id' => '% d', 'object_type '=>'% s ',' fecha_actividad '=>'% s ',); 

Insertando Datos

La función más básica de "insertar" envoltorio solo tomará una matriz de pares de valores de columna e insertará esto en la base de datos. Este no tiene por qué ser el caso: puede decidir proporcionar claves más 'amigables' que luego asigne a los nombres de las columnas. También puede decidir que algunos valores se generan automáticamente o se superan en función de los valores pasados ​​(por ejemplo: estado de publicación en wp_insert_post ()).

Tal vez los * valores * que necesitan mapeo. El formato en el que se almacenan mejor los datos no siempre es el formato más conveniente de usar. Por ejemplo, para las fechas puede ser más fácil manejar un objeto DateTime o una marca de tiempo, y luego convertirlo al formato de fecha deseado.

La función de envoltura puede ser simple o complicada, pero lo mínimo que debe hacer es limpiar la entrada. También recomendaría listas blancas para las columnas reconocidas, ya que intentar insertar datos en una columna que no existe puede generar un error.

En este ejemplo, el ID de usuario es por defecto el del usuario actual, y todos los campos están dados por su nombre de columna, que es la excepción de la fecha de actividad que se pasa como 'fecha'. La fecha, en este ejemplo, debe ser una marca de tiempo local, que se convierte antes de agregarla a la base de datos.

 / ** * Inserta un registro en la base de datos * * @ param $ matriz de datos Una matriz de pares clave => valor a insertar * @ return int El ID de registro del registro de actividad creado. O WP_Error o false en caso de error. * / function wptuts_insert_log ($ data = array ()) global $ wpdb; // Establecer los valores predeterminados $ data = wp_parse_args ($ data, array ('user_id' => get_current_user_id (), 'date' => current_time ('timestamp'),)); // Verifique la validez de la fecha 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->insertar ($ wpdb-> wptuts_activity_log, $ data, $ column_formats); devuelve $ wpdb-> insert_id; 
Propina: También es una buena idea verificar la validez de los datos. Las comprobaciones que debe realizar y cómo reacciona la API dependen completamente de su contexto. wp_insert_post (), por ejemplo, requiere un cierto grado de singularidad para publicar las babosas: si hay conflictos, se genera automáticamente uno único. wp_insert_term por otro lado devuelve un error si el término ya existe. Esto se debe a una mezcla de cómo WordPress maneja estos objetos y la semántica..

Actualización de datos

Los datos de actualización generalmente imitan mucho la inserción de datos, con la excepción de que se proporciona un identificador de fila (generalmente solo la clave principal) junto con los datos que deben actualizarse. En general, los argumentos deben coincidir con la función de inserción (por coherencia), por lo que en este ejemplo, se usa 'fecha' en lugar de 'fecha_actividad'

 / ** * Actualiza un registro de actividad con los datos proporcionados * * @ param $ log_id int ID del registro de actividad que se actualizará * @ param $ matriz de datos Una matriz de columna => pares de valores que se actualizarán * @ return bool Si el registro fue actualizado exitosamente * / function wptuts_update_log ($ log_id, $ data = array ()) global $ wpdb; // El ID de registro debe ser positivo entero $ log_id = absint ($ log_id); if (empty ($ log_id)) devuelve false; // Convertir la fecha de la actividad del sello de hora local al formato GMT mysql if (isset ($ data ['activity_date'])) $ data ['activity_date'] = date_i18n ('Ymd H: i: s', $ data ['date' ], cierto ); // Inicializar matriz de formato de columna $ column_formats = wptuts_get_log_table_columns (); // Forzar campos a minúsculas $ data = array_change_key_case ($ data); // columnas de la lista blanca $ data = array_intersect_key ($ data, $ column_formats); // Reordenar $ column_formats para que coincida con el orden de las columnas en $ 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;  devuelve true; 

Consulta de datos

La función de envoltura para consultar datos a menudo será bastante complicada, especialmente porque es posible que desee admitir todo tipo de consultas que seleccionen solo ciertos campos, restrinja mediante AND y OR, ordene por una de varias columnas posibles, etc. WP_Query clase).

El principal básico de la función de envoltura para consultar los datos es que debe tomar una 'matriz de consulta', interpretarla y formar la declaración SQL correspondiente..

 / ** * Recupera los registros de actividad de la base de datos que coincide con $ consulta. * $ consulta es una matriz que puede contener las siguientes claves: * * 'campos': una matriz de columnas para incluir en los roles devueltos. O 'contar' para contar filas. Predeterminado: vacío (todos los campos). * 'orderby' - datetime, user_id o log_id. Predeterminado: datetime. * 'order' - asc o desc * 'user_id' - ID de usuario para que coincida, o un conjunto de ID de usuario * 'since' - timestamp. Devuelve solo actividades después de esta fecha. Falso por defecto, sin restricción. * 'hasta' - marca de tiempo. Devuelve solo actividades hasta esta fecha. Falso por defecto, sin restricción. * * @ param $ query Query array * @ return array Array de registros coincidentes. Falso en error. * / function wptuts_get_logs ($ query = array ()) global $ wpdb; / * Valores predeterminados de análisis * / $ defaults = array ('fields' => array (), 'orderby' => 'datetime', 'order' => 'desc', 'user_id' => false, 'since' => falso, 'hasta' => falso, 'número' => 10, 'desplazamiento' => 0); $ query = wp_parse_args ($ query, $ defaults); / * Forme una clave de caché a partir de la consulta * / $ cache_key = 'wptuts_logs:'. Md5 (serialize ($ query)); $ cache = wp_cache_get ($ cache_key); if (false! == $ cache) $ cache = apply_filters ('wptuts_get_logs', $ cache, $ query); devuelve $ caché;  extracto ($ consulta); / * SQL Select * / // Lista blanca de campos permitidos $ allowed_fields = wptuts_get_log_table_columns (); if (is_array ($ fields)) // Convertir campos a minúsculas (ya que nuestros nombres de columna están en minúscula, ver parte 1) $ fields = array_map ('strtolower', $ fields); // Desinfectar mediante la lista blanca $ fields = array_intersect ($ fields, $ allowed_fields);  else $ fields = strtolower ($ fields);  // Regresar solo los campos seleccionados. Vacío se interpreta como todo si (vacío ($ campos)) $ 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 * / // No necesitamos esto, pero permitiremos que se filtre (consulte '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)) // Force $ user_id será una matriz if (! is_array ($ user_id)) $ user_id = array ($ user_id); $ user_id = array_map ('absint', $ user_id); // convertir como enteros positivos $ 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 ($ hasta)) $ where_sql. = $ wpdb- > preparar ('AND fecha_actividad <= %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);  / * Realizar consulta * / $ logs = $ wpdb-> get_results ($ sql); / * Agregar a caché y filtro * / wp_cache_add ($ cache_key, $ logs, 24 * 60 * 60); $ logs = apply_filters ('wptuts_get_logs', $ logs, $ query); devuelve $ logs; 

En el ejemplo anterior se incluye un poco, ya que he intentado incluir varias características que podrían considerarse al desarrollar sus funciones de envoltura, que cubrimos en las secciones siguientes.

Cache

Puede considerar que sus consultas son lo suficientemente complejas, o que se repiten regularmente, que tiene sentido almacenar los resultados en caché. Dado que las diferentes consultas arrojarán resultados diferentes, obviamente no queremos usar una clave de caché genérica, necesitamos una que sea exclusiva de esa consulta. Esto es exactamente lo que hace lo siguiente. Se serializa la matriz de consulta y luego se procesa, generando una clave única para $ consulta:

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

A continuación, verificamos si tenemos algo almacenado para esa clave de caché. Si es así, excelente, simplemente devolvemos su contenido. Si no, generamos el SQL, realizamos la consulta y luego agregamos los resultados al caché (por un máximo de 24 horas) y los devolvemos. Deberemos recordar que los registros pueden tardar hasta 24 horas en aparecer en los resultados de esta función. Por lo general, hay contextos en los que el caché se borra automáticamente, pero necesitaríamos implementar estos.

Filtros y Acciones

Los ganchos han sido cubiertos ampliamente en WPTuts + recientemente por Tom McFarlin y Pippin Williamson. En su artículo, Pippin habla sobre las razones por las que debe hacer que su código sea extensible a través de enlaces y envoltorios como wptuts_get_logs () Sirven como excelentes ejemplos de dónde pueden ser utilizados..

Hemos utilizado dos filtros en la función anterior:

  • wptuts_get_logs - Filtra el resultado de la función.
  • wptuts_logs_clauses - filtra una matriz de componentes SQL

Esto permite a los desarrolladores externos, o incluso a nosotros mismos, construir sobre la API proporcionada. Si evitamos el uso directo de SQL en nuestro complemento y solo usamos estas funciones de envoltorio que hemos creado, entonces inmediatamente es posible extender nuestro complemento. los wptuts_logs_clauses El filtro en particular permitiría a los desarrolladores alterar cada parte del SQL y, por lo tanto, realizar consultas complejas. Notaremos que es el trabajo de cualquier complemento que use estos filtros para asegurarnos de que lo que devuelven esté correctamente desinfectado.

Los ganchos son igual de útiles cuando se realizan las otras tres "operaciones" principales: insertar, actualizar y eliminar datos. Las acciones permiten que los complementos sepan cuándo se están realizando, por lo que son acciones. En nuestro contexto, esto podría significar enviar un correo electrónico a un administrador cuando un usuario particular realiza una acción particular. Los filtros, en el contexto de estas operaciones, son útiles para alterar los datos antes de su inserción.

Tenga cuidado al nombrar ganchos. Un buen nombre de gancho hace varias cosas:

  • Se comunica cuando se llama al gancho o qué está haciendo (por ejemplo, puede adivinar qué pre_get_posts y user_has_cap podría hacer.
  • Ser único. Se recomienda que prefijos ganchos con el nombre de su plug-in. A diferencia de las funciones, no habrá un error si hay un conflicto entre los nombres de gancho, en lugar de eso, probablemente solo 'silenciosamente' romperá uno o más complementos.
  • Exhibe algún tipo de estructura. Haz tus ganchos predicable, y evite nombrar ganchos 'al vuelo', ya que esto a veces puede llevar a nombres de gancho aparentemente aleatorios. En su lugar, planifique con la mayor anticipación posible los ganchos que utilizará, e invente una convención de nomenclatura adecuada, y respétela..
Propina: En general, es una buena idea imitar las mismas convenciones que WordPress, ya que los desarrolladores entenderán más rápidamente lo que está haciendo ese gancho. Con respecto al uso del nombre del complemento como prefijo: si su nombre es genérico, puede que esto no sea suficiente para garantizar la exclusividad. Por último, no hay que dar una acción y un filtro del mismo nombre..

Borrando datos

El borrado de datos suele ser el más simple de los envoltorios, aunque tal vez se requiera realizar algunas operaciones de "limpieza" y simplemente eliminar los datos.. wp_delete_post () por ejemplo, no solo borra la publicación de la * _posiciones tabla, pero también elimina el metadatos apropiado, relaciones de taxonomía, comentarios y revisiones, etc..

De acuerdo con los comentarios de la sección anterior, incluiremos dos dos acciones: una activada antes y la otra después de que se haya eliminado un registro de la tabla. Siguiendo la convención de nomenclatura de WordPress para tales acciones:

  • _borrar_ se activa antes de la eliminación
  • _deleted_ se activa después de la eliminación
 / ** * Elimina un registro de actividades de la base de datos * * @ param $ log_id int ID del registro de actividades que se eliminará * @ return bool Si el registro se eliminó correctamente. * / function wptuts_delete_log ($ log_id) global $ wpdb; // El ID de registro debe ser positivo entero $ log_id = absint ($ log_id); if (empty ($ log_id)) devuelve false; do_action ('wptuts_delete_log', $ log_id); $ sql = $ wpdb-> prepare ("ELIMINAR de $ wpdb-> wptuts_activity_log DONDE log_id =% d", $ log_id); if (! $ wpdb-> query ($ sql)) devuelve false; do_action ('wptuts_deleted_log', $ log_id); devuelve verdadero 

Documentación

He estado un poco perezoso con la documentación en la fuente de la API anterior. En esta serie, Tom McFarlin explica por qué no deberías estarlo. Es posible que haya pasado mucho tiempo desarrollando las funciones de la API, pero si otros desarrolladores no saben cómo usarlas, no lo harán. También se ayudará a sí mismo cuando, después de 6 meses, haya olvidado cómo deben proporcionarse los datos o qué debe esperar que se le devuelva..


Resumen

Los envoltorios para la tabla de su base de datos pueden variar desde los relativamente simples (por ejemplo,. get_terms ()) a extremadamente complejo (por ejemplo, el WP_Query clase). En conjunto, deben tratar de servir como puerta de entrada a su mesa: lo que le permite centrarse en el contexto en el que se utilizan y, esencialmente, olvidar lo que realmente están haciendo. El API que crea es solo un pequeño ejemplo de la noción de "separación de preocupaciones", que a menudo se atribuye a Edsger W. Dijkstra en su artículo sobre el papel del pensamiento científico:

Es lo que a veces he llamado "la separación de preocupaciones", que, aunque no sea perfectamente posible, es la única técnica disponible para ordenar eficazmente los pensamientos de uno, que yo sepa. Esto es lo que quiero decir con "centrar la atención de uno en algún aspecto": no significa ignorar los otros aspectos, simplemente está haciendo justicia al hecho de que, desde el punto de vista de este aspecto, el otro es irrelevante. Se trata de una mente y múltiples pistas simultáneamente.

Puede encontrar el código utilizado en esta serie, en su totalidad, en GitHub. En la siguiente parte de esta serie veremos cómo puede mantener su base de datos y manejar las actualizaciones..