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

Laravel Spanner Laravel Package

colopl/laravel-spanner

Laravel database driver for Google Cloud Spanner. Adds Eloquent and query builder support so applications can use Spanner as a familiar backend with models, migrations, transactions, and session handling, simplifying integration with Google's globally distributed database.

View on GitHub
Deep Wiki
Context7

Getting Started

  • Install the transport first. pecl install grpc (and enable extension=grpc.so) before composer require colopl/laravel-spanner. Almost every install failure is a missing/version-mismatched gRPC extension, not the package.
  • Version pinning: the package majors track Laravel majors (v10 → Laravel 10, v11 → Laravel 11, v12 → Laravel 12). Do not upgrade the package without upgrading Laravel in lockstep. v12.0.0 is now a stable release (the beta line ended with v12.0.0-beta2), so prefer it over any earlier v12 beta tag — it carries the session-cleanup fix plus the perf work below.
  • v12 ships the google/cloud-spanner v2 client. That is a major dependency bump: if you are coming from v11, expect transitive conflicts on google/* and protobuf packages. Resolve with composer why-not google/cloud-spanner ^2.0 before touching application code, and rebuild CI images so ext-grpc matches the new protobuf version.
  • Configure a connection in config/database.php and point DB_CONNECTION at it:
'spanner' => [
    'driver'      => 'spanner',
    'project_id'  => env('GOOGLE_CLOUD_PROJECT'),
    'instance_id' => env('SPANNER_INSTANCE'),
    'database'    => env('SPANNER_DATABASE'),
    'keyFilePath' => env('GOOGLE_APPLICATION_CREDENTIALS'), // omit on GKE/Cloud Run
    'useTransaction' => true, // transactional DML instead of mutations
    // new in v12: SERIALIZABLE (default) or REPEATABLE_READ
    'isolation_level' => env('SPANNER_ISOLATION_LEVEL', 'SERIALIZABLE'),
],
  • First use case: take one existing Eloquent model, move it to the Spanner connection, and read/write a row. Do this before touching migrations — it proves auth, gRPC, and the driver work end-to-end in ~10 minutes.
  • Local dev: run the Spanner emulator and set SPANNER_EMULATOR_HOST=localhost:9010. The emulator ignores credentials entirely, so it is the fastest way to get a working schema.
  • Then do migrations. Read the package docs on schema limitations before writing your first migration; Spanner is not MySQL and the schema builder is not a full abstraction.

Implementation Patterns

  • Always declare a primary key. Spanner requires one on every table, and there is no auto-increment. Use HasUuids/HasUlids on models and $table->string('id', 36)->primary() (or a composite $table->primary([...])) in migrations.
  • Unique indexes as a separate migration step. Spanner does not support unique constraints inside CREATE TABLE, so $table->unique() is not a reliable inline option. Create the table first, then issue a CREATE UNIQUE INDEX in a follow-up statement/migration, and drop-recreate the index when the columns change.
  • Ordered indexes now generate the right names (v12). If an index declared an ORDER BY, the driver previously derived an incorrect index name — which meant dropIndex()/renameIndex() could miss the real index in Spanner. On v12 the names line up. If you carried a workaround (raw DROP INDEX statements, hand-pinned index names) from v11, audit and remove it, and re-verify any index drops you scripted in the past.
  • Pick an isolation level deliberately (new in v12). The default SERIALIZABLE is what most apps want. REPEATABLE_READ is worth reaching for on read-heavy, contention-prone transactions (it reduces abort/retry churn), but only apply it where your code does not depend on serializable semantics — do not flip it globally for an OLTP write path.
  • Interleaved tables for parent/child hierarchies — use the package's interleaveIn() schema helper for child tables (e.g. orders → order_items). This keeps related rows co-located, which matters far more for Spanner latency than for MySQL, and lets you read a whole aggregate in one split.
  • Bulk writes: the driver maps plain insert() to Spanner mutations, which are dramatically faster than row-at-a-time DML for seeds/imports. For large imports, chunk into insert() calls (thousands of rows per batch) rather than looping Model::create().
  • Transactions: use DB::transaction() normally, but keep callbacks short and idempotent — Spanner aborts on lock contention and the driver retries. Never put HTTP calls, file writes, or queue pushes inside a transaction closure.
  • Read-heavy reporting: prefer read-only/stale reads where the business tolerates slight lag; single-region strong reads are the expensive path. Push analytics queries to a replica or BigQuery rather than adding them to the OLTP connection.
  • Testing: point the test connection at the emulator and use RefreshDatabase. Creating/dropping databases is slow, so prefer one shared emulator database per CI job with per-test truncation (DELETE FROM ... WHERE TRUE) over migrate:fresh per test class. If you do drop databases programmatically, let the driver do it — v12 clears its cached session on drop, which is exactly what fixes stale-session errors in custom harnesses.
  • Eloquent works, but indexes are on you. There are no optimizer hints from the query builder and no "index exists?" safety net — design secondary indexes explicitly against your real access patterns, and add them in their own migrations.
  • No action needed on the v12 perf changes. The parameterizer now checks for LIKE once per query rather than per parameter, and timestamp TIMESTAMP values are timezone-converted before Carbon instantiation. Correctness is unchanged; you just get faster parameterization and cheaper hydration on timestamp-heavy result sets. Good moment to re-run your benchmarks — v11 → v12 should be a net win.

Gotchas and Tips

  • v12 requires the google/cloud-spanner v2 client. This is the single biggest upgrade risk. Do the dependency bump in isolation (composer update, boot the app, run one query) before merging any Laravel 12 upgrade work, so a failure points at one cause.
  • No $table->id() / increments(). No auto-increment, no sequences. If you see AUTO_INCREMENT semantics in your schema, the migration is wrong.
  • DDL is slow and long-running. Schema changes take seconds to minutes and consume instance resources. Never run migrations inside a request; batch them into a deploy step, and avoid migrate:fresh as a habit — dropping and recreating a database is a heavyweight operation.
  • Session/database lifecycle (fixed in v12.0.0). The driver now clears its cached Spanner session when a database is dropped, so dropping/recreating outside of RefreshDatabase no longer leaves you with stale schema/session state and confusing "table not found" or aborted-transaction errors. The lesson stands: always go through the driver when dropping a database rather than issuing DDL behind its back.
  • ALTER is limited. Spanner permits only narrow column type changes (e.g. INT64 → STRING) and no in-place renames in many cases. Laravel's $table->change() will not behave like MySQL — plan for create-new-table-and-copy migrations on structural changes.
  • Unsupported MySQL-isms: enum types, unsigned integers, full-text indexes, spatial types, lockForUpdate()/shared locks. Anything relying on row-level pessimistic locking must be rewritten around Spanner's optimistic transactions.
  • Transaction limits: no nested transactions or savepoints (Laravel's nesting is a no-op join), a hard cap on transaction duration and mutation count, and a 10 MB row limit. Long-running "batch inside a transaction" jobs will be aborted — chunk outside the transaction. Changing isolation_level does not change these caps.
  • Commit timestamps: use PENDING_COMMIT_TIMESTAMP() for created_at-style columns that need Spanner-assigned ordering, rather than relying on application clocks across regions.
  • Debugging is quieter than MySQL. Errors surface as gRPC status codes wrapped in QueryException; log the full message. Turn on the package's query logging and check the Spanner console's query stats for actual latency — slow queries are usually missing or unused indexes, not driver overhead.
  • Emulator ≠ production. The emulator does not faithfully reproduce query plans, DDL timing, IAM behavior, or isolation-level semantics. Run at least one integration suite against a real (small) instance before trusting performance-sensitive paths or your isolation_level choice.
  • Docker/CI images: bake the gRPC extension matching the protobuf version pulled in by the v2 client, or you will get opaque "extension not loaded" failures at boot.
  • Pin exact versions still. v12.0.0 is stable and safe to adopt, but the v11 → v12 jump bundles a client-library major and a config-surface change. Pin 12.0.*, read the changelog for each minor, and treat isolation_level as a per-connection config you set intentionally rather than a copy-paste default.
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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings