Laravel-activitylog: Log activity on laravel-permission events model_has_roles table

Created on 26 Sep 2019  路  19Comments  路  Source: spatie/laravel-activitylog

I'm extending Permission and Role models for laravel-permissions package.

Activity log works as expected when a new Role is created, updated or deleted. Now I'm just wondering how to log any activity related to model_has_roles and role_has_permissions tables to keep track when roles are assigned to a user or permissions are attached to roles.

Having a CRUD for system users where roles can be attached/detached adds/removes records from model_has_roles table, but no activity is being logged.

Do I need to extend pivot tables or implement anything else in my custom Models? This is how my custom Role model looks like:

<?php

namespace App;

use Spatie\Permission\Models\Role as Roles;

// Activity Log
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\Contracts\Activity;

class Role extends Roles
{
    //
    use LogsActivity;

    protected $table    = 'roles';
    protected $fillable = ['name','title','guard_name'];

    // Activity Log
    protected static $logFillable = true;
    protected static $submitEmptyLogs = false;
    protected static $logOnlyDirty = true;
}

Any help will be appreciated.
Thanks in advance

Edit: BTW, my controllers are using custom models such use App\Role;
Edit 2: Roles are assigned to Users using $user->roles()->sync($roles);

enhancement hacktoberfest help wanted package-compatibility

Most helpful comment

i usually use the sync helpers, so using activity() we can manually save the relation with ease without any extra changes ex.

  • controller
$model = ... // created / updated user model

$old = [
    'roles'       => $model->getRoleNames(),
    'permissions' => $model->getPermissionNames(),
];

$model->syncRoles($request->roles);
$model->syncPermissions($request->permissions);

$new = [
    'roles'       => $model->getRoleNames(),
    'permissions' => $model->getPermissionNames(),
];

$model->saveActivity([
    'old'        => $old,
    'attributes' => $new,
]);
  • causer model
public function saveActivity($data)
{
    return activity()
        ->by(auth()->user())
        ->performedOn($this)
        ->withProperties($data);
}

All 19 comments

The only way to log relations is a pivot model which uses the trait. Keep in mind to add a id column and set $incrementing = true to have a subject assigned. #598

Thanks for your reply, but I'm not getting how to solve this. I've added a Pivot model as in my own post you referenced, but I think I will need to add something else to make custom Role model works with custom pivot model.

I'm guessing that I would need to implement some specific traits but I certainly don't know if some of these are the right ones and how to do it:

use Spatie\Permission\Guard;
use Illuminate\Database\Eloquent\Model;
use Spatie\Permission\Traits\HasPermissions;
use Spatie\Permission\Exceptions\RoleDoesNotExist;
use Spatie\Permission\Exceptions\GuardDoesNotMatch;
use Spatie\Permission\Exceptions\RoleAlreadyExists;
use Spatie\Permission\Contracts\Role as RoleContract;
use Spatie\Permission\Traits\RefreshesPermissionCache;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

This is what I have so far.

<?php

namespace App;

use Illuminate\Database\Eloquent\Relations\Pivot;

// Activity Log
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\Contracts\Activity;

class ModelHasRoles extends Pivot
{
    //
    use LogsActivity;

    protected $table    = 'model_has_roles';
    protected $fillable = ['model_type', 'model_id'];

    // Activity Log
    protected static $logFillable = true;
    protected static $submitEmptyLogs = false;
    protected static $logOnlyDirty = true;
    public $incrementing = true; // To log subject_id on pivot tables
}

I'm not in the referenced permission package. But have you told the relationship methods to use the pivot model?
I hope you mean the spatie package? If so I know that @drbyte does an amazing job and could be a great help solving your issue!

Yes, I'm combining both Spatie packages and I want to log all activities under model_has_roles and role_has_permissions.
And same as you, I think I'm missing the part where I define which pivot model to use.
I'm trying to understand how it works by looking at vendor files but It's too advanced for me.

You have to redeclare the relationship method.
https://github.com/spatie/laravel-permission/blob/f3652b51773ac276d81e836df2f93949ca27d720/src/Models/Role.php#L50-L58

You can use:

public function permissions(): BelongsToMany
{
    parent:: permissions()->using(MyPivotModel::class);
}

https://laravel.com/docs/6.x/eloquent-relationships#defining-custom-intermediate-table-models

Same to do for all other relationships you want to use a custom pivot. By using parent:: you don't have to adjust anything if the package changes relationship method content.

Got it! But unfortunately, now I'm facing a new issue.
When activity log record regards to "created" event, the subject_id is 0 and also the attributes logged contains "permission_id":0.
I'm guessing that's because public $incrementing property, but despite it's set to true it's not working fine.
What I've found is that role_has_permissions table contains a multiple column index:
$table->primary(['permission_id', 'role_id']);

