moveek/excel-bundle
Moveek Excel Bundle simplifies Excel import and export in Laravel/PHP apps, offering a lightweight wrapper for reading spreadsheets and generating downloadable files. Configure sheets, mappings, and formatting, then integrate exports and imports into controllers or jobs with minimal code.
First, confirm what you are actually installing. Despite the laravel framing in the package description, this is a Symfony bundle (moveek/excel-bundle), not a Laravel package. There is no Illuminate\Support\ServiceProvider, no extra.laravel block, and therefore no package auto-discovery. composer require moveek/excel-bundle will work — Composer does not care about frameworks — but nothing will be registered in your Laravel container. That single fact determines your entire approach.
Read the package before you build on it. It is small (a thin DI wrapper around phpoffice/phpspreadsheet), so an hour of reading is enough to decide. In order:
composer.json — the phpoffice/phpspreadsheet constraint is the most important line in the repo. If it pins ^1.x while upstream is on 2.x/3.x, the wrapper will block you from security patches in a library that parses untrusted uploaded XML. That is a blocker, not an inconvenience.MoveekExcelBundle.php / DependencyInjection/*Extension.php — expect a config tree for temp directories, caching, and reader/writer factories.Resources/config/services.* — the actual service definitions. This is the only part with any real content, and it is typically 20–50 lines.Then make the call. For a Laravel app there are only two honest options:
composer require phpoffice/phpspreadsheet, and write the ~30 lines of wiring yourself in a service provider. You get the same capability with full control over the version constraint.app/Services/Excel/, and uninstall. MIT license makes this clean.First use case — a report export. Whatever the wiring, the day-one code is the same PhpSpreadsheet API:
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
$sheet = (new Spreadsheet())->getActiveSheet();
$sheet->fromArray(['Order', 'Customer', 'Total'], null, 'A1');
$sheet->fromArray($orders->toArray(), null, 'A2');
return response()->streamDownload(function () use ($sheet) {
(new Xlsx($sheet))->save('php://output');
}, 'orders-' . now()->format('Ymd') . '.xlsx');
Read vendor/moveek/excel-bundle/ source, not the README. There are zero stars, zero dependents, and no docs beyond a short description — the source is the only specification, and there is no issue history to mine when something breaks at 2 a.m.
Wrap it behind an interface from the start. This is the load-bearing decision and it costs almost nothing. Whether you use the bundle, raw PhpSpreadsheet, or maatwebsite/excel later, call sites should not know:
namespace App\Services\Excel;
interface SpreadsheetWriter
{
public function write(iterable $rows, array $headings, string $path): string;
}
interface SpreadsheetReader
{
public function rows(string $path): iterable;
}
Bind them in AppServiceProvider:
$this->app->bind(SpreadsheetWriter::class, PhpSpreadsheetWriter::class);
If you genuinely adopt the bundle's services, this is also where you instantiate them manually, bypassing Symfony's container — at which point the bundle's config tree is dead weight and you should have just used the library.
Export pattern — one class per report, not one controller. Reports accumulate (finance, reconciliation, admin exports). Keep the data query and the sheet formatting together and away from HTTP:
class OrderExport implements SpreadsheetWriter
{
public function write(iterable $rows, array $headings, string $path): string
{
$sheet = (new Spreadsheet())->getActiveSheet();
$sheet->fromArray($headings, null, 'A1');
$row = 2;
foreach ($rows as $order) {
$sheet->fromArray([$order->id, $order->customer, $order->total], null, "A{$row}");
$row++;
}
(new Xlsx($sheet))->save($path);
return $path;
}
}
Formatting (column widths, number formats, freeze panes) belongs in here, not at the call site. If you find yourself styling in a controller, the abstraction has leaked.
Import pattern — chunk, validate, report per row. A synchronous toArray() on a user upload is fine up to a few thousand rows and a liability beyond that. Use a read filter and explicit validation:
$reader = IOFactory::createReaderForFile($path);
$reader->setReadDataOnly(true); // skips styles, big memory win
$reader->setReadFilter(new ChunkReadFilter($startRow, $endRow));
$sheet = $reader->load($path)->getActiveSheet();
foreach ($sheet->toArray(null, true, true, true) as $i => $line) {
$result = $this->importRow($line); // returns ok | array of errors
if (is_array($result)) {
$failures[$startRow + $i] = $result; // row number in the file, not in the array
}
}
Return the failures as a downloadable error sheet with the original row preserved — users will otherwise re-upload the same file and hit the same wall.
Large imports go to a queue, always. Write the upload to a storage disk, dispatch a job, and stream progress back over a broadcast channel or poll. PhpSpreadsheet loads the whole workbook into memory; doing that on a web request with a 30-second max_execution_time is how you get a support ticket. In the job, set an explicit ceiling so it fails loudly rather than OOM-killing the worker:
public function handle(SpreadsheetReader $reader): void
{
ini_set('memory_limit', '512M');
set_time_limit(300);
// ...
}
If you are actually on Symfony (the bundle's intended home), register it in config/bundles.php, configure the temp directory in config/packages/moveek_excel.yaml, and inject the reader/writer services by interface. The config tree is the entire value proposition — check that its temp path points at a persistent, swept volume.
Memory is the number one failure mode, and the wrapper does not help. PhpSpreadsheet keeps the whole workbook in memory. Mitigations, in order of impact: setReadDataOnly(true) on reads, a IReadFilter to process N rows at a time, disconnectWorksheets() between sheets, and raising memory_limit only as a last resort. Never trust a large upload to a synchronous request path.
Temp files need a lifecycle owner. PhpSpreadsheet writes temp files during read and write. In containers with ephemeral filesystems, or worse, shared volumes across replicas, you get either silent disk creep or concurrent-write collisions on the same path. Point the temp dir at a per-process path or a disk with a sweeper, and test two exports running simultaneously.
Version constraints are the real risk. Run this before committing to any design:
composer why-not phpoffice/phpspreadsheet 3.0.0
If the bundle's constraint excludes the version your security baseline requires, you have your answer. A wrapper that blocks upstream patches guarantees an emergency fork under incident pressure — and PhpSpreadsheet parses untrusted XML and compressed uploads, a historically CVE-prone surface.
No Laravel auto-discovery. If you install it expecting php artisan vendor:publish, nothing will happen. Any service definitions must be registered by hand in a service provider or you will get Target [X] is not instantiable at the worst possible moment.
XLSX only. The description promises .xlsx; do not assume CSV, .xls, or ODS behaviour. Use IOFactory::identify() to validate the extension against the real content type, and reject uploads whose declared MIME disagrees with what the file actually is.
Formula injection is on you. Any cell beginning with =, +, -, or @ from user-controlled data becomes a live formula when the file opens in Excel. Prefix with a single quote or sanitize before writing — the wrapper will not do this for you:
$value = preg_match('/^[=+\-@\t\r]/', (string) $value) ? "'" . $value : $value;
Styling, charts, formula evaluation, conditional formatting are PhpSpreadsheet features routed straight through. If the bundle's API covers only read/write, you will drop to the underlying objects anyway. Prefer owning that integration explicitly so you are not debugging two layers.
Zero support channel. With no stars, no dependents, and no issue history, there is nobody to ask. Budget the fork: if you adopt, name an internal owner, vendor the code into your repo or pin the commit, and write an ADR saying you own the maintenance. Extension points and bug fixes are both "edit the source."
When in doubt, swap in maatwebsite/excel. For a Laravel app it solves the same problem with queue-aware imports, chunked reads, model binding, and export responses — and it has a community that will find the bugs before you do. Do not add a Symfony bundle to a Laravel application to avoid writing a service provider.
How can I help you explore Laravel packages today?