Skip to content

Exportar a CSV y Excel ​

"¿Me lo puedes pasar a Excel?" es de las peticiones más comunes de un cliente. Aquí tienes dos formas: CSV (sin librerías, rapidísimo, ideal para miles de filas) y XLSX real con formato usando PhpSpreadsheet.

Opción 1: CSV que Excel abre bien ​

El truco para que Excel muestre bien los acentos es escribir el BOM de UTF-8 al inicio del archivo. Y como en México/LatAm Excel suele usar coma decimal en algunas configuraciones, puedes cambiar el separador a ; si lo necesitas.

php
<?php
require __DIR__ . '/includes/roles.php';
requiere_rol('admin');

$desde = $_GET['desde'] ?? date('Y-m-01');
$hasta = $_GET['hasta'] ?? date('Y-m-d');

$stmt = db()->prepare(
    'SELECT p.id, p.creado_en, u.nombre AS cliente, u.email, p.total, p.estado
     FROM pedidos p JOIN usuarios u ON u.id = p.usuario_id
     WHERE DATE(p.creado_en) BETWEEN ? AND ?
     ORDER BY p.creado_en'
);
$stmt->execute([$desde, $hasta]);

$nombreArchivo = "pedidos_{$desde}_{$hasta}.csv";
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $nombreArchivo . '"');
header('Cache-Control: no-store');

$salida = fopen('php://output', 'w');
fwrite($salida, "\xEF\xBB\xBF"); // BOM: Excel detecta UTF-8

$sep = ','; // cambia a ';' si tu Excel lo pide
fputcsv($salida, ['Folio', 'Fecha', 'Cliente', 'Correo', 'Total', 'Estado'], $sep);

// Fila por fila: no carga todo en memoria (sirve para 100 000+ registros)
while ($fila = $stmt->fetch()) {
    fputcsv($salida, [
        $fila['id'],
        date('d/m/Y H:i', strtotime($fila['creado_en'])),
        limpiar_csv($fila['cliente']),
        limpiar_csv($fila['email']),
        number_format((float) $fila['total'], 2, '.', ''),
        $fila['estado'],
    ], $sep);
}
fclose($salida);
exit;

/**
 * Evita "inyección de fórmulas": si un valor empieza con = + - @
 * Excel podría ejecutarlo como fórmula al abrir el archivo.
 */
function limpiar_csv(?string $valor): string
{
    $valor = (string) $valor;
    return preg_match('/^[=+\-@\t\r]/', $valor) ? "'" . $valor : $valor;
}

Botón para descargar:

html
<a href="exportar-csv.php?desde=2026-10-01&hasta=2026-10-31" class="btn">Descargar CSV</a>

Opción 2: Excel (.xlsx) con formato ​

bash
composer require phpoffice/phpspreadsheet
php
<?php
require __DIR__ . '/vendor/autoload.php';
require __DIR__ . '/includes/roles.php';
requiere_rol('admin');

use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Shared\Date;

$filas = db()->query(
    'SELECT p.id, p.creado_en, u.nombre AS cliente, p.total, p.estado
     FROM pedidos p JOIN usuarios u ON u.id = p.usuario_id
     ORDER BY p.creado_en DESC LIMIT 5000'
)->fetchAll();

$libro = new Spreadsheet();
$hoja = $libro->getActiveSheet();
$hoja->setTitle('Pedidos');

// Título
$hoja->setCellValue('A1', 'Reporte de pedidos — ' . date('d/m/Y'));
$hoja->mergeCells('A1:E1');
$hoja->getStyle('A1')->getFont()->setBold(true)->setSize(14);

// Encabezados
$encabezados = ['Folio', 'Fecha', 'Cliente', 'Total', 'Estado'];
$hoja->fromArray($encabezados, null, 'A3');
$estiloEnc = $hoja->getStyle('A3:E3');
$estiloEnc->getFont()->setBold(true)->getColor()->setRGB('FFFFFF');
$estiloEnc->getFill()->setFillType(Fill::FILL_SOLID)->getStartColor()->setRGB('4F46E5');

// Datos
$f = 4;
foreach ($filas as $p) {
    $hoja->setCellValue("A$f", (int) $p['id']);
    $hoja->setCellValue("B$f", Date::PHPToExcel(new DateTime($p['creado_en']))); // fecha real de Excel
    $hoja->setCellValue("C$f", $p['cliente']);
    $hoja->setCellValue("D$f", (float) $p['total']);
    $hoja->setCellValue("E$f", ucfirst($p['estado']));
    $f++;
}
$ultima = $f - 1;

// Formatos
$hoja->getStyle("B4:B$ultima")->getNumberFormat()->setFormatCode('dd/mm/yyyy hh:mm');
$hoja->getStyle("D4:D$ultima")->getNumberFormat()->setFormatCode('"$"#,##0.00');

// Fila de total con fórmula
$hoja->setCellValue("C$f", 'TOTAL');
$hoja->setCellValue("D$f", "=SUM(D4:D$ultima)");
$hoja->getStyle("C$f:D$f")->getFont()->setBold(true);
$hoja->getStyle("D$f")->getNumberFormat()->setFormatCode('"$"#,##0.00');

// Ancho automático, filtros y encabezado fijo
foreach (range('A', 'E') as $col) {
    $hoja->getColumnDimension($col)->setAutoSize(true);
}
$hoja->setAutoFilter("A3:E$ultima");
$hoja->freezePane('A4');

// Descargar
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment; filename="pedidos_' . date('Y-m-d') . '.xlsx"');
header('Cache-Control: max-age=0');

(new Xlsx($libro))->save('php://output');
exit;

Importar un CSV (bonus) ​

php
<?php
// Importa productos desde un CSV con columnas: nombre,precio,stock
$archivo = fopen($_FILES['csv']['tmp_name'], 'r');
$encabezado = fgetcsv($archivo); // salta la primera fila
$stmt = db()->prepare('INSERT INTO productos (nombre, precio, stock) VALUES (?, ?, ?)');

db()->beginTransaction();
$n = 0;
while (($fila = fgetcsv($archivo)) !== false) {
    [$nombre, $precio, $stock] = array_map('trim', $fila + ['', '0', '0']);
    if ($nombre === '' || !is_numeric($precio)) continue; // salta filas inválidas
    $stmt->execute([$nombre, (float) $precio, (int) $stock]);
    $n++;
}
db()->commit();
fclose($archivo);
echo "Se importaron $n productos.";

¿Cuál elegir? ​

CSVXLSX (PhpSpreadsheet)
LibreríasNingunaComposer
Velocidad / memoriaExcelenteConsume más memoria
Formato, colores, fórmulasNoSí
Varias hojasNoSí
Recomendado paraExportaciones grandes, integracionesReportes para el cliente

Error común

Hacer echo de algo (o dejar espacios antes de la etiqueta de apertura de PHP) antes de las cabeceras. El archivo descargado saldrá corrupto. Asegúrate de que nada se imprima antes de header().

Academia Web — aprende, copia y construye.