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

Tomato User Activities Laravel Package

tomatophp/tomato-user-activities

Laravel package for tracking and displaying user activities in TomatoPHP admin panels. Logs logins, CRUD events, and custom actions, with a ready-made Filament resource for browsing activity history and auditing user behavior.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish

composer require tomatophp/tomato-user-activities:^1.0
php artisan vendor:publish --tag=tomato-user-activities-config
php artisan vendor:publish --tag=tomato-user-activities-migrations
php artisan migrate

The package is auto-discovered on Laravel 10/11. If your app disables discovery, register the provider manually in bootstrap/providers.php (or config/app.php).

Where to look first, in this order

  1. config/tomato-user-activities.php — this is where the model, table name, retention window, and any middleware/enabled flags live. Read it top to bottom before writing a single logging call; it usually tells you the whole design of the package.
  2. The published migration — confirms the column set (subject_type/subject_id, causer_*, event, properties, IP/user-agent) and whether it is UUID/ULID safe. Check this against your users table key type.
  3. The package's model class (referenced in config) — shows casts, fillable, and any scopes (latest(), forUser(), ofEvent()) you should reuse instead of writing raw queries.
  4. The service provider — reveals whether there is an auto-logging middleware, model trait, or Blade/Filament component you can opt into.

First use case

Record a login and render it on a profile page. Log manually first (fastest feedback), then wire the automatic middleware once you trust the schema:

use TomatoPHP\UserActivities\Facades\UserActivities; // verify namespace after install

UserActivities::log('login', $user, ['guard' => 'web']);

Then query the timeline:

$timeline = $user->activities()->latest()->limit(20)->get();

Once that round-trips, move on to instrumenting profile updates and record changes.

Implementation Patterns

1. Model trait for the "subject" side

Attach the package trait to any model that should own a history (usually User, sometimes Team, Order). This gives you a relation you use everywhere downstream:

class User extends Authenticatable
{
    use HasActivities; // name from the package; confirm on install
}

// In a controller / Filament resource:
return view('profile.timeline', [
    'activities' => $user->activities()->with('causer')->latest()->paginate(25),
]);

Always eager-load the causer (or use the package's built-in withCauser scope). Without it, a 25-row timeline on an admin dashboard becomes 26 queries.

2. Wrapping sensitive mutations in a service, not a controller

Don't scatter logging calls across controllers. Put the write + log in one place so the event taxonomy stays consistent:

class UpdateProfile
{
    public function handle(User $user, array $data): User
    {
        $changed = array_keys($user->getDirty()); // capture before save
        $user->update($data);

        UserActivities::log('profile.updated', $user, [
            'changed' => $changed,
            'actor'   => auth()->id(),
        ]);

        return $user;
    }
}

This keeps the log call testable and lets you swap the package later behind one seam.

3. Polymorphic records on business entities

Use the same call shape against a domain model so audits read consistently across the app:

UserActivities::log('invoice.paid', $invoice, [
    'amount' => $invoice->total,
    'method' => 'card',
]);

The subject becomes the invoice, the causer stays the authenticated user. This is the pattern that makes the package worth using over plain Log::info — you get filterable history for any model from one table.

4. Automatic request logging via middleware

If the package ships a request middleware (check the provider/config), register it globally or on the web group only:

// app/Http/Kernel.php or bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [
        \TomatoPHP\UserActivities\Http\Middleware\LogUserActivity::class, // verify FQCN
    ]);
});

Scope it to web (or specific route groups). Global registration logs asset/healthcheck noise and, worse, runs on unauthenticated requests.

5. Filament / TomatoPHP admin integration

The package's value in the Tomato ecosystem is the ready-made timeline. Use a relation manager on the User resource rather than building a table by hand:

public static function getRelations(): array
{
    return [
        \TomatoPHP\UserActivities\Filament\RelationManagers\ActivitiesRelationManager::class,
    ];
}

Verify the class exists in your installed version (php artisan filament:list won't show it — grep the vendor directory). If it doesn't, build a plain Table over the activities relation with event, created_at, causer.name, and a JSON column for properties.

6. Retention as a scheduled job, not a config hope

Config retention flags are advisory — nothing prunes unless you schedule it. Add a daily command:

// routes/console.php
Schedule::command('model:prune')->daily(); // if the model implements Prunable
// or a custom command:
Schedule::call(fn () => Activity::where('created_at', '<', now()->subDays(config('tomato-user-activities.retention', 90)))->delete())->daily();

Prefer chunked deletes (->chunkById(1000, ...)) once the table is over a few million rows.

7. Explicit event naming convention

Define an enum or constant list before instrumenting 20 places. Filtering a timeline by event is the primary query path; free-form strings rot fast:

enum ActivityEvent: string {
    case Login = 'auth.login';
    case ProfileUpdated = 'profile.updated';
    case InvoicePaid = 'invoice.paid';
}

Gotchas and Tips

PII leaks into properties faster than you expect. A $user->toArray() in the properties payload will happily store emails, phone numbers, and occasionally a hashed password if the model exposes it. Whitelist keys explicitly and add a global $hidden sanity check on any model you log. This is the single biggest compliance risk with audit packages.

Auth resolution fails outside HTTP. Console commands, queued jobs, and API requests with token guards often yield null for auth()->user(), so causer is empty. Pass the actor explicitly (['causer_id' => $userId] or the package's equivalent) rather than relying on the global helper.

IP and user agent are wrong behind a load balancer. Trust proxies (TrustProxies middleware with correct $proxies) or you'll record your nginx IP for every user — and the package will store it faithfully, making it look correct.

Double logging. If you enable both the request middleware and manual log() calls on model events, one update produces two rows. Pick one strategy per surface: middleware for coarse request trails, manual calls for semantic business events.

Table growth is the real operational cost. The activities table outgrows the users table by 100x within a year on an active app. Add an index on (subject_type, subject_id, created_at) and (causer_id, created_at) in a follow-up migration — the package's own migration may not include them, and altering a large table later is painful.

Migration conflicts and key types. If your users table uses UUIDs/ULIDs and the published migration declares unsignedBigInteger on the causer column, the FK (if any) or the query will break silently — records just won't match. Diff the migration against Schema::getColumnType('users', 'id') before running it on anything with existing data.

Queues and transactions. Logging inside a DB::transaction that later rolls back leaves a phantom event if the log write is not on the same connection. Conversely, logging after commit means a crash between commit and log loses the event. Decide deliberately; for audit purposes, log after commit and accept the rare gap.

Version pinning and vendor risk. This is a 0.x-adjacent, single-maintainer package with zero dependents and changelogs that are literally "Full Changelog: link" — v1.0.3 discloses nothing. Pin ^1.0 and read the diff between tags before upgrading. Keep the write path behind a small internal service interface (see pattern #2) so you can swap to spatie/laravel-activitylog in an afternoon if the package stalls.

Testing. Use RefreshDatabase and assert on the table directly rather than the package's API, so your tests survive a package swap:

$this->actingAs($user)->put('/profile', ['name' => 'New']);
$this->assertDatabaseHas('user_activities', [
    'event' => 'profile.updated',
    'causer_id' => $user->id,
]);

Extension points worth knowing. The configurable model class in config lets you subclass and add scopes/accessors without forking. The publishable config is also the safest place to disable auto-logging in testing and local environments — otherwise your test suite fills the table and your local dev timeline is 90% seeder noise.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin