Vai al contenuto
DedicatedPHP Contatto

Evolvere contratti PHP condivisi senza bloccare le delivery

Guida all'applicazione della compatibilità retroattiva in PHP quando si modificano componenti condivisi, si migrano i consumer e si ritirano API in modo controllato.

Diagramma editoriale di contratti PHP condivisi con adapter, consumer e fasi di migrazione

Una modifica apparentemente minima in una libreria PHP condivisa può fermare delivery indipendenti. Rinominare un parametro, cambiare un valore predefinito o sostituire un'eccezione può compromettere un consumer che non viene rilasciato oggi, che risiede in un altro repository o che invoca il componente in modo indiretto. Il problema può comparire a runtime, in un task asincrono o durante la deserializzazione di dati generati prima della modifica.

La compatibilità retroattiva in PHP non consiste nel mantenere ogni interfaccia storica. È una disciplina che consente a producer e consumer di evolvere a ritmi diversi, con una finestra di migrazione esplicita e una rimozione verificabile. L'obiettivo è evitare sia deployment coordinati forzati sia l'accumulo permanente di API obsolete.

Identificare cosa fa parte del contratto interno

Identificare cosa fa parte del contratto interno — guía visual de DedicatedPHP

Un contratto interno è qualsiasi comportamento da cui dipende un altro modulo, anche se non è pubblicato come API esterna. Le dipendenze Composer e le interfacce PHP sono una parte visibile, ma non esauriscono l'ambito. Prima di modificare codice condiviso, esaminare almeno questi elementi:

  • Firme pubbliche: nomi dei metodi, parametri, ordine, tipi, nullabilità, valori predefiniti e tipo di ritorno.
  • Semantica: cosa significa ogni argomento, quali campi sono obbligatori e quale risultato è previsto in una determinata condizione.
  • Errori: eccezioni lanciate, codici di errore, messaggi elaborati dai client e risultati nulli o vuoti.
  • Dati: chiavi degli array, strutture JSON, messaggi in coda, eventi di dominio, file serializzati e dati persistiti.
  • Effetti collaterali: invio di eventi, scrittura nel database, invalidazione della cache, chiamate HTTP e ordine di esecuzione.
  • Comportamento operativo: retry, idempotenza, timeout e gestione degli errori transitori.

Per esempio, aggiungere un campo a una risposta JSON è solitamente additivo, ma smette di esserlo se un consumer convalida un elenco chiuso di proprietà. Allo stesso modo, un'eccezione più specifica può essere tecnicamente corretta, ma incompatibile se il consumer intercetta l'eccezione precedente per attivare un recupero.

Classificare la modifica prima di scrivere l'implementazione

La classificazione evita che una decisione progettuale si trasformi in un incidente di produzione. È opportuno documentarla nella proposta di modifica, insieme ai consumer noti e alla strategia di uscita.

Modifiche additive

Introducono una nuova capacità senza alterare il percorso esistente: un nuovo metodo, un parametro opzionale con semantica neutra, un evento aggiuntivo o una nuova versione di un messaggio. Sono l'opzione preferibile quando i consumer vengono rilasciati separatamente. Il nuovo percorso deve poter coesistere con quello precedente e il comportamento preesistente deve essere mantenuto in modo verificabile.

Modifiche compatibili con adattamento

Consentono di mantenere il risultato precedente mediante un livello di traduzione. Per esempio, un'interfaccia legacy può delegare a un nuovo servizio, convertendo argomenti e risultati. L'adattamento ha senso se è localizzato, ha una data di rimozione e non nasconde una differenza di business che il consumer debba decidere consapevolmente.

Modifiche incompatibili o incerte

Rimuovere un metodo, rendere più restrittivo un tipo, cambiare il significato di uno stato o modificare un formato persistito è solitamente incompatibile. Deve essere trattata come incerta anche qualsiasi modifica senza un inventario affidabile dei consumer. In entrambi i casi, non basta pubblicare una nuova versione del pacchetto: servono una transizione, una migrazione pianificata o una versione separata del contratto.

Costruire un inventario verificabile dei consumer

Non basare la decisione solo su ricerche testuali. Un componente può raggiungerne un altro tramite un contenitore di dipendenze, una configurazione, reflection, eventi, code o un'integrazione HTTP. L'inventario deve combinare evidenza statica ed esecuzione rappresentativa.

  1. Esaminare le dipendenze dichiarate in Composer, i vincoli di versione e i repository che installano il pacchetto.
  2. Cercare utilizzi diretti di classi, interfacce, metodi, eventi, chiavi di configurazione e formati di messaggio.
  3. Ispezionare factory, definizioni del container, listener, comandi, cron, worker e adapter dell'infrastruttura.
  4. Identificare i percorsi critici: addebiti, autenticazione, ordini, sincronizzazione, notifiche e processi di recupero.
  5. Registrare per ogni consumer il responsabile, la versione utilizzata, il percorso di migrazione e l'evidenza che ha completato la modifica.

