Codice PHP “vanilla” senza framework: come introdurre Composer, autoload e test

Come portare Composer, l’autoload con classmap, PSR-4 e files e i primi test di caratterizzazione con PHPUnit in un’applicazione PHP senza framework, senza riscriverla e senza cambiarne il comportamento, come base per la migrazione a PHP 8.

Copertina con il logo php∞, la categoria Framework e codice custom, il titolo PHP senza framework: Composer e test e il numero 44

Molti gestionali, portali e piccoli e-commerce scritti in PHP non usano alcun framework. Sono nati quindici o vent’anni fa da poche pagine, sono cresciuti per aggiunte successive e oggi contano centinaia di file collegati da lunghe catene di include e require_once, con funzioni globali, librerie copiate a mano in una cartella e nessun test. Funzionano, ma ogni modifica è un salto nel buio e la migrazione a PHP 8 sembra un progetto troppo grande per cominciare.

In questo articolo vediamo come introdurre Composer, l’autoload e i primi test in un’applicazione PHP “vanilla” senza riscriverla e senza cambiarne il comportamento. È il lavoro preparatorio che rende la migrazione misurabile e sicura: un passo alla volta, con comandi reali e con l’ordine che seguiamo nei nostri interventi.

Perché partire da qui

Un’applicazione senza struttura comune ha tre problemi che si sommano. Il primo riguarda le dipendenze: librerie come PHPMailer, generatori di PDF o client SOAP sono spesso copiate in una cartella anni fa, in versioni che nessuno aggiorna e che non funzionano sulle versioni recenti di PHP. Il secondo riguarda il caricamento del codice: l’ordine delle inclusioni è fragile, la stessa classe può essere inclusa da percorsi diversi e aggiungere un file significa ritoccare decine di pagine. Il terzo riguarda la verifica: senza test, l’unico modo per sapere se una modifica ha rotto qualcosa è aspettare la segnalazione di un utente.

Composer risolve i primi due, PHPUnit comincia a risolvere il terzo. Nessuno dei due richiede di adottare un framework o di riorganizzare il progetto. Si aggiungono accanto al codice esistente e producono subito un beneficio concreto: un elenco esplicito delle dipendenze, un solo punto di caricamento e una rete di sicurezza per le modifiche successive.

Prima di tutto, l’inventario

Prima di creare qualsiasi file conviene sapere su cosa si sta lavorando. Le domande da porsi sono poche:

  • quale versione di PHP esegue l’applicazione in produzione, con quali estensioni attive;
  • quali librerie di terze parti sono incluse nel codice, in che versione e da dove arrivano;
  • quali file definiscono classi, quali solo funzioni e quali sono pagine che producono output;
  • se esiste già una funzione __autoload() o un caricatore scritto a mano;
  • quali sono i flussi che non devono rompersi mai: login, ordini, fatturazione, esportazioni.

La versione di PHP conta perché determina gli strumenti utilizzabili. Le versioni correnti di Composer richiedono PHP 7.2.5 o superiore; per chi è fermo su versioni più vecchie, la documentazione di Composer indica il ramo di supporto a lungo termine 2.2, che funziona ancora con PHP 5.3.2 e successivi. Lo stesso vale per PHPUnit, le cui versioni principali sono legate a precise versioni di PHP, come mostra la tabella più avanti.

Per l’analisi del codice esistente, gli strumenti di analisi statica aiutano a capire quanto è distante da PHP 8: ne parliamo nell’articolo su PHPCompatibility, PHPStan e Rector.

Il file composer.json

Il cuore di tutto è un file composer.json nella radice del progetto. Ecco un esempio per un gestionale immaginario su PHP 7.4, con le classi storiche in lib/, le funzioni globali in inc/funzioni.php e una nuova cartella src/ per il codice scritto da qui in avanti:

{
    "name": "azienda/gestionale",
    "type": "project",
    "require": {
        "php": ">=7.4"
    },
    "require-dev": {
        "phpunit/phpunit": "^9.6"
    },
    "autoload": {
        "classmap": ["lib/"],
        "psr-4": { "App\\": "src/" },
        "files": ["inc/funzioni.php"]
    },
    "autoload-dev": {
        "psr-4": { "Tests\\": "tests/" }
    },
    "config": {
        "platform": { "php": "7.4.33" }
    }
}

La sezione config.platform dice a Composer di scegliere le versioni delle librerie come se girasse su PHP 7.4.33, anche quando lo sviluppatore ha in locale una versione più recente. Evita di installare pacchetti che in produzione non funzionerebbero. La documentazione avverte che con questa impostazione Composer non controlla più la versione reale: per questo, al rilascio, conviene eseguire composer check-platform-reqs sul server.

Autoload: tre strumenti per tre tipi di codice

Composer offre diversi modi per caricare automaticamente il codice, descritti nella documentazione dello schema. In un progetto vanilla se ne usano tre, ognuno per un tipo di file.

