PHP e cURL: caricare file tramite richieste multipart/form-data

by theArchitect
SHARE
PHP e cURL: caricare file tramite richieste multipart/form-data
© Guida-HTML5.it

Introduzione

Quando un’applicazione PHP deve inviare un file a un servizio esterno, una delle soluzioni più affidabili consiste nell’utilizzare cURL con una richiesta HTTP multipart/form-data. Questo formato è lo stesso usato dai normali moduli HTML con un campo di caricamento file e permette di inviare contemporaneamente file e dati testuali.

Un caso pratico è rappresentato da un gestionale che deve caricare un’immagine su un’API, da un sistema che invia documenti a un servizio di archiviazione o da un’applicazione che inoltra allegati a un endpoint remoto.

In questo tutorial realizzeremo una funzione riutilizzabile che:

  • verifica che il file esista e sia leggibile;
  • controlla dimensione e tipo MIME;
  • costruisce una richiesta multipart/form-data;
  • invia il file tramite cURL;
  • gestisce il codice HTTP e gli errori di trasporto;
  • restituisce una risposta strutturata al chiamante.

L’endpoint usato nell’esempio è dimostrativo. In un progetto reale dovrai sostituirlo con l’URL del servizio che riceve il file.

Codice completo

<?php

declare(strict_types=1);

/**
 * Invia un file a un endpoint remoto tramite multipart/form-data.
 *
 * @param string $endpoint URL dell´API destinataria
 * @param string $filePath Percorso del file locale
 * @param array<string, string> $fields Campi testuali aggiuntivi
 * @return array{status: int, body: string, content_type: string}
 *
 * @throws InvalidArgumentException Se il file non è valido
 * @throws RuntimeException Se cURL non riesce a completare la richiesta
 */
function uploadFile(
    string $endpoint,
    string $filePath,
    array $fields = []
): array {
    // Verifica l´esistenza e l´accessibilità del file.
    if (!is_file($filePath) || !is_readable($filePath)) {
        throw new InvalidArgumentException(
            ´Il file non esiste oppure non è leggibile.´
        );
    }

    // Limite applicativo: 5 MB.
    $maxSize = 5 * 1024 * 1024;
    $fileSize = filesize($filePath);

    if ($fileSize === false || $fileSize > $maxSize) {
        throw new InvalidArgumentException(
            ´Il file supera la dimensione massima consentita di 5 MB.´
        );
    }

    // Determina il MIME type reale, senza fidarsi solo dell´estensione.
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mimeType = $finfo->file($filePath);

    $allowedMimeTypes = [
        ´image/jpeg´,
        ´image/png´,
        ´application/pdf´,
    ];

    if (!in_array($mimeType, $allowedMimeTypes, true)) {
        throw new InvalidArgumentException(
            ´Tipo MIME non consentito: ´ . ($mimeType ?: ´sconosciuto´)
        );
    }

    // CURLFile prepara il file per una parte multipart/form-data.
    $multipartData = [
        ´document´ => new CURLFile(
            $filePath,
            $mimeType,
            basename($filePath)
        ),
        ´description´ => $fields[´description´] ?? ´Documento caricato via PHP´,
    ];

    // Aggiunge eventuali altri campi testuali.
    foreach ($fields as $name => $value) {
        if ($name !== ´description´) {
            $multipartData[$name] = $value;
        }
    }

    $ch = curl_init($endpoint);

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

    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => $multipartData,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HEADER => false,

        // Evita che una richiesta rimanga bloccata indefinitamente.
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_TIMEOUT => 60,

        // Verifica il certificato TLS durante una connessione HTTPS.
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,

        // Utile per identificare l´applicazione nei log del server.
        CURLOPT_USERAGENT => ´MiaApp/1.0 PHP-cURL´,
    ]);

    $body = curl_exec($ch);

    if ($body === false) {
        $error = curl_error($ch);
        $errorNumber = curl_errno($ch);

        curl_close($ch);

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

    $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE) ?: ´´;

    curl_close($ch);

    if ($statusCode < 200 || $statusCode >= 300) {
        throw new RuntimeException(
            "Il server ha restituito HTTP {$statusCode}: {$body}"
        );
    }

    return [
        ´status´ => $statusCode,
        ´body´ => $body,
        ´content_type´ => $contentType,
    ];
}


// Esempio di utilizzo.
try {
    $result = uploadFile(
        ´https://api.esempio.test/v1/documents´,
        __DIR__ . ´/files/contratto.pdf´,
        [
            ´description´ => ´Contratto firmato´,
            ´customer_id´ => ´CUST-1042´,
        ]
    );

    echo ´Upload completato. Codice HTTP: ´ . $result[´status´] . PHP_EOL;
    echo ´Risposta del server: ´ . $result[´body´] . PHP_EOL;
} catch (InvalidArgumentException | RuntimeException $exception) {
    error_log($exception->getMessage());
    echo ´Upload non riuscito.´ . PHP_EOL;
}

Spiegazione

Perché usare CURLFile

La classe CURLFile rappresenta un file da allegare alla richiesta. Riceve il percorso del file, il suo tipo MIME e il nome che il server remoto vedrà come nome dell’allegato.

Quando CURLOPT_POSTFIELDS riceve un array contenente un oggetto CURLFile, PHP costruisce automaticamente una richiesta multipart/form-data, compreso il relativo boundary. È importante non impostare manualmente l’header Content-Type, perché cURL deve aggiungere il boundary corretto.

Validazione del file

La funzione controlla prima che il percorso punti a un file reale e leggibile. Successivamente applica un limite di 5 MB. Questo evita di caricare accidentalmente file troppo grandi e riduce il rischio di consumo eccessivo di memoria o banda.

Il tipo MIME viene rilevato con l’estensione Fileinfo, tramite finfo. Controllare soltanto l’estensione, ad esempio .pdf o .jpg, non è sufficiente: un file potrebbe essere rinominato senza che il contenuto cambi.

Dati testuali e file nella stessa richiesta

L’array $multipartData contiene sia il campo document, che rappresenta il file, sia campi testuali come description e customer_id. Il server remoto potrà quindi leggere i dati testuali e il file usando il normale meccanismo di gestione degli upload HTTP.

Gestione della risposta

curl_exec() restituisce false quando si verifica un errore di rete, DNS, TLS o configurazione. In questo caso vengono recuperati numero e descrizione dell’errore con curl_errno() e curl_error().

Un errore HTTP, invece, non sempre provoca il valore false. Per esempio, un server può rispondere con HTTP 400 o 500 e restituire comunque correttamente un contenuto. Per questo il codice controlla separatamente CURLINFO_HTTP_CODE, accettando soltanto i codici compresi tra 200 e 299.

Best practice

  • Non disabilitare la verifica TLS: evita di usare CURLOPT_SSL_VERIFYPEER => false in produzione. Ridurrebbe la sicurezza della connessione.
  • Limita dimensione e tipi di file: applica sia controlli lato client sia controlli lato server. La validazione nel client non sostituisce quella dell’API.
  • Non fidarti del nome originale: basename() riduce il rischio di includere percorsi indesiderati nel nome inviato.
  • Non registrare dati sensibili: evita di scrivere nei log token, contenuti di documenti o risposte contenenti informazioni personali.
  • Imposta timeout ragionevoli: un upload può richiedere più tempo di una normale richiesta, ma non deve restare bloccato senza limite.
  • Controlla i codici HTTP: una connessione riuscita non significa necessariamente che l’upload sia stato accettato dal server.
  • Usa HTTPS: i documenti caricati possono contenere informazioni riservate e devono essere trasmessi su una connessione cifrata.
  • Verifica i requisiti dell’API: alcuni servizi richiedono un nome specifico per il campo file, ad esempio file, attachment o document.

Riepilogo

cURL e CURLFile permettono di inviare file da PHP usando lo standard multipart/form-data. La tecnica è adatta a immagini, PDF e altri documenti, oltre a eventuali campi testuali associati.

Un’implementazione robusta deve validare il file prima dell’invio, usare HTTPS, impostare timeout, distinguere gli errori cURL dagli errori HTTP e controllare il tipo MIME reale. Inoltre, lasciare a cURL la gestione automatica del Content-Type evita problemi con il boundary della richiesta.

Approfondisci con risorse ufficiali

SHARE