Skip to content

Commit d25ff38

Browse files
authored
Merge pull request #61 from mubbi/develop
chore: develop to main
2 parents a3be435 + 75e14f8 commit d25ff38

File tree

12 files changed

+1162
-9
lines changed

12 files changed

+1162
-9
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Console\Commands;
6+
7+
use App\Constants\CacheKeys;
8+
use App\Models\User;
9+
use Illuminate\Console\Command;
10+
use Illuminate\Support\Facades\Cache;
11+
12+
final class ClearRolePermissionCache extends Command
13+
{
14+
/**
15+
* The name and signature of the console command.
16+
*
17+
* @var string
18+
*/
19+
protected $signature = 'cache:clear-roles-permissions {--user-id= : Clear cache for specific user} {--all : Clear all user caches by incrementing version}';
20+
21+
/**
22+
* The console command description.
23+
*
24+
* @var string
25+
*/
26+
protected $description = 'Clear role and permission caches';
27+
28+
/**
29+
* Execute the console command.
30+
*/
31+
public function handle(): int
32+
{
33+
$userId = $this->option('user-id');
34+
$clearAll = $this->option('all');
35+
36+
if ($userId) {
37+
$user = User::find($userId);
38+
if (! $user) {
39+
$this->error("User with ID {$userId} not found.");
40+
41+
return 1;
42+
}
43+
44+
$user->clearCache();
45+
$this->info("Cache cleared for user: {$user->name} (ID: {$userId})");
46+
} elseif ($clearAll) {
47+
$this->clearAllUserCaches();
48+
$this->info('All user caches cleared by incrementing cache version.');
49+
} else {
50+
$this->clearGlobalCaches();
51+
$this->info('Global role and permission caches cleared successfully.');
52+
}
53+
54+
return 0;
55+
}
56+
57+
/**
58+
* Clear all user caches by incrementing version
59+
*/
60+
private function clearAllUserCaches(): void
61+
{
62+
/** @var int $currentVersion */
63+
$currentVersion = Cache::get('user_cache_version', 1);
64+
$newVersion = $currentVersion + 1;
65+
66+
Cache::put('user_cache_version', $newVersion, CacheKeys::CACHE_TTL);
67+
68+
$this->info("Cache version incremented from {$currentVersion} to {$newVersion}");
69+
$this->info('All user caches are now invalidated.');
70+
}
71+
72+
/**
73+
* Clear global role and permission caches
74+
*/
75+
private function clearGlobalCaches(): void
76+
{
77+
// Clear global caches
78+
Cache::forget(CacheKeys::ALL_ROLES_CACHE_KEY);
79+
Cache::forget(CacheKeys::ALL_PERMISSIONS_CACHE_KEY);
80+
81+
$this->info('Global caches cleared successfully.');
82+
}
83+
}

app/Constants/CacheKeys.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Constants;
6+
7+
final class CacheKeys
8+
{
9+
public const CACHE_TTL = 3600; // 1 hour
10+
11+
// User-specific cache keys
12+
public const USER_ROLES_CACHE_KEY = 'user_roles_';
13+
14+
public const USER_PERMISSIONS_CACHE_KEY = 'user_permissions_';
15+
16+
// Global cache keys
17+
public const ALL_ROLES_CACHE_KEY = 'all_roles_with_permissions';
18+
19+
public const ALL_PERMISSIONS_CACHE_KEY = 'all_permissions';
20+
21+
/**
22+
* Get user roles cache key
23+
*/
24+
public static function userRoles(int $userId): string
25+
{
26+
return self::USER_ROLES_CACHE_KEY.$userId;
27+
}
28+
29+
/**
30+
* Get user permissions cache key
31+
*/
32+
public static function userPermissions(int $userId): string
33+
{
34+
return self::USER_PERMISSIONS_CACHE_KEY.$userId;
35+
}
36+
}

