Après douze ans à développer des sites et des applications web en PHP, je peux vous dire que le download en PHP reste l’une des fonctionnalités les plus demandées par mes clients. Qu’il s’agisse de proposer le téléchargement d’une facture PDF, d’un export CSV ou d’un fichier média, la mécanique côté serveur doit être maîtrisée pour offrir une expérience fiable et sécurisée. Dans cet article, je vous présente trois méthodes concrètes, testées en production, pour gérer le téléchargement de fichiers avec PHP.
Dans cet article
- La fonction readfile() combinée aux headers HTTP est la méthode la plus simple pour forcer un download en PHP
- La fonction file_get_contents() permet de télécharger un fichier distant depuis une URL externe en quelques lignes
- cURL offre le contrôle le plus fin : timeout, authentification, suivi de redirections et gestion d’erreurs avancée
- Les headers Content-Disposition et Content-Type sont indispensables pour forcer le navigateur à télécharger plutôt qu’afficher
- La validation du chemin de fichier est critique pour la sécurité : sans elle, une attaque par traversée de répertoire peut exposer tout le serveur
- PHP reste gratuit et open source, utilisé par plus de 76 % des sites web selon W3Techs
Sommaire
- Comprendre le mécanisme du download en PHP
- Méthode 1 : readfile() et les headers HTTP
- Méthode 2 : file_get_contents() pour les fichiers distants
- Méthode 3 : cURL pour un contrôle avancé
- Comparatif des trois méthodes
- Sécuriser le téléchargement de fichiers
- Erreurs fréquentes et solutions
- Cas pratique : système de téléchargement complet
- PHP est-il encore pertinent pour le download ?
Comprendre le mécanisme du download en PHP
Avant de plonger dans le code, je tiens à expliquer ce qui se passe réellement lorsqu’un utilisateur clique sur un lien de téléchargement. Le navigateur envoie une requête HTTP au serveur. Le serveur PHP lit le fichier demandé, puis renvoie son contenu avec des headers HTTP spécifiques qui indiquent au navigateur qu’il doit enregistrer le fichier plutôt que l’afficher.
Deux headers sont absolument essentiels pour un download en PHP réussi :
- Content-Type : indique le type MIME du fichier (par exemple
application/pdfpour un PDF,application/octet-streampour forcer le téléchargement quel que soit le type) - Content-Disposition : avec la valeur
attachment; filename="nom.ext", il force le navigateur à proposer l’enregistrement du fichier
Sans ces headers, le navigateur tentera d’afficher le fichier directement dans la fenêtre, ce qui fonctionne pour un PDF ou une image, mais donne un résultat inutilisable pour un fichier ZIP ou un exécutable. Si vous travaillez avec des structures conditionnelles en PHP, vous pouvez adapter le Content-Type dynamiquement selon l’extension du fichier.
Méthode 1 : readfile() et les headers HTTP
C’est la méthode que je recommande pour 90 % des cas d’usage. Elle est native, rapide et ne nécessite aucune extension supplémentaire. La fonction readfile() lit un fichier et l’envoie directement dans le buffer de sortie.
<?php
$fichier = '/chemin/vers/document.pdf';
if (!file_exists($fichier)) {
http_response_code(404);
die('Fichier introuvable.');
}
$nom_fichier = basename($fichier);
$taille = filesize($fichier);
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $nom_fichier . '"');
header('Content-Length: ' . $taille);
header('Cache-Control: must-revalidate');
header('Pragma: public');
ob_clean();
flush();
readfile($fichier);
exit;
?>
Quelques points importants que j’ai appris à la dure en production :
- Appelez ob_clean() et flush() avant readfile() pour vider le buffer de sortie. Sans cela, des caractères parasites peuvent corrompre le fichier téléchargé.
- Terminez toujours par exit pour empêcher PHP d’envoyer du contenu supplémentaire après le fichier.
- Le header Content-Length permet au navigateur d’afficher une barre de progression. Sans lui, l’utilisateur voit un téléchargement « de taille inconnue ».
Cette approche est particulièrement adaptée aux fichiers locaux de taille modérée (jusqu’à quelques centaines de Mo). Pour les très gros fichiers, je recommande d’utiliser fread() en boucle avec un buffer de 8 Ko pour éviter de saturer la mémoire PHP.

Méthode 2 : file_get_contents() pour les fichiers distants
Quand le fichier à télécharger se trouve sur un serveur distant, file_get_contents() est la solution la plus directe. Cette fonction native de PHP peut lire le contenu d’une URL exactement comme elle lirait un fichier local, à condition que la directive allow_url_fopen soit activée dans php.ini.
<?php
$url = 'https://exemple.com/fichiers/rapport-2026.pdf';
$nom_local = 'rapport-2026.pdf';
$dossier_destination = '/var/www/downloads/';
$contenu = file_get_contents($url);
if ($contenu === false) {
die('Impossible de télécharger le fichier distant.');
}
$resultat = file_put_contents($dossier_destination . $nom_local, $contenu);
if ($resultat !== false) {
echo 'Fichier téléchargé avec succès (' . $resultat . ' octets).';
} else {
echo 'Erreur lors de l\'écriture du fichier.';
}
?>
Je l’utilise souvent dans des scripts d’import automatisé ou des tâches cron. Par exemple, pour récupérer un flux de données quotidien depuis une API partenaire. Cependant, cette méthode a des limites importantes :
- Pas de gestion fine du timeout : si le serveur distant est lent, votre script PHP peut rester bloqué.
- Aucune gestion native des redirections HTTP (301, 302).
- Le fichier entier est chargé en mémoire avant d’être écrit sur le disque, ce qui pose problème pour les fichiers volumineux.
Pour contourner le problème de mémoire, vous pouvez utiliser un stream context avec des options personnalisées. C’est un peu comme utiliser un switch PHP pour adapter le comportement selon les cas : on ajuste la configuration au contexte.
<?php
$contexte = stream_context_create([
'http' => [
'timeout' => 30,
'method' => 'GET',
'header' => 'User-Agent: MonApp/1.0'
]
]);
$contenu = file_get_contents($url, false, $contexte);
?>
Méthode 3 : cURL pour un contrôle avancé
Pour les projets qui nécessitent une gestion fine du téléchargement, cURL est ma méthode de prédilection. L’extension php-curl offre un contrôle total sur la requête HTTP : authentification, suivi de redirections, gestion des certificats SSL, timeout granulaire et écriture progressive sur le disque.
<?php
$url = 'https://exemple.com/fichiers/archive.zip';
$chemin_local = '/var/www/downloads/archive.zip';
$fp = fopen($chemin_local, 'w+');
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 120);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$resultat = curl_exec($ch);
if (curl_errno($ch)) {
echo 'Erreur cURL : ' . curl_error($ch);
} else {
$info = curl_getinfo($ch);
echo 'Téléchargement terminé. Code HTTP : ' . $info['http_code'];
echo ' | Taille : ' . $info['size_download'] . ' octets';
}
curl_close($ch);
fclose($fp);
?>
L’avantage majeur de cURL est l’option CURLOPT_FILE. Au lieu de stocker le contenu en mémoire, cURL écrit directement dans le fichier ouvert par fopen(). Résultat : même un fichier de plusieurs Go peut être téléchargé sans faire exploser la mémoire PHP.
Je l’utilise systématiquement dans les cas suivants :
- Téléchargement depuis des API avec authentification Bearer ou Basic
- Fichiers dépassant 100 Mo où la consommation mémoire compte
- Serveurs distants nécessitant un suivi de redirections multiples
- Besoin de vérifier le code HTTP avant de considérer le téléchargement comme réussi
Voici un exemple plus avancé avec barre de progression et authentification :
<?php
function telecharger_avec_progression($url, $destination, $token = null) {
$fp = fopen($destination, 'w+');
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_FILE => $fp,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 300,
CURLOPT_NOPROGRESS => false,
CURLOPT_PROGRESSFUNCTION => function($ch, $dl_total, $dl_now) {
if ($dl_total > 0) {
$pourcentage = round(($dl_now / $dl_total) * 100);
echo "\rProgression : {$pourcentage}%";
}
}
]);
if ($token) {
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token
]);
}
$succes = curl_exec($ch);
$code_http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
fclose($fp);
if (!$succes || $code_http !== 200) {
unlink($destination);
return false;
}
return true;
}
?>

Comparatif des trois méthodes
Pour vous aider à choisir la bonne approche, voici un tableau récapitulatif basé sur mon expérience en production. Le choix dépend principalement de la source du fichier et du niveau de contrôle nécessaire.
| Critère | readfile() | file_get_contents() | cURL |
|---|---|---|---|
| Fichier local | Excellent | Possible | Non adapté |
| Fichier distant | Non | Oui | Oui |
| Gestion mémoire | Bonne (streaming) | Mauvaise (tout en RAM) | Excellente (CURLOPT_FILE) |
| Suivi redirections | Non applicable | Limité | Oui (CURLOPT_FOLLOWLOCATION) |
| Authentification | Non applicable | Via stream context | Native (Bearer, Basic, Digest) |
| Timeout configurable | Non applicable | Via stream context | Oui (granulaire) |
| Gestion d’erreurs | Basique | Basique | Avancée (codes HTTP, messages) |
| Complexité | Très simple | Simple | Modérée |
| Dépendance | Aucune | allow_url_fopen | Extension php-curl |
| Fichiers volumineux (>100 Mo) | Avec fread() en boucle | Déconseillé | Recommandé |
En résumé : readfile() pour servir un fichier local à l’utilisateur, file_get_contents() pour un script rapide de récupération distante, et cURL pour tout ce qui demande robustesse et contrôle. Cette logique de choix conditionnel rappelle le fonctionnement d’un case statement en PHP : à chaque situation, sa réponse adaptée.
Sécuriser le téléchargement de fichiers
La sécurité est un sujet que je prends très au sérieux. Un script de download mal sécurisé est une porte d’entrée pour les attaques par traversée de répertoire (directory traversal). Si l’utilisateur peut influencer le nom du fichier via un paramètre GET, il peut potentiellement télécharger n’importe quel fichier du serveur, y compris /etc/passwd ou vos fichiers de configuration.
Voici les règles que j’applique systématiquement dans mes projets :
<?php
// MAUVAIS : ne jamais faire confiance à l'entrée utilisateur
$fichier = $_GET['file'];
readfile('/var/www/uploads/' . $fichier); // DANGER !
// BON : valider et assainir le chemin
function telecharger_securise($identifiant) {
$dossier_autorise = '/var/www/uploads/';
// Liste blanche des fichiers autorisés (idéalement en BDD)
$fichiers_autorises = [
'rapport-2026' => 'rapport-annuel-2026.pdf',
'facture-mars' => 'facture-mars-2026.pdf',
];
if (!isset($fichiers_autorises[$identifiant])) {
http_response_code(403);
die('Accès refusé.');
}
$nom_fichier = $fichiers_autorises[$identifiant];
$chemin_complet = $dossier_autorise . $nom_fichier;
// Vérification anti-traversée
$chemin_reel = realpath($chemin_complet);
if ($chemin_reel === false || strpos($chemin_reel, $dossier_autorise) !== 0) {
http_response_code(403);
die('Chemin non autorisé.');
}
// Envoi du fichier
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $nom_fichier . '"');
header('Content-Length: ' . filesize($chemin_reel));
ob_clean();
flush();
readfile($chemin_reel);
exit;
}
?>
Les bonnes pratiques de sécurité que je recommande :
- Utilisez une liste blanche ou un identifiant en base de données plutôt que le nom de fichier brut
- Vérifiez le chemin réel avec realpath() pour déjouer les séquences
../ - Limitez les types MIME autorisés au téléchargement
- Ajoutez une vérification d’authentification si les fichiers sont privés
- Enregistrez chaque téléchargement dans un journal d’audit
La documentation officielle de realpath() détaille les cas limites de cette fonction, notamment son comportement avec les liens symboliques. Je vous conseille de la consulter avant d’implémenter votre propre logique de validation.

