I’ve been working on a new service for the last month or so that enables people to log, track and share walks in the UK’s hills and mountains. The service is based on Laravel, and one of the things I wanted to include on the site was an “activity feed”.
There are two types of feed I wanted to create. The first was for my own benefit. I wanted to see a simple timeline of activity on the site as usage builds up. Secondly, I wanted to be able to log data so that I can create public timelines in the future.
This is all pretty simple to build from scratch, but it turns out that the team at Spatie have already got this covered with a general package for logging activity on a Laravel application.
Log activity inside your Laravel app
https://github.com/spatie/laravel-activitylog
747 forks.
5,818 stars.
1 open issues.
Recent commits:
- Bump dependabot/fetch-metadata from 2.5.0 to 3.0.0 (#1454)Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 2.5.0 to 3.0.0.- [Release notes](https://github.com/dependabot/fetch-metadata/releases)- [Commits](https://github.com/dependabot/fetch-metadata/compare/v2.5.0…v3.0.0)—updated-dependencies:- dependency-name: dependabot/fetch-metadata dependency-version: 3.0.0 dependency-type: direct:production update-type: version-update:semver-major…Signed-off-by: dependabot[bot] <support@github.com>Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>, GitHub
- Update CHANGELOG, github-actions[bot]
- v5: PHP 8.4+, Laravel 11+, modernized API (#1452)* Implement v5: require PHP 8.4+/Laravel 11+, modernize APIBreaking 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 migrationNew 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 fileModernization:- 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 parametersCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Fix styling* Update documentation for v5- 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 namesCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Drop Pest 3 support, require Pest 4+Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Use id() shorthand and drop down() method in migrationCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Add Activity::defaultCauser() as friendly API for causer resolutionProvides a cleaner alternative to using CauserResolver directly: Activity::defaultCauser($admin, fn() => …); // scoped Activity::defaultCauser($admin); // globalCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Add Activity::batch(), remove CauserResolver facade, add UPGRADING.md- 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 guidelinesCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Remove batch system, separate changes from propertiesSchema changes:- Add attribute_changes column for tracked model changes- Remove batch_uuid column (batch system dropped)- properties column now stores only custom user dataRemoved:- LogBatch class and LogBatch facade- Activity::batch() method- scopeHasBatch() and scopeForBatch() scopes- Batch-related tests and documentationRenamed:- getExtraProperty() to getProperty()- $activity->changes() to $activity->attribute_changesThe properties column is now clean user-owned space. Model attributetracking (attributes/old) is stored in the dedicated attribute_changescolumn, eliminating the previous mixing of change data and user data.Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Drop Laravel 11, simplify Activity contract- 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 methodsNote: #[Scope] attribute was investigated but doesn't work withActivity::inLog() static calls (PHP resolves the non-static methoddirectly). Keeping scope prefix convention for now.Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Fix docs: Laravel 12+ requirement, attribute_changes referencesCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Fix CI: update workflow for PHP 8.4+/Laravel 12+, fix class name- 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.mdCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Fix README, remove dead CouldNotLogChanges exception- 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>* Fix issues found in full reviewSource:- Fix getProperty() signature mismatch between contract and model- Add withChanges() to Activity facade docblock- Remove duplicate import in ActivitylogServiceProviderTests:- Remove unused getActivitylogOptions() function in DetectsChangesTestDocs:- Fix typo "litte" in introduction.md- Fix "Pretty Zonda" in logging-model-events.md- Fix key order in introduction.md attribute_changes exampleCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Fix styling* Fix null safety in getProperty() when properties is nullCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Simplify config: rename keys, remove table_name and database_connectionConfig 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 configUpdated docs, UPGRADING.md, Boost skill, and tests.Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Extract LogActivityAction and CleanActivityLogActionCore operations are now handled by action classes with small overridableprotected 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 validatedby ActivitylogConfig to ensure custom classes extend the originals.Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Use Model type-hint in LogActivityAction instead of Activity contractThe action calls save(), accesses attributes, and reads relationships,which are all Model concerns. The Activity contract is too narrow forwhat the action actually needs.Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Rename tapActivity to beforeActivityLoggedClearer name that describes when it runs, following Laravel'sbeforeSave/beforeDelete naming convention.Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Replace compound conditions with early returns and small methodsSplit all && and || conditions into separate if statements with earlyreturns. 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 checksCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Update Boost skill for Enums namespace moveCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Move config resolution to ActivitylogConfig, clean up ServiceProvider- Move determineActivityModel/getActivityModelInstance from ServiceProvider to ActivitylogConfig as activityModel()/activityModelInstance()- Extract shared resolveAction() to reduce duplication- ServiceProvider now only handles package registration and bindingsCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Move CleanActivitylogCommand to Commands namespaceCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Update docs, UPGRADING.md and Boost skill for Support namespace movesCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Remove LoggablePipe contractPipes for addLogChange() no longer need to implement an interface.Any class with a handle(EventLogBag, Closure) method works (standardLaravel pipeline convention).Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* wip* Restructure tests to mirror src directory layout- Support/ -> ActivityLoggerTest, CauserResolverTest- Models/ -> ActivityModelTest, CustomActivityModelTest, CustomDatabaseConnectionActivityModelTest, CustomTableNameModelTest- Commands/ -> CleanActivitylogCommandTest- Traits/ -> LogsActivityTest, CausesActivityTest, DetectsChangesTestCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Extract ChangeDetector, simplify LogsActivity traitExtract 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 diffingSimplify 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>* Update docs for Traits -> Models/Concerns namespace moveCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* Remove pipe system, use LogActivityAction::transformChanges() insteadRemoved:- 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 pageUsers who need to transform the changes array before saving shouldoverride 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>* Fix CI: regenerate PHPStan baseline, run pintCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* wip* wip* Fix isRestoring() to handle Laravel 13 restore behaviorLaravel 13 may touch updated_at during restore, making more than justdeleted_at dirty. Instead of checking dirty count === 1, check thatdeleted_at was previously non-null and is now null.Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>* wip* v5* wip* Fix styling* wip* wip* wip* Fix styling* wip* wip———Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>Co-authored-by: freekmurze <483853+freekmurze@users.noreply.github.com>, GitHub
- Update CHANGELOG, github-actions[bot]
- Fix LogOptions closure preventing model serialization in queued listeners (#1453)* Fix LogOptions closure preventing model serialization in queued listenersUse Laravel's SerializableClosure in LogOptions __serialize/__unserializeto allow models with a descriptionForEvent closure to be serialized whenpassed to queued event listeners.Fixes spatie/laravel-activitylog#1450* Simplify __serialize/__unserialize to use get_object_vars()Instead of manually listing every property, use get_object_vars()and only handle the closure specially. This avoids needing to updatethe serialization methods when properties are added or removed.Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>———Co-authored-by: Michael Lundbøl <mlu@emballageindberetning.dk>Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>, GitHub
You can easily log custom activity events wherever you like, it’s as easy as:
activity()->log('My custom message here');
Even better though is that you can get it to log changes to Eloquent models automatically using the handy LogsActivity trait. If you add the trait to your model, then creating, updating or deleting a model will automatically create a log entry. The log entries include the item affected, and the “causer” (normally a user).
Going beyond the basics, you can also define custom messages, choose what data to store along with log events.
The activities are logged as normal Eloquent models, so once you have your data logged it’s easy to pull out the information you need using standard Laravel querying, and views.
- Stuff I’ve used
- Error tracking with Sentry
- Autotrack for Google Analytics
- WordPress performance tracking with Time-stack
- Enforce user password strength
- WYSIWYG with Summernote
- Backing up your Laravel app
- Adding Google Maps to your Laravel application
- Activity logging in Laravel
- Image handling in PHP with Intervention Image
- Testing Laravel emails with MailThief
- Assessing software health
- IP Geolocation with MaxMind’s GeoLite2
- Uptime monitoring with Uptime Robot
- Product tours with Hopscotch
- Background processing for WordPress
- Using oEmbed resources in Laravel