app/Http/Resources/V1/Auth/UserResource.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ public function toArray(Request $request): array
3232
'website' => $this->resource->website,
3333
'roles' => $this->whenLoaded('roles', function () {
3434
return $this->resource->roles->pluck('slug');
35+
}, function () {
36+
// Fallback to cached roles if not loaded
37+
return $this->resource->getCachedRoles();
3538
}),
3639
'permissions' => $this->whenLoaded('roles', function () {
3740
$permissionSlugs = [];
@@ -47,6 +50,9 @@ public function toArray(Request $request): array
4750
}
4851

4952
return array_values(array_unique($permissionSlugs));
53+
}, function () {
54+
// Fallback to cached permissions if not loaded
55+
return $this->resource->getCachedPermissions();
5056
}),
5157
$this->mergeWhen(
5258
array_key_exists('access_token', $this->resource->getAttributes()),

app/Models/Role.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace App\Models;
66

7+
use App\Constants\CacheKeys;
78
use Illuminate\Database\Eloquent\Factories\HasFactory;
89
use Illuminate\Database\Eloquent\Model;
910
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@@ -27,6 +28,44 @@ final class Role extends Model
2728

2829
protected $guarded = [];
2930

31+
/**
32+
* Boot the model and register event listeners
33+
*/
34+
protected static function boot(): void
35+
{
36+
parent::boot();
37+
38+
// Clear user caches when role permissions are updated
39+
self::updated(function (Role $role) {
40+
$role->clearUserCaches();
41+
});
42+
43+
// Clear user caches when role is deleted
44+
self::deleted(function (Role $role) {
45+
$role->clearUserCaches();
46+
});
47+
}
48+
49+
/**
50+
* Clear caches for all users who have this role
51+
*/
52+
private function clearUserCaches(): void
53+
{
54+
// Instead of clearing individual user caches, increment the cache version
55+
// This will invalidate all user caches at once
56+
$this->incrementCacheVersion();
57+
}
58+
59+
/**
60+
* Increment cache version to invalidate all user caches
61+
*/
62+
private function incrementCacheVersion(): void
63+
{
64+
/** @var int $currentVersion */
65+
$currentVersion = \Illuminate\Support\Facades\Cache::get('user_cache_version', 1);
66+
\Illuminate\Support\Facades\Cache::put('user_cache_version', $currentVersion + 1, CacheKeys::CACHE_TTL);
67+
}
68+
3069
/**
3170
* Get the attributes that should be cast.
3271
*

app/Models/User.php

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace App\Models;
66

7+
use App\Traits\HasCachedRolesAndPermissions;
78
use Illuminate\Database\Eloquent\Factories\HasFactory;
89
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
910
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -44,7 +45,7 @@
4445
*/
4546
class User extends Authenticatable
4647
{
47-
use HasApiTokens, HasFactory, Notifiable;
48+
use HasApiTokens, HasCachedRolesAndPermissions, HasFactory, Notifiable;
4849

4950
/**
5051
* The attributes that are mass assignable.
@@ -76,6 +77,24 @@ class User extends Authenticatable
7677
'remember_token',
7778
];
7879

80+
/**
81+
* Boot the model and register event listeners
82+
*/
83+
protected static function boot(): void
84+
{
85+
parent::boot();
86+
87+
// Clear cache when user is updated
88+
static::updated(function (User $user) {
89+
$user->clearCache();
90+
});
91+
92+
// Clear cache when user is deleted
93+
static::deleted(function (User $user) {
94+
$user->clearCache();
95+
});
96+
}
97+
7998
/**
8099
* Get the attributes that should be cast.
81100
*
@@ -112,13 +131,53 @@ public function hasRole(string $role): bool
112131
return $this->roles->pluck('name')->contains($role);
113132
}
114133

134+
/**
135+
* Check if the user has any of the given roles.
136+
*
137+
* @param array<int, string> $roles
138+
*/
139+
public function hasAnyRole(array $roles): bool
140+
{
141+
$userRoles = $this->roles->pluck('name')->toArray();
142+
143+
return ! empty(array_intersect($roles, $userRoles));
144+
}
145+
146+
/**
147+
* Check if the user has all of the given roles.
148+
*
149+
* @param array<int, string> $roles
150+
*/
151+
public function hasAllRoles(array $roles): bool
152+
{
153+
$userRoles = $this->roles->pluck('name')->toArray();
154+
155+
return empty(array_diff($roles, $userRoles));
156+
}
157+
115158
/**
116159
* Check if the user has a given permission via their roles.
117160
*/
118161
public function hasPermission(string $permission): bool
119162
{
120163
try {
121-
// Load roles with permissions to avoid N+1 queries
164+
// Try to get from cache first (most efficient)
165+
if ($this->hasCachedPermission($permission)) {
166+
return true;
167+
}
168+
169+
// If cache miss, check if roles are already loaded
170+
if ($this->relationLoaded('roles')) {
171+
foreach ($this->roles as $role) {
172+
if ($role->relationLoaded('permissions')) {
173+
if ($role->permissions->contains('name', $permission)) {
174+
return true;
175+
}
176+
}
177+
}
178+
}
179+
180+
// Fallback to database query with eager loading
122181
$this->load('roles.permissions');
123182

124183
foreach ($this->roles as $role) {
@@ -142,6 +201,79 @@ public function hasPermission(string $permission): bool
142201
}
143202
}
144203

204+
/**
205+
* Check if the user has any of the given permissions.
206+
*
207+
* @param array<int, string> $permissions
208+
*/
209+
public function hasAnyPermission(array $permissions): bool
210+
{
211+
try {
212+
// Try to get from cache first
213+
if ($this->hasAnyCachedPermission($permissions)) {
214+
return true;
215+
}
216+
217+
// Fallback to database query
218+
$this->load('roles.permissions');
219+
220+
foreach ($this->roles as $role) {
221+
foreach ($role->permissions as $permission) {
222+
if (in_array($permission->name, $permissions, true)) {
223+
return true;
224+
}
225+
}
226+
}
227+
228+
return false;
229+
} catch (\Throwable $e) {
230+
\Log::error('hasAnyPermission error', [
231+
'user_id' => $this->id,
232+
'permissions' => $permissions,
233+
'error' => $e->getMessage(),
234+
]);
235+
236+
return false;
237+
}
238+
}
239+
240+
/**
241+
* Check if the user has all of the given permissions.
242+
*
243+
* @param array<int, string> $permissions
244+
*/
245+
public function hasAllPermissions(array $permissions): bool
246+
{
247+
try {
248+
// Try to get from cache first
249+
if ($this->hasAllCachedPermissions($permissions)) {
250+
return true;
251+
}
252+
253+
// Fallback to database query
254+
$this->load('roles.permissions');
255+
256+
$userPermissions = [];
257+
foreach ($this->roles as $role) {
258+
foreach ($role->permissions as $permission) {
259+
$userPermissions[] = $permission->name;
260+
}
261+
}
262+
263+
$userPermissions = array_unique($userPermissions);
264+
265+
return empty(array_diff($permissions, $userPermissions));
266+
} catch (\Throwable $e) {
267+
\Log::error('hasAllPermissions error', [
268+
'user_id' => $this->id,
269+
'permissions' => $permissions,
270+
'error' => $e->getMessage(),
271+
]);
272+
273+
return false;
274+
}
275+
}
276+
145277
/**
146278
* Get the articles created by the user.
147279
*

0 commit comments

Comments
 (0)