mj-zana / audit
Content revisions for Zana applications: who changed what and when, per locale, with history and restore
v2.19.0
2026-11-06 19:56 UTC
Requires
- php: ^8.5
- mj-zana/core: ^2.11
- mj-zana/database: ^2.12
Requires (Dev)
- mj-zana/console: ^2.0
- mj-zana/events: ^2.0
- mj-zana/http: ^2.0
- mj-zana/mediator: ^2.0
- mj-zana/security: ^2.2
- mj-zana/testing: ^2.0
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
Suggests
- mj-zana/console: For audit:install-log, audit:verify, audit:prune and audit:log (2.15)
- mj-zana/events: To record mj-zana/security's events in the audit log (2.15)
- mj-zana/http: To record the client address of each entry (2.15)
- mj-zana/mediator: For #[Audited] commands (2.15)
- mj-zana/security: To record the signed-in user as the author or actor, and (2.15) its sign-in events in the audit log
Provides
None
Conflicts
None
Replaces
None
README
Content revisions and a security audit log for Zana applications.
- Revisions: every published version of a record, per locale, with its author, its date, a history and restore.
- Security audit log (2.15): an append-only record of sensitive actions, such as sign-ins, refunds and payout account changes, for disputes and the data-protection rules. Hash chaining is optional.
composer require mj-zana/audit
Revisions
Register AuditServiceProvider and create the table in a migration:
DatabaseRevisionStore::createTable($connection);
$revisions->record('post', $post->id, 'fr', ['title' => $title, 'body' => $body]); foreach ($revisions->history('post', $post->id, 'fr') as $revision) { // $revision->version, ->author, ->createdAt, ->diff($previous) } $data = $revisions->restore('post', $post->id, 'fr', 3); // then save it to the post
- Locales: versions are numbered per record and locale.
- No duplicates: recording identical content again does nothing.
- Author: the signed-in user with
mj-zana/security. You can also pass one explicitly. - Restore: restoring records the old content as the newest version, with the note "Restored version N", so the history stays complete.
- Storage:
DatabaseRevisionStore(tableaudit.table), or your ownRevisionStoreInterface.
Security audit log (2.15)
Enable it, then create its tables:
// config/audit.php return [ 'log' => [ 'enabled' => true, // record security events; check the table 'chain' => true, // hash chaining (off by default) 'retention_days' => null, // keep entries forever; or, e.g., 3650 ], ];
php bin/zana audit:install-log
php bin/zana migrate
Record an action with AuditLog. It's scoped, so the actor is the signed-in
user, the client address is the request's, and the correlation id is the
request's, command's or job's:
$audit->record('payout.account_changed', subject: "organizer:{$id}", details: ['bank' => 'Rawbank']);
Or mark a mediator command, and add AuditedBehavior to mediator.behaviors.
The command is recorded once its handler returns, with only the listed
fields:
#[Audited('order.refund', subject: 'orderId', details: ['orderId', 'amount'])] final readonly class RefundOrder { /* ... */ }
- Security events: with
mj-zana/securityandmj-zana/events, sign-ins, failed sign-ins (with the login typed, never the password), sign-outs and password resets are recorded. - Append-only: the API has no update and no delete. You can also deny
the database user
UPDATEandDELETEonaudit_log. - Chain: each entry stores the hash of the previous one.
audit:verifyfinds an edited, inserted or removed entry, andcheckverifies the newest 1,000. - Retention:
audit:prunedeletes entries older thanaudit.log.retention_days. Without that setting, entries are kept forever. - Reading:
audit:log --action=security.sign_in_failed --limit=20, andinspect audit-log. - No secrets: entries cannot be changed, so record only what a dispute needs.
License
MIT