Audit and Change Tracking¶
Namespace: local_middag\framework\domain\audit.
The framework provides a three-table audit system for tracking changes to items and related entities.
Architecture¶
| Table | Purpose | Retention |
|---|---|---|
middag_audit_log |
Who changed what, when | Configurable |
middag_audit_diff |
Field-level before/after values | Linked to log |
middag_audit_snapshot |
Full state capture at point in time | Linked to log |
All three tables are relational with fixed schema (not EAV).
Diff Policy¶
Every mutation of a tracked entity produces an audit log entry with associated diffs:
audit_log (action: update, entity_type: segment, entity_id: 42, user_id: 7)
└── audit_diff (field: status, old_value: "draft", new_value: "active")
└── audit_diff (field: name, old_value: "Old Name", new_value: "New Name")
What is tracked:
- All field-level changes via
audit_diff(before/after per field) - The actor (
user_id), timestamp, and action type (create,update,delete)
What is NOT tracked:
- Read operations
- Bulk operations (each item produces its own log entry)
- Changes to metadata (EAV
itemmetafields) — only core entity fields
Snapshot Policy¶
Snapshots capture the full entity state at a point in time. They are produced on:
- Entity creation (initial state)
- Significant lifecycle transitions (e.g., status changes)
- Before deletion (final state preservation)
Snapshots are JSON-serialized via JsonSerializable on the entity.
Retention¶
Audit retention is configurable via admin settings:
| Setting | Default | Description |
|---|---|---|
audit_retention_days |
90 | Days to keep audit logs before purge |
The clean_audit_logs_command runs daily at 03:30 and removes entries older than the configured retention period. Associated diffs and snapshots are cascade-deleted.
Integration¶
Writing audit entries¶
Audit writes happen through audit_log_service (available via facade):
use local_middag\facade\audit_log_service;
audit_log_service::log('update', 'segment', $segment->id, $diffs);
Reading audit entries¶
The audit_query_service provides filtered reads for the admin UI:
use local_middag\extensions\core\admin\audit\audit_filter;
$filter = new audit_filter(
entity_type: 'segment',
entity_id: 42,
from: $start_timestamp,
);
$entries = $query_service->search($filter);
Transaction boundary¶
When an operation is classified as strong audit (ADR-505), the main write and the audit write share the same transaction via transaction_manager. This guarantees that either both succeed or both roll back.
Cross-references¶
- ADR-505 -- Audit trail design and diff/snapshot policy
- ADR-503 -- Persistence families (audit is relational, not EAV)
- Contracts --
audit_log_service,audit_query_service - Moodle Integration Patterns -- Transaction manager