spatie/laravel-permission

Role::getTable() method brokes withCount method

xenaio-daniil opened this issue · 1 comments

I needed to get ManyToMany relation of roles (structure of departments). To do this, I extended the \Spatie\Permission\Models\Role class with methods:

class Department extends \Spatie\Permission\Models\Role
{
    ....
    public function parents(): BelongsToMany
    {
        return $this->belongsToMany(
            Department::class,
            "department_hierarchy,
            'child_id',
            'parent_id'
        );
    }

    public function children(): BelongsToMany
    {
        return $this->belongsToMany(
            Department::class,
            'department_hierarchy',
            'parent_id',
            'child_id'
        );
    }
    ....
}

and created migration:

    Schema::create('department_hierarchy', function (Blueprint $table) {
        $table->string("rel_id", 25)->primary();
        $table->bigInteger("parent_id", false, true);
        $table->bigInteger("child_id", false, true);
        $table->boolean("is_direct")->default(false);
        $table->foreign("parent_id")->references("id")->on("roles");
        $table->foreign("child_id")->references("id")->on("roles");
    });

Everything works well except methods like

\App\Models\Department::withCount('children')

and so on: it always returns parent_count = 0.

The reason of this behavior is in overriding of method getTable in Spatie\Permission\Models\Role.

When extending Illuminate\Database\Eloquent\Model:

echo \App\Models\Department::withCount('children')->toSql();
/**
SELECT `roles`.*, (
SELECT COUNT(*)
FROM `roles` AS `laravel_reserved_0`
INNER JOIN `department_hierarchy` 
    ON `laravel_reserved_0`.`id` = `department_hierarchy`.`child_id`    <-- look at laravel_reserved_0
WHERE `roles`.`id` = `department_hierarchy`.`parent_id`) AS `children_count`
FROM `roles`

When extending \Spatie\Permission\Models\Role:

echo \App\Models\Department::withCount('children')->toSql();
/**
SELECT `roles`.*, (
SELECT COUNT(*)
FROM `roles` AS `laravel_reserved_0`
INNER JOIN `department_hierarchy` 
    ON `roles`.`id` = `department_hierarchy`.`child_id`    <------ laravel_reserved_0 != roles
WHERE `roles`.`id` = `department_hierarchy`.`parent_id`) AS `children_count`
FROM `roles`

withCount() uses Model::table field to temporary store alias name (laravel_reserved_0).

And when (in Illuminate\Database\Eloquent\Model) method Model::getTable() returns $this->table or magic name from class basename, your version returns only pre-configurated table name (config('permission.table_names.roles')) or, if configuration not found, Model::getTable(). So if permission.table_names.roles is configured, it has no change to return $this->table.

I would recommend to rewrite this method like this:

    public function getTable()
    {
        return $this->table ?? config('permission.table_names.roles', parent::getTable());
    }

or for best practice remove method Role::getTable() and replace it with

    public function __construct(array $attributes = [])
    {
        $attributes['guard_name'] = $attributes['guard_name'] ?? config('auth.defaults.guard');

        parent::__construct($attributes);

        $this->guarded[] = $this->primaryKey;

        $this->table = config('permission.table_names.roles', parent::getTable()); // <---------------
    }

Feel free to open a PR