A Laravel package that enriches Spatie ActivityLog entries by resolving foreign key IDs to human-readable labels, making your audit trails more meaningful and user-friendly.
- 🔄 Automatic Foreign Key Resolution: Convert foreign key IDs to readable labels
- 🏗️ Nested Array Support: Handle complex nested structures like
items.*.material_id - 🎯 Flexible Label Sources: Support for model attributes, accessor methods, or custom methods
- 🗂️ Predefined Configurations: Reusable mapping configurations via config file
- 🛡️ Soft Delete Awareness: Handles soft-deleted models gracefully
- 🚫 Error Resilient: Continues enrichment even when some models are missing
- 🧪 Fully Tested: Comprehensive test suite with 100% coverage
- 📋 Type Safe: Full PHP 8.1+ type declarations and PHPStan level 8 compliance
- PHP 8.2 or higher
- Laravel 9.0, 10.0, 11.0, or 12.0
- Spatie Laravel ActivityLog 4.0+
Install the package via Composer:
composer require jimmyahalpara/laravel-activitylog-enricherThe package will automatically register its service provider.
Optionally, publish the configuration file:
php artisan vendor:publish --provider="JimmyAhalpara\ActivityLogEnricher\ActivityLogEnricherServiceProvider" --tag="activity-log-enricher-config"Use the enricher in your model's tapActivity method:
<?php
use JimmyAhalpara\ActivityLogEnricher\Facades\ActivityLogEnricher;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Models\Activity;
use Spatie\Activitylog\Traits\LogsActivity;
class Job extends Model
{
use LogsActivity;
public function tapActivity(Activity $activity, string $eventName): void
{
ActivityLogEnricher::enrichActivity($activity, [
'customer_id' => [
'class' => Contact::class,
'label_attribute' => 'name',
'new_key' => 'customer_name',
],
'design_id' => [
'class' => Design::class,
'label_attribute' => 'title',
'new_key' => 'design_title',
],
]);
}
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logAll()
->logOnlyDirty()
->dontSubmitEmptyLogs();
}
}Before enrichment:
{
"old": {
"customer_id": 1,
"design_id": 5
},
"attributes": {
"customer_id": 2,
"design_id": 5
}
}After enrichment:
{
"old": {
"customer_id": 1,
"customer_name": "ABC Corporation",
"design_id": 5,
"design_title": "Premium Package Design"
},
"attributes": {
"customer_id": 2,
"customer_name": "XYZ Industries",
"design_id": 5,
"design_title": "Premium Package Design"
}
}Each field mapping supports the following options:
'foreign_key_name' => [
'class' => ModelClass::class, // Required: The model class to resolve
'label_attribute' => 'attribute_name', // Optional: Attribute/method for label (default: 'label')
'new_key' => 'enriched_key_name', // Optional: New key name (default: guessed from foreign_key)
]The enricher tries multiple methods to resolve labels in this order:
- Direct method call:
$model->getLabelWithoutClass() - Accessor method:
$model->getLabelAttribute() - Direct attribute:
$model->label - Appended attribute: If
labelis in the model's$appendsarray - Fallback:
(string) $model
Perfect for enriching line items or nested structures:
ActivityLogEnricher::enrichActivity($activity, [
'items.*.product_id' => [
'class' => Product::class,
'label_attribute' => 'name',
'new_key' => 'product_name',
],
'items.*.category_id' => [
'class' => Category::class,
'label_attribute' => 'title',
'new_key' => 'category_title',
],
]);Input:
{
"attributes": {
"items": [
{"product_id": 1, "category_id": 2, "quantity": 5},
{"product_id": 3, "category_id": 1, "quantity": 2}
]
}
}Output:
{
"attributes": {
"items": [
{
"product_id": 1,
"product_name": "Laptop Computer",
"category_id": 2,
"category_title": "Electronics",
"quantity": 5
},
{
"product_id": 3,
"product_name": "Office Chair",
"category_id": 1,
"category_title": "Furniture",
"quantity": 2
}
]
}
}Define reusable configurations in config/activity-log-enricher.php:
'mappings' => [
'orders' => [
'customer_id' => [
'class' => Customer::class,
'label_attribute' => 'company_name',
'new_key' => 'customer',
],
'items.*.product_id' => [
'class' => Product::class,
'label_attribute' => 'name',
'new_key' => 'product',
],
],
],Then use in your model:
public function tapActivity(Activity $activity, string $eventName): void
{
ActivityLogEnricher::enrichActivityWithConfig($activity, 'orders');
}Use any model method for label generation:
class Material extends Model
{
public function getLabelWithoutClass(): string
{
return $this->name . ' (' . $this->code . ')';
}
public function getFullDescriptionAttribute(): string
{
return $this->name . ' - ' . $this->description;
}
}
// Usage
ActivityLogEnricher::enrichActivity($activity, [
'material_id' => [
'class' => Material::class,
'label_attribute' => 'getLabelWithoutClass', // Direct method
],
'secondary_material_id' => [
'class' => Material::class,
'label_attribute' => 'full_description', // Accessor attribute
],
]);When new_key is not specified, the enricher automatically removes _id suffix:
ActivityLogEnricher::enrichActivity($activity, [
'customer_id' => [
'class' => Customer::class,
'label_attribute' => 'name',
// new_key will be automatically set to 'customer'
],
'shipping_address_id' => [
'class' => Address::class,
'label_attribute' => 'full_address',
// new_key will be automatically set to 'shipping_address'
],
]);class Order extends Model
{
use LogsActivity;
public function tapActivity(Activity $activity, string $eventName): void
{
ActivityLogEnricher::enrichActivity($activity, [
'customer_id' => [
'class' => Customer::class,
'label_attribute' => 'full_name',
'new_key' => 'customer',
],
'billing_address_id' => [
'class' => Address::class,
'label_attribute' => 'formatted_address',
'new_key' => 'billing_address',
],
'shipping_address_id' => [
'class' => Address::class,
'label_attribute' => 'formatted_address',
'new_key' => 'shipping_address',
],
'items.*.product_id' => [
'class' => Product::class,
'label_attribute' => 'name',
'new_key' => 'product',
],
'items.*.variant_id' => [
'class' => ProductVariant::class,
'label_attribute' => 'display_name',
'new_key' => 'variant',
],
]);
}
}class Task extends Model
{
use LogsActivity;
public function tapActivity(Activity $activity, string $eventName): void
{
ActivityLogEnricher::enrichActivity($activity, [
'project_id' => [
'class' => Project::class,
'label_attribute' => 'name',
'new_key' => 'project',
],
'assigned_to' => [
'class' => User::class,
'label_attribute' => 'full_name',
'new_key' => 'assignee',
],
'created_by' => [
'class' => User::class,
'label_attribute' => 'full_name',
'new_key' => 'creator',
],
'attachments.*.document_id' => [
'class' => Document::class,
'label_attribute' => 'filename',
'new_key' => 'document',
],
]);
}
}class Job extends Model
{
use LogsActivity;
public function tapActivity(Activity $activity, string $eventName): void
{
ActivityLogEnricher::enrichActivity($activity, [
'customer_id' => [
'class' => Contact::class,
'label_attribute' => 'label',
'new_key' => 'customer',
],
'design_id' => [
'class' => Design::class,
'label_attribute' => 'label',
'new_key' => 'design',
],
'plate_id' => [
'class' => Plate::class,
'label_attribute' => 'label',
'new_key' => 'plate',
],
'laminate_material_id' => [
'class' => Material::class,
'label_attribute' => 'getLabelWithoutClass',
'new_key' => 'laminate_material',
],
'cap_material_id' => [
'class' => Material::class,
'label_attribute' => 'getLabelWithoutClass',
'new_key' => 'cap_material',
],
'should_material_id' => [
'class' => Material::class,
'label_attribute' => 'getLabelWithoutClass',
'new_key' => 'shoulder_material',
],
'pvc_sleeve_material_id' => [
'class' => Material::class,
'label_attribute' => 'getLabelWithoutClass',
'new_key' => 'pvc_sleeve_material',
],
'liner_material_id' => [
'class' => Material::class,
'label_attribute' => 'getLabelWithoutClass',
'new_key' => 'liner_material',
],
'granules_material_id' => [
'class' => Material::class,
'label_attribute' => 'getLabelWithoutClass',
'new_key' => 'granules_material',
],
'cold_foil_material_id' => [
'class' => Material::class,
'label_attribute' => 'getLabelWithoutClass',
'new_key' => 'cold_foil_material',
],
]);
}
}The enricher is designed to be resilient and will continue processing even when encountering errors:
- Missing models: If a foreign key references a non-existent model, the enrichment for that field is skipped
- Invalid classes: Throws
InvalidModelExceptionfor configuration errors - Missing attributes: Falls back to string representation of the model
- Soft deleted models: Automatically included using
withTrashed()
use JimmyAhalpara\ActivityLogEnricher\Exceptions\InvalidModelException;
try {
ActivityLogEnricher::enrichActivity($activity, $invalidMapping);
} catch (InvalidModelException $e) {
// Handle configuration errors
Log::error('ActivityLog enrichment failed: ' . $e->getMessage());
}To optimize performance, ensure your models are properly eager loaded before enrichment:
// In your model or controller
$activities = Activity::with(['subject', 'causer'])->get();Consider implementing model caching for frequently accessed labels:
class Product extends Model
{
public function getLabelAttribute(): string
{
return Cache::remember(
"product_label_{$this->id}",
3600,
fn() => $this->name . ' (' . $this->sku . ')'
);
}
}Run the test suite:
composer testRun tests with coverage:
composer test-coverageRun static analysis:
composer analyseRun code formatting:
composer formatRun all quality checks:
composer qualityContributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Make your changes and add tests
- Ensure all tests pass:
composer quality - Commit your changes:
git commit -m 'Add amazing feature' - Push to the branch:
git push origin feature/amazing-feature - Submit a pull request
git clone https://github.com/jimmyahalpara/laravel-activitylog-enricher.git
cd laravel-activitylog-enricher
composer install
composer qualityIf you discover any security-related issues, please email jimmy@ahalpara.in instead of using the issue tracker.
- Jimmy Ahalpara - jimmy@ahalpara.in
- Spatie - For the excellent Laravel ActivityLog package
The MIT License (MIT). Please see License File for more information.
Please see CHANGELOG for more information on what has changed recently.
If you find this package useful, please consider:
- ⭐ Starring the repository
- 🐛 Reporting issues
- 💡 Contributing improvements
- 📢 Sharing with others
For support, questions, or feature requests, please open an issue on GitHub.