yectep/phpspreadsheet-bundle
A Symfony bundle that integrates PhpSpreadsheet into your app, providing service-container access for reading, writing, and exporting Excel/CSV spreadsheets with minimal setup and PhpSpreadsheet’s full API.
This is a Symfony bundle, not a Laravel package. For a Laravel codebase, its day-to-day leverage is indirect: use it as a reference for service wiring, temp-directory configuration, and reader/writer setup, but depend on phpoffice/phpspreadsheet directly. With 56 stars and 0 dependents, there is little Laravel-side validation, and installing the bundle pulls in Symfony bundle machinery you will not use.
Minimal Laravel path:
composer require phpoffice/phpspreadsheet
Spreadsheet object itself; it is stateful.// app/Providers/SpreadsheetServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Settings;
class SpreadsheetServiceProvider extends ServiceProvider
{
public function register(): void
{
$temp = storage_path('app/private/spreadsheet-tmp');
if (! is_dir($temp)) {
mkdir($temp, 0775, true);
}
Settings::setTempDir($temp);
}
}
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
Route::get('/exports/orders', function () {
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->fromArray(['ID', 'Customer', 'Total'], null, 'A1');
$sheet->fromArray(
Order::query()->limit(1000)->get(['id', 'customer', 'total'])->toArray(),
null,
'A2'
);
$path = tempnam(sys_get_temp_dir(), 'orders_') . '.xlsx';
(new Xlsx($spreadsheet))->save($path);
$spreadsheet->disconnectWorksheets();
return response()->download($path)->deleteFileAfterSend(true);
});
Where to look first in the bundle repository: the README for service IDs, the dependency-injection extension and service configuration for temp/cache setup, and the tests for intended usage. Translate only those settings into Laravel configuration; do not try to register the Symfony bundle in a Laravel app.
If you also maintain a Symfony application, install eightynine/phpspreadsheet-bundle, register Eightynine\PhpSpreadsheetBundle\PhpSpreadsheetBundle in config/bundles.php, and inject the exposed reader/writer services. For Laravel-only work, stop at PhpSpreadsheet.
Exports: wrap PhpSpreadsheet in an application service. Keep spreadsheet construction out of controllers. Inject a dedicated OrderExporter or SpreadsheetExporter class so you can test it, swap temp paths, and later move it into a queue without rewriting routes.
final class OrderExporter
{
public function export(Collection $orders): string
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->fromArray(['ID', 'Customer', 'Total'], null, 'A1');
$sheet->fromArray($orders->toArray(), null, 'A2');
$path = storage_path('app/private/exports/orders-' . now()->timestamp . '.xlsx');
(new Xlsx($spreadsheet))->save($path);
$spreadsheet->disconnectWorksheets();
return $path;
}
}
Large exports belong in a queued job. Write to a Laravel disk, then notify the user when the file is ready. Avoid streaming directly from a queue worker; there is no HTTP response there.
final class ExportOrders implements ShouldQueue
{
public function handle(OrderExporter $exporter): void
{
$path = $exporter->export(Order::query()->cursor());
Storage::disk('local')->put('exports/orders.xlsx', file_get_contents($path));
unlink($path);
}
}
Use cursor() or chunked queries rather than get() for large datasets, and call $spreadsheet->disconnectWorksheets() when finished.
Imports: use IOFactory with restrictive reader settings. The bundle’s value here is mostly the same as the underlying library: configured readers and writers. In Laravel, create the reader per import and lock it down.
$reader = IOFactory::createReaderForFile($path);
$reader->setReadDataOnly(true);
$reader->setLoadSheetsOnly(['Orders']);
$spreadsheet = $reader->load($path);
$rows = $spreadsheet->getActiveSheet()->toArray(null, true, true, true);
$spreadsheet->disconnectWorksheets();
For very large imports, implement PhpOffice\PhpSpreadsheet\Reader\IReadFilter and read in row ranges. Do not load a 100k-row workbook just to process 500 rows.
Use Laravel configuration, not Symfony bundle configuration. The bundle may expose temp-dir or cache settings through Symfony configuration. In Laravel, mirror those settings manually in config/spreadsheet.php, then apply them in your provider.
// config/spreadsheet.php
return [
'temp_dir' => storage_path('app/private/spreadsheet-tmp'),
'read_data_only' => true,
];
Testing pattern. Test exporters and importers as plain services. Use Storage::fake('local'), assert the generated file exists, and reopen it with PhpSpreadsheet to assert cell values. Do not assert binary file contents; assert structure and selected cells.
phpoffice/phpspreadsheet.Spreadsheet as a singleton. A workbook accumulates sheets, styles, and cell data. A singleton will leak memory between requests and jobs. Bind factories or construct per operation.// Good: new workbook per export
$spreadsheet = new Spreadsheet();
// Bad: shared workbook across requests
$this->app->singleton(Spreadsheet::class);
Settings::setTempDir() to a Laravel storage path, not the system temp dir, especially in containerized deployments. PhpSpreadsheet writes temporary files during reads and writes.setReadDataOnly(true) changes date behavior. Dates may arrive as Excel serial numbers rather than formatted dates. Convert explicitly with PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject() or turn off read-data-only when you need formatting.response()->download($path)->deleteFileAfterSend(true) works for local files. For files on S3 or another disk, stream the response and delete the object separately.=, +, -, or @ can be interpreted as formulas in Excel. Prefix them with a single quote or sanitize the value before writing.$value = preg_match('/^[=+\-@]/', (string) $value) ? "'" . $value : $value;
setReadDataOnly(true), setLoadSheetsOnly(), read filters, and disconnectWorksheets(). For very large exports, consider CSV instead of XLSX, or generate files in batches.How can I help you explore Laravel packages today?