Search by

tilttek / activity

phil

A journal of members' activity for gamification: each gesture recorded once by its idempotency key, cancellations as entries of their own, totals by member, verb, object and period, projections that keep their own state and replay the journal when a rule changes, and the erasure of a member

0.1.0-alpha 2026-11-11 09:53 UTC

This package is auto-updated.

Last update: 2026-11-11 14:51:08 UTC


README

What your members did, recorded once, so that points, badges and quests can all count it the same way.

tilttek/activity keeps a journal of the members' gestures: this member made this gesture, on this object, at this moment. The application records each gesture with an idempotency key, so that a listener that runs twice or a task started again never counts it twice; it cancels a gesture that was undone — an order refunded, a comment removed — by an entry of its own, never by rewriting the journal. Readers count what the journal holds by member, verb, object and period, and projections keep their own totals up to date from it, then replay it when a rule changes. Erasing a member deletes their gestures and forgets them as the object of others'. It does not award points, badges or levels, and it never stores a text the member wrote: the packages that reward gestures read this journal, and a comment is referenced, not copied.

It is for an application that rewards what its members do — a community site, a store's loyalty programme, a game site — and wants every reward to come from one record that can be read again. tilttek/activity-bundle integrates it with Symfony and keeps the journal in Doctrine DBAL; an application without Symfony takes the library alone and writes its own ActivityLog, or uses the one in memory.

It is a plain PHP library whose only dependencies are phil/resource, the opaque reference of a member or an object, and psr/clock, the clock that stamps each entry. Members are references the application gives, member:44: the library never reads a name, an e-mail or an account.

The design is small: an Activity is a fact; a Journal records, cancels, reads and counts activities through an ActivityLog; a Filter selects activities; a Projection keeps derived state, which a Projector hands the entries it has not seen, with a checkpoint kept in Checkpoints.

Status

Part State
Activity, Verb, VerbPattern, IdempotencyKey, Context: the fact and its validated parts Implemented and tested
Journal: record once by key, cancel once, declared verbs, observers told of new entries Implemented and tested
ActivityLog and InMemoryActivityLog: append, cancel, read through a filter, totals, erasure Implemented and tested
Filter: member, verbs by pattern, object, period Implemented and tested
Projections: Projector with checkpoints and batches, rebuild of a period, LiveProjections, Projections Implemented and tested
Exceptions under one ActivityException interface Implemented and tested

Documentation

Document What it covers
Installation Requirements and installing.
Recording activity Activities, verbs, keys, context, cancellations, declared verbs and observers.
Reading and counting Filters, positions, reads in order and totals.
Projections Derived state kept up to date, checkpoints, batches, rebuilds and live projections.
Erasure What erasing a member removes and what it keeps.
Writing a journal The ActivityLog contract an application's storage implements.
Extension points What an application gives the library: the journal, projections, checkpoints, observers, the clock.
Exceptions ActivityException and its exceptions, with their messages.
Testing The library's tests and mutation bar, and how to test code that records activity.
Reference Every type of the library, with its role.

Quick start

composer require tilttek/activity
use Symfony\Component\Clock\NativeClock;
use Tilttek\Activity\Activity;
use Tilttek\Activity\Filter;
use Tilttek\Activity\Journal;
use Tilttek\Activity\Log\InMemoryActivityLog;
use Phil\Resource\ResourceReference;

$journal = new Journal(new InMemoryActivityLog(), new NativeClock());

$journal->record(Activity::of('game:812:player:44', 'member:44', 'game.played', $playedAt, 'game:812'));
$journal->record(Activity::of('game:812:player:44', 'member:44', 'game.played', $playedAt, 'game:812')); // counted once

$journal->totals(Filter::all()->member(ResourceReference::of('member', '44'))->verbs('game.*'))->count; // 1
$journal->cancel('game:812:player:44');

Requirements

PHP 8.5 or later.

Contributing

Contributions are welcome on the PHIL monorepo: read the contribution guide and the code of conduct. Report a vulnerability privately, as the security policy explains, never in a public issue.

License

MIT, © Philippe Gamache, doing business as Le Gnome Archiviste; a Tilttek project. See LICENSE.