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

Cloud Bigquery Reservation Laravel Package

google/cloud-bigquery-reservation

Idiomatic PHP client for Google BigQuery Reservation. Install with composer require google/cloud-bigquery-reservation. Supports REST over HTTP/1.1 and gRPC, with authentication and debugging guides. Part of Google Cloud PHP and generally available.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a generated GAPIC client, not a Laravel package. There is no service provider, no config file, no facade. Day-to-day, you install it, wrap it once in your own thin service, and then call unary RPCs against the BigQuery Reservation Admin API. Treat it the way you'd treat the AWS SDK: infrastructure plumbing you hide behind a couple of classes.

Install

composer require google/cloud-bigquery-reservation
# optional but recommended for speed/streaming:
# install the grpc PHP extension (pecl install grpc) — else force REST transport

Where to look first

  1. The README sample (GetBiReservationRequest → ReservationServiceClient) — every other call is the same shape.
  2. The generated API reference: https://cloud.google.com/php/docs/reference/cloud-bigquery-reservation/latest — this is where the real surface lives (the README shows exactly one method).
  3. The BigQuery docs for the concepts, not the code: slot commitments vs. autoscaling vs. assignments. Most bugs here are conceptual (billing/slot semantics), not PHP.
  4. Use the new client surface: Google\Cloud\BigQuery\Reservation\V1\Client\ReservationServiceClient. The old Google\Cloud\BigQuery\Reservation\V1\ReservationServiceClient was deprecated in v1.3.0. Don't copy blog posts that use the old one.

First use case — the one that actually pays for itself: a nightly artisan command that right-sizes reservations.

// config/bigquery-reservation.php
return [
    'project_id' => env('BQ_RESERVATION_PROJECT', env('GOOGLE_CLOUD_PROJECT')),
    'location'   => env('BQ_RESERVATION_LOCATION', 'US'), // 'US', 'EU', or a region like 'us-central1'
];
// app/Services/BigQuery/ReservationManager.php
namespace App\Services\BigQuery;

use Google\Cloud\BigQuery\Reservation\V1\Client\ReservationServiceClient;
use Google\Cloud\BigQuery\Reservation\V1\{GetReservationRequest, Reservation};
use Google\Cloud\BigQuery\Reservation\V1\UpdateReservationRequest;
use Google\Protobuf\FieldMask;

class ReservationManager
{
    public function __construct(private ReservationServiceClient $client, private array $config) {}

    public function scale(string $reservationId, int $slots): Reservation
    {
        $name = ReservationServiceClient::reservationName(
            $this->config['project_id'], $this->config['location'], $reservationId
        );

        return $this->client->updateReservation(new UpdateReservationRequest([
            'reservation' => (new Reservation())->setName($name)->setSlotCapacity($slots),
            'update_mask' => new FieldMask(['paths' => ['slot_capacity']]),
        ]));
    }
}
// app/Console/Commands/ScaleBigQueryReservations.php  (schedule: ->dailyAt('20:00'))
$slots = now()->isWeekend() ? 0 : 200;
$current = app(ReservationManager::class)->get($id)->getSlotCapacity();
if ($current !== $slots) {
    app(ReservationManager::class)->scale($id, $slots);
    Log::info('bq.reservation.scaled', ['id' => $id, 'from' => $current, 'to' => $slots]);
}

That's the 80% path: bind the client as a singleton, put project_id/location in config, and always read-before-write so you don't fire a no-op updateReservation.

Implementation Patterns

Bind the client as a singleton, configure it explicitly. The client resolves Application Default Credentials on construction; doing that per-request is wasteful and noisy in logs.

// app/Providers/BigQueryReservationServiceProvider.php
public function register(): void
{
    $this->app->singleton(ReservationServiceClient::class, fn () => new ReservationServiceClient([
        'transport'       => extension_loaded('grpc') ? 'grpc' : 'rest',
        'credentials'     => config('bigquery-reservation.credentials_path'), // omit to use ADC
        'logger'          => Log::channel('bigquery')->getLogger(),            // optional PSR-3
        'retries'         => 3,                                               // ApiCore retry config
    ]));

    $this->app->singleton(ReservationManager::class, fn ($app) => new ReservationManager(
        $app->make(ReservationServiceClient::class),
        $app->make('config')->get('bigquery-reservation'),
    ));
}

Wrap every call in a domain method. Never let ReservationServiceClient leak into controllers or jobs. The generated request/response messages are proto objects with snake_case JSON and no Laravel ergonomics — convert them to plain arrays/DTOs at the boundary.

public function list(): array
{
    $out = [];
    $request = new ListReservationsRequest([
        'parent' => ReservationServiceClient::locationName($this->project, $this->location),
    ]);
    foreach ($this->client->listReservations($request)->iterateAllElements() as $r) {
        $out[] = [
            'name'          => $r->getName(),
            'slot_capacity' => $r->getSlotCapacity(),
            'edition'       => $r->getEdition(),
            'autoscale'     => $r->getAutoscale()?->getMaxSlots(),
        ];
    }
    return $out;
}

Assignment lifecycle = your onboarding/offboarding hook. Assigning a project (or folder/org) to a reservation is how teams actually consume reserved slots. Pair this with whatever "new team project" flow you already have.

use Google\Cloud\BigQuery\Reservation\V1\{Assignment, CreateAssignmentRequest, DeleteAssignmentRequest};

// Onboarding: give projects/analytics-prod a share of the `prod` reservation.
$this->client->createAssignment(new CreateAssignmentRequest([
    'parent'       => ReservationServiceClient::reservationName($project, $location, 'prod'),
    'assignment'   => (new Assignment())->setAssignee('projects/analytics-prod')
                                        ->setJobType(Assignment\JobType::QUERY),
    'assignment_id' => 'analytics-prod',
]));

// Offboarding: deleteAssignment with the full assignment name.

Move reservation mutations onto the queue. These are network calls to a regional control plane; they don't belong in a request cycle. Queue jobs with $tries = 3 and backoff() — combined with idempotent assignment_id/reservation_id, retries are safe (ALREADY_EXISTS means "it worked last time").

final class ScaleReservation implements ShouldQueue
{
    public int $tries = 3;
    public array $backoff = [5, 30, 120];

    public function __construct(public string $reservationId, public int $slots) {}

    public function handle(ReservationManager $manager): void
    {
        $manager->scale($this->reservationId, $this->slots);
    }

    public function failed(ApiException $e): void { /* page on-call if this is a nightly scale-down */ }
}

Cache reads, invalidate on write. listReservations / getReservation are cheap but they're still per-request quota. Cache::remember("bq.reservations.{$location}", 300, ...) and Cache::forget() inside the write path makes an admin dashboard free.

Expose a read-only admin endpoint for whoever owns the BigQuery budget — reservation list, commitment end dates, per-assignment slot usage. This is usually the highest-value thing you build with this package, because reservations bill by slot-hour and finance will ask.

Monitor disaster recovery. Reservation.replication_status (v2.2.0+) and soft_failover_start_time (v2.4.0+) are the fields to alert on if you run a multi-region / DR setup. A scheduled getReservation scanning these into Prometheus/Grafana beats waiting for a user to notice.

IAM lives here too. Since v2.6.0 the reservation IAM getIamPolicy / setIamPolicy / testIamPermissions methods exist, so "can this service account use the prod reservation?" is answerable from the app rather than a gcloud shell-out.

Gotchas and Tips

Names are ARNs, and they're easy to get wrong. projects/{project}/locations/{location}/reservations/{reservation} — note locations, not regions, and US/EU are valid multi-region locations for reservations even though they aren't GCP regions. Use the generated reservationName() / locationName() helpers rather than string-interpolating; a typo yields INVALID_ARGUMENT with a message that only vaguely points at the segment.

updateReservation without an update_mask will not do what you want. Proto3 presence means an unset slot_capacity is indistinguishable from 0. Always send a FieldMask listing exactly the fields you're changing — otherwise you can silently zero out max_slots, autoscale, or concurrency_control.

Capacity commitments are contracts, not config. You cannot shrink or cancel a CapacityCommitment before commitment_end_time; the API returns FAILED_PRECONDITION and there's no override. Your app must read commitment_end_time and is_flat_rate before building any "reduce spend" UI. Same for reservations: deletes are soft (they sit in a pending state) and slot capacity reductions can fail while slots are in use.

One assignment per assignee per location. Moving projects/x from reservation A to reservation B is delete-then-create, which means a real window where the project has no reservation and falls back to on-demand pricing (or fails, if the project is edition-constrained). Do it off-peak and log both steps.

The gRPC extension is a hard dependency for transport => 'grpc'. In Docker, pecl install grpc or apt-get install php-grpc; without it you get a confusing Could not construct ApplicationDefaultCredentials / extension-not-found path. Cheapest robust answer: detect extension_loaded('grpc') and fall back to 'rest'. You lose streaming methods — irrelevant here, every call in this API is unary, so there are no ->pollUntilComplete() long-running operations to worry about.

Catch ApiException, not Throwable, and switch on the status code. This is where 90% of "why did it fail" debugging happens.

use Google\ApiCore\ApiException;
use Google\Rpc\Code;

try {
    $this->client->createReservation($request);
} catch (ApiException $e) {
    match ($e->getStatus()?->getCode()) {
        Code::ALREADY_EXISTS     => null,                       // idempotent retry, fine
        Code::NOT_FOUND          => throw new ReservationMissing($id),
        Code::PERMISSION_DENIED  => throw new ReservationForbidden($id), // IAM, not code
        Code::FAILED_PRECONDITION=> throw new ReservationStateConflict($e->getMessage()),
        default                  => throw $e,
    };
}

PERMISSION_DENIED is almost always IAM, not your code. The service account needs roles/bigquery.resourceAdmin (or a narrower custom role) on the billing project, and the reservation's project must have BigQuery enabled. Half of all first-run failures are a service account with bigquery.user trying to create a reservation.

Locations are immutable. You cannot move a reservation between regions — you create a new one and reassign. Model this explicitly so nobody builds a "change region" button.

Credentials option is deprecating. Recent releases (v2.5.0) flagged the credentials client option. Prefer ADC (GOOGLE_APPLICATION_CREDENTIALS or workload identity) and reserve explicit credential injection for genuine multi-tenant cases. Universe domain templates are supported (v1.3.0) if you're on a sovereign cloud.

serializeToJsonString() returns snake_case JSON, not your API's casing. Fine for logging, wrong for anything a frontend consumes. Convert to your own DTOs.

Debugging: turn on the logger client option (documented since v2.1.1) at debug level — it prints the full request/response including the gRPC metadata, which is the fastest way to spot a malformed name or a missing field mask. ApiException::getMessage() alone is usually insufficient.

Testing: don't hit the real API. Bind a ReservationManager fake (or a mocked ReservationServiceClient) into the container in TestCase::setUp(). The generated client is final-ish in spirit and hard to mock directly — which is exactly why the wrapper class matters. Assert on the UpdateReservationRequest objects your fake received to verify the right field masks are being sent.

Version pinning: the package is GA and won't break you in minors (v2.x added replication_status, max_slots/scaling_mode, principal, reservation groups, BACKGROUND_* assignment job types). But because you're on generated code, a composer update can change proto message shapes; run your test suite and pin to ^2.8 rather than *.

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.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel