¿Qué es Composer?
Composer es un gestor de dependencias para PHP, parecido a NPM o Yarn en JavaScript y a pip en Python. Simplifica la gestión de los paquetes de los que depende tu aplicación. Como «servicio» añadido, Composer permite que los paquetes registren un autocargador. Esa combinación lo convierte, en mi opinión, en una de las herramientas más críticas del ecosistema PHP.
El repositorio de paquetes más usado por Composer es Packagist.org. Ese repositorio solo acepta paquetes open source. Si quieres aprovechar todo lo que ofrece Composer, puedes recurrir a su oferta privada. Es una versión de pago de Packagist, construida por los creadores de Composer, que permite usar paquetes de código cerrado con un control de acceso granular.
Aquí no voy a explicar cómo instalar y configurar Composer: los mantenedores lo detallan en la página de inicio del proyecto, en la introducción a Composer de su web. En todos los ejemplos en los que uso la CLI de Composer doy por hecho que está instalado globalmente y que se invoca con composer.
Separar las dependencias normales de las de desarrollo
Composer distingue dos tipos de dependencias: las normales y las de desarrollo. Las normales son las que tu código necesita siempre, sea cual sea el entorno en el que se ejecuta. Las de desarrollo solo hacen falta mientras se desarrolla: frameworks de test como PHPUnit, analizadores estáticos como Psalm o revisores de estilo como PHP Code Sniffer. No son necesarias para ejecutar el código en producción. Cuando añades una dependencia con composer require [package] queda marcada como «dependencia normal». Con la opción --dev, el paquete se marca como «dependencia de desarrollo».
Al ejecutar composer install se descargan e instalan las dependencias de producción y las de desarrollo de tu aplicación. Para descargar e instalar únicamente las de producción, añade la opción --no-dev. Es lo que normalmente quieres en tu pipeline de despliegue, donde las herramientas de desarrollo no pintan nada.
Ojo composer install descarga e instala solo las dependencias de desarrollo de tu aplicación o de tu paquete, no las de tus dependencias. Si dependes del «Paquete A» y el «Paquete A» tiene una dependencia de desarrollo con el «Paquete B», solo se descargará e instalará el «Paquete A» en tu proyecto.
Las dependencias de desarrollo se acumulan hasta formar una lista respetable y, con ella, una buena cantidad de archivos. Todos esos archivos pasan por el autocargador. Reducir al mínimo las dependencias de producción ahorra espacio de almacenamiento, tiempo de transferencia (durante el despliegue) y tiempo de ejecución. A pequeña escala el ahorro es insignificante, pero en cuanto crecen el tamaño de la aplicación o el número de personas que la mantienen, empieza a notarse. Marcar las dependencias de desarrollo tiene otra ventaja: quedan fuera del autocargador que genera Composer al ejecutar composer install --no-dev.
Carga automática ( autoload.php )
Como decía antes, otra característica de Composer es que ofrece utilidades para que los paquetes se enganchen a tu proyecto PHP mediante la carga automática ( autoload.php ). Con esta funcionalidad, los paquetes indican cómo hacer corresponder un nombre de clase con un nombre de archivo. Ya no tienes que llenar el código de llamadas a require_once y require: basta con incluir vendor/autoload.php.
Igual que con las dependencias, Composer propone dos tipos de autocargadores: las directivas autoload normales y las de desarrollo. Los paquetes lo aprovechan para reducir su tamaño y el de su autocargador, pero las aplicaciones también salen ganando. En un proyecto bien probado, los archivos que solo sirven para los tests pueden ser una parte importante del código, igual que las herramientas de línea de comandos. En producción no los necesitas. Si le indicas al autocargador que son archivos de desarrollo, quedarán fuera al ejecutar composer install --no-dev, lo que puede mejorar el rendimiento de tu aplicación. La distinción entre el autoload normal y el autoload de desarrollo se hace en el archivo de configuración composer.json:
{
"autoload": {
"psr-4": {
"MyApp\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"MyApp\Tests": "tests/"
}
}
}
Composer ofrece herramientas adicionales para acelerar la carga automática de funciones y clases mediante la optimización del autoloader.
Restricciones de versión
Antes de Composer, el desarrollador descargaba una versión concreta de una dependencia y la metía en el control de versiones. Con Composer podemos alejarnos de esa versión concreta y dejar que él decida exactamente cuál instalar. Para decidirlo se apoya en el concepto de versionado semántico. Y no solo lo hace con tu aplicación: extiende el mecanismo a las dependencias, que pueden tener las suyas propias, y así sucesivamente. El resultado son más paquetes, pero más pequeños. Y esos paquetes pequeños pueden centrarse en una única funcionalidad, lo que permite centralizarla.
Frente a NPM, por ejemplo, Composer solo admite una versión de cada paquete por aplicación. Se instalan menos paquetes, pero aparece el riesgo de colisiones de versiones. Es una buena razón para no depender de una versión concreta de una dependencia y pedir un rango de versiones. Normalmente se hace con el operador caret: ^1.0. Eso significa que se acepta cualquier versión >= 1.0.0 y < 2.0.0. Como se trata de versionado semántico, tienes garantizadas las funcionalidades que necesitas sin que se cuele ninguna ruptura de compatibilidad.
Al indicar un rango en lugar de una versión concreta, evitas las colisiones cuando otra dependencia usa el mismo paquete. Imagina esta situación:
El composer.json de tu aplicación:
{
"require": {
"vendor/package": "^1.4",
"another_vendor/package": "^1.0"
}
}
El composer.json de vendor/package:
{
"require": {
"another_vendor/package": "^1.2"
}
}
Al instalar las dependencias, Composer detectará que hay varias peticiones de another_vendor/package, pero como son restricciones podrá deducir que debe evitar la versión 1.1.5 e instalar más bien la 1.2.8.
Composer tiene varios operadores de restricción y permite incluso combinarlos. Una restricción como «php»: «^7.2» no incluye PHP 8. Para indicar que admites PHP 7.2 y PHP 8, combina dos operadores: «php»: «^7.2 || ^8.0».
Ojo Otra opción sería seguir el camino que tomó Symfony y optar por "php": >= 7.2". Personalmente no me convence, porque da a entender que el código también funcionará con PHP 9 y posteriores. Como no hay forma de saber qué rupturas de compatibilidad traerá PHP 9, prefiero no prometer tanto, y por eso he optado por "php": "^7.2 || ^8.0" en mis repositorios.
Paquete raíz o no
Después de analizar un composer.json se puede distinguir entre las dependencias que tu proyecto requiere directamente en la raíz y las que no. En la terminología de Composer, los root packages son las dependencias de la raíz que arrastran sus propias dependencias.
Tomemos este composer.json:
{
"require": {
"laminas/laminas-diactoros": "^2.0"
}
}
Ejecutar el comando composer install sobre este archivo mostraría que no solo se descarga laminas/laminas-diactoros, sino también laminas/laminas-zendframework-bridge, psr/http-factory y psr-http-message. En este ejemplo, laminas/laminas-diactoros es un root package fundamental en nuestro proyecto.
La diferencia entre un paquete de la raíz y uno derivado está en la cantidad de dependencias directas de tu proyecto. En general, menos paquetes en la raíz ( root package ) significa menos paquetes y menos conflictos al actualizarlos. Menos paquetes porque una versión nueva puede traer dependencias distintas, que Composer gestiona por su cuenta. Y menos conflictos porque hay menos restricciones en tu árbol de dependencias.
Pero ¿qué hace que un paquete merezca estar en la raíz?
{
"require": {
"laminas/laminas-diactoros": "^2.0"
}
}
Volvamos al ejemplo en el que solo dependemos de laminas/laminas-diactoros. Diactoros ofrece una implementación de las interfaces especificadas en psr-http-message y psr/http-factory. Si en mi código solo hago referencia a objetos de laminas/laminas-diactoros, sería una configuración de Composer válida. Pero si me apoyo en las interfaces definidas en psr-http-message y psr/http-factory, que en mi opinión es la buena práctica, entonces tengo un motivo para subir psr-http-message y psr/http-factory a la raíz de nuestro proyecto ( root package ).
{
"require": {
"laminas/laminas-diactoros": "^2.0",
"psr-http-message": "^1.0",
"psr/http-factory": "^1.0"
}
}
Especificar tu entorno
Construyes tu aplicación o tu paquete sobre una versión concreta de PHP. Y solo puedes garantizar que el código funciona en esa versión. Puede que además necesite alguna extensión de PHP instalada. Por eso conviene precisar qué versiones de PHP se requieren o se admiten y qué extensiones hacen falta.
Cuando desarrollas un paquete, las versiones de PHP y las extensiones se declaran como si fueran paquetes normales:
{
"require": {
"php": "^7.2",
"ext-curl": "*"
}
}
Podemos usar el comodín * para la extensión cURL porque va ligada a PHP y no hace falta precisar su versión.
También puedes aplicar este enfoque al construir una aplicación y, además, declarar esas restricciones como configuración de la plataforma:
{
"config": {
"platform": {
"php": "7.4.4",
"ext-curl": "*"
}
}
}
La ventaja de este método es que Composer usará PHP 7.4.4 como versión de referencia, aunque el sistema donde se ejecutan sus comandos tenga instalada otra distinta. Con la tendencia a virtualizar con Docker y Vagrant, esto permite lanzar los comandos de Composer en local y ejecutar la aplicación en el entorno virtualizado. También evita que un compañero con una versión de PHP muy reciente instale versiones de dependencias que el entorno de producción no admite.
Trabajar con un control de versiones ( Git, Mercurial, Subversion )
En tu proyecto, Composer deja tres cosas en el sistema de archivos: dos archivos llamados composer.json y composer.lock, y una carpeta llamada vendor*. Pero no hace falta versionar las tres.
El archivo composer.json contiene todos los paquetes requeridos con sus restricciones de versión, mira el apartado «Restricciones de versión» para más detalle. Ese archivo debe estar siempre en el control de versiones. Sin él, quien quiera usar tu código no tiene forma de saber qué paquetes necesita.
El archivo composer.lock contiene las versiones exactas, con sus sumas de verificación (checksum), de todos los paquetes. Si está presente al ejecutar composer install, se instalarán exactamente las versiones especificadas en ese composer.lock. Así te aseguras de que las dependencias sean idénticas en todos los entornos. Al crear una aplicación conviene versionarlo. La razón principal es que las aplicaciones (u otros paquetes) que usen el tuyo no lo tendrán en cuenta, pero tu entorno de desarrollo local sí. Eso puede llevarte a desarrollar o a probar con versiones obsoletas de tus dependencias mientras los usuarios de tu paquete usan las últimas.
La carpeta vendor contiene el código de los paquetes. Su contenido no debe versionarse. Si usas Git, hay que añadirla al .gitignore para dejarla fuera del repositorio.
* Puedes cambiar el nombre y la ubicación de esa carpeta con la opción de configuración vendor-dir, pero en este artículo me quedo con el valor por defecto.
Modificar las dependencias
Imagina que quieres actualizar una dependencia de tu aplicación. O varias. Lanzas unos cuantos composer update y ves cambiar composer.lock. Creas un commit. Otra tanda de actualizaciones y otro commit. Unos días después retomas el trabajo donde lo dejaste. El repositorio se mueve mucho, así que antes de seguir haces un rebase para asegurarte de que tus cambios son compatibles con la última versión del código. ¡Conflicto de merge! Un conflicto en composer.json se resuelve como cualquier otro, pero un conflicto en composer.lock cuesta más.
La forma de resolver un conflicto de merge en un composer.lock es aceptar los cambios entrantes, que son los más recientes, y volver a lanzar los comandos de Composer que provocaron tus cambios. Así se aplicarán sobre la versión más reciente de tu código. Haz commit de los cambios de composer.lock y continúa el rebase. Si tocaste Composer en otro commit, tendrás que repetir estos pasos.
Conclusión
Composer se ha convertido en una pieza esencial del desarrollo en PHP. Como todo gestor de paquetes, admite optimizaciones. Lo uso a diario en varios repositorios y por el camino he adoptado algunas prácticas que considero buenas:
- Separa tus dependencias en dependencias de producción y de desarrollo para reducir el tamaño de la aplicación.
- Separa tu autocargador en autocargador de producción y de desarrollo para reducir su tamaño y ganar rendimiento.
- No dependas de una versión fija de tus dependencias, sino de un rango, para que actualizar los paquetes duela menos.
- Depende directamente solo de aquello que realmente llamas en tu aplicación, para simplificar una actualización o el cambio a otra biblioteca.
- Pon siempre
composer.jsonen el control de versiones, perocomposer.locksolo en las aplicaciones, para no impedir que los usuarios de tu paquete actualicen otros paquetes. - Intenta no pasar de un commit por pull request o merge request con los cambios de
composer.jsonycomposer.lock, para minimizar los conflictos de fusión en esos archivos.



