├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── AuditableServiceProvider.php ├── AuditableTrait.php ├── AuditableTraitObserver.php ├── AuditableWithDeletesTrait.php ├── AuditableWithDeletesTraitObserver.php └── config └── auditable.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All Notable changes to `laravel-auditable` will be documented in this file. 4 | 5 | ## [Unreleased] 6 | 7 | ## v12.0.2 - 2025-03-22 8 | 9 | - fix: prevent touch model #30 10 | 11 | ## v12.0.1 - 2025-03-04 12 | 13 | - fix: Prevent not dirty model to get dirtied #34 14 | - fix: #33 15 | 16 | ## v12.0.0 - 2025-02-28 17 | 18 | - Laravel 12 Support #32 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2022 Arjay Angeles 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Auditable 2 | 3 | [![Latest Version on Packagist][ico-version]][link-packagist] 4 | [![Software License][ico-license]](LICENSE.md) 5 | 6 | [![Continuous Integration](https://github.com/yajra/laravel-auditable/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/yajra/laravel-auditable/actions/workflows/continuous-integration.yml) 7 | [![Static Analysis](https://github.com/yajra/laravel-auditable/actions/workflows/static-analysis.yml/badge.svg)](https://github.com/yajra/laravel-auditable/actions/workflows/static-analysis.yml) 8 | [![Total Downloads][ico-downloads]][link-downloads] 9 | 10 | Laravel Auditable is a simple Laravel auditing package for your Eloquent Model. 11 | This package automatically inserts/updates an audit log on your table on who created and last updated the record. 12 | 13 | ## Laravel Version Compatibility 14 | 15 | | Laravel | Package | 16 | |:---------|:--------| 17 | | 5.x-10.x | 4.x | 18 | | 11.x | 11.x | 19 | | 12.x | 12.x | 20 | 21 | ## Install via Composer 22 | 23 | ```bash 24 | composer require yajra/laravel-auditable:^12 25 | ``` 26 | 27 | ## Publish config file 28 | 29 | If you want to modify the `withDefault` option on auditable columns, you may publish the config file. 30 | 31 | ```bash 32 | php artisan vendor:publish --tag=auditable 33 | ``` 34 | 35 | ## Usage 36 | 37 | Update your model's migration and add `created_by` and `updated_by` field using the `auditable()` blueprint macro. 38 | 39 | ```php 40 | Schema::create('users', function (Blueprint $table) { 41 | $table->increments('id'); 42 | $table->string('name', 100); 43 | $table->auditable(); 44 | $table->timestamps(); 45 | }); 46 | ``` 47 | 48 | Then use `AuditableTrait` on your model. 49 | 50 | ``` php 51 | namespace App; 52 | 53 | use Yajra\Auditable\AuditableTrait; 54 | 55 | class User extends Model 56 | { 57 | use AuditableTrait; 58 | } 59 | ``` 60 | 61 | ## Soft Deletes 62 | 63 | If you wish to use Laravel's soft deletes, use the `auditableWithDeletes()` method on your migration instead: 64 | 65 | ```php 66 | Schema::create('users', function (Blueprint $table) { 67 | $table->increments('id'); 68 | $table->string('name', 100); 69 | $table->auditableWithDeletes(); 70 | $table->timestamps(); 71 | $table->softDeletes() 72 | }); 73 | ``` 74 | 75 | Afterwards, you need to use `AuditableWithDeletesTrait` on your model. 76 | 77 | ``` php 78 | namespace App; 79 | 80 | use Yajra\Auditable\AuditableWithDeletesTrait; 81 | 82 | class User extends Model 83 | { 84 | use AuditableWithDeletesTrait, SoftDeletes; 85 | } 86 | ``` 87 | 88 | 89 | ## Dropping columns 90 | 91 | You can drop auditable columns using `dropAuditable()` method, or `dropAuditableWithDeletes()` if using soft deletes. 92 | 93 | ```php 94 | Schema::create('users', function (Blueprint $table) { 95 | $table->dropAuditable(); 96 | }); 97 | ``` 98 | 99 | And you're done! The package will now automatically add a basic audit log for your model to track who inserted and last updated your records. 100 | 101 | ## Change log 102 | 103 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 104 | 105 | ## Testing 106 | 107 | ``` bash 108 | composer test 109 | ``` 110 | 111 | ## Contributing 112 | 113 | Please see [CONTRIBUTING](CONTRIBUTING.md) and [CONDUCT](CONDUCT.md) for details. 114 | 115 | ## Security 116 | 117 | If you discover any security related issues, please email aqangeles@gmail.com instead of using the issue tracker. 118 | 119 | ## Credits 120 | 121 | - [Arjay Angeles][link-author] 122 | - [All Contributors][link-contributors] 123 | 124 | ## License 125 | 126 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 127 | 128 | [ico-version]: https://img.shields.io/packagist/v/yajra/laravel-auditable.svg?style=flat-square 129 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 130 | [ico-travis]: https://img.shields.io/travis/yajra/laravel-auditable/master.svg?style=flat-square 131 | [ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/yajra/laravel-auditable.svg?style=flat-square 132 | [ico-code-quality]: https://img.shields.io/scrutinizer/g/yajra/laravel-auditable.svg?style=flat-square 133 | [ico-downloads]: https://img.shields.io/packagist/dt/yajra/laravel-auditable.svg?style=flat-square 134 | 135 | [link-packagist]: https://packagist.org/packages/yajra/laravel-auditable 136 | [link-travis]: https://travis-ci.org/yajra/laravel-auditable 137 | [link-downloads]: https://packagist.org/packages/yajra/laravel-auditable 138 | [link-author]: https://github.com/yajra 139 | [link-contributors]: ../../contributors 140 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yajra/laravel-auditable", 3 | "description": "A simple Laravel user auditing package for Eloquent Model.", 4 | "keywords": [ 5 | "yajra", 6 | "laravel", 7 | "auditable" 8 | ], 9 | "homepage": "https://github.com/yajra/laravel-auditable", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Arjay Angeles", 14 | "email": "aqangeles@gmail.com", 15 | "homepage": "https://yajrabox.com", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.2", 21 | "illuminate/support": "^12.0", 22 | "illuminate/database": "^12.0" 23 | }, 24 | "require-dev": { 25 | "larastan/larastan": "^3.1", 26 | "laravel/pint": "^1.21", 27 | "rector/rector": "^2.0.9", 28 | "orchestra/testbench": "^10.0", 29 | "pestphp/pest": "^3.7.4", 30 | "pestphp/pest-plugin-laravel": "^3.1" 31 | }, 32 | "config": { 33 | "allow-plugins": { 34 | "pestphp/pest-plugin": true 35 | } 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "Yajra\\Auditable\\": "src" 40 | } 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "Yajra\\Auditable\\Tests\\": "tests" 45 | } 46 | }, 47 | "extra": { 48 | "branch-alias": { 49 | "dev-master": "12.x-dev" 50 | }, 51 | "laravel": { 52 | "providers": [ 53 | "Yajra\\Auditable\\AuditableServiceProvider" 54 | ] 55 | } 56 | }, 57 | "scripts": { 58 | "test": "./vendor/bin/pest", 59 | "pint": "./vendor/bin/pint", 60 | "rector": "./vendor/bin/rector", 61 | "stan": "./vendor/bin/phpstan analyse --memory-limit=2G --ansi --no-progress --no-interaction --configuration=phpstan.neon.dist", 62 | "pr": [ 63 | "@rector", 64 | "@pint", 65 | "@stan", 66 | "@test" 67 | ] 68 | }, 69 | "funding": [ 70 | { 71 | "type": "github", 72 | "url": "https://github.com/sponsors/yajra" 73 | } 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /src/AuditableServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(__DIR__.'/config/auditable.php', 'auditable'); 16 | $this->publishes([ 17 | __DIR__.'/config/auditable.php' => base_path('config/auditable.php'), 18 | ], 'auditable'); 19 | } 20 | 21 | /** 22 | * Register the service provider. 23 | */ 24 | public function register(): void 25 | { 26 | Blueprint::macro('auditable', function () { 27 | /** @var Blueprint $blueprint */ 28 | $blueprint = $this; 29 | $blueprint->unsignedBigInteger('created_by')->nullable()->index(); 30 | $blueprint->unsignedBigInteger('updated_by')->nullable()->index(); 31 | }); 32 | 33 | Blueprint::macro('dropAuditable', function () { 34 | /** @var Blueprint $blueprint */ 35 | $blueprint = $this; 36 | $blueprint->dropColumn(['created_by', 'updated_by']); 37 | }); 38 | 39 | Blueprint::macro('auditableWithDeletes', function () { 40 | /** @var Blueprint $blueprint */ 41 | $blueprint = $this; 42 | $blueprint->unsignedBigInteger('created_by')->nullable()->index(); 43 | $blueprint->unsignedBigInteger('updated_by')->nullable()->index(); 44 | $blueprint->unsignedBigInteger('deleted_by')->nullable()->index(); 45 | }); 46 | 47 | Blueprint::macro('dropAuditableWithDeletes', function () { 48 | /** @var Blueprint $blueprint */ 49 | $blueprint = $this; 50 | $blueprint->dropColumn(['created_by', 'updated_by', 'deleted_by']); 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/AuditableTrait.php: -------------------------------------------------------------------------------- 1 | belongsTo($this->getUserClass(), $this->getCreatedByColumn()) 29 | ->withDefault(config('auditable.defaults.creator')); 30 | } 31 | 32 | /** 33 | * Get user class. 34 | */ 35 | protected function getUserClass(): string 36 | { 37 | if (property_exists($this, 'auditUser')) { 38 | return $this->auditUser; 39 | } 40 | 41 | return config('auth.providers.users.model', 'App\User'); 42 | } 43 | 44 | /** 45 | * Get column name for created by. 46 | */ 47 | public function getCreatedByColumn(): string 48 | { 49 | return defined('static::CREATED_BY') ? static::CREATED_BY : 'created_by'; 50 | } 51 | 52 | /** 53 | * Get user model who updated the record. 54 | */ 55 | public function updater(): BelongsTo 56 | { 57 | return $this->belongsTo($this->getUserClass(), $this->getUpdatedByColumn()) 58 | ->withDefault(config('auditable.defaults.updater')); 59 | } 60 | 61 | /** 62 | * Get column name for updated by. 63 | */ 64 | public function getUpdatedByColumn(): string 65 | { 66 | return defined('static::UPDATED_BY') ? static::UPDATED_BY : 'updated_by'; 67 | } 68 | 69 | /** 70 | * Get created by user full name. 71 | */ 72 | public function getCreatedByNameAttribute(): string 73 | { 74 | return $this->creator->name ?? ''; 75 | } 76 | 77 | /** 78 | * Get updated by user full name. 79 | */ 80 | public function getUpdatedByNameAttribute(): string 81 | { 82 | return $this->updater->name ?? ''; 83 | } 84 | 85 | /** 86 | * Query scope to limit results to own records. 87 | */ 88 | public function scopeOwned(Builder $query): Builder 89 | { 90 | return $query->where($this->getQualifiedUserIdColumn(), auth()->id()); 91 | } 92 | 93 | /** 94 | * Get qualified column name for user id. 95 | */ 96 | public function getQualifiedUserIdColumn(): string 97 | { 98 | return $this->getTable().'.'.$this->getUserInstance()->getKey(); 99 | } 100 | 101 | /** 102 | * Get Laravel's user class instance. 103 | */ 104 | public function getUserInstance(): Model 105 | { 106 | $class = $this->getUserClass(); 107 | 108 | return new $class; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/AuditableTraitObserver.php: -------------------------------------------------------------------------------- 1 | getCreatedByColumn(); 16 | 17 | if (! $model->$createdBy) { 18 | $model->$createdBy = $this->getAuthenticatedUserId(); 19 | } 20 | } 21 | 22 | if (method_exists($model, 'getUpdatedByColumn')) { 23 | $updatedBy = $model->getUpdatedByColumn(); 24 | 25 | if (! $model->$updatedBy) { 26 | $model->$updatedBy = $this->getAuthenticatedUserId(); 27 | } 28 | } 29 | } 30 | 31 | /** 32 | * Get authenticated user id depending on model's auth guard. 33 | */ 34 | protected function getAuthenticatedUserId(): int|string|null 35 | { 36 | return auth()->check() ? auth()->id() : null; 37 | } 38 | 39 | /** 40 | * Model's updating event hook. 41 | */ 42 | public function updating(Model $model): void 43 | { 44 | if (method_exists($model, 'getUpdatedByColumn')) { 45 | $updatedBy = $model->getUpdatedByColumn(); 46 | 47 | if (! $model->isDirty($updatedBy)) { 48 | $model->$updatedBy = $this->getAuthenticatedUserId(); 49 | } 50 | } 51 | } 52 | 53 | /** 54 | * Set updatedBy column on save if value is not the same. 55 | */ 56 | public function saved(Model $model): void 57 | { 58 | if (method_exists($model, 'getUpdatedByColumn')) { 59 | $updatedBy = $model->getUpdatedByColumn(); 60 | 61 | if ($this->getAuthenticatedUserId() && $this->getAuthenticatedUserId() != $model->$updatedBy && $model->isDirty()) { 62 | $model->$updatedBy = $this->getAuthenticatedUserId(); 63 | $model->saveQuietly(); 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/AuditableWithDeletesTrait.php: -------------------------------------------------------------------------------- 1 | belongsTo($this->getUserClass(), $this->getDeletedByColumn()) 28 | ->withDefault(config('auditable.defaults.deleter')); 29 | } 30 | 31 | /** 32 | * Get column name for deleted by. 33 | */ 34 | public function getDeletedByColumn(): string 35 | { 36 | return defined('static::DELETED_BY') ? static::DELETED_BY : 'deleted_by'; 37 | } 38 | 39 | /** 40 | * Get deleted by user full name. 41 | */ 42 | public function getDeletedByNameAttribute(): string 43 | { 44 | return $this->deleter->name ?? ''; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/AuditableWithDeletesTraitObserver.php: -------------------------------------------------------------------------------- 1 | getDeletedByColumn(); 16 | 17 | $model->$deletedBy = $this->getAuthenticatedUserId(); 18 | $model->saveQuietly(); 19 | } 20 | } 21 | 22 | /** 23 | * Get authenticated user id depending on model's auth guard. 24 | */ 25 | protected function getAuthenticatedUserId(): int|string|null 26 | { 27 | return auth()->check() ? auth()->id() : null; 28 | } 29 | 30 | /** 31 | * Model's restoring event hook 32 | */ 33 | public function restoring(Model $model): void 34 | { 35 | if (method_exists($model, 'getDeletedByColumn')) { 36 | $deletedBy = $model->getDeletedByColumn(); 37 | 38 | $model->$deletedBy = null; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/config/auditable.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'creator' => [ 6 | 'name' => '', 7 | // add creator default values if not set 8 | ], 9 | 10 | 'updater' => [ 11 | 'name' => '', 12 | // add updater default values if not set 13 | ], 14 | 15 | 'deleter' => [ 16 | 'name' => '', 17 | // add deleter default values if not set 18 | ], 19 | ], 20 | ]; 21 | --------------------------------------------------------------------------------