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

Slick Spreadsheets Laravel Package

itul/slick-spreadsheets

itul/slick-spreadsheets gives Laravel and PHP apps a clean, fluent way to read, write, import, and export spreadsheet files like Excel and CSV, wrapping the heavy lifting in a simple API for quick data imports, exports, and reports without the boilerplate.

View on GitHub
Deep Wiki
Context7

Getting Started

This package is distributed as a private Bitbucket repository, not via Packagist, so the first step is wiring up access rather than composer require.

1. Authenticate and require it

Add a VCS repository entry and pin a commit (there are no meaningful tags to rely on):

"repositories": [
  {
    "type": "vcs",
    "url": "[email protected]:bmooreitul/slick-spreadsheets.git"
  }
],
"require": {
  "itul/slick-spreadsheets": "dev-master#<commit-sha>"
}

SSH keys or a Bitbucket app password must be available to both your machine and CI. Expect composer install to differ between environments if only one has credentials — mirror the auth into your build pipeline day one.

2. Read these three things before writing a line of code

  • vendor/itul/slick-spreadsheets/composer.json — tells you the PHP version, the illuminate/* constraints, and (almost certainly) whether it wraps phpoffice/phpspreadsheet or league/csv.
  • The service provider in src/ — reveals the container bindings, any facade alias, and whether a config file is publishable (php artisan vendor:publish --provider=...).
  • src/ for the public surface: look for read, parse, rows, convert, toArray, headings, toCsv. Those method names are your API.

Since the repository is unindexed and undocumented, composer show itul/slick-spreadsheets plus a grep -rn "public function" vendor/itul/slick-spreadsheets/src is faster than guessing.

3. First use case: import a supplier price list

The realistic day-one job is "customer emails us an XLSX, we need those rows in a table." Probe the API in Tinker against a real file:

$path = Storage::disk('imports')->path('supplier-2024-05.xlsx');

// Adjust to the real class/method the provider exposes:
$rows = app(SlickSpreadsheets::class)->read($path);
dump($rows[0] ?? null);   // inspect shape: array of arrays? keyed by header?

Once you know whether the return type is an array, a generator, or a RowCollection, write the importer (see the next section) and never call the package from a controller again.

Implementation Patterns

Adapter-first, always. The package is private, single-maintainer, and has zero dependents. Treat it as a swappable detail behind your own contract, so replacing it with maatwebsite/excel or raw PhpSpreadsheet later is a one-file change.

namespace App\Support\Spreadsheets;

interface SpreadsheetReader
{
    /** @return iterable<array<string, string|null>> */
    public function rows(string $absolutePath): iterable;
}
namespace App\Support\Spreadsheets;

use SlickSpreadsheets; // whatever the real import is

class SlickSpreadsheetReader implements SpreadsheetReader
{
    public function rows(string $absolutePath): iterable
    {
        $sheet = app(SlickSpreadsheets::class)->read($absolutePath);

        foreach ($sheet as $index => $row) {
            if ($index === 0) { continue; } // header
            yield $this->normalise($row);
        }
    }

    private function normalise(array $row): array
    {
        return array_map(
            fn ($v) => is_string($v) ? trim($v) : $v,
            $row
        );
    }
}

Bind it once in a service provider:

$this->app->bind(SpreadsheetReader::class, SlickSpreadsheetReader::class);

Convert-then-read for mixed formats. If the package exposes a conversion path (xls/ods → csv or → xlsx), normalise everything to one format on upload and only ever parse that format downstream. You get one code path to test instead of four, and the quirky formats are handled at the boundary.

Queue the parse, not the upload. Files over a few thousand rows will blow past the default 30-second job timeout and PhpSpreadsheet's memory profile is not linear-free.

class ImportPriceList implements ShouldQueue
{
    public int $timeout = 600;
    public int $tries = 1;          // parsing is not idempotent-safe on retry storms

    public function __construct(public int $importId) {}

    public function handle(SpreadsheetReader $reader): void
    {
        $import = Import::findOrFail($this->importId);
        $path = Storage::disk('imports')->path($import->stored_path);

        $buffer = [];
        foreach ($reader->rows($path) as $row) {
            $buffer[] = $this->mapRow($row, $import->mapping);

            if (count($buffer) === 500) {
                PriceRow::insert($buffer);   // chunked insert, no N+1
                $buffer = [];
            }
        }
        PriceRow::insert($buffer);
    }
}

Keep column mapping as data, not code. Supplier A calls it SKU, supplier B calls it Item Code. Store a mapping JSON column on the import record and resolve it in mapRow():

private function mapRow(array $row, array $mapping): array
{
    return [
        'sku'   => $row[$mapping['sku']] ?? null,
        'price' => $this->toDecimal($row[$mapping['price']] ?? null),
    ];
}

Validate the upload with the format, not the extension. 'file' => 'required|file|mimes:xlsx,xls,csv|max:20480'. Note mimes:xlsx relies on MIME sniffing and a xlsx that was really a .csv renamed will still pass — verify the header row parses before accepting the import.

Testing. Because there is no upstream test suite you can lean on, build your own fixture corpus early: a normal file, a file with a BOM, a file with a semicolon delimiter, a file with a trailing blank row, and one with a 16-digit order number. Assert on your SpreadsheetReader output, not on the package's internals.

Gotchas and Tips

Dependency collisions are the most likely first failure. If the app already requires phpoffice/phpspreadsheet (directly or via maatwebsite/excel), Composer may resolve to an incompatible constraint. Run composer why phpoffice/phpspreadsheet before installing and lock the package commit to match.

Memory. Anything that returns "all rows at once" loads the whole sheet. For big imports prefer a generator/streaming method if one exists; otherwise raise memory_limit for the queue worker (php artisan queue:work --memory=512 --timeout=600) and set read-data-only if the underlying library exposes it.

Dates arrive as serial integers. Excel stores 2024-05-14 as 45426 unless a date format is applied. Convert explicitly (\PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($v)) rather than trusting the reader to hand you a Carbon.

Formula cells give you the formula, not the value. If a supplier ships =B2*C2, you may receive the string "=B2*C2" or null depending on whether the file was saved with cached values — getCalculatedValue() in PhpSpreadsheet is what actually evaluates it. A CSV export from the same supplier will have values. This is a common "works for me, fails in prod" split.

Leading zeros and long numbers. SKUs like 00123 and order IDs of 16+ digits get coerced by the reader. Cast to string at the boundary and store as varchar, never int or float. Phone numbers starting with + are the same bug in disguise.

CSV encoding. Files exported from desktop Excel are frequently Windows-1252 or UTF-8 with BOM. The BOM shows up as \xEF\xBB\xBF glued to your first header name, so $mapping['sku'] silently misses. Strip it and normalise encoding:

$contents = mb_convert_encoding($contents, 'UTF-8', 'UTF-8, Windows-1252');
$contents = ltrim($contents, "\xEF\xBB\xBF");

Also watch European locale exports that use ; as delimiter and 1,50 as a decimal — 1,50 parses to 1.0 if you float it naively.

Merged cells only return a value in the top-left cell. Category headers spanning five columns will appear once and then as null. Forward-fill them in your mapping step if the sheet is laid out that way.

Filesystem paths vs storage URLs. These readers want an absolute path. Storage::path() works for local disks; S3 disks will need Storage::readStream() or a temporary download. Storage::url() will not work.

Temp directory in containers. The underlying spreadsheet libraries write to sys_get_temp_dir(). Read-only container filesystems and small /tmp tmpfs mounts cause confusing failures — set TMPDIR or the library's temp-dir option explicitly.

Pin the commit. With no tags and one maintainer, dev-master means a composer update can silently change parsing behaviour. Use dev-master#<sha> and review diffs manually when bumping.

Debug by dumping raw rows first. When a mapping looks wrong, log the package's raw output (headers included) before your normalisation layer. Nine times out of ten the bug is a header-name mismatch or a merged cell, not the package.

Extension points to look for: a publishable config file (vendor:publish), a container binding for the reader/converter class you can override in your own provider, and any Macroable trait on the facade. If the binding is bind() rather than singleton(), you can swap in a subclass with stricter encoding handling without forking. If there is no such seam, keep everything behind your own SpreadsheetReader interface — that is your extension point, and it is the one thing you fully control.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource