🆙 Add cms i using 🆙

This commit is contained in:
Remco
2025-11-25 22:42:56 +01:00
parent 94704e0925
commit d44196149e
35591 changed files with 3601123 additions and 0 deletions
@@ -0,0 +1,30 @@
<?php
namespace Spatie\Activitylog;
use Illuminate\Contracts\Config\Repository;
class ActivityLogStatus
{
protected $enabled = true;
public function __construct(Repository $config)
{
$this->enabled = $config['activitylog.enabled'];
}
public function enable(): bool
{
return $this->enabled = true;
}
public function disable(): bool
{
return $this->enabled = false;
}
public function disabled(): bool
{
return $this->enabled === false;
}
}
@@ -0,0 +1,233 @@
<?php
namespace Spatie\Activitylog;
use Closure;
use DateTimeInterface;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Conditionable;
use Illuminate\Support\Traits\Macroable;
use Spatie\Activitylog\Contracts\Activity as ActivityContract;
class ActivityLogger
{
use Conditionable;
use Macroable;
protected ?string $defaultLogName = null;
protected CauserResolver $causerResolver;
protected ActivityLogStatus $logStatus;
protected ?ActivityContract $activity = null;
protected LogBatch $batch;
public function __construct(Repository $config, ActivityLogStatus $logStatus, LogBatch $batch, CauserResolver $causerResolver)
{
$this->causerResolver = $causerResolver;
$this->batch = $batch;
$this->defaultLogName = $config['activitylog']['default_log_name'];
$this->logStatus = $logStatus;
}
public function setLogStatus(ActivityLogStatus $logStatus): static
{
$this->logStatus = $logStatus;
return $this;
}
public function performedOn(Model $model): static
{
$this->getActivity()->subject()->associate($model);
return $this;
}
public function on(Model $model): static
{
return $this->performedOn($model);
}
public function causedBy(Model | int | string | null $modelOrId): static
{
if ($modelOrId === null) {
return $this;
}
$model = $this->causerResolver->resolve($modelOrId);
$this->getActivity()->causer()->associate($model);
return $this;
}
public function by(Model | int | string | null $modelOrId): static
{
return $this->causedBy($modelOrId);
}
public function causedByAnonymous(): static
{
$this->activity->causer_id = null;
$this->activity->causer_type = null;
return $this;
}
public function byAnonymous(): static
{
return $this->causedByAnonymous();
}
public function event(string $event): static
{
return $this->setEvent($event);
}
public function setEvent(string $event): static
{
$this->activity->event = $event;
return $this;
}
public function withProperties(mixed $properties): static
{
$this->getActivity()->properties = collect($properties);
return $this;
}
public function withProperty(string $key, mixed $value): static
{
$this->getActivity()->properties = $this->getActivity()->properties->put($key, $value);
return $this;
}
public function createdAt(DateTimeInterface $dateTime): static
{
$this->getActivity()->created_at = Carbon::instance($dateTime);
return $this;
}
public function useLog(?string $logName): static
{
$this->getActivity()->log_name = $logName;
return $this;
}
public function inLog(?string $logName): static
{
return $this->useLog($logName);
}
public function tap(callable $callback, ?string $eventName = null): static
{
call_user_func($callback, $this->getActivity(), $eventName);
return $this;
}
public function enableLogging(): static
{
$this->logStatus->enable();
return $this;
}
public function disableLogging(): static
{
$this->logStatus->disable();
return $this;
}
public function log(string $description): ?ActivityContract
{
if ($this->logStatus->disabled()) {
return null;
}
$activity = $this->activity;
$activity->description = $this->replacePlaceholders(
$activity->description ?? $description,
$activity
);
if (isset($activity->subject) && method_exists($activity->subject, 'tapActivity')) {
$this->tap([$activity->subject, 'tapActivity'], $activity->event ?? '');
}
$activity->save();
$this->activity = null;
return $activity;
}
public function withoutLogs(Closure $callback): mixed
{
if ($this->logStatus->disabled()) {
return $callback();
}
$this->logStatus->disable();
try {
return $callback();
} finally {
$this->logStatus->enable();
}
}
protected function replacePlaceholders(string $description, ActivityContract $activity): string
{
return preg_replace_callback('/:[a-z0-9._-]+(?<![.])/i', function ($match) use ($activity) {
$match = $match[0];
$attribute = Str::before(Str::after($match, ':'), '.');
if (! in_array($attribute, ['subject', 'causer', 'properties'])) {
return $match;
}
$propertyName = substr($match, strpos($match, '.') + 1);
$attributeValue = $activity->$attribute;
if (is_null($attributeValue)) {
return $match;
}
return data_get($attributeValue, $propertyName, $match);
}, $description);
}
protected function getActivity(): ActivityContract
{
if (! $this->activity instanceof ActivityContract) {
$this->activity = ActivitylogServiceProvider::getActivityModelInstance();
$this
->useLog($this->defaultLogName)
->withProperties([])
->causedBy($this->causerResolver->resolve());
$this->activity->batch_uuid = $this->batch->getUuid();
}
return $this->activity;
}
}
@@ -0,0 +1,57 @@
<?php
namespace Spatie\Activitylog;
use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\Contracts\Activity;
use Spatie\Activitylog\Contracts\Activity as ActivityContract;
use Spatie\Activitylog\Exceptions\InvalidConfiguration;
use Spatie\Activitylog\Models\Activity as ActivityModel;
use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider;
class ActivitylogServiceProvider extends PackageServiceProvider
{
public function configurePackage(Package $package): void
{
$package
->name('laravel-activitylog')
->hasConfigFile('activitylog')
->hasMigrations([
'create_activity_log_table',
'add_event_column_to_activity_log_table',
'add_batch_uuid_column_to_activity_log_table',
])
->hasCommand(CleanActivitylogCommand::class);
}
public function registeringPackage()
{
$this->app->bind(ActivityLogger::class);
$this->app->scoped(LogBatch::class);
$this->app->scoped(CauserResolver::class);
$this->app->scoped(ActivityLogStatus::class);
}
public static function determineActivityModel(): string
{
$activityModel = config('activitylog.activity_model') ?? ActivityModel::class;
if (! is_a($activityModel, Activity::class, true)
|| ! is_a($activityModel, Model::class, true)) {
throw InvalidConfiguration::modelIsNotValid($activityModel);
}
return $activityModel;
}
public static function getActivityModelInstance(): ActivityContract
{
$activityModelClassName = self::determineActivityModel();
return new $activityModelClassName();
}
}
@@ -0,0 +1,101 @@
<?php
namespace Spatie\Activitylog;
use Closure;
use Illuminate\Auth\AuthManager;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\Exceptions\CouldNotLogActivity;
class CauserResolver
{
protected AuthManager $authManager;
protected string | null $authDriver;
protected Closure | null $resolverOverride = null;
protected Model | null $causerOverride = null;
public function __construct(Repository $config, AuthManager $authManager)
{
$this->authManager = $authManager;
$this->authDriver = $config['activitylog']['default_auth_driver'];
}
public function resolve(Model | int | string | null $subject = null): ?Model
{
if ($this->causerOverride !== null) {
return $this->causerOverride;
}
if ($this->resolverOverride !== null) {
$resultCauser = ($this->resolverOverride)($subject);
if (! $this->isResolvable($resultCauser)) {
throw CouldNotLogActivity::couldNotDetermineUser($resultCauser);
}
return $resultCauser;
}
return $this->getCauser($subject);
}
protected function resolveUsingId(int | string $subject): Model
{
$guard = $this->authManager->guard($this->authDriver);
$provider = method_exists($guard, 'getProvider') ? $guard->getProvider() : null;
$model = method_exists($provider, 'retrieveById') ? $provider->retrieveById($subject) : null;
throw_unless($model instanceof Model, CouldNotLogActivity::couldNotDetermineUser($subject));
return $model;
}
protected function getCauser(Model | int | string | null $subject = null): ?Model
{
if ($subject instanceof Model) {
return $subject;
}
if (is_null($subject)) {
return $this->getDefaultCauser();
}
return $this->resolveUsingId($subject);
}
/**
* Override the resover using callback.
*/
public function resolveUsing(Closure $callback): static
{
$this->resolverOverride = $callback;
return $this;
}
/**
* Override default causer.
*/
public function setCauser(?Model $causer): static
{
$this->causerOverride = $causer;
return $this;
}
protected function isResolvable(mixed $model): bool
{
return $model instanceof Model || is_null($model);
}
protected function getDefaultCauser(): ?Model
{
return $this->authManager->guard($this->authDriver)->user();
}
}
@@ -0,0 +1,48 @@
<?php
namespace Spatie\Activitylog;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Database\Eloquent\Builder;
class CleanActivitylogCommand extends Command
{
use ConfirmableTrait;
protected $signature = 'activitylog:clean
{log? : (optional) The log name that will be cleaned.}
{--days= : (optional) Records older than this number of days will be cleaned.}
{--force : (optional) Force the operation to run when in production.}';
protected $description = 'Clean up old records from the activity log.';
public function handle()
{
if (! $this->confirmToProceed()) {
return 1;
}
$this->comment('Cleaning activity log...');
$log = $this->argument('log');
$maxAgeInDays = $this->option('days') ?? config('activitylog.delete_records_older_than_days');
$cutOffDate = Carbon::now()->subDays($maxAgeInDays)->format('Y-m-d H:i:s');
$activity = ActivitylogServiceProvider::getActivityModelInstance();
$amountDeleted = $activity::query()
->where('created_at', '<', $cutOffDate)
->when($log !== null, function (Builder $query) use ($log) {
$query->inLog($log);
})
->delete();
$this->info("Deleted {$amountDeleted} record(s) from the activity log.");
$this->comment('All done!');
}
}
@@ -0,0 +1,27 @@
<?php
namespace Spatie\Activitylog\Contracts;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Support\Collection;
interface Activity
{
public function subject(): MorphTo;
public function causer(): MorphTo;
public function getExtraProperty(string $propertyName, mixed $defaultValue): mixed;
public function changes(): Collection;
public function scopeInLog(Builder $query, ...$logNames): Builder;
public function scopeCausedBy(Builder $query, Model $causer): Builder;
public function scopeForEvent(Builder $query, string $event): Builder;
public function scopeForSubject(Builder $query, Model $subject): Builder;
}
@@ -0,0 +1,11 @@
<?php
namespace Spatie\Activitylog\Contracts;
use Closure;
use Spatie\Activitylog\EventLogBag;
interface LoggablePipe
{
public function handle(EventLogBag $event, Closure $next): EventLogBag;
}
@@ -0,0 +1,17 @@
<?php
namespace Spatie\Activitylog;
use Illuminate\Database\Eloquent\Model;
class EventLogBag
{
public function __construct(
public string $event,
public Model $model,
public array $changes,
public ?LogOptions $options = null
) {
$this->options ??= $model->getActivitylogOptions();
}
}
@@ -0,0 +1,13 @@
<?php
namespace Spatie\Activitylog\Exceptions;
use Exception;
class CouldNotLogActivity extends Exception
{
public static function couldNotDetermineUser($id): self
{
return new static("Could not determine a user with identifier `{$id}`.");
}
}
@@ -0,0 +1,13 @@
<?php
namespace Spatie\Activitylog\Exceptions;
use Exception;
class CouldNotLogChanges extends Exception
{
public static function invalidAttribute($attribute): self
{
return new static("Cannot log attribute `{$attribute}`. Can only log attributes of a model or a directly related model.");
}
}
@@ -0,0 +1,15 @@
<?php
namespace Spatie\Activitylog\Exceptions;
use Exception;
use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\Contracts\Activity;
class InvalidConfiguration extends Exception
{
public static function modelIsNotValid(string $className): self
{
return new static("The given model class `{$className}` does not implement `".Activity::class.'` or it does not extend `'.Model::class.'`');
}
}
@@ -0,0 +1,43 @@
<?php
namespace Spatie\Activitylog\Facades;
use Illuminate\Support\Facades\Facade;
use Spatie\Activitylog\PendingActivityLog;
/**
* @method static \Spatie\Activitylog\ActivityLogger setLogStatus(\Spatie\Activitylog\ActivityLogStatus $logStatus)
* @method static \Spatie\Activitylog\ActivityLogger performedOn(\Illuminate\Database\Eloquent\Model $model)
* @method static \Spatie\Activitylog\ActivityLogger on(\Illuminate\Database\Eloquent\Model $model)
* @method static \Spatie\Activitylog\ActivityLogger causedBy(\Illuminate\Database\Eloquent\Model|string|int|null $modelOrId)
* @method static \Spatie\Activitylog\ActivityLogger by(\Illuminate\Database\Eloquent\Model|string|int|null $modelOrId)
* @method static \Spatie\Activitylog\ActivityLogger causedByAnonymous()
* @method static \Spatie\Activitylog\ActivityLogger byAnonymous()
* @method static \Spatie\Activitylog\ActivityLogger event(string $event)
* @method static \Spatie\Activitylog\ActivityLogger setEvent(string $event)
* @method static \Spatie\Activitylog\ActivityLogger withProperties(mixed $properties)
* @method static \Spatie\Activitylog\ActivityLogger withProperty(string $key, mixed $value)
* @method static \Spatie\Activitylog\ActivityLogger createdAt(\DateTimeInterface $dateTime)
* @method static \Spatie\Activitylog\ActivityLogger useLog(string|null $logName)
* @method static \Spatie\Activitylog\ActivityLogger inLog(string|null $logName)
* @method static \Spatie\Activitylog\ActivityLogger tap(callable $callback, string|null $eventName = null)
* @method static \Spatie\Activitylog\ActivityLogger enableLogging()
* @method static \Spatie\Activitylog\ActivityLogger disableLogging()
* @method static \Spatie\Activitylog\Contracts\Activity|null log(string $description)
* @method static mixed withoutLogs(\Closure $callback)
* @method static \Spatie\Activitylog\ActivityLogger|mixed when(\Closure|mixed|null $value = null, callable|null $callback = null, callable|null $default = null)
* @method static \Spatie\Activitylog\ActivityLogger|mixed unless(\Closure|mixed|null $value = null, callable|null $callback = null, callable|null $default = null)
* @method static void macro(string $name, object|callable $macro)
* @method static void mixin(object $mixin, bool $replace = true)
* @method static bool hasMacro(string $name)
* @method static void flushMacros()
*
* @see \Spatie\Activitylog\PendingActivityLog
*/
class Activity extends Facade
{
protected static function getFacadeAccessor(): string
{
return PendingActivityLog::class;
}
}
@@ -0,0 +1,21 @@
<?php
namespace Spatie\Activitylog\Facades;
use Illuminate\Support\Facades\Facade;
use Spatie\Activitylog\CauserResolver as ActivitylogCauserResolver;
/**
* @method static \Illuminate\Database\Eloquent\Model|null resolve(\Illuminate\Database\Eloquent\Model|int|string|null $subject = null)
* @method static \Spatie\Activitylog\CauserResolver resolveUsing(\Closure $callback)
* @method static \Spatie\Activitylog\CauserResolver setCauser(\Illuminate\Database\Eloquent\Model|null $causer)
*
* @see \Spatie\Activitylog\CauserResolver
*/
class CauserResolver extends Facade
{
protected static function getFacadeAccessor(): string
{
return ActivitylogCauserResolver::class;
}
}
@@ -0,0 +1,24 @@
<?php
namespace Spatie\Activitylog\Facades;
use Illuminate\Support\Facades\Facade;
use Spatie\Activitylog\LogBatch as ActivityLogBatch;
/**
* @method static string getUuid()
* @method static mixed withinBatch(\Closure $callback)
* @method static void startBatch()
* @method static void setBatch(string $uuid): void
* @method static bool isOpen()
* @method static void endBatch()
*
* @see \Spatie\Activitylog\LogBatch
*/
class LogBatch extends Facade
{
protected static function getFacadeAccessor(): string
{
return ActivityLogBatch::class;
}
}
@@ -0,0 +1,61 @@
<?php
namespace Spatie\Activitylog;
use Closure;
use Ramsey\Uuid\Uuid;
class LogBatch
{
public ?string $uuid = null;
public int $transactions = 0;
protected function generateUuid(): string
{
return Uuid::uuid4()->toString();
}
public function getUuid(): ?string
{
return $this->uuid;
}
public function setBatch(string $uuid): void
{
$this->uuid = $uuid;
$this->transactions = 1;
}
public function withinBatch(Closure $callback): mixed
{
$this->startBatch();
$result = $callback($this->getUuid());
$this->endBatch();
return $result;
}
public function startBatch(): void
{
if (! $this->isOpen()) {
$this->uuid = $this->generateUuid();
}
$this->transactions++;
}
public function isOpen(): bool
{
return $this->transactions > 0;
}
public function endBatch(): void
{
$this->transactions = max(0, $this->transactions - 1);
if ($this->transactions === 0) {
$this->uuid = null;
}
}
}
@@ -0,0 +1,166 @@
<?php
namespace Spatie\Activitylog;
use Closure;
class LogOptions
{
public ?string $logName = null;
public bool $submitEmptyLogs = true;
public bool $logFillable = false;
public bool $logOnlyDirty = false;
public bool $logUnguarded = false;
public array $logAttributes = [];
public array $logExceptAttributes = [];
public array $dontLogIfAttributesChangedOnly = [];
public array $attributeRawValues = [];
public ?Closure $descriptionForEvent = null;
/**
* Start configuring model with the default options.
*/
public static function defaults(): self
{
return new static();
}
/**
* Log all attributes on the model.
*/
public function logAll(): self
{
return $this->logOnly(['*']);
}
/**
* Log all attributes that are not listed in $guarded.
*/
public function logUnguarded(): self
{
$this->logUnguarded = true;
return $this;
}
/**
* log changes to all the $fillable attributes of the model.
*/
public function logFillable(): self
{
$this->logFillable = true;
return $this;
}
/**
* Stop logging $fillable attributes of the model.
*/
public function dontLogFillable(): self
{
$this->logFillable = false;
return $this;
}
/**
* Log changes that has actually changed after the update.
*/
public function logOnlyDirty(): self
{
$this->logOnlyDirty = true;
return $this;
}
/**
* Log changes only if these attributes changed.
*/
public function logOnly(array $attributes): self
{
$this->logAttributes = $attributes;
return $this;
}
/**
* Exclude these attributes from being logged.
*/
public function logExcept(array $attributes): self
{
$this->logExceptAttributes = $attributes;
return $this;
}
/**
* Don't trigger an activity if these attributes changed logged.
*/
public function dontLogIfAttributesChangedOnly(array $attributes): self
{
$this->dontLogIfAttributesChangedOnly = $attributes;
return $this;
}
/**
* Don't store empty logs. Storing empty logs can happen when you only
* want to log a certain attribute but only another changes.
*/
public function dontSubmitEmptyLogs(): self
{
$this->submitEmptyLogs = false;
return $this;
}
/**
* Allow storing empty logs. Storing empty logs can happen when you only
* want to log a certain attribute but only another changes.
*/
public function submitEmptyLogs(): self
{
$this->submitEmptyLogs = true;
return $this;
}
/**
* Customize log name.
*/
public function useLogName(?string $logName): self
{
$this->logName = $logName;
return $this;
}
/**
* Customize log description using callback.
*/
public function setDescriptionForEvent(Closure $callback): self
{
$this->descriptionForEvent = $callback;
return $this;
}
/**
* Exclude these attributes from being casted.
*/
public function useAttributeRawValues(array $attributes): self
{
$this->attributeRawValues = $attributes;
return $this;
}
}
@@ -0,0 +1,138 @@
<?php
namespace Spatie\Activitylog\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Spatie\Activitylog\Contracts\Activity as ActivityContract;
/**
* Spatie\Activitylog\Models\Activity.
*
* @property int $id
* @property string|null $log_name
* @property string $description
* @property string|null $subject_type
* @property int|null $subject_id
* @property string|null $causer_type
* @property int|null $causer_id
* @property string|null $event
* @property string|null $batch_uuid
* @property \Illuminate\Support\Collection|null $properties
* @property \Carbon\Carbon|null $created_at
* @property \Carbon\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Model|\Eloquent|null $causer
* @property-read \Illuminate\Support\Collection $changes
* @property-read \Illuminate\Database\Eloquent\Model|\Eloquent|null $subject
*
* @method static \Illuminate\Database\Eloquent\Builder|\Spatie\Activitylog\Models\Activity causedBy(\Illuminate\Database\Eloquent\Model $causer)
* @method static \Illuminate\Database\Eloquent\Builder|\Spatie\Activitylog\Models\Activity forBatch(string $batchUuid)
* @method static \Illuminate\Database\Eloquent\Builder|\Spatie\Activitylog\Models\Activity forEvent(string $event)
* @method static \Illuminate\Database\Eloquent\Builder|\Spatie\Activitylog\Models\Activity forSubject(\Illuminate\Database\Eloquent\Model $subject)
* @method static \Illuminate\Database\Eloquent\Builder|\Spatie\Activitylog\Models\Activity hasBatch()
* @method static \Illuminate\Database\Eloquent\Builder|\Spatie\Activitylog\Models\Activity inLog($logNames)
* @method static \Illuminate\Database\Eloquent\Builder|\Spatie\Activitylog\Models\Activity newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\Spatie\Activitylog\Models\Activity newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\Spatie\Activitylog\Models\Activity query()
*/
class Activity extends Model implements ActivityContract
{
public $guarded = [];
protected $casts = [
'properties' => 'collection',
];
public function __construct(array $attributes = [])
{
if (! isset($this->connection)) {
$this->setConnection(config('activitylog.database_connection'));
}
if (! isset($this->table)) {
$this->setTable(config('activitylog.table_name'));
}
parent::__construct($attributes);
}
/**
* @return MorphTo<Model, $this>
*/
public function subject(): MorphTo
{
if (config('activitylog.subject_returns_soft_deleted_models')) {
return $this->morphTo()->withTrashed();
}
return $this->morphTo();
}
/**
* @return MorphTo<Model, $this>
*/
public function causer(): MorphTo
{
return $this->morphTo();
}
public function getExtraProperty(string $propertyName, mixed $defaultValue = null): mixed
{
return Arr::get($this->properties->toArray(), $propertyName, $defaultValue);
}
public function changes(): Collection
{
if (! $this->properties instanceof Collection) {
return new Collection();
}
return $this->properties->only(['attributes', 'old']);
}
public function getChangesAttribute(): Collection
{
return $this->changes();
}
public function scopeInLog(Builder $query, ...$logNames): Builder
{
if (is_array($logNames[0])) {
$logNames = $logNames[0];
}
return $query->whereIn('log_name', $logNames);
}
public function scopeCausedBy(Builder $query, Model $causer): Builder
{
return $query
->where('causer_type', $causer->getMorphClass())
->where('causer_id', $causer->getKey());
}
public function scopeForSubject(Builder $query, Model $subject): Builder
{
return $query
->where('subject_type', $subject->getMorphClass())
->where('subject_id', $subject->getKey());
}
public function scopeForEvent(Builder $query, string $event): Builder
{
return $query->where('event', $event);
}
public function scopeHasBatch(Builder $query): Builder
{
return $query->whereNotNull('batch_uuid');
}
public function scopeForBatch(Builder $query, string $batchUuid): Builder
{
return $query->where('batch_uuid', $batchUuid);
}
}
@@ -0,0 +1,32 @@
<?php
namespace Spatie\Activitylog;
use Illuminate\Support\Traits\ForwardsCalls;
/**
* @mixin \Spatie\Activitylog\ActivityLogger
*/
class PendingActivityLog
{
use ForwardsCalls;
protected ActivityLogger $logger;
public function __construct(ActivityLogger $logger, ActivityLogStatus $status)
{
$this->logger = $logger
->setLogStatus($status)
->useLog(config('activitylog.default_log_name'));
}
public function logger(): ActivityLogger
{
return $this->logger;
}
public function __call(string $method, array $parameters): mixed
{
return $this->forwardCallTo($this->logger, $method, $parameters);
}
}
@@ -0,0 +1,19 @@
<?php
namespace Spatie\Activitylog\Traits;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Spatie\Activitylog\ActivitylogServiceProvider;
use Spatie\Activitylog\Models\Activity;
trait CausesActivity
{
/** @return MorphMany<Activity, $this> */
public function actions(): MorphMany
{
return $this->morphMany(
ActivitylogServiceProvider::determineActivityModel(),
'causer'
);
}
}
@@ -0,0 +1,426 @@
<?php
namespace Spatie\Activitylog\Traits;
use Carbon\CarbonInterval;
use DateInterval;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Pipeline\Pipeline;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Spatie\Activitylog\ActivityLogger;
use Spatie\Activitylog\ActivitylogServiceProvider;
use Spatie\Activitylog\ActivityLogStatus;
use Spatie\Activitylog\Contracts\LoggablePipe;
use Spatie\Activitylog\EventLogBag;
use Spatie\Activitylog\LogOptions;
trait LogsActivity
{
public static array $changesPipes = [];
protected array $oldAttributes = [];
protected ?LogOptions $activitylogOptions;
public bool $enableLoggingModelsEvents = true;
abstract public function getActivitylogOptions(): LogOptions;
protected static function bootLogsActivity(): void
{
// Hook into eloquent events that only specified in $eventToBeRecorded array,
// checking for "updated" event hook explicitly to temporary hold original
// attributes on the model as we'll need them later to compare against.
static::eventsToBeRecorded()->each(function ($eventName) {
if ($eventName === 'updated') {
static::updating(function (Model $model) {
$oldValues = (new static())->setRawAttributes($model->getRawOriginal());
$model->oldAttributes = static::logChanges($oldValues);
});
}
static::$eventName(function (Model $model) use ($eventName) {
$model->activitylogOptions = $model->getActivitylogOptions();
if (! $model->shouldLogEvent($eventName)) {
return;
}
$changes = $model->attributeValuesToBeLogged($eventName);
$description = $model->getDescriptionForEvent($eventName);
$logName = $model->getLogNameToUse();
// Submitting empty description will cause place holder replacer to fail.
if ($description == '') {
return;
}
if ($model->isLogEmpty($changes) && ! $model->activitylogOptions->submitEmptyLogs) {
return;
}
// User can define a custom pipelines to mutate, add or remove from changes
// each pipe receives the event carrier bag with changes and the model in
// question every pipe should manipulate new and old attributes.
$event = app(Pipeline::class)
->send(new EventLogBag($eventName, $model, $changes, $model->activitylogOptions))
->through(static::$changesPipes)
->thenReturn();
// Actual logging
$logger = app(ActivityLogger::class)
->useLog($logName)
->event($eventName)
->performedOn($model)
->withProperties($event->changes);
if (method_exists($model, 'tapActivity')) {
$logger->tap([$model, 'tapActivity'], $eventName);
}
$logger->log($description);
// Reset log options so the model can be serialized.
$model->activitylogOptions = null;
});
});
}
public static function addLogChange(LoggablePipe $pipe): void
{
static::$changesPipes[] = $pipe;
}
public function isLogEmpty(array $changes): bool
{
return empty($changes['attributes'] ?? []) && empty($changes['old'] ?? []);
}
public function disableLogging(): self
{
$this->enableLoggingModelsEvents = false;
return $this;
}
public function enableLogging(): self
{
$this->enableLoggingModelsEvents = true;
return $this;
}
public function activities(): MorphMany
{
return $this->morphMany(ActivitylogServiceProvider::determineActivityModel(), 'subject');
}
public function getDescriptionForEvent(string $eventName): string
{
if (! empty($this->activitylogOptions->descriptionForEvent)) {
return ($this->activitylogOptions->descriptionForEvent)($eventName);
}
return $eventName;
}
public function getLogNameToUse(): ?string
{
if (! empty($this->activitylogOptions->logName)) {
return $this->activitylogOptions->logName;
}
return config('activitylog.default_log_name');
}
/**
* Get the event names that should be recorded.
**/
protected static function eventsToBeRecorded(): Collection
{
if (isset(static::$recordEvents)) {
return collect(static::$recordEvents);
}
$events = collect([
'created',
'updated',
'deleted',
]);
if (collect(class_uses_recursive(static::class))->contains(SoftDeletes::class)) {
$events->push('restored');
}
return $events;
}
protected function shouldLogEvent(string $eventName): bool
{
$logStatus = app(ActivityLogStatus::class);
if (! $this->enableLoggingModelsEvents || $logStatus->disabled()) {
return false;
}
if (! in_array($eventName, ['created', 'updated'])) {
return true;
}
// Do not log update event if the model is restoring
if ($this->isRestoring()) {
return false;
}
// Do not log update event if only ignored attributes are changed.
return (bool) count(Arr::except($this->getDirty(), $this->activitylogOptions->dontLogIfAttributesChangedOnly));
}
/**
* Determines if the model is restoring.
**/
protected function isRestoring(): bool
{
$deletedAtColumn = method_exists($this, 'getDeletedAtColumn')
? $this->getDeletedAtColumn()
: 'deleted_at';
return $this->isDirty($deletedAtColumn) && count($this->getDirty()) === 1;
}
/**
* Determines what attributes needs to be logged based on the configuration.
**/
public function attributesToBeLogged(): array
{
$this->activitylogOptions = $this->getActivitylogOptions();
$attributes = [];
// Check if fillable attributes will be logged then merge it to the local attributes array.
if ($this->activitylogOptions->logFillable) {
$attributes = array_merge($attributes, $this->getFillable());
}
// Determine if unguarded attributes will be logged.
if ($this->shouldLogUnguarded()) {
// Get only attribute names, not intrested in the values here then guarded
// attributes. get only keys than not present in guarded array, because
// we are logging the unguarded attributes and we cant have both!
$attributes = array_merge($attributes, array_diff(array_keys($this->getAttributes()), $this->getGuarded()));
}
if (! empty($this->activitylogOptions->logAttributes)) {
// Filter * from the logAttributes because will deal with it separately
$attributes = array_merge($attributes, array_diff($this->activitylogOptions->logAttributes, ['*']));
// If there's * get all attributes then merge it, dont respect $guarded or $fillable.
if (in_array('*', $this->activitylogOptions->logAttributes)) {
$attributes = array_merge($attributes, array_keys($this->getAttributes()));
}
}
if ($this->activitylogOptions->logExceptAttributes) {
// Filter out the attributes defined in ignoredAttributes out of the local array
$attributes = array_diff($attributes, $this->activitylogOptions->logExceptAttributes);
}
return $attributes;
}
public function shouldLogUnguarded(): bool
{
if (! $this->activitylogOptions->logUnguarded) {
return false;
}
// This case means all of the attributes are guarded
// so we'll not have any unguarded anyway.
if (in_array('*', $this->getGuarded())) {
return false;
}
return true;
}
/**
* Determines values that will be logged based on the difference.
**/
public function attributeValuesToBeLogged(string $processingEvent): array
{
// no loggable attributes, no values to be logged!
if (! count($this->attributesToBeLogged())) {
return [];
}
$properties['attributes'] = static::logChanges(
// if the current event is retrieved, get the model itself
// else get the fresh default properties from database
// as wouldn't be part of the saved model instance.
$processingEvent == 'retrieved'
? $this
: (
$this->exists
? $this->fresh() ?? $this
: $this
)
);
if (static::eventsToBeRecorded()->contains('updated') && $processingEvent == 'updated') {
// Fill the attributes with null values.
$nullProperties = array_fill_keys(array_keys($properties['attributes']), null);
// Populate the old key with keys from database and from old attributes.
$properties['old'] = array_merge($nullProperties, $this->oldAttributes);
// Fail safe.
$this->oldAttributes = [];
}
if ($this->activitylogOptions->logOnlyDirty && isset($properties['old'])) {
// Get difference between the old and new attributes.
$properties['attributes'] = array_udiff_assoc(
$properties['attributes'],
$properties['old'],
function ($new, $old) {
// Strict check for php's weird behaviors
if ($old === null || $new === null) {
return $new === $old ? 0 : 1;
}
// Handles Date interval comparisons since php cannot use spaceship
// Operator to compare them and will throw ErrorException.
if ($old instanceof DateInterval) {
return CarbonInterval::make($old)->equalTo($new) ? 0 : 1;
} elseif ($new instanceof DateInterval) {
return CarbonInterval::make($new)->equalTo($old) ? 0 : 1;
}
return $new <=> $old;
}
);
$properties['old'] = collect($properties['old'])
->only(array_keys($properties['attributes']))
->all();
}
if (static::eventsToBeRecorded()->contains('deleted') && $processingEvent == 'deleted') {
$properties['old'] = $properties['attributes'];
unset($properties['attributes']);
}
return $properties;
}
public static function logChanges(Model $model): array
{
$changes = [];
$attributes = $model->attributesToBeLogged();
foreach ($attributes as $attribute) {
if (Str::contains($attribute, '.')) {
$changes += self::getRelatedModelAttributeValue($model, $attribute);
continue;
}
if (Str::contains($attribute, '->')) {
Arr::set(
$changes,
str_replace('->', '.', $attribute),
static::getModelAttributeJsonValue($model, $attribute)
);
continue;
}
$changes[$attribute] = in_array($attribute, $model->activitylogOptions->attributeRawValues)
? $model->getAttributeFromArray($attribute)
: $model->getAttribute($attribute);
if (is_null($changes[$attribute])) {
continue;
}
if ($model->isDateAttribute($attribute)) {
$changes[$attribute] = $model->serializeDate(
$model->asDateTime($changes[$attribute])
);
}
if ($model->hasCast($attribute)) {
$cast = $model->getCasts()[$attribute];
if ($model->isEnumCastable($attribute)) {
try {
$changes[$attribute] = $model->getStorableEnumValue($changes[$attribute]);
} catch (\ArgumentCountError $e) {
// In Laravel 11, this method has an extra argument
// https://github.com/laravel/framework/pull/47465
$changes[$attribute] = $model->getStorableEnumValue($cast, $changes[$attribute]);
}
}
if ($model->isCustomDateTimeCast($cast) || $model->isImmutableCustomDateTimeCast($cast)) {
$changes[$attribute] = $model->asDateTime($changes[$attribute])->format(explode(':', $cast, 2)[1]);
}
}
}
return $changes;
}
protected static function getRelatedModelAttributeValue(Model $model, string $attribute): array
{
$relatedModelNames = explode('.', $attribute);
$relatedAttribute = array_pop($relatedModelNames);
$attributeName = [];
$relatedModel = $model;
do {
$attributeName[] = $relatedModelName = static::getRelatedModelRelationName($relatedModel, array_shift($relatedModelNames));
$relatedModel = $relatedModel->$relatedModelName ?? $relatedModel->$relatedModelName();
} while (! empty($relatedModelNames));
$attributeName[] = $relatedAttribute;
return [implode('.', $attributeName) => $relatedModel->$relatedAttribute ?? null];
}
protected static function getRelatedModelRelationName(Model $model, string $relation): string
{
return Arr::first([
$relation,
Str::snake($relation),
Str::camel($relation),
], function (string $method) use ($model): bool {
return method_exists($model, $method);
}, $relation);
}
protected static function getModelAttributeJsonValue(Model $model, string $attribute): mixed
{
$path = explode('->', $attribute);
$modelAttribute = array_shift($path);
$modelAttribute = collect($model->getAttribute($modelAttribute));
return data_get($modelAttribute, implode('.', $path));
}
}
@@ -0,0 +1,18 @@
<?php
use Spatie\Activitylog\ActivityLogger;
use Spatie\Activitylog\PendingActivityLog;
if (! function_exists('activity')) {
function activity(?string $logName = null): ActivityLogger
{
/** @var PendingActivityLog $log */
$log = app(PendingActivityLog::class);
if ($logName) {
$log->useLog($logName);
}
return $log->logger();
}
}