Skip to content

Commit 1330a65

Browse files
authored
feat!: use assignments to seed roles and permissions without code-fir… (#29)
Refactored the role/permission assignments configuration to make it work independently of code-first mode. The assignments configuration is moved from mandate.code_first.assignments to mandate.assignments at the top level, and the --seed flag now works even when code-first is disabled. Changes: - Moved assignments configuration from mandate.code_first.assignments to top-level mandate.assignments - Enabled --seed flag to work without code-first mode enabled (seed-only mode) - Updated command logic to skip code-first syncing when running in seed-only mode
1 parent 8df42b4 commit 1330a65

File tree

5 files changed

+289
-65
lines changed

5 files changed

+289
-65
lines changed

README.md

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,30 +1076,33 @@ The sync is **additive only** — it never deletes database records to prevent d
10761076

10771077
### Seeding Role Assignments
10781078

1079-
Configure role-permission assignments in the config file:
1079+
Configure role-permission assignments in the config file. This works with **both code-first and database-only workflows**:
10801080

10811081
```php
10821082
// config/mandate.php
1083-
'code_first' => [
1084-
'enabled' => true,
1085-
'assignments' => [
1086-
'admin' => [
1087-
'permissions' => ['article:*', 'user:*'],
1088-
'capabilities' => ['content-management'],
1089-
],
1090-
'editor' => [
1091-
'permissions' => ['article:view', 'article:edit'],
1092-
],
1083+
'assignments' => [
1084+
'admin' => [
1085+
'permissions' => ['article:*', 'user:*'],
1086+
'capabilities' => ['content-management'],
1087+
],
1088+
'editor' => [
1089+
'permissions' => ['article:view', 'article:edit'],
10931090
],
10941091
],
10951092
```
10961093

1097-
Then sync with the `--seed` flag:
1094+
Then seed with the `--seed` flag:
10981095

10991096
```bash
11001097
php artisan mandate:sync --seed
11011098
```
11021099

1100+
The `--seed` flag will **automatically create** any roles, permissions, or capabilities that don't exist in the database, then assign permissions to roles as configured. This makes it easy to define your entire RBAC structure in config.
1101+
1102+
**Behavior based on code-first setting:**
1103+
- **Code-first enabled**: Syncs PHP class definitions to database first, then seeds assignments
1104+
- **Code-first disabled**: Only seeds assignments (useful for database-only workflows)
1105+
11031106
### Label and Description Columns
11041107

11051108
To store labels and descriptions in the database, publish and run the metadata migration:
@@ -1247,6 +1250,12 @@ Event::listen(MandateSynced::class, function ($event) {
12471250
});
12481251
```
12491252

1253+
### Assignments Configuration
1254+
1255+
| Option | Default | Description |
1256+
|---------------|---------|----------------------------------------------------------------------------|
1257+
| `assignments` | `[]` | Role-permission/capability assignments (works with or without code-first) |
1258+
12501259
### Code-First Configuration Options
12511260

12521261
| Option | Default | Description |
@@ -1255,7 +1264,6 @@ Event::listen(MandateSynced::class, function ($event) {
12551264
| `code_first.paths.permissions` | `app_path('Permissions')` | Directory to scan for permission classes |
12561265
| `code_first.paths.roles` | `app_path('Roles')` | Directory to scan for role classes |
12571266
| `code_first.paths.capabilities` | `app_path('Capabilities')` | Directory to scan for capability classes |
1258-
| `code_first.assignments` | `[]` | Role-permission/capability assignments |
12591267
| `code_first.typescript_path` | `resource_path('js/types/mandate.ts')` | Default output path for TypeScript types |
12601268
| `feature_generator` | `null` | Custom feature generator class |
12611269

UPGRADE.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,14 @@ return [
185185
```php
186186
// config/mandate.php
187187
return [
188+
// Role-permission assignments (works with or without code-first)
189+
'assignments' => [
190+
'admin' => [
191+
'permissions' => ['user:*', 'article:*'],
192+
'capabilities' => ['user-management'],
193+
],
194+
],
195+
188196
// Code-first is now optional (disabled by default)
189197
'code_first' => [
190198
'enabled' => false,
@@ -193,12 +201,6 @@ return [
193201
'roles' => app_path('Roles'),
194202
'capabilities' => app_path('Capabilities'),
195203
],
196-
'assignments' => [
197-
'admin' => [
198-
'permissions' => ['user:*', 'article:*'],
199-
'capabilities' => ['user-management'],
200-
],
201-
],
202204
'typescript_path' => resource_path('js/types/mandate.ts'),
203205
],
204206

config/mandate.php

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,27 @@
7878
'on_missing_handler' => 'deny',
7979
],
8080

81+
/*
82+
|--------------------------------------------------------------------------
83+
| Role Assignments
84+
|--------------------------------------------------------------------------
85+
|
86+
| Define role-permission and role-capability assignments for seeding.
87+
| Use `php artisan mandate:sync --seed` to apply these assignments.
88+
| This works with both code-first and database-only workflows.
89+
|
90+
*/
91+
92+
'assignments' => [
93+
// 'admin' => [
94+
// 'permissions' => ['user:*', 'article:*'],
95+
// 'capabilities' => ['user-management'],
96+
// ],
97+
// 'editor' => [
98+
// 'permissions' => ['article:view', 'article:edit'],
99+
// ],
100+
],
101+
81102
/*
82103
|--------------------------------------------------------------------------
83104
| Code-First Definitions
@@ -89,7 +110,6 @@
89110
|
90111
| enabled: Whether code-first mode is active
91112
| paths: Directories to scan for definition classes
92-
| assignments: Role-permission and role-capability assignments for seeding
93113
|
94114
*/
95115

@@ -102,16 +122,6 @@
102122
'capabilities' => app_path('Capabilities'),
103123
],
104124

