Comment exporter des journaux financiers et des mouvements de stocks de plus de 100 000 lignes grâce aux curseurs de base de données et au flux HTTP continu sans dépasser les limites mémoire de PHP.
L'exportation de journaux financiers, de listes de factures ou d'historiques de stocks contenant des dizaines de milliers de lignes déclenche fréquemment des plantages mémoire en PHP (Fatal error: Allowed memory size exhausted).
Charger des collections d'objets Eloquent complètes en mémoire avant d'écrire un fichier CSV ou Excel est une mauvaise pratique. Voici comment nous avons conçu une réponse HTTP en flux continu (streamed response) s'appuyant sur des curseurs de base de données pour conserver une empreinte mémoire constante sous les 10 Mo.
Au lieu de faire appel à ->get(), utilisez ->cursor(). Les curseurs Eloquent récupèrent les enregistrements ligne par ligne via les générateurs PDO sans hydrater l'ensemble des modèles simultanément.
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// app/Http/Controllers/FinancialExportController.php
namespace App\Http\Controllers;
use App\Models\Transaction;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\StreamedResponse;
class FinancialExportController extends Controller
{
public function exportCsv(Request $request): StreamedResponse
{
$companyId = $request->user()->active_company_id;
$fileName = 'export_financier_' . date('Y_m_d_His') . '.csv';
$headers = [
'Content-Type' => 'text/csv',
'Content-Disposition' => "attachment; filename="{$fileName}"",
'Pragma' => 'no-cache',
'Cache-Control' => 'must-revalidate, post-check=0, pre-check=0',
'Expires' => '0',
];
return response()->stream(function () use ($companyId) {
$output = fopen('php:class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">//output', 'w');
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Écriture du BOM CSV pour la prise en charge UTF-8 sous MS Excel
fputs($output, $bom = (chr(0xEF) . chr(0xBB) . chr(0xBF)));
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Écriture de l'en-tête CSV
fputcsv($output, ['Référence', 'Date', 'Type', 'Montant', 'Statut']);
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Lecture progressive via curseur
Transaction::where('company_id', $companyId)
->orderBy('created_at', 'desc')
->cursor()
->each(function (Transaction $tx) use ($output) {
fputcsv($output, [
$tx->reference_number,
$tx->created_at->format('Y-m-d H:i:s'),
strtoupper($tx->type),
number_format($tx->amount, 2, '.', ''),
$tx->status,
]);
});
fclose($output);
}, 200, $headers);
}
}Pour télécharger des flux d'exportation sans interrompre l'expérience utilisateur de l'application monopage :
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// services/exportService.ts
import axios from 'axios';
export async function downloadFinancialExport(): Promise<void> {
const response = await axios.get('/api/exports/financial-csv', {
responseType: 'blob',
});
const blob = new Blob([response.data], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', class="text-emerald-500 dark:text-emerald-400 font-medium">`export_financier_${Date.now()}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}L'utilisation des flux continus HTTP et des curseurs a permis d'exporter plus de 100 000 lignes de transactions instantanément sans aucun pic de mémoire ni délai d'attente serveur.