PHP e CLI: creare un comando per eseguire backup automatici di directory

by theArchitect
SHARE
PHP e CLI: creare un comando per eseguire backup automatici di directory
© Guida-HTML5.it

Introduzione

Quando si parla di PHP in ambiente CLI, molti pensano subito a script per leggere argomenti, gestire log o automatizzare task generici. Un uso molto pratico, però, è la creazione di un piccolo comando da terminale per eseguire il backup automatico di una directory.

Questo scenario è utile in contesti reali: salvataggio di cartelle di progetto, archivi di upload, configurazioni locali o dataset di lavoro. Con PHP puoi creare uno script semplice ma robusto, da lanciare manualmente o tramite cron, che copia una directory in una destinazione sicura, aggiunge un timestamp al nome e opzionalmente comprime il risultato.

In questo tutorial vedremo come costruire un comando CLI in PHP che:

  • riceve directory sorgente e destinazione da terminale;
  • controlla se i percorsi sono validi;
  • crea una copia del contenuto;
  • salva il backup con un nome univoco;
  • mostra messaggi chiari in caso di errore.

Codice completo

Il seguente esempio realizza un backup di una directory in una cartella di destinazione. Se l’estensione ZipArchive è disponibile, crea anche un archivio ZIP del backup.

<?php
declare(strict_types=1);

/**
 * Backup CLI di una directory.
 * Uso:
 * php backup.php --source=/percorso/sorgente --dest=/percorso/destinazione
 */

function printUsage(): void
{
    echo "Uso:n";
    echo "  php backup.php --source=/percorso/sorgente --dest=/percorso/destinazionenn";
    echo "Opzioni:n";
    echo "  --source   Directory da salvaren";
    echo "  --dest     Directory dove salvare il backupn";
}

function fail(string $message, int $exitCode = 1): void
{
    fwrite(STDERR, "[ERRORE] {$message}n");
    exit($exitCode);
}

function ensureDirectory(string $path, bool $createIfMissing = false): void
{
    if (is_dir($path)) {
        return;
    }

    if ($createIfMissing) {
        if (!mkdir($path, 0775, true) && !is_dir($path)) {
            fail("Impossibile creare la directory: {$path}");
        }
        return;
    }

    fail("La directory non esiste: {$path}");
}

function recursiveCopy(string $source, string $destination): void
{
    $dir = opendir($source);
    if ($dir === false) {
        fail("Impossibile aprire la directory sorgente: {$source}");
    }

    if (!is_dir($destination) && !mkdir($destination, 0775, true) && !is_dir($destination)) {
        closedir($dir);
        fail("Impossibile creare la directory di destinazione: {$destination}");
    }

    while (($file = readdir($dir)) !== false) {
        if ($file === ´.´ || $file === ´..´) {
            continue;
        }

        $srcPath = $source . DIRECTORY_SEPARATOR . $file;
        $dstPath = $destination . DIRECTORY_SEPARATOR . $file;

        if (is_dir($srcPath)) {
            recursiveCopy($srcPath, $dstPath);
        } else {
            if (!copy($srcPath, $dstPath)) {
                closedir($dir);
                fail("Errore durante la copia del file: {$srcPath}");
            }
        }
    }

    closedir($dir);
}

function zipDirectory(string $sourceDir, string $zipFilePath): void
{
    if (!class_exists(´ZipArchive´)) {
        fail("L´estensione ZipArchive non è disponibile.");
    }

    $zip = new ZipArchive();
    if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
        fail("Impossibile creare l´archivio ZIP: {$zipFilePath}");
    }

    $sourceDir = rtrim($sourceDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
    $files = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($sourceDir, FilesystemIterator::SKIP_DOTS),
        RecursiveIteratorIterator::SELF_FIRST
    );

    foreach ($files as $file) {
        $filePath = $file->getRealPath();
        $relativePath = substr($filePath, strlen($sourceDir));

        if ($file->isDir()) {
            $zip->addEmptyDir($relativePath);
        } else {
            $zip->addFile($filePath, $relativePath);
        }
    }

    $zip->close();
}

$options = getopt(´´, [´source:´, ´dest:´]);

if (!isset($options[´source´], $options[´dest´])) {
    printUsage();
    exit(1);
}

$source = rtrim($options[´source´], DIRECTORY_SEPARATOR);
$dest = rtrim($options[´dest´], DIRECTORY_SEPARATOR);

if (!is_dir($source)) {
    fail("La directory sorgente non è valida: {$source}");
}

ensureDirectory($dest, true);

$timestamp = date(´Y-m-d_H-i-s´);
$backupFolder = $dest . DIRECTORY_SEPARATOR . ´backup_´ . basename($source) . ´_´ . $timestamp;

echo "[INFO] Avvio backup...n";
echo "[INFO] Sorgente: {$source}n";
echo "[INFO] Destinazione: {$backupFolder}n";

recursiveCopy($source, $backupFolder);

echo "[OK] Copia completata.n";

$zipFile = $backupFolder . ´.zip´;
zipDirectory($backupFolder, $zipFile);

echo "[OK] Archivio creato: {$zipFile}n";
echo "[FINE] Backup completato con successo.n";

Spiegazione

Vediamo il funzionamento del comando pezzo per pezzo, così da capire non solo cosa fa, ma anche perché è strutturato in questo modo.

1. Lettura delle opzioni da CLI

La funzione getopt() legge gli argomenti passati da terminale. In questo caso usiamo due opzioni obbligatorie:

  • --source: la directory da salvare;
  • --dest: la directory dove depositare il backup.

Questa scelta rende il comando chiaro e facile da ricordare. Ad esempio:

php backup.php --source=/var/www/progetto --dest=/home/user/backup

2. Validazione dei percorsi

Prima di copiare qualsiasi file, lo script verifica che la directory sorgente esista davvero. È una regola fondamentale nei programmi CLI: non fidarti mai dell’input. Se il percorso è errato, lo script termina con un messaggio esplicito.

La directory di destinazione, invece, viene creata automaticamente se non esiste. Questo rende il comando più comodo da usare in automazione, perché non richiede preparazione manuale della cartella target.

3. Copia ricorsiva

La funzione recursiveCopy() attraversa la directory sorgente e copia file e sottocartelle. È una soluzione utile perché un backup reale non riguarda quasi mai un singolo file, ma un intero albero di directory.

La logica è semplice:

  • apre la cartella sorgente;
  • legge ogni elemento con readdir();
  • se trova una directory, richiama sé stessa;
  • se trova un file, usa copy().

Questo approccio è facile da comprendere e adatto a progetti intermedi o script di utilità.

4. Nome del backup con timestamp

Il backup viene salvato in una cartella con nome composto da:

  • prefisso backup_;
  • nome della directory sorgente;
  • timestamp nel formato Y-m-d_H-i-s.

Così ogni esecuzione produce un backup distinto. È una best practice molto importante, perché evita di sovrascrivere salvataggi precedenti e facilita il versionamento manuale.

5. Creazione del file ZIP

Dopo aver copiato i file, lo script prova a creare un archivio ZIP con ZipArchive. Questo passaggio è utile per risparmiare spazio e ottenere un singolo file facilmente trasferibile o archiviabile.

Se l’estensione non è disponibile, il comando interrompe l’esecuzione con un messaggio chiaro. In un progetto più evoluto potresti rendere lo ZIP opzionale, ma per un tutorial tecnico è utile mostrare come gestire una dipendenza nativa di PHP.

Best practice

Un comando CLI ben fatto non deve solo funzionare: deve essere affidabile, leggibile e sicuro. Ecco alcune buone pratiche da seguire.

  • Valida sempre gli input: controlla che i percorsi siano corretti prima di operare sui file.
  • Usa messaggi di errore chiari: in CLI l’utente deve capire subito cosa non va.
  • Evita di sovrascrivere dati: aggiungi timestamp o identificatori univoci ai backup.
  • Separa le responsabilità: funzioni diverse per validazione, copia e compressione rendono il codice più manutenibile.
  • Gestisci le dipendenze: se usi estensioni come ZipArchive, verifica che siano disponibili.
  • Rendi lo script automatizzabile: un comando CLI è più utile se può essere eseguito da cron o da pipeline di deploy.

Un’altra buona idea è aggiungere in futuro opzioni come --dry-run per simulare il backup senza scrivere file, oppure --exclude per saltare directory temporanee come vendor, node_modules o cache.

Riepilogo

In questo tutorial abbiamo visto come PHP in modalità CLI possa essere usato per costruire un comando pratico: il backup automatico di una directory. Abbiamo affrontato i passaggi essenziali di uno script da terminale:

  • lettura delle opzioni con getopt();
  • validazione di input e percorsi;
  • copia ricorsiva di file e sottocartelle;
  • creazione di nomi backup con timestamp;
  • compressione in ZIP.

Questo tipo di script è un ottimo esercizio per chi vuole imparare a scrivere utility CLI in PHP con un taglio professionale. Ti allena a ragionare su error handling, struttura del codice e automazione, competenze molto utili anche in progetti web più grandi.

Approfondisci con risorse ufficiali

  • PHP Manual - getopt(): documentazione ufficiale per leggere argomenti da riga di comando.
  • PHP Manual - copy(): riferimento per la copia dei file.
  • PHP Manual - ZipArchive: classe ufficiale per creare e leggere archivi ZIP.
  • PHP Manual - RecursiveDirectoryIterator: utile per attraversare directory in modo ricorsivo.
  • PHP Manual - CLI SAPI: panoramica sull’esecuzione di PHP da terminale.

SHARE