Private
Public Access
1
0

chg: dev: increase the minimum PHP version to the latest major - and massive refactor on back-end, like Controllers and Repositories #4

This commit is contained in:
2026-04-12 08:01:46 +02:00
parent 92bfa5b301
commit c0dcc2896a
12 changed files with 511 additions and 104 deletions

View File

@@ -11,9 +11,14 @@
namespace App\Repository;
use App\Entity\PlayedGame;
use App\Entity\User;
use DateTime;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\NoResultException;
use Doctrine\Persistence\ManagerRegistry;
use Psr\Log\LoggerInterface;
use RuntimeException;
/**
* Class PlayedGameRepository
@@ -29,37 +34,267 @@ use Doctrine\Persistence\ManagerRegistry;
* @method PlayedGame|null findOneBy(array $criteria, array $orderBy = null)
* @method PlayedGame[] findAll()
* @method PlayedGame[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
* @method PlayedGame|null findOneByGameAssoc($gameAssoc)
*/
class PlayedGameRepository extends ServiceEntityRepository
{
/**
* PlayedGameRepository constructor.
*
* @param ManagerRegistry $registry
*/
public function __construct(ManagerRegistry $registry)
public function __construct(ManagerRegistry $registry, private readonly LoggerInterface $logger)
{
parent::__construct($registry, PlayedGame::class);
}
public function findOneByGameAssoc(string $gameAssoc): ?PlayedGame
{
$qb = $this->createQueryBuilder('p');
try {
return $qb
->where($qb->expr()->eq('p.gameAssoc', ':gameAssoc'))
->setParameter('gameAssoc', $gameAssoc)
->getQuery()
->getOneOrNullResult();
} catch (NonUniqueResultException $e) {
$this->logger->error($e->getMessage());
throw new RuntimeException(
"Unexpectedly multiple results found when looking up gameAssoc: $gameAssoc",
0,
$e,
);
}
}
public function countFinishedForUser(User $user): int
{
$qb = $this->createQueryBuilder('g');
try {
return (int) $qb
->select('COUNT(g.id)')
->where($qb->expr()->andX(
$qb->expr()->orX(
$qb->expr()->eq('g.red', ':u'),
$qb->expr()->eq('g.blue', ':u'),
),
$qb->expr()->orX(
$qb->expr()->isNotNull('g.redPoints'),
$qb->expr()->isNotNull('g.resign'),
),
))
->setParameter('u', $user)
->getQuery()
->getSingleScalarResult();
} catch (NoResultException $e) {
$this->logger->error($e->getMessage());
throw new RuntimeException(
"Unexpectedly no result found when counting finished games for user: {$user->getUsername()}",
0,
$e,
);
} catch (NonUniqueResultException $e) {
$this->logger->error($e->getMessage());
throw new RuntimeException(
"Unexpectedly multiple results found when counting finished games for user: {$user->getUsername()}",
0,
$e,
);
}
}
public function countWinsForUser(User $user): int
{
$qb = $this->createQueryBuilder('g');
try {
return (int) $qb
->select('COUNT(g.id)')
->where($qb->expr()->orX(
$qb->expr()->andX(
$qb->expr()->eq('g.red', ':u'),
$qb->expr()->gt('g.redPoints', 'g.bluePoints'),
$qb->expr()->isNull('g.resign'),
),
$qb->expr()->andX(
$qb->expr()->eq('g.blue', ':u'),
$qb->expr()->gt('g.bluePoints', 'g.redPoints'),
$qb->expr()->isNull('g.resign'),
),
$qb->expr()->andX(
$qb->expr()->eq('g.red', ':u'),
$qb->expr()->eq('g.resign', ':blue'),
),
$qb->expr()->andX(
$qb->expr()->eq('g.blue', ':u'),
$qb->expr()->eq('g.resign', ':red'),
),
))
->setParameter('u', $user)
->setParameter('blue', 'blue')
->setParameter('red', 'red')
->getQuery()
->getSingleScalarResult();
} catch (NoResultException $e) {
$this->logger->error($e->getMessage());
throw new RuntimeException(
"Unexpectedly no result found when counting wins for user: {$user->getUsername()}",
0,
$e,
);
} catch (NonUniqueResultException $e) {
$this->logger->error($e->getMessage());
throw new RuntimeException(
"Unexpectedly multiple results found when counting wins for user: {$user->getUsername()}",
0,
$e,
);
}
}
public function countLossesForUser(User $user): int
{
$qb = $this->createQueryBuilder('g');
try {
return (int) $qb
->select('COUNT(g.id)')
->where($qb->expr()->orX(
$qb->expr()->andX(
$qb->expr()->eq('g.red', ':u'),
$qb->expr()->gt('g.bluePoints', 'g.redPoints'),
$qb->expr()->isNull('g.resign'),
),
$qb->expr()->andX(
$qb->expr()->eq('g.blue', ':u'),
$qb->expr()->gt('g.redPoints', 'g.bluePoints'),
$qb->expr()->isNull('g.resign'),
),
$qb->expr()->andX(
$qb->expr()->eq('g.red', ':u'),
$qb->expr()->eq('g.resign', ':red'),
),
$qb->expr()->andX(
$qb->expr()->eq('g.blue', ':u'),
$qb->expr()->eq('g.resign', ':blue'),
),
))
->setParameter('u', $user)
->setParameter('red', 'red')
->setParameter('blue', 'blue')
->getQuery()
->getSingleScalarResult();
} catch (NoResultException $e) {
$this->logger->error($e->getMessage());
throw new RuntimeException(
"Unexpectedly no result found when counting losses for user: {$user->getUsername()}",
0,
$e,
);
} catch (NonUniqueResultException $e) {
$this->logger->error($e->getMessage());
throw new RuntimeException(
"Unexpectedly multiple results found when counting losses for user: {$user->getUsername()}",
0,
$e,
);
}
}
public function countBombsForUser(User $user): int
{
$qb = $this->createQueryBuilder('g');
try {
return (int) $qb
->select('COUNT(g.id)')
->where($qb->expr()->orX(
$qb->expr()->andX(
$qb->expr()->eq('g.red', ':u'),
$qb->expr()->eq('g.redExplodedBomb', 'true'),
),
$qb->expr()->andX(
$qb->expr()->eq('g.blue', ':u'),
$qb->expr()->eq('g.blueExplodedBomb', 'true'),
),
))
->setParameter('u', $user)
->getQuery()
->getSingleScalarResult();
} catch (NoResultException $e) {
$this->logger->error($e->getMessage());
throw new RuntimeException(
"Unexpectedly no result found when counting bombs for user: {$user->getUsername()}",
0,
$e,
);
} catch (NonUniqueResultException $e) {
$this->logger->error($e->getMessage());
throw new RuntimeException(
"Unexpectedly multiple results found when counting bombs for user: {$user->getUsername()}",
0,
$e,
);
}
}
/**
* @return PlayedGame[]
*/
public function findRecentFinishedForUser(User $user, int $limit = 10): array
{
$qb = $this->createQueryBuilder('g');
return $qb
->addSelect('rr', 'bb', 'ra', 'ba')
->leftJoin('g.red', 'rr')
->leftJoin('g.blue', 'bb')
->leftJoin('g.redAnon', 'ra')
->leftJoin('g.blueAnon', 'ba')
->where($qb->expr()->andX(
$qb->expr()->orX(
$qb->expr()->eq('g.red', ':u'),
$qb->expr()->eq('g.blue', ':u'),
),
$qb->expr()->orX(
$qb->expr()->isNotNull('g.redPoints'),
$qb->expr()->isNotNull('g.resign'),
),
))
->setParameter('u', $user)
->orderBy('g.updated', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
public function findWaitingGames(int $limit = 20): array
{
// Any legitimately waiting game was updated within the last 10 minutes.
// Abandoned games are stamped with updated = 2000-01-01, so they fail this filter.
$cutoff = new DateTime('-10 minutes');
$qb = $this->createQueryBuilder('p');
return $this->createQueryBuilder('p')
->where('p.resign IS NULL')
->andWhere('p.updated > :cutoff')
->andWhere(
'(p.red IS NOT NULL OR p.redAnon IS NOT NULL) AND (p.blue IS NULL AND p.blueAnon IS NULL)
OR (p.blue IS NOT NULL OR p.blueAnon IS NOT NULL) AND (p.red IS NULL AND p.redAnon IS NULL)'
)
return $qb
->where($qb->expr()->isNull('p.resign'))
->andWhere($qb->expr()->gt('p.updated', ':cutoff'))
->andWhere($qb->expr()->orX(
$qb->expr()->andX(
$qb->expr()->orX(
$qb->expr()->isNotNull('p.red'),
$qb->expr()->isNotNull('p.redAnon'),
),
$qb->expr()->isNull('p.blue'),
$qb->expr()->isNull('p.blueAnon'),
),
$qb->expr()->andX(
$qb->expr()->orX(
$qb->expr()->isNotNull('p.blue'),
$qb->expr()->isNotNull('p.blueAnon'),
),
$qb->expr()->isNull('p.red'),
$qb->expr()->isNull('p.redAnon'),
),
))
->orderBy('p.updated', 'DESC')
->setParameter('cutoff', $cutoff)
->setParameter('cutoff', new DateTime('-10 minutes'))
->setMaxResults($limit)
->getQuery()
->getResult();
}
}
}

View File

@@ -12,7 +12,10 @@ namespace App\Repository;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\Persistence\ManagerRegistry;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
@@ -31,14 +34,25 @@ use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
*/
class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
{
public function __construct(ManagerRegistry $registry)
public function __construct(ManagerRegistry $registry, private readonly LoggerInterface $logger)
{
parent::__construct($registry, User::class);
}
public function findOneByUsername(string $username): ?User
{
return $this->findOneBy(['username' => $username]);
$qb = $this->createQueryBuilder('u');
try {
return $qb
->where($qb->expr()->eq('u.username', ':username'))
->setParameter('username', $username)
->getQuery()
->getOneOrNullResult();
} catch (NonUniqueResultException $e) {
$this->logger->error($e->getMessage());
throw new RuntimeException("Multiple users found with the same username: $username", 0, $e);
}
}
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void