gulaandrij/google-sheets-bundle
A Symfony bundle for integrating the Google Sheets API into your application. Configure credentials and use simple services to read, write, and sync spreadsheet data, making Google Sheets a convenient lightweight data source or backend for your project.
This is a Symfony bundle, not a Laravel package — despite sitting on top of revolution/laravel-google-sheets. If you are actually in a Laravel app, skip the bundle and use the underlying package directly with its facade; you gain nothing from the Symfony DI wrapper. The rest of this assumes Symfony (or a Laravel app that has pulled in Symfony components).
Minimal path:
composer require gulaandrij/google-sheets-bundle and check composer.json first — a zero-star bundle frequently lags behind current Symfony majors. Confirm the symfony/* constraints match your app before you commit. As of v1.3.0 also check whether symfony/event-dispatcher is installed: it's optional, and without it the new write events are silently never dispatched.config/bundles.php (return [ ... Gulaandrij\GoogleSheetsBundle\...::class => ['all' => true] ];). The class name is in the repo root; read src/ — there is no documentation you can trust.src/DependencyInjection/Configuration.php before writing any YAML. That file is the authoritative list of config keys; anything you read elsewhere (including this) is a guess. Typical shape is a credentials path plus an application name.[email protected] address as Editor. This step is skipped by roughly everyone and produces the 403 you will spend an hour on.bin/console cache:clear.First use case: write a throwaway console command that injects SheetsServiceInterface $sheets, reads one range, and dump()s it. As of v1.3.0 the interface is the contract you should read first — it captures read/write/discovery/metadata in one place and is a far better map of the API surface than the concrete class, which adds escape hatches (client(), driveService()) you don't want yet. Do not start with a controller — the console gives you a tight loop and clean stack traces.
Type-hint SheetsServiceInterface, not the concrete class. As of v1.3.0 the bundle registers SheetsServiceInterface $<name> autowire aliases next to the existing SheetsService $<name> ones, and SheetsRegistry::service() returns the interface. This replaces the old "always wrap the vendor service behind your own interface" advice as the minimum — you still want a thin domain gateway (App\Sheets\SheetGateway) if your consumers only need three or four methods, but the interface means you get test doubles and swap-ability without writing that layer just for safety. Implement against the interface; keep the concrete class out of your constructors unless you genuinely need the escape hatches.
Use SheetsWriteEvent for cache invalidation and audit instead of hand-rolling it. Subscribe with #[AsEventListener(event: SheetsWriteEvent::class)] and you get operation, spreadsheetId, sheetName, range, and rowCount after every successful append, update, clear, addSheet, and deleteSheet. The obvious wiring: a listener that busts the read cache keyed on spreadsheetId + sheetName — that closes the "I wrote, now my cached read is stale" hole without threading invalidation calls through your services. Second use: an audit trail listener writing one log line per mutation.
Normalize the read path once. Sheets returns rows as arrays of strings, and trailing empty cells are trimmed — row lengths vary. Every real workload ends up with a rowsToAssociative() helper: pull row 0 as headers, array_pad each row to header length, array_combine. Write it on day one; you will use it in every consumer.
Reads behind the cache, writes behind a queue. A sheet read is an HTTP round-trip with a quota attached. Wrap reads in cache.app with a 60–300s TTL (tag it if you use Redis) and expose an explicit refresh() for the "I just edited the sheet" case — the write event listener covers mutations that go through the bundle, but not a human editing the sheet in the browser. For writes, especially append-style event logging, push to Messenger and let a worker batch them — do not open a socket to Google inside a web request.
Import pipelines: console command + Messenger handler. Read in chunks (a few hundred rows at a time), dispatch a message per chunk, let workers do the transform. A single get() on a 50k-row sheet will time out or blow memory. readAssocIterable() is the right tool here, and as of v1.3.0 a break in the middle of the loop no longer loses the profiler trace, so you can trust the dev toolbar numbers when you bail out early on a validation error.
Scheduling: Symfony Scheduler (or cron + bin/console) for the sheet→DB sync direction, with the last-synced timestamp stored somewhere. Sheets has no change feed you can cheaply poll; full re-read + diff on a normalised key is the pragmatic answer.
Test harness: keep a dedicated "scratch" spreadsheet with a fixture sheet, and a --dry-run flag on every command that writes. InMemorySheetsService is the fake to use in unit tests — and since v1.3.0 it enforces the same homogeneous-row shape check as production (mixed positional/assoc rows, or assoc rows with divergent keys, throw MixedRowShapeException). That's a feature: your test fixtures now fail loudly at the same point production would, instead of passing green and blowing up against the real API.
SheetsRegistry::service() now returns the interface. If you were calling client(), driveService(), or anything else concrete on the result of a registry lookup, that's an immediate type error on upgrade. Fetch the concrete service via the SheetsService $<name> alias instead. This is the one genuinely breaking change in v1.3.0 and it's easy to miss because it's listed under "Added".client() and driveService() are not on SheetsServiceInterface. If you need raw Google clients, you must inject the concrete class for that one consumer — don't pollute your domain gateway's signature with them, and don't expect a mock of the interface to satisfy that code path.?EventDispatcherInterface constructor argument is wired with service(EventDispatcherInterface::class)->nullOnInvalid() so apps without symfony/event-dispatcher still boot — which means if the package is missing, your SheetsWriteEvent subscribers simply never fire, with no error. If cache invalidation depends on those listeners, this is a stale-data bug that only shows in the environment where the dispatcher isn't installed. Assert the dependency is present in composer.json.refresh() remain the only defence against sheet-side edits.src/ end to end (it's small), and be prepared to fork. There is no support channel and no issue history to learn from. v1.3.0 is a solid release that fixed several real bugs, but it's still one person.$1,234.00, 12.5%, and date serials all come back as strings unless you set the value render option to unformatted/UNFORMATTED_VALUE. Casting with (float) on "$1,234.00" yields 0. Decide the render option per read, explicitly.'My Sheet'!A1:D10. Matching by sheet name is case- and whitespace-sensitive; prefer matching by sheetId where you can. findSheetNameById is now traced, so when a lookup fails you can at least see the call in the profiler instead of guessing.Google\Service\Exception specifically and inspect getErrors() — the message often names the exact range that failed.cache:clear silently keeps the old credentials path; in dev this looks like "it works on my machine, not in the container."readAssocIterable with an early break recorded zero entries, findSheetNameById recorded nothing at all, and captureOrigin frequently returned null for the call site in a real kernel/middleware stack. Don't debug request amplification on an old profiler — upgrade first, then measure.SheetsServiceInterface rather than editing vendor code — the bundle already ships three decorating implementations (Traceable, Cached, InMemory), so the pattern is proven and the interface won't move under you. Override the service definition in config/services.yaml if you need a different HTTP client or scopes. Do not subclass SheetsService — the constructor signature will move (it just gained an optional fourth argument).SheetsWriteEvent listener log the same fields.How can I help you explore Laravel packages today?