Back

Loading…

Documentation

🧩 Hacia un composer.json más consistente en nuestros proyectos Drupal

En Proton Systems mantenemos varios proyectos Drupal que han nacido y evolucionado en momentos diferentes. Con el tiempo, eso significa que nuestros archivos composer.json también han ido acumulando pequeñas diferencias: distintas versiones de plugins, diferentes formas de manejar parches, librerías externas instaladas de distintas maneras y configuraciones que quizá tuvieron sentido hace algunos años, pero que hoy podemos simplificar.

Nuestro objetivo no es que todos los proyectos tengan un composer.json idéntico.

La idea es más importante que eso:

Queremos que las diferencias entre proyectos sean intencionales y no simplemente históricas.

En este artículo explicamos cómo queremos estructurar nuestros proyectos Drupal en adelante y por qué hemos tomado estas decisiones.


📦 Composer sigue siendo el centro de las dependencias de Drupal

Nuestra estructura parte del proyecto recomendado de Drupal y mantiene Composer como responsable de las dependencias PHP y Drupal.

La base será normalmente algo parecido a:

{
  "require": {
    "drupal/core-recommended": "^11.4",
    "drupal/core-composer-scaffold": "^11.4",
    "drupal/core-project-message": "^11.4"
  }
}

drupal/core-recommended es especialmente importante porque no solamente instala Drupal Core: mantiene las versiones de sus dependencias alineadas con las versiones probadas por Drupal.

Para proyectos nuevos, esta estructura parte naturalmente de:

composer create-project drupal/recommended-project

No todos nuestros proyectos tendrán exactamente las mismas dependencias. Por ejemplo, un proyecto puede necesitar PHPUnit, herramientas de análisis estático o incluso una dependencia directa adicional de drupal/core para determinados tests.

Eso está bien.

Lo importante es poder explicar por qué existe cada excepción.


🩹 Una estrategia común para los parches

Este es probablemente el cambio más importante que queremos estandarizar.

Durante años hemos utilizado cweagans/composer-patches en varios proyectos, pero actualmente tenemos distintas versiones y estrategias.

Nuestra nueva referencia será Composer Patches 2.x:

{
  "require": {
    "cweagans/composer-patches": "^2.0"
  }
}

Drupal.org también utiliza actualmente la rama 2.x en su documentación:

composer require cweagans/composer-patches:~2.0 --update-with-dependencies

Composer Patches 2 incorpora además un concepto especialmente útil para equipos: patches.lock.json, que funciona de manera similar a composer.lock y registra la definición conocida de los parches que deben aplicarse.


📁 Los parches vivirán en nuestro repositorio

Una de nuestras decisiones principales será evitar que nuestros builds dependan directamente de archivos .patch alojados en Drupal.org, GitLab o GitHub.

En lugar de esto:

{
  "extra": {
    "patches": {
      "drupal/example": {
        "Fix issue #123456":
          "https://www.drupal.org/files/issues/2026-01-01/example.patch"
      }
    }
  }
}

preferimos algo como:

{
  "extra": {
    "patches": {
      "drupal/example": {
        "Issue #123456: Fix example":
          "patches/contrib/example/123456-fix-example.patch"
      }
    }
  }
}

con una estructura de repositorio sencilla:

patches/
├── core/
│   └── 123456-description.patch
└── contrib/
    ├── example/
    │   └── 234567-description.patch
    └── another_module/
        └── 345678-description.patch

🤔 ¿Por qué guardar un patch localmente?

No es solamente una preferencia nuestra.

Drupal.org actualmente incluye una advertencia explícita:

Do not hotlink patches from Drupal.org.

La recomendación es descargar el patch y guardarlo en el repositorio local para evitar que un build falle si el archivo remoto cambia o desaparece.

Hay varias razones importantes.

1. 🔒 El código que desplegamos queda bajo nuestro control

Un patch remoto es otra dependencia externa.

Hoy esta URL:

https://www.drupal.org/files/issues/.../fix.patch

puede contener exactamente el cambio que hemos probado.

Pero nuestra instalación depende de que:

  • el archivo continúe existiendo;
  • la URL siga funcionando;
  • su contenido no cambie;
  • el servidor externo esté disponible durante nuestro deployment.

Con un patch almacenado en Git:

patches/contrib/example/fix.patch

el código que probamos es exactamente el código que desplegamos.


2. 🔁 Nuestros builds son más reproducibles

Queremos que:

git clone
composer install

produzca hoy, mañana o dentro de un año el mismo resultado, siempre que utilicemos los mismos archivos lock.

Guardar los parches dentro del repositorio elimina una dependencia externa innecesaria.


3. 🛡️ Evitamos patches mutables

Composer Patches también advierte específicamente sobre los patches generados automáticamente desde Pull Requests o Merge Requests.

Su contenido puede cambiar cuando alguien añade nuevos commits al PR o MR.

La propia documentación recomienda que, cuando necesitemos uno de estos patches, lo descarguemos y lo incluyamos en nuestro proyecto en vez de depender directamente de la URL generada.


4. 👀 El patch forma parte de nuestro code review

Cuando el archivo está dentro del repositorio podemos revisar fácilmente:

patches/
composer.json
composer.lock
patches.lock.json

en el mismo Merge Request.

El patch deja de ser simplemente una URL y pasa a ser parte explícita del código que estamos manteniendo.


🔐 patches.lock.json

Composer Patches 2 genera:

patches.lock.json

Este archivo debe formar parte del repositorio, igual que composer.lock.

Composer Patches lo utiliza como una lista conocida de los patches que deben aplicarse y guarda, entre otras cosas, su hash SHA-256.

Nuestro repositorio podrá terminar teniendo:

composer.json
composer.lock
patches.json
patches.lock.json

patches/
└── ...

El uso de un patches.json separado es opcional, pero Composer Patches lo soporta oficialmente y señala una ventaja interesante: modificar un patch no obliga a cambiar el content hash de composer.lock, porque la información específica de patches queda bloqueada en patches.lock.json.

En proyectos con varios parches probablemente preferiremos:

patches.json

para mantener composer.json más legible.


🔄 El nuevo workflow para modificar patches

Composer Patches 2 introduce comandos específicos para este proceso.

Después de añadir o modificar un patch:

composer patches-relock
composer patches-repatch

patches-relock actualiza patches.lock.json.

patches-repatch reinstala las dependencias afectadas y vuelve a aplicar los patches.

Este workflow está documentado específicamente para equipos que mantienen proyectos con Composer Patches.


🚫 Si no tenemos patches, no necesitamos configuración de patches

También queremos evitar configuración innecesaria.

Un proyecto sin patches no necesita tener bloques como:

{
  "extra": {
    "enable-patching": true,
    "composer-exit-on-patch-failure": true
  }
}

simplemente “por si algún día lo necesitamos”.

Nuestra regla será sencilla:

¿El proyecto tiene patches?

No
→ no necesita configuración específica.

Sí
→ Composer Patches 2.x
→ patches locales
→ patches.lock.json

Composer Patches 2 puede instalarse preventivamente —su documentación indica que esto es totalmente válido—, pero para nuestros proyectos preferimos no añadir infraestructura que todavía no necesitamos.


🎨 ¿Y las librerías JavaScript y CSS?

Composer continuará gestionando:

Drupal Core
Drupal contrib
Paquetes PHP
Composer plugins

Pero una librería frontend utilizada por nuestro propio theme normalmente debería pertenecer al ecosistema frontend.

Por ejemplo:

pnpm add swiper

en lugar de introducir nuevas capas de Composer solamente para colocar:

web/libraries/swiper

Esto nos permite mantener una división mucho más natural:

Composer
└── PHP + Drupal

pnpm
└── JavaScript + frontend tooling

Hay excepciones.

Algunos módulos contrib esperan explícitamente encontrar una librería determinada dentro de:

web/libraries/

En esos casos evaluaremos la solución individualmente.

La regla no será “Composer nunca instala una librería frontend”, sino:

No usar Composer para dependencias frontend de nuestro theme cuando el gestor natural de esa dependencia es npm/pnpm.


🏗️ Composer también administra archivos de Drupal

Hay otra parte muy interesante de composer.json que a veces pasa desapercibida: Drupal Composer Scaffold.

Cuando hacemos:

composer install

Composer no solamente instala vendor/ y nuestros módulos.

Drupal también puede reconstruir determinados archivos del webroot, entre ellos:

web/index.php
web/update.php
web/robots.txt
web/.htaccess
web/sites/default/default.settings.php
web/sites/default/default.services.yml

Drupal llama a estos scaffold files.

Esto es muy útil porque permite recibir actualizaciones de esos archivos junto con Drupal Core.

Pero también significa que debemos tener cuidado si decidimos personalizarlos.


🛑 Evitar que Composer sobrescriba un archivo

Supongamos que un proyecto tiene un:

web/robots.txt

completamente personalizado y queremos asumir nosotros la responsabilidad de mantenerlo.

Podemos indicar:

{
  "extra": {
    "drupal-scaffold": {
      "locations": {
        "web-root": "web/"
      },
      "file-mapping": {
        "[web-root]/robots.txt": false
      }
    }
  }
}

A partir de ese momento Drupal Scaffold deja de reemplazar ese archivo.

Podríamos hacer lo mismo con otros archivos que realmente hayamos decidido mantener nosotros:

{
  "extra": {
    "drupal-scaffold": {
      "file-mapping": {
        "[web-root]/robots.txt": false,
        "[web-root]/.htaccess": false
      }
    }
  }
}

Pero aquí hay una advertencia importante.


⚠️ No debemos bloquear archivos sin necesidad

Drupal recomienda que, cuando sea posible, utilicemos mecanismos como append y prepend antes de excluir completamente un scaffold file.

¿Por qué?

Porque al escribir:

"[web-root]/robots.txt": false

estamos diciendo:

“Este archivo ahora es responsabilidad nuestra.”

Eso significa que dejamos de recibir automáticamente correcciones o cambios que Drupal pueda incorporar en versiones futuras.

Por eso nuestra política debería ser:

Archivo estándar de Drupal
→ dejar que Drupal Scaffold lo gestione.

Necesitamos añadir algunas líneas
→ intentar prepend / append.

Necesitamos mantener nuestra propia versión completa
→ file-mapping: false.

No queremos llenar composer.json con exclusiones preventivas.


📝 Un caso típico: robots.txt

robots.txt es probablemente el mejor ejemplo.

Si solamente necesitamos añadir unas reglas propias, podemos mantener las actualizaciones de Drupal y complementar el archivo mediante Drupal Scaffold.

Si, por el contrario, nuestro robots.txt es completamente específico del proyecto, podemos utilizar:

"[web-root]/robots.txt": false

y versionarlo como cualquier otro archivo del proyecto.

El mismo razonamiento puede aplicarse a .htaccess.

La decisión debe ser consciente: bloquear el archivo significa hacernos responsables de mantenerlo actualizado.


🧭 ¿Cómo queremos que se vea finalmente?

No existe un único composer.json válido para todos nuestros proyectos, pero conceptualmente queremos acercarnos a algo así:

composer.json
│
├── require
│   ├── Drupal Core
│   ├── Drupal contrib
│   ├── PHP libraries
│   └── Composer plugins
│
├── require-dev
│   ├── PHPUnit
│   ├── PHPStan
│   └── development tools
│
├── config
│   └── Composer configuration
│
└── extra
    ├── installer-paths
    ├── drupal-scaffold
    └── Composer Patches configuration

y alrededor:

project/
├── composer.json
├── composer.lock
├── patches.json
├── patches.lock.json
├── patches/
├── package.json
├── pnpm-lock.yaml
└── web/

No todos los archivos estarán presentes en todos los proyectos.

Ese es precisamente el punto.

Queremos una estructura común, no complejidad común.


✅ Nuestra filosofía

Podemos resumirla en unas pocas ideas:

📦 Composer administra Drupal y PHP.

🎨 pnpm administra normalmente nuestras dependencias frontend.

🩹 Los patches forman parte de nuestro código y viven en nuestro repositorio.

🔐 composer.lock y patches.lock.json hacen nuestros builds reproducibles.

🏗️ Drupal Scaffold debe seguir actualizando sus archivos siempre que sea posible.

🚫 Si decidimos excluir un scaffold file, aceptamos explícitamente la responsabilidad de mantenerlo.

Y sobre todo:

No queremos que todos nuestros proyectos sean iguales. Queremos que todos sigan las mismas decisiones arquitectónicas y que sus excepciones puedan explicarse.


🔜 ¿Qué más puede hacer composer.json?

Hasta aquí hemos hablado principalmente de dependencias, patches y Drupal Scaffold.

Pero composer.json puede hacer bastante más.

¿Qué hacen realmente scripts, autoload, repositories, config.allow-plugins, preferred-install, minimum-stability o conflict? ¿Cuáles de estas herramientas tienen sentido en un proyecto Drupal moderno? ¿Podemos utilizarlas para hacer nuestros proyectos más seguros y predecibles?

👉 En el siguiente artículo exploraremos otras funcionalidades de composer.json que vale la pena conocer y cómo pueden mejorar nuestros proyectos Drupal.

Contents