TenantAtlas/app/Services/Onboarding/OnboardingDraftMutationService.php
2026-03-19 00:00:32 +01:00

184 lines
6.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Onboarding;
use App\Exceptions\Onboarding\OnboardingDraftConflictException;
use App\Exceptions\Onboarding\OnboardingDraftImmutableException;
use App\Models\TenantOnboardingSession;
use App\Models\User;
use App\Models\Workspace;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class OnboardingDraftMutationService
{
public function __construct(
private readonly OnboardingLifecycleService $lifecycleService,
) {}
/**
* @param callable(TenantOnboardingSession):void $mutator
*/
public function createOrResume(
Workspace $workspace,
User $actor,
string $entraTenantId,
callable $mutator,
?TenantOnboardingSession $preferredDraft = null,
?int $expectedVersion = null,
bool $incrementVersion = true,
?bool &$wasCreated = null,
): TenantOnboardingSession {
return DB::transaction(function () use ($workspace, $actor, $entraTenantId, $mutator, $preferredDraft, $expectedVersion, $incrementVersion, &$wasCreated): TenantOnboardingSession {
$draft = $this->resolveDraftForIdentity($workspace, $entraTenantId, $preferredDraft);
$isNew = ! $draft instanceof TenantOnboardingSession;
$wasCreated = $isNew;
if ($isNew) {
$draft = new TenantOnboardingSession;
$draft->workspace_id = (int) $workspace->getKey();
$draft->entra_tenant_id = $entraTenantId;
$draft->started_by_user_id = (int) $actor->getKey();
$draft->version = 0;
} elseif ($expectedVersion !== null) {
$this->assertExpectedVersion($draft, $expectedVersion);
}
$draft->entra_tenant_id = $entraTenantId;
$draft->updated_by_user_id = (int) $actor->getKey();
$mutator($draft);
$this->persistDraft(
draft: $draft,
incrementVersion: $incrementVersion || $isNew,
);
return $draft->refresh();
});
}
/**
* @param callable(TenantOnboardingSession):void $mutator
*/
public function mutate(
TenantOnboardingSession $draft,
User $actor,
callable $mutator,
?int $expectedVersion = null,
bool $incrementVersion = true,
bool $allowTerminal = false,
): TenantOnboardingSession {
return DB::transaction(function () use ($draft, $actor, $mutator, $expectedVersion, $incrementVersion, $allowTerminal): TenantOnboardingSession {
$lockedDraft = TenantOnboardingSession::query()
->whereKey($draft->getKey())
->lockForUpdate()
->firstOrFail();
if (! $allowTerminal && $lockedDraft->lifecycleState()->isTerminal()) {
throw new OnboardingDraftImmutableException(
draftId: (int) $lockedDraft->getKey(),
lifecycleState: $lockedDraft->lifecycleState(),
);
}
if ($expectedVersion !== null) {
$this->assertExpectedVersion($lockedDraft, $expectedVersion);
}
$lockedDraft->updated_by_user_id = (int) $actor->getKey();
$mutator($lockedDraft);
$this->persistDraft(
draft: $lockedDraft,
incrementVersion: $incrementVersion,
);
return $lockedDraft->refresh();
});
}
public function lockForTrustedMutation(TenantOnboardingSession|int|string $draft, Workspace $workspace): TenantOnboardingSession
{
$draftId = $draft instanceof TenantOnboardingSession
? (int) $draft->getKey()
: (int) $draft;
$lockedDraft = TenantOnboardingSession::query()
->whereKey($draftId)
->where('workspace_id', (int) $workspace->getKey())
->lockForUpdate()
->first();
if (! $lockedDraft instanceof TenantOnboardingSession) {
throw new NotFoundHttpException;
}
return $lockedDraft;
}
private function resolveDraftForIdentity(
Workspace $workspace,
string $entraTenantId,
?TenantOnboardingSession $preferredDraft = null,
): ?TenantOnboardingSession {
if ($preferredDraft instanceof TenantOnboardingSession) {
$lockedPreferredDraft = TenantOnboardingSession::query()
->whereKey($preferredDraft->getKey())
->where('workspace_id', (int) $workspace->getKey())
->lockForUpdate()
->first();
if ($lockedPreferredDraft instanceof TenantOnboardingSession && $lockedPreferredDraft->entra_tenant_id === $entraTenantId) {
if ($lockedPreferredDraft->lifecycleState()->isTerminal()) {
throw new OnboardingDraftImmutableException(
draftId: (int) $lockedPreferredDraft->getKey(),
lifecycleState: $lockedPreferredDraft->lifecycleState(),
);
}
return $lockedPreferredDraft;
}
}
return TenantOnboardingSession::query()
->where('workspace_id', (int) $workspace->getKey())
->where('entra_tenant_id', $entraTenantId)
->resumable()
->orderByDesc('updated_at')
->lockForUpdate()
->first();
}
private function assertExpectedVersion(TenantOnboardingSession $draft, int $expectedVersion): void
{
$actualVersion = max(1, (int) ($draft->version ?? 1));
if ($expectedVersion === $actualVersion) {
return;
}
throw new OnboardingDraftConflictException(
draftId: (int) $draft->getKey(),
expectedVersion: $expectedVersion,
actualVersion: $actualVersion,
);
}
private function persistDraft(TenantOnboardingSession $draft, bool $incrementVersion): void
{
$currentVersion = max(0, (int) ($draft->version ?? 0));
$draft->version = $incrementVersion
? $currentVersion + 1
: max(1, $currentVersion);
$this->lifecycleService->applySnapshot($draft, false);
$draft->save();
}
}