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 Pubsub Laravel Package

google/cloud-pubsub

Idiomatic PHP client for Google Cloud Pub/Sub, a fully-managed real-time messaging service for sending and receiving messages between independent applications. Install via Composer; supports REST and gRPC with built-in authentication helpers.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install with gRPC, not just the package. The package works over REST, but streaming pull and lower latency need the extension:

composer require google/cloud-pubsub
pecl install grpc protobuf   # then enable in php.ini

Verify with php -m | grep -E 'grpc|protobuf'. Without gRPC you fall back to HTTP/1.1, which is fine for publishing from a web request but a poor fit for consumers.

2. Know that there are two clients in this package. The README sample shows the generated GAPIC client (Google\Cloud\PubSub\V1\Client\PublisherClient, SubscriberClient). Day-to-day you will almost always want the ergonomic wrapper:

use Google\Cloud\PubSub\PubSubClient;

$pubsub = new PubSubClient([
    'projectId' => config('services.pubsub.project'),   // or auto-detected from ADC
]);

Start reading at src/PubSubClient.php, then src/Topic.php, src/Subscription.php, src/Message.php. The GAPIC classes live under src/V1/ and are worth opening only when the wrapper is missing a field.

3. Authenticate with Application Default Credentials. In production use a service account attached to the workload (GKE/Cloud Run/App Engine) or GOOGLE_APPLICATION_CREDENTIALS pointing at a mounted key file. Never pass credentials inline — keyFile/keyFilePath/credentials client options are deprecated as of v2.15.0.

4. First use case: publish a domain event from an existing app.

$topic = $pubsub->topic('orders');
$topic->create();          // idempotent-ish; guard with exists() in real code

$topic->publish([
    'data'       => json_encode(['order_id' => $order->id, 'total' => $order->total]),
    'attributes' => ['type' => 'order.created', 'tenant' => (string) $order->tenant_id],
]);

Then a consumer:

$subscription = $pubsub->subscription('orders-worker', 'orders'); // topic matters on create
foreach ($subscription->pull(['maxMessages' => 50]) as $message) {
    process(json_decode($message->data(), true));
    $subscription->acknowledge([$message]);
}

That is the whole loop. Everything below is about making it survive production.

Implementation Patterns

Bind one client as a singleton. gRPC channel setup is not free; constructing PubSubClient per request/command is a measurable cost and can leak file descriptors.

// AppServiceProvider::register()
$this->app->singleton(PubSubClient::class, fn () => new PubSubClient([
    'projectId' => config('services.pubsub.project'),
    'logger'    => app(\Psr\Log\LoggerInterface::class), // supported client option
]));

Wrap it behind your own interface. The SDK has no fake, and mocking Topic/Subscription is painful. A thin MessageBus contract (publish(string $type, array $payload, array $attributes)) with a Pub/Sub implementation plus an in-memory fake pays for itself in the first week of tests.

Envelope your messages. Attributes are filterable and cost nothing extra; the payload is not. Put routing metadata (type, tenant, correlation_id, schema_version) in attributes and keep the body as raw JSON. Remember data must be a string — the client base64-encodes for you, so json_encode() it and never double-encode.

Subscriptions are configuration, not code. Create them once (deploy script, Terraform, or a guarded exists()/create() block) with the properties that matter:

$pubsub->subscription('orders-worker', 'orders')->create([
    'ackDeadlineSeconds'       => 60,          // 10–600
    'messageRetentionDuration' => '604800s',   // default 7d, max 31d
    'retryPolicy'              => ['minimumBackoff' => '10s', 'maximumBackoff' => '600s'],
    'deadLetterPolicy'         => [
        'deadLetterTopic'     => $pubsub->topic('orders-dlq')->name(),
        'maxDeliveryAttempts' => 5,
    ],
    'expirationPolicy'         => ['ttl' => '31d'],  // else idle subs vanish
]);

Also common: ['pushConfig' => ['pushEndpoint' => 'https://app.example.com/pubsub/orders']] for HTTP delivery, enableExactlyOnceDelivery => true, enableMessageOrdering => true, filter for attribute-based fan-out, and cloudStorageConfig/bigQueryConfig when you want zero-code sinks.

Consumers should be Artisan commands under Supervisor (or Horizon-style workers). One command, one subscription, signal-aware shutdown:

protected $signature = 'pubsub:consume {subscription} {--max=100}';

public function handle(PubSubClient $pubsub): int
{
    $subscription = $pubsub->subscription($this->argument('subscription'));

    while ($this->running) {
        $messages = $subscription->pull([
            'maxMessages'       => (int) $this->option('max'),
            'returnImmediately' => true,   // keeps the loop responsive to SIGTERM
        ]);

        if (! $messages) { usleep(250_000); continue; }

        foreach ($messages as $message) {
            try {
                $this->dispatchNow(new HandleEvent($message));
                $subscription->acknowledge([$message]);
            } catch (\Throwable $e) {
                report($e);   // no ack → redelivery after ack deadline
            }
        }

        gc_collect_cycles();
    }
    return self::SUCCESS;
}

Register pcntl_signal(SIGTERM, ...) to flip $this->running. On deploy, let the worker finish its batch and exit rather than kill -9.

For long jobs, decouple ack from work. Pulling and then handing each message to a queued Laravel job means the ack deadline expires while the job waits. Either (a) do the work inside the pull loop and modifyAckDeadline($messages, 600) as a heartbeat for slow handlers, or (b) copy the payload into your own queue and ack immediately, accepting at-most-once for that hop.

