*/ private const DISPLAY_FIELDS = ['id', 'name', 'description', 'created_at', 'permissions']; /** @return array{name: string, description: string|null} */ public function normalize(array $record): array { $name = $record['name'] ?? null; $description = $record['description'] ?? null; return [ 'name' => is_scalar($name) ? trim((string) $name) : '', 'description' => $this->normalizeDescription($description), ]; } /** @return array{name: string, description: string|null, valid: bool, errors: array} */ public function validate(array $record): array { $normalized = $this->normalize($record); $errors = []; if ($normalized['name'] === '') { $errors['name'] = 'Role name is required.'; } elseif (mb_strlen($normalized['name']) > 80) { $errors['name'] = 'Role name must be 80 characters or fewer.'; } elseif (preg_match('/[\x00-\x1F\x7F]/', $normalized['name']) === 1) { $errors['name'] = 'Role name contains invalid control characters.'; } if ($normalized['description'] !== null && mb_strlen($normalized['description']) > 255) { $errors['description'] = 'Role description must be 255 characters or fewer.'; } return [...$normalized, 'valid' => $errors === [], 'errors' => $errors]; } public function isAdministrator(array $record): bool { return $this->canonicalName($record['name'] ?? null) === self::ADMINISTRATOR; } public function canRename(array $record, mixed $newName): bool { return !$this->isAdministrator($record) && $this->canonicalName($newName) !== self::ADMINISTRATOR; } public function canDelete(array $record): bool { return !$this->isAdministrator($record); } public function canChangePermissions(array $record): bool { return !$this->isAdministrator($record); } public function assertCanRename(array $record, mixed $newName): void { if (!$this->canRename($record, $newName)) { throw new LogicException('The protected Administrator role cannot be renamed.'); } } public function assertCanDelete(array $record): void { if (!$this->canDelete($record)) { throw new LogicException('The protected Administrator role cannot be deleted.'); } } public function assertCanChangePermissions(array $record): void { if (!$this->canChangePermissions($record)) { throw new LogicException('The protected Administrator role permissions cannot be changed.'); } } /** @return array */ public function display(array $record): array { $safe = []; foreach (self::DISPLAY_FIELDS as $field) { if (array_key_exists($field, $record)) { $safe[$field] = $field === 'permissions' && is_array($record[$field]) ? (new PermissionMatrix())->normalize($record[$field]) : $record[$field]; } } return $safe; } /** @return array */ public function toDisplay(array $record): array { return $this->display($record); } private function normalizeDescription(mixed $value): ?string { if (!is_scalar($value)) { return null; } $value = trim((string) $value); return $value === '' ? null : $value; } private function canonicalName(mixed $value): string { return is_scalar($value) ? strtolower(trim((string) $value)) : ''; } }