luketowers/oc-easyaudit-plugin
Adds easy, configurable audit logging to October CMS. Automatically track create, update and delete events on your models, browse detailed change histories, and see exactly who changed what and when—with minimal setup and no code changes.
Install. Requires Winter CMS 1.x (PHP 7.4+/8.x) and Composer. Current release is v4.3.1:
composer require luketowers/wn-easyaudit-plugin
php artisan winter:up # runs plugin migrations
The repo/Composer name has drifted between oc-easyaudit-plugin (October-era) and wn-easyaudit-plugin (Winter). Confirm the exact package name in composer.json before you add it to CI — a typo here fails silently at install time, not at runtime.
If you configure auditing dynamically (from a base model, a behavior's boot(), a service provider, or per-tenant config) rather than with literal class properties, require ^4.3.1 — see Gotchas.
Look here first, in this order:
Plugin.php — navigation registration, permission keys, and any registerSettings()/registerListColumnTypes() hooks. This file tells you what the plugin actually wires up.models/AuditLog.php — the audit record model, its casts (old_values/new_values → arrays), and its auditable morph relation.controllers/auditlogs/config_list.yaml + config_filter.yaml — the backend list, its default columns, and available filters (event, model type, user, date range).config/config.php — driver, user resolver, queue toggle, and any retention/console settings.README.md — the exact trait namespace and the $auditInclude/$auditExclude property names.CHANGELOG.md — this thing ships patch releases with behaviour fixes rather than docs, so read it before assuming a config knob is broken.First use case (5 minutes): add the audit behavior/trait to one model, save it in the backend, and open Audit Logs in the top nav.
class Post extends Model
{
use \LukeTowers\EasyAudit\Behaviors\Auditable; // verify namespace in README
public $auditInclude = ['title', 'slug', 'content', 'is_published'];
}
If an entry appears with old/new values, you're wired correctly. If nothing appears, jump to Gotchas — it's almost always the trait, the driver, or a queue.
Scope what you audit, aggressively. The default "log everything" behavior is fine for a prototype and painful in production. Set $auditInclude on every model, and treat $auditEvents as a deliberate choice (created, updated, deleted, restored) rather than a default.
Prefer a shared base model or trait over copy-paste. Since v4.3.1, trackable overrides can be set as dynamic properties, so a common base model or a small trait that assigns them in boot() now actually takes effect:
trait AuditsByDefault
{
public function bootAuditsByDefault()
{
// honored as of v4.3.1; silently ignored on older versions
$this->auditInclude = static::auditInclude();
$this->auditEvents = ['created', 'updated', 'deleted'];
}
}
This is the cleanest way to apply one policy across dozens of models without repeating properties. On v4.3.0 and below, this pattern produces no audits at all — the override is dropped and the plugin falls back to its defaults.
Use dynamic overrides for per-instance policy. If only published content should produce audit noise, or a specific tenant's models need a narrower field list, set the properties on the instance before saving rather than maintaining parallel model classes:
$post->auditInclude = ['title', 'slug', 'content'];
$post->save();
Treat this as a documented, second-class option: verify with a test that a row actually lands, because the failure mode is silent.
Build a per-record history tab. The audit trail is most valuable inline, not in a global list. Add the morph relation to a backend controller as a RelationController or a _history.htm partial:
// In the audited model
public $hasMany = [
'audits' => [\LukeTowers\EasyAudit\Models\AuditLog::class, 'key' => 'auditable_id', 'otherKey' => 'id', /* + type scope */],
];
Render columns: timestamp, backend user (resolved via the plugin's user resolver), event, and a diff of changed attributes. This is where the plugin pays for itself during support tickets.
Attach business context. Record why a change happened, not just what changed:
\LukeTowers\EasyAudit\Models\AuditLog::withGlobalContext(['source' => 'import', 'batch' => $batchId]);
Or wrap bulk operations:
Post::withoutAuditing(fn () => $this->import($file));
Query logs programmatically in your own reports — auditable_type + auditable_id + created_at is the working index triple:
AuditLog::where('auditable_type', Post::class)->where('event', 'updated')->latest()->limit(50)->get();
Integrate with backend users. Auditing Backend\Models\User is valuable for compliance, but you must exclude secrets — password, persist_code, reset_password_code, activation_code. Same for any model holding API keys or PII.
Extend rather than fork. Register a custom list column or filter in your own plugin, or swap the audit model class in config. Keep the vendor plugin untouched so composer update stays safe.
Trackable overrides were silently ignored when set dynamically before v4.3.1. This is the one behaviour change worth pinning for. If your $auditInclude/$auditEvents/$auditExclude come from a base model, a trait boot() method, a behavior, a service provider, or per-instance assignment, older versions dropped them and used the plugin defaults — usually meaning either too much data recorded (no include list) or nothing recorded (wrong event list). After upgrading, re-check your row volume on a staging database: models that previously logged everything may now log only your intended fields, and vice versa. Do not mix plugin versions across a multi-site fleet expecting identical audit output.
License is NOASSERTION. This is the single biggest blocker. "No asserted license" is not MIT and not "public domain" — for a commercial Winter build, get a human to read LICENSE.md before you depend on it. A 3-line file that reads like MIT but isn't machine-detectable is a five-minute legal check; a genuinely unlicensed repo is a rewrite risk.
The oc- name is a lie. Despite the package label, this is a Winter CMS plugin and will not install into October CMS without shims (October uses System\Classes\PluginBase and a different migration path). On October, use owen-it/laravel-auditing directly.
It's a thin wrapper with a small bus factor. ~17 stars, one maintainer, zero dependents — though that maintainer is active (v4.3.1 landed as an external-style PR, and the changelog is real, not a stale repo). Expect to read the source rather than find a Stack Overflow answer. Budget half a day to understand Plugin.php + the audit model before you build on top of it.
Nothing shows up? Check, in order:
insert()/raw DB — those bypass Eloquent events).>=4.3.1. Add one integration test per audited model; this class of bug is invisible at runtime.config('audit.driver') — if it's not the database driver, there's no row to list.$auditEvents doesn't include the event you're triggering.withoutAuditing() left switched on somewhere in a service provider or test bootstrap.The table grows without bound. audits accumulates one row per change forever, with full JSON payloads. Add a scheduled prune (AuditLog::where('created_at', '<', now()->subMonths(12))->delete()), confirm indexes exist on auditable_type, auditable_id, created_at, and user_id. Without them the backend list gets slow in the low tens of thousands of rows.
Bloat sources: content/body/html columns, base64 blobs, and relation-heavy saves produce huge old_values/new_values payloads. Exclude large text columns from $auditInclude unless the diff genuinely matters. Watch this especially after upgrading to v4.3.1 — a model that previously used a hard-coded property while your dynamic override was ignored can suddenly change its recorded payload shape.
JSON comparison is manual. The payloads are JSON; comparing old vs new for a readable diff means decoding and diffing yourself. For anything user-facing, build a small helper rather than dumping raw JSON into a list column.
Not-audited by default: pivot attach/detach events, touch() on updated_at from related models (unless configured), and mass update() query-builder calls. If a change matters, it needs to go through Eloquent save().
Permissions bite during testing. Super users bypass plugin permissions, so the audit list looks fine for you and 403s for editors. Test with a real non-super role before shipping; check registerPermissions() in Plugin.php for the exact key.
Config override location. Winter merges plugin config from the app's config/ namespace — verify the documented path in the README rather than editing the vendor file, or your changes vanish on composer update, taking your custom user resolver with them.
How can I help you explore Laravel packages today?