Come ho introdotto CI/CD in una codebase Laravel senza test: il caso di un gestionale logistico con 14 sviluppatori e zero automazione

Come ho introdotto CI/CD in una codebase Laravel senza test: il caso di un gestionale logistico con 14 sviluppatori e zero automazione

Il 3 marzo 2025 sono entrato come consulente tecnico in un'azienda piemontese di distribuzione alimentare che gestisce la logistica per circa 180 ristoranti e mense aziendali nel quadrilatero Torino-Asti-Alessandria-Cuneo. Il loro gestionale core è un'applicazione Laravel 10 con 247 model Eloquent, 89 controller, un modulo di routing ottimizzato per i percorsi dei furgoni refrigerati, e un'integrazione bidirezionale con il magazzino fisico tramite API REST verso il WMS del fornitore. L'applicazione è mission-critical: se il gestionale si ferma la mattina alle 5, i furgoni partono senza i carichi ottimizzati e l'azienda stima una perdita di margine di circa 1.200 euro per ogni giornata di routing subottimale.

Il problema per cui mi hanno chiamato non era un bug, non era un rallentamento, non era un incidente di sicurezza. Il problema era il terrore. Quattordici sviluppatori - tre interni e undici di un'agenzia esterna - lavoravano sulla stessa codebase da quattro anni senza test automatici, senza analisi statica, senza pipeline di continuous integration, e con un processo di deploy che consisteva nel collegare FileZilla al server di produzione e trascinare i file modificati la sera del venerdì, pregando che non si rompesse nulla durante il weekend. L'ultimo incidente grave era avvenuto tre settimane prima del mio arrivo: un deploy del venerdì aveva introdotto una regressione nel calcolo delle quantità per il picking di magazzino, scoperta lunedì mattina quando il magazziniere aveva segnalato che i colli preparati non corrispondevano agli ordini. Tre giorni di lavoro per identificare il commit responsabile, perché nessuno aveva tracciabilità su cosa fosse stato deployato e quando.

Il direttore operativo mi ha detto una frase che sento spesso in questi contesti: "Non possiamo permetterci di introdurre test perché rallenterebbero lo sviluppo". La mia risposta è stata: "Non potete permettervi di non farlo, perché il costo dell'incidente di tre settimane fa è già superiore al costo dell'intera pipeline che vi sto per proporre".

Perché una codebase Laravel senza CI/CD accumula debito tecnico in modo esponenziale?

Il debito tecnico in un'applicazione Laravel senza automazione non cresce in modo lineare - cresce in modo esponenziale. Ogni commit senza verifica automatica introduce potenzialmente una regressione non rilevata. Ogni regressione non rilevata costringe il team a lavorare su codice instabile, generando ulteriori workaround che a loro volta diventano debito. Dopo quattro anni di questo pattern, nel gestionale logistico avevo 247 model di cui almeno 40 con logica di business duplicata in due o tre posti diversi, perché nessuno si fidava a consolidare senza test che garantissero la non-regressione.

La documentazione ufficiale di Laravel sul testing parte da un'assunzione che nella pratica delle PMI italiane è quasi sempre violata: che il progetto abbia test fin dall'inizio. Il framework include PHPUnit (e Pest dalla versione 11), una directory tests/ pre-configurata, helper per HTTP test, database test, mock - tutto già pronto. Ma nella realtà dei progetti che trovo nelle PMI, la directory tests/ contiene al massimo il test di esempio generato dal setup iniziale, e nessuno l'ha mai eseguito. L'azienda piemontese non faceva eccezione: php artisan test restituiva "0 tests, 0 assertions" su una codebase di 89 controller e 247 model.

Se stai gestendo un'applicazione Laravel critica senza test e vuoi capire da dove iniziare, nel mio profilo professionale trovi l'esperienza concreta su introduzione CI/CD e testing in codebase legacy per PMI.

Settimana 1: analisi statica prima dei test - PHPStan come rete di sicurezza immediata

La tentazione di chi vuole "introdurre i test" è partire scrivendo test unitari per le classi più importanti. È l'approccio sbagliato per una codebase di 4 anni senza copertura: ci vorrebbero mesi solo per coprire i path critici, e nel frattempo il team continua a deployare codice non verificato. Il mio approccio è invertito: prima l'analisi statica, poi i test funzionali, poi i test unitari.

PHPStan analizza il codice PHP senza eseguirlo e rileva errori di tipo, metodi inesistenti, parametri mancanti, dead code, e centinaia di altri problemi che in un linguaggio dinamico come PHP restano nascosti fino al runtime - tipicamente in produzione, tipicamente il venerdì sera. PHPStan ha 10 livelli di rigore (0-9): il livello 0 rileva solo errori banali, il livello 9 è quasi equivalente a un type system statico.

