Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Wn Excel Plugin Laravel Package

winter/wn-excel-plugin

Winter CMS plugin for importing and exporting Excel spreadsheet files, letting you move data between your models and .xlsx/.csv files through simple backend features and configurable import/export actions.

View on GitHub
Deep Wiki
Context7

Getting Started

The plugin is a thin Winter CMS integration around spreadsheet reading/writing (PhpSpreadsheet under the hood). Its day-to-day value is twofold: a facade/service so you don't bootstrap PhpSpreadsheet by hand, and backend list-controller behaviours that give you Import/Export buttons without writing UI code.

Minimal setup:

composer require winter/wn-excel-plugin
php artisan winter:up        # runs migrations if the plugin ships any; also refreshes plugin registry
php artisan plugin:list      # confirm Winter.Excel (or similar handle) is enabled

If you're on a locked-down deploy, add the package to composer.json explicitly and run composer install --no-dev on the server; Winter will pick the plugin up from plugins/ or vendor/ on the next winter:up.

Where to look first, in this order:

  1. README.md — the canonical examples for the facade and behaviour config. Trust this over any blog post; the API surface is small.
  2. Plugin.php — registers the facade alias, behaviours, and any console commands. This tells you the real namespace (don't assume Winter\Excel\...; confirm from composer.json autoload and this file).
  3. behaviors/ — the list-controller import/export behaviours, including their config_*.yaml-style option keys.
  4. classes/ — the reader/writer wrapper. This is where you'd look when a cell comes back null or a date comes back as 45123.

First use case: export a filtered list of records to XLSX from a backend controller, then import that same file back. Do the export first — it's read-only, gives you a known-good file shape, and immediately validates that the plugin is correctly installed and autoloading.

// illustrative — pin the real namespace from the README before shipping
use Winter\Excel\Facades\Excel;

public function exportOrders()
{
    $rows = Order::with('customer')->get()->map(fn ($o) => [
        'Reference' => $o->reference,
        'Customer'  => $o->customer->name,
        'Total'     => $o->total,
        'Placed'    => $o->placed_at->format('Y-m-d H:i'),
    ])->all();

    return Excel::export($rows, 'orders-' . now()->format('Ymd-His') . '.xlsx');
}

Implementation Patterns

Wrap the plugin behind your own service. With a low star count and no dependents, treat this as a convenience layer, not a permanent foundation. Define App\Services\Spreadsheet\Reader / Writer interfaces, implement them with the plugin today, and swap to raw PhpSpreadsheet or OpenSpout later without touching every call site. The plugin's own API is small enough that this costs you an hour and buys you an exit.

Backend list exports via behaviour config. The typical pattern is registering the behaviour on the controller, then describing columns in the behaviour config (or in listExtendColumns). Prefer letting the behaviour reuse your list column definitions so exported headers and grid headers never drift apart:

public $implement = [
    'Backend\Behaviors\ListController',
    'Winter\Excel\Behaviors\ExportBehavior', // verify class name
];

public $exportConfig = 'export_config.yaml';

Keep the YAML config in the controller's config/ directory so per-controller column mapping stays declarative and reviewable, rather than an array literal buried in PHP.

Import as a queued, chunked job. Spreadsheet uploads are user-supplied and unbounded in size. Never parse in the request cycle:

// Controller: validate, store, dispatch, return immediately
$file = System\Models\File::fromPost(input('import_file'));
ImportOrders::dispatch($file->getLocalPath(), $file->id);

// Job: chunk, validate per row, transact per chunk
foreach ($reader->rows($path) as $chunk) {
    DB::transaction(function () use ($chunk) {
        foreach ($chunk as $line => $row) {
            $data = $this->mapRow($row);
            $validator = Validator::make($data, $this->rules());
            if ($validator->fails()) {
                $this->collectError($line, $validator->errors());
                continue;
            }
            Order::updateOrCreate(['reference' => $data['reference']], $data);
        }
    });
}

Store per-row errors and surface them as a downloadable error file; users will re-upload the same 5,000 rows otherwise.

Keep mapping declarative. A protected array $columnMap = ['A' => 'reference', 'B' => 'total', ...] (or header-name → attribute) in one class is far easier to maintain than positional $row[0], $row[1] scattered through the job. Pair it with cast closures for dates, booleans ('yes'/'1'/'Y'), and currency strings.

Use model mutators, not job logic, for normalisation. Trim whitespace, uppercase references, coerce empty strings to null in setAttribute mutators so imports, backend forms, and API writes all behave identically.

Round-trip test fixture. Commit one small .xlsx fixture (3 rows, one invalid) and one test that imports it and asserts the error rows. This catches header changes and PhpSpreadsheet upgrades before users do.

Gotchas and Tips

  • getHighestRow() lies. It includes formatted-but-empty rows, so a file that looks 10 rows long reports 5,000. Use getHighestDataRow() (or skip rows where every cell is null) or you'll import thousands of blank records.
  • Dates are floats. Excel stores dates as serial numbers; a naive read gives 45123. Either map date columns explicitly with Date::excelToDateTimeObject() or format the column in the sheet and use the formatted value. Watch timezone drift — PhpSpreadsheet assumes UTC by default.
  • Formulas: getValue() vs getCalculatedValue(). getCalculatedValue() needs the calc engine and is slow on large files; if the file was generated by another system the cached value is usually what you want. Reading with setReadDataOnly(true) is the memory win but drops styles and can affect date detection.
  • Memory blows up silently. A 20 MB XLSX can exceed 512 MB unpacked. Set memory_limit and set_time_limit(0) in the job, use read-only mode, and prefer chunked reads. Fatal errors here surface as opaque queue failures — log memory_get_peak_usage() alongside import completion.
  • CSV isn't XLSX. CSV reads ignore types entirely: 007 becomes 7, 1,234.50 becomes 1, and Excel's UTF-8 BOM leaves \u{FEFF} on your first header. Strip BOM explicitly and force setInputEncoding('UTF-8').
  • Locale decimal separators. If your users are in de/fr locales, 1.234,56 arrives as a string. Normalise currency columns with a locale-aware parser, not (float).
  • Behaviour permissions. The Import/Export buttons inherit the controller's access checks in most setups — verify with a non-admin backend user, because a permissive export button on a list controller is a data-exfiltration path.
  • Temp files leak. Uploaded files moved to storage/app for queued imports are never cleaned up automatically. Delete in a finally block or a scheduled prune.
  • Pin the version. Small user base means regressions get less scrutiny. Use an exact constraint ("winter/wn-excel-plugin": "1.x.y") and read the diff before bumping, or fork and vendor it — MIT licensing makes that painless.
  • Extension points are limited. If you need per-cell styling, multi-sheet output, or streaming writes for >100k rows, drop to PhpSpreadsheet/OpenSpout directly rather than fighting the wrapper. Use the plugin for the 80% case (backend import/export buttons, simple model exports) and raw libraries for the heavy 20%.
  • Debugging tip: when a cell comes back wrong, dump $sheet->getCell('B4')->getValue() and getDataType() from a tinker session before touching your mapping code — nine times out of ten the file, not your import, is the problem.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle