📸 Téléchargement d'images avec PHP

Maîtrisez l'upload, la validation, la manipulation et le stockage sécurisé d'images

⏱️ 3-4 heures de lecture 📚 Niveau intermédiaire 💻 PHP & GD requis

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

1

Formulaire HTML

Créer un formulaire avec <input type="file">

2

Réception PHP

Récupérer le fichier via $_FILES

3

Validation

Vérifier type, taille, et sécurité

4

Manipulation

Redimensionner, compresser, optimiser

5

Stockage

Sauvegarder sur disque ou base de données

6

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

📁 check-config.php
<?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

📄 upload-form.html
<!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

Formulaire avec preview JavaScript
<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.

Structure de $_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

📁 upload.php (version 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

📁 upload-secure.php
<?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é

Critique

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']);
Critique

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';
Haute

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
Haute

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>
Moyenne

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');
}
Moyenne

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

Fonction de redimensionnement
<?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)

Upload avec génération de miniatures
<?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

Formulaire avec multiple
<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

📁 upload-multiple.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)

Zone de drop avec 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

📁 api/upload.php
<?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)

Uploader une image avec JavaScript
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

📁 api/article/{id}/images.php
<?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

GET /api/article/{id}
<?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

Exercice 1 Facile

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/
💡 Utilisez move_uploaded_file() et getimagesize()
Exercice 2 Facile

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
💡 Utilisez imagecopyresampled()
Exercice 3 Moyen

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 .htaccess dans uploads/
  • Vérifier les dimensions maximales (4000x4000)
  • Retourner un JSON avec les informations du fichier
Exercice 4 Moyen

Galerie d'images

Objectif : Créer une galerie avec base de données.

Tâches :

  • Créer une table image en 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
💡 N'oubliez pas de supprimer le fichier physique lors de la suppression
Exercice 5 Difficile

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
Projet final Difficile

Système de blog avec images

Objectif : Application complète avec API REST.

Structure BDD :

  • article : id, titre, contenu, created_at
  • image : 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
🌟 Bonus : Ajouter un éditeur WYSIWYG qui permet d'insérer des images dans le contenu

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.