{"id":310,"date":"2025-08-27T19:36:31","date_gmt":"2025-08-27T19:36:31","guid":{"rendered":"https:\/\/1v0.net\/blog\/?p=310"},"modified":"2025-08-27T19:36:33","modified_gmt":"2025-08-27T19:36:33","slug":"how-to-use-eloquent-events-for-auditing-user-actions","status":"publish","type":"post","link":"https:\/\/1v0.net\/blog\/how-to-use-eloquent-events-for-auditing-user-actions\/","title":{"rendered":"How to Use Eloquent Events for Auditing User Actions"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\"><strong>How to Use Eloquent Events for Auditing User Actions<\/strong><\/h2>\n\n\n\n<p>Auditing records who did what and when\u2014essential for debugging, compliance, and customer support. With Eloquent events (creating, created, updating, updated, deleting, deleted, restored, forceDeleted), we can capture old\/new values, the actor, and request metadata into an <em>audits<\/em> table. In this guide you\u2019ll build a reusable observer, register it, and ship a simple UI to inspect audit trails.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>1 &#8211; Migration: Audits Table<\/strong><\/h2>\n\n\n\n<p>Create a generic table to store audit entries for any model using a polymorphic relation. We\u2019ll store the event name, old\/new values, and request context.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ database\/migrations\/2025_08_27_000000_create_audits_table.php<\/span>\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Database<\/span>\\<span class=\"hljs-title\">Migrations<\/span>\\<span class=\"hljs-title\">Migration<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Database<\/span>\\<span class=\"hljs-title\">Schema<\/span>\\<span class=\"hljs-title\">Blueprint<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Support<\/span>\\<span class=\"hljs-title\">Facades<\/span>\\<span class=\"hljs-title\">Schema<\/span>;\n\n<span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">Migration<\/span> <\/span>{\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">up<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">void<\/span> <\/span>{\n        Schema::create(<span class=\"hljs-string\">'audits'<\/span>, <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">(Blueprint $table)<\/span> <\/span>{\n            $table-&gt;id();\n            <span class=\"hljs-comment\">\/\/ polymorphic: auditable_type + auditable_id<\/span>\n            $table-&gt;morphs(<span class=\"hljs-string\">'auditable'<\/span>);\n            <span class=\"hljs-comment\">\/\/ actor (nullable for system jobs)<\/span>\n            $table-&gt;foreignId(<span class=\"hljs-string\">'user_id'<\/span>)-&gt;nullable()-&gt;constrained()-&gt;nullOnDelete();\n            <span class=\"hljs-comment\">\/\/ event: created, updated, deleted, restored, force_deleted<\/span>\n            $table-&gt;string(<span class=\"hljs-string\">'event'<\/span>, <span class=\"hljs-number\">32<\/span>);\n            <span class=\"hljs-comment\">\/\/ JSON diffs<\/span>\n            $table-&gt;json(<span class=\"hljs-string\">'old_values'<\/span>)-&gt;nullable();\n            $table-&gt;json(<span class=\"hljs-string\">'new_values'<\/span>)-&gt;nullable();\n            <span class=\"hljs-comment\">\/\/ request context<\/span>\n            $table-&gt;string(<span class=\"hljs-string\">'ip_address'<\/span>, <span class=\"hljs-number\">45<\/span>)-&gt;nullable();\n            $table-&gt;text(<span class=\"hljs-string\">'user_agent'<\/span>)-&gt;nullable();\n            $table-&gt;timestamps();\n            $table-&gt;index(&#91;<span class=\"hljs-string\">'event'<\/span>, <span class=\"hljs-string\">'created_at'<\/span>]);\n        });\n    }\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">down<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">void<\/span> <\/span>{\n        Schema::dropIfExists(<span class=\"hljs-string\">'audits'<\/span>);\n    }\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This table can attach audits to any model via <code>morphs('auditable')<\/code>. We keep <code>old_values<\/code>\/<code>new_values<\/code> as JSON to store only changed keys for updates and full snapshots for create\/delete if you prefer.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>2 &#8211; Audit Model<\/strong><\/h2>\n\n\n\n<p>Define the <code>Audit<\/code> model with casts and the polymorphic relation back to the auditable model.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Models\/Audit.php<\/span>\n<span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>;\n\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Database<\/span>\\<span class=\"hljs-title\">Eloquent<\/span>\\<span class=\"hljs-title\">Model<\/span>;\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">Audit<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">Model<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">protected<\/span> $fillable = &#91;\n        <span class=\"hljs-string\">'auditable_type'<\/span>,<span class=\"hljs-string\">'auditable_id'<\/span>,<span class=\"hljs-string\">'user_id'<\/span>,<span class=\"hljs-string\">'event'<\/span>,\n        <span class=\"hljs-string\">'old_values'<\/span>,<span class=\"hljs-string\">'new_values'<\/span>,<span class=\"hljs-string\">'ip_address'<\/span>,<span class=\"hljs-string\">'user_agent'<\/span>\n    ];\n\n    <span class=\"hljs-keyword\">protected<\/span> $casts = &#91;\n        <span class=\"hljs-string\">'old_values'<\/span> =&gt; <span class=\"hljs-string\">'array'<\/span>,\n        <span class=\"hljs-string\">'new_values'<\/span> =&gt; <span class=\"hljs-string\">'array'<\/span>,\n    ];\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">auditable<\/span><span class=\"hljs-params\">()<\/span>\n    <\/span>{\n        <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">$this<\/span>-&gt;morphTo();\n    }\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">user<\/span><span class=\"hljs-params\">()<\/span>\n    <\/span>{\n        <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">$this<\/span>-&gt;belongsTo(User::class);\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Casting JSON arrays makes reading diffs straightforward in controllers and Blade. The <code>auditable()<\/code> morph lets you navigate back to the source record.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>3 &#8211; Reusable Observer to Capture Events<\/strong><\/h2>\n\n\n\n<p>The observer listens to Eloquent lifecycle events, computes diffs, redacts sensitive fields, and writes an <code>Audit<\/code> entry. Attach the same observer to any model you want to audit.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Observers\/GenericAuditObserver.php<\/span>\n<span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Observers<\/span>;\n\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>\\<span class=\"hljs-title\">Audit<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Database<\/span>\\<span class=\"hljs-title\">Eloquent<\/span>\\<span class=\"hljs-title\">Model<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Support<\/span>\\<span class=\"hljs-title\">Arr<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Support<\/span>\\<span class=\"hljs-title\">Facades<\/span>\\<span class=\"hljs-title\">Auth<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Support<\/span>\\<span class=\"hljs-title\">Facades<\/span>\\<span class=\"hljs-title\">Request<\/span>;\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">GenericAuditObserver<\/span>\n<\/span>{\n    <span class=\"hljs-comment\">\/**\n     * Keys to hide in audits (passwords, tokens, secrets).\n     *\/<\/span>\n    <span class=\"hljs-keyword\">protected<\/span> <span class=\"hljs-keyword\">array<\/span> $redacted = &#91;<span class=\"hljs-string\">'password'<\/span>,<span class=\"hljs-string\">'remember_token'<\/span>,<span class=\"hljs-string\">'api_key'<\/span>,<span class=\"hljs-string\">'secret'<\/span>,<span class=\"hljs-string\">'token'<\/span>];\n\n    <span class=\"hljs-keyword\">protected<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">redact<\/span><span class=\"hljs-params\">(array $data)<\/span>: <span class=\"hljs-title\">array<\/span>\n    <\/span>{\n        <span class=\"hljs-keyword\">foreach<\/span> (<span class=\"hljs-keyword\">$this<\/span>-&gt;redacted <span class=\"hljs-keyword\">as<\/span> $key) {\n            <span class=\"hljs-keyword\">if<\/span> (Arr::has($data, $key)) {\n                Arr::set($data, $key, <span class=\"hljs-string\">'***redacted***'<\/span>);\n            }\n        }\n        <span class=\"hljs-keyword\">return<\/span> $data;\n    }\n\n    <span class=\"hljs-keyword\">protected<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">context<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">array<\/span>\n    <\/span>{\n        <span class=\"hljs-keyword\">return<\/span> &#91;\n            <span class=\"hljs-string\">'user_id'<\/span>    =&gt; Auth::id(),\n            <span class=\"hljs-string\">'ip_address'<\/span> =&gt; Request::ip(),\n            <span class=\"hljs-string\">'user_agent'<\/span> =&gt; Request::header(<span class=\"hljs-string\">'User-Agent'<\/span>),\n        ];\n    }\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">created<\/span><span class=\"hljs-params\">(Model $model)<\/span>: <span class=\"hljs-title\">void<\/span>\n    <\/span>{\n        Audit::create(&#91;\n            <span class=\"hljs-string\">'auditable_type'<\/span> =&gt; get_class($model),\n            <span class=\"hljs-string\">'auditable_id'<\/span>   =&gt; $model-&gt;getKey(),\n            <span class=\"hljs-string\">'event'<\/span>          =&gt; <span class=\"hljs-string\">'created'<\/span>,\n            <span class=\"hljs-string\">'old_values'<\/span>     =&gt; <span class=\"hljs-keyword\">null<\/span>,\n            <span class=\"hljs-string\">'new_values'<\/span>     =&gt; <span class=\"hljs-keyword\">$this<\/span>-&gt;redact($model-&gt;getAttributes()),\n        ] + <span class=\"hljs-keyword\">$this<\/span>-&gt;context());\n    }\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">updated<\/span><span class=\"hljs-params\">(Model $model)<\/span>: <span class=\"hljs-title\">void<\/span>\n    <\/span>{\n        <span class=\"hljs-comment\">\/\/ changed attributes only<\/span>\n        $changes = $model-&gt;getChanges();             <span class=\"hljs-comment\">\/\/ new values for dirty fields<\/span>\n        $original = Arr::only($model-&gt;getOriginal(), array_keys($changes));\n\n        Audit::create(&#91;\n            <span class=\"hljs-string\">'auditable_type'<\/span> =&gt; get_class($model),\n            <span class=\"hljs-string\">'auditable_id'<\/span>   =&gt; $model-&gt;getKey(),\n            <span class=\"hljs-string\">'event'<\/span>          =&gt; <span class=\"hljs-string\">'updated'<\/span>,\n            <span class=\"hljs-string\">'old_values'<\/span>     =&gt; <span class=\"hljs-keyword\">$this<\/span>-&gt;redact($original),\n            <span class=\"hljs-string\">'new_values'<\/span>     =&gt; <span class=\"hljs-keyword\">$this<\/span>-&gt;redact($changes),\n        ] + <span class=\"hljs-keyword\">$this<\/span>-&gt;context());\n    }\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">deleted<\/span><span class=\"hljs-params\">(Model $model)<\/span>: <span class=\"hljs-title\">void<\/span>\n    <\/span>{\n        Audit::create(&#91;\n            <span class=\"hljs-string\">'auditable_type'<\/span> =&gt; get_class($model),\n            <span class=\"hljs-string\">'auditable_id'<\/span>   =&gt; $model-&gt;getKey(),\n            <span class=\"hljs-string\">'event'<\/span>          =&gt; <span class=\"hljs-string\">'deleted'<\/span>,\n            <span class=\"hljs-string\">'old_values'<\/span>     =&gt; <span class=\"hljs-keyword\">$this<\/span>-&gt;redact($model-&gt;getAttributes()),\n            <span class=\"hljs-string\">'new_values'<\/span>     =&gt; <span class=\"hljs-keyword\">null<\/span>,\n        ] + <span class=\"hljs-keyword\">$this<\/span>-&gt;context());\n    }\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">restored<\/span><span class=\"hljs-params\">(Model $model)<\/span>: <span class=\"hljs-title\">void<\/span>\n    <\/span>{\n        Audit::create(&#91;\n            <span class=\"hljs-string\">'auditable_type'<\/span> =&gt; get_class($model),\n            <span class=\"hljs-string\">'auditable_id'<\/span>   =&gt; $model-&gt;getKey(),\n            <span class=\"hljs-string\">'event'<\/span>          =&gt; <span class=\"hljs-string\">'restored'<\/span>,\n            <span class=\"hljs-string\">'old_values'<\/span>     =&gt; <span class=\"hljs-keyword\">null<\/span>,\n            <span class=\"hljs-string\">'new_values'<\/span>     =&gt; <span class=\"hljs-keyword\">null<\/span>,\n        ] + <span class=\"hljs-keyword\">$this<\/span>-&gt;context());\n    }\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">forceDeleted<\/span><span class=\"hljs-params\">(Model $model)<\/span>: <span class=\"hljs-title\">void<\/span>\n    <\/span>{\n        Audit::create(&#91;\n            <span class=\"hljs-string\">'auditable_type'<\/span> =&gt; get_class($model),\n            <span class=\"hljs-string\">'auditable_id'<\/span>   =&gt; $model-&gt;getKey(),\n            <span class=\"hljs-string\">'event'<\/span>          =&gt; <span class=\"hljs-string\">'force_deleted'<\/span>,\n            <span class=\"hljs-string\">'old_values'<\/span>     =&gt; <span class=\"hljs-keyword\">null<\/span>,\n            <span class=\"hljs-string\">'new_values'<\/span>     =&gt; <span class=\"hljs-keyword\">null<\/span>,\n        ] + <span class=\"hljs-keyword\">$this<\/span>-&gt;context());\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The observer captures five major events. For updates, it stores only changed keys by diffing <code>getOriginal()<\/code> vs <code>getChanges()<\/code>. The <code>$redacted<\/code> list ensures secrets never leak into the audit log.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>4 &#8211; Register the Observer<\/strong><\/h2>\n\n\n\n<p>You can observe multiple models. Here we audit <code>User<\/code> and <code>Post<\/code>. Add more as needed.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Providers\/AppServiceProvider.php (boot)<\/span>\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>\\<span class=\"hljs-title\">Post<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>\\<span class=\"hljs-title\">User<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Observers<\/span>\\<span class=\"hljs-title\">GenericAuditObserver<\/span>;\n\n<span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">boot<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">void<\/span>\n<\/span>{\n    User::observe(GenericAuditObserver::class);\n    Post::observe(GenericAuditObserver::class);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Placing this in <code>AppServiceProvider::boot()<\/code> wires the observer globally. From now on, creates\/updates\/deletes on these models produce audit rows automatically.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>5 &#8211; (Optional) Auditable Trait for Model-Side Opt-In<\/strong><\/h2>\n\n\n\n<p>If you prefer models to opt-in explicitly, use a trait that registers the observer on boot. This avoids listing models in the provider.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Models\/Concerns\/Auditable.php<\/span>\n<span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>\\<span class=\"hljs-title\">Concerns<\/span>;\n\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Observers<\/span>\\<span class=\"hljs-title\">GenericAuditObserver<\/span>;\n\n<span class=\"hljs-keyword\">trait<\/span> Auditable\n{\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">static<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">bootAuditable<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">void<\/span>\n    <\/span>{\n        <span class=\"hljs-keyword\">static<\/span>::observe(GenericAuditObserver::class);\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Attach the trait to any model you want audited: <code>use Auditable;<\/code>. When the model boots, it registers the observer automatically.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>6 &#8211; Querying the Audit Log<\/strong><\/h2>\n\n\n\n<p>A controller to filter audits by model, event, date range, or user. We\u2019ll display results in a simple Blade table.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Http\/Controllers\/AuditController.php<\/span>\n<span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Controllers<\/span>;\n\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>\\<span class=\"hljs-title\">Audit<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Request<\/span>;\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">AuditController<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">Controller<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">index<\/span><span class=\"hljs-params\">(Request $request)<\/span>\n    <\/span>{\n        $q = Audit::query()\n            -&gt;with(&#91;<span class=\"hljs-string\">'user'<\/span>])\n            -&gt;latest();\n\n        <span class=\"hljs-keyword\">if<\/span> ($m = $request-&gt;input(<span class=\"hljs-string\">'model'<\/span>)) {\n            <span class=\"hljs-comment\">\/\/ expects fully-qualified class or short name<\/span>\n            $q-&gt;where(<span class=\"hljs-string\">'auditable_type'<\/span>, $m);\n        }\n\n        <span class=\"hljs-keyword\">if<\/span> ($e = $request-&gt;input(<span class=\"hljs-string\">'event'<\/span>)) {\n            $q-&gt;where(<span class=\"hljs-string\">'event'<\/span>, $e);\n        }\n\n        <span class=\"hljs-keyword\">if<\/span> ($u = $request-&gt;input(<span class=\"hljs-string\">'user_id'<\/span>)) {\n            $q-&gt;where(<span class=\"hljs-string\">'user_id'<\/span>, $u);\n        }\n\n        <span class=\"hljs-keyword\">if<\/span> ($from = $request-&gt;date(<span class=\"hljs-string\">'from'<\/span>)) {\n            $q-&gt;whereDate(<span class=\"hljs-string\">'created_at'<\/span>, <span class=\"hljs-string\">'&gt;='<\/span>, $from);\n        }\n        <span class=\"hljs-keyword\">if<\/span> ($to = $request-&gt;date(<span class=\"hljs-string\">'to'<\/span>)) {\n            $q-&gt;whereDate(<span class=\"hljs-string\">'created_at'<\/span>, <span class=\"hljs-string\">'&lt;='<\/span>, $to);\n        }\n\n        $audits = $q-&gt;paginate(<span class=\"hljs-number\">15<\/span>)-&gt;withQueryString();\n        <span class=\"hljs-keyword\">return<\/span> view(<span class=\"hljs-string\">'audits.index'<\/span>, compact(<span class=\"hljs-string\">'audits'<\/span>));\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The controller builds a flexible query over the <code>audits<\/code> table. We eager-load the actor (<code>user<\/code>) and support common filters to narrow results.<\/p>\n\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ routes\/web.php (snippet)<\/span>\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Controllers<\/span>\\<span class=\"hljs-title\">AuditController<\/span>;\n\nRoute::middleware(&#91;<span class=\"hljs-string\">'auth'<\/span>])-&gt;group(<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">()<\/span> <\/span>{\n    Route::get(<span class=\"hljs-string\">'\/audits'<\/span>, &#91;AuditController::class, <span class=\"hljs-string\">'index'<\/span>])-&gt;name(<span class=\"hljs-string\">'audits.index'<\/span>);\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Audit visibility should be restricted\u2014only admins or support roles should access this page. Protect the route with <code>auth<\/code> and authorization policies as needed.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>7 &#8211; UI: Audit Log Blade with Filters<\/strong><\/h2>\n\n\n\n<p>A minimal interface to explore audit entries, see who changed what, and inspect JSON diffs quickly.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">&lt;!-- resources\/views\/audits\/index.blade.php --&gt;\n@extends(<span class=\"hljs-string\">'layouts.app'<\/span>)\n\n@section(<span class=\"hljs-string\">'content'<\/span>)\n&lt;div <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">container<\/span>\"&gt;\n  &lt;<span class=\"hljs-title\">h1<\/span> <span class=\"hljs-title\">class<\/span>=\"<span class=\"hljs-title\">mb<\/span>-4\"&gt;<span class=\"hljs-title\">Audit<\/span> <span class=\"hljs-title\">Log<\/span>&lt;\/<span class=\"hljs-title\">h1<\/span>&gt;\n\n  &lt;<span class=\"hljs-title\">form<\/span> <span class=\"hljs-title\">method<\/span>=\"<span class=\"hljs-title\">GET<\/span>\" <span class=\"hljs-title\">class<\/span>=\"<span class=\"hljs-title\">row<\/span> <span class=\"hljs-title\">g<\/span>-2 <span class=\"hljs-title\">mb<\/span>-4\"&gt;\n    &lt;<span class=\"hljs-title\">div<\/span> <span class=\"hljs-title\">class<\/span>=\"<span class=\"hljs-title\">col<\/span>-<span class=\"hljs-title\">md<\/span>-3\"&gt;\n      &lt;<span class=\"hljs-title\">input<\/span> <span class=\"hljs-title\">name<\/span>=\"<span class=\"hljs-title\">model<\/span>\" <span class=\"hljs-title\">class<\/span>=\"<span class=\"hljs-title\">form<\/span>-<span class=\"hljs-title\">control<\/span>\" <span class=\"hljs-title\">placeholder<\/span>=\"<span class=\"hljs-title\">Model<\/span> (<span class=\"hljs-title\">FQCN<\/span>)\" <span class=\"hljs-title\">value<\/span>=\"<\/span>{{ request(<span class=\"hljs-string\">'model'<\/span>) }}<span class=\"hljs-string\">\" \/&gt;\n    &lt;\/div&gt;\n    &lt;div class=\"<\/span>col-md<span class=\"hljs-number\">-2<\/span><span class=\"hljs-string\">\"&gt;\n      &lt;select name=\"<\/span>event<span class=\"hljs-string\">\" class=\"<\/span>form-select<span class=\"hljs-string\">\"&gt;\n        @php $events = &#91;'created','updated','deleted','restored','force_deleted']; @endphp\n        &lt;option value=\"<\/span><span class=\"hljs-string\">\"&gt;Any event&lt;\/option&gt;\n        @foreach($events as $e)\n          &lt;option value=\"<\/span>{{ $e }}<span class=\"hljs-string\">\" {{ request('event')===$e ? 'selected' : '' }}&gt;{{ ucfirst(str_replace('_',' ',$e)) }}&lt;\/option&gt;\n        @endforeach\n      &lt;\/select&gt;\n    &lt;\/div&gt;\n    &lt;div class=\"<\/span>col-md<span class=\"hljs-number\">-2<\/span><span class=\"hljs-string\">\"&gt;\n      &lt;input type=\"<\/span>number<span class=\"hljs-string\">\" name=\"<\/span>user_id<span class=\"hljs-string\">\" class=\"<\/span>form-control<span class=\"hljs-string\">\" placeholder=\"<\/span>User ID<span class=\"hljs-string\">\" value=\"<\/span>{{ request(<span class=\"hljs-string\">'user_id'<\/span>) }}<span class=\"hljs-string\">\" \/&gt;\n    &lt;\/div&gt;\n    &lt;div class=\"<\/span>col-md<span class=\"hljs-number\">-2<\/span><span class=\"hljs-string\">\"&gt;\n      &lt;input type=\"<\/span>date<span class=\"hljs-string\">\" name=\"<\/span>from<span class=\"hljs-string\">\" class=\"<\/span>form-control<span class=\"hljs-string\">\" value=\"<\/span>{{ request(<span class=\"hljs-string\">'from'<\/span>) }}<span class=\"hljs-string\">\" \/&gt;\n    &lt;\/div&gt;\n    &lt;div class=\"<\/span>col-md<span class=\"hljs-number\">-2<\/span><span class=\"hljs-string\">\"&gt;\n      &lt;input type=\"<\/span>date<span class=\"hljs-string\">\" name=\"<\/span>to<span class=\"hljs-string\">\" class=\"<\/span>form-control<span class=\"hljs-string\">\" value=\"<\/span>{{ request(<span class=\"hljs-string\">'to'<\/span>) }}<span class=\"hljs-string\">\" \/&gt;\n    &lt;\/div&gt;\n    &lt;div class=\"<\/span>col-md<span class=\"hljs-number\">-1<\/span> d-grid<span class=\"hljs-string\">\"&gt;\n      &lt;button class=\"<\/span>btn btn-theme<span class=\"hljs-string\">\"&gt;Filter&lt;\/button&gt;\n    &lt;\/div&gt;\n  &lt;\/form&gt;\n\n  @forelse($audits as $audit)\n    &lt;div class=\"<\/span>card mb<span class=\"hljs-number\">-3<\/span><span class=\"hljs-string\">\"&gt;\n      &lt;div class=\"<\/span>card-body<span class=\"hljs-string\">\"&gt;\n        &lt;div class=\"<\/span>d-flex justify-content-between align-items-center<span class=\"hljs-string\">\"&gt;\n          &lt;div&gt;\n            &lt;strong&gt;{{ class_basename($audit-&gt;auditable_type) }}#{{ $audit-&gt;auditable_id }}&lt;\/strong&gt;\n            &lt;span class=\"<\/span>badge bg-secondary<span class=\"hljs-string\">\"&gt;{{ $audit-&gt;event }}&lt;\/span&gt;\n          &lt;\/div&gt;\n          &lt;small class=\"<\/span>text-muted<span class=\"hljs-string\">\"&gt;{{ $audit-&gt;created_at }} by {{ optional($audit-&gt;user)-&gt;name ?? 'system' }}&lt;\/small&gt;\n        &lt;\/div&gt;\n\n        &lt;div class=\"<\/span>mt<span class=\"hljs-number\">-3<\/span><span class=\"hljs-string\">\"&gt;\n          &lt;details&gt;\n            &lt;summary&gt;Old Values&lt;\/summary&gt;\n            &lt;pre class=\"<\/span>mb<span class=\"hljs-number\">-0<\/span><span class=\"hljs-string\">\"&gt;{{ json_encode($audit-&gt;old_values, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES) }}&lt;\/pre&gt;\n          &lt;\/details&gt;\n          &lt;details class=\"<\/span>mt<span class=\"hljs-number\">-2<\/span><span class=\"hljs-string\">\"&gt;\n            &lt;summary&gt;New Values&lt;\/summary&gt;\n            &lt;pre class=\"<\/span>mb<span class=\"hljs-number\">-0<\/span><span class=\"hljs-string\">\"&gt;{{ json_encode($audit-&gt;new_values, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES) }}&lt;\/pre&gt;\n          &lt;\/details&gt;\n        &lt;\/div&gt;\n\n        &lt;small class=\"<\/span>text-muted<span class=\"hljs-string\">\"&gt;IP: {{ $audit-&gt;ip_address }} | Agent: {{ Str::limit($audit-&gt;user_agent, 80) }}&lt;\/small&gt;\n      &lt;\/div&gt;\n    &lt;\/div&gt;\n  @empty\n    &lt;p class=\"<\/span>text-muted<span class=\"hljs-string\">\"&gt;No audit entries found.&lt;\/p&gt;\n  @endforelse\n\n  {{ $audits-&gt;links() }}\n&lt;\/div&gt;\n@endsection<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The page shows each event with who did it and when. Expandable <code>&lt;details&gt;<\/code> blocks display JSON diffs without overwhelming the layout. Filters make it easy to narrow down incidents.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>8 &#8211; Applying Auditing to a Model<\/strong><\/h2>\n\n\n\n<p>Here\u2019s how you would opt-in a typical model (e.g., <code>Post<\/code>). We\u2019ll also show a quick controller action to trigger events.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Models\/Post.php (snippet)<\/span>\n<span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>;\n\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Database<\/span>\\<span class=\"hljs-title\">Eloquent<\/span>\\<span class=\"hljs-title\">Model<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Database<\/span>\\<span class=\"hljs-title\">Eloquent<\/span>\\<span class=\"hljs-title\">SoftDeletes<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>\\<span class=\"hljs-title\">Concerns<\/span>\\<span class=\"hljs-title\">Auditable<\/span>;\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">Post<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">Model<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">SoftDeletes<\/span>, <span class=\"hljs-title\">Auditable<\/span>;\n\n    <span class=\"hljs-keyword\">protected<\/span> $fillable = &#91;<span class=\"hljs-string\">'user_id'<\/span>,<span class=\"hljs-string\">'title'<\/span>,<span class=\"hljs-string\">'body'<\/span>,<span class=\"hljs-string\">'status'<\/span>];\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Including <code>Auditable<\/code> ensures the observer is registered for <code>Post<\/code>. Combining with <code>SoftDeletes<\/code> records <em>deleted<\/em>, <em>restored<\/em>, and <em>force_deleted<\/em> events as well.<\/p>\n\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Http\/Controllers\/PostController.php (snippet)<\/span>\n<span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">update<\/span><span class=\"hljs-params\">(Request $request, Post $post)<\/span>\n<\/span>{\n    $data = $request-&gt;validate(&#91;\n        <span class=\"hljs-string\">'title'<\/span> =&gt; &#91;<span class=\"hljs-string\">'required'<\/span>,<span class=\"hljs-string\">'string'<\/span>,<span class=\"hljs-string\">'max:150'<\/span>],\n        <span class=\"hljs-string\">'body'<\/span>  =&gt; &#91;<span class=\"hljs-string\">'required'<\/span>,<span class=\"hljs-string\">'string'<\/span>],\n        <span class=\"hljs-string\">'status'<\/span>=&gt; &#91;<span class=\"hljs-string\">'required'<\/span>,<span class=\"hljs-string\">'in:draft,published'<\/span>],\n    ]);\n\n    $post-&gt;update($data); <span class=\"hljs-comment\">\/\/ triggers \"updated\" audit<\/span>\n    <span class=\"hljs-keyword\">return<\/span> back()-&gt;with(<span class=\"hljs-string\">'status'<\/span>,<span class=\"hljs-string\">'Post updated.'<\/span>);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Saving the model is all that\u2019s needed\u2014events fire automatically, the observer writes the audit, and you can view it in the Audit Log UI.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\">Wrapping Up<\/h2>\n\n\n\n<p>You built a robust audit trail with Eloquent events: a polymorphic audits table, a reusable observer that records diffs and request context, registration via provider or trait, and a filterable UI. This approach is lightweight, framework-native, and easy to extend with policies, retention rules, or offloading to a log index if volumes grow.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\">What\u2019s Next<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/blog\/eager-loading-vs-lazy-loading-in-laravel-best-practices\">Eager Loading vs Lazy Loading in Laravel: Best Practices<\/a><\/li>\n<li><a href=\"\/blog\/filtering-and-searching-with-laravel-eloquent-query-builder\">Filtering and Searching with Eloquent Query Builder<\/a><\/li>\n<li><a href=\"\/blog\/10-proven-ways-to-optimize-laravel-for-high-traffic\">10 Proven Ways to Optimize Laravel for High Traffic<\/a><\/li>\n<\/ul>\n\n","protected":false},"excerpt":{"rendered":"<p>How to Use Eloquent Events for Auditing User Actions Auditing records who did what and when\u2014essential for debugging, compliance, and customer support. With Eloquent events (creating, created, updating, updated, deleting, deleted, restored, forceDeleted), we can capture old\/new values, the actor, and request metadata into an audits table. In this guide you\u2019ll build a reusable observer, [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":314,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[47,48,46],"class_list":["post-310","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-laravel","tag-auditing","tag-compliance","tag-events"],"_links":{"self":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/310","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/comments?post=310"}],"version-history":[{"count":1,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/310\/revisions"}],"predecessor-version":[{"id":313,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/310\/revisions\/313"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/media\/314"}],"wp:attachment":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/media?parent=310"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/categories?post=310"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/tags?post=310"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}