webmunkeez / context-bundle
Context storage for Symfony, backed by cookies.
Package info
github.com/yannissgarra/context-bundle
Type:symfony-bundle
pkg:composer/webmunkeez/context-bundle
Requires
- php: >=8.2
- firebase/php-jwt: ^7.0
- symfony/config: ^7.4
- symfony/dependency-injection: ^7.4
- symfony/http-foundation: ^7.4
- symfony/http-kernel: ^7.4
- symfony/property-access: ^7.4
- symfony/property-info: ^7.4
- symfony/serializer: ^7.4
- symfony/string: ^7.4
Requires (Dev)
This package is auto-updated.
Last update: 2026-08-21 14:00:50 UTC
README
This bundle brings cookie-backed context storage to Symfony applications — persisting small pieces of state (e.g. an anonymous visitor's data) across requests without authentication.
Installation
Use Composer to install this bundle:
$ composer require webmunkeez/context-bundle
Add the bundle in your application kernel:
// config/bundles.php return [ // ... Webmunkeez\ContextBundle\WebmunkeezContextBundle::class => ['all' => true], // ... ];
Usage
Writing a context
A context implements \Webmunkeez\ContextBundle\Context\ContextInterface — in practice you extend \Webmunkeez\ContextBundle\Context\AbstractContext, which derives the context's reference from its class name (ProfileContext → profile, used both as the request attribute key and the cookie name):
final class ProfileContext extends AbstractContext { /** * @var array<Profile> */ private array $profiles = []; /** * @return array<Profile> */ public function getProfiles(): array { return $this->profiles; } /** * @param array<Profile> $profiles */ public function setProfiles(array $profiles): self { $this->profiles = $profiles; return $this; } public function getHash(): string { return hash('xxh128', serialize($this->profiles)); } }
getHash() is used to detect whether a context changed since it was read — it must return the same value for two contexts that should be considered equal, and a different value otherwise.
Reading and writing a context
\Webmunkeez\ContextBundle\Context\ContextProviderInterface (autowireable, backed by ContextProvider) is the entry point:
final class ProfileController { public function __construct( private readonly ContextProviderInterface $contextProvider, ) { } public function __invoke(Request $request): Response { $context = $this->contextProvider->get(ProfileContext::class); // ... mutate $context ... $this->contextProvider->update($context); // ... } }
get(string $contextClass): ContextInterfacedenormalizes the context from the current request, or returns a freshnew $contextClass()if none was stored yet.update(ContextInterface $context): voidcompares the given context'sgetHash()against the currently stored one and, only if it changed, normalizes it and marks it for persistence.
How it's wired
Two kernel.event_listeners do the actual cookie I/O so the rest of the request lifecycle only ever deals with request attributes:
ContextRequestListener(kernel.request) reads every cookie named{reference}_context, verifies and decodes its JWT viaTokenEncoderInterface, and copies the resulting array payload into thecontext.{reference}request attribute. A cookie that fails to decode — malformed, expired, or signed with a different secret — is silently ignored, exactly as if it had never been set.ContextProvider::get()lazily denormalizes that attribute (a plain array, via Symfony'sNormalizerInterface/DenormalizerInterface) into the requested context class;update()normalizes the context back to an array and flagscontext.{reference}.refreshastrueonly when the hash actually changed (write-on-change).ContextResponseListener(kernel.response, main request only) looks forcontext.{reference}.refresh === trueand, when found, encodes the array payload viaTokenEncoderInterfaceand writes the result as the{reference}_contextcookie (HttpOnly,SameSite=Lax,Securewhen the request itself is HTTPS, expiring after the configuredttl).
Cookie signing
\Webmunkeez\ContextBundle\Token\TokenEncoderInterface is a generic array-payload-to-string-token codec — it knows nothing about contexts. The cookie content is a JWT (backed by \Webmunkeez\ContextBundle\Jwt\JwtTokenEncoder and firebase/php-jwt), signed with HS256 and carrying an exp claim matching the cookie's lifetime. The context's normalized array is embedded directly as the data claim — not JSON-encoded twice — keeping the cookie as small as the data actually requires. This guarantees integrity — a tampered or forged cookie is rejected — but not confidentiality: the payload is base64url-encoded, not encrypted, so it's still readable by anyone with the cookie value. Don't store anything sensitive in a context.
Configuration
# config/packages/webmunkeez_context.yaml webmunkeez_context: secret: '%env(CONTEXT_SECRET)%' # defaults to kernel.secret ttl: '1 year' # this is the default
secretis the JWT signing key. It must be at least 32 characters long (HS256 requires a 256-bit key) orJwtTokenEncoder::encode()throws a\DomainException.ttlis a relative date/time string (anything accepted bystrtotime('+'.$ttl), e.g.'30 days','2 weeks') used both for the JWT'sexpclaim and the cookie'sExpiresattribute, so they always stay in sync.