Push subscriptions map cleanly onto a Laravel route. Register a controller, disable CSRF for it, verify the Google-signed OIDC JWT in the Authorization: Bearer header (issuer https://accounts.google.com, audience = your endpoint, email = your push service account), then return 2xx to ack, 5xx to retry. Use the token's email claim as the gate — an unauthenticated push endpoint is an open door.

Prefer filtering over fan-out duplication. One topic with several subscriptions, each with an attribute filter, is cheaper and simpler than many topics. Use separate topics when retention, IAM, or schema differ.

Testing strategy: run the official emulator in CI (gcloud beta emulators pubsub start), export PUBSUB_EMULATOR_HOST=localhost:8085 and create the client with an insecure/emulator-friendly config. Treat the emulator as a smoke test — it does not enforce real quotas, retry policies, dead lettering, or exactly-once semantics, so also cover ack/retry logic with your MessageBus fake.

Gotchas and Tips

  • The README leads with the wrong client for most readers. The PublisherClient sample is a generated-code formality; the wrapper (PubSubClient) is what has topic(), subscription(), batching, IAM helpers, and snapshots. Don't conclude the library is verbose based on the README alone.
  • subscription($name) without a topic cannot be created. create() needs the topic to build projects/*/topics/*; pass it as the second argument ($pubsub->subscription('name', 'topic')) or supply the fully-qualified resource name.
  • At-least-once is the default and duplicates are normal. Any exception, timeout, redeploy, or ack failure produces redelivery. Make handlers idempotent (unique constraint on order_id + event id) or opt into enableExactlyOnceDelivery — which is per-subscription, not free (throughput cost), and surfaces ack failures you must handle instead of swallowing.
  • Ack deadlines, not timeouts, cause mystery duplicates. Default is 10 seconds; a Laravel job that touches the DB, an HTTP call, and a mail send will blow through it. Raise ackDeadlineSeconds (max 600) and extend mid-flight for long handlers. pull() blocks; returnImmediately => true is what makes signals and shutdown work.
  • acknowledge() returns void. Without exactly-once, a failed ack is silent — the message comes back later and you get to debug it in production. Log ack outcomes and watch num_undelivered_messages / oldest-unacked-age in Cloud Monitoring.
  • Publishing is synchronous and one round trip per call. For a request path, dispatchAfterResponse() or a queued publish job keeps p99 latency down. When you do batch, pass an array of messages to a single publish() (one API request, subject to the 1000-message / 10 MB request cap) — there is no background flusher like the Node client, so your loop's shape is your throughput.
  • Message size cap is 10 MB, and data is a string. Passing an array throws. Base64-encode only if your payload is binary — for JSON, don't.
  • Ordering keys change failure semantics. With enableMessageOrdering, one failed publish stalls subsequent messages for that key; you must resume with the same orderingKey and know that redelivery can produce out-of-order across keys. Only use it when you actually need per-entity ordering.
  • Retries can duplicate publishes. A ServiceException on publish does not mean the message wasn't accepted. Carry your own event UUID in attributes and dedupe downstream, or accept the duplication.
  • keyFile / keyFilePath / credentials are deprecated (v2.15.0, and the v2.14.0 credentials-client-option deprecation). Move to ADC now; it's a one-line change while you remember, and a nasty surprise during a Google Cloud migration later.
  • Pin the version and skim the changelog. This is a GA library on a fast cadence (v2.21.0 at time of writing) where new Pub/Sub features appear as proto additions: messageTransforms (v2.10.0, with enabled deprecated for disabled in v2.11.0), AIInference transforms, Bigtable ingestion, compiled_proto_schema. None of these break you, but they're invisible unless you read releases — and the underlying API silently gains features your code should be using.
  • Debugging: pass a PSR-3 logger in client options to see RPC-level logs; wrap calls and inspect Google\ApiCore\ApiException (GAPIC) or Google\Cloud\Core\Exception\ServiceException subclasses (wrapper) including getStatus()/getDetails(). Log PubSubClient::SERVICE_NAME and the package version at boot — added in v2.9.1 and worth having in incident tickets.
  • Long-running consumer hygiene: PHP leaks memory on long loops more readily than you'd like. Clear container-bound references (app()->forgetInstance(...)), avoid accumulating messages in arrays, and call gc_collect_cycles() between batches. Also cap batch size — maxMessages => 1000 with fat payloads is a fine way to OOM.
  • Emulator is dev-only. If PUBSUB_EMULATOR_HOST exists in production, auth is bypassed and traffic goes to localhost. Add it to your config-exposure checks and never default it in config/services.php.
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.
alesima/laravel-azure-service-bus
tomshaw/shopcart
likyly/laravel
darthsoup/shoppingcart
tdt/core
labyrinthe/connect-php
encore2/laravel-admin2
cboxdk/laravel-console-kit
shapecode/doctrine-dbal-datetimeutc
gilads-otiannoh254/inertia-protocol
dakiquang/laravel-oauth-refresh-lock
woda/laravel-worktrees
dalefranks/ramasite
faghy/laracashbook
roundly-consulting/google-places-for-laravel
roundly-consulting/kubernetes-api-for-laravel
roundly-consulting/plausible-for-laravel
gvp/gvp-laravel-template
metasyncsite/laravel-metasync-client
roundly-consulting/onboarding-for-laravel