Logging Changes to settings? #37
Replies: 3 comments 3 replies
-
Hi @ragingdave, I've added the events exactly for such things, I think it's the cleanest option to reach them in my opinion. Since at that moment properties are not encrypted or casted, which they will be when stored within an eloquent model. |
Beta Was this translation helpful? Give feedback.
-
I don't know if this is still relevant but I have solved the logging of changes using the event listener. I have created a listener and used this code in the handler:
There might be a better way of doing this, but this works for me. |
Beta Was this translation helpful? Give feedback.
-
spatie/laravel-activitylog does have a trait How I was able to start logging updated events was use Spatie\LaravelSettings\Events\SavingSettings as SpatieSavingSettings;
final class SavingSettings
{
public function __construct(
private ChangeSetBuffer $buffer,
private Redactor $redactor
) {}
public function handle(SpatieSavingSettings $event): void
{
$original = $this->redactor->redact($event->originalValues->toArray());
$updated = $this->redactor->redact($event->properties->toArray());
$this->buffer->put(new ChangeSetData(
group: $event->settings::group(),
class: $event->settings::class,
original: $original,
updated: $updated
));
}
} Secrets that should not be logged are redacted, loaded into a buffer, via a data object. final class Redactor
{
public function redact(array $data): array
{
foreach ($data as $key => $value) {
if (self::mask($key) && $value !== null) {
$value = '***';
}
}
return $data;
}
private static function mask(string $key): bool
{
if (str_ends_with($key, '_secret')) {
return true;
}
if (str_contains($key, 'password')) {
return true;
}
if (str_contains($key, 'token')) {
return true;
}
return false;
}
} final class ChangeSetBuffer
{
private array $bag = [];
/**
* Put into bag
*/
public function put(ChangeSetData $set): void
{
$this->bag[$set->class] = $set;
}
/**
* Take from bad
*/
public function take(string $class): ?ChangeSetData
{
$set = $this->bag[$class] ?? null;
unset($this->bar[$class]);
return $set;
}
} final class ChangeSetData
{
public function __construct(
public string $group,
public string $class,
public array $original,
public array $updated,
) {}
/**
* Keys
*/
public function keys(): array
{
return array_values(array_unique([
...array_keys($this->original),
...array_keys($this->updated),
]));
}
/**
* Has value changed
*/
public function changed(string $key): bool
{
return ($this->original[$key] ?? null) !== ($this->updated[$key] ?? null);
}
/**
* Get original value
*/
public function original(string $key): mixed
{
return $this->original[$key] ?? null;
}
/**
* Get updated value
*/
public function updated(string $key): mixed
{
return $this->updated[$key] ?? null;
}
/**
* Is change empty
*/
public function isEmpty(): bool
{
foreach ($this->keys() as $key) {
if ($this->changed($key)) {
return false;
}
}
return true;
}
} The buffer is passed to the class that taps into use Spatie\LaravelSettings\Events\SettingsSaved as SpatieSettingsSaved;
final class SettingsSaved
{
public function __construct(
private ChangeSetBuffer $buffer,
private Dispatcher $dispatcher
) {}
public function handle(SpatieSettingsSaved $event): void
{
$set = $this->buffer->take($event->settings::class);
if (! $set || $set->isEmpty()) {
return;
}
$context = [
'causer' => request()->user(),
'ip' => request()->ip(),
'user_agent' => request()->userAgent(),
'meta' => []
];
$this->dispatcher->dispatch(new SettingsChanged(
changes: $set,
context: $context
));
}
} final class SettingsChanged
{
public function __construct(
public ChangeSetData $changes,
public array $context = [],
) {}
} An event listener which consumes the change set, context, and queues the log creation using final class LogSettingsChanged implements ShouldQueue
{
use InteractsWithQueue;
public function handle(SettingsChanged $event): void
{
$changes = $event->changes;
$context = $event->context;
$causer = ($context['causer'] ?? null) instanceof User ? $context['causer'] : null;
foreach ($changes->keys() as $key) {
if (! $changes->changed($key)) {
continue;
}
$activity = activity($changes->group);
$activity->on($this->performedOn($changes->group, $key));
$activity->event('updated');
$activity->causedBy($causer);
$activity->withProperties([
'group' => $changes->group,
'key' => $key,
'original' => $changes->original($key),
'updated' => $changes->updated($key),
'ip' => $context['ip'] ?? null,
'user_agent' => $context['user_agent'] ?? null,
'meta' => $context['meta'] ?? null,
]);
$activity->log("Updated setting [{$changes->group}.{$key}]");
}
}
private function performedOn(string $group, string $key)
{
return SettingsProperty::where('group', $group)
->where('name', $key)->first();
}
} Finally wire everything up in final class SettingsServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->scoped(ChangeSetBuffer::class, fn() => new ChangeSetBuffer());
$this->app->singleton(Redactor::class, fn() => new Redactor());
}
public function boot(Dispatcher $events): void
{
$events->listen(SpatieSavingSettings::class, SavingSettings::class);
$events->listen(SpatieSettingsSaved::class, SettingsSaved::class);
$events->listen(SettingsChanged::class, LogSettingsChanged::class);
}
} |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
As I'm starting to integrate this into my app, I wonder how to go about logging changes. I can see a way forward by hooking into the events themselves, but I'm wondering if there's a simple path I'm missing.
Before this became stable-ish/out of beta, I was using a standard eloquent model to access an identical data structure for settings. Going this route, I was able to use the standard spatie/laravel-activitylog package to log those changes. That has it's own issues around nested data, but it works for tracking changes.
Beta Was this translation helpful? Give feedback.
All reactions