tourze/symfony-utm-bundle
A Symfony bundle for UTM parameter tracking that helps capture campaign query parameters and persist attribution data across requests, making it easier to integrate marketing analytics and conversion tracking into Symfony applications.
tourze/symfony-utm-bundle is a Symfony bundle, not a Laravel package — that distinction drives everything below. Its job is capturing UTM query parameters (utm_source, utm_medium, utm_campaign, utm_term, utm_content) on inbound requests and making them available later in the request/session lifecycle for attribution.
composer require tourze/symfony-utm-bundle
In a Symfony app: register the bundle in config/bundles.php (there's no Flex recipe, so it's manual), then check vendor/tourze/symfony-utm-bundle/DependencyInjection/Configuration.php for the config tree and add config/packages/tourze_utm.yaml.
In a Laravel app: Laravel's package discovery only looks at extra.laravel.providers. A class extending Symfony's Bundle will not be auto-registered, and the bundle's kernel.request listener will never fire under Laravel's HTTP kernel. Treat this as a reference implementation: read the subscriber class and port the ~40 lines into a middleware, or (if you're on a hybrid/Symfony-based stack) wire the listener manually in a service provider.
Where to look first, in order:
kernel.request — that's where capture happens.DependencyInjection/Configuration.php — the only real documentation of available knobs.First use case: a marketing landing page at /pricing?utm_source=newsletter&utm_campaign=spring24. Capture on entry, persist, then read the values when the visitor converts (form submit, signup, order) and write them onto the lead/order record. That single flow is 90% of real-world usage.
Capture → persist → read at conversion. Keep these three phases explicit in your code. Capture belongs in middleware/subscriber (before your controller), persistence belongs in a dedicated service, and reading happens in your form handler or model observer.
First-touch vs. last-touch. Decide this deliberately, because it changes your schema:
Middleware ordering (Laravel). Register the capture middleware in the web group before StartSession, Authenticate, and anything that issues redirects — otherwise a login redirect or locale redirect swallows the query string. In Symfony, give the listener a priority high enough to beat firewall/redirect listeners.
Laravel middleware shape (porting the bundle's behaviour):
public function handle($request, Closure $next)
{
if ($request->hasAny(['utm_source', 'utm_medium', 'utm_campaign'])) {
if (! $request->session()->has('utm')) {
$request->session()->put('utm', $request->only([
'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
]));
}
}
return $next($request);
}
Carry UTMs across multi-step forms. Session survives same-site steps but not a redirect to an external payment domain. Stash the array into a hidden field or a meta JSON column when the flow starts.
Don't fork the bundle to change storage. If the values are held by a named service, decorate it (Symfony's decorates: / a Laravel binding swap) rather than vendoring the class — you'll want to upgrade when 1.1.x patches land.
Test it like a behaviour, not a class. Assert on the session/cookie after hitting a URL with params, and assert that a second request without params doesn't clear it:
$this->get('/?utm_source=google&utm_campaign=spring')
->assertSessionHas('utm.utm_source', 'google');
config/utm.php, no facades. If you're in Laravel, budget a half-day to port rather than hours debugging why nothing fires.compare/1.0.1...1.1.0 before bumping production — small single-maintainer bundles occasionally slip behaviour changes into minor versions. Pin ^1.0.1 first, test, then move.Vary/bypass) or capture client-side and post it back.www. and checkout on shop. means you need domain: .example.com or the attribution vanishes at the most important step.utm_source=Google vs google will fragment your reports forever. Lowercase and trim; whitelist known values if you can.debug:event-dispatcher in Symfony, route:list -v for middleware in Laravel), then check event priority — silent no-ops are almost always registration or ordering, not parsing.attribution column) on every convertible model before you ship, so historical reporting isn't a backfill project later.How can I help you explore Laravel packages today?