setono / client-bundle
Integrate the client library into your Symfony application
Requires
- php: >=8.1
- doctrine/dbal: ^2.13 || ^3.0 || ^4.0
- doctrine/doctrine-bundle: ^2.7
- doctrine/orm: ^2.0 || ^3.0
- doctrine/persistence: ^2.5 || ^3.0
- psr/event-dispatcher: ^1.0
- setono/client: ^1.1.1
- setono/doctrine-orm-trait: ^1.1
- symfony/config: ^6.4 || ^7.0
- symfony/dependency-injection: ^6.4 || ^7.0
- symfony/event-dispatcher: ^6.4 || ^7.0
- symfony/http-foundation: ^6.4 || ^7.0
- symfony/http-kernel: ^6.4 || ^7.0
- symfony/uid: ^6.4 || ^7.0
- symfony/var-exporter: ^6.4 || ^7.0
Requires (Dev)
- ergebnis/composer-normalize: ^2.50
- infection/infection: ^0.27 || ^0.28
- jangregor/phpstan-prophecy: ^2.3
- matthiasnoback/symfony-dependency-injection-test: ^4.3.1 || ^5.1
- phpspec/prophecy: ^1.20
- phpspec/prophecy-phpunit: ^2.5
- phpstan/extension-installer: ^1.4.3
- phpstan/phpstan: ^2.1.46
- phpstan/phpstan-doctrine: ^2.0
- phpstan/phpstan-phpunit: ^2.0.16
- phpstan/phpstan-strict-rules: ^2.0.10
- phpstan/phpstan-symfony: ^2.0
- phpstan/phpstan-webmozart-assert: ^2.0
- phpunit/phpunit: ^10.5
- rector/rector: ^2.4.1
- shipmonk/composer-dependency-analyser: ^1.8.4
- sylius-labs/coding-standard: ^4.5
This package is auto-updated.
Last update: 2026-06-15 13:31:44 UTC
README
Recognize returning visitors in your Symfony application and attach your own metadata to each of them.
The bundle assigns every visitor a stable client id, stored in a first‑party cookie (setono_client_id by
default) that also remembers when the visitor was first and last seen. On top of that you can persist arbitrary
metadata per client — for example the first Google click id (gclid) a visitor arrived with, an A/B test variant,
or a referrer. Metadata is lazy: if you never read or write it, the bundle never touches the database.
Typical use cases: first‑party analytics, marketing attribution, returning‑visitor personalization.
Contents
- Requirements
- Installation
- Usage
- Configuration
- How it works
- Extending the bundle
- Contributing
- License
Requirements
| Version | |
|---|---|
| PHP | >= 8.1 |
| Symfony | ^6.4 or ^7.0 |
| Doctrine ORM | ^2.0 or ^3.0 |
Installation
composer require setono/client-bundle
If you use Symfony Flex the bundle is registered automatically.
Otherwise, enable it manually in config/bundles.php:
return [ // ... Setono\ClientBundle\SetonoClientBundle::class => ['all' => true], ];
The bundle maps a Metadata entity (table setono_client__metadata). If you intend to store metadata, create the
table with Doctrine Migrations:
php bin/console doctrine:migrations:diff php bin/console doctrine:migrations:migrate
If you only need the client id cookie and never store metadata, the table is never queried — but generating it now keeps things simple if you add metadata later.
Usage
Get the current client
The simplest way to access the current visitor is to type‑hint Setono\Client\Client in a controller — the bundle
resolves it for you:
use Setono\Client\Client; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; final class HomeController extends AbstractController { public function index(Client $client): Response { return $this->render('home.html.twig', [ 'clientId' => $client->id, ]); } }
Anywhere else (services, subscribers) inject ClientContextInterface:
use Setono\ClientBundle\Context\ClientContextInterface; final class SomeService { public function __construct(private readonly ClientContextInterface $clientContext) { } public function __invoke(): void { $client = $this->clientContext->getClient(); // $client->id, $client->metadata } }
Read and write metadata
$client->metadata is a key/value store. Reading a key for the first time lazily loads the metadata from the
database; writing marks it dirty so it is persisted at the end of the request.
$metadata = $client->metadata; $metadata->set('variant', 'B'); // store a value $metadata->set('promo', 'X', ttl: 3600); // optional TTL in seconds $metadata->has('variant'); // true $metadata->get('variant'); // 'B' — throws if the key is missing, so guard with has() $metadata->remove('variant'); foreach ($metadata as $key => $value) { // iterate everything // ... }
Set metadata from a request
A common pattern is to capture data from the incoming request — here, the Google click id:
use Setono\ClientBundle\Context\ClientContextInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\KernelEvents; final class GoogleClickIdSubscriber implements EventSubscriberInterface { public function __construct(private readonly ClientContextInterface $clientContext) { } public static function getSubscribedEvents(): array { return [KernelEvents::REQUEST => 'capture']; } public function capture(RequestEvent $event): void { if (!$event->isMainRequest() || !$event->getRequest()->query->has('gclid')) { return; } $this->clientContext->getClient()->metadata->set( 'google_click_id', $event->getRequest()->query->get('gclid'), ); } }
Access the cookie directly
The cookie stores the client id plus the first/last seen timestamps. Read it through CookieProviderInterface:
use Setono\ClientBundle\CookieProvider\CookieProviderInterface; final class SomeService { public function __construct(private readonly CookieProviderInterface $cookieProvider) { } public function __invoke(): void { $cookie = $this->cookieProvider->getCookie(); if (null === $cookie) { return; // visitor has no cookie yet } $cookie->clientId; // the client id $cookie->firstSeenAt; // unix timestamp of first visit $cookie->lastSeenAt; // unix timestamp of the previous visit } }
The cookie is intentionally not HttpOnly, so you can also read it from JavaScript (e.g. to send the client id
to a third‑party tag).
Skip storing the cookie (consent)
Because the cookie identifies a visitor, you may need consent before storing it. Listen to PreStoreCookieEvent
(dispatched on the response, before the cookie is written) and set $event->store = false to skip it for that
request:
use Setono\ClientBundle\Event\PreStoreCookieEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; final class CookieConsentSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [PreStoreCookieEvent::class => 'onPreStore']; } public function onPreStore(PreStoreCookieEvent $event): void { if (!$this->hasConsent($event->request)) { $event->store = false; } } // ... }
Configuration
All options are optional; the defaults are shown below:
setono_client: cookie: # Name of the cookie that holds the client id. # NOTE: changing this makes every visitor with the old cookie name look like a new client. name: setono_client_id # Cookie lifetime, expressed as any string strtotime() can parse. expiration: '+365 days' # Entity used to persist metadata. Override it with your own class to add mapped fields # or behaviour; it must implement Setono\ClientBundle\Entity\MetadataInterface. metadata_class: Setono\ClientBundle\Entity\Metadata
How it works
- Cookie — on each main response,
StoreCookieSubscriberwrites/refreshes the cookie and bumpslastSeenAt. A new client id is generated only when no valid cookie is present. - Lazy metadata —
$client->metadatais a lazy ghost object. The database is queried only the first time you read a value, and a row is written only at the end of the request, and only if the metadata was actually changed. - Resolution chain —
ClientContextInterfaceis built from a chain of decorating services (CachedClientContext→CookieBasedClientContext→DefaultClientContext). The cached layer memoizes the client per request; the cookie‑based layer builds it from the cookie; the default layer creates a fresh, anonymous client.
Extending the bundle
Every moving part is a service behind an interface, so you can swap or decorate it:
| Concern | Interface |
|---|---|
| Resolving the current client | Setono\ClientBundle\Context\ClientContextInterface |
| Reading the cookie | Setono\ClientBundle\CookieProvider\CookieProviderInterface |
| Loading metadata | Setono\ClientBundle\MetadataProvider\MetadataProviderInterface |
| Persisting metadata | Setono\ClientBundle\MetadataPersister\MetadataPersisterInterface |
| Metadata entity | Setono\ClientBundle\Entity\MetadataInterface |
To add behaviour, decorate the relevant service rather than replacing it outright — that is exactly how the bundle composes its own defaults.
Contributing
git clone https://github.com/Setono/client-bundle.git
cd client-bundle
composer install
Quality tooling (also run in CI):
| Command | Purpose |
|---|---|
composer phpunit |
Run the test suite |
composer analyse |
Static analysis (PHPStan, level: max) |
composer check-style / composer fix-style |
Coding standard (ECS) |
composer infection |
Mutation testing (requires a coverage driver) |
vendor/bin/composer-dependency-analyser |
Detect unused/undeclared dependencies |
License
This bundle is released under the MIT License.