1
0
Files
breadcrumb-the-shire/core/Service/Access/AssignableRoleService.php
fs 0e86925464 refactor: rename lib/ to core/ for clearer core-module separation
Rename the top-level lib/ directory to core/ so the project root
immediately communicates which code is core platform and which lives
in modules/. PHP namespaces (MintyPHP\*) are unchanged — only the
Composer PSR-4 path mapping moves from lib/ to core/.

Module-internal lib/ directories (modules/*/lib/) are untouched.

Updated across the full stack:
- composer.json PSR-4 mapping
- bootstrap entry points (web/index.php, bin/*, tests/bootstrap.php)
- tooling configs (phpstan.neon, phpunit.xml, php-cs-fixer)
- 26 architecture contract tests
- enforcement-policy, guard-catalog, quality-gates
- all documentation (CLAUDE.md, README, 25 docs/, .agents/skills/)
- bin/qa-extended.sh search paths
- .claude/settings.local.json permission paths

Workflow: .agents/runs/CORE-LIB-RENAME-001/ (Analyst → Planner →
Executor → Code Review (4 findings fixed) → Security Review →
Acceptance Test → Finalizer)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 23:20:42 +02:00

102 lines
3.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace MintyPHP\Service\Access;
use MintyPHP\Repository\Access\RoleAssignableRoleRepositoryInterface;
use MintyPHP\Repository\Access\RoleRepositoryInterface;
use MintyPHP\Repository\Access\UserRoleRepositoryInterface;
use MintyPHP\Service\Audit\AuditRecorderInterface;
class AssignableRoleService
{
public function __construct(
private readonly RoleAssignableRoleRepositoryInterface $assignableRoleRepository,
private readonly UserRoleRepositoryInterface $userRoleRepository,
private readonly PermissionService $permissionService,
private readonly RoleRepositoryInterface $roleRepository,
private readonly AuditRecorderInterface $systemAuditService
) {
}
/**
* Returns role IDs the given actor is allowed to assign.
* Users with 'roles.assign_all' get all active role IDs.
* Otherwise: union of assignable_role_ids from all of the actor's roles.
*
* @return int[]
*/
public function listAssignableRoleIdsForActor(int $actorUserId): array
{
if ($actorUserId <= 0) {
return [];
}
if ($this->permissionService->userHas($actorUserId, PermissionService::ROLES_ASSIGN_ALL)) {
return $this->roleRepository->listActiveIds();
}
$actorRoleIds = $this->userRoleRepository->listRoleIdsByUserId($actorUserId);
if (!$actorRoleIds) {
return [];
}
return $this->assignableRoleRepository->listAssignableRoleIdsByRoleIds($actorRoleIds);
}
/**
* Computes the effective role set respecting the actor's assignable scope.
*
* Roles the actor cannot assign are "frozen" — they remain as-is.
* frozen = currentIds ∩ NOT-assignable
* touched = submittedIds ∩ assignable
* result = frozen touched
*
* @param int[] $submittedIds Role IDs the form submitted
* @param int[] $currentIds Role IDs the target user currently has
* @return int[]
*/
public function computeEffectiveRoleIds(int $actorUserId, array $submittedIds, array $currentIds): array
{
$assignable = $this->listAssignableRoleIdsForActor($actorUserId);
$assignableMap = array_fill_keys($assignable, true);
// Roles the actor cannot touch — keep them as they are.
$frozen = array_values(array_filter(
$currentIds,
static fn (int $id): bool => !isset($assignableMap[$id])
));
// From the submitted set, only keep roles the actor is allowed to assign.
$touched = array_values(array_filter(
$submittedIds,
static fn (int $id): bool => isset($assignableMap[$id])
));
return array_values(array_unique(array_merge($frozen, $touched)));
}
/**
* Saves which roles this role can assign (admin UI) and logs the change.
*
* @param int[] $assignableRoleIds
*/
public function replaceAssignableRoles(int $roleId, array $assignableRoleIds, int $actorUserId): bool
{
$beforeIds = $this->assignableRoleRepository->listAssignableRoleIdsByRoleId($roleId);
$result = $this->assignableRoleRepository->replaceForRole($roleId, $assignableRoleIds);
$afterIds = $this->assignableRoleRepository->listAssignableRoleIdsByRoleId($roleId);
if ($beforeIds !== $afterIds) {
$this->systemAuditService->record('admin.roles.assignable_roles.update', 'success', [
'actor_user_id' => $actorUserId > 0 ? $actorUserId : null,
'target_type' => 'role',
'target_id' => $roleId,
'before' => ['assignable_role_ids' => $beforeIds],
'after' => ['assignable_role_ids' => $afterIds],
]);
}
return $result;
}
}