So public $incrementing = false results in no activity being logged and public $incrementing = true results in a wrong activity log record.

This is how it looks my pivot Model now:

<?php

namespace App;

use Illuminate\Database\Eloquent\Relations\Pivot;

// Activity Log
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\Contracts\Activity;

class RoleHasPermissions extends Pivot
{
    //
    use LogsActivity;

    protected $primaryKey = 'permission_id';
    protected $table    = 'role_has_permissions';
    protected $fillable = ['role_id','permission_id'];

    // Activity Log
    protected static $logFillable = true;
    protected static $submitEmptyLogs = false;
    protected static $logOnlyDirty = true;
    public $incrementing = true; // To log subject_id on pivot tables

    public function tapActivity(Activity $activity, string $eventName)
    {
        $activity->parent_id = $this->role_id;
        $activity->parent_type = 'App\Role';
    }
}

I also tried by adding a multiple keys to pivot model but no luck. It throws "message": "Illegal offset type",

protected function setKeysForSaveQuery(Builder $query)
    {
        $keys = $this->getKeyName();
        if(!is_array($keys)){
            return parent::setKeysForSaveQuery($query);
        }

        foreach($keys as $keyName){
            $query->where($keyName, '=', $this->getKeyForSaveQuery($keyName));
        }

        return $query;
    }

    /**
     * Get the primary key value for a save query.
     *
     * @param mixed $keyName
     * @return mixed
     */
    protected function getKeyForSaveQuery($keyName = null)
    {
        if(is_null($keyName)){
            $keyName = $this->getKeyName();
        }

        if (isset($this->original[$keyName])) {
            return $this->original[$keyName];
        }

        return $this->getAttribute($keyName);
    }

    protected $primaryKey = ['role_id','permission_id'];

I'm not sure if it's possible to fix this or I will have to desist on this. I thought that both packages would have better compatibility.

If you enable $incrementing you will have to add an id column to the pivot table like in the normal role, permission, user tables. The DB will increment it by it's own.

The $incrementing only tells Laravel to retrieve the incremented ID after an insert.

Ok, manipulating the PK and FKs doesn't look like a good idea. As soon I edited them, the package started failing everywhere.
I finally decided to perform a manual activity log in my controller when permissions are attached to roles.
I think that I will have to handle all pivot tables related events in this way if I want to keep track of changes, at least for complex tables like the ones used in Spatie permissions package.
Hope there will be a native way to handle pivot models activity in the future. So far this is enough for me and this package is doing a 95% of what I expected from it.

I really appreciate all your help @Gummibeer
Thank you!

Thanks @Gummibeer for the shoutout and the excellent assistance and insights shared here. I learned several things from your posts here ;)

@luchorengo I've bookmarked this thread as something to investigate when time permits. I may ask @Gummibeer for more insights then too!

You shouldn't have to manipulate FKs. Only add a PK which should be ignored by the permission packages code, auto incremented by DB itself and read by activitylog.
But yes, combine the two packages occured several times in the issues. So it could be a good idea if Chris and I work out a trait, doc article, package or whatever could solve this best.
At the end it shouldn't need black magic. But I haven't used the permission package yet and have no time right now to quick check it.
But will also keep an eye on this and try to find a solution for all easy to implement.

That's why I reopen the issue.

@Gummibeer Thank you for response on issue #679

The thread in here is pretty helpful, but there are a few aspects that have to be done right in order for it to start working. I'll try to provide explanations in case one would find it useful at some later point in time.

The first issue I had was the way of overriding the roles method as it did not want to use the parent::roles() as it does not really belong to the parent class but comes from a trait instead. A working solution seems to be something like this instead. First, import roles method as a private one with a different name.

use HasRoles
{
    roles as private traitRoles;
}

Second, define the now missing roles method and use the previously defined one while adding a new class. Please note that UserRole class could be defined in any namespace and important, using the default one fro models will be the easiest.

public function roles(): BelongsToMany
{
    return $this->traitRoles()->using(UserRole::class);
}

Third, make sure you use MorphPivot instead on standard Pivot

<?php

namespace App;

use Illuminate\Database\Eloquent\Relations\MorphPivot;
use Spatie\Activitylog\Traits\LogsActivity;

final class UserRole extends MorphPivot
{
    use LogsActivity;

    protected static $logAttributes = ['*'];
    protected static $submitEmptyLogs = false;
}

Please note that it does not have the public $incrementing = true; present. It makes sense to have that, but it does not prevent activity from being stored into the database. The only issue is that it will not be searchable (though this identifier is mostly useless as the contents of that table will not change, those could either be inserted or deleted) and the value of subject_id will be set to NULL.

If needed, then both role_id and model_id would be present in theproperties field and would be quite easily searchable from that JSON column.

Please do comment if that has potential of causing issues if used like that with identifier not present. Big integer has loads of values, but just adding an auto increment in the without any good reason did not feel right.

Similar approach could also be extended to permissions and even permissions of roles, if that needs to be tracked.

Maybe tapAttributes method could be used in UserRoleto replace description value with createdUserRole/removedUserRole (depending on event), subject_type being set to App\User (or App\Models\User, depending on the location), subject_id being replaced with identifier of a user model. It would create a bit of an inconsistent behavior and might better be left as it is by default instead.

@pjotrsavitski thanks for this already very detailed explanation and hinting at some quirks and what could be hacked in addition.

Just in case you are willing: I would accept a PR that adds a fully-fledged out page to the documentation.
placed at https://spatie.be/docs/laravel-activitylog/v3/compatible-packages/spatie/laravel-permission or similar - not sure about the package-vendor - possibly spatie-laravel-permission will be better.

This page should be a full walkthrough of how to make the relation loggable. This would be the best approach to handle compatibility with other packages - primary spatie ones.

@Gummibeer Yes, I think that writing about the Polymorphic relationships using spatie/laravel-permission package with an example for user roles would be doable on my side. Will that be enough? The logic seems to be rather general one and applicable to other cases. Do you have a repository to fork and submit a PR to? I'll be willing to take it up next week as it could be helpful.

The docs are right inside this repo: https://github.com/spatie/laravel-activitylog/tree/master/docs
We already have the general pivot model section: https://spatie.be/docs/laravel-activitylog/v3/advanced-usage/logging-model-events#logging-on-pivot-models

So this package should fully and only focus on the permission package. I will be thankful for everything you can submit! 馃檪

I've got some kind of initial version of documentation extension done here. It will still need a lot of tunes and changes, at least on the textual side. One more issue could be with adding the increments as primary key as one has already been defined. I will have to check how it works before going forward.

Looking at the code got me thinking about it a bit. Model relation changes can only capture creation events as deletion is done on the database side and rather silently (at least for the pivot models themselves). If it would be possible to add some kind of events to the public API to clearly identify what happened.

It would require changes to the HasRoles trait (only considering the roles side of things) with changes to methods assignRole, removeRole and syncRoles:

  • assignRole - would have an event with model and list of roles that were provided (disregarding the possibility of some of those already being present)
  • removeRole - would have an event with model and role being removed
  • syncRoles would have an event for removed (detached roles) with adding new ones being handled by the assignRole

It might also be possible to detect changes between assigned roles and already present ones within the syncRoles event and only reporting the ones that are being removed. Sync could also list roles before, roles applied and roles after. Leaving it to the event handler to decide if that is of interest or not.

This will probably also need events for direct permissions and role permissions so that the implementation would be consistent.

Does that sound sane?

@pjotrsavitski These related notes may be of interest to you.
The first is probably the lightest-weight way to add some basic event triggers.
The others show the additional complexity that baking this into the permissions package would add, which is why we're not doing that at this time.
https://github.com/spatie/laravel-permission/issues/754#issuecomment-468229002
https://github.com/spatie/laravel-permission/issues/1412
https://github.com/spatie/laravel-permission/issues/724
https://github.com/spatie/laravel-permission/issues/1221

@drbyte Thanks for the links. The first one seems to be a rather working solution with sync event missing. It seems that overriding the trait is the easiest way to achieve that and should be the easiest one to deal with. I did not dig too much into the mentioned observer logic, but events seem to be easy enough. It could easily be a standalone package that just defined some additional events and traits that extend the original ones.

i usually use the sync helpers, so using activity() we can manually save the relation with ease without any extra changes ex.

  • controller
$model = ... // created / updated user model

$old = [
    'roles'       => $model->getRoleNames(),
    'permissions' => $model->getPermissionNames(),
];

$model->syncRoles($request->roles);
$model->syncPermissions($request->permissions);

$new = [
    'roles'       => $model->getRoleNames(),
    'permissions' => $model->getPermissionNames(),
];

$model->saveActivity([
    'old'        => $old,
    'attributes' => $new,
]);
  • causer model
public function saveActivity($data)
{
    return activity()
        ->by(auth()->user())
        ->performedOn($this)
        ->withProperties($data);
}

I will close this issue even if v4 isn't released yet. But the task itself is done and I want to check which tasks are really open. Please keep an eye on #787

Was this page helpful?
0 / 5 - 0 ratings