google/cloud-spanner
Idiomatic PHP client for Google Cloud Spanner, a fully managed relational database with global transactional consistency, ANSI SQL, schemas, and automatic synchronous replication. Install via Composer; requires the gRPC extension and Google Cloud authentication.
Installation:
composer require google/cloud-spanner
Ensure the grpc PHP extension is installed (see gRPC guide). Pin to ^2.11.3 or later so you get the buffered-mutation/commit-retry fix (see Gotchas).
Authentication:
Configure credentials via environment variables (GOOGLE_APPLICATION_CREDENTIALS) or a service account JSON file.
Follow the Authentication guide.
First Query:
use Google\Cloud\Spanner\SpannerClient;
$spanner = new SpannerClient();
$db = $spanner->instance('your-instance')->database('your-database');
$rows = $db->execute('SELECT * FROM Users WHERE id = @id', ['parameters' => ['id' => 1]]);
foreach ($rows as $row) {
print_r($row);
}
$spanner->instance('id')->database('name')$db->execute('SQL', ['parameters' => [...]])$db->runInTransaction(fn($transaction) => $transaction->execute('SQL'))// Create
$db->execute('INSERT INTO Users (id, name) VALUES (@id, @name)', [
'parameters' => ['id' => 1, 'name' => 'John']
]);
// Read
$rows = $db->execute('SELECT * FROM Users WHERE id = @id', [
'parameters' => ['id' => 1]
]);
// Update
$db->execute('UPDATE Users SET name = @name WHERE id = @id', [
'parameters' => ['id' => 1, 'name' => 'Updated']
]);
// Delete
$db->execute('DELETE FROM Users WHERE id = @id', ['parameters' => ['id' => 1]]);
$db->runInTransaction(function ($transaction) {
$transaction->execute('INSERT INTO Orders (user_id, amount) VALUES (@user_id, @amount)', [
'parameters' => ['user_id' => 1, 'amount' => 100.00]
]);
$transaction->execute('UPDATE Users SET balance = balance - @amount WHERE id = @user_id', [
'parameters' => ['user_id' => 1, 'amount' => 100.00]
]);
});
$db->execute([
'sql' => [
'INSERT INTO Users (id, name) VALUES (@id1, @name1)',
'INSERT INTO Users (id, name) VALUES (@id2, @name2)'
],
'parameters' => [
['id1' => 2, 'name1' => 'Alice'],
['id2' => 3, 'name2' => 'Bob']
]
]);
When using insertBatch/updateBatch/deleteBatch with a precommit token (for idempotent commit retries), the client now clears its buffered mutation queue on retry (fixed in v2.11.3). Prefer this pattern over manual commit() retry loops:
$db->insertBatch('Users', [
['id' => 10, 'name' => 'Carol'],
['id' => 11, 'name' => 'Dave'],
]);
// The SDK handles commit retries using the precommit token and reuses
// the same mutation buffer without duplicating it (>= v2.11.3).
$stmt = $db->prepare('SELECT * FROM Users WHERE id = @id');
$rows = $stmt->execute(['parameters' => ['id' => 1]]);
$db->execute('CREATE TABLE Users (id INT64, name STRING(100))');
$db->execute('ALTER TABLE Users ADD COLUMN email STRING(255)');
$db->execute('DROP TABLE Users');
Use vladimir-yuldashev/laravel-spanner for seamless integration:
// config/database.php
'spanner' => [
'driver' => 'spanner',
'instance' => env('SPANNER_INSTANCE'),
'database' => env('SPANNER_DATABASE'),
'project_id' => env('GOOGLE_PROJECT_ID'),
],
Extend Laravel's query builder for Spanner-specific behaviour:
use Google\Cloud\Spanner\SpannerClient;
class SpannerConnection extends \Illuminate\Database\Connection { /* ... */ }
class SpannerQueryBuilder extends \Illuminate\Database\Query\Builder {
protected function connect($dsn = null) {
$spanner = new SpannerClient();
$db = $spanner->instance(config('database.spanner.instance'))
->database(config('database.spanner.database'));
return new SpannerConnection($db);
}
}
For high-concurrency apps, customize the session cache:
use Symfony\Component\Cache\Adapter\RedisAdapter;
$cache = new RedisAdapter();
$spanner = new SpannerClient(['cacheItemPool' => $cache]);
Class 'GRPC\Channel' not foundgrpc extension and enable it in php.ini:
extension=grpc.so
Could not authenticate requestGOOGLE_APPLICATION_CREDENTIALS points to a valid service account JSON file.$spanner->instance('id')->database('name')->session()->refresh();
commit using a precommit token was retried, the buffered mutation queue was not cleared, so the mutation batch could be replayed and rows inserted/updated twice.google/cloud-spanner >= 2.11.3. If you cannot upgrade, avoid mixing insertBatch/updateBatch/deleteBatch with manual commit retry loop; use runInTransaction/transaction() instead, or wrap retries with an idempotency key check.@param) are required; positional binding is not supported.// Correct
$db->execute('SELECT * FROM Users WHERE id = @id', ['parameters' => ['id' => 1]]);
// Incorrect (will fail)
$db->execute('SELECT * FROM Users WHERE id = ?', [1]);
$rows = $db->execute('SELECT * FROM LargeTable');
foreach ($rows as $row) {
// Process row-by-row
}
Add to php.ini:
grpc.verbose_logging = 1
Logs will appear in stderr.
Use EXPLAIN to analyze query performance:
$plan = $db->execute('EXPLAIN SELECT * FROM Users');
print_r($plan->metadata());
Enable transaction debug mode:
$db->runInTransaction(function ($transaction) {
$transaction->setDebugMode(true);
// Your transaction logic
});
Implement exponential backoff for transient errors:
use Google\Cloud\Core\Retry\RetryPolicy;
$retryPolicy = new RetryPolicy();
$retryPolicy->setMaxAttempts(3);
$retryPolicy->setInitialBackoff(100); // ms
$db->executeWithRetry('SELECT * FROM Users', ['parameters' => []], $retryPolicy);
If you suspect duplicate writes after an ABORTED/UNAVAILABLE commit retry, check composer.lock for google/cloud-spanner version. Anything < 2.11.3 is affected. Enable verbose gRPC logging to confirm the commit is being re-issued with a precommit token.
Add request/response middleware for logging or metrics:
use Google\Cloud\Core\Grpc\Middleware\MiddlewareInterface;
class LoggingMiddleware implements MiddlewareInterface {
public function handle($request, callable $next) {
\Log::debug('Spanner request:', $request->getMetadata());
$response = $next($request);
\Log::debug('Spanner response:', $response->getMetadata());
return $response;
}
}
$spanner = new SpannerClient([
'middleware' => [new LoggingMiddleware()]
]);
For distributed environments, replace the default lock with a custom implementation:
use Google\Cloud\Core\Lock\LockInterface;
class RedisLock implements LockInterface {
public function acquire(array $options = []) { /* ... */ }
public function release() { /* ... */ }
public function synchronize(callable $func, array $options = []) { /* ... */ }
}
$spanner->instance('id')->database('name', ['lock' => new RedisLock()]);
Extend result set processing:
use Google\Cloud\Spanner\Database;
class CustomDatabase extends Database {
public function execute($sql, array $options = []) {
$result = parent::execute($sql, $options);
// Wrap or post-process the result set.
return new CustomResult($result);
}
}
How can I help you explore Laravel packages today?