pdphilip / omnievent
OmniEvent for Laravel is a Laravel Model event tracking and searching with Elasticsearch module
Fund package maintenance!
pdphilip
Installs: 20 764
Dependents: 0
Suggesters: 0
Security: 0
Stars: 3
Watchers: 1
Forks: 0
Open Issues: 2
pkg:composer/pdphilip/omnievent
Requires
- php: ^8.2
- illuminate/contracts: ^10.0||^11.0||^12.0
- pdphilip/cf-request: ^3
- pdphilip/elasticsearch: ^5
- pdphilip/omniterm: ^2
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^2||^3
- laravel/pint: ^1.14
- nunomaduro/collision: ^7.10.0||^8.1.1
- orchestra/testbench: ^8.0||^9.0||^10.0
- pestphp/pest: ^2||^3
- pestphp/pest-plugin-arch: ^2||^3
- pestphp/pest-plugin-laravel: ^2||^3
- phpstan/extension-installer: ^1.3
- phpstan/phpstan-deprecation-rules: ^1||^2
- phpstan/phpstan-phpunit: ^1||^2
README
OmniEvent for Laravel
Track model events in Elasticsearch. Query them with Eloquent.
$user->triggerEvent('login', ['ip' => $request->ip()]); $user->triggerEvent('purchase', ['amount' => 99.95, 'currency' => 'USD']); $user->triggerEvent('password_reset');
Every event is stored as a document in Elasticsearch with a timestamp, optional metadata, and automatic request context (IP, browser, device, geolocation) via CfRequest.
Query events using the full Elasticsearch Eloquent builder - the same API you already know from Laravel-Elasticsearch:
// Quick search - returns Collection $logins = User::eventSearch('login'); // Full power - Elasticsearch Eloquent builder User::viaEvents() ->where('event', 'purchase') ->where('meta.amount', '>=', 50) ->where('created_at', '>=', now()->subDays(30)) ->orderByDesc('created_at') ->paginate(20);
Events are automatically cleaned up when a model is deleted.
Requirements
| Version | |
|---|---|
| PHP | 8.2+ |
| Laravel | 10 / 11 / 12 |
| Elasticsearch | 8.x |
Installation
composer require pdphilip/omnievent
php artisan omnievent:install
This publishes config/omnievent.php.
Requires a Laravel-Elasticsearch connection. See configuration guide.
Quick Start
1. Generate an event model
php artisan omnievent:make User
This creates app/Models/Events/UserEvent.php:
namespace App\Models\Events; use App\Models\User; use PDPhilip\OmniEvent\EventModel; class UserEvent extends EventModel { protected $baseModel = User::class; public function modelType(User $model): ?string { return null; } }
2. Add the trait
use PDPhilip\OmniEvent\Eventable; class User extends Model { use Eventable; }
That's it. The Elasticsearch index is created automatically on first boot.
3. Track events
$user->triggerEvent('login'); $user->triggerEvent('purchase', ['amount' => 49.99, 'currency' => 'EUR']);
Querying Events
Quick Search
Returns a Collection of event records matching the event name:
$logins = User::eventSearch('login');
Full Builder
Returns the Elasticsearch Eloquent builder for the event model. Chain any query method you'd use on a normal Eloquent model:
// Filter by event type and date range User::viaEvents() ->where('event', 'purchase') ->where('created_at', '>=', now()->subMonth()) ->get(); // Paginate User::viaEvents() ->where('event', 'login') ->orderByDesc('created_at') ->paginate(15); // Count User::viaEvents() ->where('event', 'purchase') ->count(); // Filter by metadata User::viaEvents() ->where('meta.currency', 'USD') ->where('meta.amount', '>=', 100) ->get(); // Filter by request context User::viaEvents() ->where('request.country', 'US') ->where('request.deviceType', 'mobile') ->get();
Since viaEvents() returns a standard Elasticsearch Eloquent builder, you get access to everything: where, orderBy, limit, paginate, count, aggregate, searchTerm, distinct, and more. See the Laravel-Elasticsearch docs for the full query API.
Event Record Structure
Each event is stored as an Elasticsearch document with these fields:
| Field | Type | Description |
|---|---|---|
model_id |
keyword | The base model's primary key |
model_type |
keyword | Optional type from modelType() method |
event |
keyword | The event name |
ts |
integer | Unix timestamp |
meta |
flattened | Custom metadata (any key-value data) |
created_at |
datetime | Laravel timestamp |
When save_request is enabled in config, each event also captures:
| Field | Type | Description |
|---|---|---|
request.ip |
keyword | Client IP address |
request.browser |
keyword | Browser name and version |
request.device |
keyword | Device brand and model |
request.deviceType |
keyword | desktop, mobile, tablet, tv |
request.os |
keyword | Operating system |
request.country |
keyword | ISO country code |
request.region |
keyword | Region/state |
request.city |
keyword | City |
request.postal_code |
keyword | Postal code |
request.lat |
float | Latitude |
request.lon |
float | Longitude |
request.timezone |
keyword | Timezone |
request.is_bot |
boolean | Bot detection flag |
request.geo |
geo_point | GeoJSON point for geo queries |
Request data is captured automatically via CfRequest. Behind Cloudflare, you get full geolocation. Without Cloudflare, you still get IP, browser, device, and bot detection.
Model Types
The modelType() method on your event model lets you tag events with a model subtype. Useful for segmenting events by user role, account tier, or any model attribute:
class UserEvent extends EventModel { protected $baseModel = User::class; public function modelType(User $model): ?string { return $model->plan; // 'free', 'pro', 'enterprise' } }
Then filter by type:
User::viaEvents() ->where('model_type', 'pro') ->where('event', 'export') ->count();
Return null to skip the type field entirely.
Resolving the Base Model
Every event record can resolve back to its source model:
$event = UserEvent::where('event', 'login')->first(); // Via relationship (eager-loadable) $event->model; // User instance $event->model->name; // 'David' // Via direct lookup $event->asModel(); // User instance or null // Eager load across a collection $events = UserEvent::where('event', 'login') ->with('model') ->get();
The base model is resolved from the $baseModel property on your event model. If not set, OmniEvent guesses it from the table name (user_events -> User).
Automatic Cleanup
When a model is deleted, all its events are automatically removed:
$user->delete(); // All UserEvent records for this user are deleted
This is registered via the Eventable trait's boot method. No manual cleanup needed.
Configuration
// config/omnievent.php return [ // Elasticsearch connection name 'database' => 'elasticsearch', // Throw exceptions on event save failure (false = log and return false) 'throw_exceptions' => true, // Capture request metadata (IP, browser, geo) with each event 'save_request' => true, // Namespaces for model resolution 'namespaces' => [ 'models' => 'App\Models', 'events' => 'App\Models\Events', ], // File paths (relative to app/) for event model discovery 'app_paths' => [ 'models' => 'Models/', 'events' => 'Models/Events/', ], ];
Error Handling
By default, exceptions from Elasticsearch are thrown. Set throw_exceptions to false to silently log errors and return false from triggerEvent():
// config/omnievent.php 'throw_exceptions' => false,
// Now returns false instead of throwing $success = $user->triggerEvent('login'); // false if ES is down
This is useful in production where event tracking should never break your application flow.
API Reference
Eventable Trait (on your base model)
| Method | Returns | Description |
|---|---|---|
$model->triggerEvent(string $event, array $meta = []) |
bool |
Save an event with optional metadata |
Model::viaEvents() |
Builder |
Elasticsearch Eloquent builder for the event model |
Model::eventSearch(string $event) |
Collection |
Shorthand for viaEvents()->where('event', $event)->get() |
EventModel (your event model class)
| Method | Returns | Description |
|---|---|---|
$event->model() |
BelongsTo |
Relationship to the base model (eager-loadable) |
$event->asModel() |
?Model |
Direct lookup of the base model |
EventModel::saveEvent(Model $model, string $event, array $meta = []) |
bool |
Create an event record |
EventModel::deleteAllEvents(Model $model) |
void |
Delete all events for a model |
EventModel::validateSchema() |
array |
Create the Elasticsearch index if it doesn't exist |
Changelog
See CHANGELOG for recent changes.
Credits
License
The MIT License (MIT). See License File for details.