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

Client Id Bundle Laravel Package

setono/client-id-bundle

A Symfony bundle that generates and persists a unique client ID, helping identify anonymous visitors across requests for analytics, tracking, or personalization without requiring users to log in.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and enable the bundle; there is no Flex recipe for a package this small, so registration is manual:

composer require setono/client-id-bundle
// config/bundles.php
return [
    // ...
    Setono\ClientIdBundle\SetonoClientIdBundle::class => ['all' => true],
];

The bundle's job is narrow: give you a stable, server-generated visitor ID and persist it in a first-party cookie. Start by finding the two extension points in the source:

  1. src/ClientId/ — the ClientIdProviderInterface (and its default implementation). This is what you inject everywhere.
  2. src/EventListener/ (or src/EventSubscriber/) — the request/response listener that reads the cookie and writes a new one when absent.
  3. src/DependencyInjection/Configuration.php — the source of truth for config keys.

Dump the reference before writing config, since the tree is small and keys are easy to guess wrong:

bin/console config:dump-reference setono_client_id

A typical configuration looks like this (confirm names against the dump):

# config/packages/setono_client_id.yaml
setono_client_id:
    cookie_name: setono_client_id
    cookie_lifetime: 31536000   # one year
    cookie_secure: auto
    cookie_http_only: true
    cookie_same_site: lax

As of v0.2.3 there is also a compatibility layer with setono/client-bundle. If your app already pulls that bundle in (common when you use other Setono tracking packages), this bundle now bridges its ClientId concept to the shared one, so a single visitor ID flows through both. Check what actually got wired rather than assuming:

bin/console debug:container ClientIdProviderInterface
bin/console debug:container 'Setono\ClientBundle\ClientId\*'   # if client-bundle is installed

If the aliases resolve to the same underlying service, you are done — new code can type-hint whichever interface your other Setono packages already use. If client-bundle is not installed the compat layer is inert and nothing changes for you.

The first real use case is server-side analytics — feeding GA4 Measurement Protocol, Facebook CAPI, or your own event store with a client_id that does not depend on JavaScript:

use Setono\ClientIdBundle\ClientId\ClientIdProviderInterface;

final class MeasurementProtocolClient
{
    public function __construct(
        private ClientIdProviderInterface $clientIdProvider,
        private HttpClientInterface $httpClient,
    ) {}

    public function track(string $eventName, array $params): void
    {
        $this->httpClient->post('https://www.google-analytics.com/mp/collect?api_secret=...&measurement_id=...', [
            'json' => [
                'client_id' => (string) $this->clientIdProvider->getClientId(),
                'events' => [['name' => $eventName, 'params' => $params]],
            ],
        ]);
    }
}

If the service autowires, you are done. If not, alias the interface to the bundle's default implementation in services.yaml — but see the gotcha below about the compat layer and alias ordering before you override anything.

Implementation Patterns

Inject the provider, not the cookie. Treat ClientIdProviderInterface as the only API. It handles "read existing cookie or mint a new one" so your tracking services never touch $_COOKIE or Request directly. Decorate the interface if you need a different generation strategy (UUIDv7, ULID, or adoption of an existing _ga value).

Pick one interface and standardise on it (new in 0.2.3). With the setono/client-bundle bridge in place there are now potentially two contracts pointing at the same value. Sharing code with other Setono packages is easier if you type-hint the client-bundle interface; staying self-contained is easier if you keep Setono\ClientIdBundle\ClientId\ClientIdProviderInterface. Either way, do not mix both across your codebase — you will get two decorators registered against two different service IDs and wonder why your GA-aware provider is bypassed on half the call sites.

Reconcile with client-side IDs. If you already run GA4 via gtag, the browser holds a _ga cookie of the form GA1.1.<client_id>. A decorator can parse it and return that value, so server-side and client-side hits land on the same user:

final class GaCookieAwareProvider implements ClientIdProviderInterface
{
    public function __construct(
        private ClientIdProviderInterface $inner,
        private RequestStack $requestStack,
    ) {}

    public function getClientId(): ClientId
    {
        $ga = $this->requestStack->getMainRequest()?->cookies->get('_ga');
        if (is_string($ga) && preg_match('/^GA\d+\.\d+\.(.+)$/', $ga, $m)) {
            return new ClientId($m[1]);
        }
        return $this->inner->getClientId();
    }
}

Register it as a decorator of the bundle's service. With the compat layer active, decorate the shared service ID so both interfaces see your decorator.

Keep tracking off the request path. Sending Measurement Protocol or CAPI calls inline adds latency to every page view. Dispatch a Messenger message carrying the client ID string and do the HTTP call in a worker. The ID is just a string — snapshot it, don't serialize the provider.

Respect consent. Generate and set the cookie only when consent allows it. Wrap the provider in a consent-aware decorator that throws or returns a null-object ID when consent is missing, and check that the bundle's listener does not write the cookie unconditionally. This is the most common compliance failure with server-side ID packages. If you decorate now, make sure the decorator sits outside the compat bridge, not underneath it, or the other bundle will still read the raw ID.

Use it as a correlation key, not a user account. The value is stable per browser/cookie jar. Store it alongside your own events table so you can join server-side purchases back to sessions, but never treat it as an authenticated identity.

Testing. In unit tests, mock the interface. In functional tests, use KernelBrowser and assert the Set-Cookie header on the first response and that the provider returns the same value on a second request that replays the cookie. Add one test that asserts both interfaces (this bundle's and client-bundle's, if installed) return an identical value — that is the contract the 0.2.3 compat layer promises, and it is the thing most likely to regress in a future minor.

Gotchas and Tips

  • Bundle registration order matters for the compat layer. The 0.2.3 bridge is conditional on setono/client-bundle being present. If you register bundles in an unusual order, or you enable SetonoClientIdBundle in one kernel and SetonoClientBundle in another (e.g. a separate admin kernel), the alias may resolve differently per kernel. Keep debug:container output from each kernel in your notes.
  • The bridge can silently win over your alias. If you previously aliased ClientIdProviderInterface to your own implementation in services.yaml, and the compat layer also registers an alias, the last one loaded wins. Load your overrides after the bundle config, or use a decorator instead of an alias.
  • Pin the version if you depend on the interop. The compat layer is one release old in a pre-1.0 package. Use "setono/client-id-bundle": "^0.2.3" and keep an integration test asserting the cross-bundle ID equality so a future refactor cannot quietly break attribution.
  • The ID must exist on the first request. Check whether the provider generates lazily (good — available in the same request) or only reads what the listener wrote on a previous request (bad — first page view has no ID). Read ClientIdProvider before building on it.
  • Response listener ordering. The cookie is written on kernel.response. If you short-circuit with a streamed response, an early return new JsonResponse(...) from a listener at higher priority, or work in kernel.terminate, the Set-Cookie header may never reach the client.
  • HTTP caching will bite you. Any response carrying Set-Cookie is effectively uncacheable and can poison a shared cache (Varnish, Cloudflare, Symfony HttpCache). Either exclude the cookie-setting response from caching, add Vary: Cookie, or set the cookie only on HTML navigations.
  • Cookie flags behind a proxy. secure: auto depends on Request::isSecure(). Without trusted_proxies / trusted_headers configured for your load balancer, the cookie silently loses Secure and may be rejected in modern browsers.
  • Subdomains. If your funnel spans www. and app., set the cookie domain to .example.com, otherwise each subdomain mints a separate client ID and your funnel attribution breaks. This matters double once client-bundle is in play, because two packages reading two different cookies is harder to spot than one.
  • SameSite trade-off. Lax (the sane default) means the cookie is not sent on cross-site POSTs. If you receive server-to-server callbacks or cross-origin form posts that need the ID, you need None; Secure and a real HTTPS setup.
  • Bots and crawlers get IDs too. If the listener is unconditional, every crawler hit generates a cookie and pollutes your visitor counts. Filter on user agent or only issue IDs on routes you care about.
  • Debugging checklist. No Set-Cookie in the response → listener not registered or response short-circuited. Cookie present but ID changes each request → wrong cookie name/domain/path, or the provider is generating instead of reading. ID differs between PHP-FPM and CLI → expected; never compare across contexts. ID differs between this bundle's provider and client-bundle's after 0.2.3 → the compat alias is not registered, or your own decorator is only attached to one side.
  • Extension points are thin by design. There is no event system for "client ID generated" — if you need one, decorate the provider or subscribe to kernel.response yourself and compare against the request cookie.
  • Low adoption means you are the documentation. With a single star and zero dependents, expect no recipe, no docs site, and API churn between minor versions — the 0.2.3 compat layer is itself an unannounced-in-docs integration surface. Pin the version in composer.json, read the source once end-to-end (it is small), and add an integration test that asserts the cookie contract so an upgrade cannot silently break tracking.
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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console