Diagramma in tre colonne. A sinistra, prima: un file index.php con una lista di require_once verso config.php, lib/db.php, lib/Utente.php, lib/Ordine.php, lib/Fattura.php, inc/funzioni.php e inc/date.php, ripetuta in ogni pagina, con la nota ordine fragile e inclusioni duplicate. Al centro le quattro sezioni di composer.json: classmap su lib/ per le classi esistenti senza namespace, psr-4 che associa App a src/ per le classi nuove, files su inc/funzioni.php per le funzioni globali caricate sempre, autoload-dev che associa Tests a tests/ solo in sviluppo. A destra, dopo: il comando composer dump-autoload genera vendor/autoload.php e i file autoload_classmap, autoload_psr4 e autoload_files, e index.php contiene una sola riga require di vendor/autoload.php
Le regole di autoload sostituiscono le catene di inclusioni con un solo punto di caricamento.

classmap per il codice esistente

La regola classmap analizza tutti i file .php e .inc delle cartelle indicate e costruisce una mappa che associa ogni classe al proprio file. Non richiede namespace né una struttura particolare: funziona anche con classi come Ordine o DB_Connessione definite in file dal nome qualunque. È lo strumento giusto per il primo passo, perché non obbliga a spostare o rinominare nulla. Il limite è che la mappa è statica: ogni volta che si aggiunge una classe in quelle cartelle bisogna rigenerarla con composer dump-autoload.

psr-4 per il codice nuovo

Con psr-4 il nome della classe determina il percorso del file: App\Fatture\Numeratore si trova in src/Fatture/Numeratore.php. Le nuove classi vengono trovate senza rigenerare nulla. È la convenzione da adottare per tutto il codice scritto o estratto da qui in avanti, così che la parte ordinata del progetto cresca e quella storica si riduca.

files per le funzioni globali

PHP sa caricare automaticamente le classi, non le funzioni. I file che definiscono funzioni globali vanno quindi elencati in files: Composer li include a ogni richiesta, subito dopo vendor/autoload.php. Va fatto con criterio, perché un file in questa lista viene sempre caricato. Inserisci solo file che definiscono funzioni o costanti, mai pagine che producono output o aprono connessioni al database.

Il vecchio __autoload()

Se il progetto definisce una funzione __autoload(), attenzione: è deprecata da PHP 7.2 e rimossa in PHP 8.0, e quando si registra un caricatore con spl_autoload_register(), come fa Composer, PHP smette di chiamarla. Il caricatore storico va quindi registrato in modo esplicito oppure, meglio, sostituito dalle regole di Composer dopo aver verificato quali classi caricava.

I comandi, nell’ordine

Con il file pronto, i passaggi sono questi. Lavora su un ramo del repository e in un ambiente di prova, mai direttamente in produzione:

# Controlla che composer.json sia valido
composer validate

# Genera vendor/autoload.php con le regole di autoload
composer dump-autoload

# Aggiunge PHPUnit solo come dipendenza di sviluppo
composer require --dev phpunit/phpunit:^9.6

# Cerca avvisi di sicurezza e pacchetti abbandonati tra le dipendenze
composer audit

# In produzione: niente pacchetti di sviluppo, autoload ottimizzato
composer install --no-dev --optimize-autoloader
composer check-platform-reqs

Poi, nel file di avvio comune a tutte le pagine (spesso un config.php o un bootstrap.php), si aggiunge la riga require __DIR__ . '/vendor/autoload.php';. Le vecchie inclusioni delle classi possono restare al loro posto all’inizio: con require_once e un autoload attivo non succede nulla di male. Si tolgono gradualmente, una pagina alla volta, verificando che tutto funzioni.

Le librerie copiate a mano meritano un discorso a parte. Se esistono come pacchetti su Packagist, sostituiscile con composer require nella versione più vicina a quella in uso, poi aggiorna per gradi. Se sono state modificate localmente, confronta prima il codice con l’originale: le modifiche nascoste sono una delle sorprese più comuni.

I primi test: fotografare il comportamento attuale

Su un codice senza test non ha senso partire dai test unitari “da manuale”. Il primo obiettivo è registrare cosa fa oggi l’applicazione, anche quando il risultato sembra discutibile: sono i cosiddetti test di caratterizzazione. Si sceglie una funzione critica, la si esegue con dati realistici, si annota il risultato e lo si fissa in un test. Se una modifica futura cambia quel risultato, il test fallisce e lo segnala.

Un file phpunit.xml minimo nella radice del progetto:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Carica l'autoload di Composer prima di ogni test -->
<phpunit bootstrap="vendor/autoload.php" colors="true">
    <testsuites>
        <testsuite name="caratterizzazione">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

E un test per una funzione immaginaria calcola_totale() definita in inc/funzioni.php:

<?php
// tests/CalcoloTotaleTest.php
namespace Tests;

use PHPUnit\Framework\TestCase;

final class CalcoloTotaleTest extends TestCase
{
    public function testTotaleConIvaRestaQuelloDiOggi(): void
    {
        // Valore ottenuto eseguendo il codice attuale, non da una specifica
        $this->assertSame('122.00', calcola_totale(100, 22));
    }

    public function testImportoZeroNonGeneraErrori(): void
    {
        $this->assertSame('0.00', calcola_totale(0, 22));
    }
}

