- Refactor ORM annotations across entities for consistency.

- Adjusted migrations to align with refactored ORM annotations.
- Added new PHPStan and PHP-CS-Fixer configurations, including stricter rules.
- Introduced `tests/object-manager.php` to improve test environment setup.
This commit is contained in:
2025-10-04 12:40:45 +02:00
parent 3e39550c90
commit ca460cca7f
31 changed files with 155 additions and 136 deletions

View File

@@ -33,12 +33,12 @@ class QuizController extends AbstractController
private readonly TranslatorInterface $translator,
) {}
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
#[Route(
'/backoffice/season/{seasonCode:season}/quiz/{quiz}',
name: 'tvdt_backoffice_quiz',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'quiz' => Requirement::UUID],
)]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function index(Season $season, Quiz $quiz): Response
{
return $this->render('backoffice/quiz.html.twig', [
@@ -48,12 +48,12 @@ class QuizController extends AbstractController
]);
}
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
#[Route(
'/backoffice/season/{seasonCode:season}/quiz/{quiz}/enable',
name: 'tvdt_backoffice_enable',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'quiz' => Requirement::UUID.'|null'],
)]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function enableQuiz(Season $season, ?Quiz $quiz, EntityManagerInterface $em): RedirectResponse
{
$season->setActiveQuiz($quiz);
@@ -66,12 +66,12 @@ class QuizController extends AbstractController
return $this->redirectToRoute('tvdt_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
}
#[IsGranted(SeasonVoter::EDIT, subject: 'quiz')]
#[Route(
'/backoffice/quiz/{quiz}/clear',
name: 'tvdt_backoffice_quiz_clear',
requirements: ['quiz' => Requirement::UUID],
)]
#[IsGranted(SeasonVoter::EDIT, subject: 'quiz')]
public function clearQuiz(Quiz $quiz, QuizRepository $quizRepository): RedirectResponse
{
try {
@@ -84,12 +84,12 @@ class QuizController extends AbstractController
return $this->redirectToRoute('tvdt_backoffice_quiz', ['seasonCode' => $quiz->getSeason()->getSeasonCode(), 'quiz' => $quiz->getId()]);
}
#[IsGranted(SeasonVoter::DELETE, subject: 'quiz')]
#[Route(
'/backoffice/quiz/{quiz}/delete',
name: 'tvdt_backoffice_quiz_delete',
requirements: ['quiz' => Requirement::UUID],
)]
#[IsGranted(SeasonVoter::DELETE, subject: 'quiz')]
public function deleteQuiz(Quiz $quiz, QuizRepository $quizRepository): RedirectResponse
{
$quizRepository->deleteQuiz($quiz);
@@ -99,12 +99,12 @@ class QuizController extends AbstractController
return $this->redirectToRoute('tvdt_backoffice_season', ['seasonCode' => $quiz->getSeason()->getSeasonCode()]);
}
#[IsGranted(SeasonVoter::EDIT, subject: 'quiz')]
#[Route(
'/backoffice/quiz/{quiz}/candidate/{candidate}/modify_correction',
name: 'tvdt_backoffice_modify_correction',
requirements: ['quiz' => Requirement::UUID, 'candidate' => Requirement::UUID],
)]
#[IsGranted(SeasonVoter::EDIT, subject: 'quiz')]
public function modifyCorrection(Quiz $quiz, Candidate $candidate, QuizCandidateRepository $quizCandidateRepository, Request $request): RedirectResponse
{
if (!$request->isMethod('POST')) {

View File

@@ -32,12 +32,12 @@ class SeasonController extends AbstractController
private readonly EntityManagerInterface $em,
) {}
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
#[Route(
'/backoffice/season/{seasonCode:season}',
name: 'tvdt_backoffice_season',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX],
)]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function index(Season $season, Request $request): Response
{
$form = $this->createForm(SettingsForm::class, $season->getSettings());
@@ -54,13 +54,13 @@ class SeasonController extends AbstractController
]);
}
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
#[Route(
'/backoffice/season/{seasonCode:season}/add-candidate',
name: 'tvdt_backoffice_add_candidates',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX],
priority: 10,
)]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function addCandidates(Season $season, Request $request): Response
{
$form = $this->createForm(AddCandidatesFormType::class);
@@ -80,13 +80,13 @@ class SeasonController extends AbstractController
return $this->render('backoffice/season_add_candidates.html.twig', ['form' => $form]);
}
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
#[Route(
'/backoffice/season/{seasonCode:season}/add-quiz',
name: 'tvdt_backoffice_quiz_add',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX],
priority: 10,
)]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function addQuiz(Request $request, Season $season, QuizSpreadsheetService $quizSpreadsheet): Response
{
$quiz = new Quiz();

View File

@@ -28,8 +28,8 @@ final class EliminationController extends AbstractController
{
public function __construct(private readonly TranslatorInterface $translator) {}
#[Route('/elimination/{elimination}', name: 'tvdt_elimination', requirements: ['elimination' => Requirement::UUID])]
#[IsGranted(SeasonVoter::ELIMINATION, 'elimination')]
#[Route('/elimination/{elimination}', name: 'tvdt_elimination', requirements: ['elimination' => Requirement::UUID])]
public function index(#[MapEntity] Elimination $elimination, Request $request): Response
{
$form = $this->createForm(EliminationEnterNameType::class);
@@ -48,8 +48,8 @@ final class EliminationController extends AbstractController
]);
}
#[Route('/elimination/{elimination}/{candidateHash}', name: 'tvdt_elimination_candidate', requirements: ['elimination' => Requirement::UUID, 'candidateHash' => self::CANDIDATE_HASH_REGEX])]
#[IsGranted(SeasonVoter::ELIMINATION, 'elimination')]
#[Route('/elimination/{elimination}/{candidateHash}', name: 'tvdt_elimination_candidate', requirements: ['elimination' => Requirement::UUID, 'candidateHash' => self::CANDIDATE_HASH_REGEX])]
public function candidateScreen(Elimination $elimination, string $candidateHash, CandidateRepository $candidateRepository): Response
{
$candidate = $candidateRepository->getCandidateByHash($elimination->getQuiz()->getSeason(), $candidateHash);

View File

@@ -16,17 +16,17 @@ use Tvdt\Repository\AnswerRepository;
#[ORM\Entity(repositoryClass: AnswerRepository::class)]
class Answer
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\Id]
private Uuid $id;
#[ORM\Column(type: Types::SMALLINT, options: ['default' => 0])]
private int $ordering = 0;
#[ORM\ManyToOne(inversedBy: 'answers')]
#[ORM\JoinColumn(nullable: false)]
#[ORM\ManyToOne(inversedBy: 'answers')]
private Question $question;
/** @var Collection<int, Candidate> */

View File

@@ -17,14 +17,14 @@ use Tvdt\Repository\CandidateRepository;
#[ORM\UniqueConstraint(fields: ['name', 'season'])]
class Candidate
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\Id]
private Uuid $id;
#[ORM\ManyToOne(inversedBy: 'candidates')]
#[ORM\JoinColumn(nullable: false)]
#[ORM\ManyToOne(inversedBy: 'candidates')]
private Season $season;
/** @var Collection<int, Answer> */

View File

@@ -21,10 +21,10 @@ class Elimination
public const string SCREEN_RED = 'red';
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\Id]
private Uuid $id;
/** @var array<string, mixed> */
@@ -35,8 +35,8 @@ class Elimination
private \DateTimeImmutable $created;
public function __construct(
#[ORM\ManyToOne(inversedBy: 'eliminations')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[ORM\ManyToOne(inversedBy: 'eliminations')]
private Quiz $quiz,
) {}

View File

@@ -16,26 +16,26 @@ use Tvdt\Repository\GivenAnswerRepository;
#[ORM\HasLifecycleCallbacks]
class GivenAnswer
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\Id]
private Uuid $id;
#[ORM\Column(type: Types::DATETIMETZ_IMMUTABLE, nullable: false)]
private \DateTimeImmutable $created;
public function __construct(
#[ORM\ManyToOne(inversedBy: 'givenAnswers')]
#[ORM\JoinColumn(nullable: false)]
#[ORM\ManyToOne(inversedBy: 'givenAnswers')]
private Candidate $candidate,
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[ORM\ManyToOne]
private Quiz $quiz,
#[ORM\ManyToOne(inversedBy: 'givenAnswers')]
#[ORM\JoinColumn(nullable: false)]
#[ORM\ManyToOne(inversedBy: 'givenAnswers')]
private Answer $answer,
) {}

View File

@@ -16,10 +16,10 @@ use Tvdt\Repository\QuestionRepository;
#[ORM\Entity(repositoryClass: QuestionRepository::class)]
class Question
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\Id]
private Uuid $id;
#[ORM\Column(type: Types::SMALLINT, options: ['default' => 0])]
@@ -28,8 +28,8 @@ class Question
#[ORM\Column(type: Types::STRING, length: 255)]
private string $question;
#[ORM\ManyToOne(inversedBy: 'questions')]
#[ORM\JoinColumn(nullable: false)]
#[ORM\ManyToOne(inversedBy: 'questions')]
private Quiz $quiz;
#[ORM\Column]

View File

@@ -16,17 +16,17 @@ use Tvdt\Repository\QuizRepository;
#[ORM\UniqueConstraint(fields: ['name', 'season'])]
class Quiz
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\Id]
private Uuid $id;
#[ORM\Column(length: 64)]
private string $name;
#[ORM\ManyToOne(inversedBy: 'quizzes')]
#[ORM\JoinColumn(nullable: false)]
#[ORM\ManyToOne(inversedBy: 'quizzes')]
private Season $season;
/** @var Collection<int, Question> */

