diff --git a/config/shinobi.php b/config/shinobi.php index b71a8b3..ef30c83 100644 --- a/config/shinobi.php +++ b/config/shinobi.php @@ -2,6 +2,44 @@ return [ + /* + |-------------------------------------------------------------------------- + | Experimental Cache + |-------------------------------------------------------------------------- + | + | Shinobi ships with an experimental caching layer in an attempt to lessen + | the load on resources when checking and validating permissions. By + | default this is disabled, please enable to provide feedback. + */ + + 'cache' => [ + + /** + * You may enable or disable the built in caching system. This is useful + * when debugging your application. If your application already has its + * own caching layer, we suggest disabling the cache here as well. + */ + + 'enabled' => false, + + /** + * Define the length of time permissions should be cached for before being + * refreshed. Accepted values are either in seconds or as a DateInterval + * object. By default we cache for 86400 seconds (aka, 24 hours). + */ + + 'length' => 60 * 60 * 24, + + /** + * When using a cache driver that supports tags, we'll tag the shinobi + * cache with this tag. This is useful for busting only the cache + * responsible for storing permissions and not anything else. + */ + + 'tag' => 'shinobi', + + ], + 'models' => [ /* diff --git a/src/Concerns/HasPermissions.php b/src/Concerns/HasPermissions.php index 1847952..034da51 100644 --- a/src/Concerns/HasPermissions.php +++ b/src/Concerns/HasPermissions.php @@ -119,19 +119,19 @@ public function syncPermissions(...$permissions): self * @param array $permissions * @return Permission */ - protected function getPermissions(array $permissions) + protected function getPermissions(array $collection) { return array_map(function($permission) { $model = $this->getPermissionModel(); - if ($permission instanceof $model) { + if ($permission instanceof Permission) { return $permission->id; } $permission = $model->where('slug', $permission)->first(); return $permission->id; - }, $permissions); + }, $collection); } /** @@ -144,7 +144,7 @@ protected function hasPermission($permission): bool { $model = $this->getPermissionModel(); - if ($permission instanceof $model) { + if ($permission instanceof Permission) { $permission = $permission->slug; } @@ -154,10 +154,20 @@ protected function hasPermission($permission): bool /** * Get the model instance responsible for permissions. * - * @return \Caffeinated\Shinobi\Contracts\Permission + * @return \Caffeinated\Shinobi\Contracts\Permission|\Illuminate\Database\Eloquent\Collection */ - protected function getPermissionModel(): Permission + protected function getPermissionModel() { + if (config('shinobi.cache.enabled')) { + return cache()->tags(config('shinobi.cache.tag'))->remember( + 'permissions', + config('shinobi.cache.length'), + function() { + return app()->make(config('shinobi.models.permission'))->get(); + } + ); + } + return app()->make(config('shinobi.models.permission')); } } \ No newline at end of file diff --git a/src/Concerns/HasRoles.php b/src/Concerns/HasRoles.php index 7a32111..fd06a86 100644 --- a/src/Concerns/HasRoles.php +++ b/src/Concerns/HasRoles.php @@ -30,6 +30,40 @@ public function hasRole($role): bool return (bool) $this->roles->where('slug', $slug)->count(); } + /** + * Checks if the model has any of the given roles assigned. + * + * @param array $roles + * @return bool + */ + public function hasAnyRole(...$roles): bool + { + foreach ($roles as $role) { + if ($this->hasRole($role)) { + return true; + } + } + + return false; + } + + /** + * Checks if the model has all of the given roles assigned. + * + * @param array $roles + * @return bool + */ + public function hasAllRoles(...$roles): bool + { + foreach ($roles as $role) { + if (! $this->hasRole($role)) { + return false; + } + } + + return true; + } + public function hasRoles(): bool { return (bool) $this->roles->count(); diff --git a/src/Concerns/RefreshesPermissionCache.php b/src/Concerns/RefreshesPermissionCache.php new file mode 100644 index 0000000..2f92f08 --- /dev/null +++ b/src/Concerns/RefreshesPermissionCache.php @@ -0,0 +1,17 @@ +tags(config('shinobi.cache.tag'))->flush(); + }); + + static::deleted(function() { + cache()->tags(config('shinobi.cache.tag'))->flush(); + }); + } +} \ No newline at end of file diff --git a/src/Models/Permission.php b/src/Models/Permission.php index d623d74..b5964c3 100644 --- a/src/Models/Permission.php +++ b/src/Models/Permission.php @@ -4,10 +4,13 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Caffeinated\Shinobi\Concerns\RefreshesPermissionCache; use Caffeinated\Shinobi\Contracts\Permission as PermissionContract; class Permission extends Model implements PermissionContract { + use RefreshesPermissionCache; + /** * The attributes that are fillable via mass assignment. * diff --git a/src/ShinobiServiceProvider.php b/src/ShinobiServiceProvider.php index d7bf3f7..261d16a 100644 --- a/src/ShinobiServiceProvider.php +++ b/src/ShinobiServiceProvider.php @@ -4,13 +4,9 @@ use Exception; use Illuminate\Support\Facades\Gate; -use Caffeinated\Shinobi\Models\Role; use Illuminate\Support\Facades\Blade; use Illuminate\Foundation\Application; -use Illuminate\Support\Facades\Schema; use Illuminate\Support\ServiceProvider; -use Caffeinated\Shinobi\Facades\Shinobi; -use Caffeinated\Shinobi\Models\Permission; use Illuminate\Contracts\Auth\Access\Authorizable; class ShinobiServiceProvider extends ServiceProvider @@ -76,6 +72,14 @@ protected function registerBladeDirectives() Blade::if('role', function($role) { return auth()->user() and auth()->user()->hasRole($role); }); + + Blade::if('anyrole', function(...$roles) { + return auth()->user() and auth()->user()->hasAnyRole(...$roles); + }); + + Blade::if('allroles', function(...$roles) { + return auth()->user() and auth()->user()->hasAllRoles(...$roles); + }); } /** diff --git a/tests/BladeTest.php b/tests/BladeTest.php index 20c34df..ac8060b 100644 --- a/tests/BladeTest.php +++ b/tests/BladeTest.php @@ -127,4 +127,128 @@ public function guests_do_not_have_roles() $this->assertEquals($result, 'does not have admin or moderator roles'); } + + /** @test */ + public function the_anyrole_directive_evaluates_true_when_at_least_one_role_is_found() + { + $admin = factory(Role::class)->create([ + 'name' => 'Admin', + 'slug' => 'admin', + ]); + + $user = factory(User::class)->create(); + + $user->assignRoles($admin); + + $this->actingAs($user); + + $result = $this->renderView('anyrole_directive'); + + $this->assertEquals($result, 'has either moderator or admin role'); + } + + /** @test */ + public function the_anyrole_directive_evaluates_true_when_at_least_one_role_is_found_when_using_else() + { + $editor = factory(Role::class)->create([ + 'name' => 'Editor', + 'slug' => 'editor', + ]); + + $user = factory(User::class)->create(); + + $user->assignRoles($editor); + + $this->actingAs($user); + + $result = $this->renderView('anyrole_directive'); + + $this->assertEquals($result, 'has either editor or contributor role'); + } + + /** @test */ + public function the_anyrole_directive_evaluates_false_when_no_matching_role_is_found() + { + $vip = factory(Role::class)->create([ + 'name' => 'VIP', + 'slug' => 'vip', + ]); + + $user = factory(User::class)->create(); + + $user->assignRoles($vip); + + $this->actingAs($user); + + $result = $this->renderView('anyrole_directive'); + + $this->assertEquals($result, 'does not have any of the defined roles'); + } + + /** @test */ + public function the_allrole_directive_evaluates_true_when_all_roles_are_found() + { + $moderator = factory(Role::class)->create([ + 'name' => 'Moderator', + 'slug' => 'moderator', + ]); + + $editor = factory(Role::class)->create([ + 'name' => 'Editor', + 'slug' => 'editor', + ]); + + $user = factory(User::class)->create(); + + $user->assignRoles($moderator, $editor); + + $this->actingAs($user); + + $result = $this->renderView('allroles_directive'); + + $this->assertEquals($result, 'has both moderator and editor roles'); + } + + /** @test */ + public function the_allrole_directive_evaluates_true_when_all_roles_are_found_when_using_else() + { + $vip = factory(Role::class)->create([ + 'name' => 'VIP', + 'slug' => 'vip', + ]); + + $premium = factory(Role::class)->create([ + 'name' => 'Premium', + 'slug' => 'premium', + ]); + + $user = factory(User::class)->create(); + + $user->assignRoles($vip, $premium); + + $this->actingAs($user); + + $result = $this->renderView('allroles_directive'); + + $this->assertEquals($result, 'has both VIP and premium roles'); + } + + /** @test */ + public function the_allroles_directive_evaluates_false_when_all_defined_roles_are_not_met() + { + $vip = factory(Role::class)->create([ + 'name' => 'VIP', + 'slug' => 'vip', + ]); + + $user = factory(User::class)->create(); + + $user->assignRoles($vip); + + $this->actingAs($user); + + $result = $this->renderView('allroles_directive'); + + $this->assertEquals($result, 'does not have any of the defined roles'); + } } \ No newline at end of file diff --git a/tests/UserTest.php b/tests/UserTest.php index 3715b8e..b55493d 100644 --- a/tests/UserTest.php +++ b/tests/UserTest.php @@ -233,4 +233,65 @@ public function it_has_no_permissions_when_assigned_a_role_with_a_no_access_flag $this->assertFalse($user->fresh()->hasPermissionTo($permission->slug)); } + + /** @test */ + public function it_can_verify_it_has_defined_role() + { + $user = factory(User::class)->create(); + $role = factory(Role::class)->create(); + + $this->assertFalse($user->fresh()->hasRole($role->slug)); + + $user->assignRoles($role); + + $this->assertTrue($user->fresh()->hasRole($role->slug)); + } + + /** @test */ + public function it_can_verify_it_has_any_defined_role() + { + $editor = factory(Role::class)->create([ + 'name' => 'Editor', + 'slug' => 'editor', + ]); + + $moderator = factory(Role::class)->create([ + 'name' => 'Moderator', + 'slug' => 'moderator', + ]); + + $user = factory(User::class)->create(); + + $this->assertFalse($user->fresh()->hasAnyRole('moderator', 'editor')); + + $user->assignRoles($editor); + + $this->assertTrue($user->fresh()->hasAnyRole('moderator', 'editor')); + } + + /** @test */ + public function it_can_verify_it_has_all_defined_roles() + { + $editor = factory(Role::class)->create([ + 'name' => 'Editor', + 'slug' => 'editor', + ]); + + $moderator = factory(Role::class)->create([ + 'name' => 'Moderator', + 'slug' => 'moderator', + ]); + + $user = factory(User::class)->create(); + + $this->assertFalse($user->fresh()->hasAllRoles('moderator', 'editor')); + + $user->assignRoles($editor); + + $this->assertFalse($user->fresh()->hasAllRoles('moderator', 'editor')); + + $user->assignRoles($moderator); + + $this->assertTrue($user->fresh()->hasAllRoles('moderator', 'editor')); + } } \ No newline at end of file diff --git a/tests/resources/views/allroles_directive.blade.php b/tests/resources/views/allroles_directive.blade.php new file mode 100644 index 0000000..1086123 --- /dev/null +++ b/tests/resources/views/allroles_directive.blade.php @@ -0,0 +1,7 @@ +@allroles('moderator', 'editor') +has both moderator and editor roles +@elseallroles('vip', 'premium') +has both VIP and premium roles +@else +does not have any of the defined roles +@endallroles \ No newline at end of file diff --git a/tests/resources/views/anyrole_directive.blade.php b/tests/resources/views/anyrole_directive.blade.php new file mode 100644 index 0000000..3bc871c --- /dev/null +++ b/tests/resources/views/anyrole_directive.blade.php @@ -0,0 +1,7 @@ +@anyrole('moderator', 'admin') +has either moderator or admin role +@elseanyrole('editor', 'contributor') +has either editor or contributor role +@else +does not have any of the defined roles +@endanyrole \ No newline at end of file