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

Google Sheets Bundle Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. 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.
  2. Register the bundle in 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.
  3. Open 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.
  4. Create the Google side: new GCP project → enable Google Sheets API and Google Drive API → service account → JSON key. Then share the target spreadsheet with the service account's [email protected] address as Editor. This step is skipped by roughly everyone and produces the 403 you will spend an hour on.
  5. Point the bundle config at the JSON file (mount it as a file; don't stuff the JSON into an env var — newlines in env vars are misery), then 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.

Implementation Patterns

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.

Gotchas and Tips

  • 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".
  • Escape hatches are concrete-only, by design. 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.
  • The event dispatcher is optional and fails silently. The new ?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.
  • Read events do not exist, and that's deliberate. They'd fire on every row of a streaming read. So: writes invalidate the cache via listener, but a human editing the spreadsheet does not. Your TTL and explicit refresh() remain the only defence against sheet-side edits.
  • Zero stars, zero dependents, one maintainer. Treat this as unaudited code you are effectively adopting. Pin an exact version, read 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.
  • Symfony version lock. Bundle config and DI extension signatures break across majors. Before any Symfony upgrade, check whether this bundle's constraints allow it; it may be the thing pinning you to an old version.
  • Service accounts cannot see "your" Drive. Sharing the spreadsheet with the service account email is mandatory. Also: a service account cannot act as a Workspace user without domain-wide delegation configured in the Admin console — that's an org-level change, not a config flag.
  • Cell values are strings, and formatted. $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.
  • Quota is real. ~300 requests/minute per project, ~60/minute per user, 429s with no automatic backoff in most wrappers. Add retry-with-jitter around every call, and never loop over rows issuing one request each.
  • A1 notation and sheet names. Sheets with spaces must be quoted in the range: '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.
  • Empty sheet vs missing sheet raise different exceptions and both look like "no data" if you catch broadly. Catch Google\Service\Exception specifically and inspect getErrors() — the message often names the exact range that failed.
  • Container caching. Bundle config is read at compile time. Editing the YAML without cache:clear silently keeps the old credentials path; in dev this looks like "it works on my machine, not in the container."
  • Profiler numbers were lying before v1.3.0. If you're on an older version, 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.
  • Extension points: decorate 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).
  • Log the spreadsheet ID and range on every call. When a sync writes to the wrong tab at 2am, that one log line is the difference between a five-minute fix and an hour. Belt and braces: have your SheetsWriteEvent listener log the same fields.
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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core