Skip to content

v5: PHP 8.4+, Laravel 11+, modernized API#1452

Merged
freekmurze merged 47 commits intomainfrom
v5
Mar 25, 2026
Merged

v5: PHP 8.4+, Laravel 11+, modernized API#1452
freekmurze merged 47 commits intomainfrom
v5

Conversation

@freekmurze
Copy link
Copy Markdown
Member

@freekmurze freekmurze commented Mar 22, 2026

Summary

v5 major release. Modernized API, simplified configuration, swappable action classes, PHP 8.4+/Laravel 12+.

Breaking Changes

Schema

  • attribute_changes column (new, json): tracked model changes ({"attributes": {...}, "old": {...}})
  • properties column: now exclusively custom user data (via withProperties())
  • batch_uuid column: removed (batch system dropped)

Batch system removed

LogBatch, LogBatch facade, batch scopes all removed. Group activities with custom properties if needed.

Trait collision fix (#215, #115, #18, #224)

  • activities() -> activitiesAsSubject()
  • actions() -> activitiesAsCauser()
  • New HasActivity trait combines both

CauserResolver facade removed

Use Activity::defaultCauser() instead.

Config simplified

  • Env vars: ACTIVITY_LOGGER_* -> ACTIVITYLOG_*
  • delete_records_older_than_days -> clean_after_days
  • subject_returns_soft_deleted_models -> include_soft_deleted_subjects
  • table_name and database_connection removed (use custom model)
  • New actions config for swappable action classes

Renamed methods

v4 v5
$activity->changes() $activity->attribute_changes
getExtraProperty() getProperty()
dontSubmitEmptyLogs() dontLogEmptyChanges()
withoutLogs() withoutLogging()
$model->activities (LogsActivity) $model->activitiesAsSubject
$model->actions (CausesActivity) $model->activitiesAsCauser

Other breaking changes

  • getActivitylogOptions() now optional (defaults to no attribute tracking)
  • PHP 8.4+, Laravel 12+, Pest 4+ required
  • Single consolidated migration
  • Activity contract simplified to 3 methods (scopes removed from interface)

New Features

Action classes

Core operations extracted into overridable action classes:

  • LogActivityAction (protected methods: resolveDescription, tapActivity, save)
  • CleanActivityLogAction (protected methods: getCutOffDate, deleteOldActivities)

Swappable via config, validated by ActivitylogConfig helper.

ActivityEvent enum

Activity::forEvent(ActivityEvent::Created)->get();

Activity::defaultCauser() (#582, #649, #661)

Scoped or global causer override.

Global excluded columns (#1100, #460)

default_except_attributes config option.

LogOptions serialization fix (#1450)

Safely strips closures during serialization.

Boost v2 skill

resources/boost/guidelines/core.blade.php for Laravel Boost auto-discovery.

Modernization

  • Str::uuid() replaces ramsey/uuid
  • array_find() (PHP 8.4)
  • Casts as casts() method
  • $table->id(), no down() in migration
  • Strict comparisons, typed parameters, dead code removed

Related

Issues: #215, #115, #18, #224, #1371, #1450, #582, #649, #661, #460
Discussions: #1063, #1100

freekmurze and others added 2 commits March 22, 2026 10:00
Breaking changes:
- Rename activities() to activitiesAsSubject(), actions() to activitiesAsCauser()
- Add HasActivity trait that combines LogsActivity + CausesActivity
- Rename ActivityLogStatus to ActivitylogStatus (consistent casing)
- Rename dontSubmitEmptyLogs() to dontLogEmptyChanges()
- Rename withoutLogs() to withoutLogging()
- Make getActivitylogOptions() optional (defaults to LogOptions::defaults())
- Require PHP 8.4+ and Laravel 11+
- Consolidate 3 migrations into single migration

New features:
- ActivityEvent enum (Created, Updated, Deleted, Restored)
- CauserResolver::withCauser() for scoped causer overrides
- Global default_except_attributes config option
- LogOptions serialization safety (strips closures)
- Boost v2 guidelines file

Modernization:
- Use Str::uuid() instead of ramsey/uuid
- Use array_find() (PHP 8.4) instead of Arr::first()
- Casts as methods on Activity model
- Strict comparisons, short nullable notation, typed parameters

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
freekmurze and others added 18 commits March 22, 2026 10:09
- Update version references from v4 to v5
- Update requirements to PHP 8.4+ and Laravel 11+
- Document optional getActivitylogOptions() (no config needed for basic usage)
- Document HasActivity trait combining LogsActivity and CausesActivity
- Document ActivityEvent enum
- Document CauserResolver::withCauser() for scoped causer overrides
- Document default_except_attributes config option
- Rename dontSubmitEmptyLogs to dontLogEmptyChanges
- Rename withoutLogs to withoutLogging
- Rename actions() to activitiesAsCauser()
- Update LogOptions API reference with new property/method names

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Provides a cleaner alternative to using CauserResolver directly:

  Activity::defaultCauser($admin, fn() => ...); // scoped
  Activity::defaultCauser($admin);              // global

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add Activity::batch(fn) as friendly API for batching activities
- Remove CauserResolver facade (use Activity::defaultCauser() instead)
- Update tests to use CauserResolver class directly
- Add v4 to v5 upgrade guide to UPGRADING.md
- Update docs for new API surface
- Update Boost skill guidelines

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Schema changes:
- Add attribute_changes column for tracked model changes
- Remove batch_uuid column (batch system dropped)
- properties column now stores only custom user data

Removed:
- LogBatch class and LogBatch facade
- Activity::batch() method
- scopeHasBatch() and scopeForBatch() scopes
- Batch-related tests and documentation

Renamed:
- getExtraProperty() to getProperty()
- $activity->changes() to $activity->attribute_changes

The properties column is now clean user-owned space. Model attribute
tracking (attributes/old) is stored in the dedicated attribute_changes
column, eliminating the previous mixing of change data and user data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Require Laravel 12+ (for future attribute support)
- Remove scopes from Activity contract interface (scopes are a query
  builder concern, not a model contract concern)
- Contract now only requires: subject(), causer(), getProperty()
- Custom Activity models no longer need to implement scope methods

Note: #[Scope] attribute was investigated but doesn't work with
Activity::inLog() static calls (PHP resolves the non-static method
directly). Keeping scope prefix convention for now.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Update run-tests.yml matrix to only PHP 8.4/8.5 and Laravel 12/13
- Remove nesbot/carbon constraint from CI
- Keep ActivityLogStatus class name (php-cs-fixer enforces it)
- Update all references to use ActivityLogStatus consistently
- Remove class rename from UPGRADING.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Update README to use getProperty() and attribute_changes
- Remove CouldNotLogChanges exception class (never thrown anywhere)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Source:
- Fix getProperty() signature mismatch between contract and model
- Add withChanges() to Activity facade docblock
- Remove duplicate import in ActivitylogServiceProvider

Tests:
- Remove unused getActivitylogOptions() function in DetectsChangesTest

Docs:
- Fix typo "litte" in introduction.md
- Fix "Pretty Zonda" in logging-model-events.md
- Fix key order in introduction.md attribute_changes example

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Config changes:
- Rename env var ACTIVITY_LOGGER_ENABLED to ACTIVITYLOG_ENABLED
- Rename delete_records_older_than_days to clean_after_days
- Rename subject_returns_soft_deleted_models to include_soft_deleted_subjects
- Remove table_name and database_connection options (use custom model instead)
- Fix default_except_attributes comment (merged, not overridden)

Activity model:
- Hardcode $table = 'activity_log' instead of reading from config
- Remove constructor that read config (custom table/connection via subclass)

Migration:
- Hardcode table name instead of reading from config

Updated docs, UPGRADING.md, Boost skill, and tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Core operations are now handled by action classes with small overridable
protected methods, following the Spatie action pattern.

New files:
- src/Actions/LogActivityAction.php (resolveDescription, tapActivity, save)
- src/Actions/CleanActivityLogAction.php (getCutOffDate, deleteOldActivities)
- src/ActivitylogConfig.php (resolves action classes from config, validates they extend base)

Both actions are configurable via config/activitylog.php and validated
by ActivitylogConfig to ensure custom classes extend the originals.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The action calls save(), accesses attributes, and reads relationships,
which are all Model concerns. The Activity contract is too narrow for
what the action actually needs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clearer name that describes when it runs, following Laravel's
beforeSave/beforeDelete naming convention.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Split all && and || conditions into separate if statements with early
returns. Extracted helper methods for better readability:

- resolveModelForLogging() - determines which model instance to log
- shouldLogOnlyDirtyAttributes() - checks if dirty filtering applies
- filterDirtyAttributes() - performs the actual dirty diff
- isUpdatedEvent() / isDeletedEvent() - named event checks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
freekmurze and others added 13 commits March 23, 2026 10:35
Pipes for addLogChange() no longer need to implement an interface.
Any class with a handle(EventLogBag, Closure) method works (standard
Laravel pipeline convention).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Support/ -> ActivityLoggerTest, CauserResolverTest
- Models/ -> ActivityModelTest, CustomActivityModelTest, CustomDatabaseConnectionActivityModelTest, CustomTableNameModelTest
- Commands/ -> CleanActivitylogCommandTest
- Traits/ -> LogsActivityTest, CausesActivityTest, DetectsChangesTest

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract pure logic into ChangeDetector support class:
- resolveRelatedAttribute() for dot-notation relations
- resolveJsonAttribute() for JSON arrow-notation
- resolveRelationName() for relation method discovery
- filterDirty() and compareValues() for dirty attribute diffing

Simplify LogsActivity trait:
- attributesToBeLogged() now uses collection pipeline
- Split into fillableAttributes(), unguardedAttributes(), explicitAttributes(), excludedAttributes()
- buildChanges() replaces attributeValuesToBeLogged()
- runChangesPipeline() extracts pipeline logic from boot
- shouldSkipEmptyLog() replaces inline check
- hasChangedAttributesBeyondIgnored() extracts dirty check
- formatAttributeValue() checks casts before dates (fixes custom date cast order)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removed:
- addLogChange() static method
- $changesPipes static array
- EventLogBag DTO
- LoggablePipe contract
- runChangesPipeline() method
- Pipeline dependency in LogsActivity trait
- 3 pipe-related tests
- manipulate-changes-array.md docs page
- event-bag.md API docs page

Users who need to transform the changes array before saving should
override transformChanges() on a custom LogActivityAction instead.
This is simpler (one method override vs registering pipe classes)
and consistent with the action pattern used throughout v5.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolve conflicts:
- Remove LogBatch files (batch system removed in v5)
- Use SerializableClosure version for LogOptions serialization

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Laravel 13 may touch updated_at during restore, making more than just
deleted_at dirty. Instead of checking dirty count === 1, check that
deleted_at was previously non-null and is now null.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Schema::connection(config('activitylog.database_connection'))->create(config('activitylog.table_name'), function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('log_name')->nullable();
Schema::create('activity_log', function (Blueprint $table) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest naming it activity_logs to match Laravel naming conventions

@Abdulmajeed-Jamaan
Copy link
Copy Markdown

Would like buffer behaviour to bulk insert logs to database, just like how laravel nightwatch do

@pxlrbt
Copy link
Copy Markdown
Contributor

pxlrbt commented Mar 24, 2026

We heavily rely on the batch system. Any reason it gets dropped?

@freekmurze
Copy link
Copy Markdown
Member Author

freekmurze commented Mar 25, 2026

@pxlrbt

The batch system was removed in v5 to simplify the package. It added a lot of complexity for a feature that most users didn't need.

The good news is that you can rebuild it in userland with very little code, using the beforeLogging hook.

1. Add the column:

Schema::table('activity_log', function (Blueprint $table) {
    $table->uuid('batch_uuid')->nullable()->index();
});

2. Register the hook in your service provider:

use Illuminate\Support\Str;
use Spatie\Activitylog\Facades\Activity;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $batchUuid = null;

        Activity::beforeLogging(function ($activity) use (&$batchUuid) {
            if ($batchUuid) {
                $activity->batch_uuid = $batchUuid;
            }
        });
    }
}

3. Start and end batches wherever you need them:

$batchUuid = (string) Str::uuid();

$author = Author::create(['name' => 'Philip K. Dick']);
$book = Book::create(['title' => 'A Scanner Darkly', 'author_id' => $author->id]);
$book->update(['title' => 'Updated title']);

$batchUuid = null;

All activities between setting and clearing $batchUuid share the same UUID. This works for both manual activity()->log() calls and automatic model event logging via the LogsActivity trait.

To query by batch:

Activity::where('batch_uuid', $uuid)->get();

No custom model needed. See the beforeLogging hook docs for more details.

@freekmurze
Copy link
Copy Markdown
Member Author

We've added this! Enable it with a single env variable:

ACTIVITYLOG_BUFFER_ENABLED=true

All activities are then collected in memory during the request and inserted in a single bulk query after the response is sent. No code changes needed.

Docs: https://github.com/spatie/laravel-activitylog/blob/v5/docs/advanced-usage/buffering.md

@freekmurze freekmurze merged commit 0e00fe7 into main Mar 25, 2026
12 checks passed
@pxlrbt
Copy link
Copy Markdown
Contributor

pxlrbt commented Mar 26, 2026

@freekmurze Thanks for that detailed explanation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants