google/cloud-bigquery-connection
Official Google Cloud PHP client for BigQuery Connection. Install with composer require google/cloud-bigquery-connection, authenticate with Google Cloud credentials, then use ConnectionServiceClient to manage and retrieve BigQuery connection resources over REST or gRPC.
1. Install
composer require google/cloud-bigquery-connection
Optional but recommended for lower latency:
pecl install grpc && composer require google/protobuf
The client auto-selects gRPC when ext-grpc is loaded and silently falls back to REST otherwise — no code changes needed.
2. Authenticate
Use Application Default Credentials (GOOGLE_APPLICATION_CREDENTIALS=/path/to/sa.json or workload identity). Don't commit JSON keys; put them in .env/Secret Manager and reference a path:
// config/services.php
'bigquery_connection' => [
'project' => env('GOOGLE_CLOUD_PROJECT'),
'location' => env('BIGQUERY_CONNECTION_LOCATION', 'US'),
],
Note the credentials client option is deprecated as of v2.1.0 — pass a path via GOOGLE_APPLICATION_CREDENTIALS, or inject a Google\Auth\Credentials\ServiceAccountCredentials object through credentialsCallback/requestHandler if you truly need runtime secrets.
3. Look here first
Google\Cloud\BigQuery\Connection\V1\Client\ConnectionServiceClient — the only class you normally touch (the older ConnectionServiceGapicClient and the legacy V1 surface are deprecated since 1.5.0).Google\Cloud\BigQuery\Connection\V1\Connection — the resource DTO you'll serialize/hydrate.ConnectionServiceClient::connectionName($project, $location, $connection), ::locationName(), ::projectName().4. First use case — audit existing connections
The fastest sanity check and a genuinely useful Artisan command:
use Google\Cloud\BigQuery\Connection\V1\Client\ConnectionServiceClient;
use Google\Cloud\BigQuery\Connection\V1\ListConnectionsRequest;
$client = new ConnectionServiceClient();
$request = (new ListConnectionsRequest())
->setParent(ConnectionServiceClient::locationName(config('services.bigquery_connection.project'), 'US'));
foreach ($client->listConnections($request)->iterateAllElements() as $connection) {
$this->line($connection->getName().' -> '.$connection->getFriendlyName());
}
If that prints your connections, auth, region, and networking are all correct.
Scope reminder: this package manages connection resources (Cloud SQL, Spanner, BigLake, Spark, Salesforce). It does not run queries. Querying lives in
google/cloud-bigquery.
Bind the client as a singleton. Creating a client builds a gRPC channel / Guzzle handler — expensive per request.
// app/Providers/BigQueryConnectionServiceProvider.php
$this->app->singleton(ConnectionServiceClient::class, fn () => new ConnectionServiceClient([
'transport' => config('services.bigquery_connection.transport', 'grpc'),
]));
$this->app->singleton(BigQueryConnectionManager::class, fn ($app) => new BigQueryConnectionManager(
$app->make(ConnectionServiceClient::class),
config('services.bigquery_connection.project'),
config('services.bigquery_connection.location'),
));
Wrap it in a domain service. Keep protobuf out of controllers; return arrays/DTOs so your app stays testable.
class BigQueryConnectionManager
{
public function __construct(
private ConnectionServiceClient $client,
private string $project,
private string $location,
) {}
public function get(string $id): array
{
$name = ConnectionServiceClient::connectionName($this->project, $this->location, $id);
return json_decode(
$this->client->getConnection(new GetConnectionRequest(['name' => $name]))->serializeToJsonString(),
true
);
}
}
Provision connections from a job, not a request. Creating a Cloud SQL connection blocks on Google-side validation (up to ~30s). Dispatch a queued job that creates the connection, then writes the connection id back to your tenant/integrationsettings model.
$connection = $client->createConnection(
(new CreateConnectionRequest())
->setParent(ConnectionServiceClient::locationName($this->project, $this->location))
->setConnectionId("tenant-{$tenant->id}")
->setConnection((new Connection())
->setFriendlyName("tenant-{$tenant->id}")
->setCloudSql((new CloudSqlProperties())
->setInstanceId('my-project:us-central1:my-db')
->setDatabase('app')
->setType(CloudSqlProperties\DatabaseType::POSTGRES)
->setCredential((new CloudSqlCredential())->setUsername('bq')->setPassword($secret))))
);
Store the connection's friendly name, not the full resource path. Federated queries reference the friendly/connection id (EXTERNAL_QUERY("my-project.US.tenant-42", ...)), and it's what other teams will ask for.
Credential rotation is an update with a field mask. This is the most common day-to-day operation:
$client->updateConnection(
(new UpdateConnectionRequest())
->setConnection((new Connection())
->setName(ConnectionServiceClient::connectionName($this->project, $this->location, $id))
->setCloudSql((new CloudSqlProperties())
->setCredential((new CloudSqlCredential())
->setUsername($user)->setPassword($newPassword))))
->setUpdateMask((new FieldMask())->setPaths(['cloud_sql.credential.password']))
);
Preflight permissions. Before letting a user attempt creation in a UI, call testIamPermissions with bigquery.connections.create to render a clean 403 instead of a stack trace.
Cache the connection list. listConnections has no server-side filter and eats your BigQuery Connection API quota. Cache per project/location for a few minutes with Cache::remember(), keyed so a create/delete in your app busts it.
Paginate deliberately. listConnections() returns a PagedResponse; use iterateAllElements() for commands/jobs and getPage() inside HTTP endpoints so you can expose a page token to the frontend and avoid unbounded loops.
Testing. Never hit the network in CI. Bind a ConnectionServiceClient configured with a MockHttpHandler/mock transport, or (simpler) type-hint your own BigQueryConnectionManager interface and fake it. Assertions on $connection->serializeToJsonString() are readable and stable.
US, EU, and specific regions are distinct parents — a connection created in US is invisible to a EU list call. Make location an explicit config value, and expect NOT_FOUND when someone forgets it.EXTERNAL_QUERY execution, datasets, and jobs all live in google/cloud-bigquery. Reaching for this package to run SQL is the #1 wrong turn.updateConnection without an updateMask silently does nothing useful. Only display_name, description, and cloud_sql.credential.* are mutable; attempting to change instance_id/database requires delete + recreate. Build the mask from the fields you actually changed rather than hardcoding.Connection objects are mutable; if you tweak one for output and later reuse it, you'll send stale/leaky fields. clone before modifying, and prefer sending a freshly built message.setIamPolicy is a full replace. Read the current policy with getIamPolicy, append your binding, and use the returned etag — omitting it causes ABORTED on concurrent edits. Additive IAM is not a thing here.ApiException with FAILED_PRECONDITION and surface "in use" rather than a 500.create/testIamPermissions, either rely on GAX's default retry idempotency or wrap in Laravel's Retry::times(3)->exponential() — but never retry createConnection blindly without a deterministic connection_id, or you'll get ALREADY_EXISTS.ApiException is your only failure type. Map getStatus(): PERMISSION_DENIED / UNAUTHENTICATED → config or auth problem; NOT_FOUND → wrong project/location/name; INVALID_ARGUMENT → usually a missing or malformed field-mask path. Log $e->getStatus() and $e->getDetails(), not the whole credential-bearing message.print_r($connection). Use serializeToJsonString() after stripping, and keep Cloud SQL passwords out of logs and out of your toArray() DTOs entirely.['transport' => 'rest'] explicitly if you want deterministic behavior between local and prod.serviceAddress templates). If you use a non-default universe or a VPC-SC perimeter, set the endpoint template in config rather than editing generated code.^2.2 and let Dependabot do its thing; don't mix v1 and v2 examples from the docs.['defaultCallOptions' => ['timeoutMillis' => 60000]], custom retrySettings, and logger (documented in 2.0.2) can all be passed in the constructor array — that's usually a better lever than wrapping every call.How can I help you explore Laravel packages today?