diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 5940c1979..8c6da4eb4 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ -blank_issues_enabled: false +blank_issues_enabled: true contact_links: - - name: Ask a Question - url: https://github.com/spatie/laravel-permission/discussions/new?category=q-a - about: Ask the community for help - name: Feature Request url: https://github.com/spatie/laravel-permission/discussions/new?category=ideas about: Share ideas for new features + - name: Ask a Question + url: https://github.com/spatie/laravel-permission/discussions/new?category=q-a + about: Ask the community for help diff --git a/.github/workflows/fix-php-code-style-issues.yml b/.github/workflows/fix-php-code-style-issues.yml index 60dc90d6f..e3be7f171 100644 --- a/.github/workflows/fix-php-code-style-issues.yml +++ b/.github/workflows/fix-php-code-style-issues.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: ${{ github.head_ref }} @@ -19,6 +19,6 @@ jobs: uses: aglipanci/laravel-pint-action@v2 - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v5 + uses: stefanzweifel/git-auto-commit-action@v7 with: commit_message: Fix styling diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index be7882397..17bbcdf3f 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -15,7 +15,7 @@ jobs: name: phpstan runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 3f47884ba..2dd07ca39 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - php: [8.4, 8.3, 8.2, 8.1, 8.0] + php: [8.5, 8.4, 8.3, 8.2, 8.1, 8.0] laravel: ["^12.0", "^11.0", "^10.0", "^9.0", "^8.12"] dependency-version: [prefer-lowest, prefer-stable] include: @@ -34,16 +34,22 @@ jobs: php: 8.0 - laravel: "^10.0" php: 8.0 + - laravel: "^10.0" + php: 8.5 + - laravel: "^9.0" + php: 8.5 - laravel: "^8.12" php: 8.3 - laravel: "^8.12" php: 8.4 + - laravel: "^8.12" + php: 8.5 name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/test-cache-drivers.yml b/.github/workflows/test-cache-drivers.yml index 2bfd5b28a..3b9398c9c 100644 --- a/.github/workflows/test-cache-drivers.yml +++ b/.github/workflows/test-cache-drivers.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index 0cdea2336..cb7fa9f8b 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -10,7 +10,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: main @@ -21,7 +21,7 @@ jobs: release-notes: ${{ github.event.release.body }} - name: Commit updated CHANGELOG - uses: stefanzweifel/git-auto-commit-action@v5 + uses: stefanzweifel/git-auto-commit-action@v7 with: branch: main commit_message: Update CHANGELOG diff --git a/CHANGELOG.md b/CHANGELOG.md index 3778ef834..b76054f36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,63 @@ All notable changes to `laravel-permission` will be documented in this file +## 6.21.0 - 2025-07-23 + +### What's Changed + +* Allow removing multiple roles with the `removeRole` method by @TobMoeller in https://github.com/spatie/laravel-permission/pull/2859 +* [Docs] Correct middleware order for documentation example in `teams-permissions.md` by @dualklip in https://github.com/spatie/laravel-permission/pull/2863 + +### New Contributors + +* @dualklip made their first contribution in https://github.com/spatie/laravel-permission/pull/2863 +* @TobMoeller made their first contribution in https://github.com/spatie/laravel-permission/pull/2859 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.20.0...6.21.0 + +## 6.20.0 - 2025-06-14 + +### What's Changed + +* Add translations support for exception messages by @nAa6666 in https://github.com/spatie/laravel-permission/pull/2852 + +### New Contributors + +* @nAa6666 made their first contribution in https://github.com/spatie/laravel-permission/pull/2852 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.19.0...6.20.0 + +## 6.19.0 - 2025-05-31 + +### What's Changed + +* Revert "Remove `collectPermissions` that is not being assigned" by @erikn69 in https://github.com/spatie/laravel-permission/pull/2851 +* Fix guard_name not used to set default attribute in Role and Permission model by @Ken-vdE in https://github.com/spatie/laravel-permission/pull/2837 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.18.0...6.19.0 + +## 6.18.0 - 2025-05-14 + +### What's Changed + +* refactor exception throwing in migration file to use throw_if() by @ccaioadriano in https://github.com/spatie/laravel-permission/pull/2819 +* Fix: Example in config comment includes `permission.` prefix on `wildcard_permission` key by @jerrens in https://github.com/spatie/laravel-permission/pull/2835 +* Fix commented config key typo by @erikn69 in https://github.com/spatie/laravel-permission/pull/2844 +* Remove `collectPermissions` that is not being assigned by @JHWelch in https://github.com/spatie/laravel-permission/pull/2840 +* [Docs] Update multiple-guards.md by @Ken-vdE in https://github.com/spatie/laravel-permission/pull/2836 +* [Docs] Remove extra period by @coreyhn in https://github.com/spatie/laravel-permission/pull/2841 +* Add JetAdmin as UI Option. by @aliqasemzadeh in https://github.com/spatie/laravel-permission/pull/2814 + +### New Contributors + +* @ccaioadriano made their first contribution in https://github.com/spatie/laravel-permission/pull/2819 +* @coreyhn made their first contribution in https://github.com/spatie/laravel-permission/pull/2841 +* @jerrens made their first contribution in https://github.com/spatie/laravel-permission/pull/2835 +* @Ken-vdE made their first contribution in https://github.com/spatie/laravel-permission/pull/2836 +* @JHWelch made their first contribution in https://github.com/spatie/laravel-permission/pull/2840 + +**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.17.0...6.18.0 + ## 6.17.0 - 2025-04-09 ### What's Changed @@ -1002,6 +1059,10 @@ The following changes are not "breaking", but worth making the updates to your a + + + + @@ -1080,6 +1141,10 @@ The following changes are not "breaking", but worth making the updates to your a + + + + diff --git a/README.md b/README.md index a8a54d3fd..413063953 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ - Logo for laravel-permission + Logo for laravel-permission

Associate users with permissions and roles

[![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-permission.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-permission) -[![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/spatie/laravel-permission/run-tests-L8.yml?branch=main&label=Tests)](https://github.com/spatie/laravel-permission/actions?query=workflow%3ATests+branch%3Amain) +[![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/spatie/laravel-permission/run-tests.yml?branch=main&label=Tests)](https://github.com/spatie/laravel-permission/actions?query=workflow%3ATests+branch%3Amain) [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-permission.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-permission) diff --git a/config/permission.php b/config/permission.php index 8e84e9d53..f39f6b5bf 100644 --- a/config/permission.php +++ b/config/permission.php @@ -172,7 +172,7 @@ * The class to use for interpreting wildcard permissions. * If you need to modify delimiters, override the class and specify its name here. */ - // 'permission.wildcard_permission' => Spatie\Permission\WildcardPermission::class, + // 'wildcard_permission' => Spatie\Permission\WildcardPermission::class, /* Cache-specific settings */ diff --git a/docs/advanced-usage/seeding.md b/docs/advanced-usage/seeding.md index 30dc21bf2..51c56e8d2 100644 --- a/docs/advanced-usage/seeding.md +++ b/docs/advanced-usage/seeding.md @@ -7,7 +7,7 @@ weight: 2 You may discover that it is best to flush this package's cache **BEFORE seeding, to avoid cache conflict errors**. -And if you use the `WithoutModelEvents` trait in your seeders, flush it **AFTER creating any roles/permissions as well, before assigning or granting them.**. +And if you use the `WithoutModelEvents` trait in your seeders, flush it **AFTER creating any roles/permissions as well, before assigning or granting them**. ```php // reset cached roles and permissions diff --git a/docs/advanced-usage/ui-options.md b/docs/advanced-usage/ui-options.md index ebfb552ef..4e1d5d41d 100644 --- a/docs/advanced-usage/ui-options.md +++ b/docs/advanced-usage/ui-options.md @@ -31,3 +31,5 @@ If you decide you need a UI, even if it's not for creating/editing role/permissi - [LiveWire Base Admin Panel](https://github.com/aliqasemzadeh/bap) User management by [AliQasemzadeh](https://github.com/aliqasemzadeh) - [JetAdmin](https://github.com/aliqasemzadeh/jetadmin) JetAdmin use laravel livewire starter kit and manage permissions. [AliQasemzadeh](https://github.com/aliqasemzadeh) + +- [QuickPanel](https://github.com/aliqasemzadeh/quickpanel) Quick Panel (TALL Flowbite Starter Kit). [AliQasemzadeh](https://github.com/aliqasemzadeh) diff --git a/docs/basic-usage/direct-permissions.md b/docs/basic-usage/direct-permissions.md index e171c8163..6b4987ec5 100644 --- a/docs/basic-usage/direct-permissions.md +++ b/docs/basic-usage/direct-permissions.md @@ -46,7 +46,7 @@ Like all permissions assigned via roles, you can check if a user has a permissio $user->can('edit articles'); ``` -> NOTE: The following `hasPermissionTo`, `hasAnyPermission`, `hasAllPermissions` functions do not support Super-Admin functionality. Use `can`, `canAny`, `canAll` instead. +> NOTE: The following `hasPermissionTo`, `hasAnyPermission`, `hasAllPermissions` functions do not support Super-Admin functionality. Use `can`, `canAny` instead. You can check if a user has a permission: diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md index 259e9214f..e9173ec59 100644 --- a/docs/basic-usage/middleware.md +++ b/docs/basic-usage/middleware.md @@ -22,7 +22,7 @@ This package comes with `RoleMiddleware`, `PermissionMiddleware` and `RoleOrPerm You can register their aliases for easy reference elsewhere in your app: -In Laravel 11 open `/bootstrap/app.php` and register them there: +In Laravel 11+ open `/bootstrap/app.php` and register them there: ```php ->withMiddleware(function (Middleware $middleware) { diff --git a/docs/basic-usage/multiple-guards.md b/docs/basic-usage/multiple-guards.md index d9360d6cf..062e49fad 100644 --- a/docs/basic-usage/multiple-guards.md +++ b/docs/basic-usage/multiple-guards.md @@ -20,7 +20,8 @@ Note that this package requires you to register a permission name (same for role If your app structure does NOT differentiate between guards when it comes to roles/permissions, (ie: if ALL your roles/permissions are the SAME for ALL guards), you can override the `getDefaultGuardName` function by adding it to your User model, and specifying your desired `$guard_name`. Then you only need to create roles/permissions for that single `$guard_name`, not duplicating them. The example here sets it to `web`, but use whatever your application's default is: ```php - protected function getDefaultGuardName(): string { return 'web'; } + protected string $guard_name = 'web'; + protected function getDefaultGuardName(): string { return $this->guard_name; } ```` diff --git a/docs/basic-usage/teams-permissions.md b/docs/basic-usage/teams-permissions.md index ad220a1ac..15095a7ab 100644 --- a/docs/basic-usage/teams-permissions.md +++ b/docs/basic-usage/teams-permissions.md @@ -77,12 +77,15 @@ class AppServiceProvider extends ServiceProvider $kernel = app()->make(Kernel::class); $kernel->addToMiddlewarePriorityBefore( - SubstituteBindings::class, YourCustomMiddlewareClass::class, + SubstituteBindings::class, ); } } ``` +### Using LiveWire? + +You may need to register your team middleware as Persisted in Livewire. See [Livewire docs: Configuring Persistent Middleware](https://livewire.laravel.com/docs/security#configuring-persistent-middleware) ## Roles Creating @@ -99,7 +102,7 @@ Role::create(['name' => 'reader', 'team_id' => 1]); Role::create(['name' => 'reviewer']); ``` -## Roles/Permissions Assignment & Removal +## Roles/Permissions Assignment and Removal The role/permission assignment and removal for teams are the same as without teams, but they take the global `team_id` which is set on login. diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 022878407..e6a4b0f89 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -17,3 +17,6 @@ parameters: # wildcard permissions: - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Model::getWildcardClass#' - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Model::getAllPermissions#' + # contract checks: + - '#Call to function is_a\(\) with (.*) and ''Spatie\\\\Permission\\\\Contracts\\\\Permission'' will always evaluate to true\.$#' + - '#Call to function is_a\(\) with (.*) and ''Spatie\\\\Permission\\\\Contracts\\\\Role'' will always evaluate to true\.$#' diff --git a/src/Events/RoleDetached.php b/src/Events/RoleDetached.php index 287995fe6..9a32518ab 100644 --- a/src/Events/RoleDetached.php +++ b/src/Events/RoleDetached.php @@ -18,8 +18,8 @@ class RoleDetached use SerializesModels; /** - * Internally the HasRoles trait passes $rolesOrIds as a single Eloquent record - * Theoretically one could register the event to other places with an array etc + * Internally the HasRoles trait passes an array of role ids (eg: int's or uuid's) + * Theoretically one could register the event to other places passing other types * So a Listener should inspect the type of $rolesOrIds received before using. * * @param array|int[]|string[]|Role|Role[]|Collection $rolesOrIds diff --git a/src/Exceptions/GuardDoesNotMatch.php b/src/Exceptions/GuardDoesNotMatch.php index 6506bb658..7c57b3155 100644 --- a/src/Exceptions/GuardDoesNotMatch.php +++ b/src/Exceptions/GuardDoesNotMatch.php @@ -9,6 +9,9 @@ class GuardDoesNotMatch extends InvalidArgumentException { public static function create(string $givenGuard, Collection $expectedGuards) { - return new static("The given role or permission should use guard `{$expectedGuards->implode(', ')}` instead of `{$givenGuard}`."); + return new static(__('The given role or permission should use guard `:expected` instead of `:given`.', [ + 'expected' => $expectedGuards->implode(', '), + 'given' => $givenGuard, + ])); } } diff --git a/src/Exceptions/PermissionAlreadyExists.php b/src/Exceptions/PermissionAlreadyExists.php index 2748c9d1d..2270db148 100644 --- a/src/Exceptions/PermissionAlreadyExists.php +++ b/src/Exceptions/PermissionAlreadyExists.php @@ -8,6 +8,9 @@ class PermissionAlreadyExists extends InvalidArgumentException { public static function create(string $permissionName, string $guardName) { - return new static("A `{$permissionName}` permission already exists for guard `{$guardName}`."); + return new static(__('A `:permission` permission already exists for guard `:guard`.', [ + 'permission' => $permissionName, + 'guard' => $guardName, + ])); } } diff --git a/src/Exceptions/PermissionDoesNotExist.php b/src/Exceptions/PermissionDoesNotExist.php index c04f5eaf0..853bab10e 100644 --- a/src/Exceptions/PermissionDoesNotExist.php +++ b/src/Exceptions/PermissionDoesNotExist.php @@ -8,7 +8,10 @@ class PermissionDoesNotExist extends InvalidArgumentException { public static function create(string $permissionName, ?string $guardName) { - return new static("There is no permission named `{$permissionName}` for guard `{$guardName}`."); + return new static(__('There is no permission named `:permission` for guard `:guard`.', [ + 'permission' => $permissionName, + 'guard' => $guardName, + ])); } /** @@ -17,6 +20,9 @@ public static function create(string $permissionName, ?string $guardName) */ public static function withId($permissionId, ?string $guardName) { - return new static("There is no [permission] with ID `{$permissionId}` for guard `{$guardName}`."); + return new static(__('There is no [permission] with ID `:id` for guard `:guard`.', [ + 'id' => $permissionId, + 'guard' => $guardName, + ])); } } diff --git a/src/Exceptions/RoleAlreadyExists.php b/src/Exceptions/RoleAlreadyExists.php index 939c55bac..141be8d26 100644 --- a/src/Exceptions/RoleAlreadyExists.php +++ b/src/Exceptions/RoleAlreadyExists.php @@ -8,6 +8,9 @@ class RoleAlreadyExists extends InvalidArgumentException { public static function create(string $roleName, string $guardName) { - return new static("A role `{$roleName}` already exists for guard `{$guardName}`."); + return new static(__('A role `:role` already exists for guard `:guard`.', [ + 'role' => $roleName, + 'guard' => $guardName, + ])); } } diff --git a/src/Exceptions/RoleDoesNotExist.php b/src/Exceptions/RoleDoesNotExist.php index a4871c665..641b4d37f 100644 --- a/src/Exceptions/RoleDoesNotExist.php +++ b/src/Exceptions/RoleDoesNotExist.php @@ -8,7 +8,10 @@ class RoleDoesNotExist extends InvalidArgumentException { public static function named(string $roleName, ?string $guardName) { - return new static("There is no role named `{$roleName}` for guard `{$guardName}`."); + return new static(__('There is no role named `:role` for guard `:guard`.', [ + 'role' => $roleName, + 'guard' => $guardName, + ])); } /** @@ -17,6 +20,9 @@ public static function named(string $roleName, ?string $guardName) */ public static function withId($roleId, ?string $guardName) { - return new static("There is no role with ID `{$roleId}` for guard `{$guardName}`."); + return new static(__('There is no role with ID `:id` for guard `:guard`.', [ + 'id' => $roleId, + 'guard' => $guardName, + ])); } } diff --git a/src/Exceptions/UnauthorizedException.php b/src/Exceptions/UnauthorizedException.php index 249898a75..b9e9fe902 100644 --- a/src/Exceptions/UnauthorizedException.php +++ b/src/Exceptions/UnauthorizedException.php @@ -13,10 +13,10 @@ class UnauthorizedException extends HttpException public static function forRoles(array $roles): self { - $message = 'User does not have the right roles.'; + $message = __('User does not have the right roles.'); if (config('permission.display_role_in_exception')) { - $message .= ' Necessary roles are '.implode(', ', $roles); + $message .= ' '.__('Necessary roles are :roles', ['roles' => implode(', ', $roles)]); } $exception = new static(403, $message, null, []); @@ -27,10 +27,10 @@ public static function forRoles(array $roles): self public static function forPermissions(array $permissions): self { - $message = 'User does not have the right permissions.'; + $message = __('User does not have the right permissions.'); if (config('permission.display_permission_in_exception')) { - $message .= ' Necessary permissions are '.implode(', ', $permissions); + $message .= ' '.__('Necessary permissions are :permissions', ['permissions' => implode(', ', $permissions)]); } $exception = new static(403, $message, null, []); @@ -41,10 +41,10 @@ public static function forPermissions(array $permissions): self public static function forRolesOrPermissions(array $rolesOrPermissions): self { - $message = 'User does not have any of the necessary access rights.'; + $message = __('User does not have any of the necessary access rights.'); if (config('permission.display_permission_in_exception') && config('permission.display_role_in_exception')) { - $message .= ' Necessary roles or permissions are '.implode(', ', $rolesOrPermissions); + $message .= ' '.__('Necessary roles or permissions are :values', ['values' => implode(', ', $rolesOrPermissions)]); } $exception = new static(403, $message, null, []); @@ -57,12 +57,14 @@ public static function missingTraitHasRoles(Authorizable $user): self { $class = get_class($user); - return new static(403, "Authorizable class `{$class}` must use Spatie\Permission\Traits\HasRoles trait.", null, []); + return new static(403, __('Authorizable class `:class` must use Spatie\\Permission\\Traits\\HasRoles trait.', [ + 'class' => $class, + ]), null, []); } public static function notLoggedIn(): self { - return new static(403, 'User is not logged in.', null, []); + return new static(403, __('User is not logged in.'), null, []); } public function getRequiredRoles(): array diff --git a/src/Exceptions/WildcardPermissionInvalidArgument.php b/src/Exceptions/WildcardPermissionInvalidArgument.php index 44f092401..2c0a802ee 100644 --- a/src/Exceptions/WildcardPermissionInvalidArgument.php +++ b/src/Exceptions/WildcardPermissionInvalidArgument.php @@ -8,6 +8,6 @@ class WildcardPermissionInvalidArgument extends InvalidArgumentException { public static function create() { - return new static('Wildcard permission must be string, permission id or permission instance'); + return new static(__('Wildcard permission must be string, permission id or permission instance')); } } diff --git a/src/Exceptions/WildcardPermissionNotImplementsContract.php b/src/Exceptions/WildcardPermissionNotImplementsContract.php index 7b81c73c9..52deaa456 100644 --- a/src/Exceptions/WildcardPermissionNotImplementsContract.php +++ b/src/Exceptions/WildcardPermissionNotImplementsContract.php @@ -8,6 +8,6 @@ class WildcardPermissionNotImplementsContract extends InvalidArgumentException { public static function create() { - return new static('Wildcard permission class must implements Spatie\Permission\Contracts\Wildcard contract'); + return new static(__('Wildcard permission class must implement Spatie\\Permission\\Contracts\\Wildcard contract')); } } diff --git a/src/Exceptions/WildcardPermissionNotProperlyFormatted.php b/src/Exceptions/WildcardPermissionNotProperlyFormatted.php index 721157e6c..54b1e2915 100644 --- a/src/Exceptions/WildcardPermissionNotProperlyFormatted.php +++ b/src/Exceptions/WildcardPermissionNotProperlyFormatted.php @@ -8,6 +8,8 @@ class WildcardPermissionNotProperlyFormatted extends InvalidArgumentException { public static function create(string $permission) { - return new static("Wildcard permission `{$permission}` is not properly formatted."); + return new static(__('Wildcard permission `:permission` is not properly formatted.', [ + 'permission' => $permission, + ])); } } diff --git a/src/Models/Permission.php b/src/Models/Permission.php index fd54b8ca0..490a6c23c 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -26,7 +26,7 @@ class Permission extends Model implements PermissionContract public function __construct(array $attributes = []) { - $attributes['guard_name'] = $attributes['guard_name'] ?? config('auth.defaults.guard'); + $attributes['guard_name'] ??= Guard::getDefaultName(static::class); parent::__construct($attributes); @@ -41,7 +41,7 @@ public function __construct(array $attributes = []) */ public static function create(array $attributes = []) { - $attributes['guard_name'] = $attributes['guard_name'] ?? Guard::getDefaultName(static::class); + $attributes['guard_name'] ??= Guard::getDefaultName(static::class); $permission = static::getPermission(['name' => $attributes['name'], 'guard_name' => $attributes['guard_name']]); @@ -88,7 +88,7 @@ public function users(): BelongsToMany */ public static function findByName(string $name, ?string $guardName = null): PermissionContract { - $guardName = $guardName ?? Guard::getDefaultName(static::class); + $guardName ??= Guard::getDefaultName(static::class); $permission = static::getPermission(['name' => $name, 'guard_name' => $guardName]); if (! $permission) { throw PermissionDoesNotExist::create($name, $guardName); @@ -106,7 +106,7 @@ public static function findByName(string $name, ?string $guardName = null): Perm */ public static function findById(int|string $id, ?string $guardName = null): PermissionContract { - $guardName = $guardName ?? Guard::getDefaultName(static::class); + $guardName ??= Guard::getDefaultName(static::class); $permission = static::getPermission([(new static)->getKeyName() => $id, 'guard_name' => $guardName]); if (! $permission) { @@ -123,7 +123,7 @@ public static function findById(int|string $id, ?string $guardName = null): Perm */ public static function findOrCreate(string $name, ?string $guardName = null): PermissionContract { - $guardName = $guardName ?? Guard::getDefaultName(static::class); + $guardName ??= Guard::getDefaultName(static::class); $permission = static::getPermission(['name' => $name, 'guard_name' => $guardName]); if (! $permission) { diff --git a/src/Models/Role.php b/src/Models/Role.php index 5bab4878e..7aa729dc1 100644 --- a/src/Models/Role.php +++ b/src/Models/Role.php @@ -27,7 +27,7 @@ class Role extends Model implements RoleContract public function __construct(array $attributes = []) { - $attributes['guard_name'] = $attributes['guard_name'] ?? config('auth.defaults.guard'); + $attributes['guard_name'] ??= Guard::getDefaultName(static::class); parent::__construct($attributes); @@ -42,7 +42,7 @@ public function __construct(array $attributes = []) */ public static function create(array $attributes = []) { - $attributes['guard_name'] = $attributes['guard_name'] ?? Guard::getDefaultName(static::class); + $attributes['guard_name'] ??= Guard::getDefaultName(static::class); $params = ['name' => $attributes['name'], 'guard_name' => $attributes['guard_name']]; if (app(PermissionRegistrar::class)->teams) { @@ -97,7 +97,7 @@ public function users(): BelongsToMany */ public static function findByName(string $name, ?string $guardName = null): RoleContract { - $guardName = $guardName ?? Guard::getDefaultName(static::class); + $guardName ??= Guard::getDefaultName(static::class); $role = static::findByParam(['name' => $name, 'guard_name' => $guardName]); @@ -115,7 +115,7 @@ public static function findByName(string $name, ?string $guardName = null): Role */ public static function findById(int|string $id, ?string $guardName = null): RoleContract { - $guardName = $guardName ?? Guard::getDefaultName(static::class); + $guardName ??= Guard::getDefaultName(static::class); $role = static::findByParam([(new static)->getKeyName() => $id, 'guard_name' => $guardName]); @@ -133,7 +133,7 @@ public static function findById(int|string $id, ?string $guardName = null): Role */ public static function findOrCreate(string $name, ?string $guardName = null): RoleContract { - $guardName = $guardName ?? Guard::getDefaultName(static::class); + $guardName ??= Guard::getDefaultName(static::class); $role = static::findByParam(['name' => $name, 'guard_name' => $guardName]); diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php index d10dd77e3..38d2f02c0 100644 --- a/src/Traits/HasRoles.php +++ b/src/Traits/HasRoles.php @@ -193,13 +193,14 @@ function ($object) use ($roles, $model, $teamPivot, &$saved) { /** * Revoke the given role from the model. * - * @param string|int|Role|\BackedEnum $role + * @param string|int|array|Role|Collection|\BackedEnum ...$role + * @return $this */ - public function removeRole($role) + public function removeRole(...$role) { - $storedRole = $this->getStoredRole($role); + $roles = $this->collectRoles($role); - $this->roles()->detach($storedRole); + $this->roles()->detach($roles); $this->unsetRelation('roles'); @@ -208,7 +209,7 @@ public function removeRole($role) } if (config('permission.events_enabled')) { - event(new RoleDetached($this->getModel(), $storedRole)); + event(new RoleDetached($this->getModel(), $roles)); } return $this; diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php index 49f450359..95ad3a473 100644 --- a/tests/HasPermissionsTest.php +++ b/tests/HasPermissionsTest.php @@ -611,6 +611,23 @@ public function it_can_avoid_sync_duplicated_permissions() $this->assertTrue($this->testUser->hasDirectPermission('edit-blog')); } + /** @test */ + #[Test] + public function it_can_avoid_detach_on_permission_that_does_not_exist_sync() + { + $this->testUser->syncPermissions('edit-articles'); + + try { + $this->testUser->syncPermissions('permission-does-not-exist'); + $this->fail('Expected PermissionDoesNotExist exception was not thrown.'); + } catch (PermissionDoesNotExist $e) { + // + } + + $this->assertTrue($this->testUser->hasDirectPermission('edit-articles')); + $this->assertFalse($this->testUser->checkPermissionTo('permission-does-not-exist')); + } + /** @test */ #[Test] public function it_can_sync_multiple_permissions_by_id() diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php index 7f8a137e0..734394706 100644 --- a/tests/HasRolesTest.php +++ b/tests/HasRolesTest.php @@ -181,42 +181,62 @@ public function it_can_assign_and_remove_a_role_on_a_permission() /** @test */ #[Test] - public function it_can_assign_a_role_using_an_object() + public function it_can_assign_and_remove_a_role_using_an_object() { $this->testUser->assignRole($this->testUserRole); $this->assertTrue($this->testUser->hasRole($this->testUserRole)); + + $this->testUser->removeRole($this->testUserRole); + + $this->assertFalse($this->testUser->hasRole($this->testUserRole)); } /** @test */ #[Test] - public function it_can_assign_a_role_using_an_id() + public function it_can_assign_and_remove_a_role_using_an_id() { $this->testUser->assignRole($this->testUserRole->getKey()); $this->assertTrue($this->testUser->hasRole($this->testUserRole)); + + $this->testUser->removeRole($this->testUserRole->getKey()); + + $this->assertFalse($this->testUser->hasRole($this->testUserRole)); } /** @test */ #[Test] - public function it_can_assign_multiple_roles_at_once() + public function it_can_assign_and_remove_multiple_roles_at_once() { $this->testUser->assignRole($this->testUserRole->getKey(), 'testRole2'); $this->assertTrue($this->testUser->hasRole('testRole')); $this->assertTrue($this->testUser->hasRole('testRole2')); + + $this->testUser->removeRole($this->testUserRole->getKey(), 'testRole2'); + + $this->assertFalse($this->testUser->hasRole('testRole')); + + $this->assertFalse($this->testUser->hasRole('testRole2')); } /** @test */ #[Test] - public function it_can_assign_multiple_roles_using_an_array() + public function it_can_assign_and_remove_multiple_roles_using_an_array() { $this->testUser->assignRole([$this->testUserRole->getKey(), 'testRole2']); $this->assertTrue($this->testUser->hasRole('testRole')); $this->assertTrue($this->testUser->hasRole('testRole2')); + + $this->testUser->removeRole([$this->testUserRole->getKey(), 'testRole2']); + + $this->assertFalse($this->testUser->hasRole('testRole')); + + $this->assertFalse($this->testUser->hasRole('testRole2')); } /** @test */ @@ -318,6 +338,23 @@ public function it_can_avoid_sync_duplicated_roles() $this->assertTrue($this->testUser->hasRole('testRole2')); } + /** @test */ + #[Test] + public function it_can_avoid_detach_on_role_that_does_not_exist_sync() + { + $this->testUser->syncRoles('testRole'); + + try { + $this->testUser->syncRoles('role-does-not-exist'); + $this->fail('Expected RoleDoesNotExist exception was not thrown.'); + } catch (RoleDoesNotExist $e) { + // + } + + $this->assertTrue($this->testUser->hasRole('testRole')); + $this->assertFalse($this->testUser->hasRole('role-does-not-exist')); + } + /** @test */ #[Test] public function it_can_sync_multiple_roles() @@ -956,14 +993,19 @@ public function it_fires_an_event_when_a_role_is_removed() Event::fake(); app('config')->set('permission.events_enabled', true); - $this->testUser->assignRole('testRole'); + $this->testUser->assignRole('testRole', 'testRole2'); - $this->testUser->removeRole('testRole'); + $this->testUser->removeRole('testRole', 'testRole2'); + + $roleIds = app(Role::class)::whereIn('name', ['testRole', 'testRole2']) + ->pluck($this->testUserRole->getKeyName()) + ->toArray(); - Event::assertDispatched(RoleDetached::class, function ($event) { + Event::assertDispatched(RoleDetached::class, function ($event) use ($roleIds) { return $event->model instanceof User && ! $event->model->hasRole('testRole') - && $event->rolesOrIds->name === 'testRole'; + && ! $event->model->hasRole('testRole2') + && $event->rolesOrIds === $roleIds; }); } diff --git a/tests/TestCase.php b/tests/TestCase.php index 073e7c8fb..cc243faa1 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -148,7 +148,7 @@ protected function getEnvironmentSetUp($app) // FOR MANUAL TESTING OF ALTERNATE CACHE STORES: // $app['config']->set('cache.default', 'array'); // Laravel supports: array, database, file - // requires extensions: apc, memcached, redis, dynamodb, octane + // requires extensions: memcached, redis, dynamodb, octane } /** @@ -230,7 +230,7 @@ protected function setUpPassport($app): void $app['config']->set('auth.guards.api', ['driver' => 'passport', 'provider' => 'users']); // mimic passport:install (must load migrations using our own call to loadMigrationsFrom() else rollbacks won't occur, and migrations will be left in skeleton directory - $this->artisan('passport:keys'); + // $this->artisan('passport:keys'); $this->loadMigrationsFrom(__DIR__.'/../vendor/laravel/passport/database/migrations/'); $provider = in_array('users', array_keys(config('auth.providers'))) ? 'users' : null; $this->artisan('passport:client', ['--personal' => true, '--name' => config('app.name').' Personal Access Client']);