Download the PHP package rasuvaeff/yii3-utm without Composer
On this page you can find all versions of the php package rasuvaeff/yii3-utm. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download rasuvaeff/yii3-utm
More information about rasuvaeff/yii3-utm
Files in rasuvaeff/yii3-utm
Package yii3-utm
Short Description UTM capture and attribution for Yii3: touchpoint history in one cookie, click-id support, consent-gated middleware and an append-only attribution journal
License BSD-3-Clause
Homepage https://github.com/rasuvaeff/yii3-utm
Informations about the package yii3-utm
rasuvaeff/yii3-utm
Marketing attribution for Yii3 applications: capture UTM parameters, click identifiers and referrers, keep a short touchpoint history, and record an append-only attribution journal for registrations, purchases and any other business event.
Using an AI coding assistant? llms.txt is a compact API reference written for LLMs. The package also ships an agent skill through llm/skills.
Status: feature-complete. Use rasuvaeff/yii3-utm-db for portable
yiisoft/db persistence, provide an application repository, or use the
shipped in-memory implementation in tests.
Requirements
- PHP 8.3 – 8.5
ext-json,ext-mbstring
Installation
Why a journal and not a column
Between an ad click and a purchase there are days and several visits. A single "current UTM" column answers the wrong question. This package keeps the last touchpoints and, when a business event happens, writes one row per touchpoint, so first-touch, last-touch and multi-touch models are all answerable later.
Three invariants shape the whole API:
- Everything the client sends — query, headers, body, cookies,
localStorage— is untrusted. Values are normalised, truncated or dropped, never authenticated. - The journal is append-only and ordered by the server. A client cannot make a late delivery become the first touch.
- Deduplication happens per touchpoint within one business event, so a retry of the same event writes nothing new while a genuinely new event does.
Usage
Campaign parameters
UtmParameters is the campaign tuple: the five standard utm_* fields plus
GA4 utm_id. Factories normalise untrusted input — control characters are
stripped, values trimmed and truncated to 255 characters, empty strings become
null.
Click identifiers
Auto-tagging platforms attach a click id and no utm_* at all — Google Ads
sends a bare gclid. ClickIds accepts only whitelisted keys, in whitelist
order, and caps its serialised length at the storage column width.
Supported keys: gclid, gbraid, wbraid, fbclid, yclid, ttclid,
msclkid, li_fat_id, twclid.
Touchpoints and history
A UtmTouchpoint is one contact: campaign tuple, click ids, referrer, landing
page and the timestamp the source claims. UtmHistory keeps them newest first.
| Method | Behaviour |
|---|---|
UtmHistory::of(...$touchpoints) |
Sorts newest first; ties broken deterministically |
with(UtmTouchpoint) |
Returns a new history with the touchpoint added |
deduplicated(UtmSimilarity) |
Collapses similar touchpoints, keeping the oldest of each group |
limited(int) |
Keeps at most N newest touchpoints |
latest() / oldest() / all() / count() / isEmpty() |
Read accessors |
UtmSimilarity decides what "similar" means: Full (campaign tuple and click
ids), Campaign (source, medium, campaign) or SourceMedium.
Interaction types
Which business events exist is the application's decision, so the type is a validated string, not an enum:
Channel classification
Channel is derived on read and deliberately not stored — classification rules
change more often than a major release allows.
Rule order: click id → utm_medium → referrer host. Vocabularies (paid, email
and social mediums, social and search hosts) are constructor arguments.
Capture
One middleware; the transports it understands are configuration, not separate classes.
The attributes are always set, so downstream code never distinguishes "the middleware did not run" from "nothing was captured".
| Transport | Source | Use for |
|---|---|---|
| Query string | QueryUtmSource |
Server-rendered pages; landing page and Referer are captured too |
X-Utm-* headers |
HeaderUtmSource |
SPA and API clients; click ids use JSON in X-Utm-Click-Ids |
Nested utm body key |
BodyUtmSource |
SPA and API; the recommended cross-domain transport |
All three sources drop a referrer that matches the current request's own host
(Referrer::external(), not Referrer::of()): navigating from one page of
your site to another is not a touchpoint to attribute the visit to.
History lives in a single cookie (utm_history by default) encoded by
UtmCookieCodec: HttpOnly, Secure, SameSite=Lax, 30 days. A client
profile (httpOnly: false) exists for same-domain SPA reads and is spoofable by
definition. DefaultLandingPageSanitizer — the shipped implementation — keeps scheme, host,
port and path, drops the fragment and every query parameter outside its
allow-list (utm_* and click ids by default), and truncates to 500 characters.
The cookie is treated as untrusted input on the way in as well: the codec
runs the referrer and the landing page of a decoded entry through the same
sanitizer, so a hand-edited cookie cannot inject a javascript: URL or an
unsanitised landing page into the history. Referrer::of() accepts http and
https only. Pass your own sanitizer to UtmCookieCodec when you configure a
custom allow-list — the shipped config/di.php already does.
UtmCookieCodec::$maxLength (3500 by default) is the size of the
percent-encoded value, which is what Set-Cookie carries: the codec drops
the oldest touchpoints until the encoded value fits, leaving room for the cookie
name and its attributes inside the 4096-byte browser limit.
NullUtmHistoryStore stores nothing — the right choice for
stateless APIs and cacheable routes, since capture otherwise adds a
Set-Cookie header and makes a response uncacheable.
The cookie is written only when the history changes, which is what keeps an
unchanged response free of Set-Cookie and cacheable — and what makes ttlDays
count from the last touchpoint, not from the last visit. A visitor who
returns daily through a direct link gets no new Set-Cookie, so the attribution
window closes 30 days after the last campaign touch even though the visitor
never left. Refreshing the cookie on a plain visit would need a "written at"
stamp in the cookie payload and would put Set-Cookie on responses that are
cacheable today; raise ttlDays if a longer window is what you need.
| Option | Default | Effect |
|---|---|---|
enabled |
true |
Master switch |
ignoredPaths |
[] |
Paths to skip, matched on a segment boundary: /api skips /api and /api/v1, but not /api-docs |
similarity |
Full |
What counts as "the same campaign" |
updateExisting |
false |
Whether a touchpoint similar to the newest stored one is appended |
captureOrganic |
false |
Whether a visit with neither campaign nor click id becomes a touchpoint |
maxTouchpoints |
5 |
History cap |
maxTouchpointAge |
90 days | Retention window for a claimed occurredAt: a future claim is capped to now, an older one produces no touchpoint and drops a stored one |
clearHistoryWithoutConsent |
false |
Whether a stored history is expired when consent is absent |
Consent
ConsentPolicy::allowsPersistence() gates the whole thing: without consent
nothing is read and nothing is written. The default is AllowAllConsentPolicy
— for applications where consent is enforced earlier in the stack.
The method name matches rasuvaeff/yii3-ab-testing-web, so an application that
already has a policy reuses it in one line.
Configuration
The package ships config/di.php and config/params.php for
yiisoft/config. It binds the capture stack, the codec, the sanitizer, the
channel resolver and the consent default — and deliberately not
UtmAttributionRepository, which must come from exactly one source.
The rasuvaeff/yii3-utm params group exposes:
capture.sources.query.utmKeysandclickIdKeys;capture.sources.header.prefixandclickIdKeys;capture.sources.body.keyandclickIdKeys;sanitizer.allowedQueryKeysandmaxLength;channel.paidMediums,emailMediums,socialMediums,socialHostsandsearchHosts.
Attribution
A business event becomes one row per touchpoint. UtmAttribution derives its
own fingerprint and dedupeKey — they are never constructor arguments,
because a mismatched fingerprint would silently defeat the unique index of the
journal.
| Guarantee | Detail |
|---|---|
| Retry of the same event | Writes nothing: deduplication is keyed by event id and touchpoint |
| A genuinely new event | Writes rows even for an identical campaign |
| Partial write | Self-healing — redelivery adds what is missing and duplicates nothing, which is why no transaction wraps the batch |
| Order | Oldest touchpoint first; server assigns the canonical order at write time |
| Empty touchpoints | Skipped — a row attributing nothing is noise |
UtmAttributionEventHandler is a ready listener (__invoke), but the package
does not subscribe it: wiring is the application's decision.
Storage
UtmAttributionRepository is the storage contract — append(),
findByEntity(), findFirst(), findLast(), countByEntity(),
deleteByEntity(), purgeOlderThan() and countOlderThan() (what
purgeOlderThan() would remove, without removing it — for a dry run). The
core does not bind it: an
implementation comes from rasuvaeff/yii3-utm-db or from the application.
InMemoryUtmAttributionRepository is shipped for tests and returns
InMemoryUtmAttributionRecord instances; it is never bound.
Implementations must make append() race-safe — an upsert that does nothing on
conflict, or an insert whose duplicate-key error is handled. "Check, then
insert" is not enough.
Security
| Aspect | Behaviour |
|---|---|
| Client input | Untrusted: normalised, truncated, invalid values become null. Values stay arbitrary text — escaping on output is the consumer's job |
occurredAt |
A claim by the source, never proof of when a visit happened |
| Ordering | Server-assigned; a late delivery cannot become the first touch |
| Deduplication | Fingerprint and dedupe key are derived, never accepted from callers |
| Referrer | http/https only; only its host takes part in the fingerprint |
| Landing page | Truncated to 500 characters on a boundary that keeps it a URL; query sanitisation is applied before storage |
| Cookie | Sanitised on decoding exactly like a query string, and its size is budgeted after percent-encoding |
Escaping is the consumer's job. Normalisation strips control characters,
trims and truncates — it does not make a value safe to render.
?utm_source=<img src=x onerror=alert(1)> reaches the cookie, the request
attributes and the attribution journal as that exact text, because a library
that renders nothing cannot know which context (HTML, an attribute, JSON, a
CSV cell) the value will end up in. Escape at the point of output — a marketing
dashboard listing campaign names is the typical place this matters.
Examples
Runnable scripts live in examples/.
Development
Without Make, run the same targets through Docker — see AGENTS.md.
License
BSD-3-Clause. See LICENSE.md.
All versions of yii3-utm with dependencies
ext-json Version *
ext-mbstring Version *
psr/clock Version ^1.0
psr/http-message Version ^2.0
psr/http-server-handler Version ^1.0
psr/http-server-middleware Version ^1.0
yiisoft/cookies Version ^1.2.3