Aller au contenu principal
Site en cours de refonte — quelques pages peuvent bouger ou évoluer.
14 avril 2026

Ton import Laravel « lent » ? Parfois c’est juste la validation wildcard qui part en O(n²)

Tu optimises la DB, tu queues, tu chunk… et ton import Laravel rame toujours. Si tu valides des gros tableaux avec des wildcards, tu peux juste être en train de brûler du CPU en O(n²).

10 min de lecture
36 vues
réactions
Partager :
Ton import Laravel « lent » ? Parfois c’est juste la validation wildcard qui part en O(n²)

Si ton import Laravel met 30 secondes « sans raison », que tes requêtes SQL sont propres et que tu as déjà chunké tout ce qui bouge, je vais te dire un truc pas très glamour : tu es peut-être en train de perdre ton temps dans la validation, pas dans l’insert.

Et le pire, c’est que ça peut arriver sur un code parfaitement « normal » : validation d’un gros tableau (CSV, payload API, batch sync) avec des règles en wildcards du style rows.*.email, plus deux-trois règles conditionnelles. Sur un import de quelques milliers de lignes, ça peut exploser très vite. Le symptôme ressemble à une DB lente. En vrai, c’est juste du CPU qui mouline à vide.

Le pattern qui fait mal : wildcards + gros tableaux = explosion de travail

La validation Laravel des tableaux est super pratique. Tu reçois rows, tu fais des règles sur rows.*.email, rows.*.country, et tu as l’impression d’avoir une validation propre et « déclarative ».

Sauf qu’avec des gros tableaux, certaines combinaisons de règles amènent Laravel à étendre les règles wildcard en parcourant beaucoup de clés et de sous-clés. Dans le cas discuté récemment (et que j’ai déjà vu en import CSV), la mécanique ressemble à « on liste toutes les clés possibles, on fait matcher des patterns, on génère des règles concrètes ». Quand tu as N lignes et que tu fais ça de manière pas très linéaire, tu te retrouves à payer un coût qui ressemble à du O(n²) : chaque nouvelle ligne fait grossir le travail total plus que proportionnellement.

Ça arrive souvent quand tu as des règles du type required_if, prohibited_unless, sometimes, des closures, ou des règles qui dépendent d’autres champs dans la même ligne. Ça force le validateur à regarder « autour », donc à scanner plus large. Sur 50 lignes, tu ne vois rien. Sur 10 000, c’est la boucherie.

Comment prouver que c’est la validation (et arrêter d’accuser MySQL)

Le piège classique, c’est de profiler l’import « globalement » et de conclure que « l’import est lent ». Moi je veux savoir part le temps, tout de suite. Sur un import, tu as généralement trois gros blocs : parsing (CSV/JSON), validation, écriture (DB / API / events). Si tu ne mesures pas séparément, tu vas optimiser au hasard.

Le moyen le plus simple est bête mais efficace : tu timebox chaque phase avec des timestamps. Pas besoin d’un outil fancy pour avoir un premier signal fiable.

$t0 = microtime(true);

// 1) parse
$rows = $this->parseCsv($path);
$t1 = microtime(true);

// 2) validate
$validator = \Illuminate\Support\Facades\Validator::make(
    ['rows' => $rows],
    $this->rules()
);
$validator->validate();
$t2 = microtime(true);

// 3) write
$this->persistRows($rows);
$t3 = microtime(true);

logger()->info('import timings', [
    'parse_ms' => (int)(($t1 - $t0) * 1000),
    'validate_ms' => (int)(($t2 - $t1) * 1000),
    'write_ms' => (int)(($t3 - $t2) * 1000),
    'total_ms' => (int)(($t3 - $t0) * 1000),
    'rows' => count($rows),
]);

Si tu vois validate_ms prendre 80% du temps, fin de l’histoire. La DB est innocente. Et si tu veux aller plus loin, tu peux brancher un profiler (Blackfire, Tideways, Xdebug profiler). Le but n’est pas de faire un roman, juste d’attraper la fonction ou la section qui scanne tout ton tableau.

