├── .styleci.yml ├── src ├── Exceptions │ ├── CouldNotLogActivity.php │ ├── CouldNotLogChanges.php │ └── InvalidConfiguration.php ├── Traits │ ├── CausesActivity.php │ ├── LogsActivity.php │ └── DetectsChanges.php ├── helpers.php ├── ActivityLogStatus.php ├── Contracts │ └── Activity.php ├── CleanActivitylogCommand.php ├── ActivitylogServiceProvider.php ├── Models │ └── Activity.php └── ActivityLogger.php ├── UPGRADING.md ├── LICENSE.md ├── migrations └── create_activity_log_table.php.stub ├── config └── activitylog.php ├── composer.json ├── CONTRIBUTING.md ├── CHANGELOG.md └── README.md /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | -------------------------------------------------------------------------------- /src/Exceptions/CouldNotLogActivity.php: -------------------------------------------------------------------------------- 1 | morphMany( 13 | ActivitylogServiceProvider::determineActivityModel(), 14 | 'causer' 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidConfiguration.php: -------------------------------------------------------------------------------- 1 | useLog($logName ?? $defaultLogName) 15 | ->setLogStatus($logStatus); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ActivityLogStatus.php: -------------------------------------------------------------------------------- 1 | enabled = $config['activitylog.enabled']; 14 | } 15 | 16 | public function enable(): bool 17 | { 18 | return $this->enabled = true; 19 | } 20 | 21 | public function disable(): bool 22 | { 23 | return $this->enabled = false; 24 | } 25 | 26 | public function disabled(): bool 27 | { 28 | return $this->enabled === false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Contracts/Activity.php: -------------------------------------------------------------------------------- 1 | changes()` to `$activity->changes` 6 | - the `activity` relation of the `CausesActivity` trait has been renamed to `actions`. Rename all uses from `$user->activity` to `$user->actions` 7 | - the `activity` relation of the `LogsActivity` trait has been renamed to `activities`. Rename all uses from `$yourModel->activity` to `$yourModel->activities`. 8 | - the deprecated `loggedActivity` relation has been removed. Use `activities` instead. 9 | - the `HasActivity` trait has been removed. Use both `CausesActivity` and `LogsActivity` traits instead. -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 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 | -------------------------------------------------------------------------------- /migrations/create_activity_log_table.php.stub: -------------------------------------------------------------------------------- 1 | increments('id'); 16 | $table->string('log_name')->nullable(); 17 | $table->text('description'); 18 | $table->integer('subject_id')->nullable(); 19 | $table->string('subject_type')->nullable(); 20 | $table->integer('causer_id')->nullable(); 21 | $table->string('causer_type')->nullable(); 22 | $table->text('properties')->nullable(); 23 | $table->timestamps(); 24 | 25 | $table->index('log_name'); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists(config('activitylog.table_name')); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/CleanActivitylogCommand.php: -------------------------------------------------------------------------------- 1 | comment('Cleaning activity log...'); 19 | 20 | $log = $this->argument('log'); 21 | 22 | $maxAgeInDays = config('activitylog.delete_records_older_than_days'); 23 | 24 | $cutOffDate = Carbon::now()->subDays($maxAgeInDays)->format('Y-m-d H:i:s'); 25 | 26 | $activity = ActivitylogServiceProvider::getActivityModelInstance(); 27 | 28 | $amountDeleted = $activity::where('created_at', '<', $cutOffDate) 29 | ->when($log !== null, function (Builder $query) use ($log) { 30 | $query->inLog($log); 31 | }) 32 | ->delete(); 33 | 34 | $this->info("Deleted {$amountDeleted} record(s) from the activity log."); 35 | 36 | $this->comment('All done!'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /config/activitylog.php: -------------------------------------------------------------------------------- 1 | env('ACTIVITY_LOGGER_ENABLED', true), 9 | 10 | /* 11 | * When the clean-command is executed, all recording activities older than 12 | * the number of days specified here will be deleted. 13 | */ 14 | 'delete_records_older_than_days' => 365, 15 | 16 | /* 17 | * If no log name is passed to the activity() helper 18 | * we use this default log name. 19 | */ 20 | 'default_log_name' => 'default', 21 | 22 | /* 23 | * You can specify an auth driver here that gets user models. 24 | * If this is null we'll use the default Laravel auth driver. 25 | */ 26 | 'default_auth_driver' => null, 27 | 28 | /* 29 | * If set to true, the subject returns soft deleted models. 30 | */ 31 | 'subject_returns_soft_deleted_models' => false, 32 | 33 | /* 34 | * This model will be used to log activity. 35 | * It should be implements the Spatie\Activitylog\Contracts\Activity interface 36 | * and extend Illuminate\Database\Eloquent\Model. 37 | */ 38 | 'activity_model' => \Spatie\Activitylog\Models\Activity::class, 39 | 40 | /* 41 | * This is the name of the table that will be created by the migration and 42 | * used by the Activity model shipped with this package. 43 | */ 44 | 'table_name' => 'activity_log', 45 | ]; 46 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/laravel-activitylog", 3 | "description": "A very simple activity logger to monitor the users of your website or application", 4 | "homepage": "https://github.com/spatie/activitylog", 5 | "keywords": 6 | [ 7 | "spatie", 8 | "log", 9 | "user", 10 | "activity", 11 | "laravel" 12 | ], 13 | "authors": [ 14 | { 15 | "name": "Freek Van der Herten", 16 | "email": "freek@spatie.be", 17 | "homepage": "https://spatie.be", 18 | "role": "Developer" 19 | }, 20 | { 21 | "name": "Sebastian De Deyne", 22 | "email": "sebastian@spatie.be", 23 | "homepage": "https://spatie.be", 24 | "role": "Developer" 25 | } 26 | ], 27 | "require": { 28 | "php" : "^7.2", 29 | "illuminate/config": "~5.8.0", 30 | "illuminate/database": "~5.8.0", 31 | "illuminate/support": "~5.8.0", 32 | "spatie/string": "^2.1" 33 | }, 34 | "require-dev": { 35 | "ext-json": "*", 36 | "phpunit/phpunit": "^7.5|^8.0", 37 | "orchestra/testbench": "~3.8.0", 38 | "scrutinizer/ocular": "^1.5" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "Spatie\\Activitylog\\": "src" 43 | }, 44 | "files": [ 45 | "src/helpers.php" 46 | ] 47 | }, 48 | "autoload-dev": { 49 | "psr-4": { 50 | "Spatie\\Activitylog\\Test\\": "tests" 51 | } 52 | }, 53 | "scripts": { 54 | "test": "vendor/bin/phpunit" 55 | }, 56 | "license": "MIT", 57 | "extra": { 58 | "laravel": { 59 | "providers": [ 60 | "Spatie\\Activitylog\\ActivitylogServiceProvider" 61 | ] 62 | } 63 | }, 64 | "config": { 65 | "sort-packages": true 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/ActivitylogServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 17 | __DIR__.'/../config/activitylog.php' => config_path('activitylog.php'), 18 | ], 'config'); 19 | 20 | $this->mergeConfigFrom(__DIR__.'/../config/activitylog.php', 'activitylog'); 21 | 22 | if (! class_exists('CreateActivityLogTable')) { 23 | $timestamp = date('Y_m_d_His', time()); 24 | 25 | $this->publishes([ 26 | __DIR__.'/../migrations/create_activity_log_table.php.stub' => database_path("/migrations/{$timestamp}_create_activity_log_table.php"), 27 | ], 'migrations'); 28 | } 29 | } 30 | 31 | public function register() 32 | { 33 | $this->app->bind('command.activitylog:clean', CleanActivitylogCommand::class); 34 | 35 | $this->commands([ 36 | 'command.activitylog:clean', 37 | ]); 38 | 39 | $this->app->bind(ActivityLogger::class); 40 | 41 | $this->app->singleton(ActivityLogStatus::class); 42 | } 43 | 44 | public static function determineActivityModel(): string 45 | { 46 | $activityModel = config('activitylog.activity_model') ?? ActivityModel::class; 47 | 48 | if (! is_a($activityModel, Activity::class, true) 49 | || ! is_a($activityModel, Model::class, true)) { 50 | throw InvalidConfiguration::modelIsNotValid($activityModel); 51 | } 52 | 53 | return $activityModel; 54 | } 55 | 56 | public static function getActivityModelInstance(): ActivityContract 57 | { 58 | $activityModelClassName = self::determineActivityModel(); 59 | 60 | return new $activityModelClassName(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Models/Activity.php: -------------------------------------------------------------------------------- 1 | 'collection', 18 | ]; 19 | 20 | public function __construct(array $attributes = []) 21 | { 22 | if (! isset($this->table)) { 23 | $this->setTable(config('activitylog.table_name')); 24 | } 25 | 26 | parent::__construct($attributes); 27 | } 28 | 29 | public function subject(): MorphTo 30 | { 31 | if (config('activitylog.subject_returns_soft_deleted_models')) { 32 | return $this->morphTo()->withTrashed(); 33 | } 34 | 35 | return $this->morphTo(); 36 | } 37 | 38 | public function causer(): MorphTo 39 | { 40 | return $this->morphTo(); 41 | } 42 | 43 | public function getExtraProperty(string $propertyName) 44 | { 45 | return Arr::get($this->properties->toArray(), $propertyName); 46 | } 47 | 48 | public function changes(): Collection 49 | { 50 | if (! $this->properties instanceof Collection) { 51 | return new Collection(); 52 | } 53 | 54 | return $this->properties->only(['attributes', 'old']); 55 | } 56 | 57 | public function getChangesAttribute(): Collection 58 | { 59 | return $this->changes(); 60 | } 61 | 62 | public function scopeInLog(Builder $query, ...$logNames): Builder 63 | { 64 | if (is_array($logNames[0])) { 65 | $logNames = $logNames[0]; 66 | } 67 | 68 | return $query->whereIn('log_name', $logNames); 69 | } 70 | 71 | public function scopeCausedBy(Builder $query, Model $causer): Builder 72 | { 73 | return $query 74 | ->where('causer_type', $causer->getMorphClass()) 75 | ->where('causer_id', $causer->getKey()); 76 | } 77 | 78 | public function scopeForSubject(Builder $query, Model $subject): Builder 79 | { 80 | return $query 81 | ->where('subject_type', $subject->getMorphClass()) 82 | ->where('subject_id', $subject->getKey()); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 52 | 53 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 54 | 55 | **Happy coding**! 56 | -------------------------------------------------------------------------------- /src/Traits/LogsActivity.php: -------------------------------------------------------------------------------- 1 | each(function ($eventName) { 22 | return static::$eventName(function (Model $model) use ($eventName) { 23 | if (! $model->shouldLogEvent($eventName)) { 24 | return; 25 | } 26 | 27 | $description = $model->getDescriptionForEvent($eventName); 28 | 29 | $logName = $model->getLogNameToUse($eventName); 30 | 31 | if ($description == '') { 32 | return; 33 | } 34 | 35 | $logger = app(ActivityLogger::class) 36 | ->useLog($logName) 37 | ->performedOn($model) 38 | ->withProperties($model->attributeValuesToBeLogged($eventName)); 39 | 40 | if (method_exists($model, 'tapActivity')) { 41 | $logger->tap([$model, 'tapActivity'], $eventName); 42 | } 43 | 44 | $logger->log($description); 45 | }); 46 | }); 47 | } 48 | 49 | public function disableLogging() 50 | { 51 | $this->enableLoggingModelsEvents = false; 52 | 53 | return $this; 54 | } 55 | 56 | public function enableLogging() 57 | { 58 | $this->enableLoggingModelsEvents = true; 59 | 60 | return $this; 61 | } 62 | 63 | public function activities(): MorphMany 64 | { 65 | return $this->morphMany(ActivitylogServiceProvider::determineActivityModel(), 'subject'); 66 | } 67 | 68 | public function getDescriptionForEvent(string $eventName): string 69 | { 70 | return $eventName; 71 | } 72 | 73 | public function getLogNameToUse(string $eventName = ''): string 74 | { 75 | if (isset(static::$logName)) { 76 | return static::$logName; 77 | } 78 | 79 | return config('activitylog.default_log_name'); 80 | } 81 | 82 | /* 83 | * Get the event names that should be recorded. 84 | */ 85 | protected static function eventsToBeRecorded(): Collection 86 | { 87 | if (isset(static::$recordEvents)) { 88 | return collect(static::$recordEvents); 89 | } 90 | 91 | $events = collect([ 92 | 'created', 93 | 'updated', 94 | 'deleted', 95 | ]); 96 | 97 | if (collect(class_uses_recursive(static::class))->contains(SoftDeletes::class)) { 98 | $events->push('restored'); 99 | } 100 | 101 | return $events; 102 | } 103 | 104 | public function attributesToBeIgnored(): array 105 | { 106 | if (! isset(static::$ignoreChangedAttributes)) { 107 | return []; 108 | } 109 | 110 | return static::$ignoreChangedAttributes; 111 | } 112 | 113 | protected function shouldLogEvent(string $eventName): bool 114 | { 115 | if (! $this->enableLoggingModelsEvents) { 116 | return false; 117 | } 118 | 119 | if (! in_array($eventName, ['created', 'updated'])) { 120 | return true; 121 | } 122 | 123 | if (Arr::has($this->getDirty(), 'deleted_at')) { 124 | if ($this->getDirty()['deleted_at'] === null) { 125 | return false; 126 | } 127 | } 128 | 129 | //do not log update event if only ignored attributes are changed 130 | return (bool) count(Arr::except($this->getDirty(), $this->attributesToBeIgnored())); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Traits/DetectsChanges.php: -------------------------------------------------------------------------------- 1 | contains('updated')) { 16 | static::updating(function (Model $model) { 17 | 18 | //temporary hold the original attributes on the model 19 | //as we'll need these in the updating event 20 | $oldValues = $model->replicate()->setRawAttributes($model->getOriginal()); 21 | 22 | $model->oldAttributes = static::logChanges($oldValues); 23 | }); 24 | } 25 | } 26 | 27 | public function attributesToBeLogged(): array 28 | { 29 | $attributes = []; 30 | 31 | if (isset(static::$logFillable) && static::$logFillable) { 32 | $attributes = array_merge($attributes, $this->getFillable()); 33 | } 34 | 35 | if ($this->shouldLogUnguarded()) { 36 | $attributes = array_merge($attributes, array_diff(array_keys($this->getAttributes()), $this->getGuarded())); 37 | } 38 | 39 | if (isset(static::$logAttributes) && is_array(static::$logAttributes)) { 40 | $attributes = array_merge($attributes, array_diff(static::$logAttributes, ['*'])); 41 | 42 | if (in_array('*', static::$logAttributes)) { 43 | $attributes = array_merge($attributes, array_keys($this->getAttributes())); 44 | } 45 | } 46 | 47 | if (isset(static::$logAttributesToIgnore) && is_array(static::$logAttributesToIgnore)) { 48 | $attributes = array_diff($attributes, static::$logAttributesToIgnore); 49 | } 50 | 51 | return $attributes; 52 | } 53 | 54 | public function shouldLogOnlyDirty(): bool 55 | { 56 | if (! isset(static::$logOnlyDirty)) { 57 | return false; 58 | } 59 | 60 | return static::$logOnlyDirty; 61 | } 62 | 63 | public function shouldLogUnguarded(): bool 64 | { 65 | if (! isset(static::$logUnguarded)) { 66 | return false; 67 | } 68 | 69 | if (! static::$logUnguarded) { 70 | return false; 71 | } 72 | 73 | if (in_array('*', $this->getGuarded())) { 74 | return false; 75 | } 76 | 77 | return true; 78 | } 79 | 80 | public function attributeValuesToBeLogged(string $processingEvent): array 81 | { 82 | if (! count($this->attributesToBeLogged())) { 83 | return []; 84 | } 85 | 86 | $properties['attributes'] = static::logChanges( 87 | $this->exists 88 | ? $this->fresh() ?? $this 89 | : $this 90 | ); 91 | 92 | if (static::eventsToBeRecorded()->contains('updated') && $processingEvent == 'updated') { 93 | $nullProperties = array_fill_keys(array_keys($properties['attributes']), null); 94 | 95 | $properties['old'] = array_merge($nullProperties, $this->oldAttributes); 96 | } 97 | 98 | if ($this->shouldLogOnlyDirty() && isset($properties['old'])) { 99 | $properties['attributes'] = array_udiff_assoc( 100 | $properties['attributes'], 101 | $properties['old'], 102 | function ($new, $old) { 103 | return $new <=> $old; 104 | } 105 | ); 106 | $properties['old'] = collect($properties['old']) 107 | ->only(array_keys($properties['attributes'])) 108 | ->all(); 109 | } 110 | 111 | return $properties; 112 | } 113 | 114 | public static function logChanges(Model $model): array 115 | { 116 | $changes = []; 117 | $attributes = $model->attributesToBeLogged(); 118 | $model = clone $model; 119 | $model->append(array_filter($attributes, function ($key) use ($model) { 120 | return $model->hasGetMutator($key); 121 | })); 122 | $model->setHidden(array_diff($model->getHidden(), $attributes)); 123 | $collection = collect($model); 124 | 125 | foreach ($attributes as $attribute) { 126 | if (Str::contains($attribute, '.')) { 127 | $changes += self::getRelatedModelAttributeValue($model, $attribute); 128 | } else { 129 | $changes += $collection->only($attribute)->toArray(); 130 | } 131 | } 132 | 133 | return $changes; 134 | } 135 | 136 | protected static function getRelatedModelAttributeValue(Model $model, string $attribute): array 137 | { 138 | if (substr_count($attribute, '.') > 1) { 139 | throw CouldNotLogChanges::invalidAttribute($attribute); 140 | } 141 | 142 | list($relatedModelName, $relatedAttribute) = explode('.', $attribute); 143 | 144 | $relatedModel = $model->$relatedModelName ?? $model->$relatedModelName(); 145 | 146 | return ["{$relatedModelName}.{$relatedAttribute}" => $relatedModel->$relatedAttribute ?? null]; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/ActivityLogger.php: -------------------------------------------------------------------------------- 1 | auth = $auth; 35 | 36 | $this->authDriver = $config['activitylog']['default_auth_driver'] ?? $auth->getDefaultDriver(); 37 | 38 | $this->defaultLogName = $config['activitylog']['default_log_name']; 39 | 40 | $this->logStatus = $logStatus; 41 | } 42 | 43 | public function setLogStatus(ActivityLogStatus $logStatus) 44 | { 45 | $this->logStatus = $logStatus; 46 | 47 | return $this; 48 | } 49 | 50 | public function performedOn(Model $model) 51 | { 52 | $this->getActivity()->subject()->associate($model); 53 | 54 | return $this; 55 | } 56 | 57 | public function on(Model $model) 58 | { 59 | return $this->performedOn($model); 60 | } 61 | 62 | public function causedBy($modelOrId) 63 | { 64 | if ($modelOrId === null) { 65 | return $this; 66 | } 67 | 68 | $model = $this->normalizeCauser($modelOrId); 69 | 70 | $this->getActivity()->causer()->associate($model); 71 | 72 | return $this; 73 | } 74 | 75 | public function by($modelOrId) 76 | { 77 | return $this->causedBy($modelOrId); 78 | } 79 | 80 | public function withProperties($properties) 81 | { 82 | $this->getActivity()->properties = collect($properties); 83 | 84 | return $this; 85 | } 86 | 87 | public function withProperty(string $key, $value) 88 | { 89 | $this->getActivity()->properties = $this->getActivity()->properties->put($key, $value); 90 | 91 | return $this; 92 | } 93 | 94 | public function useLog(string $logName) 95 | { 96 | $this->getActivity()->log_name = $logName; 97 | 98 | return $this; 99 | } 100 | 101 | public function inLog(string $logName) 102 | { 103 | return $this->useLog($logName); 104 | } 105 | 106 | public function tap(callable $callback, string $eventName = null) 107 | { 108 | call_user_func($callback, $this->getActivity(), $eventName); 109 | 110 | return $this; 111 | } 112 | 113 | public function enableLogging() 114 | { 115 | $this->logStatus->enable(); 116 | 117 | return $this; 118 | } 119 | 120 | public function disableLogging() 121 | { 122 | $this->logStatus->disable(); 123 | 124 | return $this; 125 | } 126 | 127 | public function log(string $description) 128 | { 129 | if ($this->logStatus->disabled()) { 130 | return; 131 | } 132 | 133 | $activity = $this->activity; 134 | 135 | $activity->description = $this->replacePlaceholders($description, $activity); 136 | 137 | $activity->save(); 138 | 139 | $this->activity = null; 140 | 141 | return $activity; 142 | } 143 | 144 | protected function normalizeCauser($modelOrId): Model 145 | { 146 | if ($modelOrId instanceof Model) { 147 | return $modelOrId; 148 | } 149 | 150 | $guard = $this->auth->guard($this->authDriver); 151 | $provider = method_exists($guard, 'getProvider') ? $guard->getProvider() : null; 152 | $model = method_exists($provider, 'retrieveById') ? $provider->retrieveById($modelOrId) : null; 153 | 154 | if ($model instanceof Model) { 155 | return $model; 156 | } 157 | 158 | throw CouldNotLogActivity::couldNotDetermineUser($modelOrId); 159 | } 160 | 161 | protected function replacePlaceholders(string $description, ActivityContract $activity): string 162 | { 163 | return preg_replace_callback('/:[a-z0-9._-]+/i', function ($match) use ($activity) { 164 | $match = $match[0]; 165 | 166 | $attribute = (string) (new Str($match))->between(':', '.'); 167 | 168 | if (! in_array($attribute, ['subject', 'causer', 'properties'])) { 169 | return $match; 170 | } 171 | 172 | $propertyName = substr($match, strpos($match, '.') + 1); 173 | 174 | $attributeValue = $activity->$attribute; 175 | 176 | if (is_null($attributeValue)) { 177 | return $match; 178 | } 179 | 180 | $attributeValue = $attributeValue->toArray(); 181 | 182 | return Arr::get($attributeValue, $propertyName, $match); 183 | }, $description); 184 | } 185 | 186 | protected function getActivity(): ActivityContract 187 | { 188 | if (! $this->activity instanceof ActivityContract) { 189 | $this->activity = ActivitylogServiceProvider::getActivityModelInstance(); 190 | $this 191 | ->useLog($this->defaultLogName) 192 | ->withProperties([]) 193 | ->causedBy($this->auth->guard($this->authDriver)->user()); 194 | } 195 | 196 | return $this->activity; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `spatie/laravel-activitylog` will be documented in this file 4 | 5 | ## 3.3.0 - 2019-02-27 6 | 7 | - drop support for Laravel 5.7 and lower 8 | - drop support for PHP 7.1 and lower 9 | 10 | ## 3.2.2 - 2019-02-27 11 | 12 | - add support for Laravel 5.8 13 | - fix logging hidden attributes 14 | - fix logging for a causer model without a provider 15 | - add code coverage reporting for repository 16 | 17 | ## 3.2.1 - 2019-02-01 18 | 19 | - use Str:: and Arr:: instead of helper methods 20 | 21 | ## 3.2.0 - 2019-01-29 22 | 23 | - add `ActivityLogger::tap()` method 24 | - add `LogsActivity::tapActivity()` method 25 | - the `ActivityLogger` will work on an activity model instance instead of cache variables 26 | 27 | ## 3.1.2 - 2018-10-18 28 | 29 | - add `shouldLogUnguarded()` method 30 | - fix typo in methodname `shouldLogOnlyDirty()` 31 | 32 | ## 3.1.1 - 2018-10-17 33 | 34 | - fix `$logUnguarded` 35 | 36 | ## 3.1.0 - 2018-10-17 37 | 38 | - add `$logUnguarded` 39 | 40 | ## 3.0.0 - 2018-10-16 41 | - the preferred way to get changes on an `Activity` model is through the `changes` property instead of the `changes()` function 42 | - the `activity` relation of the `CausesActivity` trait has been renamed to `actions` 43 | - the `activity` relation of the `LogsActivity` trait has been renamed to `activities` 44 | - the deprecated `loggedActivity` relation has been removed 45 | - the `HasActivity` trait has been removed. 46 | - fix for setting a custom table name for the `Activity` model via the `$table` property 47 | - support for PHP 7.0 has been dropped 48 | 49 | ## 2.8.4. - 2018-09-23 50 | - improve migration 51 | 52 | ## 2.8.3 - 2018-09-01 53 | - add support for L5.7 54 | 55 | ## 2.8.2 - 2018-07-28 56 | - allow `null` to be passed to `causedBy` 57 | 58 | ## 2.8.1 - 2018-07-28 59 | - make sure a fresh instance of `ActivityLogger` is used 60 | 61 | ## 2.8.0 - 2018-07-21 62 | - add `enableLogging()` and `disableLogging()` 63 | 64 | ## 2.7.0 - 2018-06-18 65 | - add ability to ignore changes to attributes specified in `$logAttributesToIgnore` 66 | 67 | ## 2.6.0 - 2018-04-03 68 | - add `table_name` config option 69 | 70 | ## 2.5.1 - 2018-02-11 71 | - improve support for soft deletes 72 | 73 | ## 2.5.0 - 2018-02-09 74 | - allow model to override the default log name 75 | 76 | ## 2.4.2 - 2018-02-08 77 | - add compatibility with L5.6 78 | 79 | ## 2.4.1 - 2018-01-20 80 | - use a `text` column for `description` 81 | 82 | ## 2.4.0 - 2018-01-20 83 | - add `HasActivity` 84 | 85 | ## 2.3.2 - 2017-12-13 86 | - fix bugs concerning `attributesToBeLogged` 87 | 88 | ## 2.3.1 - 2017-11-13 89 | - allow nullable relation when using `logChanges` 90 | 91 | ## 2.3.0 - 2017-11-07 92 | - add a `log` argument to `activitylog:clean` 93 | 94 | ## 2.2.0 - 2017-10-16 95 | - add support for logging all changed attributes using `*` 96 | 97 | ## 2.1.2 - 2017-09-28 98 | - fix for logging changes attributes when deleting soft deletable models 99 | 100 | ## 2.1.1 - 2017-09-12 101 | - make sure `properties` always is a collection 102 | 103 | ## 2.1.0 - 2017-09-19 104 | - added support for logging fillable attributes 105 | 106 | ## 2.0.0 - 2017-08-30 107 | - added support for Laravel 5.5, dropped support for older laravel versions 108 | - renamed config file from `laravel-activitylog` to `activitylog` 109 | - rename `getChangesAttribute` function to `changes` so it doesn't conflict with Laravel's native functionality 110 | 111 | ## 1.16.0 - 2017-06-28 112 | - added `enableLogging` and `disableLogging` 113 | 114 | ## 1.15.5 - 2017-08-08 115 | - fix model scope 116 | 117 | ## 1.15.4 - 2017-08-05 118 | - fix detecting `SoftDeletes` 119 | 120 | ## 1.15.3 - 2017-06-23 121 | - fix for when there is no 'web' guard 122 | 123 | ## 1.15.2 - 2017-06-15 124 | - fixes errors in `DetectsChanges` 125 | 126 | ## 1.15.1 - 2017-04-28 127 | - fixes error in `DetectsChanges` 128 | 129 | ## 1.15.0 - 2017-04-28 130 | - add compatibility with L5.1 and L5.2 131 | 132 | ## 1.14.0 - 2017-04-16 133 | - add support array/collection casted attributes when using `logDirtyOnly` 134 | 135 | ## 1.13.0 - 2017-04-16 136 | - add `logDirtyOnly` 137 | 138 | ## 1.12.2 - 2017-03-22 139 | - fix a bug where changes to a related model would not be logged 140 | 141 | ## 1.12.1 - 2017-02-12 142 | - avoid PHP error when dealing with placeholders that cannot be filled 143 | 144 | ## 1.12.0 - 2017-02-04 145 | - drop support for L5.2 and lower 146 | - add ability to log attributes of related models 147 | 148 | ## 1.11.0 - 2017-01-23 149 | - add support for L5.4 150 | 151 | ## 1.10.4 - 2017-01-20 152 | - `Activity` now extends from `Model` instead of `Eloquent` 153 | 154 | ## 1.10.2 - 2016-11-26 155 | - fix compatibilty for Laravel 5.1 156 | 157 | ## 1.10.1 - 2016-10-11 158 | - fix `scopeCausedBy` and `scopeForSubject` 159 | 160 | ## 1.10.0 - 2016-10-10 161 | - add support for `restored` event 162 | 163 | ## 1.9.2 - 2016-09-27 164 | - fixed a bug where the delete event would not be logged 165 | 166 | ## 1.9.1 - 2016-09-16 167 | - fixed the return value of `activity()->log()`. It will now return the created `Activity`-model. 168 | 169 | ## 1.9.0 - 2016-09-16 170 | - added `Macroable` to `ActivityLogger` 171 | 172 | ## 1.8.0 - 2016-09-12 173 | - added `causedBy` and `forSubject` scopes 174 | 175 | ## 1.7.1 - 2016-08-23 176 | - Added L5.3 compatibility 177 | 178 | ## 1.7.0 - 2016-08-17 179 | - Added `enabled` option in the config file. 180 | 181 | ## 1.6.0 - 2016-08-11 182 | - Added `ignoreChangedAttributes` 183 | 184 | ## 1.5.0 - 2016-08-11 185 | - Added support for using a custom `Activity` model 186 | 187 | ## 1.4.0 - 2016-08-10 188 | - Added support for soft deletes 189 | 190 | ## 1.3.2 - 2016-08-09 191 | - This version replaces version `1.3.0` 192 | - Dropped L5.1 compatibility 193 | 194 | ## 1.3.1 - 2016-08-09 195 | - this version removes the features introduced in 1.3.0 and is compatible with L5.1 196 | 197 | ## 1.3.0 - 2016-07-29 198 | 199 | **DO NOT USE THIS VERSION IF YOU'RE ON L5.1** 200 | 201 | Please upgrade to: 202 | - `1.3.1` for Laravel 5.1 203 | - `1.3.2` for Laravel 5.2 and higher 204 | 205 | Introduced features 206 | - made the auth driver configurable 207 | 208 | ## 1.3.0 - 2016-07-29 209 | 210 | - made the auth driver configurable 211 | 212 | ## 1.2.1 - 2016-07-09 213 | 214 | - use config repo contract 215 | 216 | ## 1.2.0 - 2016-07-08 217 | 218 | - added `getLogNameToUse` 219 | 220 | ## 1.1.0 - 2016-07-04 221 | 222 | - added `activity`-method on both the `CausesActivity` and `LogsActivity`-trait 223 | 224 | ## 1.0.3 - 2016-07-01 225 | 226 | - the package is now compatible with Laravel 5.1 227 | 228 | ## 1.0.2 - 2016-06-29 229 | 230 | - fixed naming of `inLog` scope 231 | - add `inLog` function alias 232 | 233 | ## 1.0.1 - 2016-06-29 234 | 235 | - fixed error when publishing migrations 236 | 237 | ## 1.0.0 - 2016-06-28 238 | 239 | - initial release 240 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Log activity inside your Laravel app 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-activitylog.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-activitylog) 4 | [![Build Status](https://img.shields.io/travis/spatie/laravel-activitylog/master.svg?style=flat-square)](https://travis-ci.org/spatie/laravel-activitylog) 5 | [![Code coverage](https://scrutinizer-ci.com/g/spatie/laravel-activitylog/badges/coverage.png)](https://scrutinizer-ci.com/g/spatie/laravel-activitylog) 6 | [![Quality Score](https://img.shields.io/scrutinizer/g/spatie/laravel-activitylog.svg?style=flat-square)](https://scrutinizer-ci.com/g/spatie/laravel-activitylog) 7 | [![StyleCI](https://styleci.io/repos/61802818/shield)](https://styleci.io/repos/61802818) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-activitylog.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-activitylog) 9 | 10 | The `spatie/laravel-activitylog` package provides easy to use functions to log the activities of the users of your app. It can also automatically log model events. 11 | The Package stores all activity in the `activity_log` table. 12 | 13 | Here's a demo of how you can use it: 14 | 15 | ```php 16 | activity()->log('Look, I logged something'); 17 | ``` 18 | 19 | You can retrieve all activity using the `Spatie\Activitylog\Models\Activity` model. 20 | 21 | ```php 22 | Activity::all(); 23 | ``` 24 | 25 | Here's a more advanced example: 26 | ```php 27 | activity() 28 | ->performedOn($anEloquentModel) 29 | ->causedBy($user) 30 | ->withProperties(['customProperty' => 'customValue']) 31 | ->log('Look, I logged something'); 32 | 33 | $lastLoggedActivity = Activity::all()->last(); 34 | 35 | $lastLoggedActivity->subject; //returns an instance of an eloquent model 36 | $lastLoggedActivity->causer; //returns an instance of your user model 37 | $lastLoggedActivity->getExtraProperty('customProperty'); //returns 'customValue' 38 | $lastLoggedActivity->description; //returns 'Look, I logged something' 39 | ``` 40 | 41 | 42 | Here's an example on [event logging](https://docs.spatie.be/laravel-activitylog/v2/advanced-usage/logging-model-events). 43 | 44 | ```php 45 | $newsItem->name = 'updated name'; 46 | $newsItem->save(); 47 | 48 | //updating the newsItem will cause the logging of an activity 49 | $activity = Activity::all()->last(); 50 | 51 | $activity->description; //returns 'updated' 52 | $activity->subject; //returns the instance of NewsItem that was created 53 | ``` 54 | 55 | Calling `$activity->changes()` will return this array: 56 | 57 | ```php 58 | [ 59 | 'attributes' => [ 60 | 'name' => 'updated name', 61 | 'text' => 'Lorum', 62 | ], 63 | 'old' => [ 64 | 'name' => 'original name', 65 | 'text' => 'Lorum', 66 | ], 67 | ]; 68 | ``` 69 | 70 | 71 | ## Documentation 72 | You'll find the documentation on [https://docs.spatie.be/laravel-activitylog/v3](https://docs.spatie.be/laravel-activitylog/v3). 73 | 74 | Find yourself stuck using the package? Found a bug? Do you have general questions or suggestions for improving the activity log? Feel free to [create an issue on GitHub](https://github.com/spatie/laravel-activitylog/issues), we'll try to address it as soon as possible. 75 | 76 | If you've found a security issue please mail [freek@spatie.be](mailto:freek@spatie.be) instead of using the issue tracker. 77 | 78 | 79 | ## Installation 80 | 81 | You can install the package via composer: 82 | 83 | ``` bash 84 | composer require spatie/laravel-activitylog 85 | ``` 86 | 87 | The package will automatically register itself. 88 | 89 | You can publish the migration with: 90 | ```bash 91 | php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="migrations" 92 | ``` 93 | 94 | *Note*: The default migration assumes you are using integers for your model IDs. If you are using UUIDs, or some other format, adjust the format of the subject_id and causer_id fields in the published migration before continuing. 95 | 96 | After publishing the migration you can create the `activity_log` table by running the migrations: 97 | 98 | 99 | ```bash 100 | php artisan migrate 101 | ``` 102 | 103 | You can optionally publish the config file with: 104 | ```bash 105 | php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="config" 106 | ``` 107 | 108 | This is the contents of the published config file: 109 | 110 | ```php 111 | return [ 112 | 113 | /* 114 | * If set to false, no activities will be saved to the database. 115 | */ 116 | 'enabled' => env('ACTIVITY_LOGGER_ENABLED', true), 117 | 118 | /* 119 | * When the clean-command is executed, all recording activities older than 120 | * the number of days specified here will be deleted. 121 | */ 122 | 'delete_records_older_than_days' => 365, 123 | 124 | /* 125 | * If no log name is passed to the activity() helper 126 | * we use this default log name. 127 | */ 128 | 'default_log_name' => 'default', 129 | 130 | /* 131 | * You can specify an auth driver here that gets user models. 132 | * If this is null we'll use the default Laravel auth driver. 133 | */ 134 | 'default_auth_driver' => null, 135 | 136 | /* 137 | * If set to true, the subject returns soft deleted models. 138 | */ 139 | 'subject_returns_soft_deleted_models' => false, 140 | 141 | /* 142 | * This model will be used to log activity. 143 | * It should be implements the Spatie\Activitylog\Contracts\Activity interface 144 | * and extend Illuminate\Database\Eloquent\Model. 145 | */ 146 | 'activity_model' => \Spatie\Activitylog\Models\Activity::class, 147 | 148 | /* 149 | * This is the name of the table that will be created by the migration and 150 | * used by the Activity model shipped with this package. 151 | */ 152 | 'table_name' => 'activity_log', 153 | ]; 154 | ``` 155 | 156 | ## Changelog 157 | 158 | Please see [CHANGELOG](CHANGELOG.md) for more information about recent changes. 159 | 160 | ## Upgrading 161 | 162 | Please see [UPGRADING](UPGRADING.md) for details. 163 | 164 | 165 | ## Testing 166 | 167 | ``` bash 168 | composer test 169 | ``` 170 | 171 | ## Contributing 172 | 173 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 174 | 175 | ## Security 176 | 177 | If you discover any security related issues, please email freek@spatie.be instead of using the issue tracker. 178 | 179 | ## Postcardware 180 | 181 | You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. 182 | 183 | Our address is: Spatie, Samberstraat 69D, 2060 Antwerp, Belgium. 184 | 185 | We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). 186 | 187 | ## Credits 188 | 189 | - [Freek Van der Herten](https://github.com/freekmurze) 190 | - [Sebastian De Deyne](https://github.com/sebastiandedeyne) 191 | - [All Contributors](../../contributors) 192 | 193 | ## Support us 194 | 195 | Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). 196 | 197 | Does your business depend on our contributions? Reach out and support us on [Patreon](https://www.patreon.com/spatie). 198 | All pledges will be dedicated to allocating workforce on maintenance and new awesome stuff. 199 | 200 | ## License 201 | 202 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 203 | --------------------------------------------------------------------------------