Form API (II): Modificando formularios en Drupal 8 mediante form_alter

@davidjguru
Drupal & /me
Published in
16 min readApr 23, 2018

Ejemplos básicos de intervenciones sobre formularios en proyectos Drupal 8.

Photo by Ghost Presenter on Unsplash

Sigo por las procelosas aguas del mar de la API de formularios en Drupal en esta continuación del primer post original que -como siempre me ocurre- comenzó como un artículo aislado y cuando ya llevaba tres mil quinientas palabras empecé a pensar en hacer una serie. Cosas mías.

El artículo anterior tuvo una relativa buena acogida (tres tweets como feedback, voy caminando ebrio de éxito), así que vuelvo a la carga reuniendo notas por aquí y por allá, experiencias de proyectos noveladas en forma de tickets de tareas y alguna opinión. En este episodio había pensado continuar con el asunto justo donde lo dejé (modificaciones con form_alter) y completar el contexto de los formularios con algunos métodos y clases para la localización y el renderizado de estos.

Que ustedes lo disfruten.

Índice: en este artículo
1- Introducción: ¿Esto cómo va?
1.1- La premisa: La importancia de alterar formularios
1.2- Hooks para modificar formularios en Drupal
2- Rebobinamos: Cosas que ya sabemos de formularios en Drupal
3- Ahora sí, vamos al lío: Modificaciones en formularios
3.1- Gestionar formularios: localizar y mostrar
3.2- Hooks, Módulos y Formularios en Drupal
3.3- Cambios y acciones en formularios
3.3.1- Obteniendo información
3.3.2- Añadiendo nuevos componentes
3.3.3- Modificaciones sobre un formulario
4- Recomendaciones y lecturas aconsejadas
4.1- Drupal 8 Module Development, by Daniel Sipos
4.2- Form #states en Drupal (enlaces)
4.3- Extendiendo Form API #states con Expresiones Regulares
Este artículo viene de:
Form API(I): Comprender, crear y modificar formularios en Drupal 8
Este artículo continúa en:
Form API(III): Ejemplo de modificación de formulario en Drupal 8
Otros artículos de la serie:
Form API(I): Comprender, crear y modificar formularios en Drupal 8
Form API (III): Ejemplo de modificación de formulario en Drupal 8
Otros artículos que podrían interesarte:
Drupal Fast Tips (I) - Using links in Drupal 8
Docker, Docker-Compose and DDEV - Cheatsheet
OpenEuropa - The European Commission in Drupal 8
Development environments for Drupal with DDEV

1- Introducción: ¿Esto cómo va?

Al finalizar el artículo anterior, teníamos algo así como tres piezas elementales: por un lado habíamos construido un pequeño módulo para Drupal 8 con un formulario inside con tres campos para recopilar información. Le habíamos asignado una validación (validate) y un envío (submit) a dicho formulario. De forma complementaria le dimos una estructura mínima para almacenar los datos recopilados en forma de nueva tabla de la base de datos, le añadimos además un acceso en forma de dirección URL como ruta y nos habíamos quedado en la tercera pieza: realizarles modificaciones de manera externa al formulario.

Pero es tan importante aprender y practicar con la modificación de formularios…¿Hay necesidad de ello? Pensemos en esto como en el desarrollo de una premisa.

1.1- La premisa: la importancia de alterar formularios

Para empezar, cualquier entrada de datos que esté basada en la interacción persona — máquina (hay conexiones entre plataformas con entrada de datos automatizada mediante servicios web que no requieren a un usuario) en nuestro sitio web hecho con Drupal requiere formularios para múltiples acciones: usamos forms cuando creamos un nodo de un Tipo de Contenido específico, cuando realizamos registro /login de usuario, usamos formularios de contacto, cumplimentamos pedidos de commerce, etc.

Los formularios existen, son necesarios por miles y no siempre son tan sencillos, localizables e intuitivos como el que hemos creado en el artículo anterior: es muy común tener casos avanzados de formularios multi-step (con varias páginas a cumplimentar) o formularios compuestos por campos que son formados mostrando entidades que han sido creadas previamente (manejados a través de Entity Reference, por ejemplo) y que sirven a su vez para construir instancias de otros modelos de entidades de nuestro sitio basado en Drupal.

Son muchas posibilidades, usos, componentes, enfoques…

Drupal, Forms & Pixelated Rick

…pero podemos mantener la hipótesis de que si logramos adquirir un conocimiento elemental cruzado con la asimilación de algunas mecánicas de trabajo (depuración, creación, gestión, modificaciones) podremos avanzar sin problemas hacia casos cada vez más complejos. Este es el camino a través de la Form API que hemos iniciado.

1.2- Hooks para modificar formularios en Drupal

Para quien no esté familiarizado con la terminología Drupal, es necesario comenzar explicando que un “Hook” no es más que (en esencia) una función en el contexto de Drupal. ¿Qué tiene de especial esa función? ¿Por qué adquiere un nombrado distinto? bueno, hay una definición que aprendimos en su momento -hace años- y que a pesar de no ser muy exacta, permite una aproximación muy intuitiva, a saber:

Un Hook es una función que interrelaciona el núcleo de Drupal con módulos.

Ok, ¿y esto que quiere decir? bueno, como siempre se ha considerado una herejía y un motivo para morir en la hoguera el hecho de realizar cambios sobre el código del core (núcleo) de Drupal, la mejor manera de interactuar con él y modificar/añadir funcionalidades es la de usar estas funciones desde puntos externos al core, esto es, módulos creados ad-hoc para tus funciones.

Los Hooks son localizados de manera recursiva dentro de los directorios de módulos de Drupal y sus implementaciones como funciones internas de módulos son activadas en base a acciones y eventos específicos.

Aquí puedes ver el listado de Hooks disponibles para Drupal 8 (versión 8.5)+

Y de todos de ellos existen unos cinco relacionados con la Form API, de los cuales nos interesan especialmente los siguientes:

function hook_form_alter(&$form, $form_state, $form_id)function hook_form_BASE_FORM_ID_alter(&$form, $form_state, $form_id)function hook_form_FORM_ID_alter(&$form, $form_state, $form_id)

De hecho el orden del llamada de alter hooks para forms en Drupal se realiza de la siguiente manera:

  1. En primer lugar se llama a hook_form_alter( ) ¿Existen cambios definidos para algún formulario? y su propósito como Hook es el de facilitar la realización de modificaciones en uno o varios formularios.
  2. En segundo lugar, se llama a hook_form_BASE_FORM_ID_alter( ) ¿Este formulario fue creado desde una factoría de cierto tipo de formulario genérico? ¿Hay modificaciones a realizar en algún formulario base? y su propósito como Hook es el de facilitar cambios en formularios base.
  3. En tercer lugar, se llama a hook_form_FORM_ID_alter( ) ¿Se deben realizar alteraciones en un formulario específico? y su propósito como Hook es el de facilitar alteraciones sobre un formulario único.

Como podemos observar en las cabeceras de los métodos anteriores, en todos los casos de estos tres Hooks se está pasando la variable que referencia al formulario en cuestión (&$form) a través de un paso por referencia (y no por valor), lo que quiere decir que el ámbito (scope) de la variable es “global” más allá el corpus de la función; los cambios que realicemos sobre el formulario serán persistentes. Ojo cuidao.

Así que en el capítulo de hoy íbamos a practicar con estos Hooks en el contexto de modificación de formularios, pero como hook_form_FORM_ID_alter es muy directo y el hook_form_BASE_FORM_ID_alter es muy intuitivo, a pesar de que a nivel de código es más cargante trabajar con estructuras procedimentales if-else/switch, vamos a jugar un poco con el hook_form_alter. Para ello, haremos un nuevo módulo para modificar el pequeño formulario que ya realizamos en el capítulo anterior+.

2- Rebobinamos: Cosas que ya sabemos de formularios en Drupal

En el episodio anterior, dimos un repaso general por los fundamentos de la construcción de formularios en Drupal: vimos métodos básicos, la mecánica elemental de un formulario, el concepto de render array sobre el que se asientan los formularios en Drupal y como se construyen estos a partir de la declaración de componentes y métodos para la validación y uso ¿Os acordáis? Repasemos de forma intuitiva la mecánica básica de un formulario en Drupal:

Diagrama intuitivo de la mecánica de formularios en Drupal

3- Ahora sí, vamos al lío: Modificaciones en formularios

