L'erreur

Lors de l’importation de fichiers Excel volumineux dans une application Laravel, vous avez probablement rencontré cette erreur frustrante :

[2025-11-08 14:31:16] local.ERROR: Error: SQLSTATE[HY000]: General error: 2006 MySQL server has gone away

Cette erreur survient généralement lorsque vous tentez d’importer des milliers de lignes de données en une seule fois. La connexion MySQL se ferme brutalement, laissant votre application dans un état d’échec.

Le contexte

Dans mon cas, je travaillais sur une fonctionnalité d’import de personnalités pour une application Laravel. L’utilisateur peut uploader un fichier Excel contenant des centaines, voire des milliers de personnalités avec leurs informations (nom, prénom, profession, nationalité, année de naissance, biographie).

Voici la route que j’utilisais :

Route::post(‘personnalities/upload’, ‘App\Http\Controllers\PersonnalityController@uploadExcelPersonnalities’);

Ma première implémentation était simple et directe : recevoir le fichier, le valider, puis l’importer immédiatement avec Laravel Excel. Tout fonctionnait parfaitement… jusqu’à ce que les fichiers dépassent quelques centaines de lignes.

Les raisons de l'erreur

L’erreur « MySQL server has gone away » survient pour plusieurs raisons principales :

  1. Timeout de connexion MySQL Lorsque votre script PHP prend trop de temps à s’exécuter, MySQL considère que la connexion est morte et la ferme automatiquement. Par défaut, ce timeout est souvent configuré à 28800 secondes (8 heures), mais peut être plus court selon votre configuration.
  2. Dépassement de la mémoire L’import de milliers de lignes en une seule fois charge toutes les données en mémoire. Si votre processus PHP dépasse la limite de mémoire allouée, cela peut provoquer l’interruption de la connexion.
  3. Packet trop volumineux MySQL a une limite sur la taille des paquets (max_allowed_packet). Si vous tentez d’insérer trop de données en une seule requête, le serveur refuse la connexion.
  4. Timeout du script PHP Le max_execution_time de PHP peut également expirer avant la fin de l’import, causant l’interruption de la connexion à la base de données.

La solution : Jobs asynchrones et traitement par lots

Pour résoudre ce problème, j’ai implémenté une approche en deux volets : traitement immédiat pour les petits fichiers et traitement asynchrone par lots pour les fichiers volumineux.

Code contrôleur

    
public function uploadExcelPersonnalities(Request $request)
{
    $validator = Validator::make($request->all(), [
        'file' => 'required|file',
    ]);

    if ($validator->fails()) {
        return response()->json([
            'errors' => $validator->errors(),
        ], 422);
    }

    try {
        // Pour les fichiers < 300 Ko : traitement immédiat
        if ($request->file('file')->getSize() < 307200) {
            Excel::import(new PersonnalityImport, $request->file('file'));
                
            return [
                'message' => 'File uploaded successfully',
                'status' => 'finish'
            ];
        }
        else {
            // Pour les gros fichiers : traitement asynchrone
            $path = Storage::disk('local')->putFile('personnalities', $request->file('file'));

            $batch = $this->personnalityService->uploadPersonnalities($path);

            return [
                'message' => 'Import successfully launched',
                'batch_id' => $batch->id,
                'status' => 'ongoing'
            ];
        }
    } catch (\Throwable $th) {
        Log::error('Error: ' . $th->getMessage());
        Log::error('Error: ' . $th->getTraceAsString());

        if (isset($path)) {
            Storage::delete($path);
        }

        return response()->json([
            'error' => 'Error during import.'
        ], 500);
    }
}
    

Le service de découpage

Le service PersonnalityServices se charge de découper le fichier en morceaux gérables :

    

namespace App\Services;

use App\Imports\PersonnalityImport;
use App\Jobs\ProcessPersonnalityChunk;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Maatwebsite\Excel\Facades\Excel;

class PersonnalityServices
{
    public function uploadPersonnalities($path)
    {
        try {
            $filePath = Storage::disk('local')->path($path);

            // Conversion du fichier Excel en tableau
            $rows = Excel::toArray(new PersonnalityImport, $filePath);

            if (empty($rows)) {
                throw new \Exception("Le fichier est vide.");
            }

            // Découpage en chunks de 40 lignes
            $chunks = array_chunk($rows[0], 40);

            if (empty($chunks)) {
                throw new \Exception("Impossible de découper le fichier.");
            }

            $jobs = [];

            // Création d'un job pour chaque chunk
            foreach ($chunks as $index => $chunk) {
                $chunkFile = "imports/chunks/chunk_{$index}.json";
                Storage::put($chunkFile, json_encode($chunk));

                $jobs[] = new ProcessPersonnalityChunk($chunkFile);
            }

            // Lancement du batch de jobs
            $batch = Bus::batch($jobs)
                ->then(function () use ($path) {
                    Storage::delete($path);
                })
                ->catch(function () use ($path) {
                    Storage::delete($path);
                })
                ->dispatch();

            return $batch;

        } catch (\Throwable $th) {
            Log::error("Error uploading personnalities: " . $th->getMessage());
        }
    }
}
    

Le Job de traitement

Chaque chunk est traité par un job indépendant :

    
namespace App\Jobs;

use App\Models\Personnality;
use Illuminate\Bus\Batchable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;

class ProcessPersonnalityChunk implements ShouldQueue
{
    use Queueable, InteractsWithQueue, SerializesModels;
    use Batchable;

    public int $tries = 3;
    public int $timeout = 300;

    public string $file;

    public function __construct(string $file)
    {
        $this->file = $file;
    }

    public function handle(): void
    {
        // Vérification si le batch a été annulé
        if ($this->batch()?->cancelled()) {
            Log::warning("Batch cancelled for file: {$this->file}");
            return;
        }

        if (!Storage::exists($this->file)) {
            Log::error("Chunk file not found: {$this->file}");
            return;
        }

        $chunk = json_decode(Storage::get($this->file), true);

        // Traitement ligne par ligne
        foreach ($chunk as $row) {
            try {
                Personnality::create([
                    'first_name' => $row[1],
                    'last_name' => $row[2],
                    'profession' => $row[4],
                    'nationality' => $row[5],
                    'birth_year' => $row[6],
                    'short_bio' => $row[7],
                ]);
            } catch (\Throwable $th) {
                throw $th;
            }
        }

        // Nettoyage du fichier chunk
        Storage::delete($this->file);
    }

    public function failed(\Throwable $exception): void
    {
        Log::error("Job failed for file: {$this->file}", [
            'error' => $exception->getMessage(),
            'trace' => $exception->getTraceAsString()
        ]);
    }
}
    

Chaque chunk est traité par un job indépendant :

    
namespace App\Jobs;

use App\Models\Personnality;
use Illuminate\Bus\Batchable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;

class ProcessPersonnalityChunk implements ShouldQueue
{
    use Queueable, InteractsWithQueue, SerializesModels;
    use Batchable;

    public int $tries = 3;
    public int $timeout = 300;

    public string $file;

    public function __construct(string $file)
    {
        $this->file = $file;
    }

    public function handle(): void
    {
        // Vérification si le batch a été annulé
        if ($this->batch()?->cancelled()) {
            Log::warning("Batch cancelled for file: {$this->file}");
            return;
        }

        if (!Storage::exists($this->file)) {
            Log::error("Chunk file not found: {$this->file}");
            return;
        }

        $chunk = json_decode(Storage::get($this->file), true);

        // Traitement ligne par ligne
        foreach ($chunk as $row) {
            try {
                Personnality::create([
                    'first_name' => $row[1],
                    'last_name' => $row[2],
                    'profession' => $row[4],
                    'nationality' => $row[5],
                    'birth_year' => $row[6],
                    'short_bio' => $row[7],
                ]);
            } catch (\Throwable $th) {
                throw $th;
            }
        }

        // Nettoyage du fichier chunk
        Storage::delete($this->file);
    }

    public function failed(\Throwable $exception): void
    {
        Log::error("Job failed for file: {$this->file}", [
            'error' => $exception->getMessage(),
            'trace' => $exception->getTraceAsString()
        ]);
    }
}
    

Pourquoi cette solution fonctionne

Cette approche résout tous les problèmes mentionnés précédemment :

  1. Traitement asynchrone :

    Les jobs sont exécutés en arrière-plan par le système de queues de Laravel. La requête HTTP se termine immédiatement, évitant les timeouts PHP. L’utilisateur reçoit un batch_id pour suivre la progression.
  2. Découpage intelligent :

    En divisant le fichier en chunks de 40 lignes, chaque job traite une quantité limitée de données. Cela évite les dépassements de mémoire et garantit que chaque insertion reste dans les limites de max_allowed_packet.
  3. Connexions courtes :

    Chaque job établit sa propre connexion à la base de données, l’utilise brièvement, puis la libère. Cela évite les timeouts de connexion MySQL.
  4. Tolérance aux pannes :

    Avec public int $tries = 3, chaque job peut échouer et être réessayé jusqu’à 3 fois. Si un chunk échoue, les autres continuent leur traitement.
  5. Gestion hybride :

    Les petits fichiers (< 300 Ko) sont traités immédiatement pour une meilleure expérience utilisateur. Les gros fichiers sont automatiquement basculés vers le traitement asynchrone.
  6. Nettoyage automatique :

    Les callbacks then() et catch() du batch garantissent que le fichier original est supprimé, que l’import réussisse ou échoue.

Conclusion

L’erreur « MySQL server has gone away » est un problème courant lors du traitement de données volumineuses avec Laravel. La solution ne consiste pas à augmenter indéfiniment les timeouts ou les limites de mémoire, mais plutôt à repenser l’architecture du traitement.

En combinant le système de queues de Laravel, le traitement par lots et une gestion intelligente des fichiers, vous pouvez importer des dizaines de milliers de lignes sans aucun problème de timeout ou de mémoire.

Cette approche offre également une meilleure expérience utilisateur : au lieu d’attendre plusieurs minutes devant un écran de chargement, l’utilisateur reçoit immédiatement une confirmation que son import est en cours et peut continuer à utiliser l’application pendant le traitement.

Points clés à retenir :

    • Utilisez le système de queues pour les opérations longues
    • Découpez les données en chunks gérables
    • Implémentez une logique de retry pour la résilience
    • Nettoyez toujours les fichiers temporaires
    • Offrez un feedback immédiat à l’utilisateur

N’oubliez pas de configurer correctement votre queue worker et de créer la table jobs avec la migration appropriée. Avec cette solution, vos imports massifs se dérouleront sans accroc, quelle que soit la taille des fichiers !