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 => falsein 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,attachmentodocument.
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.
