📸 Téléchargement d'images avec PHP
Maîtrisez l'upload, la validation, la manipulation et le stockage sécurisé d'images
Pourquoi gérer l'upload d'images ?
Le contexte
Dans une application web moderne, permettre aux utilisateurs de télécharger des images est essentiel : photos de profil, images d'articles, galeries, etc. PHP offre des outils puissants pour gérer ces uploads de manière sécurisée et efficace.
Le processus complet
Formulaire HTML
Créer un formulaire avec <input type="file">
Réception PHP
Récupérer le fichier via $_FILES
Validation
Vérifier type, taille, et sécurité
Manipulation
Redimensionner, compresser, optimiser
Stockage
Sauvegarder sur disque ou base de données
Affichage
Servir l'image aux utilisateurs
Sécurité avant tout
L'upload de fichiers est une porte d'entrée potentielle pour les attaques. Il est crucial de valider et sécuriser chaque étape du processus.
Configuration PHP
Avant de commencer, vous devez vérifier et configurer certains paramètres PHP.
Paramètres dans php.ini
file_uploads
On
Active l'upload de fichiers
upload_max_filesize
8M
Taille maximale d'un fichier uploadé
post_max_size
10M
Taille maximale des données POST
(doit être > upload_max_filesize)
max_file_uploads
20
Nombre max de fichiers simultanés
upload_tmp_dir
/tmp
Dossier temporaire pour uploads
max_execution_time
30
Temps max d'exécution (secondes)
Vérifier la configuration
<?php
// Vérifier les paramètres d'upload
echo "<h2>Configuration PHP pour Upload</h2>";
echo "<table border='1' cellpadding='10'>";
echo "<tr><th>Paramètre</th><th>Valeur</th></tr>";
$params = [
'file_uploads',
'upload_max_filesize',
'post_max_size',
'max_file_uploads',
'upload_tmp_dir',
'max_execution_time'
];
foreach ($params as $param) {
$value = ini_get($param);
$value = $value ?: '(non défini)';
echo "<tr><td>$param</td><td><strong>$value</strong></td></tr>";
}
echo "</table>";
// Vérifier l'extension GD
if (extension_loaded('gd')) {
echo "<p style='color: green;'>✓ Extension GD installée</p>";
$info = gd_info();
echo "<pre>" . print_r($info, true) . "</pre>";
} else {
echo "<p style='color: red;'>✗ Extension GD non disponible</p>";
}
?>
Modifier php.ini
XAMPP : C:\xampp\php\php.ini
Linux : /etc/php/8.x/apache2/php.ini
Mac (MAMP) : /Applications/MAMP/bin/php/phpX.X.X/conf/php.ini
Après modification, redémarrez Apache.
Créer le formulaire HTML
Pour permettre l'upload d'images, votre formulaire doit avoir des attributs spécifiques.
Formulaire de base
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Upload d'image</title>
</head>
<body>
<h1>Télécharger une image</h1>
<form action="upload.php" method="POST" enctype="multipart/form-data">
<label for="image">Sélectionnez une image :</label>
<input type="file"
name="image"
id="image"
accept="image/*"
required>
<button type="submit">Télécharger</button>
</form>
</body>
</html>
⚠️ Attributs OBLIGATOIRES
| Attribut | Valeur | Raison |
|---|---|---|
method |
"POST" |
Les fichiers ne peuvent pas être envoyés en GET |
enctype |
"multipart/form-data" |
Permet l'envoi de fichiers binaires |
type |
"file" |
Crée un champ de sélection de fichier |
Formulaire amélioré avec prévisualisation
<form action="upload.php" method="POST" enctype="multipart/form-data">
<div class="upload-area">
<label for="image" class="upload-label">
<span class="upload-icon">📸</span>
<span>Cliquez ou glissez une image ici</span>
<span class="upload-hint">JPG, PNG, GIF (Max 5MB)</span>
</label>
<input type="file"
name="image"
id="image"
accept="image/jpeg,image/png,image/gif"
onchange="previewImage(this)">
</div>
<div id="preview" class="preview-container" style="display:none;">
<img id="preview-image" src="" alt="Aperçu">
<p id="file-info"></p>
</div>
<button type="submit">Télécharger</button>
</form>
<script>
function previewImage(input) {
if (input.files && input.files[0]) {
const file = input.files[0];
const reader = new FileReader();
// Vérifier la taille (5MB max)
if (file.size > 5 * 1024 * 1024) {
alert('Fichier trop volumineux (max 5MB)');
input.value = '';
return;
}
reader.onload = function(e) {
document.getElementById('preview').style.display = 'block';
document.getElementById('preview-image').src = e.target.result;
document.getElementById('file-info').textContent =
`${file.name} (${(file.size / 1024).toFixed(2)} KB)`;
};
reader.readAsDataURL(file);
}
}
</script>
Traitement PHP de base
Structure de $_FILES
Lorsqu'un fichier est uploadé, PHP le place dans le tableau superglobal $_FILES.
<?php
// Si input name="image"
$_FILES['image'] = [
'name' => 'photo.jpg', // Nom original du fichier
'type' => 'image/jpeg', // Type MIME
'tmp_name' => '/tmp/phpXXXXXX', // Chemin temporaire
'error' => 0, // Code d'erreur (0 = succès)
'size' => 245678 // Taille en octets
];
?>
Script d'upload simple
<?php
// Vérifier qu'un fichier a été uploadé
if (!isset($_FILES['image']) || $_FILES['image']['error'] !== UPLOAD_ERR_OK) {
die('Erreur: Aucun fichier uploadé');
}
$file = $_FILES['image'];
// Informations sur le fichier
$fileName = $file['name'];
$fileTmpPath = $file['tmp_name'];
$fileSize = $file['size'];
$fileType = $file['type'];
// Dossier de destination
$uploadDir = 'uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// Générer un nom unique pour éviter les collisions
$fileExtension = pathinfo($fileName, PATHINFO_EXTENSION);
$newFileName = uniqid('img_', true) . '.' . $fileExtension;
$destination = $uploadDir . $newFileName;
// Déplacer le fichier du dossier temporaire vers la destination
if (move_uploaded_file($fileTmpPath, $destination)) {
echo "Fichier uploadé avec succès : $newFileName";
} else {
echo "Erreur lors du déplacement du fichier";
}
?>
IMPORTANT : move_uploaded_file()
Utilisez toujours move_uploaded_file() et non move() ou copy(). Cette fonction vérifie que le fichier provient bien d'un upload HTTP.
Codes d'erreur PHP
| Code | Constante | Description |
|---|---|---|
| 0 | UPLOAD_ERR_OK |
Succès, aucune erreur |
| 1 | UPLOAD_ERR_INI_SIZE |
Fichier > upload_max_filesize |
| 2 | UPLOAD_ERR_FORM_SIZE |
Fichier > MAX_FILE_SIZE du formulaire |
| 3 | UPLOAD_ERR_PARTIAL |
Fichier partiellement uploadé |
| 4 | UPLOAD_ERR_NO_FILE |
Aucun fichier uploadé |
| 6 | UPLOAD_ERR_NO_TMP_DIR |
Dossier temporaire manquant |
| 7 | UPLOAD_ERR_CANT_WRITE |
Échec d'écriture sur disque |
Validation des fichiers
La validation est cruciale pour la sécurité et la performance de votre application.
Script de validation complet
<?php
function uploadImage($fileInput) {
// 1. Vérifier qu'un fichier existe
if (!isset($_FILES[$fileInput]) || $_FILES[$fileInput]['error'] !== UPLOAD_ERR_OK) {
return ['success' => false, 'error' => 'Aucun fichier ou erreur d\'upload'];
}
$file = $_FILES[$fileInput];
// 2. Vérifier la taille (5MB max)
$maxSize = 5 * 1024 * 1024; // 5MB
if ($file['size'] > $maxSize) {
return ['success' => false, 'error' => 'Fichier trop volumineux (max 5MB)'];
}
// 3. Vérifier le type MIME réel (pas celui déclaré)
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($mimeType, $allowedTypes)) {
return ['success' => false, 'error' => 'Type de fichier non autorisé'];
}
// 4. Vérifier l'extension
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (!in_array($extension, $allowedExtensions)) {
return ['success' => false, 'error' => 'Extension non autorisée'];
}
// 5. Vérifier que c'est vraiment une image
$imageInfo = getimagesize($file['tmp_name']);
if ($imageInfo === false) {
return ['success' => false, 'error' => 'Le fichier n\'est pas une image valide'];
}
// 6. Créer le dossier de destination si nécessaire
$uploadDir = 'uploads/' . date('Y/m') . '/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// 7. Générer un nom de fichier sécurisé
$newFileName = bin2hex(random_bytes(16)) . '.' . $extension;
$destination = $uploadDir . $newFileName;
// 8. Déplacer le fichier
if (!move_uploaded_file($file['tmp_name'], $destination)) {
return ['success' => false, 'error' => 'Erreur lors de la sauvegarde'];
}
// 9. Retourner les informations
return [
'success' => true,
'filename' => $newFileName,
'path' => $destination,
'size' => $file['size'],
'width' => $imageInfo[0],
'height' => $imageInfo[1],
'mime' => $mimeType
];
}
// Utilisation
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$result = uploadImage('image');
if ($result['success']) {
echo "Image uploadée : {$result['path']}<br>";
echo "Dimensions : {$result['width']}x{$result['height']}<br>";
echo "<img src='{$result['path']}' width='300'>";
} else {
echo "Erreur : {$result['error']}";
}
}
?>
Points de validation
Taille du fichier
Limiter la taille pour éviter la surcharge serveur
$file['size'] < 5MB
Type MIME réel
Vérifier avec finfo_file(), pas $_FILES['type']
finfo_file($finfo, $path)
Extension
Whitelister les extensions autorisées
in_array($ext, $allowed)
Image valide
Vérifier que c'est une vraie image
getimagesize($path)
Nom sécurisé
Générer un nom aléatoire unique
bin2hex(random_bytes())
Dossier protégé
Permissions appropriées (755)
mkdir($dir, 0755)
Sécurité
🚨 Menaces courantes
- Upload de code malveillant : Un attaquant upload un fichier PHP déguisé en image
- Path traversal : Manipulation du nom de fichier pour accéder à d'autres dossiers
- Déni de service (DoS) : Upload massif de fichiers volumineux
- XSS via SVG : SVG contenant du JavaScript malveillant
Bonnes pratiques de sécurité
1. Ne JAMAIS faire confiance au client
Le type MIME envoyé par le navigateur ($_FILES['type']) peut être falsifié.
// ❌ DANGEREUX
$type = $_FILES['image']['type'];
// ✅ SÉCURISÉ
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$type = finfo_file($finfo, $_FILES['image']['tmp_name']);
2. Générer des noms de fichiers sécurisés
N'utilisez JAMAIS le nom original du fichier tel quel.
// ❌ DANGEREUX
$name = $_FILES['image']['name'];
// ✅ SÉCURISÉ
$name = bin2hex(random_bytes(16)) . '.jpg';
3. Stocker en dehors du webroot
Idéalement, les uploads ne doivent pas être accessibles directement.
// ✅ SÉCURISÉ
$uploadDir = '/var/www/uploads_private/';
// Servir via un script PHP
// image.php?id=123
4. Fichier .htaccess de protection
Empêcher l'exécution de scripts dans le dossier uploads.
# uploads/.htaccess
# Interdire l'exécution de PHP
php_flag engine off
# Bloquer l'accès aux fichiers dangereux
<FilesMatch "\.(php|php3|php4|php5|phtml|pl|py|jsp|asp|sh|cgi)$">
Order allow,deny
Deny from all
</FilesMatch>
5. Limiter les dimensions
Vérifier la largeur et hauteur pour éviter les images énormes.
$imageInfo = getimagesize($tmpPath);
$maxWidth = 5000;
$maxHeight = 5000;
if ($imageInfo[0] > $maxWidth || $imageInfo[1] > $maxHeight) {
die('Dimensions trop grandes');
}
6. Rate limiting
Limiter le nombre d'uploads par utilisateur/IP.
// Exemple simple avec session
session_start();
$_SESSION['uploads'] = $_SESSION['uploads'] ?? [];
// Max 10 uploads par heure
$recentUploads = array_filter(
$_SESSION['uploads'],
fn($t) => time() - $t < 3600
);
if (count($recentUploads) >= 10) {
die('Trop d\'uploads récents');
}
$_SESSION['uploads'][] = time();
Checklist de sécurité
- ☑️ Vérifier le type MIME avec
finfo_file() - ☑️ Vérifier que c'est une image avec
getimagesize() - ☑️ Générer un nom de fichier aléatoire
- ☑️ Limiter la taille du fichier
- ☑️ Limiter les dimensions de l'image
- ☑️ Protéger le dossier uploads avec .htaccess
- ☑️ Ne jamais exécuter d'images uploadées
- ☑️ Implémenter un rate limiting
Manipulation d'images avec GD
PHP inclut la bibliothèque GD pour manipuler les images : redimensionnement, recadrage, rotation, compression, etc.
Redimensionner une image
<?php
function resizeImage($sourcePath, $destPath, $maxWidth, $maxHeight, $quality = 85) {
// 1. Obtenir les informations de l'image
$imageInfo = getimagesize($sourcePath);
if ($imageInfo === false) {
return false;
}
list($origWidth, $origHeight, $imageType) = $imageInfo;
// 2. Créer une ressource image depuis le fichier source
switch ($imageType) {
case IMAGETYPE_JPEG:
$sourceImage = imagecreatefromjpeg($sourcePath);
break;
case IMAGETYPE_PNG:
$sourceImage = imagecreatefrompng($sourcePath);
break;
case IMAGETYPE_GIF:
$sourceImage = imagecreatefromgif($sourcePath);
break;
case IMAGETYPE_WEBP:
$sourceImage = imagecreatefromwebp($sourcePath);
break;
default:
return false;
}
// 3. Calculer les nouvelles dimensions (conserver le ratio)
$ratio = min($maxWidth / $origWidth, $maxHeight / $origHeight);
// Si l'image est déjà plus petite, ne pas l'agrandir
if ($ratio >= 1) {
$newWidth = $origWidth;
$newHeight = $origHeight;
} else {
$newWidth = (int)($origWidth * $ratio);
$newHeight = (int)($origHeight * $ratio);
}
// 4. Créer une nouvelle image vide
$destImage = imagecreatetruecolor($newWidth, $newHeight);
// 5. Préserver la transparence pour PNG et GIF
if ($imageType === IMAGETYPE_PNG || $imageType === IMAGETYPE_GIF) {
imagealphablending($destImage, false);
imagesavealpha($destImage, true);
$transparent = imagecolorallocatealpha($destImage, 255, 255, 255, 127);
imagefilledrectangle($destImage, 0, 0, $newWidth, $newHeight, $transparent);
}
// 6. Copier et redimensionner
imagecopyresampled(
$destImage, $sourceImage,
0, 0, 0, 0,
$newWidth, $newHeight,
$origWidth, $origHeight
);
// 7. Sauvegarder l'image redimensionnée
switch ($imageType) {
case IMAGETYPE_JPEG:
imagejpeg($destImage, $destPath, $quality);
break;
case IMAGETYPE_PNG:
// PNG : qualité de 0 (pas de compression) à 9 (max compression)
$pngQuality = (int)(9 - ($quality / 100) * 9);
imagepng($destImage, $destPath, $pngQuality);
break;
case IMAGETYPE_GIF:
imagegif($destImage, $destPath);
break;
case IMAGETYPE_WEBP:
imagewebp($destImage, $destPath, $quality);
break;
}
// 8. Libérer la mémoire
imagedestroy($sourceImage);
imagedestroy($destImage);
return true;
}
// Utilisation
$source = 'uploads/original.jpg';
$dest = 'uploads/thumb_300x300.jpg';
resizeImage($source, $dest, 300, 300, 85);
?>
Créer plusieurs versions (thumbnails)
<?php
function uploadImageWithThumbs($fileInput) {
// Upload de l'image originale
$result = uploadImage($fileInput);
if (!$result['success']) {
return $result;
}
$originalPath = $result['path'];
$baseDir = dirname($originalPath) . '/';
$filename = pathinfo($originalPath, PATHINFO_FILENAME);
$extension = pathinfo($originalPath, PATHINFO_EXTENSION);
// Définir les tailles de miniatures
$sizes = [
'thumb' => ['width' => 150, 'height' => 150],
'small' => ['width' => 300, 'height' => 300],
'medium' => ['width' => 800, 'height' => 600],
'large' => ['width' => 1920, 'height' => 1080]
];
$thumbnails = [];
foreach ($sizes as $name => $dimensions) {
$thumbPath = $baseDir . $filename . '_' . $name . '.' . $extension;
if (resizeImage(
$originalPath,
$thumbPath,
$dimensions['width'],
$dimensions['height']
)) {
$thumbnails[$name] = $thumbPath;
}
}
$result['thumbnails'] = $thumbnails;
return $result;
}
?>
Autres manipulations GD
Recadrage carré (crop)
function cropSquare($src, $dst, $size) {
$img = imagecreatefromjpeg($src);
list($w, $h) = getimagesize($src);
// Trouver le côté le plus petit
$min = min($w, $h);
// Calculer les offsets pour centrer
$offsetX = ($w - $min) / 2;
$offsetY = ($h - $min) / 2;
$dest = imagecreatetruecolor($size, $size);
imagecopyresampled(
$dest, $img,
0, 0, $offsetX, $offsetY,
$size, $size, $min, $min
);
imagejpeg($dest, $dst, 90);
imagedestroy($img);
imagedestroy($dest);
}
Rotation
$image = imagecreatefromjpeg('photo.jpg');
// Rotation de 90° dans le sens horaire
$rotated = imagerotate($image, -90, 0);
imagejpeg($rotated, 'photo_rotated.jpg', 90);
imagedestroy($image);
imagedestroy($rotated);
Filigrane (watermark)
$main = imagecreatefromjpeg('photo.jpg');
$watermark = imagecreatefrompng('logo.png');
list($wmW, $wmH) = getimagesize('logo.png');
list($mainW, $mainH) = getimagesize('photo.jpg');
// Position en bas à droite
$x = $mainW - $wmW - 10;
$y = $mainH - $wmH - 10;
imagecopy($main, $watermark, $x, $y, 0, 0, $wmW, $wmH);
imagejpeg($main, 'photo_wm.jpg', 90);
imagedestroy($main);
imagedestroy($watermark);
Filtre noir et blanc
$image = imagecreatefromjpeg('photo.jpg');
// Appliquer un filtre
imagefilter($image, IMG_FILTER_GRAYSCALE);
// Autres filtres disponibles:
// IMG_FILTER_BRIGHTNESS
// IMG_FILTER_CONTRAST
// IMG_FILTER_COLORIZE
// IMG_FILTER_EDGEDETECT
// IMG_FILTER_EMBOSS
// IMG_FILTER_GAUSSIAN_BLUR
imagejpeg($image, 'photo_bw.jpg', 90);
imagedestroy($image);
Bibliothèques tierces
Pour des manipulations plus avancées, utilisez des bibliothèques spécialisées.
🎨 Intervention Image
Bibliothèque PHP moderne et élégante pour la manipulation d'images
Installation (Composer)
composer require intervention/image
Exemple d'utilisation
<?php
require 'vendor/autoload.php';
use Intervention\Image\ImageManagerStatic as Image;
// Redimensionner
Image::make('photo.jpg')
->resize(300, 200)
->save('photo_small.jpg', 80);
// Recadrage carré
Image::make('photo.jpg')
->fit(500, 500)
->save('photo_square.jpg');
// Filtres
Image::make('photo.jpg')
->greyscale()
->blur(10)
->save('photo_artistic.jpg');
// Texte
Image::make('photo.jpg')
->text('Copyright 2026', 10, 10, function($font) {
$font->file('arial.ttf');
$font->size(24);
$font->color('#ffffff');
$font->align('left');
})
->save('photo_copyright.jpg');
?>
✨ Avantages
- API fluide et intuitive
- Support GD et Imagick
- Nombreux filtres et effets
- Gestion automatique de la transparence
🖼️ ImageMagick (Imagick)
Extension PHP pour ImageMagick, très puissante pour le traitement d'images
Vérifier l'installation
<?php
if (extension_loaded('imagick')) {
echo "Imagick disponible";
} else {
echo "Installez l'extension imagick";
}
?>
Exemple d'utilisation
<?php
$image = new Imagick('photo.jpg');
// Redimensionner en conservant le ratio
$image->thumbnailImage(800, 600, true);
// Compression
$image->setImageCompression(Imagick::COMPRESSION_JPEG);
$image->setImageCompressionQuality(85);
// Rotation automatique selon EXIF
$image->autoOrientImage();
// Supprimer les métadonnées (EXIF, etc.)
$image->stripImage();
// Recadrage intelligent (détection de contenu)
$image->cropThumbnailImage(500, 500);
// Sauvegarder
$image->writeImage('photo_processed.jpg');
$image->clear();
$image->destroy();
?>
✨ Avantages
- Plus rapide que GD
- Support de nombreux formats
- Recadrage intelligent
- Gestion avancée des métadonnées
⚡ PHP GD (natif)
Bibliothèque native de PHP, incluse par défaut
✅ Avantages
- Incluse avec PHP (aucune installation)
- Légère et rapide
- Suffisante pour la plupart des besoins
❌ Inconvénients
- API moins élégante
- Moins de formats supportés
- Moins performante qu'ImageMagick
Quelle bibliothèque choisir ?
GD : Pour des besoins simples (redimensionnement, crop)
Intervention Image : Pour une API moderne et élégante
Imagick : Pour des besoins avancés et performance maximale
Upload d'images multiples
Formulaire pour plusieurs images
<form action="upload-multiple.php" method="POST" enctype="multipart/form-data">
<label>Sélectionnez plusieurs images :</label>
<input type="file"
name="images[]"
multiple
accept="image/*">
<button type="submit">Télécharger</button>
</form>
Important
Notez les crochets [] dans name="images[]". Cela crée un tableau dans $_FILES.
Traitement PHP
<?php
function uploadMultipleImages($fileInput) {
if (!isset($_FILES[$fileInput])) {
return ['success' => false, 'error' => 'Aucun fichier'];
}
$files = $_FILES[$fileInput];
$uploadedFiles = [];
$errors = [];
// Nombre de fichiers
$fileCount = count($files['name']);
// Limiter le nombre de fichiers
if ($fileCount > 10) {
return ['success' => false, 'error' => 'Maximum 10 fichiers'];
}
// Traiter chaque fichier
for ($i = 0; $i < $fileCount; $i++) {
// Vérifier les erreurs
if ($files['error'][$i] !== UPLOAD_ERR_OK) {
$errors[] = "Erreur pour {$files['name'][$i]}";
continue;
}
// Reconstituer le tableau $_FILES pour un fichier
$singleFile = [
'name' => $files['name'][$i],
'type' => $files['type'][$i],
'tmp_name' => $files['tmp_name'][$i],
'error' => $files['error'][$i],
'size' => $files['size'][$i]
];
// Sauvegarder temporairement dans $_FILES
$_FILES['temp_single'] = $singleFile;
// Utiliser la fonction d'upload existante
$result = uploadImage('temp_single');
if ($result['success']) {
$uploadedFiles[] = $result;
} else {
$errors[] = $result['error'];
}
}
return [
'success' => count($uploadedFiles) > 0,
'uploaded' => $uploadedFiles,
'errors' => $errors,
'count' => count($uploadedFiles)
];
}
// Utilisation
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$result = uploadMultipleImages('images');
if ($result['success']) {
echo "<h2>{$result['count']} image(s) uploadée(s)</h2>";
foreach ($result['uploaded'] as $file) {
echo "<div>";
echo "<img src='{$file['path']}' width='200'><br>";
echo "Fichier : {$file['filename']}<br>";
echo "Taille : " . round($file['size'] / 1024, 2) . " KB";
echo "</div>";
}
if (!empty($result['errors'])) {
echo "<h3>Erreurs :</h3><ul>";
foreach ($result['errors'] as $error) {
echo "<li>$error</li>";
}
echo "</ul>";
}
} else {
echo "Erreur : {$result['error']}";
}
}
?>
Upload via drag & drop (JavaScript)
<div id="dropzone" class="dropzone">
Glissez-déposez des images ici
</div>
<div id="preview-container"></div>
<style>
.dropzone {
border: 3px dashed #4f46e5;
border-radius: 10px;
padding: 50px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.dropzone.dragover {
background: rgba(79, 70, 229, 0.1);
border-color: #6366f1;
}
</style>
<script>
const dropzone = document.getElementById('dropzone');
const previewContainer = document.getElementById('preview-container');
// Empêcher le comportement par défaut
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropzone.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
// Effet visuel lors du survol
['dragenter', 'dragover'].forEach(eventName => {
dropzone.addEventListener(eventName, () => {
dropzone.classList.add('dragover');
}, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropzone.addEventListener(eventName, () => {
dropzone.classList.remove('dragover');
}, false);
});
// Gestion du drop
dropzone.addEventListener('drop', function(e) {
const files = e.dataTransfer.files;
handleFiles(files);
}, false);
// Upload via clic
dropzone.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.accept = 'image/*';
input.onchange = e => handleFiles(e.target.files);
input.click();
});
function handleFiles(files) {
[...files].forEach(uploadFile);
}
function uploadFile(file) {
const formData = new FormData();
formData.append('image', file);
fetch('upload.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
const img = document.createElement('img');
img.src = data.path;
img.width = 200;
previewContainer.appendChild(img);
}
})
.catch(error => console.error('Erreur:', error));
}
</script>
Stratégies de stockage
📂 Système de fichiers
✅ Avantages
- Simple à mettre en place
- Performance native (pas de conversion)
- Facile à sauvegarder
- Serveur web peut servir directement
❌ Inconvénients
- Difficile à scaler horizontalement
- Gestion des permissions
- Backup plus complexe en cluster
// Structure recommandée
uploads/
├── 2026/
│ ├── 01/
│ │ ├── abc123.jpg
│ │ └── def456.png
│ └── 02/
└── .htaccess
// Code
$uploadDir = 'uploads/' . date('Y/m') . '/';
mkdir($uploadDir, 0755, true);
💾 Base de données (BLOB)
✅ Avantages
- Tout centralisé
- Transactions atomiques
- Backup avec le reste des données
- Pas de problème de permissions
❌ Inconvénients
- Performance réduite
- Taille de base de données élevée
- Pas de cache navigateur
- Pas recommandé pour SQLite
-- Table pour stocker les images
CREATE TABLE image (
id INTEGER PRIMARY KEY,
filename TEXT,
mime_type TEXT,
size INTEGER,
data BLOB,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
// Insérer une image
$imageData = file_get_contents($tmpPath);
$stmt = $pdo->prepare("
INSERT INTO image (filename, mime_type, size, data)
VALUES (?, ?, ?, ?)
");
$stmt->execute([
$filename,
$mimeType,
filesize($tmpPath),
$imageData
]);
// Récupérer et afficher
$stmt = $pdo->prepare("SELECT * FROM image WHERE id = ?");
$stmt->execute([$id]);
$image = $stmt->fetch();
header("Content-Type: {$image['mime_type']}");
echo $image['data'];
🔗 Chemin en BDD + Fichier
✅ Avantages (RECOMMANDÉ)
- Meilleur des deux mondes
- Performance optimale
- Métadonnées en BDD
- Fichiers sur disque
CREATE TABLE image (
id INTEGER PRIMARY KEY,
filename TEXT NOT NULL,
path TEXT NOT NULL,
mime_type TEXT,
size INTEGER,
width INTEGER,
height INTEGER,
article_id INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (article_id) REFERENCES article(id)
);
// Sauvegarder le chemin en BDD
$stmt = $pdo->prepare("
INSERT INTO image
(filename, path, mime_type, size, width, height, article_id)
VALUES (?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$newFileName,
$destination,
$mimeType,
$fileSize,
$width,
$height,
$articleId
]);
// Récupérer les images d'un article
$stmt = $pdo->prepare("
SELECT * FROM image WHERE article_id = ?
");
$stmt->execute([$articleId]);
$images = $stmt->fetchAll();
☁️ Cloud Storage (S3, etc.)
✅ Avantages
- Scalabilité infinie
- CDN intégré
- Pas de gestion serveur
- Géoréplication
❌ Inconvénients
- Coûts variables
- Dépendance externe
- Configuration plus complexe
// Exemple avec AWS S3 (nécessite aws/aws-sdk-php)
use Aws\S3\S3Client;
$s3 = new S3Client([
'version' => 'latest',
'region' => 'us-east-1'
]);
$result = $s3->putObject([
'Bucket' => 'mon-bucket',
'Key' => 'uploads/' . $filename,
'SourceFile' => $tmpPath,
'ACL' => 'public-read'
]);
$url = $result['ObjectURL'];
Recommandation
Pour la plupart des projets : Fichiers sur disque + métadonnées en BDD
C'est le meilleur compromis entre performance, simplicité et fonctionnalités.
Upload d'images via API REST
Endpoint d'upload
<?php
header('Content-Type: application/json; charset=utf-8');
// Autoriser CORS (ajustez selon vos besoins)
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit(0);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Méthode non autorisée']);
exit;
}
// Connexion à la base de données
try {
$pdo = new PDO('sqlite:../database.db');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['error' => 'Erreur de connexion BDD']);
exit;
}
// Upload de l'image
require_once '../functions/upload.php';
$result = uploadImage('image');
if (!$result['success']) {
http_response_code(400);
echo json_encode(['error' => $result['error']]);
exit;
}
// Sauvegarder en BDD
try {
$stmt = $pdo->prepare("
INSERT INTO image (filename, path, mime_type, size, width, height)
VALUES (:filename, :path, :mime, :size, :width, :height)
");
$stmt->execute([
':filename' => $result['filename'],
':path' => $result['path'],
':mime' => $result['mime'],
':size' => $result['size'],
':width' => $result['width'],
':height' => $result['height']
]);
$imageId = $pdo->lastInsertId();
// Réponse
http_response_code(201);
echo json_encode([
'success' => true,
'data' => [
'id' => $imageId,
'filename' => $result['filename'],
'url' => 'https://' . $_SERVER['HTTP_HOST'] . '/' . $result['path'],
'size' => $result['size'],
'dimensions' => [
'width' => $result['width'],
'height' => $result['height']
]
]
], JSON_PRETTY_PRINT);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['error' => 'Erreur lors de la sauvegarde']);
}
?>
Client JavaScript (Fetch API)
async function uploadImage(file) {
// Créer un FormData
const formData = new FormData();
formData.append('image', file);
try {
const response = await fetch('api/upload.php', {
method: 'POST',
body: formData
});
const data = await response.json();
if (response.ok) {
console.log('Image uploadée:', data);
return data;
} else {
throw new Error(data.error || 'Erreur d\'upload');
}
} catch (error) {
console.error('Erreur:', error);
throw error;
}
}
// Utilisation avec un input file
document.getElementById('image-input').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const result = await uploadImage(file);
// Afficher l'image
const img = document.createElement('img');
img.src = result.data.url;
img.width = 300;
document.body.appendChild(img);
console.log('ID de l\'image:', result.data.id);
} catch (error) {
alert('Erreur: ' + error.message);
}
});
Associer des images à un article
<?php
// POST /api/article/5/images - Upload d'image pour l'article 5
$articleId = isset($_GET['id']) ? (int)$_GET['id'] : 0;
// Vérifier que l'article existe
$stmt = $pdo->prepare("SELECT id FROM article WHERE id = ?");
$stmt->execute([$articleId]);
if (!$stmt->fetch()) {
http_response_code(404);
echo json_encode(['error' => 'Article non trouvé']);
exit;
}
// Upload
$result = uploadImage('image');
if (!$result['success']) {
http_response_code(400);
echo json_encode(['error' => $result['error']]);
exit;
}
// Sauvegarder avec article_id
$stmt = $pdo->prepare("
INSERT INTO image
(filename, path, mime_type, size, width, height, article_id)
VALUES (?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$result['filename'],
$result['path'],
$result['mime'],
$result['size'],
$result['width'],
$result['height'],
$articleId
]);
http_response_code(201);
echo json_encode([
'success' => true,
'data' => [
'id' => $pdo->lastInsertId(),
'article_id' => $articleId,
'url' => 'https://' . $_SERVER['HTTP_HOST'] . '/' . $result['path']
]
]);
?>
Récupérer les images d'un article
<?php
// Récupérer un article avec ses images
$articleId = (int)$_GET['id'];
$stmt = $pdo->prepare("
SELECT * FROM article WHERE id = ?
");
$stmt->execute([$articleId]);
$article = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$article) {
http_response_code(404);
echo json_encode(['error' => 'Article non trouvé']);
exit;
}
// Récupérer les images
$stmt = $pdo->prepare("
SELECT id, filename, path, size, width, height
FROM image
WHERE article_id = ?
ORDER BY created_at
");
$stmt->execute([$articleId]);
$images = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Ajouter l'URL complète
foreach ($images as &$image) {
$image['url'] = 'https://' . $_SERVER['HTTP_HOST'] . '/' . $image['path'];
}
$article['images'] = $images;
echo json_encode([
'success' => true,
'data' => $article
], JSON_PRETTY_PRINT);
?>
Exercices pratiques
Formulaire d'upload basique
Objectif : Créer un formulaire HTML et un script PHP pour uploader une image.
Tâches :
- Créer un formulaire avec
<input type="file"> - Valider le type de fichier (JPG, PNG uniquement)
- Limiter la taille à 2MB
- Afficher l'image uploadée
- Sauvegarder dans le dossier
uploads/
move_uploaded_file() et getimagesize()
Génération de miniature
Objectif : Après l'upload, créer automatiquement une miniature 200x200.
Tâches :
- Reprendre l'exercice 1
- Créer une fonction
createThumbnail() - Redimensionner l'image en conservant le ratio
- Sauvegarder avec le suffixe
_thumb - Afficher l'originale ET la miniature
imagecopyresampled()
Upload sécurisé
Objectif : Implémenter toutes les validations de sécurité.
Tâches :
- Vérifier le type MIME avec
finfo_file() - Générer un nom aléatoire avec
bin2hex(random_bytes()) - Créer un fichier
.htaccessdans uploads/ - Vérifier les dimensions maximales (4000x4000)
- Retourner un JSON avec les informations du fichier
Galerie d'images
Objectif : Créer une galerie avec base de données.
Tâches :
- Créer une table
imageen SQLite - Formulaire d'upload avec titre et description
- Sauvegarder le chemin en BDD
- Page de galerie affichant toutes les images
- Possibilité de supprimer une image
Upload multiple avec drag & drop
Objectif : Interface moderne avec JavaScript.
Tâches :
- Zone de drop avec feedback visuel
- Prévisualisation avant upload
- Barre de progression pour chaque fichier
- Upload asynchrone avec Fetch API
- Validation côté client ET serveur
- Affichage en grille après upload
Système de blog avec images
Objectif : Application complète avec API REST.
Structure BDD :
article: id, titre, contenu, created_atimage: id, filename, path, article_id, is_featured, order, created_at
Fonctionnalités :
- API REST complète (CRUD articles)
- Upload d'images pour un article
- Image mise en avant (featured)
- Ordre personnalisable des images
- Génération automatique de 3 tailles (thumb, medium, large)
- Interface admin pour gérer les articles et images
- Page publique affichant les articles avec images
Glossaire
$_FILES
Tableau superglobal PHP contenant les informations sur les fichiers uploadés via HTTP POST.
multipart/form-data
Type d'encodage requis pour les formulaires HTML qui envoient des fichiers. Permet la transmission de données binaires.
MIME Type
Type de média internet qui indique le format d'un fichier (ex: image/jpeg, image/png). Aussi appelé Content-Type.
move_uploaded_file()
Fonction PHP sécurisée pour déplacer un fichier uploadé du dossier temporaire vers sa destination finale.
GD Library
Bibliothèque PHP native pour la manipulation d'images : redimensionnement, recadrage, filtres, etc.
ImageMagick
Suite logicielle puissante de manipulation d'images. Accessible en PHP via l'extension Imagick.
Thumbnail (Miniature)
Version réduite d'une image, utilisée pour l'affichage en galerie ou en prévisualisation.
BLOB (Binary Large Object)
Type de données SQL pour stocker de gros objets binaires comme des images ou des fichiers.
Path Traversal
Attaque de sécurité où un utilisateur manipule le chemin de fichier pour accéder à des dossiers non autorisés (ex: ../../../etc/passwd).
Upload Directory
Dossier du serveur où sont stockés les fichiers uploadés par les utilisateurs.
finfo_file()
Fonction PHP qui détecte le véritable type MIME d'un fichier en analysant son contenu (et non son extension).
getimagesize()
Fonction PHP qui retourne les dimensions et le type d'une image. Valide qu'un fichier est bien une image.
imagecopyresampled()
Fonction GD pour copier et redimensionner une image avec rééchantillonnage (meilleure qualité).
FormData
API JavaScript pour construire des données de formulaire, y compris des fichiers, pour envoi via Fetch ou XMLHttpRequest.
Rate Limiting
Technique de sécurité limitant le nombre de requêtes (uploads) qu'un utilisateur peut effectuer dans un laps de temps.
Watermark (Filigrane)
Image ou texte superposé sur une image pour indiquer la propriété ou protéger contre la copie non autorisée.
Ressources supplémentaires
PHP Manual - File Upload
Documentation officielle PHP sur l'upload de fichiers
PHP Manual - GD
Documentation complète de la bibliothèque GD
Intervention Image
Documentation de la bibliothèque Intervention Image
PHP Imagick
Documentation de l'extension ImageMagick pour PHP
OWASP - File Upload
Guide de sécurité pour l'upload de fichiers
MDN - File API
Documentation JavaScript pour la manipulation de fichiers