Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Epilog Laravel Package

rap2hpoutre/laravel-epilog

Laravel Epilog is a lightweight audit-log package for Eloquent models. It records create, update, and delete events to a database table, helping you track model changes and activity without adding a heavier activity-logging dependency.

View on GitHub
Deep Wiki
Context7

Getting Started

Know what you're adopting first. The last release is from September 2015, which puts it in the Laravel 5.1/5.2 era. It will install cleanly on those versions; on Laravel 6+ you should expect to fork and patch it (helper functions like array_get(), str_*, untyped model properties, and pre-autodiscovery service provider registration are the usual breakage points). Budget an afternoon for the fork, or vendor the two files it actually ships.

Install and wire up manually. Autodiscovery did not exist when this was written, so register the provider yourself:

// config/app.php
'providers' => [
    // ...
    Rap2hpoutre\Epilog\EpilogServiceProvider::class,
],

'aliases' => [
    'Epilog' => Rap2hpoutre\Epilog\Facades\Epilog::class,
],

Publish and migrate before you write a single log call.

composer require rap2hpoutre/laravel-epilog
php artisan vendor:publish            # inspect tags if the provider is ambiguous
php artisan migrate

Read the published migration before running it. You want to see the table name (epilogs), the column types, and whether indexes exist — you will almost certainly want to add an index on (user_id, created_at) and on whatever you use as the event type. Editing the published migration and then migrating is far cheaper than a follow-up migration on a table that is already growing.

First use case: an auth trail. The cheapest win is recording logins and logouts, because it exercises the whole path (provider → facade → table) with data you can eyeball immediately:

// app/Providers/EventServiceProvider.php
protected $listen = [
    'Illuminate\Auth\Events\Login' => [\App\Listeners\LogSuccessfulLogin::class],
];
public function handle(Login $event)
{
    Epilog::log('auth.login', [
        'user_id' => $event->user->getKey(),
        'ip'      => request()->ip(),
    ]);
}

Then verify with select * from epilogs order by id desc limit 10; rather than trusting the code. If rows appear, the plumbing works and you can move on to real domain events. The exact facade method name and payload signature is the one thing worth confirming against the package source first — it is a tiny package, so open src/ and read it end to end (10 minutes) instead of guessing from the README.

Implementation Patterns

Log at the domain boundary, not in controllers. Controllers are transport; the event you care about ("invoice was paid", "user was suspended") happens in a service or model method. Logging there means console commands, queued jobs, and future API endpoints all produce the same audit trail for free. Controller-based logging silently misses everything triggered outside HTTP.

class MarkInvoicePaid
{
    public function handle(Invoice $invoice)
    {
        $invoice->update(['paid_at' => now()]);

        Epilog::log('invoice.paid', [
            'invoice_id' => $invoice->id,
            'amount'     => $invoice->total_cents,
            'actor_id'   => auth()->id(),
        ]);
    }
}

Use Eloquent model events for CRUD-shaped auditing. For "who changed this record", registered observers are far less code than sprinkling calls:

class AuditsModelChanges
{
    public function updated($model)
    {
        Epilog::log('model.updated', [
            'model'  => get_class($model),
            'key'    => $model->getKey(),
            'dirty'  => array_keys($model->getChanges()),
        ]);
    }
}

Register conservatively — every model, every update, produces a lot of rows fast. Scope it to models where the history has business value (users, roles, billing) and skip join tables, counters, and session rows.

Standardize the event name and payload shape. This is the single highest-leverage decision, because the table is schemaless in practice (the context is JSON). Adopt a dotted convention — resource.action like invoice.paid, user.role_changed — and keep payload keys snake_case and stable. When you later want select type, count(*) ... group by type, or a filter dropdown in an admin screen, a consistent vocabulary is the difference between a usable audit log and a pile of prose.

Write a thin application-level wrapper and call that, not the package. Given the package's age, treat it as an implementation detail behind your own class:

class ActivityLogger
{
    public function log(string $type, array $context = []): void
    {
        Epilog::log($type, array_merge([
            'actor_id' => auth()->id(),
            'ip'       => request()->ip(),
            'trace'    => Str::uuid(),
        ], $context));
    }
}

This costs nothing now and buys you a one-file migration if you swap to spatie/laravel-activitylog later. Every call site stays put.

Attach a correlation ID per request (middleware + Log::withContext-style merge) so a single user action that generates three epilog rows can be reconstructed as one story. Without it, a 40-row burst at the same second is unreadable.

Feed it into queues carefully. If you dispatch from a job, auth()->id() is null and request()->ip() is meaningless. Prefer an explicit actor_id argument over ambient request state for any non-HTTP caller, which is exactly why the wrapper above takes an explicit context.

For the read side, add a scoped model. Extend the package model (or point a config key at a subclass if the package supports it) and add query scopes — forUser(), ofType(), between(). A paginated admin timeline plus a per-user activity tab covers most of the day-to-day value and takes an hour to build.

Gotchas and Tips

It's abandoned — plan for ownership. No security or compatibility fixes are coming. Vendor-and-fork, or better, accept that the concept is 200 lines of code you could write yourself. If this is for compliance, note that a table your DB user can DELETE from is not a compliant audit trail; it's an operational log. Add append-only handling at the database level (revoke delete grants, or replicate to cold storage) if auditors are the audience.

Transaction coupling is the biggest correctness trap. Write inside the same transaction as your business change and it rolls back together — often what you want. But logs written to the same connection before a rollback vanish, and logs written via a queued job inside a transaction can fail on a missing FK if the parent row never committed. Decide deliberately: same transaction for "must match reality", separate connection for "must always record". Don't mix.

Guard against recursion. If your observer listens to updated on every model and the Epilog model is a normal Eloquent model, you can end up logging the logging. Exclude the log model class explicitly in the observer's early-return, and be aware that epilogs inserts inside a saved hook fire on every write — watch for surprise doubled rows.

Never let the log break the request. Wrap the call in try/catch (or at least in the wrapper). An audit insert failing on a read-replica connection, a JSON encode error, or a long column truncation should not 500 a user's checkout.

JSON storage is version-dependent. On MySQL 5.7+/Postgres you may get a real JSON type; on older MySQL it's TEXT and you must cast in the model (protected $casts = ['context' => 'array']) or you'll get strings back and reach for json_decode in every query site. Watch for double-encoding when you pass an already-encoded array. Also don't try to query inside the context on MySQL 5.6 — extract a dedicated column instead.

Sanitize before you store. Never pass $request->all(), $model->toArray(), or a full user model into the context. Passwords, remember tokens, API keys, and PII have a habit of landing in audit tables that are later dumped to a staging environment or read by support staff. Write the payload by hand, every time.

The table will grow unbounded. Plan the retention policy on day one: a scheduled job deleting rows older than N months, or partitioning by month. epilogs on a busy app hits millions of rows faster than people expect, and the package ships no rotation.

Migration and naming collisions. Run the published migration on a fresh local DB first. If your app already has an epilogs-adjacent concept, or if you've used the same publish tag for another package, review the file rather than trusting the timestamp ordering. Add indexes in the same migration — retrofitting an index on a 5M-row table in production is a different afternoon.

Debugging checklist when nothing appears: is the provider actually in config/app.php (not just composer.json)? Did you publish and migrate? Are you calling the correct facade method — read src/ and confirm? Is an exception being swallowed by your try/catch? Turn on DB::listen() for thirty seconds and watch whether the insert is even attempted. Nine times out of ten it's the provider registration.

Extension points are thin, which is fine. The realistic levers are: a config override for the table name (if present), a subclassed model bound into the container, and your own wrapper class. Don't fight the package to add features — the wrapper is the extension point, and it's the piece you control.

Pin the version. composer require rap2hpoutre/laravel-epilog:^1.0 plus a composer.lock commit. An abandoned package with a floating constraint is a time bomb the next time someone runs composer update on a newer PHP.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource