PHP CLI: creare un comando per sincronizzare dati da un’API REST

by theArchitect
SHARE
PHP CLI: creare un comando per sincronizzare dati da un’API REST
© Guida-HTML5.it

Introduzione

La riga di comando è uno strumento molto utile per eseguire attività automatiche, pianificate o ripetibili. Un caso concreto consiste nel creare uno script PHP CLI che interroga un’API REST, legge i dati paginati e li salva localmente in un file JSON.

Questo approccio può essere utilizzato per sincronizzare prodotti da un gestionale, scaricare utenti da un servizio esterno o aggiornare periodicamente una cache locale. A differenza di uno script web, un programma CLI può essere eseguito da cron, da un sistema CI/CD o manualmente da un amministratore.

Realizzeremo un comando chiamato sync-users.php. Lo script:

  • accetterà un endpoint API come argomento;
  • invierà richieste HTTP usando cURL;
  • gestirà la paginazione;
  • salverà i risultati in un file JSON;
  • mostrerà un riepilogo dell’operazione;
  • restituirà codici di uscita utili per l’automazione.

Codice completo

Salviamo il seguente contenuto in un file chiamato sync-users.php:

<?php

declare(strict_types=1);

/**
 * Sincronizza utenti da un´API REST paginata.
 *
 * Esempio:
 * php sync-users.php https://api.example.com/users output/users.json
 */

if (PHP_SAPI !== ´cli´) {
    fwrite(STDERR, "Questo script deve essere eseguito da PHP CLI.n");
    exit(1);
}

if ($argc < 3) {
    fwrite(
        STDERR,
        "Uso: php sync-users.php <endpoint> <file-output>n"
    );
    exit(1);
}

$endpoint = $argv[1];
$outputFile = $argv[2];

if (!filter_var($endpoint, FILTER_VALIDATE_URL)) {
    fwrite(STDERR, "Errore: l´endpoint non è un URL valido.n");
    exit(1);
}

try {
    $users = fetchAllUsers($endpoint);

    $directory = dirname($outputFile);

    if (!is_dir($directory) && !mkdir($directory, 0755, true)) {
        throw new RuntimeException(
            "Impossibile creare la directory: {$directory}"
        );
    }

    $json = json_encode(
        [
            ´synced_at´ => date(DATE_ATOM),
            ´count´ => count($users),
            ´users´ => $users,
        ],
        JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
    );

    if (file_put_contents($outputFile, $json . PHP_EOL) === false) {
        throw new RuntimeException(
            "Impossibile scrivere il file: {$outputFile}"
        );
    }

    echo "Sincronizzazione completata.n";
    echo "Utenti ricevuti: " . count($users) . "n";
    echo "File creato: {$outputFile}n";

    exit(0);
} catch (Throwable $exception) {
    fwrite(STDERR, "Errore: " . $exception->getMessage() . "n");
    exit(2);
}

/**
 * Recupera tutte le pagine dell´API.
 *
 * L´API di esempio dovrebbe restituire una struttura simile:
 * {
 *   "data": [...],
 *   "current_page": 1,
 *   "last_page": 3
 * }
 */
function fetchAllUsers(string $endpoint): array
{
    $users = [];
    $page = 1;

    do {
        $separator = str_contains($endpoint, ´?´) ? ´&´ : ´?´;
        $url = $endpoint . $separator . ´page=´ . $page;

        echo "Richiesta pagina {$page}: {$url}n";

        $response = requestJson($url);

        if (
            !isset($response[´data´]) ||
            !is_array($response[´data´])
        ) {
            throw new RuntimeException(
                "Formato API non valido: manca il campo data."
            );
        }

        foreach ($response[´data´] as $user) {
            $users[] = $user;
        }

        $currentPage = (int) ($response[´current_page´] ?? $page);
        $lastPage = (int) ($response[´last_page´] ?? $currentPage);

        $page = $currentPage + 1;
    } while ($currentPage < $lastPage);

    return $users;
}

/**
 * Esegue una richiesta HTTP e converte la risposta JSON in array PHP.
 */