Bien una vez revisitados algunos lugares comunes y refrescado las ideas básicas, podemos pasar a adentrarnos un poco más en el mundo de los formularios en Drupal.

3.1- Gestionar formularios: localizar y mostrar

La clase FormBuilder es una clase PHP de Drupal orientada a facilitar la construcción y procesado de formularios en el contexto de un proyecto Drupal. Puedes consultar sus métodos disponibles aquí+. Pero lo interesante es que está declarada como servicio (Servicio en el contexto de Symfony o Drupal), es decir, una clase que puede usarse desde cualquier ubicación dentro del proyecto (eso es bueno).

La clase form_builder declarada como un servicio reutilizable del core de Drupal

Al estar disponible como servicio, podemos usar -por ejemplo- este tipo de expresiones que vienen a decir “eh constructor de forms, dame un formulario”:

\Drupal::formBuilder()->getForm()

Aunque nos falta acercarnos a un pequeño detalle: ¿Qué es “\Drupal” en la sentencia anterior? básicamente, una solución de compromiso temporal para tener un contenedor desde el que llamar a servicios externos de la manera más centralizada posible. Aquí tienes disponibles las funciones existentes que se le pueden solicitar+ y hay material para salir del paso obteniendo información sobre usuarios, bases de datos, entidades y formularios en diferentes formatos (objetos, colecciones o recibiendo directamente el servicio, como en el caso de la llamada a formBuilder).

Así que repasemos:

1- Por un lado, tenemos el acceso “Explícito” a un formulario a través de formBuilder( ), que puede usarse para localizar, traer, mostrar y procesar un formulario. Normalmente el parámetro necesario será el nombre de la clase del formulario (que debía implementar FormBuilderInterface)y a partir de ahí se devolverá el HTML del form listo para renderizar por Drupal. En nuestro caso teniendo en cuenta el formulario creado en el capítulo anterior, sería:

$form = \Drupal::formBuilder()
->getForm(‘Drupal\formexample\Form\FormExample’);
\Drupal::service('renderer')->render($form);

2- Por otro lado, tenemos el acceso “Implícito” a un formulario a través de la ruta que hayamos determinado en nuestro fichero de routing:

formexample_form:  
path: 'formexample/myform'

y al final -por decirlo de una manera simplificada- tenemos una llamada al método de construcción de cada formulario solicitado:

public function buildForm($form, $form_state)

Never Forget: En esencia, un formulario en Drupal tiene dos pilares básicos, una variable $form que almacena los componentes que tiene, y una variable $form_state que almacena los valores que tienen esos componentes en un momento determinado.

3.2- Hooks, Módulos y Formularios en Drupal

Lo primero que tenemos que plantearnos es la creación de un módulo para albergar estos casos que podemos probar. Vamos a crear un modulico Drupal 8 que se llame -por ejemplo- “Alterforms” (locos por el branding), con el objetivo de centralizar los ejercicios a realizar sobre la alteración de formularios.

Como ya sabemos, hay una serie de ficheros que debemos cumplimentar para la construcción de nuestro módulo. Comenzamos por el fichero info.yml, donde se declara la información de versión, contexto y dependencias del módulo. En este caso como inicialmente quiero jugar depurando con kint y search_kint (como vimos en este apartado del capítulo anterior+), los añado como dependencias. Recuerda que debes instalarlos y activarlos (vía composer, git clone, drush, wget, ftp, interfaz, etc).

Y a continuación el fichero central, alterforms.module. En este caso he organizado las cosas como en tres bloques diferenciados que pueden localizarse en el fichero a nivel de comentarios de código dentro del Hook form_alter:

1- Zona inicial: para identificar de que formulario se trata y procesar información para mostrar en pantalla.

2- Zona de acciones: Una vez identificado el formulario (como se usa una estructura switch se pueden integrar alteraciones de tantos formularios como queramos), decidimos que hacemos con él.

3- Zona de debugging: Una región dentro del fichero para jugar un poco con las opciones de depuración del formulario. Por defecto tengo descomentada la salida para kint con $form como parámetro.

Nota: Sí, mi inglés es lamentable.

Será en estas tres secciones en las que vayamos incluyendo nuestras pruebas.

3.3- Cambios y acciones en formularios

