RMI permissions table/structure & has permission trait

This commit is contained in:
a6a2f5842 2025-06-19 13:36:10 +02:00
parent 79e5c832db
commit d9bdc10f78
7 changed files with 126 additions and 75 deletions

View File

@ -5,16 +5,15 @@ return [
'models' => [ 'models' => [
'role' => \Blax\Roles\Models\Role::class, 'role' => \Blax\Roles\Models\Role::class,
'role_member' => \Blax\Roles\Models\RoleMember::class, 'role_member' => \Blax\Roles\Models\RoleMember::class,
'role_permission' => \Blax\Roles\Models\RolePermission::class,
'permission' => \Blax\Roles\Models\Permission::class, 'permission' => \Blax\Roles\Models\Permission::class,
'permission_usage' => \Blax\Roles\Models\PermissionUsage::class, 'permission_usage' => \Blax\Roles\Models\PermissionUsage::class,
'permission_members' => \Blax\Roles\Models\PermissionMember::class, 'permission_member' => \Blax\Roles\Models\PermissionMember::class,
], ],
'table_names' => [ 'table_names' => [
'permissions' => 'permissions', 'permissions' => 'permissions',
'permission_usage' => 'permission_usages', 'permission_usage' => 'permission_usages',
'permission_members' => 'permission_members', 'permission_member' => 'permission_member',
'roles' => 'roles', 'roles' => 'roles',
'role_member' => 'role_members', 'role_member' => 'role_members',
'role_permission' => 'role_permissions', 'role_permission' => 'role_permissions',

View File

@ -22,7 +22,7 @@ return new class extends Migration
}); });
// PermissionMember // PermissionMember
Schema::create(config('roles.table_names.permission_members'), function (Blueprint $table) { Schema::create(config('roles.table_names.permission_member'), function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('permission_id')->constrained('permissions')->onDelete('cascade'); $table->foreignId('permission_id')->constrained('permissions')->onDelete('cascade');
$table->morphs('member'); $table->morphs('member');
@ -63,16 +63,6 @@ return new class extends Migration
$table->timestamp('expires_at')->nullable(); $table->timestamp('expires_at')->nullable();
$table->timestamps(); $table->timestamps();
}); });
// RolePermission
Schema::create(config('roles.table_names.role_permission'), function (Blueprint $table) {
$table->id();
$table->foreignId('role_id')->constrained('roles')->onDelete('cascade');
$table->foreignId('permission_id')->constrained('permissions')->onDelete('cascade');
$table->json('context')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
});
} }
/** /**
@ -80,11 +70,10 @@ return new class extends Migration
*/ */
public function down(): void public function down(): void
{ {
Schema::dropIfExists(config('roles.table_names.role_permission'));
Schema::dropIfExists(config('roles.table_names.role_members')); Schema::dropIfExists(config('roles.table_names.role_members'));
Schema::dropIfExists(config('roles.table_names.roles')); Schema::dropIfExists(config('roles.table_names.roles'));
Schema::dropIfExists(config('roles.table_names.permission_usage')); Schema::dropIfExists(config('roles.table_names.permission_usage'));
Schema::dropIfExists(config('roles.table_names.permission_members')); Schema::dropIfExists(config('roles.table_names.permission_member'));
Schema::dropIfExists(config('roles.table_names.permissions')); Schema::dropIfExists(config('roles.table_names.permissions'));
} }
}; };

View File

@ -20,16 +20,22 @@ class Permission extends Model
public function usages() public function usages()
{ {
return $this->hasMany(PermissionUsage::class); return $this->hasMany(config('roles.table_names.permission_usage'));
} }
public function roles() public function roles()
{ {
return $this->belongsToMany(RolePermission::class); return $this->morphToMany(
config('roles.table_names.role'),
'member',
config('roles.table_names.permission_member'),
'permission_id',
'member_id'
)->where('member_type', config('roles.table_names.role'));
} }
public function members() public function members()
{ {
return $this->hasMany(PermissionMember::class); return $this->hasMany(config('roles.table_names.permission_member'));
} }
} }

View File

