🔄 composer install vs composer update: cómo funciona realmente Composer en un proyecto Drupal
En los artículos anteriores vimos cómo estructurar mejor nuestro composer.json y qué funcionalidades importantes ofrece Composer más allá de declarar dependencias.
Ahora toca una de las dudas más frecuentes —y también una de las más importantes para trabajar en equipo:
¿Cuál es realmente la diferencia entre
composer installycomposer update?
A primera vista parecen comandos similares.
Ambos descargan paquetes.
Ambos pueden modificar nuestro directorio:
vendor/
y ambos pueden afectar nuestro proyecto Drupal.
Pero conceptualmente hacen cosas muy diferentes.
Entender esta diferencia es fundamental para conseguir:
- 🔁 builds reproducibles;
- 🚀 deployments seguros;
- 👥 entornos de desarrollo alineados;
- 🔐 actualizaciones controladas;
- 🧹 menos sorpresas después de hacer
git pull.
🧠 Primero: composer.json y composer.lock no tienen la misma función
La forma más sencilla de entender Composer es separar estos dos archivos:
composer.json
composer.lock
composer.json describe qué aceptamos
Por ejemplo:
{
"require": {
"drupal/core-recommended": "^11.4",
"drupal/pathauto": "^1.13"
}
}
Estamos diciendo aproximadamente:
Drupal Core
→ queremos una versión compatible con ^11.4
Pathauto
→ queremos una versión compatible con ^1.13
Pero no estamos diciendo necesariamente qué versión exacta debe instalarse.
Dependiendo de las versiones disponibles, Composer podría resolver esas constraints de diferentes maneras.
🔒 composer.lock describe qué hemos decidido instalar
Después de resolver las dependencias, Composer guarda las versiones concretas en:
composer.lock
Conceptualmente:
composer.json
↓
¿Qué versiones están permitidas?
↓
Composer resuelve dependencias
↓
composer.lock
↓
Estas son las versiones exactas seleccionadas
Por ejemplo, composer.json podría permitir:
drupal/pathauto: ^1.13
mientras composer.lock fija algo concreto como:
drupal/pathauto
1.x.y
junto con todas sus dependencias transitivas.
Este es uno de los motivos por los que composer.lock es tan importante en una aplicación Drupal.
🔐
composer.jsonexpresa nuestras reglas;composer.lockregistra el resultado concreto de esas reglas.
📥 ¿Qué hace composer install?
Cuando ejecutamos:
composer install
y existe composer.lock, Composer utiliza las versiones almacenadas en ese lock file.
No intenta buscar arbitrariamente versiones nuevas permitidas por composer.json.
En términos simplificados:
composer install
↓
lee composer.lock
↓
instala exactamente ese árbol de dependencias
Eso significa que si un desarrollador ha actualizado correctamente el proyecto y ha hecho commit de:
composer.json
composer.lock
otro desarrollador puede hacer:
git pull
composer install
y obtener el mismo conjunto de dependencias.
Ese comportamiento es precisamente lo que buscamos en nuestros proyectos.
🔄 ¿Qué hace entonces composer update?
composer update tiene una responsabilidad diferente.
Cuando ejecutamos:
composer update
Composer vuelve a resolver las dependencias utilizando las restricciones declaradas en:
composer.json
y las versiones actualmente disponibles.
Después escribe el nuevo resultado en:
composer.lock
Conceptualmente:
composer.json
↓
composer update
↓
buscar versiones compatibles actuales
↓
resolver dependencias
↓
actualizar composer.lock
↓
instalar nuevas versiones
Por eso:
composer updatecambia potencialmente nuestra decisión de versiones.
Mientras:
composer installreproduce una decisión ya tomada.
Esta diferencia parece pequeña, pero cambia completamente cuándo debemos utilizar cada comando.
👨💻 Después de git pull: normalmente composer install
Supongamos que un compañero actualiza Drupal o instala un nuevo módulo.
Ejecuta algo como:
composer require drupal/pathauto
Composer modifica:
composer.json
composer.lock
y esos dos archivos se incluyen en el commit.
Después nosotros hacemos:
git pull
¿Qué debemos ejecutar?
Normalmente:
composer install
No:
composer update
¿Por qué?
Porque nuestro compañero ya tomó la decisión sobre el árbol de dependencias y la dejó registrada en composer.lock.
No necesitamos volver a resolverla.
Queremos reproducirla.
Compañero
composer require/update
↓
composer.lock cambia
↓
Git
↓
Nosotros
git pull
↓
composer install
Este es uno de los workflows más importantes que queremos mantener consistentes.
⚠️ ¿Qué podría pasar si hacemos composer update después de cada git pull?
Imaginemos:
composer.json
contiene:
{
"require": {
"vendor/package": "^2.0"
}
}
Nuestro compañero probó el proyecto utilizando:
vendor/package 2.4
y esa versión quedó registrada en composer.lock.
Después hacemos:
git pull
composer update
Pero entretanto se publicó:
vendor/package 2.5
Como sigue siendo compatible con:
^2.0
Composer podría seleccionar esa nueva versión.
Ahora tenemos algo distinto de lo que nuestro compañero probó.
Eso rompe una propiedad muy valiosa:
mismo commit
=
mismas dependencias
Por eso composer update no debería formar parte del workflow rutinario simplemente para “sincronizar” un proyecto.
🛠️ Entonces, ¿cuándo usamos composer update?
Cuando queremos actualizar dependencias.
Por ejemplo:
composer update drupal/pathauto
o, si necesitamos actualizar también sus dependencias:
composer update drupal/pathauto --with-all-dependencies
La documentación actual de Drupal recomienda precisamente este patrón para actualizar módulos contrib.
También podemos actualizar Drupal Core mediante un comando dirigido, por ejemplo:
composer update "drupal/core-*" --with-all-dependencies
dependiendo de la estructura del proyecto. Drupal documenta actualmente este workflow para proyectos basados en core-recommended.
La idea importante es:
🎯 Preferimos actualizaciones explícitas y con intención.
En lugar de ejecutar:
composer update
sin saber qué va a cambiar, podemos actualizar exactamente aquello que queremos revisar.
🔍 Antes de actualizar: composer outdated
Composer puede decirnos qué dependencias tienen nuevas versiones disponibles:
composer outdated
Para concentrarnos en Drupal:
composer outdated "drupal/*"
Drupal incluye actualmente este comando dentro de su workflow recomendado para revisar actualizaciones.
Esto nos permite separar dos acciones:
Ver qué podría actualizarse
→ composer outdated
Decidir qué queremos actualizar
→ composer update paquete
Mucho mejor que actualizar primero y mirar qué ocurrió después.
🧪 --dry-run: mirar antes de tocar
Otra herramienta extremadamente útil es:
composer update drupal/example --with-all-dependencies --dry-run
Composer calcula lo que haría sin aplicar realmente los cambios.
Drupal recomienda también utilizar --dry-run al revisar actualizaciones.
Nos permite ver operaciones como:
Upgrading package A
Upgrading package B
Removing package C
Installing package D
antes de modificar nuestro proyecto.
Para actualizaciones sensibles, es un hábito excelente.
📦 composer require también puede actualizar
Cuando hacemos:
composer require drupal/pathauto
Composer no se limita a escribir una línea dentro de composer.json.
También:
modifica composer.json
↓
resuelve dependencias
↓
modifica composer.lock
↓
instala los paquetes
Por eso, cuando añadimos una nueva dependencia, normalmente debemos hacer commit de ambos:
composer.json
composer.lock
Lo mismo aplica cuando cambiamos manualmente una constraint y luego resolvemos las nuevas dependencias.
🔐 composer.lock debe formar parte de Git
Para aplicaciones como nuestros sitios Drupal, composer.lock debe mantenerse en el repositorio.
Nuestro commit debería contener algo como:
composer.json
composer.lock
y, cuando utilizamos Composer Patches 2:
patches.lock.json
también.
Esto permite que:
composer install
reconstruya el árbol de dependencias que hemos probado.
Drupal recomienda explícitamente hacer commit de composer.lock y desplegar utilizando ese archivo.
🚀 Producción: composer install, no composer update
Esta es probablemente la regla más importante de todo el artículo.
En producción queremos reproducir un build conocido.
No queremos decidir nuevas versiones.
Por eso Drupal recomienda explícitamente:
composer install --no-dev
en producción y no:
composer update
Nuestro deployment debería conceptualmente ser:
Desarrollo / CI
─────────────────────────
actualizar dependencias
↓
probar
↓
commit composer.lock
↓
Merge Request
↓
review
↓
merge
Producción
─────────────────────────
git pull / checkout release
↓
composer install --no-dev
↓
Drupal deployment
El servidor de producción no debería estar tomando decisiones sobre qué versión nueva instalar.
Esas decisiones deberían haberse tomado y probado antes.
🧪 ¿Qué significa --no-dev?
En nuestro composer.json podemos tener:
{
"require-dev": {
"phpunit/phpunit": "...",
"phpstan/phpstan": "..."
}
}
Estas herramientas son útiles durante desarrollo y CI, pero no necesariamente deben instalarse en producción.
Con:
composer install --no-dev
Composer utiliza el mismo composer.lock, pero omite los paquetes marcados como dependencias de desarrollo.
Esto permite tener:
Desarrollo
→ herramientas de testing y análisis
Producción
→ solamente runtime dependencies
Drupal también utiliza este patrón en su documentación para deployments.
🚀 --optimize-autoloader
En producción podemos además utilizar:
composer install --no-dev --optimize-autoloader
Composer optimiza la información utilizada para cargar clases PHP.
Es una opción habitual para aplicaciones desplegadas en producción.
Nuestro comando de build puede por tanto terminar siendo:
composer install \
--no-dev \
--optimize-autoloader
La configuración exacta puede depender de nuestra infraestructura, pero conceptualmente sigue siendo un install, no un update.
🗄️ Composer actualiza código; Drupal puede necesitar actualizar la base de datos
Hay otro detalle fundamental.
Después de:
composer install
podemos tener una nueva versión de Drupal Core o de un módulo.
Eso no significa necesariamente que el sitio esté completamente actualizado.
Un módulo puede incluir:
hook_update_N()
u otros cambios que necesitan ejecutarse contra la base de datos.
Por eso un deployment Drupal normalmente continúa con:
drush updatedb -y
y posteriormente otras operaciones necesarias.
La documentación actual de Drupal describe precisamente este workflow: primero construir el código a partir de composer.lock y después ejecutar las actualizaciones de Drupal.
⚙️ ¿Y la configuración de Drupal?
En proyectos donde utilizamos Configuration Management, también tenemos:
config/sync/
Versionado en Git.
Por eso nuestro deployment tiene realmente varias capas:
Git
↓
código + composer.lock + configuración
Composer
↓
dependencias PHP/Drupal
Drupal
↓
actualizaciones de base de datos
Config Management
↓
configuración de Drupal
Un workflow típico podría ser:
composer install --no-dev --optimize-autoloader
drush updatedb -y
drush config:import -y
drush cache:rebuild
Drupal documenta esta secuencia de deployment y señala además que los pasos posteriores a Composer pueden realizarse mediante Drush.
🪄 drush deploy: agrupar el deployment Drupal
En versiones modernas de Drush podemos simplificar parte de ese proceso con:
drush deploy -y
Drupal menciona expresamente drush deploy como una forma de agrupar los pasos posteriores del deployment y proporcionar un flujo consistente.
Esto nos permite pensar el deployment en dos bloques claros:
composer install --no-dev --optimize-autoloader
drush deploy -y
Composer se encarga del árbol de código y dependencias.
Drush se encarga del estado de Drupal.
Esa separación conceptual es muy útil.
👥 El workflow que queremos para un equipo
Podemos resumir nuestras operaciones normales así.
📥 Recibir cambios de un compañero
git pull
composer install
Después ejecutar los pasos Drupal que requiera el proyecto.
📦 Añadir una dependencia
composer require drupal/example
Revisar:
composer.json
composer.lock
Probar y hacer commit de ambos.
🔄 Actualizar una dependencia
Primero:
composer outdated "drupal/*"
Después:
composer update drupal/example --with-all-dependencies
Revisar el diff, probar y hacer commit de composer.lock.
🚀 Desplegar
composer install --no-dev --optimize-autoloader
drush deploy -y
Nunca utilizar composer update simplemente como parte rutinaria del deployment.
🧩 ¿Qué ocurre si composer.json cambia pero composer.lock no?
Esta situación merece atención.
Supongamos que alguien modifica:
"drupal/example": "^2.0"
manualmente, pero no actualiza:
composer.lock
Ahora los dos archivos pueden estar desalineados.
Cuando ejecutamos:
composer install
Composer puede advertirnos que el lock file no está actualizado respecto a composer.json.
Eso es una señal importante.
No deberíamos solucionar automáticamente el problema con:
composer update
sin entender qué cambio faltó.
Primero debemos determinar:
¿Se olvidó hacer commit de composer.lock?
¿Se modificó composer.json manualmente?
¿Realmente queríamos actualizar una dependencia?
El lock file forma parte del cambio funcional.
🚫 No editar composer.lock manualmente
composer.lock es generado por Composer.
No deberíamos modificar directamente:
version
source
dist
content-hash
para “corregir” una instalación.
La solución debe realizarse mediante comandos Composer que reconstruyan correctamente el grafo de dependencias.
🧯 ¿Y si quiero reconstruir vendor/?
Una buena propiedad de un proyecto Composer es que:
vendor/
no debería ser nuestra fuente de verdad.
Nuestra fuente de verdad es:
composer.json
composer.lock
Por eso, conceptualmente podemos eliminar:
vendor/
y ejecutar:
composer install
para reconstruirlo.
En Drupal sucede algo parecido con módulos y themes administrados por Composer: no deberíamos modificar directamente el código instalado dentro de:
web/modules/contrib/
web/themes/contrib/
vendor/
Si necesitamos modificar upstream, utilizamos mecanismos como patches.
🔐 Añadamos una comprobación más: composer audit
Después de instalar o actualizar dependencias podemos ejecutar:
composer audit
Composer revisa las dependencias contra advisories de seguridad conocidos.
La documentación actual de Drupal incluye composer audit dentro de su workflow para revisar problemas de seguridad en dependencias PHP y Drupal.
Esto puede encajar muy bien en CI:
composer install
↓
composer audit
↓
tests
↓
static analysis
🤖 CI debe reproducir, no decidir
Nuestro pipeline de CI debería comportarse igual que otro miembro del equipo:
composer install
No debería ejecutar:
composer update
porque CI debe comprobar el estado que hemos comprometido en Git.
Queremos saber:
“¿Funciona este commit con las dependencias que hemos declarado y bloqueado?”
No:
“¿Funciona este commit con cualquier versión nueva que haya aparecido esta mañana?”
La diferencia es enorme.
🧠 Una forma sencilla de recordarlo
Podemos resumirlo así:
composer require
= quiero añadir/cambiar una dependencia
composer update
= quiero tomar nuevas decisiones de versiones
composer install
= quiero reproducir las decisiones existentes
O todavía más corto:
📥 Install reproduce. 🔄 Update resuelve. ➕ Require modifica requisitos y resuelve.
🏗️ Nuestro workflow ideal
En un proyecto Drupal mantenido por un equipo queremos llegar a algo parecido a esto:
Developer A
────────────
composer update drupal/example
↓
composer.lock cambia
↓
tests
↓
commit
↓
Merge Request
Git
Developer B
────────────
git pull
composer install
CI
────────────
composer install
tests
Production
────────────
checkout release
composer install --no-dev
drush deploy
Todos trabajan con el mismo árbol de dependencias.
Solo el desarrollador que está realizando deliberadamente una actualización ejecuta composer update.
✅ La regla que queremos recordar
Si solamente pudiéramos quedarnos con una idea de este artículo sería esta:
composer updatees una operación de desarrollo.composer installes una operación de reproducción.
Utilizamos update cuando queremos cambiar nuestro árbol de dependencias.
Probamos ese cambio.
Versionamos:
composer.json
composer.lock
y después dejamos que los demás entornos utilicen:
composer install
para reproducirlo.
Ese pequeño cambio mental hace que Composer sea mucho más fácil de entender y hace nuestros proyectos mucho más predecibles.
🔜 ¿Qué podemos explorar después?
Ahora sabemos:
- cómo estructurar
composer.json; - qué otras herramientas ofrece;
- cómo funciona
composer.lock; - cuándo utilizar
install,updateyrequire; - y cómo llevar esas decisiones hasta producción.
Pero todavía queda otra pregunta muy práctica:
¿Cómo sabemos exactamente por qué Composer instaló una determinada versión o por qué se niega a actualizar un paquete?
Composer incluye herramientas muy potentes para investigar su propio árbol de dependencias:
composer show
composer outdated
composer why
composer why-not
composer prohibits
👉 En el próximo artículo podemos aprender a diagnosticar Composer: entender conflictos de versiones, descubrir quién requiere un paquete y resolver actualizaciones bloqueadas sin recurrir al ensayo y error.