Vamos a pasar a practicar con formularios operando directamente sobre ellos, incluyendo de manera sucesiva nuevas líneas de código en los espacios dentro del fichero alterforms.module que hemos comentado anterior, con la idea de mejorar nuestro Kung-Fú sobre formularios en Drupal.

3.3.1- Obteniendo información a partir de un formulario

Vamos a ver cuanto juego nos puede dar usar el formulario y nuestro hook de form alter para extraer información útil.

Identificar el formulario

Vamos a empezar por extraer el id del formulario. Poniendo esta línea de código dentro del hook_form_alter mostramos en pantalla el identificador de cada formulario que visitemos:

// Usamos la variable $form_id que se le pasa al hook_form_alter
// y la mostramos en pantalla a través de un clásico mensaje Drupal
drupal_set_message("Form ID is : " . $form_id);

Identificar el formulario base

Igualmente, existen formularios de tipo “base” que mediante una factoría, generan N formularios que heredan de esa base y se muestran a lo largo de todo el sitio web. Se pueden modificar mediante el hook_form_BASE_FORM_ID_alter que comentábamos anteriormente en este post para realizar cambios que se ejecuten de manera centralizada en todas sus instancias:

function hook_form_BASE_FORM_ID_alter(&$form, $form_state, $form_id)

Y si queremos saber cual es el form base a partir del que se pueda estar instanciando el actual:

// Extraemos la información a partir del array BuildInfo
// Del $form_state y la mostramos en pantalla con dpm()
$baseform = $form_state->getBuildInfo()['base_form_id'];
dpm($baseform, "Contenido del array BuildInfo, ID del Form Base: ");

Y en pantalla:

y ahora podríamos usar directamente el Hook:

function hook_form_contact_message_form_alter(&$form, $form_state, $form_id)

Identificar la entidad en la que se basa un formulario

A veces necesitamos conocer la naturaleza de un formulario y queremos saber si el form que estamos viendo en pantalla es en realidad una instancia de EntityForm y a que tipo de Entidad responde dicho formulario. Para ello volvemos a la inmensa fuente de información que es $form_state y le solicitamos el FormObject:

// Primero: Conseguimos el FormObject$form_object = $form_state->getFormObject();// Segundo: Comprobamos si es una instancia de EntityForm$isEntityForm = false;
if ($form_object instanceof \Drupal\Core\Entity\EntityFormInterface) {
$isEntityForm = true;
drupal_set_message("The Form is build from a EntityForm");
}
// Tercero: Extraemos y mostramos el tipo de entidad$entity_type = $form_object->getFormObject()->getEntity()
->getEntityTypeId();
drupal_set_message("The EntityForm is: " . $entity_type);
// Version breveif ($form_state->getFormObject() instanceof \Drupal\Core\Entity\EntityFormInterface) {
$entity_type = $form_state->getFormObject()->getEntity()
->getEntityTypeId();
drupal_set_message($entity_type);
}

3.3.2- Añadiendo nuevos componentes en un formulario

Uno de las tareas más habituales es la de añadir nuevos elementos en un formulario. Simplemente accediendo a la estructura switch con la que filtramos el formulario que vamos a usar, podemos sumar nuevos componentes a la estructura array que usa el form. En este caso volviendo al form del capítulo anterior, le añadimos un nuevo checkbox y un elemento colorpicker, uno de los recursos que se añadieron con el soporte de HTML5+:

function alterforms_form_alter(&$form, $form_state, $form_id) { 

switch ($form_id){
case "formexample_drupal8":
$form['newcheck'] = array(
'#type' => 'checkbox',
'#title' => t('I certify that this is my true name'),
);
$form['background_color'] = array(
'#type' => 'color',
'#title' => t('Select your favourite color'),
);
break;
}
}

Algo que podemos necesitar es que además de añadir nuevos elementos a un formulario, también tengamos que modificar el orden en el que son renderizados los elementos. En el caso anterior, hemos añadido un nuevo check que al mostrarse en pantalla, aparece en penúltimo lugar: Si se no indica otra cosa, el orden de renderizado de los elementos del formulario es justo el de su inclusión a nivel de código, lo que es equivalente al orden que ocupan dentro del array de información del form. Pero podemos alterar esto.