function requestJson(string $url): array
{
    $curl = curl_init($url);

    if ($curl === false) {
        throw new RuntimeException("Impossibile inizializzare cURL.");
    }

    curl_setopt_array($curl, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_TIMEOUT => 30,
        CURLOPT_HTTPHEADER => [
            ´Accept: application/json´,
            ´User-Agent: PHP-CLI-Sync/1.0´,
        ],
    ]);

    $body = curl_exec($curl);

    if ($body === false) {
        $error = curl_error($curl);
        curl_close($curl);

        throw new RuntimeException("Errore HTTP: {$error}");
    }

    $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
    curl_close($curl);

    if ($statusCode < 200 || $statusCode >= 300) {
        throw new RuntimeException(
            "L´API ha restituito il codice HTTP {$statusCode}."
        );
    }

    $decoded = json_decode($body, true);

    if (!is_array($decoded)) {
        throw new RuntimeException(
            "La risposta dell´API non contiene JSON valido."
        );
    }

    return $decoded;
}

Spiegazione

Controllare l’ambiente di esecuzione

La costante PHP_SAPI identifica il tipo di interfaccia utilizzata da PHP. Il valore cli indica l’esecuzione da terminale. Il controllo iniziale impedisce che lo script venga richiamato accidentalmente tramite un server web.

Leggere gli argomenti

La variabile $argc contiene il numero degli argomenti, mentre $argv è un array che li contiene. In questo esempio:

  • $argv[0] è il nome dello script;
  • $argv[1] è l’endpoint API;
  • $argv[2] è il percorso del file di destinazione.

È importante validare sempre gli input. La funzione filter_var() controlla che l’endpoint sia un URL valido prima di avviare la richiesta.

Gestire la paginazione

Molte API non restituiscono tutti i risultati in una sola risposta. Il metodo fetchAllUsers() parte dalla pagina numero uno e continua finché current_page è minore di last_page.

La funzione è volutamente separata dalla gestione della richiesta HTTP. Questa divisione rende il codice più facile da testare e da modificare se l’API utilizza una struttura di paginazione diversa.

Effettuare richieste HTTP

requestJson() utilizza cURL con alcuni parametri importanti:

  • CURLOPT_CONNECTTIMEOUT limita il tempo per stabilire la connessione;
  • CURLOPT_TIMEOUT evita che lo script rimanga bloccato troppo a lungo;
  • CURLOPT_RETURNTRANSFER restituisce il corpo della risposta come stringa;
  • CURLOPT_FOLLOWLOCATION segue eventuali reindirizzamenti.

Il codice verifica sia gli errori cURL sia il codice HTTP. Una risposta 404 o 500, per esempio, genera un’eccezione e termina il programma con un errore.

Eseguire il comando

Da terminale è possibile lanciare lo script così:

php sync-users.php 
  https://api.example.com/users 
  storage/users.json

Prima di provarlo, assicurati che PHP disponga dell’estensione cURL:

php -m | grep curl

Se il comando restituisce curl, l’estensione è disponibile. In caso contrario, occorre installare o abilitare l’estensione nell’installazione PHP utilizzata dalla CLI.

Best practice

  • Usa codici di uscita coerenti: 0 indica successo, mentre valori diversi da zero segnalano un problema.
  • Scrivi gli errori su STDERR: in questo modo l’output normale può essere reindirizzato separatamente.
  • Imposta sempre timeout HTTP: una rete lenta o un servizio irraggiungibile non deve bloccare indefinitamente il processo.
  • Non salvare direttamente file importanti: per dati critici puoi scrivere prima in un file temporaneo e poi rinominarlo.
  • Limita la memoria: per migliaia di record è preferibile elaborare ogni pagina e scriverla progressivamente, invece di accumulare tutto in un array.
  • Gestisci l’autenticazione in modo sicuro: token e chiavi API non dovrebbero essere inseriti direttamente nel codice sorgente.
  • Prevedi retry controllati: errori temporanei come timeout o risposte 503 possono essere ritentati, ma con un numero massimo di tentativi.
  • Registra il momento della sincronizzazione: il campo synced_at aiuta a diagnosticare problemi e a verificare la freschezza dei dati.

Riepilogo

Uno script PHP CLI per sincronizzare un’API REST combina diversi concetti importanti: lettura degli argomenti, validazione degli input, richieste HTTP, decodifica JSON, paginazione, gestione delle eccezioni e codici di uscita.

Il comando può essere eseguito manualmente oppure automatizzato con cron. Per esempio, una sincronizzazione ogni ora potrebbe essere configurata con una riga simile:

0 * * * * /usr/bin/php /var/www/sync-users.php 
https://api.example.com/users 
/var/www/storage/users.json >> /var/log/user-sync.log 2>&1

Partendo da questo esempio puoi aggiungere autenticazione, filtri, retry, modalità incrementale e scrittura su database.

Approfondisci con risorse ufficiali

SHARE