diff --git a/config/roles.php b/config/roles.php index b66ab7e..9dcf575 100644 --- a/config/roles.php +++ b/config/roles.php @@ -5,16 +5,15 @@ return [ 'models' => [ 'role' => \Blax\Roles\Models\Role::class, 'role_member' => \Blax\Roles\Models\RoleMember::class, - 'role_permission' => \Blax\Roles\Models\RolePermission::class, 'permission' => \Blax\Roles\Models\Permission::class, 'permission_usage' => \Blax\Roles\Models\PermissionUsage::class, - 'permission_members' => \Blax\Roles\Models\PermissionMember::class, + 'permission_member' => \Blax\Roles\Models\PermissionMember::class, ], 'table_names' => [ 'permissions' => 'permissions', 'permission_usage' => 'permission_usages', - 'permission_members' => 'permission_members', + 'permission_member' => 'permission_member', 'roles' => 'roles', 'role_member' => 'role_members', 'role_permission' => 'role_permissions', diff --git a/database/migrations/create_blax_role_tables.php.stub b/database/migrations/create_blax_role_tables.php.stub index 197912a..7ccf7dc 100644 --- a/database/migrations/create_blax_role_tables.php.stub +++ b/database/migrations/create_blax_role_tables.php.stub @@ -22,7 +22,7 @@ return new class extends Migration }); // 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->foreignId('permission_id')->constrained('permissions')->onDelete('cascade'); $table->morphs('member'); @@ -63,16 +63,6 @@ return new class extends Migration $table->timestamp('expires_at')->nullable(); $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 { - Schema::dropIfExists(config('roles.table_names.role_permission')); Schema::dropIfExists(config('roles.table_names.role_members')); Schema::dropIfExists(config('roles.table_names.roles')); 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')); } }; diff --git a/src/Models/Permission.php b/src/Models/Permission.php index 5bf86a3..10ca73d 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -20,16 +20,22 @@ class Permission extends Model public function usages() { - return $this->hasMany(PermissionUsage::class); + return $this->hasMany(config('roles.table_names.permission_usage')); } 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() { - return $this->hasMany(PermissionMember::class); + return $this->hasMany(config('roles.table_names.permission_member')); } } diff --git a/src/Models/PermissionMember.php b/src/Models/PermissionMember.php index df139a6..e96f484 100644 --- a/src/Models/PermissionMember.php +++ b/src/Models/PermissionMember.php @@ -26,7 +26,7 @@ class PermissionMember extends Model { 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() diff --git a/src/Models/RolePermission.php b/src/Models/RolePermission.php deleted file mode 100644 index 40dfca9..0000000 --- a/src/Models/RolePermission.php +++ /dev/null @@ -1,33 +0,0 @@ -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); - } -} diff --git a/src/RolesServiceProvider.php b/src/RolesServiceProvider.php index 3c963fd..ed858da 100644 --- a/src/RolesServiceProvider.php +++ b/src/RolesServiceProvider.php @@ -12,7 +12,7 @@ class RolesServiceProvider extends \Illuminate\Support\ServiceProvider public function register() { $this->mergeConfigFrom( - __DIR__.'/../config/roles.php', + __DIR__ . '/../config/roles.php', 'roles' ); } @@ -43,15 +43,15 @@ class RolesServiceProvider extends \Illuminate\Support\ServiceProvider } $this->publishes([ - __DIR__.'/../config/roles.php' => $this->app->configPath('roles.php'), + __DIR__ . '/../config/roles.php' => $this->app->configPath('roles.php'), ], 'roles-config'); $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'); } - /** + /** * Returns existing migration file if found, else uses the current timestamp. */ protected function getMigrationFileName(string $migrationFileName): string @@ -60,19 +60,18 @@ class RolesServiceProvider extends \Illuminate\Support\ServiceProvider $filesystem = $this->app->make(\Illuminate\Filesystem\Filesystem::class); - return \Illuminate\Support\Collection::make([$this->app->databasePath().DIRECTORY_SEPARATOR.'migrations'.DIRECTORY_SEPARATOR]) - ->flatMap(fn ($path) => $filesystem->glob($path.'*_'.$migrationFileName)) - ->push($this->app->databasePath()."/migrations/{$timestamp}_{$migrationFileName}") + return \Illuminate\Support\Collection::make([$this->app->databasePath() . DIRECTORY_SEPARATOR . 'migrations' . DIRECTORY_SEPARATOR]) + ->flatMap(fn($path) => $filesystem->glob($path . '*_' . $migrationFileName)) + ->push($this->app->databasePath() . "/migrations/{$timestamp}_{$migrationFileName}") ->first(); } 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\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\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_members'])); + $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\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\PermissionMember::class, fn($app) => $app->make($app->config['roles.models.permission_member'])); } } diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php index e1dec9c..c4f5f2c 100644 --- a/src/Traits/HasPermissions.php +++ b/src/Traits/HasPermissions.php @@ -2,26 +2,117 @@ namespace Blax\Roles\Traits; +use Illuminate\Support\Collection; + trait HasPermissions { - public function hasPermission(string $permission, array $context = []): bool + public function hasPermission(string $permission): bool { return $this->permissions() ->where('name', $permission) - ->where(function ($query) use ($context) { - if (!empty($context)) { - $query->where('context', $context); - } - }) + ->orWhere('slug', '*') ->exists(); } public function permissions() { - return $this->morphToMany( - config('roles.models.permission'), + $permissionClass = config('roles.models.permission'); + $permissionTable = config('roles.table_names.permissions'); + $permissionMemberTable = config('roles.table_names.permission_member'); + + // direct assignment + $direct = $this->morphToMany( + $permissionClass, '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(); } }