View File

@@ -13,14 +13,14 @@ use Symfony\Component\Uid\Uuid;
use Tvdt\Repository\QuizCandidateRepository;
#[ORM\Entity(repositoryClass: QuizCandidateRepository::class)]
#[ORM\UniqueConstraint(columns: ['candidate_id', 'quiz_id'])]
#[ORM\HasLifecycleCallbacks]
#[ORM\UniqueConstraint(columns: ['candidate_id', 'quiz_id'])]
class QuizCandidate
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\Id]
private Uuid $id;
#[ORM\Column]
@@ -30,12 +30,12 @@ class QuizCandidate
private \DateTimeImmutable $created;
public function __construct(
#[ORM\ManyToOne(inversedBy: 'candidateData')]
#[ORM\JoinColumn(nullable: false)]
#[ORM\ManyToOne(inversedBy: 'candidateData')]
private Quiz $quiz,
#[ORM\ManyToOne(inversedBy: 'quizData')]
#[ORM\JoinColumn(nullable: false)]
#[ORM\ManyToOne(inversedBy: 'quizData')]
private Candidate $candidate,
) {}

View File

@@ -17,10 +17,10 @@ class Season
{
private const string SEASON_CODE_CHARACTERS = 'bcdfghjklmnpqrstvwxz';
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\Id]
private Uuid $id;
#[ORM\Column(length: 64)]
@@ -42,12 +42,12 @@ class Season
#[ORM\ManyToMany(targetEntity: User::class, inversedBy: 'seasons')]
private Collection $owners;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
#[ORM\ManyToOne]
private ?Quiz $ActiveQuiz = null;
#[ORM\OneToOne(cascade: ['persist', 'remove'])]
#[ORM\JoinColumn(nullable: true)]
#[ORM\OneToOne(cascade: ['persist', 'remove'])]
private ?SeasonSettings $settings = null;
public function __construct()

View File

@@ -14,10 +14,10 @@ use Tvdt\Repository\SeasonSettingsRepository;
#[ORM\Entity(repositoryClass: SeasonSettingsRepository::class)]
class SeasonSettings
{
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\Column(type: UuidType::NAME)]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\Id]
private Uuid $id;
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => false])]

View File

@@ -22,10 +22,10 @@ use Tvdt\Repository\UserRepository;
#[UniqueEntity(fields: ['email'], message: 'There is already an account with this email')]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\Id]
private Uuid $id;
#[ORM\Column(length: 180)]

View File

@@ -38,7 +38,7 @@ class CandidateRepository extends ServiceEntityRepository
select c from Tvdt\Entity\Candidate c
where c.season = :season
and lower(c.name) = lower(:name)
DQL
DQL
)->setParameter('season', $season)
->setParameter('name', $name)
->getOneOrNullResult();
@@ -57,21 +57,21 @@ class CandidateRepository extends ServiceEntityRepository
public function getScores(Quiz $quiz): array
{
return $this->getEntityManager()->createQuery(<<<DQL
select
c.id,
c.name,
sum(case when a.isRightAnswer = true then 1 else 0 end) as correct,
qc.corrections,
max(ga.created) - qc.created as time,
(sum(case when a.isRightAnswer = true then 1 else 0 end) + qc.corrections) as score
from Tvdt\Entity\Candidate c
join c.givenAnswers ga
join ga.answer a
join c.quizData qc
where qc.quiz = :quiz and ga.quiz = :quiz
group by ga.quiz, c.id, qc.id
order by score desc, time asc
DQL
select
c.id,
c.name,
count(case when a.isRightAnswer = true then 1 else null end) as correct,
qc.corrections,
max(ga.created) - qc.created as time,
(count(case when a.isRightAnswer = true then 1 else null end) + qc.corrections) as score
from Tvdt\Entity\Candidate c
join c.givenAnswers ga
join ga.answer a
join c.quizData qc
where qc.quiz = :quiz and ga.quiz = :quiz
group by ga.quiz, c.id, qc.id
order by score desc, time asc
DQL
)->setParameter('quiz', $quiz)->getResult();
}
}

View File

@@ -32,7 +32,7 @@ class QuestionRepository extends ServiceEntityRepository
and q1.quiz = :quiz
)
and qz = :quiz
DQL)
DQL)
->setMaxResults(1)
->setParameter('candidate', $candidate)
->setParameter('quiz', $candidate->getSeason()->getActiveQuiz())

View File

@@ -24,7 +24,7 @@ class SeasonRepository extends ServiceEntityRepository
{
return $this->getEntityManager()->createQuery(<<<DQL
select s from Tvdt\Entity\Season s where :user member of s.owners order by s.name
DQL
DQL
)->setParameter('user', $user)->getResult();
}
}