AccountDeletionManager
in package
Account deletion requested by the user — the whole flow in one place:
- request — the owner confirms with the password (self::requestDeletion()); an account without a password (OAuth) gets a confirmation link instead (self::confirmDeletion()),
- grace period (
account.deletion.graceDays) — every session is revoked, sign-in is refused with a short-lived ticket (self::buildPendingSignInRefusal()), the deletion can be cancelled from the e-mail, after a sign-in, or by the administration (self::cancelDeletion()), - anonymization by the cron (self::anonymizeDue()) or by the administration at once: every module step (AccountAnonymizationStepInterface) and then the account row.
Manager tier (process orchestrator): it owns no table, the cms_system_users writes go through
UserManager. Every state change is written to the accountAudit log channel (user id and
the way it happened, never the e-mail).
Table of Contents
Constants
- AUDIT_CHANNEL : string = 'accountAudit'
- DELETED_LABEL_KEY : string = 'front.general.user.deleted'
- Translation of the author name shown instead of a deleted account.
- MAX_CONSECUTIVE_FAILURES : int = 5
- The cron stops the batch after this many failed accounts in a row (database outage, not a bad account).
- VIA_ADMIN : string = 'admin'
- VIA_CRON : string = 'cron'
- VIA_EMAIL : string = 'email'
- VIA_PASSWORD : string = 'password'
- VIA_TICKET : string = 'sign-in'
Properties
- $accountDeletionTicket : AccountDeletionTicket
- $anonymizationSteps : array<string|int, mixed>
- $passwordHasher : PasswordHasher
- $translator : Translator
- $userExternalLoginManager : UserExternalLoginManager
- $userManager : UserManager
- $userRefreshTokenManager : UserRefreshTokenManager
Methods
- __construct() : mixed
- anonymizeDue() : array{anonymized: int, failed: int, graceDays: int, aborted: bool}
- Cron — anonymizes accounts whose grace period has passed (oldest requests first). Every account runs in isolation: an exception (or a failed result) is logged by user id only, counted in `failed` and the batch goes on, so the account is retried by the next run. After {@see self::MAX_CONSECUTIVE_FAILURES} failures in a row the batch stops (most likely a database outage) and `aborted` is `true`.
- anonymizeNow() : Result
- Administration: anonymize a pending account at once instead of waiting for the grace period.
- buildPendingSignInRefusal() : Result
- Sign-in refused because the deletion is pending (password and OAuth sign-in, token refresh).
- cancelDeletion() : Result
- Cancels a pending deletion — by the cancel token from the e-mail, or by the ticket handed out at a refused sign-in (`<userId>.<expires>.<hmac>`). Unknown, used and expired keys share one answer (`USER_DELETION_KEY_INVALID`): the action is public.
- cancelDeletionByAdmin() : Result
- Administration: cancel the pending deletion of an account.
- confirmDeletion() : Result
- The confirmation link of an account without a password was clicked — the deletion starts.
- getDeletedUserLabel() : string
- "Deleted user" in the current language — shown instead of the author of a deleted account.
- requestDeletion() : Result
- The signed-in owner asks for the deletion of their own account. `$userId` must come from the verified identity (session / JWT), never from the request.
- anonymize() : Result
- IMPORTANT: a plain read-then-write by id would overwrite a cancel confirmed in between (e-mail, ticket, admin) and anonymize a restored account. Therefore: 1. atomic claim of exactly the request read (`deletion_requested_at` + nonce) — a cancel is refused from this moment (until the claim goes stale, {@see AccountDeletion::CLAIM_STALE_SECONDS}), 2. the claim is re-checked before every irreversible step; a lost claim stops the run, 3. the final overwrite of the row is one UPDATE guarded by the claim.
- audit() : void
- claimLost() : Result
- The run lost its claim (cancelled after it went stale, new request, another run) — no further step.
- clearRequest() : Result
- IMPORTANT: compare-and-set on the request read by the caller — refused (`$refusedCode`, nothing written) when an anonymization holds a fresh claim, the account was anonymized or the request changed in the meantime. OK means the account IS restored.
- findByToken() : User|null
- Account whose stored deletion token hash matches the plain token (fresh from the DB).
- requestEmailConfirmation() : Result
- startDeletion() : Result
Constants
AUDIT_CHANNEL
public
string
AUDIT_CHANNEL
= 'accountAudit'
DELETED_LABEL_KEY
Translation of the author name shown instead of a deleted account.
public
string
DELETED_LABEL_KEY
= 'front.general.user.deleted'
MAX_CONSECUTIVE_FAILURES
The cron stops the batch after this many failed accounts in a row (database outage, not a bad account).
public
int
MAX_CONSECUTIVE_FAILURES
= 5
VIA_ADMIN
public
string
VIA_ADMIN
= 'admin'
VIA_CRON
public
string
VIA_CRON
= 'cron'
VIA_EMAIL
public
string
VIA_EMAIL
= 'email'
VIA_PASSWORD
public
string
VIA_PASSWORD
= 'password'
VIA_TICKET
public
string
VIA_TICKET
= 'sign-in'
Properties
$accountDeletionTicket read-only
private
AccountDeletionTicket
$accountDeletionTicket
$anonymizationSteps read-only
private
array<string|int, mixed>
$anonymizationSteps
$passwordHasher read-only
private
PasswordHasher
$passwordHasher
$translator read-only
private
Translator
$translator
$userExternalLoginManager read-only
private
UserExternalLoginManager
$userExternalLoginManager
$userManager read-only
private
UserManager
$userManager
$userRefreshTokenManager read-only
private
UserRefreshTokenManager
$userRefreshTokenManager
Methods
__construct()
public
__construct(UserManager $userManager, UserRefreshTokenManager $userRefreshTokenManager, UserExternalLoginManager $userExternalLoginManager, PasswordHasher $passwordHasher, AccountDeletionTicket $accountDeletionTicket, Translator $translator, array<string|int, AccountAnonymizationStepInterface> $anonymizationSteps) : mixed
Parameters
- $userManager : UserManager
- $userRefreshTokenManager : UserRefreshTokenManager
- $userExternalLoginManager : UserExternalLoginManager
- $passwordHasher : PasswordHasher
- $accountDeletionTicket : AccountDeletionTicket
- $translator : Translator
- $anonymizationSteps : array<string|int, AccountAnonymizationStepInterface>
-
every module's share (DI collects them)
anonymizeDue()
Cron — anonymizes accounts whose grace period has passed (oldest requests first). Every account runs in isolation: an exception (or a failed result) is logged by user id only, counted in `failed` and the batch goes on, so the account is retried by the next run. After {@see self::MAX_CONSECUTIVE_FAILURES} failures in a row the batch stops (most likely a database outage) and `aborted` is `true`.
public
anonymizeDue([int $limit = 50 ]) : array{anonymized: int, failed: int, graceDays: int, aborted: bool}
Parameters
- $limit : int = 50
Return values
array{anonymized: int, failed: int, graceDays: int, aborted: bool}anonymizeNow()
Administration: anonymize a pending account at once instead of waiting for the grace period.
public
anonymizeNow(int $userId) : Result
Parameters
- $userId : int
Return values
ResultbuildPendingSignInRefusal()
Sign-in refused because the deletion is pending (password and OAuth sign-in, token refresh).
public
buildPendingSignInRefusal(User $user) : Result
The caller has just proven the account (password / provider), so the answer carries the date and a short-lived ticket that cancels the deletion — the sign-in itself never restores the account on its own.
Parameters
- $user : User
Return values
ResultcancelDeletion()
Cancels a pending deletion — by the cancel token from the e-mail, or by the ticket handed out at a refused sign-in (`<userId>.<expires>.<hmac>`). Unknown, used and expired keys share one answer (`USER_DELETION_KEY_INVALID`): the action is public.
public
cancelDeletion(string $key) : Result
Parameters
- $key : string
Return values
ResultcancelDeletionByAdmin()
Administration: cancel the pending deletion of an account.
public
cancelDeletionByAdmin(int $userId) : Result
Parameters
- $userId : int
Return values
ResultconfirmDeletion()
The confirmation link of an account without a password was clicked — the deletion starts.
public
confirmDeletion(string $plainToken) : Result
Parameters
- $plainToken : string
Return values
ResultgetDeletedUserLabel()
"Deleted user" in the current language — shown instead of the author of a deleted account.
public
getDeletedUserLabel() : string
Return values
stringrequestDeletion()
The signed-in owner asks for the deletion of their own account. `$userId` must come from the verified identity (session / JWT), never from the request.
public
requestDeletion(int $userId, string $password) : Result
- account with a password: the password is verified (
INVALID_PASSWORD), then the deletion starts at once — datascheduledAt, - account without a password (created by an OAuth sign-in): a confirmation link is e-mailed,
USER_DELETION_CONFIRMATION_SENT; nothing else changes until it is clicked.
Parameters
- $userId : int
- $password : string
Return values
Resultanonymize()
IMPORTANT: a plain read-then-write by id would overwrite a cancel confirmed in between (e-mail, ticket, admin) and anonymize a restored account. Therefore: 1. atomic claim of exactly the request read (`deletion_requested_at` + nonce) — a cancel is refused from this moment (until the claim goes stale, {@see AccountDeletion::CLAIM_STALE_SECONDS}), 2. the claim is re-checked before every irreversible step; a lost claim stops the run, 3. the final overwrite of the row is one UPDATE guarded by the claim.
private
anonymize(User $user, string $via) : Result
A failed step keeps the claim: the cron (or the administration) retries after it went stale.
Parameters
- $user : User
- $via : string
Return values
Resultaudit()
private
audit(string $event, int $userId[, array<string, scalar> $meta = [] ]) : void
Parameters
- $event : string
- $userId : int
- $meta : array<string, scalar> = []
claimLost()
The run lost its claim (cancelled after it went stale, new request, another run) — no further step.
private
claimLost(int $userId, string $via, string $step) : Result
Parameters
- $userId : int
- $via : string
- $step : string
Return values
ResultclearRequest()
IMPORTANT: compare-and-set on the request read by the caller — refused (`$refusedCode`, nothing written) when an anonymization holds a fresh claim, the account was anonymized or the request changed in the meantime. OK means the account IS restored.
private
clearRequest(User $user, string $via, ResultCode $refusedCode) : Result
Parameters
- $user : User
- $via : string
- $refusedCode : ResultCode
Return values
ResultfindByToken()
Account whose stored deletion token hash matches the plain token (fresh from the DB).
private
findByToken(string $plainToken) : User|null
Parameters
- $plainToken : string
Return values
User|nullrequestEmailConfirmation()
private
requestEmailConfirmation(User $user) : Result
Parameters
- $user : User
Return values
ResultstartDeletion()
private
startDeletion(User $user, string $via) : Result
Parameters
- $user : User
- $via : string