feat: add a cleanup command for accounts without recent activity
This commit is contained in:
1 parent
7ec21c1cac
commit
8a6ce7606f
3 files changed
+488
No files matched your search
@@ -4,7 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\BookingEditDraft;
|
||||
use App\Entity\Groups\Accommodation;
|
||||
use App\Entity\Groups\AccommodationBooking;
|
||||
use App\Entity\Groups\AccommodationPrice;
|
||||
use App\Entity\Groups\AdditionalService;
|
||||
use App\Entity\Groups\BoardService;
|
||||
use App\Entity\User;
|
||||
use App\Form\Model\Filter\UserFilterDto;
|
||||
use App\Repository\Filter\AppliesListFiltersTrait;
|
||||
@@ -122,4 +127,87 @@ class UserRepository extends ServiceEntityRepository
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
/**
|
||||
* The entities that point at a User and would silently lose that pointer if the account were
|
||||
* deleted. Every one of these join columns is ON DELETE SET NULL, so the database does not
|
||||
* stand in the way — it just erases who created, last touched or looks after a group booking.
|
||||
* The cleanup therefore has to ask, rather than let the constraint decide.
|
||||
*
|
||||
* @var array<class-string, string[]>
|
||||
*/
|
||||
private const USER_REFERENCES = [
|
||||
Accommodation::class => ['createdBy', 'updatedBy'],
|
||||
AccommodationPrice::class => ['createdBy', 'updatedBy'],
|
||||
AccommodationBooking::class => ['createdBy', 'updatedBy', 'managedBy'],
|
||||
AdditionalService::class => ['createdBy', 'updatedBy'],
|
||||
BoardService::class => ['createdBy', 'updatedBy'],
|
||||
];
|
||||
|
||||
/**
|
||||
* Accounts the cleanup may consider: holding no booking edit draft and named by nothing in the
|
||||
* Groups subsystem. $inactiveSince narrows that to accounts dormant since then, and is the
|
||||
* caller's choice rather than this method's default — BpnAuthenticator recreates an account on
|
||||
* sight, so how long ago somebody last signed in says nothing about whether the row may go.
|
||||
*
|
||||
* Deliberately unbounded: the caller still has to drop everything whose roles disqualify it,
|
||||
* so a LIMIT here would cap the rows examined rather than the rows acted on, and a caller
|
||||
* asking for ten deletions would get however many of the first ten candidates survived the
|
||||
* role filter. Ordered oldest login first so a caller that does cap the result takes the
|
||||
* least recently seen accounts.
|
||||
*
|
||||
* The roles are deliberately not part of this query. They live in a JSON array column with no
|
||||
* JSON_CONTAINS registered, and the column holds more than one spelling of the same set — rows
|
||||
* written by different Doctrine versions differ in whitespace — so a LIKE or an equality test
|
||||
* on the raw JSON would quietly miss accounts or, far worse, match the wrong ones. The caller
|
||||
* filters the shortlist through Role::effectiveOnly(), which is the only thing that knows what
|
||||
* a role set actually means.
|
||||
*
|
||||
* @return User[]
|
||||
*/
|
||||
public function findCleanupCandidates(?\DateTimeImmutable $inactiveSince = null): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('u');
|
||||
|
||||
if (null !== $inactiveSince) {
|
||||
$qb
|
||||
->andWhere('u.lastLoginAt IS NOT NULL')
|
||||
->andWhere('u.lastLoginAt < :inactiveSince')
|
||||
->setParameter('inactiveSince', $inactiveSince)
|
||||
;
|
||||
}
|
||||
|
||||
$qb->andWhere($qb->expr()->not($qb->expr()->exists(
|
||||
$this->getEntityManager()->createQueryBuilder()
|
||||
->select('draft.id')
|
||||
->from(BookingEditDraft::class, 'draft')
|
||||
->andWhere('draft.user = u')
|
||||
->getDQL()
|
||||
)));
|
||||
|
||||
$index = 0;
|
||||
|
||||
foreach (self::USER_REFERENCES as $class => $fields) {
|
||||
foreach ($fields as $field) {
|
||||
$alias = sprintf('ref%d%s', $index++, $field);
|
||||
|
||||
$qb->andWhere($qb->expr()->not($qb->expr()->exists(
|
||||
$this->getEntityManager()->createQueryBuilder()
|
||||
->select($alias.'.id')
|
||||
->from($class, $alias)
|
||||
->andWhere(sprintf('%s.%s = u', $alias, $field))
|
||||
->getDQL()
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
/** @var User[] $users */
|
||||
$users = $qb
|
||||
->orderBy('u.lastLoginAt', 'ASC')
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
|
||||
return $users;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user