@ -26,7 +26,7 @@ class PermissionMember extends Model
{ {
parent::__construct($attributes); parent::__construct($attributes);
$this->table = config('roles.table_names.permission_members') ?: parent::getTable(); $this->table = config('roles.table_names.permission_member') ?: parent::getTable();
} }
public function permission() public function permission()

View File

@ -1,33 +0,0 @@
<?php
namespace Blax\Roles\Models;
use Blax\Roles\Traits\WillExpire;
use Illuminate\Database\Eloquent\Model;
class RolePermission extends Model
{
use WillExpire;
protected $fillable = [
'role_id',
'permission_id',
];
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->table = config('roles.table_names.role_permissions') ?: parent::getTable();
}
public function role()
{
return $this->belongsTo(Role::class);
}
public function permission()
{
return $this->belongsTo(Permission::class);
}
}

View File

@ -12,7 +12,7 @@ class RolesServiceProvider extends \Illuminate\Support\ServiceProvider
public function register() public function register()
{ {
$this->mergeConfigFrom( $this->mergeConfigFrom(
__DIR__.'/../config/roles.php', __DIR__ . '/../config/roles.php',
'roles' 'roles'
); );
} }
@ -43,15 +43,15 @@ class RolesServiceProvider extends \Illuminate\Support\ServiceProvider
} }
$this->publishes([ $this->publishes([
__DIR__.'/../config/roles.php' => $this->app->configPath('roles.php'), __DIR__ . '/../config/roles.php' => $this->app->configPath('roles.php'),
], 'roles-config'); ], 'roles-config');
$this->publishes([ $this->publishes([
__DIR__.'/../database/migrations/create_blax_role_tables.php.stub' => $this->getMigrationFileName('create_blax_role_tables.php'), __DIR__ . '/../database/migrations/create_blax_role_tables.php.stub' => $this->getMigrationFileName('create_blax_role_tables.php'),
], 'roles-migrations'); ], 'roles-migrations');
} }
/** /**
* Returns existing migration file if found, else uses the current timestamp. * Returns existing migration file if found, else uses the current timestamp.
*/ */
protected function getMigrationFileName(string $migrationFileName): string protected function getMigrationFileName(string $migrationFileName): string
@ -60,19 +60,18 @@ class RolesServiceProvider extends \Illuminate\Support\ServiceProvider
$filesystem = $this->app->make(\Illuminate\Filesystem\Filesystem::class); $filesystem = $this->app->make(\Illuminate\Filesystem\Filesystem::class);
return \Illuminate\Support\Collection::make([$this->app->databasePath().DIRECTORY_SEPARATOR.'migrations'.DIRECTORY_SEPARATOR]) return \Illuminate\Support\Collection::make([$this->app->databasePath() . DIRECTORY_SEPARATOR . 'migrations' . DIRECTORY_SEPARATOR])
->flatMap(fn ($path) => $filesystem->glob($path.'*_'.$migrationFileName)) ->flatMap(fn($path) => $filesystem->glob($path . '*_' . $migrationFileName))
->push($this->app->databasePath()."/migrations/{$timestamp}_{$migrationFileName}") ->push($this->app->databasePath() . "/migrations/{$timestamp}_{$migrationFileName}")
->first(); ->first();
} }
protected function registerModelBindings(): void protected function registerModelBindings(): void
{ {
$this->app->bind(\Blax\Roles\Models\Role::class, fn ($app) => $app->make($app->config['roles.models.role'])); $this->app->bind(\Blax\Roles\Models\Role::class, fn($app) => $app->make($app->config['roles.models.role']));
$this->app->bind(\Blax\Roles\Models\RoleMember::class, fn ($app) => $app->make($app->config['roles.models.role_member'])); $this->app->bind(\Blax\Roles\Models\RoleMember::class, fn($app) => $app->make($app->config['roles.models.role_member']));
$this->app->bind(\Blax\Roles\Models\RolePermission::class, fn ($app) => $app->make($app->config['roles.models.role_permission'])); $this->app->bind(\Blax\Roles\Models\Permission::class, fn($app) => $app->make($app->config['roles.models.permission']));
$this->app->bind(\Blax\Roles\Models\Permission::class, fn ($app) => $app->make($app->config['roles.models.permission'])); $this->app->bind(\Blax\Roles\Models\PermissionUsage::class, fn($app) => $app->make($app->config['roles.models.permission_usage']));
$this->app->bind(\Blax\Roles\Models\PermissionUsage::class, fn ($app) => $app->make($app->config['roles.models.permission_usage'])); $this->app->bind(\Blax\Roles\Models\PermissionMember::class, fn($app) => $app->make($app->config['roles.models.permission_member']));
$this->app->bind(\Blax\Roles\Models\PermissionMember::class, fn ($app) => $app->make($app->config['roles.models.permission_members']));
} }
} }