La pubblicazione di una libreria e il deployment di un'applicazione sono azioni diverse. Pubblicare una versione compatibile consente a ogni consumer di aggiornarsi quando è pronto; rilasciare contemporaneamente tutti i consumer trasforma un'evoluzione ordinaria in una dipendenza organizzativa fragile.

Applicare evoluzione additiva e adapter al confine corretto

Quando un nuovo requisito altera il modello, introdurre prima una nuova capacità e mantenere temporaneamente quella precedente. Un'interfaccia legacy può delegare alla nuova implementazione, purché la conversione sia univoca. In questo modo i consumer migrano senza dover coordinare un'unica finestra.

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();
    }
}

L'adapter appartiene normalmente al confine tra contratti, non al nucleo del dominio. Il dominio deve esprimere il modello attuale; la traduzione di argomenti legacy, valori sentinella o formati storici deve rimanere in un livello dedicato. Se il dominio conserva condizioni per ogni generazione di client, la complessità storica si propaga a ogni modifica futura.

Non forzare un adapter quando vi è perdita di informazioni o una nuova decisione di business. Se il contratto precedente non contiene i dati necessari per il nuovo comportamento, mantenere entrambi i contratti durante la transizione oppure richiedere esplicitamente al consumer le informazioni aggiuntive.

Trasformare la deprecazione in una rimozione gestita

Un'API contrassegnata come obsoleta senza alternativa, scadenza né responsabile non è una deprecazione: è debito senza monitoraggio. Una rimozione efficace deve includere un segnale nel codice, istruzioni di migrazione, una condizione di eliminazione e l'osservazione dell'utilizzo quando possibile.

  • Contrassegnare il metodo o la classe legacy con documentazione chiara e, se opportuno, emettere un avviso controllato con trigger_error(..., E_USER_DEPRECATED).
  • Indicare l'alternativa esatta, incluse le differenze di semantica, errori e valori predefiniti.
  • Definire una condizione di uscita verificabile: tutti i repository inventariati migrati, assenza di chiamate osservate o fine del supporto di una versione specifica.
  • Assegnare un responsabile che esamini l'avanzamento e rimuova il livello quando la condizione è soddisfatta.

Evitare di emettere avvisi indiscriminati nei percorsi ad alto volume senza una strategia di aggregazione: il rumore può nascondere segnali rilevanti e aumentare il costo operativo. L'osservabilità deve rispondere a una domanda concreta: quali consumer stanno ancora utilizzando il contratto precedente e in quale percorso.

Testare la transizione ed eseguire la sequenza di delivery

I test unitari del componente non dimostrano da soli che i consumer continuino a funzionare. Aggiungere test di contratto per gli input, gli output e gli errori necessari a ciascun consumer. Mantenere casi di regressione per l'interfaccia precedente finché è supportata e testare esplicitamente valori mancanti, payload serializzati precedenti ed eccezioni previste.

La sequenza sicura segue solitamente questo ordine:

  1. Pubblicare il nuovo contratto o l'implementazione additiva mantenendo il percorso precedente.
  2. Aggiornare e rilasciare i consumer in modo indipendente, utilizzando test di integrazione dove il rischio lo giustifica.
  3. Osservare errori, avvisi di deprecazione e utilizzo dell'interfaccia legacy.
  4. Confermare l'inventario della migrazione e risolvere i consumer indiretti rilevati.
  5. Rimuovere l'adapter o il contratto precedente in una delivery separata, con test che ne confermino l'assenza.

Checklist per approvare la modifica

Checklist per approvare la modifica — guía visual de DedicatedPHP
  • Il contratto interessato è definito oltre la firma PHP?
  • La modifica è classificata come additiva, adattabile, incompatibile o incerta?
  • Esiste un inventario dei consumer, inclusi eventi, dati e percorsi indiretti?
  • La soluzione evita di richiedere deployment simultanei?
  • L'adapter, se esiste, è esterno al dominio e ha una rimozione pianificata?
  • Sono stati testati il comportamento precedente, la nuova capacità e gli errori previsti?
  • La deprecazione indica alternativa, condizione di rimozione e responsabile?
  • Esiste un segnale per rilevare dipendenze nascoste prima di eliminare l'API?

La decisione corretta non è mantenere la compatibilità indefinitamente né imporre un coordinamento totale. È progettare una transizione con limiti: preservare ciò che serve, migrare con evidenza ed eliminare la compatibilità storica quando non apporta più sicurezza.

Vuoi applicare queste idee al tuo progetto?Parliamo della tua piattaforma PHP.
Visualizza il servizio correlato