feat: soft-delete for teamer accounts
addresses #869dv9br3
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Teamer;
|
||||
|
||||
use App\Entity\Teamer;
|
||||
use App\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Soft-deletes a teamer together with their user account, and restores them again.
|
||||
*
|
||||
* A deletion excludes the person from every forward-looking process - the teamer list,
|
||||
* autocompletes, forms, mailings and cron reminders - while every existing record stays
|
||||
* untouched and visible: dispositions, applications, documents, feedback and the
|
||||
* contracts and invoices rendered from them are business records that have to survive.
|
||||
*
|
||||
* Both Teamer and User carry their own flag, because neither side is guaranteed to
|
||||
* exist and the queries that have to filter only ever have one of the two aliases at
|
||||
* hand. This class is the only place that writes either flag, so the two cannot drift
|
||||
* apart.
|
||||
*
|
||||
* Deletion is deliberately independent of the block expressed by User::$disabledAt: a
|
||||
* disciplinary block has to survive a deletion and the restore that follows it.
|
||||
*/
|
||||
class AccountDeletionHandler
|
||||
{
|
||||
public const SOURCE_ADMIN = 'admin';
|
||||
public const SOURCE_SELF = 'self';
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
public function delete(Teamer|User $subject, string $source): void
|
||||
{
|
||||
[$teamer, $user] = $this->resolvePair($subject);
|
||||
|
||||
if (false === $this->isDeleted($teamer, $user)) {
|
||||
$teamer?->setDeleted();
|
||||
$user?->setDeleted();
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->logger->info('Delete account', [...$this->logContext($teamer, $user), 'source' => $source]);
|
||||
}
|
||||
}
|
||||
|
||||
public function restore(Teamer|User $subject): void
|
||||
{
|
||||
[$teamer, $user] = $this->resolvePair($subject);
|
||||
|
||||
if (true === $this->isDeleted($teamer, $user)) {
|
||||
$teamer?->setRestored();
|
||||
$user?->setRestored();
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->logger->info('Restore account', $this->logContext($teamer, $user));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: ?Teamer, 1: ?User}
|
||||
*/
|
||||
private function resolvePair(Teamer|User $subject): array
|
||||
{
|
||||
if ($subject instanceof Teamer) {
|
||||
return [$subject, $subject->getUser()];
|
||||
}
|
||||
|
||||
return [$subject->getTeamer(), $subject];
|
||||
}
|
||||
|
||||
/**
|
||||
* Either side being flagged counts as deleted, so that a pair left inconsistent by
|
||||
* an earlier failure is repaired rather than skipped.
|
||||
*/
|
||||
private function isDeleted(?Teamer $teamer, ?User $user): bool
|
||||
{
|
||||
return true === $teamer?->isDeleted() || true === $user?->isDeleted();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function logContext(?Teamer $teamer, ?User $user): array
|
||||
{
|
||||
return [
|
||||
'teamer' => $teamer?->getFullName(),
|
||||
'teamer_id' => $teamer?->getId(),
|
||||
'user' => $user?->getEmail(),
|
||||
'user_id' => $user?->getId(),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user