View File

@ -2,26 +2,117 @@
namespace Blax\Roles\Traits; namespace Blax\Roles\Traits;
use Illuminate\Support\Collection;
trait HasPermissions trait HasPermissions
{ {
public function hasPermission(string $permission, array $context = []): bool public function hasPermission(string $permission): bool
{ {
return $this->permissions() return $this->permissions()
->where('name', $permission) ->where('name', $permission)
->where(function ($query) use ($context) { ->orWhere('slug', '*')
if (!empty($context)) {
$query->where('context', $context);
}
})
->exists(); ->exists();
} }
public function permissions() public function permissions()
{ {
return $this->morphToMany( $permissionClass = config('roles.models.permission');
config('roles.models.permission'), $permissionTable = config('roles.table_names.permissions');
$permissionMemberTable = config('roles.table_names.permission_member');
// direct assignment
$direct = $this->morphToMany(
$permissionClass,
'member', 'member',
config('roles.table_names.permission_members') $permissionMemberTable
); );
if (! method_exists($this, 'roles')) {
return $direct;
}
// inherited via roles
$permissionRoleTable = config('roles.table_names.permission_role');
$roleMemberTable = config('roles.table_names.role_member');
$memberType = $this->getMorphClass();
$viaRoles = $permissionClass::query()
->select("$permissionTable.*")
->join($permissionRoleTable, "$permissionTable.id", '=', "$permissionRoleTable.permission_id")
->join($roleMemberTable, "$permissionRoleTable.role_id", '=', "$roleMemberTable.role_id")
->where("$roleMemberTable.member_id", $this->getKey())
->where("$roleMemberTable.member_type", $memberType);
return $direct->union($viaRoles);
}
public function addPermission($permission): bool
{
$permission_class = config('roles.models.permission');
if (is_numeric($permission)) {
$permission = $permission_class::find($permission);
} elseif (is_string($permission)) {
$permission = $permission_class::where('slug', $permission)->firstOrCreate();
} elseif ($permission instanceof $permission_class) {
// Already a Permission instance
} else {
throw new \InvalidArgumentException('Permission must be a string, numeric ID, or an instance of Permission.');
}
if ($permission) {
return $this->permissions()->attach($permission);
}
return false;
}
public function removePermission($permission): bool
{
$permission_class = config('roles.models.permission');
if (is_numeric($permission)) {
$permission = $permission_class::find($permission);
} elseif (is_string($permission)) {
$permission = $permission_class::where('slug', $permission)->first();
} elseif ($permission instanceof $permission_class) {
// Already a Permission instance
} else {
throw new \InvalidArgumentException('Permission must be a string, numeric ID, or an instance of Permission.');
}
if ($permission) {
return $this->permissions()->detach($permission);
}
return false;
}
/**
* Get all permissions directly assigned or inherited via roles.
*
* @return Collection
*/
public function allPermissions(): Collection
{
// Directly assigned permissions
$direct = $this->permissions()->get();
// Permissions via roles (if the roles() relation exists)
if (method_exists($this, 'roles')) {
$rolePermissions = $this->roles()
->with('permissions')
->get()
->pluck('permissions')
->flatten();
} else {
$rolePermissions = collect();
}
// Merge and dedupe by 'id'
return $direct
->merge($rolePermissions)
->unique('id')
->values();
} }
} }