├── CONTRIBUTING.md ├── config └── eloquent-logs.php ├── src ├── Facades │ └── CacheEloquentLogQueries.php ├── EloquentLogsServiceProvider.php ├── Models │ └── EloquentLog.php ├── CacheEloquentLogQueries.php └── Concerns │ └── HasLogs.php ├── CHANGELOG.md ├── database ├── factories │ └── EloquentLogFactoryFactory.php └── migrations │ └── create_eloquent_logs_table.php ├── LICENSE.md ├── composer.json └── README.md /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Any kind contribution is welcomed :) 4 | 5 | ## TODO 6 | 7 | - Set on the models which listeners to register. 8 | - Add choice to use log files. 9 | -------------------------------------------------------------------------------- /config/eloquent-logs.php: -------------------------------------------------------------------------------- 1 | \ElaborateCode\EloquentLogs\Models\EloquentLog::class, 6 | 'logs_table' => 'eloquent_logs', 7 | 8 | /** @phpstan-ignore-next-line */ 9 | 'user' => \App\Models\User::class, 10 | 11 | ]; 12 | -------------------------------------------------------------------------------- /src/Facades/CacheEloquentLogQueries.php: -------------------------------------------------------------------------------- 1 | name('eloquent-logs') 19 | ->hasConfigFile() 20 | ->hasMigration('create_eloquent_logs_table'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /database/migrations/create_eloquent_logs_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->morphs('loggable'); 14 | $table->unsignedBigInteger('user_id')->index()->nullable(); 15 | $table->string('action'); 16 | $table->timestamps(); // ? Not sure about having an updated_at field 17 | $table->softDeletes(); // ? Not sure about this 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/Models/EloquentLog.php: -------------------------------------------------------------------------------- 1 | table = config('eloquent-logs.logs_table') ?? 'eloquent_logs'; 25 | } 26 | 27 | public function loggable(): MorphTo 28 | { 29 | return $this->morphTo(); 30 | } 31 | 32 | public function user(): BelongsTo 33 | { 34 | /** @phpstan-ignore-next-line */ 35 | return $this->belongsTo(config('eloquent-logs.user') ?? \App\Models\User::class, 'id', 'user_id'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) elaborate-code 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elaborate-code/laravel-eloquent-logs", 3 | "description": "A simple way to log changes that occur on Eloquent models", 4 | "keywords": [ 5 | "laravel", 6 | "eloquent", 7 | "log" 8 | ], 9 | "homepage": "https://github.com/elaborate-code/laravel-eloquent-logs", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "medilies", 14 | "email": "medilies@gmail.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.1", 20 | "spatie/laravel-package-tools": "^1.9.2", 21 | "illuminate/contracts": "^9.0" 22 | }, 23 | "require-dev": { 24 | "laravel/pint": "^1.0", 25 | "nunomaduro/collision": "^6.0", 26 | "nunomaduro/larastan": "^2.0.1", 27 | "orchestra/testbench": "^7.0", 28 | "pestphp/pest": "^1.21", 29 | "pestphp/pest-plugin-laravel": "^1.1", 30 | "phpstan/extension-installer": "^1.1", 31 | "phpstan/phpstan-deprecation-rules": "^1.0", 32 | "phpstan/phpstan-phpunit": "^1.0", 33 | "phpunit/phpunit": "^9.5" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "ElaborateCode\\EloquentLogs\\": "src", 38 | "ElaborateCode\\EloquentLogs\\Database\\Factories\\": "database/factories" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "ElaborateCode\\EloquentLogs\\Tests\\": "tests" 44 | } 45 | }, 46 | "scripts": { 47 | "analyse": "vendor/bin/phpstan analyse", 48 | "test": "vendor/bin/pest", 49 | "test-coverage": "vendor/bin/pest --coverage", 50 | "format": "vendor/bin/pint" 51 | }, 52 | "config": { 53 | "sort-packages": true, 54 | "allow-plugins": { 55 | "pestphp/pest-plugin": true, 56 | "phpstan/extension-installer": true 57 | } 58 | }, 59 | "extra": { 60 | "laravel": { 61 | "providers": [ 62 | "ElaborateCode\\EloquentLogs\\EloquentLogsServiceProvider" 63 | ], 64 | "aliases": { 65 | "CacheEloquentLogQueries": "ElaborateCode\\EloquentLogs\\Facades\\CacheEloquentLogQueries" 66 | } 67 | } 68 | }, 69 | "minimum-stability": "dev", 70 | "prefer-stable": true 71 | } 72 | -------------------------------------------------------------------------------- /src/CacheEloquentLogQueries.php: -------------------------------------------------------------------------------- 1 | isCaching()) { 18 | throw new \Exception('Misplaced usage of the CacheEloquentLogQueries::start() queries cache is already set'); 19 | } 20 | 21 | $this->caching = true; 22 | } 23 | 24 | /** 25 | * Suspends caching without affecting the queries cache 26 | */ 27 | public function suspend(): self 28 | { 29 | if (! $this->isCaching()) { 30 | throw new \Exception("Misplaced usage of the CacheEloquentLogQueries::suspend() queries cache isn't set"); 31 | } 32 | 33 | $this->caching = false; 34 | 35 | return $this; 36 | } 37 | 38 | /** 39 | * Flushes the queries cache without halting the caching process 40 | */ 41 | public function flushQueries(): self 42 | { 43 | $this->queries = []; 44 | 45 | return $this; 46 | } 47 | 48 | /** 49 | * Flushes the queries cache and suspends further caching 50 | */ 51 | public function reset(): void 52 | { 53 | if (! $this->isCaching()) { 54 | throw new \Exception("Misplaced usage of the CacheEloquentLogQueries::reset() queries cache isn't set"); 55 | } 56 | 57 | $this->suspend()->flushQueries(); 58 | } 59 | 60 | public function isCaching(): bool 61 | { 62 | return $this->caching; 63 | } 64 | 65 | /** 66 | * Add query data to the cache 67 | */ 68 | public function pushQuery(Model $model, string $event, ?int $user_id): void 69 | { 70 | $user_id ??= Auth::id(); 71 | 72 | array_push( 73 | $this->queries, 74 | [ 75 | 'loggable_type' => get_class($model), 76 | /** @phpstan-ignore-next-line */ 77 | 'loggable_id' => $model->id, 78 | 'action' => $event, 79 | 'user_id' => $user_id, 80 | ] 81 | ); 82 | } 83 | 84 | public function execute(): void 85 | { 86 | if (empty($this->queries)) { 87 | return; 88 | } 89 | 90 | DB::table(config('eloquent-logs.logs_table')) 91 | ->insert($this->queries); 92 | 93 | $this->reset(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Concerns/HasLogs.php: -------------------------------------------------------------------------------- 1 | eloquentLogs() 46 | ->create([ 47 | 'action' => $event, 48 | 'user_id' => Auth::id(), 49 | ]); 50 | } 51 | 52 | public static function isIgnored(string $event): bool 53 | { 54 | if ( 55 | ! isset(self::$loggableOptions) || 56 | ! isset(self::$loggableOptions['ignore']) 57 | ) { 58 | return false; 59 | } 60 | 61 | if (! is_array(self::$loggableOptions['ignore'])) { 62 | throw new \Exception('self::$loggableOptions[\'ignore\'] must be an array'); 63 | } 64 | 65 | if ( 66 | in_array($event, self::$loggableOptions['ignore']) || 67 | in_array('*', self::$loggableOptions['ignore']) 68 | ) { 69 | return true; 70 | } 71 | 72 | return false; 73 | } 74 | 75 | /* 76 | |-------------------------------------------------------------------------- 77 | | Handlers 78 | |-------------------------------------------------------------------------- 79 | | 80 | */ 81 | 82 | /** 83 | * Handles the created event 84 | */ 85 | public static function createdHandler(Model $model): void 86 | { 87 | self::log($model, self::eventPhraseFromHandlerName(__FUNCTION__)); 88 | } 89 | 90 | /** 91 | * Handles the updated event 92 | */ 93 | public static function updatedHandler(Model $model): void 94 | { 95 | if (self::isLoggableBeingRestored($model)) { 96 | // This is a restored event so don't log! 97 | return; 98 | } 99 | 100 | self::log($model, self::eventPhraseFromHandlerName(__FUNCTION__)); 101 | } 102 | 103 | /** 104 | * Handles the deleted event 105 | */ 106 | public static function deletedHandler(Model $model): void 107 | { 108 | if (in_array('Illuminate\Database\Eloquent\SoftDeletes', (class_uses(self::class)))) { 109 | // This is a softDeleted or a forceDeleted event so don't log! 110 | return; 111 | } 112 | 113 | self::log($model, self::eventPhraseFromHandlerName(__FUNCTION__)); 114 | } 115 | 116 | /** 117 | * Handles the softDeleted event 118 | */ 119 | public static function softDeletedHandler(Model $model): void 120 | { 121 | self::log($model, self::eventPhraseFromHandlerName(__FUNCTION__)); 122 | } 123 | 124 | /** 125 | * Handles the forceDeleted event 126 | */ 127 | public static function forceDeletedHandler(Model $model): void 128 | { 129 | self::log($model, self::eventPhraseFromHandlerName(__FUNCTION__)); 130 | } 131 | 132 | /** 133 | * Handles the restored event 134 | */ 135 | public static function restoredHandler(Model $model): void 136 | { 137 | self::log($model, self::eventPhraseFromHandlerName(__FUNCTION__)); 138 | 139 | self::resetLoggableBeingRestoredFlag($model); 140 | } 141 | 142 | /* 143 | |-------------------------------------------------------------------------- 144 | | Relationships 145 | |-------------------------------------------------------------------------- 146 | | 147 | */ 148 | 149 | public function eloquentLogs(): MorphMany 150 | { 151 | return $this->morphMany(config('eloquent-logs.logs_model') ?? ElaborateCode\EloquentLogs\Models\EloquentLog::class, 'loggable'); 152 | } 153 | 154 | /* 155 | |-------------------------------------------------------------------------- 156 | | Helpers 157 | |-------------------------------------------------------------------------- 158 | | 159 | */ 160 | 161 | /** 162 | * Transforms the string input to lower case separated by spaces 163 | * and rejects the 'Handler' substring if found 164 | */ 165 | public static function eventPhraseFromHandlerName(string $str): string 166 | { 167 | return Str::snake(str_replace('Handler', '', $str), ' '); 168 | } 169 | 170 | public static function setLoggableBeingRestoredFlag(Model $model): void 171 | { 172 | $model->loggableBeingRestoredFlag = true; 173 | } 174 | 175 | public static function resetLoggableBeingRestoredFlag(Model $model): void 176 | { 177 | $model->loggableBeingRestoredFlag = false; 178 | } 179 | 180 | public static function isLoggableBeingRestored(Model $model): bool 181 | { 182 | return $model->loggableBeingRestoredFlag; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # laravel-eloquent-logs 2 | 3 | [![Packagist Version](https://img.shields.io/packagist/v/elaborate-code/laravel-eloquent-logs?style=for-the-badge)](https://packagist.org/packages/elaborate-code/laravel-eloquent-logs) 4 | [![Packagist Downloads (custom server)](https://img.shields.io/packagist/dt/elaborate-code/laravel-eloquent-logs?style=for-the-badge)](https://packagist.org/packages/elaborate-code/laravel-eloquent-logs) 5 | [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/elaborate-code/laravel-eloquent-logs/run-tests?label=Tests&style=for-the-badge)](https://github.com/elaborate-code/laravel-eloquent-logs/actions/workflows/run-tests.yml) 6 | [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/elaborate-code/laravel-eloquent-logs/Fix%20PHP%20code%20style%20issues?label=Code%20Style&style=for-the-badge)](https://github.com/elaborate-code/laravel-eloquent-logs/actions/workflows/fix-php-code-style-issues.yml) 7 | ![maintained](https://img.shields.io/maintenance/yes/2022) 8 | ![Production ready](https://img.shields.io/badge/Production%20ready-no-red) 9 | 10 | ![banner](https://banners.beyondco.de/Eloquent%20logs.png?theme=dark&packageManager=composer+require&packageName=elaborate-code%2Flaravel-eloquent-logs&pattern=circuitBoard&style=style_1&description=A+simple+way+to+log+changes+that+occur+on+Eloquent+models&md=1&showWatermark=0&fontSize=100px&images=https%3A%2F%2Flaravel.com%2Fimg%2Flogomark.min.svg) 11 | 12 | Log what happens to your Eloquent models (`created`|`updated`|`deleted`|`soft deleted`|`restored`|`force deleted`) and keep and eye on **who** made the change, **how** and **when**. 13 | 14 | This solution is simple to integrate and introduces minimal changes to your project: 1 migration, 1 model, 1 trait, and 1 facade. 15 | 16 | ## Installation 17 | 18 | Install the package via composer: 19 | 20 | ```bash 21 | composer require elaborate-code/laravel-eloquent-logs 22 | ``` 23 | 24 | Publish the migrations: 25 | 26 | ```bash 27 | php artisan vendor:publish --tag="eloquent-logs-migrations" 28 | ``` 29 | 30 | Run the migrations: 31 | 32 | ```bash 33 | php artisan migrate 34 | ``` 35 | 36 | ### Publishing config file [Optional] 37 | 38 | You can publish the config file with: 39 | 40 | ```bash 41 | php artisan vendor:publish --tag="eloquent-logs-config" 42 | ``` 43 | 44 | This is the contents of the published config file: 45 | 46 | ```php 47 | return [ 48 | 'logs_model' => \ElaborateCode\EloquentLogs\Models\EloquentLog::class, 49 | 'logs_table' => 'eloquent_logs', 50 | 51 | 'user' => \App\Models\User::class, 52 | ]; 53 | ``` 54 | 55 | That allows you to rename the `logs_table` before running the migrations. 56 | 57 | ## Usage 58 | 59 | Pick an **Eloquent model** that you want to log the changes that happen to it and add the `HasLogs` trait to it. 60 | 61 | ```php 62 | namespace App\Models; 63 | 64 | use Illuminate\Database\Eloquent\Model; 65 | 66 | class ExampleModel extends Model 67 | { 68 | use \ElaborateCode\EloquentLogs\Concerns\HasLogs; 69 | // ... 70 | } 71 | ``` 72 | 73 | After adding that trait, every change made to the model will be recorded. 74 | 75 | Important warning from [Laravel docs](https://laravel.com/docs/9.x/eloquent#events:~:text=When%20issuing%20a%20mass%20update%20or,when%20performing%20mass%20updates%20or%20deletes.) 76 | 77 | > When issuing a **mass update or delete** query via Eloquent, the `saved`, `updated`, `deleting`, and `deleted` model events will not be dispatched for the affected models. This is because the models are never actually retrieved when performing mass updates or deletes. 78 | 79 | ### Retrieving logs 80 | 81 | You can load a model's logs using the `eloquentLogs` relationship: 82 | 83 | ```php 84 | $example_model->eloquentLogs; 85 | 86 | $example_model->load('eloquentLogs'); 87 | 88 | App\Models\ExampleModel::with('eloquentLogs')->find($id); 89 | ``` 90 | 91 | And you can query logs directly: 92 | 93 | ```php 94 | // latest 5 logs with affected models 95 | ElaborateCode\EloquentLogs\Models\EloquentLog::with('loggable')->latest()->limit(5)->get() 96 | ``` 97 | 98 | ### Grouping queries 99 | 100 | By default each one model event will result in a query to log the action. 101 | 102 | ```php 103 | $example_model = ExampleModel::create(['name' => 'foo']); 104 | 105 | $example_model->update(['name' => 'bar']); 106 | 107 | $example_model->delete(); 108 | 109 | // ⚠️ This will result in 3 queries to insert the 3 events logs into the database 110 | ``` 111 | 112 | You can improve the logging process by using the `CacheEloquentLogQueries` facade 113 | 114 | ```php 115 | use ElaborateCode\EloquentLogs\Facades\CacheEloquentLogQueries; 116 | 117 | CacheEloquentLogQueries::start(); 118 | 119 | $example_model = ExampleModel::create(['name' => 'foo']); 120 | 121 | $example_model->update(['name' => 'bar']); 122 | 123 | $example_model->delete(); 124 | 125 | CacheEloquentLogQueries::execute(); 126 | 127 | // 👍 This will result in 1 query to insert the 3 events logs into the database 128 | ``` 129 | 130 | The facade includes other methods that you wouldn't necessarily need to use: 131 | 132 | ```php 133 | // Stops caching and empties the cache without queries execution 134 | CacheEloquentLogQueries::reset(); 135 | 136 | // Empties the cache but doesn't stop caching 137 | CacheEloquentLogQueries::flushQueries(); 138 | 139 | // Stops caching until the reuse of start() and doesn't empty the cache 140 | CacheEloquentLogQueries::suspend(); 141 | 142 | // Returns a boolean 143 | CacheEloquentLogQueries::isCaching(); 144 | ``` 145 | 146 | ### Ignoring events 147 | 148 | You can specify the events to not log on the model instances by listing the events to ignore on `YourModel::$loggableOptions['ignore']`. 149 | 150 | ```php 151 | namespace App\Models; 152 | 153 | use Illuminate\Database\Eloquent\Model; 154 | 155 | class ExampleModel extends Model 156 | { 157 | use \ElaborateCode\EloquentLogs\Concerns\HasLogs; 158 | 159 | public static array $loggableOptions = [ 160 | 'ignore' => ['created', 'updated', 'deleted', 'softDeleted', 'forceDeleted', 'restored'], 161 | ]; 162 | // ... 163 | } 164 | ``` 165 | 166 | ### Muting Eloquent events [Laravel stuff] 167 | 168 | From seeders: 169 | 170 | ```php 171 | namespace Database\Seeders; 172 | 173 | use Illuminate\Database\Console\Seeds\WithoutModelEvents; 174 | use Illuminate\Database\Seeder; 175 | 176 | class DatabaseSeeder extends Seeder 177 | { 178 | use WithoutModelEvents; // Add this trait 179 | 180 | public function run(): void 181 | { 182 | // Silent eloquent queries ... 183 | } 184 | } 185 | ``` 186 | 187 | Anywhere from your code: 188 | 189 | ```php 190 | \Illuminate\Database\Eloquent\Model::unsetEventDispatcher(); 191 | 192 | // Silent eloquent queries ... 193 | 194 | \Illuminate\Database\Eloquent\Model::setEventDispatcher(app(Dispatcher::class)); 195 | // ... 196 | ``` 197 | 198 | Explore the [Eloquent docs](https://laravel.com/docs/9.x/eloquent#muting-events) for more options 199 | 200 | ## Alternative 201 | 202 | Among the bajillion packages that Spatie has so graciously bestowed upon the community, you'll find the excellent [laravel-Alternative](https://github.com/spatie/laravel-activitylog) package. 203 | 204 | ## Testing 205 | 206 | ```bash 207 | composer test 208 | ``` 209 | 210 | ## Changelog 211 | 212 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 213 | 214 | ## Contributing 215 | 216 | Please see [CONTRIBUTING](https://github.com/elaborate-code/.github/blob/main/CONTRIBUTING.md) for details. 217 | 218 | ## Security Vulnerabilities 219 | 220 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 221 | 222 | ## Credits 223 | 224 | - [medilies](https://github.com/elaborate-code) 225 | - [All Contributors](../../contributors) 226 | 227 | ## License 228 | 229 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 230 | --------------------------------------------------------------------------------