PHPackages                             clicktrail/psr-middleware - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. clicktrail/psr-middleware

ActiveLibrary

clicktrail/psr-middleware
=========================

PSR-15 middleware for ClickTrail attribution: capture UTMs/click IDs from incoming requests, preserve first/last touch, persist only when consent permits, attach context downstream. No remote calls during the request.

v0.1.0(yesterday)10[1 issues](https://github.com/vizuh/clicktrail-psr-middleware/issues)MITPHPPHP &gt;=8.1CI passing

Since Aug 24Pushed todayCompare

[ Source](https://github.com/vizuh/clicktrail-psr-middleware)[ Packagist](https://packagist.org/packages/clicktrail/psr-middleware)[ RSS](/packages/clicktrail-psr-middleware/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (4)Versions (2)Used By (0)

[English](README.md) | [Português](README.pt-BR.md) | [Deutsch](README.de.md) | [中文](README.zh-CN.md)

**clicktrail/psr-middleware**

PSR-15 middleware for ClickTrail attribution in any PSR-7 framework (Slim, Mezzio, Laminas, custom) — deterministic first/last touch on the request, nothing written without consent.

[![CI](https://github.com/vizuh/clicktrail-psr-middleware/actions/workflows/ci.yml/badge.svg)](https://github.com/vizuh/clicktrail-psr-middleware/actions/workflows/ci.yml)[![License: MIT](https://camo.githubusercontent.com/08cef40a9105b6526ca22088bc514fbfdbc9aac1ddbf8d4e6c750e3a88a44dca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d626c75652e737667)](LICENSE)

Index
-----

[](#index)

- [Why](#why)
- [Installation](#installation)
- [Quick start](#quick-start)
- [Storage adapters](#storage-adapters)
- [Consent](#consent)
- [Reading the context](#reading-the-context)
- [How it differs](#how-it-differs)
- [Testing](#testing)
- [License](#license)

Why
---

[](#why)

Most attribution middleware captures UTMs into a cookie and calls it done — no merge law, no consent gate, storage decisions baked in. This package runs the deterministic ClickTrail core inside your PSR-15 stack and hands your handlers an immutable `AttributionContext`: merged touches, resolved consent, and an audit trail of everything that was suppressed and why. Part of the ClickTrail PHP/Twig expansion (ADR-0001 polyrepo, layer 1).

Installation
------------

[](#installation)

```
composer require clicktrail/psr-middleware
```

Quick start
-----------

[](#quick-start)

```
use ClickTrail\Middleware\ArrayStore;
use ClickTrail\Middleware\CaptureAttributionMiddleware as Capture;
use ClickTrail\Middleware\ConsentMiddleware;
use ClickTrail\Middleware\CookieStore;
use ClickTrail\Middleware\NullConsentResolver;

$clock = fn (): string => (new DateTimeImmutable('now', new DateTimeZone('UTC')))
    ->format('Y-m-d\TH:i:s.v\Z');

$app->add(new ConsentMiddleware($myCmpAdapter));   // optional; resolves the snapshot once
$app->add(new Capture(
    store: new CookieStore('ct_attr'),             // or ArrayStore / your session impl
    consentResolver: new NullConsentResolver(),    // swap in your CMP adapter
    clock: $clock,
));

// downstream handler/controller:
$context = $request->getAttribute(Capture::DEFAULT_ATTRIBUTE);
$context->firstTouch();       // ?Touch - original acquisition, untouched by later direct visits
$context->lastTouch();        // ?Touch - most recent signal
$context->canPersist();       // true only when consent allowed the storage write
$context->suppressionReasons; // audit trail of what was blocked and why
```

A paid-search hit followed by a direct visit leaves `firstTouch()` unchanged while `lastTouch()` moves — that is the merge law of the shared core, not this package's opinion. Without a consent grant, no store write happens at all.

Storage adapters
----------------

[](#storage-adapters)

The middleware never decides where state lives. Implement `StateStoreInterface` (session, database, cache) or ship one of the built-ins:

- **`ArrayStore`** — per-request memory. Tests and stateless workers.
- **`CookieStore`** — cookie-backed persistence, e.g. `new CookieStore('ct_attr')`.

If the injected `ConsentResolverInterface` returns no grant, `StateStoreInterface::save()` is never called — no cookie, no session entry, nothing.

Consent
-------

[](#consent)

A `null` snapshot means *unknown*, which is denied by default per the [consent compatibility contract](../docs/consent-compatibility-plan.md). Two ways to wire it:

- Pass a `consentResolver` to `CaptureAttributionMiddleware`; it gates persistence directly.
- Add `ConsentMiddleware` upstream; it resolves the snapshot once per request and attaches it under its own attribute (`clicktrail.consent`) for anything else downstream.

`NullConsentResolver` is the safe default: every persistence attempt becomes a recorded suppression reason instead of a write.

Reading the context
-------------------

[](#reading-the-context)

`AttributionContext` is an immutable value object attached to the request (`Capture::DEFAULT_ATTRIBUTE`, override via the `attribute` constructor argument):

```
$context->attribution;        // StoredState - full merged first/last touch state
$context->consent;            // ?ConsentSnapshot - null when unknown
$context->persisted;          // bool - did this request actually persist?
$context->suppressionReasons; // string[] - human-readable audit entries
```

Snapshots travel with the lead, so months later the conversion worker knows exactly which permissions existed at capture.

How it differs
--------------

[](#how-it-differs)

Typical tracking middlewareclicktrail/psr-middlewareMakes remote calls during the request cycleNo remote calls, ever — event delivery belongs to `clicktrail/php-sdk`Reads wall-clock time itselfInjected clock callable returning ISO-8601 millisecond timestampsWrites cookies, then asks about consentNo grant → no `save()` call → no writeBundles its own storage backendStorage belongs to the adapter: bring `StateStoreInterface`, or use `ArrayStore` / `CookieStore`Testing
-------

[](#testing)

```
podman run --rm -v "$PWD:/app" -v "$PWD/../clicktrail-php:/sdk:ro" \
  wordpress:php8.3-apache php /app/tests/_runner.php
```

License
-------

[](#license)

MIT © 2026 Vizuh OÜ

###  Health Score

31

—

LowBetter than 65% of packages

Maintenance80

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity32

Early-stage or recently created project

 Bus Factor1

Top contributor holds 87.5% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

1d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/20295730?v=4)[Hugo ](/maintainers/Atroci)[@Atroci](https://github.com/Atroci)

---

Top Contributors

[![Atroci](https://avatars.githubusercontent.com/u/20295730?v=4)](https://github.com/Atroci "Atroci (7 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (1 commits)")

---

Tags

analyticsattributionclickstreamconsent-managementconversion-trackingfirst-party-datagdprmarketing-analyticsmezziomiddlewarephppsr-15psr-7slimutmmiddlewarepsr-15UTMattributionconsentgclid

### Embed Badge

![Health badge](/badges/clicktrail-psr-middleware/health.svg)

```
[![Health](https://phpackages.com/badges/clicktrail-psr-middleware/health.svg)](https://phpackages.com/packages/clicktrail-psr-middleware)
```

###  Alternatives

[cakephp/cakephp

The CakePHP framework

8.9k20.0M1.9k](/packages/cakephp-cakephp)[cakephp/authentication

Authentication plugin for CakePHP

1214.3M120](/packages/cakephp-authentication)[typo3/cms

TYPO3 CMS is a free open source Content Management Framework initially created by Kasper Skaarhoj and licensed under GNU/GPL.

1.2k1.9M122](/packages/typo3-cms)[typo3/cms-core

TYPO3 CMS Core

3313.6M5.7k](/packages/typo3-cms-core)[typo3/cms-adminpanel

TYPO3 CMS Admin Panel - The Admin Panel displays information about your site in the frontend and contains a range of metrics including debug and caching information.

115.8M71](/packages/typo3-cms-adminpanel)[sunrise/http-router

A powerful solution as the foundation of your project.

16852.3k12](/packages/sunrise-http-router)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
