### Example URL with Filters
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/3.essentials/2.filament-ui.md
An example of a URL demonstrating the use of query parameters for filtering. These parameters correspond to the `#[Url]` attributes defined in the Livewire component.
```text
/admin/people/42?type=activity_log&from=2026-01-01&to=2026-04-30
```
--------------------------------
### Setup BeforeEach Hook
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/5.testing/1.index.md
Log in an admin user and create a record before each test that interacts with the timeline UI.
```php
use App\Models\Person;
use App\Models\User;
beforeEach(function (): void {
$this->actingAs(User::factory()->admin()->create());
$this->record = Person::factory()->create();
});
```
--------------------------------
### Install Package via Composer
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/1.getting-started/1.installation.md
Use this command to add the relaticle/activity-log package to your project dependencies.
```bash
composer require relaticle/activity-log
```
--------------------------------
### Setup Base Test Case
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/5.testing/1.index.md
Configure the base test case for Pest, enabling database refreshing for feature tests.
```php
// tests/Pest.php
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class)->in('Feature');
```
--------------------------------
### Minimal Custom Timeline Renderer
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/3.essentials/4.customization.md
Implement a custom timeline renderer by extending the TimelineRenderer contract. This example delegates to ActivityLogSummary for data and renders a custom Blade view.
```php
use Illuminate\Contracts\View\View;
use Relaticle\ActivityLog\Contracts\TimelineRenderer;
use Relaticle\ActivityLog\Support\ActivityLogSummary;
use Relaticle\ActivityLog\Timeline\TimelineEntry;
final class AuditedActivityLogRenderer implements TimelineRenderer
{
public function render(TimelineEntry $entry): View
{
$summary = ActivityLogSummary::from($entry);
return view('audit.timeline.entry', [
'entry' => $entry,
'summary' => $summary,
]);
}
}
```
--------------------------------
### Blade View for Custom Timeline Entry
Source: https://context7.com/relaticle/activity-log/llms.txt
Example Blade view for rendering a custom timeline entry, utilizing Filament components and displaying entry details.
```blade
{{-- resources/views/app/timeline/email-sent.blade.php --}}
{{ $entry->title }}
{{ $entry->description }} · {{ $entry->occurredAt->diffForHumans() }}
```
--------------------------------
### Retrieve Timeline Entries
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/3.essentials/3.refining-the-timeline.md
Fetch timeline entries using `get()` for a collection, `paginate()` for paginated results, or `count()` for the total number of entries. Note that `count()` runs `get()` internally.
```php
$entries = $record->timeline()->fromActivityLog()->get();
```
```php
$page = $record->timeline()->fromActivityLog()->paginate(perPage: 25, page: 2);
```
```php
$total = $record->timeline()->fromActivityLog()->count();
```
--------------------------------
### Get entries from the subject's own activity log
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/3.essentials/1.sources.md
Use `fromActivityLog()` to register the `ActivityLogSource`. This reads from the `activity_log` table where `subject_type` and `subject_id` match the subject. The subject must be persisted before calling `timeline()`. The default priority is 10.
```php
public function fromActivityLog(?int $priority = null): self
```
```php
$entries = $record->timeline()
->fromActivityLog()
->get();
```
--------------------------------
### Enable Per-Call Caching with TTL
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/3.essentials/5.caching.md
Wrap a `paginate()` call with `cached()` to enable per-call caching. Specify the Time-To-Live in seconds. This should be called before `paginate()` or `get()`.
```php
$entries = $record->timeline()->cached(ttlSeconds: 300)->paginate();
```
--------------------------------
### Get entries from related models' activity logs
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/3.essentials/1.sources.md
Use `fromActivityLogOf()` to register the `RelatedActivityLogSource`. This loads related rows and then queries `activity_log` for matching `(subject_type, subject_id)` pairs. Be aware of potential performance issues with relations containing thousands of rows, as all related rows are loaded into memory.
```php
public function fromActivityLogOf(array $relations, ?int $priority = null): self
```
```php
$entries = $opportunity->timeline()
->fromActivityLogOf(['comments', 'tasks'])
->get();
```
--------------------------------
### Publish Configuration File
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/1.getting-started/1.installation.md
Run this Artisan command to publish the configuration file if you need to override default settings.
```bash
php artisan vendor:publish --tag=activity-log-config
```
--------------------------------
### Run Tests
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/7.community/1.contributing.md
Execute the project's test suite using Composer.
```bash
composer test
```
--------------------------------
### Registering Timeline Sources
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/2.concepts/1.how-it-works.md
Demonstrates how to chain different built-in sources to the TimelineBuilder, including custom configurations for related models.
```php
$record->timeline()
->fromActivityLog() // ActivityLogSource
->fromActivityLogOf(['comments']) // RelatedActivityLogSource
->fromRelation('invoices', fn ($s) => $s->title(...)) // RelatedModelSource
->fromCustom(fn ($subject, $window) => yield ...); // CustomEventSource
```
--------------------------------
### Make Model Timeline-Capable
Source: https://github.com/relaticle/activity-log/blob/1.x/README.md
Implement the HasTimeline contract and use the InteractsWithTimeline concern in your Eloquent model. Define the timeline method to configure the TimelineBuilder, starting with the fromActivityLog source.
```php
use Relaticle\ActivityLog\Concerns\InteractsWithTimeline;
use Relaticle\ActivityLog\Contracts\HasTimeline;
use Relaticle\ActivityLog\Timeline\TimelineBuilder;
use Spatie\Activitylog\Traits\LogsActivity;
class Person extends Model implements HasTimeline
{
use InteractsWithTimeline;
use LogsActivity;
public function timeline(): TimelineBuilder
{
return TimelineBuilder::make($this)->fromActivityLog();
}
}
```
--------------------------------
### Activity Log Configuration Reference
Source: https://context7.com/relaticle/activity-log/llms.txt
This is a reference for the `config/activity-log.php` file, showing default values and options for pagination, deduplication, source priorities, renderers, and caching.
```php
// config/activity-log.php (after vendor:publish --tag=activity-log-config)
return [
'default_per_page' => 20, // used by paginate(null); UI surfaces have own defaults
'pagination_buffer' => 2, // cap = perPage × (page + buffer); increase for heavy dedup
'deduplicate_by_default' => true, // override per-call with ->deduplicate(false)
'source_priorities' => [
'activity_log' => 10, // ActivityLogSource default priority
'related_activity_log' => 10, // RelatedActivityLogSource priority (entries still type='activity_log')
'related_model' => 20, // RelatedModelSource default priority
'custom' => 30, // CustomEventSource default priority
],
'renderers' => [
// 'email_sent' => \App\Timeline\Renderers\EmailSentRenderer::class,
],
'cache' => [
'store' => null, // null = default cache; set to dedicated store name recommended
'ttl_seconds' => 0, // reserved; use ->cached($ttl) on the builder instead
'key_prefix' => 'activity-log', // prefix for all timeline cache keys
],
];
```
--------------------------------
### Publish Activity Log Configuration
Source: https://context7.com/relaticle/activity-log/llms.txt
Optionally publish the package's configuration file to customize its behavior. This command makes the configuration file available for modification.
```bash
# Optionally publish the config
php artisan vendor:publish --tag=activity-log-config
```
--------------------------------
### Format Code
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/7.community/1.contributing.md
Format the project's code according to the defined style using Composer.
```bash
composer pint
```
--------------------------------
### Test Header Action
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/5.testing/1.index.md
Verify that clicking the default 'activityLog' header action correctly opens the activity log slide-over.
```php
use App\Filament\Resources\People\Pages\ViewPerson;
use function Pest\Livewire\livewire;
it('opens the activity log slide-over', function (): void {
livewire(ViewPerson::class, ['record' => $this->record->getKey()])
->callAction('activityLog')
->assertSuccessful();
});
```
--------------------------------
### Use Activity Factory
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/5.testing/1.index.md
Demonstrates how to use the custom Activity factory to create an activity associated with a record. Ensure Laravel's factory resolver is overridden in a service provider.
```php
use Database\Factories\Spatie\ActivityFactory;
ActivityFactory::new()->for($this->record)->create();
```
--------------------------------
### Register and Unregister Renderers via Facade
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/3.essentials/4.customization.md
Demonstrates registering a renderer using its class string and then unregistering it. Unregistering is useful for tests or conditional logic to revert to default renderers.
```php
use Relaticle\ActivityLog\Facades\Timeline;
Timeline::registerRenderer('email_sent', \App\Timeline\Renderers\EmailSentRenderer::class);
Timeline::unregisterRenderer('email_sent');
```
--------------------------------
### Implementing a Reusable Timeline Source
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/3.essentials/1.sources.md
Create a class that implements the `TimelineSource` contract for reusable sources. This allows for constructor dependencies and better testability. Register the instance using `addSource()`.
```php
namespace App\Timeline\Sources;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Model;
use Relaticle\ActivityLog\Contracts\TimelineSource;
use Relaticle\ActivityLog\Timeline\TimelineEntry;
use Relaticle\ActivityLog\Timeline\Window;
final class StripePaymentSource implements TimelineSource
{
public function __construct(private readonly int $priority = 30) {}
public function priority(): int
{
return $this->priority;
}
public function resolve(Model $subject, Window $window): iterable
{
$query = $subject->stripePayments()
->orderByDesc('created_at')
->limit($window->cap);
if ($window->from instanceof CarbonImmutable) {
$query->where('created_at', '>=', $window->from);
}
foreach ($query->cursor() as $payment) {
yield new TimelineEntry(
id: "stripe:payment:{$payment->id}",
type: 'custom',
event: "payment.{$payment->status}",
occurredAt: CarbonImmutable::parse($payment->created_at),
dedupKey: "stripe:payment:{$payment->id}",
sourcePriority: $this->priority,
subject: $subject,
title: "Payment {$payment->amount_formatted}",
);
}
}
}
```
--------------------------------
### Retrieve Own Spatie Activity Log Entries
Source: https://context7.com/relaticle/activity-log/llms.txt
Use `fromActivityLog()` to register the `ActivityLogSource` for reading the `activity_log` table where the subject matches. Subject must be persisted.
```php
$entries = $person->timeline()
->fromActivityLog() // default priority: 10 (config: source_priorities.activity_log)
// ->fromActivityLog(priority: 50) // per-call override
->get();
// Each entry has:
// $entry->type => 'activity_log'
// $entry->event => 'created' | 'updated' | 'deleted' | 'restored'
// $entry->causer => User model or null
// $entry->properties => ['attributes' => [...], 'old' => [...]]
// $entry->occurredAt => CarbonImmutable
```
--------------------------------
### Implement Custom Timeline Source with addSource()
Source: https://context7.com/relaticle/activity-log/llms.txt
Implement the TimelineSource contract to create reusable custom sources. Register them using addSource() to integrate with the activity log timeline. Constructor dependencies can be injected.
```php
namespace App\Timeline\Sources;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Model;
use Relaticle\ActivityLog\Contracts\TimelineSource;
use Relaticle\ActivityLog\Timeline\TimelineEntry;
use Relaticle\ActivityLog\Timeline\Window;
final class StripePaymentSource implements TimelineSource
{
public function __construct(private readonly int $priority = 30) {}
public function priority(): int
{
return $this->priority;
}
public function resolve(Model $subject, Window $window): iterable
{
$query = $subject->stripePayments()
->orderByDesc('created_at')
->limit($window->cap);
if ($window->from instanceof CarbonImmutable) {
$query->where('created_at', '>=', $window->from);
}
foreach ($query->cursor() as $payment) {
yield new TimelineEntry(
id: "stripe:payment:{$payment->id}",
type: 'custom',
event: "payment.{$payment->status}",
occurredAt: CarbonImmutable::parse($payment->created_at),
dedupKey: "stripe:payment:{$payment->id}",
sourcePriority: $this->priority,
subject: $subject,
title: "Payment {$payment->amount_formatted}",
);
}
}
}
// Registration
$person->timeline()
->fromActivityLog()
->addSource(new StripePaymentSource(priority: 50));
```
--------------------------------
### fromActivityLog()
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/3.essentials/1.sources.md
Registers the ActivityLogSource to include entries from the subject's own spatie activity log. Reads from the 'activity_log' table where subject_type and subject_id match the subject. Each row becomes a TimelineEntry with type='activity_log'. The subject must be persisted. An optional priority can be set.
```APIDOC
## `fromActivityLog()`
Registers `Relaticle
ActivityLog
Timeline
Sources
ActivityLogSource` — the subject's own spatie activity log.
```php
public function fromActivityLog(?int $priority = null): self
```
```php
$entries = $record->timeline()
->fromActivityLog()
->get();
```
Reads from the `activity_log` table where `subject_type` and `subject_id` match the subject. Each row becomes a `TimelineEntry` with `type='activity_log'`.
::callout{icon="i-lucide-alert-triangle" color="warning"}
**Subject must be persisted.** `ActivityLogSource::resolve()` throws a `DomainException` when `$subject->getKey() === null`. Don't call `timeline()` from a `creating` model event or on a fresh, unsaved instance.
::
`$priority` overrides the default of `10` (see `source_priorities.activity_log`).
```
--------------------------------
### Registering a Custom Event Source
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/3.essentials/1.sources.md
Use `fromCustom` to register a closure that yields `TimelineEntry` instances directly. Ensure the closure respects the `Window`'s cap, from, and to parameters to manage memory and filtering. Yielding types other than `TimelineEntry` will result in a `TypeError`.
```php
use Carbon\CarbonImmutable;
use Relaticle\ActivityLog\Timeline\TimelineEntry;
use Relaticle\ActivityLog\Timeline\Window;
$record->timeline()
->fromCustom(function ($subject, Window $window) {
$query = $subject->stripeCharges()
->orderByDesc('created_at')
->limit($window->cap);
if ($window->from instanceof CarbonImmutable) {
$query->where('created_at', '>=', $window->from);
}
if ($window->to instanceof CarbonImmutable) {
$query->where('created_at', '<=', $window->to);
}
foreach ($query->cursor() as $charge) {
yield new TimelineEntry(
id: "stripe:charge:{$charge->id}",
type: 'custom',
event: 'charge.succeeded',
occurredAt: CarbonImmutable::parse($charge->created_at),
dedupKey: "stripe:charge:{$charge->id}",
sourcePriority: 30,
subject: $subject,
title: "Charge {$charge->amount_formatted}",
);
}
});
```
--------------------------------
### Display Activity Log in Person View
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/4.recipes/1.crm-person-feed.md
Integrate the `ActivityLog` infolist component into the Person view page to display the activity feed. Configure options like grouping by date and items per page.
```php
use Relaticle\ActivityLog\Filament\Infolists\Components\ActivityLog;
public function infolist(Schema $schema): Schema
{
return $schema->components([
// ... your existing entries
ActivityLog::make('feed')
->heading('Activity')
->groupByDate()
->perPage(20)
->columnSpanFull(),
]);
}
```
--------------------------------
### Adding a Reusable Timeline Source Instance
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/3.essentials/1.sources.md
Register an instance of a custom `TimelineSource` implementation with the activity log. This is preferred over `fromCustom` for reusable or complex sources.
```php
$record->timeline()
->fromActivityLog()
->addSource(new StripePaymentSource());
```
--------------------------------
### Activity Log URL Filter Parameter Mapping
Source: https://context7.com/relaticle/activity-log/llms.txt
Explains the mapping between URL parameters (`type`, `from`, `to`) and their corresponding builder method calls in the Activity Log package.
```php
// The three URL parameters map to builder calls internally:
// ?type=activity_log → $builder->ofType(['activity_log'])
// ?from=2026-01-01 → $builder->between(CarbonImmutable::parse('2026-01-01'), ...)
// ?to=2026-04-30 → $builder->between(..., CarbonImmutable::parse('2026-04-30'))
// Valid ?type values: 'activity_log', 'related_model', 'custom'
// 'related_activity_log' matches nothing — see type taxonomy in docs
```
--------------------------------
### Registering Custom Events with `fromCustom`
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/3.essentials/1.sources.md
Use the `fromCustom` method to register a closure that yields `TimelineEntry` instances directly. This is useful for ad-hoc or model-specific event generation. The closure receives the subject and a `Window` object to manage data fetching.
```APIDOC
## `fromCustom(Closure $resolver, ?int $priority = null)`
Registers `Relaticle\ActivityLog\Timeline\Sources\CustomEventSource` — yields `Relaticle\ActivityLog\Timeline\TimelineEntry` instances directly, with no assumptions about where they come from.
### Parameters
#### Path Parameters
- **resolver** (Closure) - Required - A closure that resolves timeline entries.
- **priority** (int) - Optional - The priority of this source.
### Request Example
```php
use Carbon\CarbonImmutable;
use Relaticle\ActivityLog\Timeline\TimelineEntry;
use Relaticle\ActivityLog\Timeline\Window;
$record->timeline()
->fromCustom(function ($subject, Window $window) {
$query = $subject->stripeCharges()
->orderByDesc('created_at')
->limit($window->cap);
if ($window->from instanceof CarbonImmutable) {
$query->where('created_at', '>=', $window->from);
}
if ($window->to instanceof CarbonImmutable) {
$query->where('created_at', '<=', $window->to);
}
foreach ($query->cursor() as $charge) {
yield new TimelineEntry(
id: "stripe:charge:{$charge->id}",
type: 'custom',
event: 'charge.succeeded',
occurredAt: CarbonImmutable::parse($charge->created_at),
dedupKey: "stripe:charge:{$charge->id}",
sourcePriority: 30,
subject: $subject,
title: "Charge {$charge->amount_formatted}",
);
}
});
```
### Notes
- The closure receives the subject and the active `Window` — respect `cap`, `from`, and `to` to keep memory bounded and honor `->between(...)` filters.
- Yielding anything other than a `TimelineEntry` instance throws `TypeError`.
```
--------------------------------
### Embed Activity Log in Filament Infolist
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/3.essentials/2.filament-ui.md
Use the `ActivityLog` infolist component to display the timeline within an existing Filament infolist. Configure options like grouping by date, items per page, and infinite scrolling.
```php
use Relaticle\ActivityLog\Filament\Infolists\Components\ActivityLog;
ActivityLog::make('timeline')
->groupByDate()
->perPage(10)
->emptyState('Nothing here yet — give it time.')
->infiniteScroll()
->columnSpanFull(),
```
--------------------------------
### Make Model Timeline-Capable
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/1.getting-started/2.quick-start.md
Integrates a model with the activity log and timeline functionality. Ensures Spatie's LogsActivity records changes and the model can generate a timeline from its activity log.
```php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Relaticle\ActivityLog\Concerns\InteractsWithTimeline;
use Relaticle\ActivityLog\Contracts\HasTimeline;
use Relaticle\ActivityLog\Timeline\TimelineBuilder;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
final class Person extends Model implements HasTimeline
{
use InteractsWithTimeline;
use LogsActivity;
protected $fillable = ['name', 'email'];
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()->logFillable();
}
public function timeline(): TimelineBuilder
{
return TimelineBuilder::make($this)->fromActivityLog();
}
}
```
--------------------------------
### Test Infolist Entry Rendering
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/5.testing/1.index.md
Assert that an activity log entry is rendered on the person view page after a record update. This test targets the default diff sentence output.
```php
use App\Filament\Resources\People\Pages\ViewPerson;
use function Pest\Livewire\livewire;
it('renders an entry on the person view page after an update', function (): void {
$this->record->update(['name' => 'Updated name']);
livewire(ViewPerson::class, ['record' => $this->record->getKey()])
->assertSeeHtml('Updated name');
});
```
--------------------------------
### Trigger Activity via Tinker
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/1.getting-started/2.quick-start.md
Demonstrates how to trigger an activity log entry by updating a model instance using Artisan Tinker. This is useful for testing the timeline functionality.
```bash
php artisan tinker --execute 'App\Models\Person::find(1)->update(["name" => "New name"]);'
```
--------------------------------
### Add Activity Log Action to Filament Header
Source: https://context7.com/relaticle/activity-log/llms.txt
Use `ActivityLogAction` to open the timeline slide-over from any page header. Standard Filament action overrides apply.
```php
use Relaticle\ActivityLog\Filament\Actions\ActivityLogAction;
protected function getHeaderActions(): array
{
return [
ActivityLogAction::make(),
// Standard Filament action overrides work normally
// ActivityLogAction::make()->label('History')->color('primary'),
];
}
// In tests
$this->callAction('activityLog');
```
--------------------------------
### Configure Default Renderers in Config File
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/3.essentials/4.customization.md
Set static default renderers in the `config/activity-log.php` file. This is suitable for application-wide defaults that do not depend on runtime state.
```php
// config/activity-log.php
'renderers' => [
'email_sent' => \App\Timeline\Renderers\EmailSentRenderer::class,
],
```
--------------------------------
### Assert Timeline Entries Directly
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/5.testing/1.index.md
Test the activity log builder by creating an activity entry and asserting the properties of the generated timeline entries.
```php
use Spatie\Activitylog\Models\Activity;
it('emits an entry per activity row', function (): void {
Activity::create([
'log_name' => 'default',
'description' => 'updated',
'event' => 'updated',
'subject_type' => $this->record->getMorphClass(),
'subject_id' => $this->record->getKey(),
'properties' => ['attributes' => ['name' => 'New'], 'old' => ['name' => 'Old']],
]);
$entries = $this->record->timeline()->fromActivityLog()->get();
expect($entries)
->toHaveCount(1)
->and($entries->first()->event)->toBe('updated');
});
```
--------------------------------
### Add Activity Log Header Action
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/3.essentials/2.filament-ui.md
Register the `ActivityLogAction` in your resource's `getHeaderActions()` method to enable a slide-over panel displaying the activity log from any page header.
```php
use Relaticle\ActivityLog\Filament\Actions\ActivityLogAction;
protected function getHeaderActions(): array
{
return [
ActivityLogAction::make(),
];
}
```
--------------------------------
### Add Activity Log Infolist to Person Resource
Source: https://context7.com/relaticle/activity-log/llms.txt
Integrates the ActivityLog component into the 'view' page of the Person resource using Filament's infolist schema. This displays the timeline feed.
```php
public function infolist(Schema $schema): Schema
{
return $schema->components([
ActivityLog::make('feed')
->heading('Activity')
->groupByDate()
->perPage(20)
->columnSpanFull(),
]);
}
```
--------------------------------
### Register Custom Renderers via Facade
Source: https://context7.com/relaticle/activity-log/llms.txt
Register or unregister timeline renderers globally at runtime using the `Timeline` facade. Useful for dynamic registration or in tests.
```php
use Relaticle\ActivityLog\Facades\Timeline;
// In AppServiceProvider::boot()
Timeline::registerRenderer('email_sent', \App\Timeline\Renderers\EmailSentRenderer::class);
// Unregister (useful in tests or when a feature flag is off)
Timeline::unregisterRenderer('email_sent');
```
--------------------------------
### Register Custom Timeline Events from a Closure
Source: https://context7.com/relaticle/activity-log/llms.txt
Use `fromCustom()` to register timeline entries generated by a closure. The closure receives the subject and a `Window` object. It must yield `TimelineEntry` instances and respect the `$window->cap`, `$window->from`, and `$window->to` parameters for efficient data retrieval. A `TypeError` is thrown if the closure yields anything other than `TimelineEntry` instances.
```php
use Carbon\CarbonImmutable;
use Relaticle\ActivityLog\Timeline\TimelineEntry;
use Relaticle\ActivityLog\Timeline\Window;
$person->timeline()
->fromCustom(function ($subject, Window $window) {
$query = $subject->stripeCharges()
->orderByDesc('created_at')
->limit($window->cap);
if ($window->from instanceof CarbonImmutable) {
$query->where('created_at', '>=', $window->from);
}
if ($window->to instanceof CarbonImmutable) {
$query->where('created_at', '<=', $window->to);
}
foreach ($query->cursor() as $charge) {
yield new TimelineEntry(
id: "stripe:charge:{$charge->id}",
type: 'custom',
event: 'charge.succeeded',
occurredAt: CarbonImmutable::parse($charge->created_at),
dedupKey: "stripe:charge:{$charge->id}",
sourcePriority: 30,
subject: $subject,
title: "Charge {$charge->amount_formatted}",
icon: 'heroicon-o-credit-card',
color: 'success',
);
}
})
->get();
// TypeError is thrown if the closure yields anything other than TimelineEntry
```
--------------------------------
### Build and Paginate Timeline Entries with TimelineBuilder
Source: https://context7.com/relaticle/activity-log/llms.txt
Use TimelineBuilder to chain sources, filters, and options to retrieve paginated activity log entries. Ensure the subject is persisted when using `fromActivityLog()`.
```php
use Relaticle\ActivityLog\Timeline\TimelineBuilder;
use Relaticle\ActivityLog\Timeline\Sources\RelatedModelSource;
use Carbon\CarbonImmutable;
$entries = TimelineBuilder::make($person)
// Sources
->fromActivityLog() // own spatie log, priority 10
->fromActivityLogOf(['notes', 'tasks']) // related models' spatie logs, priority 10
->fromRelation('invoices', fn (RelatedModelSource $s) => $s
->event('issued_at', 'invoice.issued', icon: 'heroicon-o-document-text', color: 'info')
->event('paid_at', 'invoice.paid', icon: 'heroicon-o-check-circle', color: 'success')
)
->fromCustom(fn ($subject, $window) => []) // custom closure source, priority 30
// Filtering
->between(now()->subMonth(), now()) // date window (either side nullable)
->ofType(['activity_log'])
->exceptType(['custom'])
->ofEvent(['created', 'updated'])
->exceptEvent(['draft_saved'])
// Sorting
->sortByDateDesc() // default; newest first
// ->sortByDateAsc() // oldest first
// Deduplication
->deduplicate(true) // default: true
->dedupKeyUsing(fn ($entry) => "{$entry->type}:{$entry->event}:{$entry->occurredAt->toDateString()}")
// Caching (optional, per-call; call before paginate())
->cached(ttlSeconds: 300)
// Execution
->paginate(perPage: 25, page: 1); // LengthAwarePaginator
// ->get(); // Collection (cap 10000)
// ->count(); // int (runs get() internally)
```
--------------------------------
### Adding Reusable Timeline Sources with `addSource`
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/3.essentials/1.sources.md
Implement the `TimelineSource` contract in a separate class and register it using `addSource`. This is preferred for reusable sources, sources with constructor dependencies, or those needing isolated testing.
```APIDOC
## `addSource(TimelineSource $source)`
Registers a reusable source class that implements the `Relaticle\ActivityLog\Contracts\TimelineSource` contract.
### Parameters
#### Path Parameters
- **source** (TimelineSource) - Required - An instance of a class implementing the `TimelineSource` contract.
### Request Example
```php
namespace App\Timeline\Sources;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Model;
use Relaticle\ActivityLog\Contracts\TimelineSource;
use Relaticle\ActivityLog\Timeline\TimelineEntry;
use Relaticle\ActivityLog\Timeline\Window;
final class StripePaymentSource implements TimelineSource
{
public function __construct(private readonly int $priority = 30) {}
public function priority(): int
{
return $this->priority;
}
public function resolve(Model $subject, Window $window): iterable
{
$query = $subject->stripePayments()
->orderByDesc('created_at')
->limit($window->cap);
if ($window->from instanceof CarbonImmutable) {
$query->where('created_at', '>=', $window->from);
}
foreach ($query->cursor() as $payment) {
yield new TimelineEntry(
id: "stripe:payment:{$payment->id}",
type: 'custom',
event: "payment.{$payment->status}",
occurredAt: CarbonImmutable::parse($payment->created_at),
dedupKey: "stripe:payment:{$payment->id}",
sourcePriority: $this->priority,
subject: $subject,
title: "Payment {$payment->amount_formatted}",
);
}
}
}
```
```php
$record->timeline()
->fromActivityLog()
->addSource(new StripePaymentSource());
```
```
--------------------------------
### User Model with Focused Timeline
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/4.recipes/2.audit-log-for-admins.md
Configure the User model to implement `HasTimeline` and use `LogsActivity`. Specify `logFillable()` and `logOnlyDirty()` for focused logging and filter events to CRUD operations using `ofEvent([...])` for a clean audit stream.
```php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Relaticle\ActivityLog\Concerns\InteractsWithTimeline;
use Relaticle\ActivityLog\Contracts\HasTimeline;
use Relaticle\ActivityLog\Timeline\TimelineBuilder;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
final class User extends Authenticatable implements HasTimeline
{
use InteractsWithTimeline;
use LogsActivity;
protected $fillable = ['name', 'email', 'role', 'password'];
protected $hidden = ['password', 'remember_token'];
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logFillable()
->logOnlyDirty()
->dontSubmitEmptyLogs();
}
public function isAdmin(): bool
{
return $this->role === 'admin';
}
public function timeline(): TimelineBuilder
{
return TimelineBuilder::make($this)
->fromActivityLog()
->ofEvent(['created', 'updated', 'deleted', 'restored']);
}
}
```
--------------------------------
### Register Filament Panel Plugin
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/1.getting-started/1.installation.md
This PHP code shows how to register the activity log panel plugin within your Filament panel configuration. This is necessary for custom renderers and auto-discovery covers other aspects.
```php
use Relaticle\ActivityLog\Filament\ActivityLogPlugin;
public function panel(Panel $panel): Panel
{
return $panel
->plugin(ActivityLogPlugin::make())
// ... rest of panel config
;
}
```
--------------------------------
### Paginate Timeline with Trait Helper
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/3.essentials/3.refining-the-timeline.md
Use the `paginateTimeline()` helper method from the `InteractsWithTimeline` trait for convenient pagination. This method is a pass-through to the builder's `paginate()` method and is used internally by UI components.
```php
$page = $opportunity->paginateTimeline(perPage: 20, page: 1);
```
--------------------------------
### Add Index to spatie activity_log Table
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/1.getting-started/1.installation.md
This PHP code snippet demonstrates how to add a required compound index to the `activity_log` table for optimal performance. This is crucial for responsive timeline queries.
```php
Schema::table('activity_log', function (Blueprint $table) {
$table->index(['subject_type', 'subject_id', 'created_at']);
});
```
--------------------------------
### Admin-Gated Header Action for Audit Log
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/4.recipes/2.audit-log-for-admins.md
Add the `ActivityLogAction` to the Filament User view page. Use `->visible()` with an authorization check to ensure only admins can see the 'Audit log' action.
```php
namespace App\Filament\Resources\Users\Pages;
use App\Filament\Resources\Users\UserResource;
use Filament\Resources\Pages\ViewRecord;
use Relaticle\ActivityLog\Filament\Actions\ActivityLogAction;
final class ViewUser extends ViewRecord
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
ActivityLogAction::make()
->label('Audit log')
->icon('heroicon-o-shield-check')
->visible(fn (): bool => auth()->user()?->isAdmin() ?? false),
];
}
}
```
--------------------------------
### Timeline Pipeline Flow
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/2.concepts/1.how-it-works.md
Illustrates the data flow from a record to a rendered timeline view, showing the stages of resolution, filtering, deduplication, sorting, and pagination.
```text
$record->timeline() [TimelineBuilder]
↓ resolves
TimelineSource(s) per source [resolve(subject, Window)]
↓ yields
TimelineEntry stream [filter → dedup → sort]
↓ paginated
LengthAwarePaginator → Renderer → Blade view
```
--------------------------------
### Render Activity Log in Filament Infolist
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/1.getting-started/2.quick-start.md
Adds the ActivityLog component to a Filament resource's infolist to display timeline entries. This component must be used on a page that renders a model record.
```php
namespace App\Filament\Resources;\n\nuse Filament\Schemas\Schema;\nuse Relaticle\ActivityLog\Filament\Infolists\Components\ActivityLog;\n\npublic function infolist(Schema $schema): Schema\n{\n return $schema->components([\n // ... your existing entries\n ActivityLog::make('activity')->columnSpanFull(),\n ]);\n}
```
--------------------------------
### Test Relation Manager Mounting
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/5.testing/1.index.md
Mount the Activity Log relation manager for a specific record, ensuring it attaches correctly to the host resource page.
```php
use App\Filament\Resources\People\Pages\EditPerson;
use Relaticle\ActivityLog\Filament\RelationManagers\ActivityLogRelationManager;
use function Pest\Livewire\livewire;
it('mounts the activity log relation manager for a record', function (): void {
livewire(ActivityLogRelationManager::class, [
'ownerRecord' => $this->record,
'pageClass' => EditPerson::class,
])->assertSuccessful();
});
```
--------------------------------
### Register Renderer with Class String
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/3.essentials/4.customization.md
Register a custom renderer using its class string. The renderer will be resolved through the container, allowing for dependency injection in its constructor.
```php
use Relaticle\ActivityLog\Facades\Timeline;
Timeline::registerRenderer('email_sent', \App\Timeline\Renderers\EmailSentRenderer::class);
```
--------------------------------
### Register Activity Log Filament Plugin with Custom Renderers
Source: https://context7.com/relaticle/activity-log/llms.txt
Register the `ActivityLogPlugin` when custom renderers are needed. Specify custom renderers using the `renderers()` method.
```php
use Relaticle\ActivityLog\Filament\ActivityLogPlugin;
public function panel(Panel $panel): Panel
{
return $panel
->plugin(
ActivityLogPlugin::make()->renderers([
'email_sent' => \App\Timeline\Renderers\EmailSentRenderer::class,
])
);
}
```
--------------------------------
### Retrieve Related Models' Spatie Logs
Source: https://context7.com/relaticle/activity-log/llms.txt
Use `fromActivityLogOf()` to register `RelatedActivityLogSource`, which queries `activity_log` for related model IDs. Related-log entries have a non-null `$entry->relatedModel`.
```php
$entries = $opportunity->timeline()
->fromActivityLog()
->fromActivityLogOf(['comments', 'tasks']) // default priority: 10
->get();
// Filter: related-log entries only (inspect relatedModel, not type)
$relatedOnly = $entries->filter(fn ($entry) => $entry->relatedModel !== null);
```
--------------------------------
### Use ActivityLogSummary in Custom Renderer
Source: https://context7.com/relaticle/activity-log/llms.txt
Utilize `ActivityLogSummary` within a custom renderer to parse `TimelineEntry` data from spatie activity-log, providing structured access to changes and operations.
```php
use Relaticle\ActivityLog\Contracts\TimelineRenderer;
use Relaticle\ActivityLog\Support\ActivityLogSummary;
use Relaticle\ActivityLog\Support\ActivityLogOperation;
use Relaticle\ActivityLog\Timeline\TimelineEntry;
use Illuminate\Contracts\View\View;
final class AuditedActivityLogRenderer implements TimelineRenderer
{
public function render(TimelineEntry $entry): View
{
$summary = ActivityLogSummary::from($entry);
// Available on $summary:
// ->causerName string "Alice" or "System"
// ->operation ?ActivityLogOperation Created|Updated|Deleted|Restored or null
// ->changedFieldLabels list ["Name", "Email"]
// ->summarySentence string "Alice changed Name"
// ->diffRows list
// ->hasDiff bool true only for Updated with rows
foreach ($summary->diffRows as $row) {
// $row->label "Email Address"
// $row->formattedOld() "old@example.com"
// $row->formattedNew() "new@example.com"
}
return view('audit.timeline.entry', [
'entry' => $entry,
'summary' => $summary,
]);
}
}
// ActivityLogOperation enum
$op = ActivityLogOperation::Updated;
$op->verb(); // "updated"
$op->icon(); // "ri-edit-line"
```
--------------------------------
### Register Renderer with Closure
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/3.essentials/4.customization.md
Register a custom renderer using a closure for simple, inline markup or quick prototyping. The closure receives the TimelineEntry object and should return Blade markup.
```php
use Illuminate\Support\HtmlString;
use Relaticle\ActivityLog\Facades\Timeline;
use Relaticle\ActivityLog\Timeline\TimelineEntry;
Timeline::registerRenderer('task_done', fn (TimelineEntry $entry) => new HtmlString(
"{$entry->title} finished",
));
```
--------------------------------
### Render Activity Log in Filament Infolist
Source: https://github.com/relaticle/activity-log/blob/1.x/README.md
Add the ActivityLog component to your Filament resource's infolist schema to display the activity feed. This component is designed to span the full width of the column.
```php
use Filament\Schemas\Schema;
use Relaticle\ActivityLog\Filament\Infolists\Components\ActivityLog;
public static function infolist(Schema $schema): Schema
{
return $schema->components([
ActivityLog::make('activity')->columnSpanFull(),
]);
}
```
--------------------------------
### Configure Person Model for Activity Timeline
Source: https://context7.com/relaticle/activity-log/llms.txt
Defines the Person model to include its own activity log and integrate logs from related models like emails, notes, and tasks. Customizes timeline events for specific relations.
```php
final class Person extends Model implements HasTimeline
{
use InteractsWithTimeline;
use LogsActivity;
protected $fillable = ['name', 'email', 'company'];
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()->logFillable();
}
public function emails(): HasMany { return $this->hasMany(Email::class); }
public function notes(): HasMany { return $this->hasMany(Note::class); }
public function tasks(): HasMany { return $this->hasMany(Task::class); }
public function timeline(): TimelineBuilder
{
return TimelineBuilder::make($this)
->fromActivityLog()
->fromActivityLogOf(['emails', 'notes', 'tasks'])
->fromRelation('emails', function (RelatedModelSource $source): void {
$source
->event('sent_at', 'email_sent', icon: 'heroicon-o-paper-airplane', color: 'primary')
->title(fn (Email $e): string => $e->subject ?? 'Email')
->description(fn (Email $e): string => "to {$e->recipient}")
->causer(fn (Email $e) => $e->sender);
})
->fromRelation('tasks', function (RelatedModelSource $source): void {
$source
->event('completed_at', 'task_completed', icon: 'heroicon-o-check-circle', color: 'success')
->title(fn (Task $t): string => $t->title);
});
}
}
```
--------------------------------
### Define Activity Factory
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/5.testing/1.index.md
Defines a custom factory for the Spatie Activity model. This factory includes a `for` method to associate activities with a specific subject model.
```php
namespace Database\Factories\Spatie;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\Models\Activity;
/** @extends Factory */
final class ActivityFactory extends Factory
{
protected $model = Activity::class;
public function definition(): array
{
return [
'log_name' => 'default',
'description' => $this->faker->word(),
'event' => $this->faker->randomElement(['created', 'updated', 'deleted']),
'subject_type' => null,
'subject_id' => null,
'causer_type' => null,
'causer_id' => null,
'properties' => [],
];
}
public function for(Model $subject): static
{
return $this->state([
'subject_type' => $subject->getMorphClass(),
'subject_id' => $subject->getKey(),
]);
}
}
```
--------------------------------
### Composing Timeline Sources in Person Model
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/4.recipes/1.crm-person-feed.md
Defines the `timeline()` method on the `Person` model to aggregate activity from multiple sources. It includes spatie logs from the person and related models, as well as custom events derived from timestamp columns like `sent_at` and `completed_at`. This method configures the `TimelineBuilder` to pull data from the person's own activity log, related emails, notes, and tasks, and defines custom event details for email sending and task completion.
```php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Relaticle\ActivityLog\Concerns\InteractsWithTimeline;
use Relaticle\ActivityLog\Contracts\HasTimeline;
use Relaticle\ActivityLog\Timeline\Sources\RelatedModelSource;
use Relaticle\ActivityLog\Timeline\TimelineBuilder;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
final class Person extends Model implements HasTimeline
{
use InteractsWithTimeline;
use LogsActivity;
protected $fillable = ['name', 'email', 'company'];
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()->logFillable();
}
public function emails(): HasMany
{
return $this->hasMany(Email::class);
}
public function notes(): HasMany
{
return $this->hasMany(Note::class);
}
public function tasks(): HasMany
{
return $this->hasMany(Task::class);
}
public function timeline(): TimelineBuilder
{
return TimelineBuilder::make($this)
->fromActivityLog()
->fromActivityLogOf(['emails', 'notes', 'tasks'])
->fromRelation('emails', function (RelatedModelSource $source): void {
$source
->event(column: 'sent_at', event: 'email_sent', icon: 'heroicon-o-paper-airplane', color: 'primary')
->title(fn (Email $email): string => $email->subject ?? 'Email')
->description(fn (Email $email): ?string => "to {$email->recipient}")
->causer(fn (Email $email) => $email->sender);
})
->fromRelation('tasks', function (RelatedModelSource $source): void {
$source
->event(column: 'completed_at', event: 'task_completed', icon: 'heroicon-o-check-circle', color: 'success')
->title(fn (Task $task): string => $task->title);
});
}
}
```
--------------------------------
### Register Activity Log Relation Manager
Source: https://github.com/relaticle/activity-log/blob/1.x/docs/content/3.essentials/2.filament-ui.md
Add the `ActivityLogRelationManager` to a resource to provide a dedicated 'Activity' tab. This manager automatically displays the activity log for each record.
```php
use Relaticle\ActivityLog\Filament\RelationManagers\ActivityLogRelationManager;
public static function getRelations(): array
{
return [
ActivityLogRelationManager::class,
];
}
```