Un autre indice très parlant : tu doubles le nombre de lignes, et le temps de validation est multiplié par 3, 4, 5… Là tu sais que tu n’es pas sur un coût linéaire classique.

Les règles qui déclenchent l’explosion (celles que je repère en premier)

Je me méfie quand je vois des règles wildcard profondes sur des structures très larges. Typiquement rows.*.data.*.value ou des objets « flexibles » où chaque ligne a des clés différentes. Plus c’est hétérogène, plus la phase d’expansion doit faire des hypothèses, matcher des clés, et ça finit par coûter cher.

Je me méfie aussi quand la règle dépend d’un autre champ sur la même ligne. Exemple vécu : rows.*.vat_number est requis si rows.*.country vaut FR, mais optionnel sinon. C’est parfaitement logique côté métier. Côté validateur, ça peut inciter à parcourir le tableau pour trouver les bons chemins et appliquer la règle aux bons index.

Enfin, je me méfie quand on met « trop de logique » dans la validation parce que « c’est pratique ». La validation est faite pour être une barrière d’entrée, pas un mini-moteur de règles sur 50 000 lignes. Quand tu en es là, tu as besoin de contrôler le parcours, pas de laisser le framework deviner comment il doit itérer.

Niveau 1 : le fix le plus rentable, segmenter et valider par chunk

Le fix qui sauve le plus de prod, c’est de ne pas valider 20 000 lignes d’un coup. Pas parce que la validation en chunk est plus « pure », mais parce que tu limites mécaniquement la taille de l’espace que Laravel doit parcourir et matcher. Et tu évites d’avoir un seul énorme pic CPU qui finit en timeout.

Concrètement, tu lis ton CSV en streaming, tu accumules 200 ou 500 lignes, tu valides ce chunk, puis tu écris, puis tu continues. Oui, tu perds la « belle validation globale » qui te remonte toutes les erreurs d’un coup. En échange, ton import termine, et tu peux remonter des erreurs progressivement (par exemple en stockant les erreurs par chunk dans un fichier ou une table).

Ce que j’aime avec cette approche, c’est qu’elle ne te force pas à réinventer ton système de règles. Tu gardes la plupart de tes règles Laravel, tu changes juste la façon dont tu nourris le validateur.

Niveau 2 : arrêter d’« étendre » les rules, et valider ligne par ligne

Quand tu valides un tableau entier avec des wildcards, tu pousses Laravel à construire mentalement un mapping complet « règle → chemins concrets ». Si tu veux reprendre la main, une approche simple consiste à valider chaque ligne avec des règles non-wildcard, donc sans phase d’expansion massive.

Ça demande de reformuler tes règles. Au lieu de rows.*.email, tu valides email. Au lieu de dépendre d’un chemin global, tu valides une structure locale. Tu peux même réutiliser les mêmes règles, mais appliquées à un item unique.

foreach ($rows as $i => $row) {
    $validator = \Illuminate\Support\Facades\Validator::make($row, [
        'email' => ['required', 'email'],
        'country' => ['required', 'string', 'size:2'],
        'vat_number' => ['nullable', 'string'],
    ]);

    $validator->sometimes('vat_number', ['required'], function ($input) {
        return $input->country === 'FR';
    });

    if ($validator->fails()) {
        // Tu logges/stockes l'erreur avec l'index $i, et tu décides si tu stop ou si tu continues.
        $errors[$i] = $validator->errors()->toArray();
        continue;
    }

    $this->persistRow($row);
}

Ce n’est pas aussi « élégant » que trois règles avec des étoiles. Mais c’est lisible, contrôlable, et surtout le coût devient beaucoup plus prévisible. Tu peux aussi arrêter au premier échec si ton besoin le permet, ou continuer pour produire un rapport d’erreurs complet sans faire fondre le CPU.

Niveau 3 : validation “streaming” et parcours O(n) assumé

