Cómo implementar Post Updates en Drupal
Los post updates son procesos que se ejecutan para actualizar la base de datos de una instalación Drupal. Son extremadamente útiles cuando necesitamos abordar ciertos cambios en la base de datos o en su estructura.
Un caso real
Imagina que hay cambios en una API externa que tu sitio web utiliza para hacer peticiones POST. Estos nuevos cambios que realizaron en la API provocan ahora un error de incompatibilidad en las peticiones, debido a que la API espera que el valor de uno de los campos nunca le llegue vacío. El primer paso para solucionar esto, es modificar la configuración de dicho campo para que sea requerido, y que los nuevos contenidos generados siempre tengan ese campo con algún valor. Pero, ¿qué ocurre con los contenidos ya creados? Muchos de estos contenidos tienen ese campo vacío, y son los que provocan el error con la API. Es aquí donde podemos utilizar los post updates que Drupal provee.
Cómo implementar un post update en la base de datos
Necesitamos programar un script que procese todos los contenidos del tipo "API Data" y que asigne el valor "0" al campo "field_api_target" que tenga un valor vacío.
El primer paso para implementar un post update es crear un fichero MODULE.post_update.php, donde "MODULE" es el nombre de tu módulo personalizado.
A continuación, vamos a utilizar el hook_post_update_NAME(). Aquí debemos cambiar la cadena "hook" por el nombre de nuestro módulo custom y la cadena "NAME" por el identificador que queramos asignarle a esta tarea de actualización.
/**
* Implements hook_post_update_NAME().
*/
function MODULE_post_update_api_data_value(&$sandbox) { }
En primer lugar, vamos a ajustar la variable $sandbox, que es la que tiene la información del proceso de actualización. Mediante la condición if (!isset($sandbox['total'])) conseguimos que únicamente entre en este bloque en la primera iteración del proceso por lotes, y será para establecer el total de nodos a procesar y el actual (0).
if (!isset($sandbox['total'])) {
// Get API Data content type nid's in first iteration.
$nids = Drupal::entityQuery('node')
->condition('type', 'apidata')
->execute();
$sandbox['total'] = count($nids);
$sandbox['current'] = 0;
}
Continuamos configurando cómo será el proceso por lotes. Establecemos que los nodos que se procesen por batch sean de 50 en 50. Debido a que la función se llamará así mismo recursivamente hasta finalizar el proceso de actualización, debemos ir actualizando el progreso del mismo. Esto lo hacemos mediante una entityQuery indicando el tipo de contenido y el rango, que será en lotes de 50.
$nodes_per_batch = 50;
$nids = Drupal::entityQuery('node')
->condition('type', 'apidata')
->range($sandbox['current'], $sandbox['current'] + $nodes_per_batch)
->execute();
Ahora es momento de realizar los cambios que necesitemos en la entidad nodo. Para nuestro ejemplo en concreto, debemos actualizar el campo "field_api_target" y asignarle el valor "0", en aquellos nodos que esté sin valor (vacío). Para ello recorremos $nids definido en el actual bloque de código, cargamos la entidad nodo, comprobamos si el valor del campo está vacío, y si lo está establecemos el valor "0". Por último guardamos el nodo y aumentamos de forma secuencial la variable $sandbox['current'].
foreach ($nids as $nid) {
$node = Drupal::entityTypeManager()
->getStorage('node')
->load($nid);
// Check if the field is empty.
if (empty($node->field_api_target->value)) {
// If is empty, set default value (0).
$node->field_api_target->value = 0;
}
$node->save();
$sandbox['current']++;
}
Para finalizar, añadimos una condición para comprobar si se ha finalizado el proceso por lotes, y si no es así, que el porcentaje de progreso se actualice.
if ($sandbox['total'] === 0) {
// Until it is finished, the batch does not stop.
$sandbox['#finished'] = 1;
}
else {
// Indicates the percentage of the completed.
$sandbox['#finished'] = ($sandbox['current'] / $sandbox['total']);
}
El código completo quedaría así.
/**
* Implements hook_post_update_NAME().
*/
function MODULE_post_update_api_data_value(&$sandbox) {
if (!isset($sandbox['total'])) {
// Get API Data content type nid's.
$nids = Drupal::entityQuery('node')
->condition('type', 'apidata')
->execute();
$sandbox['total'] = count($nids);
$sandbox['current'] = 0;
}
$nodes_per_batch = 50;
$nids = Drupal::entityQuery('node')
->condition('type', 'apidata')
->range($sandbox['current'], $nodes_per_batch)
->execute();
foreach ($nids as $nid) {
$node = Drupal::entityTypeManager()
->getStorage('node')
->load($nid);
// Check if the field is empty.
if (empty($node->field_api_target->value)) {
// If is empty, set default value (0).
$node->field_api_target->value = 0;
}
$node->save();
$sandbox['current']++;
}
if ($sandbox['total'] === 0) {
// Until it is finished, the batch does not stop.
$sandbox['#finished'] = 1;
}
else {
// Indicates the percentage of the completed.
$sandbox['#finished'] = ($sandbox['current'] / $sandbox['total']);
}
}
Cómo ejecutar un post update desde Drupal
Para ejecutar un post update debemos iniciar un proceso de actualización de base de datos. Por ello, es altamente recomendable:
- Hacer una copia de seguridad de la base de datos.
- Poner el sitio web en mantenimiento.
Para iniciar una actualización de la base de datos, podemos hacerlo desde drush o la interfaz web.
Actualizar base de datos desde Drush
Llevar a cabo una actualización de base de datos desde Drush es seguro, eficiente y más recomendable que por la interfaz web. Esto se debe a que desde la interfaz web corremos el riesgo de obtener algún error de timeout que paralice el proceso por lotes o que no muestre el progreso de actualización correctamente.
En este artículo se explica cómo instalar Drush.
Para lanzar el proceso de actualización de la base de datos, debemos ejecutar el siguiente comando Drush:
drush updb
Se nos pedirá una confirmación. Tras confirmar, arrancará el proceso de actualización.
Actualizar base de datos desde interfaz web
Para iniciar el proceso de actualización en Drupal desde la interfaz web, debemos visitar la URL your-domain.com/update.php. Es importante que estemos logados con un usuario cuyo rol tenga permisos para realizar actualizaciones del sistema. Desde aquí se nos mostrará gráficamente las actualizaciones pendientes y que se llevarán a cabo.
Finalizar un proceso de actualización de base de datos
Para concluir, debemos revisar el estado del sitio, comprobar si los nodos de tipo "API Data" tiene el valor del campo "field_api_target" a "0" y no vacío. Si todo está correcto, podemos desactivar el modo mantenimiento del sitio web.
Las próximas veces que queramos implementar nuevos post updates, tenemos que tener en cuenta que los que se implementaron en el pasado se quedan registrados en base de datos, por lo que no se podrán volver a ejecutar más.
Podemos obtener un listado completo de todos los post updates ejecutados en un sitio web Drupal a través del siguiente comando Drush:
drush eval '
$key_value = \Drupal::keyValue("post_update");
$update_list = $key_value->get("existing_updates");
print_r($update_list);
'
Volver a ejecutar hook_post_update_NAME() más de una vez
Es posible que en ciertas ocasiones necesites ejecutar más de una vez un hook_post_update_NAME() para actualizar la base de datos, sobre todo en entornos de desarrollo para realizar pruebas y validar que una implementación funciona.
En estos casos, lamento decirte que Drupal no permite volver a lanzar un hook_post_update_NAME(). Cuando el hook se ejecuta por primera vez, una vez finalizado el proceso, se guarda en la tabla key_value de la base de datos de modo que nunca más podrá lanzarse el mismo NAME del post_update; tendrías que crear otro.
Pero no te preocupes, existe una solución extraoficial que permite, a través de un comando drush personalizado, eliminar uno o más post_updates ejecutados de la tabla key_value de la base de datos, para que puedan volver a ejecutarse normalmente. El script puedes encontrarlo en este post, y te lo muestro a continuación:
#!/usr/bin/env drush
$key_value = \Drupal::keyValue('post_update');
$update_list = $key_value->get('existing_updates');
$choice = drush_choice($update_list, dt('Which post_update hook do you want to reset?'));
if ($choice) {
$removed_el = $update_list[$choice];
unset($update_list[$choice]);
$key_value->set('existing_updates', $update_list);
drush_print("$removed_el was reset");
} else {
drush_print("Reset was cancelled");
}
Puedes ver la versión completa del script aquí.
Luego tan solo sería ejecutar el comando drush por línea de comandos, para obtener la siguiente salida:
./scripts/reset_hook_post_update_NAME.drush
Which post_update hook do you want to reset?
[0] : Cancel
[1] : system_post_update_add_region_to_entity_displays
[2] : system_post_update_hashes_clear_cache
[3] : system_post_update_recalculate_configuration_entity_dependencies
[4] : system_post_update_timestamp_plugins
[5] : my_module_post_update_example_hook
# The script pauses for user input.
5
my_module_post_update_example_hook was reset
De esta manera, podremos volver a lanzar le mismo hook_post_update_NAME() tantas veces como necesitemos.
Conclusión
Los post updates son realmente útiles en el día a día, y si aprendemos a trabajar con ellos correctamente nos ahorraremos muchos problemas durante su implementación.
Y tú, ¿has utilizado alguna vez un post update? ¿Cómo lo has implementado? Deja tu comentario y echa una mano a la comunidad de Drupal Sapiens ;)
¡Hasta la próxima!