Supongamos que necesitamos que el check se muestre en primer lugar dentro del formulario: será suficiente con introducir una propiedad ‘weight’ en cada elemento, como cuando construimos elementos de menú mediante site-building y configuraciones, que le otorgará un peso en relación al resto de elementos para determinar su orden visual. La mecánica es la misma.

// Creo variables para mostrar info en pantalla
// Asignamos un valor al atributo "weight" en cada caso
$peso1 = $form['name']['#weight'] = 2;
$peso2 = $form['surname']['#weight'] = 3;
$peso3 = $form['age']['#weight'] = 4;
$peso4 = $form['newcheck']['#weight'] = 1;
$peso5 = $form['background_color']['#weight'] = 5;
drupal_set_message(
"Peso del elemento 1 - Nombre: " . $peso1 .
"Peso del elemento 2 - Apellido: " . $peso2 .
"Peso del elemento 3 - Edad: " . $peso3 .
"Peso del elemento 4 - Check: " . $peso4 .
"Peso del elemento 5 - Color: " . $peso5);

3.3.3- Modificaciones sobre un formulario

Primero veíamos que información podíamos extraer a partir de un formulario y en segundo lugar hemos visto como añadir otros componentes. Ahora en este tercer apartado vamos a hacer algunas pruebas con modificaciones del comportamiento de un formulario, jugando con el comportamiento del form. Veamos.

Prefilling de campos de formulario

Un requisito pseudo-funcional habitual puede ser el de extraer información de la sesión activa: en ciertos formularios -especialmente aquellos forms que registran información del usuario- es importante traer valores a pantalla e incorporarlos a un campo para mostrar tras un renderizado (cargar /user, por ejemplo). Vamos a hacer algunas pruebas de pre-fill de formularios: cargar valores en campos desde nuestro hook_form_alter.

Desde el corpus de código del hook_form_alter podemos invocar servicios externos y clases útiles para gestionar la información que necesitemos. Ahora en nuestro formulario necesitamos un nuevo campo para registrar el correo electrónico del usuario y además que se usen datos ya registrados del propio usuario para mostrarlos ya pre-cargados en el formulario. Para eso nos serviremos de información ya registrada en el propio perfil del usuario al que accederemos mediante el objeto “user” que almacena toda esta información, extraeremos los valores que necesitamos y los pre-cargaremos en dos campos cuando se acceda al formulario (pasos primero y tercero del siguiente bloque).

