genesis-global/gps-messenger-bundle
genesis-global/gps-messenger-bundle integrates GPS messenger capabilities into PHP/Laravel applications, providing configurable messaging services for sending, receiving, and handling location-aware messages with simple framework wiring.
1. Install and register. This is a Symfony bundle, not a Laravel package — the only Laravel-relevant path is composer require genesis-global/gps-messenger-bundle into a project that already pulls Symfony components, and then registering the bundle manually (no Flex recipe ships with a 0-star fork, so add it to config/bundles.php / app/AppKernel.php yourself). Confirm the fork actually relaxed the PHP constraint by reading its composer.json before you commit.
2. Provision GCP first. The transport is a thin wrapper around google/cloud-pubsub; it does not manage infrastructure for you. Create the topic and subscription (gcloud pubsub topics create, gcloud pubsub subscriptions create) and grant the runtime service account roles/pubsub.publisher (producer side) and roles/pubsub.subscriber (consumer side). Credentials come from GOOGLE_APPLICATION_CREDENTIALS or the transport's client-config option.
3. Configure the transport. The DSN scheme is gps://; project/topic/subscription are passed as options (verify exact key names in DependencyInjection/Configuration.php, they differ slightly between upstream revisions):
# config/packages/messenger.yaml
framework:
messenger:
transports:
gps:
dsn: 'gps://'
options:
project_id: '%env(GPS_PROJECT_ID)%'
topic_name: 'orders'
subscription_name: 'orders-worker'
# batch_size: 100 # 1.3+: publish in batches
# client_config: '%kernel.project_dir%/config/gcp-service-account.json'
routing:
'App\Message\OrderPlaced': gps
4. First use case. Dispatch a plain message object onto the bus, then run the consumer in a worker:
$bus->dispatch(new OrderPlaced($orderId));
// $ bin/console messenger:consume gps -vv
Where to look first, in this order: GpsTransportFactory (what DSN/options are actually accepted — including batch_size as of 1.3), GpsSender (now the batching logic lives here: buffer, flush trigger, publishBatch call), GpsReceiver (the pull loop, ack/nack semantics, ack-deadline default), then DependencyInjection/Configuration.php. Those four files are the entire surface area of the package; everything else is Symfony Messenger.
Reality check before you build on it: this is still a fork whose only stated purpose was PHP 7.3 support, zero dependents, one original release in 2021. 1.3 is the first substantive release since — it adds batch_size and caches the topic-existence check per process. That's a genuine improvement, not a bugfix train, so the maintenance-risk caveat still stands: if your project is on PHP 7.4+ or Symfony 5.3+, compare against upstream petitpress/gps-messenger-bundle (check whether these two features landed there too) and treat this fork as a temporary pin.
Treat the transport as a MessageBroker, not an event bus. One transport = one topic + one subscription. Model your domain as a handful of topics (e.g. orders, notifications) and route message classes explicitly rather than dumping everything on a single gps transport — Pub/Sub has no routing or filtering at the transport layer, so the split is purely a deployment/consumer-scaling decision.
Turn on batch_size for producer-heavy workloads, and understand what you're buying. 1.3 batches publishes into a single publishBatch call, which cuts per-message HTTP/API overhead and is the single biggest throughput win available without touching GCP quotas. The trade-off is latency and failure granularity: a message handed to the bus may sit in the buffer until the batch fills (or the transport flushes), and a partial batch failure surfaces as one send result, so your dispatch-side error handling has to be coarse. Keep batch_size unset (or 1) for low-volume, latency-sensitive topics like transactional notifications; raise it (50–100) for high-fan-out event topics.
Flush discipline matters in workers and short-lived processes. If you enable batching inside a messenger:consume process, remember that the sender holds a buffer across messages — make sure your process actually drains before exit (--time-limit shutdown, container SIGTERM) or you drop the tail of the buffer. In a web request that dispatches a handful of messages then returns, a large batch_size means nothing gets published until the request ends; size it to your realistic per-request volume.
Use JSON serialization, not PHP serialization, if anything outside PHP touches the topic. Symfony's default messenger.transport.native_php_serializer produces payloads no other language can read. Switch the Messenger serializer to the Symfony Serializer with a JSON encoder, and accept that the type header travels in the Pub/Sub message attributes — a Go/Node consumer must read that attribute or decode the wrapper to know the class. Validate the byte-for-byte layout with a real gcloud pubsub subscriptions pull before designing around it. Note this interacts with batching: verify the per-message attributes are preserved per-message in a batch, not collapsed onto the first one.
Consumers are long-running workers, so treat deployment like a queue daemon. Run messenger:consume gps --time-limit=3600 --memory-limit=128M under supervisor/systemd, not cron. Pub/Sub delivers each message to exactly one subscriber on a subscription, so horizontal scaling is just "run more of these containers with the same subscription name" — no sharding config needed. Use distinct subscription names per environment and per logical consumer group; two services sharing one subscription will steal each other's messages.
Handle redelivery, because Pub/Sub is at-least-once. A thrown exception in the handler means nack, and the message comes back. Wire up Messenger's retry strategy with exponential backoff and a failure_transport, and make handlers idempotent keyed on your own business identifier (order ID, event ID) rather than relying on exactly-once delivery. This is the single most common production surprise with this bundle — and 1.3's batching makes it slightly worse on the producer side, since a batch that half-succeeds may be retried whole. Make publish-side idempotency (or duplicate-tolerant consumers) non-optional once batching is on.
Keep the handler fast relative to the ack deadline. Pub/Sub's default ack deadline is 10 seconds (configurable to 600 max). A handler that takes 45 seconds will get its message redelivered and processed twice. Split slow work into a second dispatch to another transport, or raise the subscription's ack deadline in GCP infrastructure-as-code.
Test by swapping the DSN, not by mocking the transport. In the test environment override framework.messenger.transports.gps.dsn with in-memory:// or sync:// so the same routing config exercises handlers directly. If you need to assert on the wire format — important now that batching changes the publish shape — use the Pub/Sub emulator (PUBSUB_EMULATOR_HOST=localhost:8085) — but first confirm the fork's client construction respects that env var rather than always building an authenticated client.
Bridging from Laravel. If this lives inside a Laravel app, publish via Laravel's job dispatch or a small service that wraps MessageBusInterface, and run the consumer as a second process (php artisan command that calls messenger:consume via the Symfony console kernel, or a standalone Symfony worker container). Don't try to make Laravel's queue worker drive this transport; the two have incompatible retry/ack models.
Cross-language producers. Anything else publishing to the topic must replicate the envelope Symfony's serializer expects. Practically: publish JSON whose shape matches what your handler's message class deserializes from, and set the type attribute correctly. Keep a shared schema (or at least a contract test) so a Node producer and a PHP consumer don't drift.
batch_size defaults and the exact caching scope are worth knowing precisely. Pin the exact version in composer.lock, and if you're on PHP 8, this remains a dead end; go upstream or talk to whoever owns the fork.batch_size changes the meaning of "dispatched". With batching on, dispatch() returning successfully no longer means the message is in Pub/Sub — it means it's buffered. Don't build read-after-write flows on top of a batched transport, and don't assume a caught exception means nothing was published (partial batches). For transactional outbox patterns, flush explicitly or keep that transport unbuffered.project_id fails late and cryptically. Misconfiguration surfaces as a Google client exception at dispatch/consume time, not at container compile time. Add a smoke test that boots the container and asserts the transport service exists — with 1.3's caching, a misconfigured topic surfaces at the first publish of the process, which is even later than before.NotFound on your first dispatch is infrastructure, not your message class. Prefer Terraform/gcloud provisioning over hoping for auto-creation (and check GpsReceiver's behavior — some revisions create the subscription lazily, some don't).GOOGLE_APPLICATION_CREDENTIALS or pass an explicit client config path. Watch file permissions inside containers and avoid committing the JSON.--sleep, and some rate-limiting options, behave differently than with AMQP/Redis because the receiver blocks on pull(). Tune batching/max-messages in the transport options if throughput is a concern; use --time-limit plus supervisor restart to bound memory growth.framework.messenger.reset_on_message: true so Doctrine connections and other services are reset, and remember Doctrine connections die over long worker lifetimes regardless.messenger:consume gps -vvv for handler-level detail, gcloud pubsub subscriptions pull orders-worker --auto-ack --limit=5 to inspect raw payloads and attributes, and GCP's Pub/Sub metrics for oldest_unacked_message_age (a rising value means your workers are down or wedged). With batching on, also watch publish latency and batch-failure counts — a stuck buffer looks identical to "nothing is being dispatched."GpsReceiver to add logging/metrics around ack/nack, replace the transport-factory service to inject a custom PubSubClient (useful for publish batching tuning or emulator support), and override the serializer on the transport if you need a custom envelope format. If 1.3's batching doesn't fit your flush semantics, subclassing GpsSender is now the natural seam.failure_transport and a messenger:failed:show/retry runbook; without it, poison messages cycle until Pub/Sub's own redelivery gives up and they land in whatever dead-letter policy you (hopefully) configured at the subscription level.How can I help you explore Laravel packages today?