Quand tu importes massivement, la vraie question n’est pas « comment faire entrer ça dans le validateur », c’est « comment parcourir une fois, valider une fois, et ne pas faire de travail répété ». Si ton cas est extrême, tu vas naturellement vers une validation plus « streaming » où tu contrôles le parcours et où tu appliques les règles au fil de l’eau.

C’est exactement l’idée de certains contournements évoqués par la communauté : remplacer une expansion wildcard qui rescane les clés par une traversal linéaire. Je ne vais pas te vendre un package les yeux fermés pour un sujet aussi central, mais l’architecture est saine : tu itères sur les données et tu appliques des règles sans rematcher l’ensemble des chemins à chaque fois.

Sur un import CSV, ça colle très bien : tu lis une ligne, tu normalises, tu valides localement, tu écris, tu passes à la suivante. Tu payes un coût proche du O(n). Et surtout, tu sais pourquoi.

Les erreurs d’arbitrage que je vois tout le temps sur les imports Laravel

La première, c’est de mettre la validation « comme sur un formulaire », mais à l’échelle d’un batch. Laravel te laisse le faire, donc on le fait. Le problème, c’est que l’import n’a pas le même contrat : tu as du volume, du sale, et souvent un besoin de résilience. Tu ne veux pas forcément jeter tout le batch parce que la ligne 8123 a un champ manquant.

La deuxième, c’est d’essayer de compenser avec de l’infra. Augmenter le timeout, mettre en queue, scaler des workers. Parfois c’est nécessaire. Mais si ton algo est en train de partir en O(n²), tu ne fais que payer plus cher pour brûler plus de CPU. Et tu te retrouves avec un import qui passe en staging et qui explose en prod le jour où tu as un fichier un peu plus gros.

La troisième, c’est de croire que « plus de règles = plus de sécurité ». Non. Sur un import, le meilleur gain qualité vient souvent d’une normalisation stricte (trim, encoding, mapping des headers, valeurs par défaut), puis d’une validation simple et robuste. Les règles ultra fines, tu peux les déplacer après l’import, en contrôles métier asynchrones, surtout si elles impliquent des dépendances externes ou des cross-checks entre lignes.

Avant de lancer un import en prod : ce que je vérifie vraiment

Je veux un timing par phase dans les logs, parce que sans ça tu pilotes à l’instinct. Je veux connaître la taille max réaliste d’un fichier ou d’un payload, pas la taille « habituelle ». Je veux que l’import tienne en chunk, et qu’il sache produire un rapport d’erreurs sans faire tomber tout le job. Et je veux un garde-fou simple, même moche, qui coupe l’import si on détecte un comportement anormal, parce que le pire scénario c’est un worker qui mouline 20 minutes pour finir en timeout, tout ça en silence.

Dernier point, très terre-à-terre : si tu gardes des wildcards, teste avec un dataset « méchant ». Un CSV propre ne te dira rien. Ce sont les données hétérogènes, les champs manquants, les colonnes optionnelles et les structures imbriquées qui déclenchent les comportements coûteux.

Conclusion : parfois, “Laravel est lent” veut juste dire “on a validé comme des bourrins”

Laravel n’est pas magiquement lent. Il est juste très bon pour te donner une API agréable, et parfois tu payes l’addition quand tu l’utilises hors de son terrain naturel. La validation wildcard sur de gros imports, c’est typiquement le genre de coin où tu peux te faire surprendre.

La bonne nouvelle, c’est que tu n’as pas besoin de jeter ton import. Mesure la phase de validation, identifie si tu es dans ce pattern, puis choisis ton niveau de fix. Chunker suffit souvent. Valider ligne par ligne résout la majorité des cas. Et si tu es sur un import vraiment massif, assume une approche streaming et linéaire, tu dormiras mieux.

Sources

Cet article vous a plu ?

Commentaires

Laisser un commentaire

Entre 10 et 2000 caractères

Les commentaires sont modérés avant publication.

Aucun commentaire pour le moment.

Soyez le premier à donner votre avis !