Back

Loading…

Documentation

🛠️ Más allá de las dependencias: qué puede hacer composer.json en un proyecto Drupal

En el artículo anterior vimos cómo queremos organizar composer.json en nuestros proyectos Drupal: qué papel tiene Composer, cómo gestionar patches, cuándo utilizar pnpm y cómo tratar los archivos administrados por Drupal Scaffold.

Pero composer.json es mucho más que una lista de paquetes.

Además de decir:

"require": {
  "drupal/core-recommended": "^11.4"
}

podemos utilizarlo para controlar:

  • 🔐 qué plugins pueden ejecutar código;
  • ⚙️ comandos habituales del proyecto;
  • 🧩 cómo se cargan clases PHP;
  • 📦 de dónde se obtienen determinados paquetes;
  • 🚧 qué dependencias queremos impedir;
  • 🎯 qué versiones y niveles de estabilidad aceptamos;
  • 🚀 cómo instala Composer nuestras dependencias.

Veamos cuáles de estas posibilidades tienen realmente sentido en un proyecto Drupal moderno.


🔐 allow-plugins: controlar qué plugins pueden ejecutar código

Esta es probablemente una de las secciones más importantes de un composer.json moderno.

Composer permite que determinados paquetes funcionen como plugins. A diferencia de una librería PHP normal, un plugin puede ejecutar código durante operaciones como:

composer install
composer update
composer require

Por esa razón, desde Composer 2.2 existe:

"config": {
  "allow-plugins": {
    "composer/installers": true,
    "drupal/core-composer-scaffold": true,
    "drupal/core-project-message": true
  }
}

allow-plugins funciona como una lista explícita de confianza.

Composer describe esta funcionalidad como una capa de seguridad que restringe qué plugins tienen permiso para ejecutar código. (getcomposer.org)

Drupal adoptó este mecanismo en sus propios project templates precisamente para que instalaciones y pipelines no tengan que responder interactivamente si confían o no en cada plugin. (drupal.org)

Si utilizamos Composer Patches, por ejemplo, veremos también:

"cweagans/composer-patches": true

La regla que queremos aplicar es sencilla:

🔐 Autorizar únicamente plugins que conocemos y necesitamos.

No queremos solucionar un problema poniendo:

"allow-plugins": true

porque eso elimina prácticamente la protección.


⚙️ scripts: convertir tareas habituales en comandos del proyecto

Composer permite definir comandos reutilizables:

"scripts": {
  "test": "phpunit",
  "analyse": "phpstan analyse",
  "check": [
    "@test",
    "@analyse"
  ]
}

Ahora cualquier desarrollador puede ejecutar:

composer check

sin tener que recordar cada comando individual.

Esto puede ser útil para tareas como:

composer test
composer analyse
composer lint
composer check

Composer también permite ejecutar scripts automáticamente en determinados eventos.

Por ejemplo:

"scripts": {
  "post-install-cmd": [
    "..."
  ]
}

Sin embargo, aquí conviene ser conservadores.

Un composer install debería ser una operación predecible.

No queremos convertirlo en una cadena oculta de:

composer install
   ↓
importar configuración
   ↓
actualizar base de datos
   ↓
limpiar caches
   ↓
compilar frontend
   ↓
ejecutar comandos personalizados

sin que el desarrollador sea consciente de ello.

Por eso podemos utilizar scripts, pero preferiremos comandos explícitos para procesos importantes.

Por ejemplo:

"scripts": {
  "quality": [
    "@phpstan",
    "@phpcs"
  ]
}

es mucho más claro que ejecutar esas tareas silenciosamente después de cada instalación.

💡 Un buen script simplifica un comando. Un mal script esconde comportamiento.


🧩 autoload: enseñar a Composer dónde están nuestras clases

Composer también es responsable del autoloading PHP.

Una librería o aplicación PHP suele declarar algo como:

"autoload": {
  "psr-4": {
    "ProtonSystems\\Example\\": "src/"
  }
}

