Por Qué un Módulo Custom y Cuándo Tiene Sentido Crearlo
Drupal.org tiene más de cincuenta mil módulos contribuidos. Pero te toca extender ese módulo contrib que casi hace lo que necesitas y descubres que el patch acabaría más sucio que escribir tu propio módulo desde cero. Ahí entra el desarrollo a medida.
La pregunta no es "¿puedo programarlo?". Es "¿debo programarlo?". Un módulo contribuido con tests, releases de seguridad y mantenedores activos casi siempre gana a código propio que tendrás que arrastrar tú. El custom se justifica cuando la lógica es genuinamente tuya: un algoritmo de pricing con variables que solo existen en tu negocio, una integración con un ERP interno, un flujo de aprobación que ningún Workflow estándar contempla. O cuando necesitas un control de rendimiento que los módulos genéricos no te dan.
Y existe una tercera vía que los que llevamos años en esto explotamos siempre que podemos: usar un contrib como base y extenderlo con hooks, plugins o un EventSubscriber, escribiendo solo la capa fina de personalización. Aprovechas la infraestructura probada y te ahorras reinventar la rueda. Es lo que menos deuda técnica genera, y por eso es lo primero que proponemos en la mayoría de proyectos.
Anatomía de un Módulo Drupal: Ficheros Esenciales
Todo módulo custom en Drupal 10 y 11 vive bajo modules/custom/ con una estructura predecible. El nombre de máquina —minúsculas y guiones bajos, por convención— se repite en la carpeta y en cada fichero que cuelga de ella. Es tedioso al principio. Luego lo agradeces.
El fichero que no puede faltar es mi_modulo.info.yml. Declara el nombre legible, descripción, tipo (module), core compatible, package y dependencias. Sin él, Drupal ni se entera de que existes. A partir de ahí, cada fichero extra desbloquea una capacidad concreta.
mi_modulo.module es el punto de entrada clásico para los hooks procedurales. Drupal lleva años empujando hacia OOP, pero los hooks siguen siendo la vía más directa para alterar comportamiento ajeno sin sobreescribir clases. Aquí defines mi_modulo_form_alter(), mi_modulo_node_presave() o mi_modulo_theme().
Para rutas y páginas usas mi_modulo.routing.yml. Cada entrada empareja un path con un controlador PHP, declara permisos y, si hace falta, pone requisitos sobre los parámetros. El controlador suele vivir en src/Controller/ extendiendo ControllerBase y devuelve render arrays que Drupal convierte en HTML.
La inyección de dependencias se configura en mi_modulo.services.yml. Lo que declares ahí queda registrado en el contenedor de Symfony y disponible para inyección en controladores, plugins o forms. Declarar servicios propios desacopla la lógica de negocio de la capa de presentación y te salva la vida cuando llegan los tests unitarios.
Los formularios extienden FormBase o ConfigFormBase y viven en src/Form/. El esquema de configuración —necesario para que el formulario de ajustes hable bien con el config system— se define en config/schema/mi_modulo.schema.yml.
Hooks, Plugins y Eventos: Tres Mecanismos Complementarios
Saber cuándo usar cada uno es lo que separa al junior del senior. Los hooks son funciones globales que Drupal invoca en momentos específicos del request. Sencillos de escribir, complicados de testear de forma aislada y, hasta hace nada, sin autoloading ni inyección de dependencias nativa. Aun así, para alteraciones puntuales tipo hook_form_alter, hook_entity_presave o hook_preprocess_HOOK, siguen siendo la herramienta correcta.
El sistema de plugins juega en otra liga: OOP puro. Drupal define tipos de plugin —blocks, fields, widgets, formatters, conditions, migrations— y tu módulo aporta implementaciones anotadas con atributos PHP. Crear un Block, por ejemplo, es escribir una clase en src/Plugin/Block/ que implemente BlockPluginInterface y declare su metadata con #[Block]. El descubrimiento es automático: Drupal escanea los namespaces registrados y los encuentra solos. Sin registro manual. Es el patrón que más extensibilidad da con menos acoplamiento.
Los eventos de Symfony llegaron con la 8 y abrieron un tercer canal entre módulos. Despachas un evento personalizado donde te interese, y otro módulo se suscribe implementando EventSubscriberInterface. Sirve para desacoplar módulos que necesitan comunicarse sin conocerse. Caso típico: el módulo de pedidos despacha OrderCompleted, el de notificaciones se suscribe y manda el mail. El de pedidos ni sabe que existe el de notificaciones.
Desde Drupal 11, los hooks también pueden registrarse como métodos de clase con atributos PHP, lo que los acerca al modelo de plugins y eventos. Las fronteras se difuminan, pero la regla mental sigue valiendo: hooks para alterar comportamiento existente, plugins para implementaciones intercambiables de un tipo definido, eventos para notificaciones desacopladas.
Buenas Prácticas de Codificación y Estándares
Drupal impone estándares de codificación derivados de PSR-12 con sus propias rarezas. Cumplirlos no es opcional si quieres que el código aguante el paso del tiempo y la rotación del equipo. PHP_CodeSniffer con los rulesets de Drupal y DrupalPractice metidos en CI cazan las violaciones antes de que aterricen en main.
La inyección de dependencias merece párrafo aparte. Cada vez que tu módulo necesita el entity storage, el mail service, el logger o lo que sea del núcleo, inyectas el servicio por constructor o por el método create() cuando lo instancia el contenedor. Tirar de \Drupal::service() o de helpers estáticos tipo \Drupal::entityTypeManager() funciona, sí. Pero te complica el testing y te acopla a algo que mañana cambiará de sitio.
Separar lógica de negocio de presentación parece de manual, y aun así se viola constantemente. Un controlador no debería tener queries directas ni cálculos pesados; su trabajo es orquestar servicios y devolver una respuesta. Lo gordo va en servicios inyectables que puedan probarse aislados.
La configuración pide cabeza. Los valores por defecto se exportan como YAML en config/install/, y el schema se define para que traducción y formulario de ajustes funcionen. Si necesitas modificar configuración de otro módulo, lo haces vía config/optional/ o desde un hook de instalación. Nunca editando los ficheros del módulo ajeno —eso te lo borra el siguiente composer update.
Testing: PHPUnit, Kernel Tests y Tests Funcionales
Un módulo custom sin tests es una bomba de relojería. Drupal te da varios niveles integrados con PHPUnit, y cada uno tiene su trade-off entre velocidad y cobertura.
Los unitarios (UnitTestCase) corren sin levantar Drupal: rapidísimos, ideales para validar lógica pura de servicios que no tocan el contenedor. Los kernel tests (KernelTestBase) arrancan un Drupal mínimo con SQLite en memoria y prueban interacciones con el entity system o con la config sin overhead de navegador; suelen ser el sweet spot para módulos custom. Los funcionales (BrowserTestBase) levantan Drupal entero y simulan navegador, lo que los hace imprescindibles para forms, permisos y flujos de usuario completos.
Una cobertura razonable: unitarios por cada servicio, kernel tests para la integración con entities y config, y al menos un funcional por ruta o formulario expuesto. La inversión la amortizas en la primera actualización mayor de Drupal que te toque comerte.
Del Entorno Local al Despliegue en Producción
El ciclo arranca en un entorno local reproducible. DDEV o Lando te dan contenedores Docker preconfigurados que se parecen mucho a producción. El código del módulo vive en el repo del proyecto, gestionado con Composer.
El loop habitual: escribes, pasas tests en local, pasas el linter, abres merge request, esperas revisión. El CI corre PHPUnit, PHPStan (nivel 6 como suelo razonable) y el sniffer de estándares. Si algo se rompe, no hay despliegue.
En producción ejecutas drush deploy, que encadena import de configuración, updates pendientes, limpieza de cachés y hooks post-deploy. Las migraciones de datos o cambios de esquema van en hook_update_N(), que Drupal ejecuta secuencialmente y de forma idempotente —cosa que agradeces cuando un deploy se queda a medias y tienes que relanzarlo.
Si tu organización necesita desarrollar módulos personalizados en Drupal para funcionalidades a medida y quiere un equipo con experiencia en arquitectura Drupal empresarial, contacta con nuestro equipo de desarrollo y evaluamos juntos el enfoque técnico.
La Arquitectura como Inversión a Largo Plazo
Un módulo custom no es código que funcione hoy: es código que va a convivir con el proyecto años. Las decisiones de arquitectura —qué mecanismo de extensión usar, cómo cortar los servicios, dónde poner la abstracción— determinan el coste de cada cambio futuro. Un módulo bien planteado absorbe nuevos requisitos con cambios quirúrgicos. Uno mal planteado te obliga a reescribir cada vez que el negocio se mueve.
Invertir en estándares, inyección de dependencias, tests y documentación técnica no es lujo. Es lo que separa un proyecto que escala con cabeza de uno que acumula deuda hasta volverse intratable. Cada hora dedicada a hacerlo decentemente en desarrollo se devuelve multiplicada en mantenimiento y ampliaciones futuras.