Erreurs fréquentes et solutions
En douze ans de développement PHP, j’ai rencontré pratiquement toutes les erreurs possibles liées au download. Voici les plus courantes et comment les résoudre.
Le fichier téléchargé est corrompu ou vide
C’est le problème numéro un. Dans 90 % des cas, c’est parce que du contenu a été envoyé avant les headers. Un simple espace avant la balise <?php, un echo de débogage oublié ou un fichier inclus avec un BOM UTF-8 suffit à corrompre le fichier. La solution : utilisez ob_clean() avant l’envoi, et vérifiez qu’aucun output n’est généré en amont.
L’erreur « Headers already sent »
Ce message apparaît quand PHP a déjà commencé à envoyer le corps de la réponse. Impossible alors de modifier les headers. Deux solutions : activez le output buffering dans php.ini (output_buffering = On) ou appelez ob_start() au tout début de votre script.
Le téléchargement est tronqué pour les gros fichiers
PHP a une limite d’exécution par défaut de 30 secondes. Pour un fichier de 500 Mo sur une connexion lente, ce n’est pas suffisant. Ajustez le max_execution_time et la memory_limit :
<?php
set_time_limit(0); // Pas de limite de temps
ini_set('memory_limit', '256M');
?>
Le nom du fichier contient des caractères spéciaux
Les accents et espaces dans les noms de fichiers posent problème avec certains navigateurs. J’utilise systématiquement un encodage RFC 5987 pour le header Content-Disposition. Cela permet de gérer correctement des noms comme « Récapitulatif dépenses été 2026.pdf ». Si vous manipulez des chaînes avec des caractères spéciaux, la fonction explode en PHP peut vous aider à découper et nettoyer les noms de fichiers.
header('Content-Disposition: attachment; filename="' . rawurlencode($nom) . '"; filename*=UTF-8\'\'' . rawurlencode($nom));
Cas pratique : système de téléchargement complet
Je vous propose un exemple complet que j’utilise comme base dans mes projets WordPress et PHP natif. Ce script gère la validation, la sécurité, le logging et le téléchargement proprement dit.
<?php
class GestionnaireTelechargement {
private string $dossier_base;
private array $types_autorises;
public function __construct(string $dossier, array $types = []) {
$this->dossier_base = rtrim($dossier, '/') . '/';
$this->types_autorises = $types ?: [
'pdf' => 'application/pdf',
'zip' => 'application/zip',
'csv' => 'text/csv',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
];
}
public function telecharger(string $nom_fichier): void {
$chemin = $this->validerChemin($nom_fichier);
$this->verifierType($chemin);
$this->enregistrerLog($chemin);
$this->envoyerFichier($chemin, $nom_fichier);
}
private function validerChemin(string $nom): string {
$chemin = realpath($this->dossier_base . basename($nom));
if ($chemin === false || strpos($chemin, realpath($this->dossier_base)) !== 0) {
http_response_code(403);
throw new RuntimeException('Chemin non autorisé.');
}
return $chemin;
}
private function verifierType(string $chemin): void {
$extension = strtolower(pathinfo($chemin, PATHINFO_EXTENSION));
if (!isset($this->types_autorises[$extension])) {
http_response_code(415);
throw new RuntimeException('Type de fichier non autorisé.');
}
}
private function enregistrerLog(string $chemin): void {
$log = sprintf(
"[%s] IP: %s | Fichier: %s\n",
date('Y-m-d H:i:s'),
$_SERVER['REMOTE_ADDR'] ?? 'CLI',
basename($chemin)
);
file_put_contents($this->dossier_base . '../logs/downloads.log', $log, FILE_APPEND);
}
private function envoyerFichier(string $chemin, string $nom): void {
$extension = strtolower(pathinfo($chemin, PATHINFO_EXTENSION));
$type_mime = $this->types_autorises[$extension];
header('Content-Type: ' . $type_mime);
header('Content-Disposition: attachment; filename="' . basename($nom) . '"');
header('Content-Length: ' . filesize($chemin));
header('Cache-Control: no-cache, must-revalidate');
header('X-Content-Type-Options: nosniff');
ob_clean();
flush();
readfile($chemin);
exit;
}
}
// Utilisation
try {
$gestionnaire = new GestionnaireTelechargement('/var/www/uploads/');
$gestionnaire->telecharger($_GET['fichier'] ?? '');
} catch (RuntimeException $e) {
echo $e->getMessage();
}
?>
Ce code orienté objet est facilement extensible. Vous pouvez ajouter une vérification d’authentification, un système de compteur de téléchargements, ou une gestion de quotas par utilisateur. La structure conditionnelle avec les exceptions fonctionne un peu comme un switch imbriqué en PHP : chaque étape de validation peut interrompre le processus proprement.
PHP est-il encore pertinent pour le download ?
Je vois régulièrement cette question revenir dans les forums et les discussions techniques. La réponse est oui, absolument. Selon les statistiques W3Techs, PHP est utilisé côté serveur par plus de 76 % des sites web dont le langage est connu. WordPress, qui propulse plus de 40 % du web mondial, est entièrement bâti sur PHP.
PHP est gratuit et open source, distribué sous la licence PHP License. Vous pouvez le télécharger et l’utiliser sans aucun coût pour des projets personnels comme professionnels. Pour l’installer, il suffit généralement d’une commande sur votre serveur :
# Debian/Ubuntu
sudo apt update && sudo apt install php php-curl
# CentOS/RHEL
sudo yum install php php-curl
Avec les versions récentes de PHP (8.2, 8.3, 8.4), les performances ont considérablement augmenté. Le moteur d’exécution est plus rapide, la gestion de la mémoire plus efficace, et les fonctionnalités modernes (typage strict, enums, fibers) permettent d’écrire du code plus robuste. Pour tout ce qui touche à la conversion de devises ou au traitement de données, PHP reste un choix solide et éprouvé.
Concernant la conversion de PDF avec PHP, c’est possible grâce à des bibliothèques comme TCPDF, FPDF ou Dompdf. Ces outils permettent de générer des fichiers PDF à partir de HTML/CSS, puis de les proposer en téléchargement via les méthodes décrites plus haut. La documentation officielle de PHP reste la meilleure ressource pour explorer toutes les fonctions disponibles.
À retenir
- Utilisez readfile() avec les bons headers HTTP pour un download local simple et fiable
- Préférez cURL avec CURLOPT_FILE pour les fichiers distants volumineux afin d’éviter la saturation mémoire
- Validez toujours le chemin de fichier avec realpath() et une liste blanche pour empêcher les attaques par traversée de répertoire
- Ajoutez ob_clean() et flush() avant readfile() pour éviter la corruption du fichier téléchargé
- Enregistrez chaque téléchargement dans un journal d’audit pour la traçabilité et le débogage
Questions fréquentes
Comment forcer le téléchargement d’un fichier en PHP ?
Pour forcer un download en PHP, envoyez les headers HTTP Content-Type: application/octet-stream et Content-Disposition: attachment; filename= »nom.ext » avant d’utiliser la fonction readfile() pour envoyer le contenu du fichier. N’oubliez pas d’appeler ob_clean() et flush() avant readfile(), puis de terminer par exit pour éviter toute corruption.
PHP télécharger un fichier distant depuis une URL, comment faire ?
Deux méthodes principales existent. La plus simple est file_get_contents($url) combinée à file_put_contents() pour enregistrer le fichier en local. Pour un contrôle avancé (timeout, authentification, gros fichiers), utilisez cURL avec l’option CURLOPT_FILE qui écrit directement sur le disque sans charger tout le contenu en mémoire.
PHP est-il gratuit à utiliser ?
Oui, PHP est entièrement gratuit et open source. Il est distribué sous la licence PHP License et peut être utilisé sans restriction pour des projets personnels, commerciaux ou institutionnels. Il est disponible sur tous les systèmes d’exploitation majeurs (Linux, macOS, Windows) et peut être installé en une seule commande sur la plupart des distributions Linux.
Est-ce que PHP est encore utilisé en 2026 ?
Absolument. PHP reste le langage côté serveur le plus utilisé au monde avec plus de 76 % de parts de marché selon W3Techs. WordPress, Laravel, Symfony et de nombreux frameworks modernes continuent de faire évoluer l’écosystème. Les versions PHP 8.x apportent des performances et des fonctionnalités qui maintiennent PHP comme un choix pertinent pour le développement web.
Comment puis-je convertir un PDF en PHP ?
Pour générer un PDF avec PHP, utilisez une bibliothèque comme TCPDF, FPDF ou Dompdf. Dompdf est la plus accessible : elle convertit du code HTML/CSS en PDF. Pour lire et extraire le contenu d’un PDF existant, la bibliothèque Smalot PDF Parser est une solution efficace. Ces fichiers PDF peuvent ensuite être proposés en téléchargement via readfile() et les headers appropriés.
Quelle est la différence entre readfile() et file_get_contents() pour le download en PHP ?
readfile() lit un fichier et l’envoie directement au navigateur via le buffer de sortie, sans charger tout le contenu en mémoire. C’est idéal pour servir des fichiers en téléchargement. file_get_contents() charge le contenu complet du fichier dans une variable PHP (en RAM), ce qui permet de le manipuler avant de l’envoyer ou de l’enregistrer, mais consomme plus de mémoire.
Nathan Morel est développeur web freelance depuis 12 ans dans la Loire. Spécialisé WordPress et solutions sur mesure, il a accompagné plus de 200 PME et partage son expérience technique et entrepreneuriale sur NA Web.