Il primo giorno di lavoro ho installato PHPStan con Larastan (il plugin per Laravel) e l'ho lanciato a livello 0 sulla codebase:

composer require --dev phpstan/phpstan nunomaduro/larastan
# phpstan.neon
includes:
    - vendor/nunomaduro/larastan/extension.neon

parameters:
    paths:
        - app/
    level: 0
    tmpDir: .phpstan-cache

Livello 0 ha trovato 47 errori. Metodi chiamati su variabili potenzialmente null, classi referenziate con namespace sbagliato, parametri con tipo incompatibile. Quarantasette problemi che erano nel codice da mesi o anni e che potevano esplodere in qualsiasi momento. Li abbiamo risolti in due giorni, e già a quel punto il team aveva un riscontro tangibile: "PHPStan ci ha trovato un bug nel calcolo IVA che non avevamo visto perché si manifestava solo per clienti con regime forfettario - sono 3 su 180, ma generavano fatture sbagliate da sei mesi".

Nei giorni successivi ho alzato progressivamente il livello: 1, poi 2, poi 3. A ogni livello, nuovi errori, nuove correzioni, e una comprensione crescente della codebase da parte del team. Dopo la prima settimana eravamo a livello 5 con zero errori - un risultato che garantisce type safety sui parametri delle funzioni, sui return type, e sulle property dei model Eloquent. La regola che ho fissato con il team: nessun merge su main con errori PHPStan a livello 5. Punto.

Settimana 2: Pest per i test funzionali sui path critici

Con PHPStan come guardia statica, nella seconda settimana ho introdotto Pest - il framework di testing per PHP che Nuno Maduro ha costruito sopra PHPUnit con una sintassi più espressiva e meno boilerplate. La scelta di Pest rispetto a PHPUnit puro è stata deliberata: per un team che non ha mai scritto test, la barriera d'ingresso conta. Un test Pest è leggibile anche da chi non ha mai fatto testing:

<?php
// tests/Feature/OrderPickingTest.php

use App\Models\Order;
use App\Services\PickingCalculator;

it('calcola correttamente le quantità di picking per un ordine standard', function () {
    $order = Order::factory()
        ->hasItems(5)
        ->create(['status' => 'confirmed']);

    $calculator = app(PickingCalculator::class);
    $result = $calculator->calculate($order);

    expect($result->totalUnits())->toBe($order->items->sum('quantity'));
    expect($result->pallets())->toBeGreaterThan(0);
    expect($result->isValid())->toBeTrue();
});

it('rifiuta ordini con quantità negative', function () {
    $order = Order::factory()
        ->hasItems(1, ['quantity' => -5])
        ->create();

    app(PickingCalculator::class)->calculate($order);
})->throws(InvalidArgumentException::class, 'Le quantità devono essere positive');

Non ho chiesto al team di scrivere test unitari per ogni metodo di ogni model. Ho chiesto una cosa molto più concreta: identificare i 5 path critici che, se rotti, fermano il business. Per il gestionale logistico erano: (1) calcolo quantità picking, (2) ottimizzazione routing furgoni, (3) sincronizzazione con WMS esterno, (4) generazione DDT (documento di trasporto), (5) calcolo fatturazione con regime IVA differenziato. Per ognuno di questi path ho scritto io i primi 3-4 test come template, poi il team ha continuato aggiungendo casi edge che solo chi usa il sistema ogni giorno conosce.

Alla fine della seconda settimana avevamo 340 test funzionali che coprivano i 5 path critici. Non era una copertura del 100% - era una copertura del 100% su ciò che conta. La suite girava in 45 secondi con Pest in parallelo (--parallel), abbastanza veloce da poterla eseguire prima di ogni merge senza rallentare il workflow. Ho descritto il metodo iterativo per introdurre test in codebase legacy nell'articolo su test minimi in PHP legacy senza bloccare lo sviluppo.

La pipeline GitHub Actions: dal commit al deploy in 8 minuti

La terza settimana è stata dedicata a collegare il tutto in una pipeline GitHub Actions che eseguisse analisi statica, test, code style check e deploy automatico. La struttura:

# .github/workflows/ci.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  code-style:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
      - run: composer install --no-interaction
      - run: vendor/bin/pint --test

  static-analysis:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          extensions: redis, pdo_mysql
      - run: composer install --no-interaction
      - uses: actions/cache@v4
        with:
          path: .phpstan-cache
          key: phpstan-${{ github.sha }}
          restore-keys: phpstan-
      - run: vendor/bin/phpstan analyse --memory-limit=512M

  tests:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_DATABASE: testing
          MYSQL_ROOT_PASSWORD: secret
        ports: ['3306:3306']
        options: >-
          --health-cmd="mysqladmin ping"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=3
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          extensions: redis, pdo_mysql
      - run: composer install --no-interaction
      - run: cp .env.testing .env
      - run: php artisan key:generate
      - run: vendor/bin/pest --ci --parallel

  deploy:
    needs: [code-style, static-analysis, tests]
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - run: |
          curl -s -X POST "${{ secrets.DEPLOY_WEBHOOK_URL }}" \
            -H "X-Deploy-Token: ${{ secrets.DEPLOY_TOKEN }}"

I tre job di verifica (code-style, static-analysis, tests) girano in parallelo - il tempo totale è determinato dal più lento (i test a 45 secondi) più l'overhead di setup (circa 2 minuti). Il job di deploy scatta solo se tutti e tre passano, solo su push a main, e consiste in una chiamata webhook al server che esegue uno script di deploy atomico.

Lo script di deploy sul server è un principio che non negozio: deploy atomico con symlink, non sovrascrittura in-place. La directory corrente è un symlink a una release numerata. Ogni deploy crea una nuova release, installa le dipendenze, esegue le migration, compila le cache, e solo alla fine scambia il symlink. Se qualcosa va storto, il rollback è cambiare il symlink alla release precedente - 90 secondi al massimo, nessuna ricostruzione:

#!/usr/bin/env bash
set -euo pipefail

DEPLOY_DIR="/var/www/gestionale"
RELEASE="$(date +%Y%m%d_%H%M%S)"
RELEASE_DIR="${DEPLOY_DIR}/releases/${RELEASE}"

# Clona e prepara la nuova release
git clone --depth 1 --branch main "$REPO_URL" "$RELEASE_DIR"
cd "$RELEASE_DIR"
composer install --no-dev --optimize-autoloader --no-interaction
cp "${DEPLOY_DIR}/shared/.env" .env
php artisan migrate --force
php artisan optimize

# Scambia il symlink (operazione atomica)
ln -snf "$RELEASE_DIR" "${DEPLOY_DIR}/current"

# Riavvia i worker
sudo systemctl restart php8.2-fpm
php artisan queue:restart

# Pulizia: mantieni solo le ultime 5 release
ls -dt "${DEPLOY_DIR}/releases"/* | tail -n +6 | xargs rm -rf

Le resistenze del team e come le ho gestite

La parte più difficile non è stata la tecnica. La tecnica, per un consulente che fa questo di mestiere, è il pezzo prevedibile. La parte difficile è stata la cultura del team.

Le resistenze che ho incontrato sono le stesse che incontro in ogni progetto di questo tipo: "Non ho tempo per scrivere test" (risposta: i test ti restituiscono tempo eliminando le sessioni di debug del lunedì mattina). "PHPStan mi segna errori che funzionano in produzione" (risposta: funzionano oggi, con questi dati - quando i dati cambieranno, quell'errore diventerà un bug). "Il deploy automatico mi spaventa" (risposta: il deploy via FileZilla ti dovrebbe spaventare di più - non hai tracciabilità, non hai rollback, non hai ripetibilità).

La strategia che ha funzionato è stata non imporre nulla dall'alto, ma dimostrare il valore con un caso concreto. Il bug sul calcolo IVA per i forfettari, trovato da PHPStan nella prima settimana, valeva da solo più dell'intero investimento in tooling. Quando il team ha visto che un tool trovava in 30 secondi un bug che generava fatture sbagliate da sei mesi, la resistenza è crollata.

Per chi sta affrontando un percorso simile su una codebase legacy - non necessariamente Laravel, il principio vale per qualsiasi stack PHP - ho documentato il metodo di audit tecnico nei primi 30 giorni che applico come primo passo prima di introdurre qualsiasi automazione. E per chi ha già la pipeline ma non ha ancora affrontato il debito tecnico accumulato negli anni precedenti, il piano di consolidamento del debito tecnico nei 90 giorni post-subentro descrive come misurare, prioritizzare e ridurre il debito in modo strutturato.

Il risultato sul gestionale logistico, a tre mesi dall'introduzione della pipeline: zero incidenti di deploy in produzione (vs uno al mese nei sei mesi precedenti), PHPStan a livello 6 (salito dal 5 iniziale), 520 test Pest (da 340 iniziali - il team ha continuato ad aggiungerne), tempo medio dal commit al deploy: 8 minuti, tempo di rollback: 90 secondi. Il direttore operativo, lo stesso che mi aveva detto "non possiamo permetterci di introdurre test", adesso dice al suo team: "non possiamo permetterci di mergere senza che la pipeline sia verde". È la trasformazione culturale che conta più di qualsiasi tool. Contattami se vuoi portare questo approccio nella tua organizzazione - il primo passo è sempre un assessment della codebase e del workflow attuale, da cui costruiamo insieme la roadmap di automazione calibrata sulle tue priorità di business.

Ultima modifica: