Files
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

186 lines
6.1 KiB
PHP

<?php
namespace MintyPHP\Service\Access;
use MintyPHP\Repository\Access\RoleRepositoryInterface;
use MintyPHP\Service\Audit\AuditRecorderInterface;
use MintyPHP\Service\Directory\DirectorySettingsGateway;
class RoleService
{
public function __construct(
private readonly RoleRepositoryInterface $roleRepository,
private readonly DirectorySettingsGateway $settingsGateway,
private readonly AuditRecorderInterface $systemAuditService
) {
}
public function list(): array
{
return $this->roleRepository->list();
}
public function listActive(): array
{
return $this->roleRepository->listActive();
}
public function listPaged(array $options): array
{
return $this->roleRepository->listPaged($options);
}
public function findByUuid(string $uuid): ?array
{
return $this->roleRepository->findByUuid($uuid);
}
public function findById(int $id): ?array
{
return $this->roleRepository->find($id);
}
public function createFromAdmin(array $input, int $currentUserId = 0): array
{
$form = $this->sanitizeBase($input);
$errors = $this->validateBase($form);
$warnings = $this->warningsForCode($form, 0);
if ($errors) {
return ['ok' => false, 'errors' => $errors, 'warnings' => $warnings, 'form' => $form];
}
$createdId = $this->roleRepository->create([
'description' => $form['description'],
'code' => $form['code'] ?: null,
'active' => $form['active'],
'created_by' => $currentUserId > 0 ? $currentUserId : null,
]);
if (!$createdId) {
return ['ok' => false, 'errors' => [t('Role can not be created')], 'form' => $form];
}
$createdRole = $this->roleRepository->find((int) $createdId);
$uuid = $createdRole['uuid'] ?? null;
if (!empty($input['is_default'])) {
$this->settingsGateway->setDefaultRoleId((int) $createdId);
}
$this->systemAuditService->record('admin.roles.create', 'success', [
'actor_user_id' => $currentUserId > 0 ? $currentUserId : null,
'target_type' => 'role',
'target_id' => (int) $createdId,
'target_uuid' => is_string($uuid) ? $uuid : '',
'metadata' => ['active' => $form['active']],
]);
return ['ok' => true, 'form' => $form, 'warnings' => $warnings, 'uuid' => $uuid, 'id' => (int) $createdId];
}
public function updateFromAdmin(int $roleId, array $input, int $currentUserId = 0): array
{
$form = $this->sanitizeBase($input);
$errors = $this->validateBase($form);
$warnings = $this->warningsForCode($form, $roleId);
if ($errors) {
return ['ok' => false, 'errors' => $errors, 'warnings' => $warnings, 'form' => $form];
}
$existingBefore = $this->roleRepository->find($roleId) ?? [];
$updated = $this->roleRepository->update($roleId, [
'description' => $form['description'],
'code' => $form['code'] ?: null,
'active' => $form['active'],
'modified_by' => $currentUserId > 0 ? $currentUserId : null,
]);
if (!$updated) {
return ['ok' => false, 'errors' => [t('Role can not be updated')], 'form' => $form];
}
$this->systemAuditService->record('admin.roles.update', 'success', [
'actor_user_id' => $currentUserId > 0 ? $currentUserId : null,
'target_type' => 'role',
'target_id' => $roleId,
'target_uuid' => (string) ($existingBefore['uuid'] ?? ''),
'before' => ['active' => $existingBefore['active'] ?? null],
'after' => ['active' => $form['active']],
]);
return ['ok' => true, 'form' => $form, 'warnings' => $warnings];
}
public function deleteByUuid(string $uuid): array
{
$uuid = trim($uuid);
if ($uuid === '') {
return ['ok' => false, 'status' => 404, 'error' => 'not_found'];
}
$role = $this->roleRepository->findByUuid($uuid);
if (!$role || !isset($role['id'])) {
return ['ok' => false, 'status' => 404, 'error' => 'not_found'];
}
if (($role['description'] ?? '') === 'Admin') {
return ['ok' => false, 'status' => 403, 'error' => 'admin_role_protected'];
}
$deleted = $this->roleRepository->delete((int) $role['id']);
if (!$deleted) {
return ['ok' => false, 'status' => 500, 'error' => 'delete_failed'];
}
$this->systemAuditService->record('admin.roles.delete', 'success', [
'target_type' => 'role',
'target_id' => (int) $role['id'],
'target_uuid' => (string) ($role['uuid'] ?? ''),
]);
return ['ok' => true, 'role' => $role];
}
private function sanitizeBase(array $input): array
{
return [
'description' => trim((string) ($input['description'] ?? '')),
'code' => trim((string) ($input['code'] ?? '')),
'active' => $this->normalizeActive($input['active'] ?? 1),
];
}
private function validateBase(array $form): array
{
$errors = [];
if ($form['description'] === '') {
$errors[] = t('Description cannot be empty');
}
return $errors;
}
// Duplicate codes are a warning, not a hard error — codes are optional identifiers,
// not unique keys, so duplicates are allowed but surfaced to the user.
private function warningsForCode(array $form, int $excludeId): array
{
$warnings = [];
$code = $form['code'] ?? '';
if ($code !== '' && $this->roleRepository->existsByCode($code, $excludeId)) {
$warnings[] = t('Role code already exists');
}
return $warnings;
}
private function normalizeActive($value): int
{
$value = strtolower(trim((string) $value));
if (in_array($value, ['0', 'false', 'inactive'], true)) {
return 0;
}
return 1;
}
}