switch ($form_id){case  "your_string_form_id":// Algunos ejemplos
// Primero: Obtener el objeto completo del usuario actual

$currentUser = \Drupal\user\Entity\User::load(\Drupal::currentUser()->id());
// Segundo: Si quieres saber cuanta información está almacenada
// en el objeto user, lanza el objeto a pantalla vía dpm()
dpm($currentUser, "Usuario actual");// Tercero: Extraemos dos valores (nombre y mail)del perfil de
// usuario a través del objeto user, para pre-cargar en formulario
$userMail = $currentUser->get('mail')->value;
$userName = $currentUser->get('name')->value;
$form['testmail']['#value'] = $userMail;
$form['name']['#value'] = $userName;
// Cuarto: Solo extraemos el id del usuario actual
// para realizar consultas de valores a BBDD
$userID = \Drupal::currentUser()->id();// Quinto: lanzar una consulta a base de datos
// para extraer valores asociados al usuario y pre-cargar campos.
// Importante: en realidad hacerlo así está un poco deprecated
// y es mejor usar ya el método estático de conexiones BBDD
// a través de la clase \Drupal, lo uso aquí por fines didácticos.

$result = db_select('table_in_database', 't')
->condition('t.column', $userID, '=')
->fields('t',array('name_column_to_extract'))
->execute()
->fetchField(0);

Los pasos anteriores sirven para realizar ese pre-filling de formulario que comentábamos anteriormente:

Formulario de prueba con valores pre-cargados a partir del objeto User

Componentes dinámicos en un form con #states

Desde Drupal 7, existe una especie de sistema interno para dinamizar componentes de formulario llamado #states. Este permite crear elementos dentro de un formulario que puedan alterar su estado -show, hide, disable, enable, etc.-en base a condiciones tanto propias del elemento como de otro elemento distinto del formulario (que al clickar uno se oculte otro, por ejemplo).

¿Y esto como va? bueno se supone que la idea es que dada una sintaxis específica, usted joven declare acciones y Drupal desde su lado proporcione todo el Javascript/JQuery necesario para que esas acciones declaradas ocurran sobre la marcha. Todo se inicia con el uso de #states como una propiedad más a la hora de declarar el elemento del formulario, y a partir de ahí Drupal se encarga de añadir el Javascript necesario para cambiar elementos a través de la función drupal_process_states+.

Veamos un set de opciones que aporta Drupal (versión 8.6) para #states:

States que pueden aplicarse en condiciones remotas (origen):
empty, filled, checked, unchecked, expanded, collapsed, value.
States que pueden aplicarse sobre un elemento (destino):
enabled, disabled, required, optional, visible, invisible, checked, unchecked, expanded, collapsed.

Ok, ahora veamos el esquema básico de su sintaxis (forma típica de array anidado):

'#states' => array(
'STATE' => array(
JQUERY_SELECTOR => REMOTE_CONDITIONS,
JQUERY_SELECTOR => REMOTE_CONDITIONS,
JQUERY_SELECTOR => REMOTE_CONDITIONS,
JQUERY_SELECTOR => REMOTE_CONDITIONS,
...
),
),

Vamos a hacer que en nuestro lumpen-formulario de ejemplo, el campo “surname” para el apellido dependa de si el check inicial esté marcado o no. Sí se marca este check el campo “surname” se ocultará. Buen momento para unificar todos los cambios realizados al campo y unificarlo todo dentro del hook_form_alter:

$form['surname'] = array(
'#type' => 'textfield',
'#title' => t('Surname:'),
'#required' => FALSE,
'#weight' => 3,
'#states' => array(
'invisible' => array(
':input[name="newcheck"]' => array('checked' =>TRUE),
),
),
);

Modificar la validación del formulario

Una consecuencia lógica de tanto cambio acometido sobre el formulario es la de tener que modificar su proceso de validación para incluir sus nuevos elementos y/o nuevas condiciones de validación sobre sus valores, por lo que tendremos que alterar también la llamada a la función validate y reconstruirla a nuestro gusto:

// Cambiamos la validación del form en pre-submit
$form['#validate'][] = 'your_string_form_validate';
// Añadimos al final del fichero el nuevo método de validación
function your_string_form_validate($form, $form_state){ dpm($form_state, 'Testing the format with dpm -Devel-');
}

¿Qué tal? ¿Qué os parecen estos ejercicios que estamos realizando? creo que una vez pasadas las tres mil quinientas palabras vale la pena detenerse, coger aire y preparar el siguiente post con información complementaria: se me ocurre que en realidad #states da mucho más juego y se pueden construir condiciones de comportamiento más interesantes…pero antes, ¿Qué tal un caso práctico de intervención sobre formularios en el contexto de un proyecto real? ummm…nos vemos en el siguiente capítulo.

4- Recomendaciones y lecturas aconsejadas

Como complemento, paso a listar algunas referencias que pueden ser interesantes para ampliar el topic.

4.1- Drupal 8 Module Development, by Daniel Sipos

De todos los manuales técnicos sobre desarrollo para Drupal 8, que he podido leer (y sigo leyendo), en esta ocasión recomiendo este libro de Daniel Sipos+, mítico desarrollador de la comunidad Drupal.

En este caso, tiene un apartado específico tanto para formularios en Drupal 8 dentro del contexto de la inyección de servicios (página 38) como para el uso de #states dentro de elementos de formularios (página 387).

Está disponible en Packt en varios formatos+, incluido digital. Recomendado.

4.2- Form #states en Drupal

Para comprender bien las posibilidades que ofrece el uso de #states, añado unos enlaces muy útiles sobre el tema. Aunque basados en Drupal 7, siguen siendo muy interesantes.

4.3- Extendiendo Form API #states con Expresiones Regulares

Este artículo me llamó la atención en su momento y aunque está enfocado a Drupal 7, es totalmente reutilizable. Una manera de llevar #states un paso más allá.

--

--

@davidjguru
Drupal & /me

I’m @davidjguru this is my off-broadway channel, speaking about Drupal like in https://www.therussianlullaby.com or https://davidjguru.github.io but in spanish.