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.
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.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.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'),
],
SPANNER_EMULATOR_HOST=localhost:9010. The emulator ignores credentials entirely, so it is the fastest way to get a working schema.HasUuids/HasUlids on models and $table->string('id', 36)->primary() (or a composite $table->primary([...])) in migrations.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.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.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.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.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().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.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.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.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.$table->id() / increments(). No auto-increment, no sequences. If you see AUTO_INCREMENT semantics in your schema, the migration is wrong.migrate:fresh as a habit — dropping and recreating a database is a heavyweight operation.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.lockForUpdate()/shared locks. Anything relying on row-level pessimistic locking must be rewritten around Spanner's optimistic transactions.isolation_level does not change these caps.PENDING_COMMIT_TIMESTAMP() for created_at-style columns that need Spanner-assigned ordering, rather than relying on application clocks across regions.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.isolation_level choice.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.How can I help you explore Laravel packages today?