Esto significa:

ProtonSystems\Example\Service
               ↓
src/Service.php

Composer genera esta información dentro de:

vendor/autoload.php

En un proyecto Drupal típico debemos tener cuidado con esta sección.

Nuestros módulos ya tienen su propia estructura PSR-4:

web/modules/custom/example/
└── src/
    └── Service/

Drupal sabe cargar esas clases.

Por eso normalmente no necesitamos registrar nuestros módulos custom desde el composer.json raíz.

Si encontramos algo como:

"autoload": {
  "psr-4": {
    "Drupal\\my_module\\": "web/modules/custom/my_module/src"
  }
}

debemos preguntarnos:

🤔 ¿Por qué el proyecto raíz necesita conocer directamente las clases de este módulo?

Puede existir una razón válida, pero no debería ser nuestra solución por defecto.


🧪 autoload-dev: código que solamente necesitamos durante desarrollo

Existe también:

"autoload-dev": {
  "psr-4": {
    "Tests\\": "tests/"
  }
}

Esto sirve especialmente para:

  • PHPUnit;
  • fixtures;
  • helpers para tests;
  • clases utilizadas exclusivamente durante desarrollo.

La idea encaja perfectamente con:

"require-dev": {
  "phpunit/phpunit": "..."
}

En producción podemos instalar:

composer install --no-dev

y esas dependencias y herramientas de desarrollo dejan de formar parte de la instalación.


📦 repositories: Composer no solamente conoce Packagist

Cuando ejecutamos:

composer require vendor/package

Composer normalmente busca el paquete en Packagist.

Drupal añade además su propio repositorio:

"repositories": [
  {
    "type": "composer",
    "url": "https://packages.drupal.org/8"
  }
]

Gracias a esto podemos hacer:

composer require drupal/pathauto

aunque los proyectos de Drupal.org no funcionen simplemente como paquetes publicados directamente en Packagist.

Drupal recomienda actualmente Composer como mecanismo para administrar Core y sus dependencias y el recommended-project configura esta infraestructura automáticamente. (drupal.org)


🧭 ¿Cuándo debemos añadir otro repository?

Composer soporta varios tipos:

composer
vcs
package
path
artifact

Esto permite hacer cosas interesantes.

Por ejemplo, durante el desarrollo de un paquete interno podríamos utilizar un repositorio local:

"repositories": [
  {
    "type": "path",
    "url": "../our-module"
  }
]

También podemos apuntar Composer a un repositorio privado.

Pero debemos evitar añadir repositories como solución rápida cuando existe una alternativa estándar.

Cada repositorio adicional significa otra fuente de paquetes que el proyecto necesita comprender y mantener.

Nuestra preferencia será:

Drupal.org
→ packages.drupal.org

PHP público
→ Packagist

Frontend
→ npm/pnpm

Repositorio custom
→ solamente cuando realmente exista una necesidad

🚧 conflict: decir explícitamente qué NO puede instalarse

Una sección pequeña pero muy útil es:

"conflict": {
  "drupal/drupal": "*"
}

De hecho, este patrón aparece en proyectos Drupal Composer.

conflict significa:

Este proyecto es incompatible con determinadas versiones o paquetes.

Puede utilizarse, por ejemplo, si sabemos que una dependencia concreta causa un problema:

"conflict": {
  "vendor/package": "<2.4"
}

Composer impedirá crear una combinación de dependencias que viole esa regla.

Esto es mejor que simplemente escribir en un README:

⚠️ No actualizar este paquete a determinada versión.

Porque Composer puede hacer cumplir la restricción automáticamente.


🎯 Las restricciones de versiones también documentan nuestra estrategia

Un detalle aparentemente pequeño como:

"drupal/core-recommended": "^11.4"

contiene bastante información.

Composer interpreta:

^11.4

aproximadamente como:

>=11.4.0 <12.0.0

Mientras:

~11.4.0

limita las actualizaciones a:

>=11.4.0 <11.5.0

Composer documenta ^ como una forma de permitir actualizaciones compatibles siguiendo versionado semántico. (getcomposer.org)

Por eso una constraint no solamente determina qué puede instalar Composer.

También expresa una decisión de mantenimiento.

^11.4
→ aceptamos siguientes minors de Drupal 11

~11.4.0
→ queremos permanecer en Drupal 11.4.x

Ninguna es universalmente correcta.

Lo importante es que la elección sea intencional.


🧪 minimum-stability: ¿qué tan experimental puede ser nuestro proyecto?

Composer utiliza por defecto:

"minimum-stability": "stable"

Esto significa que normalmente seleccionará releases estables.

Los niveles posibles son:

stable
RC
beta
alpha
dev

Composer documenta stable como el valor predeterminado. (getcomposer.org)

Podríamos cambiar globalmente:

"minimum-stability": "dev"

pero eso abriría todo el proyecto a versiones de desarrollo.

Normalmente es preferible expresar la excepción en el paquete concreto:

"drupal/example": "^2.0@beta"

En otras palabras:

🧪 Si una dependencia necesita ser experimental, hagamos experimental esa dependencia, no todo el proyecto.


⚖️ prefer-stable

Relacionado con lo anterior encontramos:

"prefer-stable": true

Esto indica a Composer que, cuando existan varias posibilidades compatibles, prefiera releases más estables. (getcomposer.org)

En proyectos que utilizan alguna dependencia alpha, beta o dev, puede proporcionar una capa adicional de previsibilidad.

Pero tampoco debemos copiarlo mecánicamente.

Si nuestro minimum-stability ya es stable y todas nuestras dependencias son estables, su efecto será mucho menos relevante.


🚀 preferred-install: source o dist

Composer puede instalar una dependencia principalmente de dos maneras.

source

Normalmente significa clonar su repositorio:

vendor/package/.git/

dist

Normalmente significa descargar un archivo preparado, como un ZIP.

Composer utiliza actualmente dist por defecto. (getcomposer.org)

Podemos especificarlo:

"config": {
  "preferred-install": "dist"
}

Para CI y producción, dist suele ser la opción natural:

  • más rápido;
  • menos datos;
  • no necesitamos el historial Git de cada dependencia.

Si un desarrollador está trabajando directamente sobre una dependencia, puede preferir source localmente.

Composer incluso recomienda configurar preferencias específicas de desarrollo globalmente en la máquina del desarrollador en lugar de obligar al CI a utilizar source. (getcomposer.org)


🔃 sort-packages: un pequeño detalle que mejora los diffs

Otro ajuste habitual:

"config": {
  "sort-packages": true
}

hace que Composer mantenga ordenadas las dependencias cuando ejecutamos comandos como:

composer require drupal/pathauto

Parece insignificante, pero en proyectos con varios desarrolladores tiene ventajas:

  • diffs más limpios;
  • menos cambios de orden innecesarios;
  • composer.json más fácil de leer.

Es exactamente el tipo de configuración pequeña y predecible que sí tiene sentido compartir entre proyectos.


🌐 config.platform: simular una versión concreta de PHP

Composer también permite:

"config": {
  "platform": {
    "php": "8.4"
  }
}

Esto hace que el resolver de Composer se comporte como si estuviera ejecutándose con esa versión de PHP.

Puede resultar útil cuando:

Desarrollo
PHP 8.4

CI
PHP 8.4

Producción
PHP 8.4

y queremos impedir que alguien con PHP 8.5 instale accidentalmente una dependencia incompatible con el servidor.

Pero tiene una consecuencia importante:

Composer está simulando esa plataforma.

No significa que la máquina realmente tenga PHP 8.4.

Por eso debemos utilizarlo deliberadamente y continuar verificando el entorno real durante CI/deployment.


🗂️ extra: el lugar donde los plugins amplían Composer

En nuestros proyectos Drupal probablemente una de las secciones más grandes será:

"extra": {
  ...
}

Composer proporciona este espacio para que diferentes paquetes y plugins almacenen su propia configuración.

En Drupal encontramos allí cosas como:

drupal-scaffold
installer-paths
drupal-core-project-message
composer-patches

Es importante entender una diferencia:

extra no es una única funcionalidad de Composer.

Es un espacio que utilizan otras herramientas.

Por eso, antes de copiar alguna propiedad encontrada en otro proyecto como:

"enable-patching": true

debemos preguntar:

¿Quién interpreta esta opción?
¿El plugin sigue instalado?
¿La opción sigue existiendo en nuestra versión?
¿Todavía la necesitamos?

Este pequeño hábito ayuda muchísimo a eliminar configuración histórica.


🧹 El principio más importante: menos configuración también es una mejora

Después de revisar varios proyectos que han evolucionado durante años, es fácil caer en la tentación de crear un enorme composer.json estándar que contenga todas las posibilidades.

Pero ese no es nuestro objetivo.

No queremos:

autoload
scripts
repositories
patching
platform
custom installers
scaffold mappings

en todos los proyectos solamente porque Composer los soporta.

Preferimos:

¿Lo necesitamos?
    ↓
Sí → configurarlo de forma explícita.

No → no añadirlo.

Un composer.json pequeño y comprensible es una ventaja arquitectónica.


🧭 Nuestra clasificación práctica

Podemos pensar en las diferentes áreas de composer.json así:

✅ Casi siempre relevantes

require
require-dev
config.allow-plugins
config.sort-packages
extra.drupal-scaffold
installer-paths

🟡 Útiles cuando existe una necesidad

scripts
autoload
autoload-dev
conflict
config.platform
repositories adicionales
Composer Patches

⚠️ Modificar solamente con una razón clara

minimum-stability
prefer-stable
custom package repositories
custom installer behavior
global autoloading of Drupal modules
automatic post-install scripts

🏗️ composer.json como documentación de arquitectura

Quizá esta sea la idea más interesante.

Cuando abrimos un buen composer.json, deberíamos poder entender rápidamente:

¿Qué versión de Drupal utiliza?

¿Qué necesita para ejecutarse?

¿Qué herramientas necesitan los desarrolladores?

¿Qué Composer plugins pueden ejecutar código?

¿Tiene patches?

¿De dónde vienen sus paquetes?

¿Existe alguna restricción especial?

¿Hay comportamiento automático al instalar?

Eso convierte composer.json en algo más que un archivo requerido por Composer.

Se convierte en una pequeña descripción ejecutable de la arquitectura del proyecto.


✅ Una filosofía sencilla

Después de explorar todas estas posibilidades, nuestra regla sigue siendo la misma que en el artículo anterior:

No buscamos que todos nuestros composer.json sean idénticos. Buscamos que podamos explicar cada una de sus diferencias.

Queremos configuraciones:

🔎 visibles, 📖 comprensibles, 🔐 seguras, 🔁 reproducibles, 🧹 y tan pequeñas como sea razonable.

Composer tiene muchas herramientas.

La clave no está en utilizarlas todas.

La clave está en saber cuándo una de ellas resuelve realmente un problema de nuestro proyecto.


🔜 ¿Y ahora qué?

Después de entender mejor composer.json, hay una pregunta natural:

¿Qué ocurre realmente cuando ejecutamos composer install, composer update o composer require?

¿Por qué composer install es apropiado después de hacer git pull, pero composer update puede cambiar nuestro árbol de dependencias? ¿Qué papel cumple exactamente composer.lock? ¿Qué debemos ejecutar en CI y producción?

👉 En el siguiente artículo podemos entrar en el ciclo completo de Composer en un proyecto Drupal: composer.json vs composer.lock, install vs update, y cómo conseguir deployments realmente reproducibles.

Contents