Un cambio aparentemente menor en una librería PHP compartida puede detener entregas independientes. Renombrar un parámetro, cambiar un valor por defecto o sustituir una excepción puede romper a un consumidor que no se despliega hoy, que vive en otro repositorio o que invoca el componente de forma indirecta. El fallo puede aparecer en tiempo de ejecución, en una tarea asíncrona o al deserializar datos generados antes del cambio.
La compatibilidad hacia atrás en PHP no consiste en conservar toda interfaz histórica. Es una disciplina para permitir que productores y consumidores evolucionen a ritmos distintos, con una ventana de migración explícita y una retirada verificable. El objetivo es evitar tanto los despliegues coordinados forzosos como la acumulación permanente de APIs obsoletas.
Identificar qué forma parte del contrato interno

Un contrato interno es cualquier comportamiento del que otro módulo depende, aunque no esté publicado como API externa. Las dependencias de Composer y las interfaces PHP son una parte visible, pero no agotan el alcance. Antes de cambiar código compartido, revise al menos estos elementos:
- Firmas públicas: nombres de métodos, parámetros, orden, tipos, nulabilidad, valores por defecto y tipo de retorno.
- Semántica: qué significa cada argumento, qué campos son obligatorios y qué resultado se espera ante una condición concreta.
- Errores: excepciones lanzadas, códigos de error, mensajes procesados por clientes y resultados nulos o vacíos.
- Datos: claves de arrays, estructuras JSON, mensajes de cola, eventos de dominio, archivos serializados y datos persistidos.
- Efectos secundarios: envío de eventos, escritura en base de datos, invalidación de caché, llamadas HTTP y orden de ejecución.
- Comportamiento operativo: reintentos, idempotencia, límites de tiempo y tratamiento de fallos transitorios.
Por ejemplo, añadir un campo a una respuesta JSON suele ser aditivo, pero deja de serlo si un consumidor valida una lista cerrada de propiedades. Del mismo modo, una excepción más específica puede ser técnicamente correcta, pero incompatible si el consumidor captura la excepción anterior para activar una recuperación.
Clasificar el cambio antes de escribir la implementación
La clasificación evita que una decisión de diseño se convierta en una incidencia de producción. Conviene documentarla en la propuesta del cambio, junto con los consumidores conocidos y la estrategia de salida.
Cambios aditivos
Incorporan una nueva capacidad sin alterar la ruta existente: un método nuevo, un parámetro opcional con semántica neutra, un evento adicional o una nueva versión de un mensaje. Son la opción preferible cuando los consumidores se despliegan por separado. La nueva vía debe poder convivir con la anterior y el comportamiento previo debe conservarse de forma comprobable.
Cambios compatibles con adaptación
Permiten mantener el resultado anterior mediante una capa de traducción. Por ejemplo, una interfaz antigua puede delegar en un nuevo servicio, convirtiendo argumentos y resultados. La adaptación tiene sentido si está localizada, tiene fecha de retirada y no oculta una diferencia de negocio que el consumidor deba decidir conscientemente.
Cambios incompatibles o inciertos
Eliminar un método, endurecer un tipo, cambiar el significado de un estado o modificar un formato persistido suele ser incompatible. También debe tratarse como incierto cualquier cambio sin inventario fiable de consumidores. En ambos casos, no basta con publicar una nueva versión del paquete: hace falta una transición, una migración planificada o una versión de contrato separada.
Construir un inventario verificable de consumidores
No base la decisión sólo en búsquedas de texto. Un componente puede llegar a otro mediante un contenedor de dependencias, una configuración, reflexión, eventos, colas o una integración HTTP. El inventario debe combinar evidencia estática y ejecución representativa.
- Revise dependencias declaradas en Composer, restricciones de versiones y repositorios que instalan el paquete.
- Busque usos directos de clases, interfaces, métodos, eventos, claves de configuración y formatos de mensaje.
- Inspeccione fábricas, definiciones del contenedor, listeners, comandos, cron, workers y adaptadores de infraestructura.
- Identifique rutas críticas: cobro, autenticación, pedidos, sincronización, notificaciones y procesos de recuperación.
- Registre para cada consumidor el propietario, la versión usada, la vía de migración y la evidencia de que completó el cambio.
La publicación de una librería y el despliegue de una aplicación son acciones distintas. Publicar una versión compatible permite a cada consumidor actualizar cuando esté preparado; desplegar de forma simultánea todos los consumidores convierte una evolución ordinaria en una dependencia organizativa frágil.
Aplicar evolución aditiva y adaptadores en el borde correcto
Cuando un nuevo requisito altera el modelo, introduzca primero una capacidad nueva y conserve temporalmente la anterior. Una interfaz heredada puede delegar en la nueva implementación, siempre que la conversión sea inequívoca. Así los consumidores migran sin tener que coordinar una ventana única.
interface LegacyPriceCalculator
{
public function calculate(int $amount): int;
}
final class LegacyPriceCalculatorAdapter implements LegacyPriceCalculator
{
public function __construct(private PriceCalculator $calculator) {}
public function calculate(int $amount): int
{
return $this->calculator->calculate(new Money($amount, 'EUR'))->amount();
}
}El adaptador pertenece normalmente al borde entre contratos, no al núcleo del dominio. El dominio debe expresar el modelo actual; la traducción de argumentos antiguos, valores centinela o formatos históricos debe quedar en una capa dedicada. Si el dominio conserva condiciones para cada generación de clientes, la complejidad histórica se propaga a toda modificación futura.
No fuerce un adaptador cuando haya pérdida de información o una decisión de negocio nueva. Si el contrato antiguo no contiene los datos necesarios para el nuevo comportamiento, mantenga ambos contratos durante la transición o solicite al consumidor la información adicional de manera explícita.
Convertir la deprecación en una retirada gestionada
Una API marcada como obsoleta sin alternativa, plazo ni responsable no es una deprecación: es deuda sin seguimiento. Una retirada útil debe incluir una señal en el código, instrucciones de migración, una condición de eliminación y observación del uso cuando sea posible.
- Marque el método o clase heredada con documentación clara y, si corresponde, emita un aviso controlado con
trigger_error(..., E_USER_DEPRECATED). - Indique la alternativa exacta, incluidas diferencias de semántica, errores y valores por defecto.
- Defina una condición de salida verificable: todos los repositorios inventariados migrados, ausencia de llamadas observadas o fin de soporte de una versión concreta.
- Asigne un responsable que revise el avance y retire la capa cuando se cumpla la condición.
Evite emitir avisos indiscriminados en rutas de alto volumen sin una estrategia de agregación: el ruido puede ocultar señales relevantes y elevar el coste operativo. La observabilidad debe responder a una pregunta concreta: qué consumidores siguen usando el contrato anterior y en qué ruta.
Probar la transición y ejecutar la secuencia de entrega
Las pruebas unitarias del componente no demuestran por sí solas que los consumidores continúen funcionando. Añada pruebas de contrato para las entradas, salidas y errores que cada consumidor necesita. Mantenga casos de regresión para la interfaz antigua mientras esté soportada y pruebe explícitamente valores ausentes, cargas serializadas previas y excepciones esperadas.
La secuencia segura suele seguir este orden:
- Publicar el contrato nuevo o la implementación aditiva manteniendo la ruta anterior.
- Actualizar y desplegar consumidores de forma independiente, usando pruebas de integración donde el riesgo lo justifique.
- Observar errores, avisos de deprecación y uso de la interfaz heredada.
- Confirmar el inventario de migración y resolver consumidores indirectos detectados.
- Retirar el adaptador o contrato antiguo en una entrega separada, con pruebas que confirmen su ausencia.
Lista de comprobación para aprobar el cambio

- ¿Está definido el contrato afectado más allá de la firma PHP?
- ¿El cambio está clasificado como aditivo, adaptable, incompatible o incierto?
- ¿Existe un inventario de consumidores, incluidos eventos, datos y rutas indirectas?
- ¿La solución evita exigir despliegues simultáneos?
- ¿El adaptador, si existe, está fuera del dominio y tiene retirada prevista?
- ¿Se han probado comportamiento previo, nueva capacidad y errores esperados?
- ¿La deprecación indica alternativa, condición de retirada y responsable?
- ¿Hay una señal para detectar dependencias ocultas antes de eliminar la API?
La decisión correcta no es mantener compatibilidad indefinida ni imponer coordinación total. Es diseñar una transición con límites: preservar lo necesario, migrar con evidencia y eliminar la compatibilidad histórica cuando deja de aportar seguridad.