Si esegue con vendor/bin/phpunit. Per le pagine che mescolano HTML, query e logica, dove una funzione da chiamare non esiste, il test di caratterizzazione si fa dall’esterno: si richiede la pagina in un ambiente di prova con un database noto e si confronta l’output, o una sua parte, con quello registrato. Sono test più lenti e più fragili, ma coprono subito i flussi che contano.

Quale PHPUnit per quale PHP

La versione di PHPUnit dipende da quella di PHP. La tabella riassume i requisiti pubblicati nella pagina delle versioni supportate di PHPUnit.

PHPUnitVersione di PHP richiesta
5da PHP 5.6 a 7.1
6da PHP 7.0 a 7.2
7da PHP 7.1 a 7.3
8PHP 7.2 o successivo
9PHP 7.3 o successivo
10PHP 8.1 o successivo
11PHP 8.2 o successivo
12PHP 8.3 o successivo
13PHP 8.4 o successivo
Fonte: phpunit.de, novembre 2026.

Su PHP 5.6 si resta quindi su PHPUnit 5, senza tipi di ritorno come : void nei test. Non è un ostacolo: i test scritti oggi sul vecchio PHP sono proprio quelli che dimostreranno, dopo la migrazione, che il comportamento non è cambiato. Al passaggio a PHP 8 si aggiorna PHPUnit e si adattano i test, che di solito richiedono modifiche minime.

Un percorso in sei passi

Linea del tempo con sei passi numerati: 1 inventario di versione di PHP, estensioni e include; 2 composer.json con il vincolo su php e config.platform; 3 classmap per l’autoload del codice esistente così com’è; 4 test di caratterizzazione sui flussi critici; 5 PSR-4 per classi nuove e codice estratto; 6 controlli con composer audit e test a ogni modifica. Una fascia viola sotto i primi tre passi dice nessun cambiamento di comportamento, una fascia verde acqua sotto gli ultimi tre dice rete di sicurezza per cambiare il codice. Nota: i primi tre passi si fanno anche su PHP 5.6 con Composer 2.2 LTS
Prima l’ordine senza toccare la logica, poi i test, poi le modifiche.

L’ordine dei passi è la parte importante. I primi tre non cambiano il comportamento dell’applicazione: aggiungono un file di configurazione e un caricatore, e possono andare in produzione in tempi brevi. Il quarto crea la rete di sicurezza. Solo dopo ha senso estrarre codice in classi nuove, sostituire librerie e affrontare le incompatibilità con PHP 8, come il passaggio dalle vecchie funzioni mysql_* a PDO che descriviamo nell’articolo su come migrare da mysql a PDO.

Il sesto passo rende il lavoro duraturo: composer audit e la suite di test vanno eseguiti a ogni modifica, a mano o in una pipeline di integrazione continua. Così le dipendenze vulnerabili emergono subito e ogni regressione viene intercettata prima del rilascio.

Errori da evitare

  • Pubblicare la cartella vendor/ sul web. La radice pubblica del sito dovrebbe essere una sottocartella, oppure vendor/ va protetta dal server web. Nei progetti storici la radice coincide spesso con quella del repository.
  • Aggiungere namespace al codice esistente subito. Rinominare le classi tocca ogni punto in cui sono usate: si fa dopo, con i test, una classe alla volta.
  • Aggiornare tutte le librerie insieme. Un salto di più versioni principali alla volta rende impossibile capire cosa ha causato un errore.
  • Dimenticare il file composer.lock. Va salvato nel repository, perché garantisce che produzione e sviluppo installino esattamente le stesse versioni.
  • Mettere tutto in files. Ogni file elencato viene caricato a ogni richiesta, con effetti su prestazioni e comportamento.

Possiamo dunque riassumere

Un’applicazione PHP senza framework si può mettere in ordine senza riscriverla. Composer, con un composer.json che usa classmap per il codice esistente, psr-4 per quello nuovo e files per le funzioni globali, sostituisce le catene di inclusioni con un solo punto di caricamento e rende esplicite le dipendenze. PHPUnit, scelto in base alla versione di PHP, permette di fissare con test di caratterizzazione il comportamento attuale dei flussi critici. Sono passi piccoli e reversibili, che funzionano anche su versioni vecchie di PHP con Composer 2.2 LTS, e che trasformano la migrazione a PHP 8 da salto nel buio a lavoro misurabile. Se vuoi impostarli sul tuo progetto, possiamo affiancarti nella migrazione del codice custom.

IL PROSSIMO PASSO

Mettiamo
in sicurezza.

Indicaci versione di PHP, sistema operativo e cosa ti preoccupa. Ti risponderemo con una valutazione dell’esposizione e una proposta di intervento.

Richiedi supporto

Ti porta al modulo di richiesta, in quattro passi.

PHP LEGACY · MANAGED SERVER S.R.L.info@phplegacy.it +39 02 5656 9681

Lunedì – venerdì · 09:30 – 19:30

Urgenza in corso? Reperibilità H24
+39 340 985 4556 ↗