105-
'assignments' => [
106-
// 'admin' => [
107-
// 'permissions' => ['user:*', 'article:*'],
108-
// 'capabilities' => ['user-management'],
109-
// ],
110-
// 'editor' => [
111-
// 'permissions' => ['article:view', 'article:edit'],
112-
// ],
113-
],
114-
115125
'typescript_path' => resource_path('js/types/mandate.ts'),
116126
],
117127

src/Commands/SyncCommand.php

Lines changed: 67 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,14 @@ public function handle(
5454
DefinitionCache $cache,
5555
MandateRegistrar $registrar
5656
): int {
57-
if (! config('mandate.code_first.enabled', false)) {
57+
$codeFirstEnabled = config('mandate.code_first.enabled', false);
58+
$seedOnly = $this->option('seed')
59+
&& ! $this->option('permissions')
60+
&& ! $this->option('roles')
61+
&& ! $this->option('capabilities');
62+
63+
// Allow --seed to work without code-first enabled
64+
if (! $codeFirstEnabled && ! $seedOnly) {
5865
$this->components->error('Code-first mode is not enabled. Set mandate.code_first.enabled to true.');
5966

6067
return self::FAILURE;
@@ -71,16 +78,16 @@ public function handle(
7178
$isDryRun = (bool) $this->option('dry-run');
7279
/** @var string|null $guard */
7380
$guard = $this->option('guard') ?: null;
74-
$syncAll = ! $this->option('permissions') && ! $this->option('roles') && ! $this->option('capabilities');
81+
$syncAll = ! $this->option('permissions') && ! $this->option('roles') && ! $this->option('capabilities') && ! $seedOnly;
7582

7683
if ($isDryRun) {
7784
$this->components->info('Dry run mode - no changes will be made.');
7885
}
7986

80-
// Determine what to sync
81-
$syncPermissions = $syncAll || $this->option('permissions');
82-
$syncRoles = $syncAll || $this->option('roles');
83-
$syncCapabilities = ($syncAll || $this->option('capabilities')) && config('mandate.capabilities.enabled', false);
87+
// Determine what to sync (skip if seed-only mode)
88+
$syncPermissions = $codeFirstEnabled && ($syncAll || $this->option('permissions'));
89+
$syncRoles = $codeFirstEnabled && ($syncAll || $this->option('roles'));
90+
$syncCapabilities = $codeFirstEnabled && ($syncAll || $this->option('capabilities')) && config('mandate.capabilities.enabled', false);
8491

8592
// Discover and sync
8693
if ($syncPermissions) {
@@ -414,7 +421,7 @@ private function syncCapability(
414421
*/
415422
private function seedAssignments(?string $guard): void
416423
{
417-
$assignments = config('mandate.code_first.assignments', []);
424+
$assignments = config('mandate.assignments', []);
418425

419426
if (empty($assignments)) {
420427
return;
@@ -438,28 +445,43 @@ private function seedAssignments(?string $guard): void
438445
->where('guard', $roleGuard)
439446
->first();
440447

441-
if (! $role) {
442-
$this->components->warn("Role '{$roleName}' not found, skipping assignments.");
443-
444-
continue;
448+
if ($role === null) {
449+
$role = $roleClass::create([
450+
'name' => $roleName,
451+
'guard' => $roleGuard,
452+
]);
453+
$this->components->twoColumnDetail(
454+
' <fg=green>Created role</>',
455+
$roleName
456+
);
445457
}
446458

447459
// Sync permissions
448460
if (! empty($assignment['permissions'])) {
449461
/** @var array<string> $permissionNames */
450462
$permissionNames = $assignment['permissions'];
451-
$permissionIds = collect($permissionNames)
452-
->map(function (string $name) use ($permissionClass, $roleGuard) {
453-
/** @var Permission|null $permission */
454-
$permission = $permissionClass::query()
455-
->where('name', $name)
456-
->where('guard', $roleGuard)
457-
->first();
458-
459-
return $permission?->getKey();
460-
})
461-
->filter()
462-
->all();
463+
$permissionIds = [];
464+
465+
foreach ($permissionNames as $permissionName) {
466+
/** @var Permission|null $permission */
467+
$permission = $permissionClass::query()
468+
->where('name', $permissionName)
469+
->where('guard', $roleGuard)
470+
->first();
471+
472+
if ($permission === null) {
473+
$permission = $permissionClass::create([
474+
'name' => $permissionName,
475+
'guard' => $roleGuard,
476+
]);
477+
$this->components->twoColumnDetail(
478+
' <fg=green>Created permission</>',
479+
$permissionName
480+
);
481+
}
482+
483+
$permissionIds[] = $permission->getKey();
484+
}
463485

464486
if (! empty($permissionIds)) {
465487
$role->permissions()->syncWithoutDetaching($permissionIds);
@@ -474,18 +496,28 @@ private function seedAssignments(?string $guard): void
474496
if (config('mandate.capabilities.enabled', false) && ! empty($assignment['capabilities'])) {
475497
/** @var array<string> $capabilityNames */
476498
$capabilityNames = $assignment['capabilities'];
477-
$capabilityIds = collect($capabilityNames)
478-
->map(function (string $name) use ($capabilityClass, $roleGuard) {
479-
/** @var Capability|null $capability */
480-
$capability = $capabilityClass::query()
481-
->where('name', $name)
482-
->where('guard', $roleGuard)
483-
->first();
484-
485-
return $capability?->getKey();
486-
})
487-
->filter()
488-
->all();
499+
$capabilityIds = [];
500+
501+
foreach ($capabilityNames as $capabilityName) {
502+
/** @var Capability|null $capability */
503+
$capability = $capabilityClass::query()
504+
->where('name', $capabilityName)
505+
->where('guard', $roleGuard)
506+
->first();
507+
508+
if ($capability === null) {
509+
$capability = $capabilityClass::create([
510+
'name' => $capabilityName,
511+
'guard' => $roleGuard,
512+
]);
513+
$this->components->twoColumnDetail(
514+
' <fg=green>Created capability</>',
515+
$capabilityName
516+
);
517+
}
518+
519+
$capabilityIds[] = $capability->getKey();
520+
}
489521

490522
if (! empty($capabilityIds)) {
491523
$role->capabilities()->syncWithoutDetaching($capabilityIds);

0 commit comments

Comments
 (0)