rasuvaeff / yii3-utm-db
Portable yiisoft/db storage for rasuvaeff/yii3-utm: append-only attribution journal with race-safe deduplication, retention and erasure
Requires
- php: 8.3 - 8.5
- ext-json: *
- ext-pdo: *
- psr/clock: ^1.0
- rasuvaeff/yii3-utm: ^1.0
- symfony/console: ^6.4 || ^7.0 || ^8.0
- yiisoft/db: ^2.0
- yiisoft/db-migration: ^2.0
Requires (Dev)
- ergebnis/composer-normalize: ^2.51
- friendsofphp/php-cs-fixer: ^3.95
- infection/infection: ^0.33
- maglnet/composer-require-checker: ^4.17
- nyholm/psr7: ^1.8
- rector/rector: ^2.4
- roave/backward-compatibility-check: ^8.0
- testo/bridge-infection: ^0.1.6
- testo/testo: ^0.10.25
- vimeo/psalm: ^6.16
- yiisoft/cache: ^3.2
- yiisoft/db-sqlite: ^2.0
- yiisoft/di: ^1.2.1
- yiisoft/injector: ^1.2
- yiisoft/test-support: ^3.1
This package is auto-updated.
Last update: 2026-08-03 16:14:28 UTC
README
Database storage for rasuvaeff/yii3-utm:
a portable yiisoft/db implementation of the attribution journal, with a
race-safe append, canonical server-side ordering, retention and erasure.
Using an AI coding assistant? llms.txt is a compact API reference written for LLMs.
Requirements
- PHP 8.3 – 8.5
ext-json,ext-pdorasuvaeff/yii3-utm,yiisoft/db2.x,yiisoft/db-migration2.x
Installation
composer require rasuvaeff/yii3-utm-db
Installing this package is all the wiring there is: the core deliberately
leaves UtmAttributionRepository unbound and this package binds it. Never bind
it a second time in the application — yiisoft/config reports
Duplicate key while building "di" when two sources define one key.
Migration
MigrationService::setSourceNamespaces() does not find migrations of this
package: yiisoft/db-migration matches the PSR-4 map by string prefix, and
Rasuvaeff\Yii3Utm\ is a prefix of Rasuvaeff\Yii3UtmDb\, so discovery walks
into the core package and silently finds nothing. Apply the migration directly
instead:
use Rasuvaeff\Yii3UtmDb\Migration\M260801000000CreateUtmAttributionTable; use Yiisoft\Db\Migration\MigrationBuilder; use Yiisoft\Db\Migration\Informer\NullMigrationInformer; $builder = new MigrationBuilder($db, new NullMigrationInformer()); $injector->make(M260801000000CreateUtmAttributionTable::class)->up($builder);
Injector::make() resolves the table name by type, so the migration and
the repository can never disagree about it.
Schema
| Column | Type | Meaning |
|---|---|---|
id |
big primary key | Insert order; part of the canonical order |
entity_id |
string(191) |
Whom the attribution belongs to |
event_id |
string(191) |
Application-owned idempotency key |
interaction_type |
string(32) |
purchase, registration, anything validated |
utm_source … utm_id |
string(255) |
Campaign tuple, GA4 utm_id included |
click_ids |
string(500) |
JSON object in whitelist order, {} when empty |
referrer, referrer_host |
string(500), string(255) |
Only the host takes part in the fingerprint |
landing_page |
string(500) |
Sanitised before it ever reaches the row |
occurred_at |
string(30) |
Claimed by the source; analytics only |
recorded_at |
string(30) |
Server clock at insert time |
fingerprint, dedupe_key |
string(64) |
Derived; the unique index sits on dedupe_key |
Indexes: UNIQUE (dedupe_key), (entity_id, recorded_at, id),
(entity_id, interaction_type, recorded_at, id), (recorded_at).
Timestamps are fixed-width UTC strings (Y-m-d H:i:s.u) so that the canonical
order is lexicographic on every driver. There is no is_first_interaction
column: first touch is whatever the server recorded first.
Configuration
// config/common/params.php 'rasuvaeff/yii3-utm-db' => [ 'table' => 'utm_attribution', 'table_prefix' => '', 'retentionDays' => 365, ],
Retention and erasure
./yii utm:purge --older-than=P365D # retention sweep
./yii utm:purge --older-than=P90D --dry-run
--older-than accepts a positive ISO-8601 interval such as P90D, P6M or
PT12H. Without the option, retentionDays supplies the default. --dry-run
reports the number of rows that would be deleted, without deleting them.
Deleting one person's data is a different operation and lives on the
repository: deleteByEntity($entityId).
Security
| Aspect | Behaviour |
|---|---|
| SQL | Every value is a bound parameter |
| Identifiers | Table and index names come only from the validated UtmAttributionTableName |
| Ordering | Server-assigned; a client cannot backdate itself into first touch |
| Duplicates | Prevented by the unique index plus a race-safe upsert, not by check-then-insert |
| Foreign rows | A row that does not match the canonical schema raises MalformedRow instead of being coerced. Every exception of this package implements UtmDbException, which extends the core UtmException. |
Examples
Runnable scripts live in examples/.
Development
make build
make test
make cs-fix
make psalm
The Integration suite runs against in-memory SQLite:
docker run --rm -v "$PWD":/app -w /app composer:2 vendor/bin/testo --suite=Integration
License
BSD-3-Clause. See LICENSE.md.