spatie / laravel-activitylog

Log activity inside your Laravel app
https://docs.spatie.be/laravel-activitylog
MIT License
5.31k stars 712 forks source link

How to log activity when changing relationships? #679

Closed marbuser closed 4 years ago

marbuser commented 4 years ago

I recently found this package and want to use it in collaboration with another package you manage called Laravel-MediaLibrary (https://github.com/spatie/laravel-medialibrary).

Basically when a created or updated event is fired, I'd like to push an attribute so that I can show the user the image was updated as well as the other attributes. Currently it only logs direct attributes on the Model and using the dot notation I've not had any luck, so I'm not sure if I'm doing something wrong or this simply isn't possible.

Any help is appreciated.

Gummibeer commented 4 years ago

Hey, it's a one to many relationship - so you can only log it from the media side. The dot notation doesn't allow a pluck() like behavior. (Could be an idea for a future release) So from the media model you could log model.name for example.

There are two ways for your case: Add the trait to the media model - you will have to extend it and register your own one in the config of medialibrary. This will create two activities - one for the model with the changed attributes and one for the media model.

The other way would be a custom activity. Because the change detection is like the major code part of this package I can't recommend this!

There are open issues that could help you in this case. #560 for example would allow you to have these two activities in a batch which would make it easier for you to display/merge them together.

marbuser commented 4 years ago

@Gummibeer Thanks for the quick reply. In regards to your first suggestion, I've added the trait to my custom media model, and I do see 2 created events. But I'm not sure how to "link" them if that makes sense?

Since ideally I'd like to be able to do something like; $model->activities and have that include the attributes getting updated as well as show the media update so I can also have some type of message that states; "User updated; column1, column2, and image."

I read the issue you linked but I'm not really sure I understand. Hopefully you can elaborate. Thanks!

Gummibeer commented 4 years ago

The idea of the issue is a new column batch_number (tbd) that indicates which activities belong to the same batch. Useful if you update a product which typically consists of multiple models (prices, variants ...).

The easiest solution for you would be to use the tapActivity() method on the media model and set a custom property like model or subject or whatever you want to name it and is filled with the two properties that are needed for morph. It could get even easier if you add two columns which should be nullable to be able to use a real morph relationship. You could name it parent_subject or something like that. https://laravel.com/docs/5.8/eloquent-relationships#polymorphic-relationships This way you could use $model->activities->merge($model->child_activities).

I hope this helps you to get it working!?

marbuser commented 4 years ago

@Gummibeer Thanks again for the reply. Is it possible you could give a small code example? I think I sort of understand what you mean, but I'm still a little confused on how to execute some of these solutions you've put forward.

Thanks!

Gummibeer commented 4 years ago

migration

$table->unsignedBigInteger('parent_subject_id')->nullable();
$table->string('parent_subject_type')->nullable();

media model

public function parent_subject(): MorphTo
{
    if (config('activitylog.subject_returns_soft_deleted_models')) {
        return $this->morphTo()->withTrashed();
    }

    return $this->morphTo();
}

public function tapActivity(Activity $activity, string $eventName)
{
    if ($activity->subject instanceof Media) {
        $activity->parent_subject()->save($activity->subject->model);
    }
}

https://docs.spatie.be/laravel-activitylog/v3/advanced-usage/logging-model-events/#tap-activity-before-logged-from-event

main model

public function child_activities(): MorphMany
{
    return $this->morphMany(ActivitylogServiceProvider::determineActivityModel(), 'parent_subject');
}

I hope that these snippets help you to understand my idea?

marbuser commented 4 years ago

@Gummibeer Thanks for this, your code snippets have helped me understand better.

However 1 small confusion, whenyou refer to "main model" at the end with child_activities, is that the Activity model, or the model that is actually logging the events e.g NewsArticle?

Thanks!

EDIT: I've tried implementing this, and it seems this line doesn't "work"? $activity->parent_subject()->save($activity->subject->model);

It executes but parent_subject_id and parent_subject_type are left NULL? Tried to debug it but can't fix it and not sure what's wrong with it.

marbuser commented 4 years ago

@Gummibeer I did actually get this working. Turns out it's associate($model) instead of save($model).

However another issue has come up and after 10+ hours of debugging I'm not really sure what the cause is.

In MediaLibrary, I'm using registerMediaConversions() to generate thumbnails for certain images. Since this updates the model, it triggers the updated event on the Media model. However, the attribute that is changing I'm not even logging on $logAttributes, and I also have $logOnlyDirty enabled.

So I figured I'd use $recordEvents and exclude updated because for media I don't really care about the updated event. But even with all that, it still keeps creating an empty updated event in my database everytime the thumbnail is generated. I tried logging inside tapActivity() and at no point does a updated event name ever get put through there.

So that's pretty strange, but now here's the weirder part. I completely removed everything from the Media model to do with activity logging. The trait, the properties, everything. The original event for created was no longer there, but the randomly empty updated one still got created when the thumbnail got generated.

So now I'm just beyond confused and I have absolutely no idea what is causing this.

Gummibeer commented 4 years ago

Are you sure that the updated is generated for the media model? Could it be that the main model is touched and the updated event is triggered by the main model?

https://docs.spatie.be/laravel-activitylog/v3/advanced-usage/logging-model-events/#prevent-save-logs-items-that-have-no-changed-attribute This will also prevent empty logs at all - no changes, no log.

github-actions[bot] commented 4 years ago

This issue is stale because it has been open 21 days with no activity. Remove stale label or comment or this will be closed in 7 days

pjotrsavitski commented 3 years ago

The suggested approach does not work if there are morph relationships on both sides: example a user model with roles attached using spatie/laravel-permission. One can override the Role class, but it does not really get any events for attaching to or detaching from a User model.

The only solution seems to be a custom activity storage from controller code side, though it will miss all the possible changes that would be made outside of it. Would it be possible to attach some event to sync and the rest of the methods that will create custom events every time the relationship is being changed on the User model side? I have not seen any events being triggered by the relation system, though I could be wrong.

Gummibeer commented 3 years ago

599 is for spatie permission compatibility.

But you are right that Laravel doesn't fire any events. I think the primary reason is that most relationship logic is done with the base DB query builder and not on any eloquent classes.

There are packages trying to fire events for relationships.

But the best will be to use the return of sync() and log this (enriched). You could automate it by adding wrapper methods like syncRoles() on the user model.

But until there are officiall events we won't support relationship logging.

ctf0 commented 3 years ago

based on https://github.com/spatie/laravel-permission/issues/754#issuecomment-468228085 i managed to save the media attachment via media observer ex.

public function saving(Media $media)
{
    $this->saveActivityLog($media, 'added', 'attributes');
}

public function deleting(Media $media)
{
    $this->saveActivityLog($media, 'removed', 'old');
}

protected function saveActivityLog($media, $event, $key)
{
    return activity()
        ->useLog('relation')
        ->by(auth()->user())
        ->performedOn($media->model)
        ->withProperties([
            $key => ['media' => $media->name],
        ])
        ->log($event);
}