snowplow/referer-parser
Referer-parser for PHP identifies where your traffic comes from by parsing referer URLs. It detects search engines, social networks, email and other sources, extracting search terms and medium details so you can classify visits and attribute conversions accurately.
Install the package and parse your first referrer in under a minute:
composer require snowplow/referer-parser
use Snowplow\RefererParser\Parser;
$parser = new Parser();
$referer = $parser->parse(
$_SERVER['HTTP_REFERER'] ?? '',
'https://example.com/pricing' // current page URL (optional but recommended)
);
$referer->isKnown(); // true if the source is recognised
$referer->getMedium(); // "search", "social", "internal", "unknown", ...
$referer->getSource(); // "Google", "Facebook", "Twitter", ...
$referer->getTerm(); // "gateway oracle cards" (search term, if present)
Where to look first:
README.md — canonical usage and method signatures.src/Parser.php — the parse($referrerUrl, $pageUrl) entry point and construction options.src/Referer.php — the result object (isKnown, getSource, getMedium, getTerm, getCampaign, getSearchEngine).references/referers.yml (or the bundled YAML data file) — the actual source list; this is your extension point.tests/ — fixtures showing expected output for known/unknown/campaign URLs.First real use case: attribution capture on an incoming HTTP request. Register Parser as a singleton in AppServiceProvider, then call parse() inside a middleware or a registration/conversion handler and persist source, medium, and term alongside the user or order. That single hook answers "where did this visitor come from?" for the rest of the app.
Bind the parser as a singleton. It reads the YAML reference data on construction, so build it once and let the container share it:
// app/Providers/AppServiceProvider.php
use Snowplow\RefererParser\Parser;
public function register(): void
{
$this->app->singleton(Parser::class, fn () => new Parser());
}
Middleware for first-touch attribution. Capture on the first request that has a referrer, then freeze it in the session (or write to a dedicated column so it survives session loss):
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Snowplow\RefererParser\Parser;
class CaptureAttribution
{
public function __construct(private Parser $parser) {}
public function handle(Request $request, Closure $next)
{
if (! $request->session()->has('attribution')) {
$result = $this->parser->parse(
$request->headers->get('referer', ''),
$request->fullUrl()
);
$request->session()->put('attribution', [
'known' => $result->isKnown(),
'source' => $result->getSource(),
'medium' => $result->getMedium(),
'term' => $result->getTerm(),
]);
}
return $next($request);
}
}
Campaign parameters are parsed from the page URL, not the referrer. When a user lands on ?utm_source=newsletter&utm_medium=email&utm_campaign=spring, pass that URL as the second argument; the campaign object exposes the UTM fields:
$result = $parser->parse($request->headers->get('referer', ''), $request->fullUrl());
$campaign = $result->getCampaign();
if ($campaign) {
$campaign->getSource(); // newsletter
$campaign->getMedium(); // email
$campaign->getName(); // spring (utm_campaign)
$campaign->getTerm(); // utm_term
$campaign->getContent(); // utm_content
}
Treat UTM/campaign data as higher priority than referrer-derived data in your attribution model — it is explicit intent from the marketer, whereas the referrer header is a browser hint.
Persist a normalized row rather than echoing raw headers. A small attributions table (source, medium, term, campaign_source, campaign_medium, campaign_name, raw_referer, landing_url, created_at) gives you something queryable for reporting and keeps PII-laden raw URLs out of analytics events.
Enrich analytics events. If you ship events to Snowplow (the package's origin), map the parsed fields to refr_source, refr_medium, refr_term, mkt_source, mkt_medium, mkt_campaign. Doing it once in a shared event transformer keeps all downstream events consistent.
Test against a fixture set. Snapshot a handful of real-world URLs (Google search, a social profile, a direct hit with no referrer, an internal link, a UTM-tagged landing page) and assert exact outputs. This protects you when reference data changes on upgrade:
public function test_google_search_is_attributed(): void
{
$parser = new Parser();
$result = $parser->parse(
'https://www.google.com/search?q=laravel+queues',
'https://example.com/'
);
$this->assertTrue($result->isKnown());
$this->assertSame('search', $result->getMedium());
$this->assertSame('Google', $result->getSource());
}
Custom reference data for niche sources. If your marketing team needs partner domains the bundled list doesn't cover, pass a custom YAML path to the Parser constructor and add your own entries rather than maintaining regex post-processing.
HTTP_REFERER is frequently absent. Direct navigation, bookmarks, mobile apps, and strict referrer policies all produce an empty string. isKnown() returns false and getMedium() typically yields unknown — don't treat that as an error, treat it as a valid bucket ("direct").https://example.com/other-page is same-site traffic. Skipping the second argument means internal navigation gets misclassified instead of flagged as internal.Referrer-Policy: strict-origin-when-cross-origin (the default in Chrome/Firefox), cross-origin referrers arrive as bare origins — so getTerm() is usually empty and search-engine detection can degrade. Set a permissive Referrer-Policy if you genuinely need terms, and never make business logic depend on them.https://), strip fragments, and decode as needed. A malformed URL may throw or silently miss — wrap the call in a try/catch at ingestion boundaries (webhooks, imported data) and fall back to an "unknown" record.Google.com vs google.com and a stray trailing dot can change results. Lowercase hostnames upstream.isKnown() false-negative rates in staging before assuming a bug — the fix is usually extending the YAML, not the code.getTerm() on non-search sources. Only treat it as a keyword when the medium is search; social and internal sources will leave it null or meaningless.Parser if you need to add post-processing (e.g., mapping vendor names to internal channel labels). Keeping that mapping outside the package keeps upgrades painless.How can I help you explore Laravel packages today?