1
0
Files
breadcrumb-the-shire/core/Service/User/UserLifecycleRestoreService.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

179 lines
6.8 KiB
PHP

<?php
namespace MintyPHP\Service\User;
use MintyPHP\Repository\Support\DatabaseSessionRepository;
use MintyPHP\Repository\Support\RepoQuery;
use MintyPHP\Repository\User\UserReadRepositoryInterface;
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
use MintyPHP\Service\Audit\UserLifecycleAuditInterface;
class UserLifecycleRestoreService
{
public function __construct(
private readonly UserReadRepositoryInterface $userReadRepository,
private readonly UserWriteRepositoryInterface $userWriteRepository,
private readonly UserLifecycleAuditInterface $userLifecycleAuditService,
private readonly DatabaseSessionRepository $databaseSessionRepository,
private readonly UserSettingsGateway $userSettingsGateway
) {
}
public function restoreFromLifecycleAudit(int $auditId, int $actorUserId): array
{
if ($auditId <= 0 || $actorUserId <= 0) {
return ['ok' => false, 'error' => 'invalid_request'];
}
$transactionStarted = false;
try {
$this->databaseSessionRepository->beginTransaction();
$transactionStarted = true;
$event = $this->userLifecycleAuditService->findDeleteEventForRestore($auditId, true);
if (!$event) {
$this->rollbackQuietly();
return ['ok' => false, 'error' => 'audit_event_not_found'];
}
if (!empty($event['restored_at'])) {
$this->rollbackQuietly();
return ['ok' => false, 'error' => 'audit_event_already_restored'];
}
$snapshot = $this->userLifecycleAuditService->decryptSnapshot($event);
if (!$snapshot) {
$this->rollbackQuietly();
return ['ok' => false, 'error' => 'snapshot_unavailable'];
}
$uuid = trim((string) ($snapshot['uuid'] ?? ''));
$email = trim((string) ($snapshot['email'] ?? ''));
if ($uuid === '' || $email === '') {
$this->rollbackQuietly();
return ['ok' => false, 'error' => 'snapshot_invalid'];
}
if ($this->userReadRepository->findByUuid($uuid)) {
$this->rollbackQuietly();
return ['ok' => false, 'error' => 'restore_uuid_exists'];
}
if ($this->userReadRepository->findByEmail($email)) {
$this->rollbackQuietly();
return ['ok' => false, 'error' => 'restore_email_exists'];
}
$createdId = $this->userWriteRepository->create([
'uuid' => $uuid,
'first_name' => trim((string) ($snapshot['first_name'] ?? '')),
'last_name' => trim((string) ($snapshot['last_name'] ?? '')),
'email' => $email,
'profile_description' => $this->nullableText($snapshot['profile_description'] ?? null),
'job_title' => $this->nullableText($snapshot['job_title'] ?? null),
'phone' => $this->nullableText($snapshot['phone'] ?? null),
'mobile' => $this->nullableText($snapshot['mobile'] ?? null),
'short_dial' => $this->nullableText($snapshot['short_dial'] ?? null),
'address' => $this->nullableText($snapshot['address'] ?? null),
'postal_code' => $this->nullableText($snapshot['postal_code'] ?? null),
'city' => $this->nullableText($snapshot['city'] ?? null),
'country' => $this->nullableText($snapshot['country'] ?? null),
'region' => $this->nullableText($snapshot['region'] ?? null),
'hire_date' => $this->nullableDate($snapshot['hire_date'] ?? null),
'password' => $this->randomPassword(),
'locale' => $this->nullableText($snapshot['locale'] ?? null),
'totp_secret' => '',
'theme' => $this->normalizeTheme($snapshot['theme'] ?? null),
'primary_tenant_id' => null,
'current_tenant_id' => null,
'created_by' => $actorUserId,
'active' => 0,
'active_changed_at' => gmdate('Y-m-d H:i:s'),
'active_changed_by' => $actorUserId,
]);
if (!$createdId) {
$this->rollbackQuietly();
return ['ok' => false, 'error' => 'restore_create_failed'];
}
$restoredUserId = (int) $createdId;
$marked = $this->userLifecycleAuditService->markDeleteEventRestored(
$auditId,
$actorUserId,
$restoredUserId
);
if (!$marked) {
$this->rollbackQuietly();
return ['ok' => false, 'error' => 'restore_mark_failed'];
}
$this->userLifecycleAuditService->logRestore(
RepoQuery::uuidV4(),
'manual',
[
'deactivate_days' => (int) ($event['policy_deactivate_days'] ?? 0),
'delete_days' => (int) ($event['policy_delete_days'] ?? 0),
],
$actorUserId,
[
'id' => $restoredUserId,
'uuid' => $uuid,
'email' => $email,
],
'success',
null
);
$this->databaseSessionRepository->commitTransaction();
$transactionStarted = false;
return [
'ok' => true,
'restored_user_id' => $restoredUserId,
'restored_user_uuid' => $uuid,
'audit_id' => $auditId,
];
} catch (\Throwable $exception) {
if ($transactionStarted) {
$this->rollbackQuietly();
}
return ['ok' => false, 'error' => 'restore_unexpected_error'];
}
}
private function rollbackQuietly(): void
{
try {
$this->databaseSessionRepository->rollbackTransaction();
} catch (\Throwable $exception) {
// no-op
}
}
private function nullableText(mixed $value): ?string
{
$value = trim((string) $value);
return $value !== '' ? $value : null;
}
private function nullableDate(mixed $value): ?string
{
$value = trim((string) $value);
if ($value === '') {
return null;
}
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
return null;
}
return $value;
}
private function randomPassword(): string
{
return bin2hex(random_bytes(24));
}
private function normalizeTheme(mixed $value): string
{
$theme = is_scalar($value) ? (string) $value : null;
return $this->userSettingsGateway->normalizeTheme($theme);
}
}