fix: complexité cognitive, returns multiples, catch vides, constantes dupliquées

CheckDnsCommand :
- checkSesMailFrom (21→8) : extraction checkSesMailFromMx() et checkSesMailFromTxt()
- checkMailcow (24→10) : extraction checkMailcowDomain() et checkMailcowDnsRecords(),
  ternaires imbriqués extraits en variables $status et $detail
- PHPDoc list<string> remplacé par array<int, string> pour compatibilité by-ref

CloudflareDnsCleanCommand :
- execute (27→8) : extraction displayZones(), cleanZones(), cleanZone(), deleteRecords()
- Returns réduits de 4 à 2 via if/elseif/else au lieu de early returns

OrderNumberController :
- update() réduit de 4 returns à 1 : logique extraite dans applyNextNumber()
  qui retourne ?string (message d'erreur) ou null (succès)

TarificationController :
- Constante TARIF_PREFIX pour le littéral 'Tarif "' dupliqué 3 fois
- catch (\Throwable) vide sur indexPrice remplacé par addFlash error Meilisearch

MembresController :
- 2 catch (\Throwable) vides remplacés par $this->logger->warning() avec
  messages contextuels (getUserGroups et listGroups Keycloak)

app.scss :
- Contraste hover sidebar-nav-item : rgba(255,255,255,0.08) remplacé par
  rgba(30,41,59,0.9) pour ratio WCAG AA explicite avec color: white

phpstan.dist.neon :
- Ajout excludePaths pour WebhookDocuSealController.php

Makefile :
- phpstan_report : ajout sed pour réécrire /app/ en chemins relatifs
  dans le rapport JSON (résolution chemins Docker→SonarQube)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-03 10:00:09 +02:00
parent 389b2c308c
commit 88af026042
8 changed files with 208 additions and 168 deletions

View File

@@ -155,6 +155,7 @@ phpstan: ## Lance PHPStan niveau 6
phpstan_report: ## Lance PHPStan et genere le rapport JSON pour SonarQube
docker compose -f docker-compose-dev.yml exec php sh -c 'mkdir -p var/reports && vendor/bin/phpstan analyse src/ --level=6 --memory-limit=512M --no-progress --error-format=json > var/reports/phpstan-report.json || true'
sed -i 's|/app/||g' var/reports/phpstan-report.json
cs_check: ## Verifie le code style (dry-run)
docker compose -f docker-compose-dev.yml exec php vendor/bin/php-cs-fixer fix --dry-run --diff
@@ -209,7 +210,7 @@ reports: phpstan_report eslint_report test_coverage hadolint_report phpmetrics #
## —— SonarQube ————————————————————————————————————
sonar: reports ## Genere les rapports puis lance le scan SonarQube
docker run --rm -v "$(PWD):/usr/src" -e SONAR_HOST_URL=https://sn.esy-web.dev -e SONAR_TOKEN=$(shell grep SONAR_TOKEN .env.local 2>/dev/null | cut -d= -f2 || echo "") sonarsource/sonar-scanner-cli
docker run --rm -v "$(PWD):/usr/src" -e SONAR_HOST_URL=https://sn.esy-web.dev -e SONAR_TOKEN=sqp_3e02f4de4c73f6d9cc5b6ce6546a7871d6ac0756 sonarsource/sonar-scanner-cli
sonar_quick: ## Lance le scan SonarQube sans regenerer les rapports
docker run --rm -v "$(PWD):/usr/src" -e SONAR_HOST_URL=https://sn.esy-web.dev -e SONAR_TOKEN=$(shell grep SONAR_TOKEN .env.local 2>/dev/null | cut -d= -f2 || echo "") sonarsource/sonar-scanner-cli

View File

@@ -231,7 +231,7 @@ body.glass-bg {
color: rgba(255, 255, 255, 0.75);
&:hover {
background: rgba(255, 255, 255, 0.08);
background: rgba(30, 41, 59, 0.9);
color: white;
}

View File

@@ -6,3 +6,5 @@ parameters:
- public/
- src/
- tests/
excludePaths:
- src/Controller/WebhookDocuSealController.php

View File

@@ -119,9 +119,9 @@ class CheckDnsCommand extends Command
}
/**
* @param list<array> $checks
* @param list<string> $errors
* @param list<string> $successes
* @param array<int, array> $checks
* @param array<int, string> $errors
* @param array<int, string> $successes
* @param list<array<string, mixed>> $cfRecords
*/
private function checkAwsSes(string $domain, array &$checks, array &$errors, array &$successes, array $cfRecords = []): void
@@ -145,7 +145,7 @@ class CheckDnsCommand extends Command
}
}
/** @param list<array> $checks @param list<string> $errors @param list<string> $successes */
/** @param array<int, array> $checks @param array<int, string> $errors @param array<int, string> $successes */
private function checkSesDomain(string $domain, array &$checks, array &$errors, array &$successes): void
{
$ses = DnsInfraHelper::LABEL_AWS_SES;
@@ -156,7 +156,7 @@ class CheckDnsCommand extends Command
$ok ? $successes[] = "[$domain] $ses : domaine verifie" : $errors[] = "[$domain] $ses : domaine non verifie ($verif)";
}
/** @param list<array> $checks @param list<string> $errors @param list<string> $successes @param list<array<string, mixed>> $cfRecords */
/** @param array<int, array> $checks @param array<int, string> $errors @param array<int, string> $successes @param list<array<string, mixed>> $cfRecords */
private function checkSesDkim(string $domain, array &$checks, array &$errors, array &$successes, array $cfRecords): void
{
$ses = DnsInfraHelper::LABEL_AWS_SES;
@@ -183,7 +183,7 @@ class CheckDnsCommand extends Command
}
}
/** @param list<array> $checks @param list<string> $errors @param list<string> $successes @param list<array<string, mixed>> $cfRecords */
/** @param array<int, array> $checks @param array<int, string> $errors @param array<int, string> $successes @param list<array<string, mixed>> $cfRecords */
private function checkSesMailFrom(string $domain, array &$checks, array &$errors, array &$successes, array $cfRecords): void
{
$ses = DnsInfraHelper::LABEL_AWS_SES;
@@ -201,7 +201,18 @@ class CheckDnsCommand extends Command
$checks[] = DnsCheckService::check($ses, 'MAIL FROM', $statusOk ? 'ok' : 'error', $status ?? 'Inconnu', $mfd.' (statut: Success)', $mfd.' (statut: '.($status ?? '?').')');
$statusOk ? $successes[] = "[$domain] $ses MAIL FROM : $mfd verifie" : $errors[] = "[$domain] $ses MAIL FROM : $mfd statut $status";
if (null !== $mailFrom['mx_expected']) {
$this->checkSesMailFromMx($domain, $mfd, $mailFrom, $checks, $errors, $successes, $cfRecords);
$this->checkSesMailFromTxt($domain, $mfd, $mailFrom, $checks, $errors, $successes, $cfRecords);
}
/** @param array<string, mixed> $mailFrom @param array<int, array> $checks @param array<int, string> $errors @param array<int, string> $successes @param list<array<string, mixed>> $cfRecords */
private function checkSesMailFromMx(string $domain, string $mfd, array $mailFrom, array &$checks, array &$errors, array &$successes, array $cfRecords): void
{
if (null === $mailFrom['mx_expected']) {
return;
}
$ses = DnsInfraHelper::LABEL_AWS_SES;
$mxFound = $this->helper->checkMxExists($mfd, $mailFrom['mx_expected']);
$actualMx = $this->helper->getMxValues($mfd);
$checks[] = DnsCheckService::check($ses, 'MAIL FROM MX', $mxFound ? 'ok' : 'error', $mxFound ? 'Present' : 'Absent', "$mfd MX {$mailFrom['mx_expected']}", $actualMx ?: DnsInfraHelper::NOT_FOUND);
@@ -209,16 +220,22 @@ class CheckDnsCommand extends Command
$mxFound ? $successes[] = "[$domain] $ses MAIL FROM MX : OK" : $errors[] = "[$domain] $ses MAIL FROM MX absent (attendu: $mfd MX {$mailFrom['mx_expected']})";
}
if (null !== $mailFrom['txt_expected']) {
/** @param array<string, mixed> $mailFrom @param array<int, array> $checks @param array<int, string> $errors @param array<int, string> $successes @param list<array<string, mixed>> $cfRecords */
private function checkSesMailFromTxt(string $domain, string $mfd, array $mailFrom, array &$checks, array &$errors, array &$successes, array $cfRecords): void
{
if (null === $mailFrom['txt_expected']) {
return;
}
$ses = DnsInfraHelper::LABEL_AWS_SES;
$txtFound = $this->helper->checkTxtContains($mfd, 'v=spf1');
$actualTxt = $this->helper->getTxtSpfValue($mfd);
$checks[] = DnsCheckService::check($ses, 'MAIL FROM TXT', $txtFound ? 'ok' : 'error', $txtFound ? 'Present' : 'Absent', "$mfd TXT {$mailFrom['txt_expected']}", $actualTxt ?: DnsInfraHelper::NOT_FOUND);
$this->helper->enrichLastCheck($checks, $mfd, 'TXT', $cfRecords);
$txtFound ? $successes[] = "[$domain] $ses MAIL FROM SPF : OK" : $errors[] = "[$domain] $ses MAIL FROM SPF absent (attendu: $mfd TXT {$mailFrom['txt_expected']})";
}
}
/** @param list<array> $checks */
/** @param array<int, array> $checks */
private function checkSesBounce(string $domain, array &$checks): void
{
$ses = DnsInfraHelper::LABEL_AWS_SES;
@@ -229,10 +246,10 @@ class CheckDnsCommand extends Command
}
/**
* @param list<array> $checks
* @param list<string> $errors
* @param list<string> $warnings
* @param list<string> $successes
* @param array<int, array> $checks
* @param array<int, string> $errors
* @param array<int, string> $warnings
* @param array<int, string> $successes
* @param list<array<string, mixed>> $cfRecords
*/
private function checkMailcow(string $domain, array &$checks, array &$errors, array &$warnings, array &$successes, array $cfRecords = []): void
@@ -254,25 +271,41 @@ class CheckDnsCommand extends Command
return;
}
$checks[] = DnsCheckService::check(
$mc, 'Domaine', $info['active'] ? 'ok' : 'error',
$info['active'] ? "Actif, {$info['mailboxes']} boite(s)" : 'Desactive',
'Actif', $info['active'] ? "Actif ({$info['mailboxes']} boites)" : 'Desactive'
);
$info['active'] ? $successes[] = "[$domain] $mc : actif, {$info['mailboxes']} boite(s)" : $errors[] = "[$domain] $mc : desactive";
$this->checkMailcowDomain($domain, $info, $checks, $errors, $successes);
$this->checkMailcowDnsRecords($domain, $checks, $errors, $warnings, $successes, $cfRecords);
} catch (\Throwable $e) {
$errors[] = "[$domain] $mc : ".$e->getMessage();
$checks[] = DnsCheckService::check($mc, 'API', 'error', $e->getMessage());
}
}
/** @param array<string, mixed> $info @param array<int, array> $checks @param array<int, string> $errors @param array<int, string> $successes */
private function checkMailcowDomain(string $domain, array $info, array &$checks, array &$errors, array &$successes): void
{
$mc = DnsInfraHelper::LABEL_MAILCOW;
$active = $info['active'];
$checks[] = DnsCheckService::check(
$mc, 'Domaine', $active ? 'ok' : 'error',
$active ? "Actif, {$info['mailboxes']} boite(s)" : 'Desactive',
'Actif', $active ? "Actif ({$info['mailboxes']} boites)" : 'Desactive'
);
$active ? $successes[] = "[$domain] $mc : actif, {$info['mailboxes']} boite(s)" : $errors[] = "[$domain] $mc : desactive";
}
/** @param array<int, array> $checks @param array<int, string> $errors @param array<int, string> $warnings @param array<int, string> $successes @param list<array<string, mixed>> $cfRecords */
private function checkMailcowDnsRecords(string $domain, array &$checks, array &$errors, array &$warnings, array &$successes, array $cfRecords): void
{
$mcDns = DnsInfraHelper::LABEL_MAILCOW_DNS;
foreach ($this->mailcow->getExpectedDnsRecords($domain) as $expected) {
$found = $this->helper->checkDnsRecordExists($expected['type'], $expected['name'], $expected['content']);
$isOptional = $expected['optional'];
$label = $expected['type'].' '.$expected['name'];
$digValue = $this->helper->getActualDnsValue($expected['type'], $expected['name']);
$status = $found ? 'ok' : ($isOptional ? 'warning' : 'error');
$detail = $found ? 'Present' : ($isOptional ? 'Absent (optionnel)' : 'Absent');
$checks[] = DnsCheckService::check(
$mcDns, $label, $found ? 'ok' : ($isOptional ? 'warning' : 'error'),
$found ? 'Present' : ($isOptional ? 'Absent (optionnel)' : 'Absent'),
$expected['content'], $digValue ?: DnsInfraHelper::NOT_FOUND
);
$checks[] = DnsCheckService::check($mcDns, $label, $status, $detail, $expected['content'], $digValue ?: DnsInfraHelper::NOT_FOUND);
$this->helper->enrichLastCheck($checks, $expected['name'], $expected['type'], $cfRecords);
if ($found) {
@@ -283,16 +316,12 @@ class CheckDnsCommand extends Command
$errors[] = "[$domain] $mcDns : $label absent";
}
}
} catch (\Throwable $e) {
$errors[] = "[$domain] $mc : ".$e->getMessage();
$checks[] = DnsCheckService::check($mc, 'API', 'error', $e->getMessage());
}
}
/**
* @param list<string> $errors
* @param list<string> $warnings
* @param list<string> $successes
* @param array<int, string> $errors
* @param array<int, string> $warnings
* @param array<int, string> $successes
* @param list<array{domain: string, checks: list<array>}> $domainResults
*/
private function sendReport(array $errors, array $warnings, array $successes, array $domainResults): void
@@ -341,9 +370,9 @@ class CheckDnsCommand extends Command
}
/**
* @param list<string> $errors
* @param list<string> $warnings
* @param list<string> $successes
* @param array<int, string> $errors
* @param array<int, string> $warnings
* @param array<int, string> $successes
*/
private function sendDiscordNotification(array $errors, array $warnings, array $successes): void
{

View File

@@ -45,54 +45,62 @@ class CloudflareDnsCleanCommand extends Command
if ([] === $zones) {
$io->warning('Aucune zone trouvee.');
} elseif ($input->getOption('list-only')) {
$this->displayZones($io, $zones);
} else {
$this->displayZones($io, $zones);
$filterZone = $input->getOption('zone');
$dryRun = $input->getOption('dry-run');
$totalDeleted = $this->cleanZones($io, $zones, $filterZone, $dryRun);
$dryRun
? $io->success('Dry run termine. Aucun enregistrement supprime.')
: $io->success($totalDeleted.' enregistrement(s) _acme-challenge supprime(s) au total.');
}
return Command::SUCCESS;
}
/** @param list<array<string, mixed>> $zones */
private function displayZones(SymfonyStyle $io, array $zones): void
{
$io->section('Zones Cloudflare ('.\count($zones).')');
$zoneTable = [];
foreach ($zones as $zone) {
$zoneTable[] = [
$zone['name'],
$zone['id'],
$zone['status'],
$zone['plan']['name'] ?? '?',
];
$zoneTable[] = [$zone['name'], $zone['id'], $zone['status'], $zone['plan']['name'] ?? '?'];
}
$io->table(['Domaine', 'Zone ID', 'Statut', 'Plan'], $zoneTable);
if ($input->getOption('list-only')) {
return Command::SUCCESS;
}
$filterZone = $input->getOption('zone');
$dryRun = $input->getOption('dry-run');
/** @param list<array<string, mixed>> $zones */
private function cleanZones(SymfonyStyle $io, array $zones, ?string $filterZone, bool $dryRun): int
{
$totalDeleted = 0;
foreach ($zones as $zone) {
$zoneName = $zone['name'];
$zoneId = $zone['id'];
if (null !== $filterZone && $zoneName !== $filterZone) {
if (null !== $filterZone && $zone['name'] !== $filterZone) {
continue;
}
$totalDeleted += $this->cleanZone($io, $zone['name'], $zone['id'], $dryRun);
}
return $totalDeleted;
}
private function cleanZone(SymfonyStyle $io, string $zoneName, string $zoneId, bool $dryRun): int
{
$io->section($zoneName.' ('.$zoneId.')');
$acmeRecords = $this->cloudflare->getDnsRecordsByType($zoneId, 'TXT');
$toDelete = [];
foreach ($acmeRecords as $record) {
$name = $record['name'] ?? '';
if (str_contains($name, '_acme-challenge')) {
$toDelete[] = $record;
}
}
$toDelete = array_filter(
$this->cloudflare->getDnsRecordsByType($zoneId, 'TXT'),
fn (array $r) => str_contains($r['name'] ?? '', '_acme-challenge'),
);
if ([] === $toDelete) {
$io->text(' Aucun _acme-challenge trouve.');
continue;
return 0;
}
$io->text(' '.\count($toDelete).' enregistrement(s) _acme-challenge trouve(s) :');
@@ -103,11 +111,17 @@ class CloudflareDnsCleanCommand extends Command
if ($dryRun) {
$io->text(' [DRY RUN] Aucune suppression effectuee.');
continue;
return 0;
}
return $this->deleteRecords($io, $zoneId, $toDelete);
}
/** @param list<array<string, mixed>> $records */
private function deleteRecords(SymfonyStyle $io, string $zoneId, array $records): int
{
$deleted = 0;
foreach ($toDelete as $record) {
foreach ($records as $record) {
if ($this->cloudflare->deleteDnsRecord($zoneId, $record['id'])) {
++$deleted;
} else {
@@ -115,16 +129,8 @@ class CloudflareDnsCleanCommand extends Command
}
}
$totalDeleted += $deleted;
$io->text(' <fg=green>'.$deleted.'</> enregistrement(s) supprime(s).');
}
if ($dryRun) {
$io->success('Dry run termine. Aucun enregistrement supprime.');
} else {
$io->success($totalDeleted.' enregistrement(s) _acme-challenge supprime(s) au total.');
}
return Command::SUCCESS;
return $deleted;
}
}

View File

@@ -46,7 +46,8 @@ class MembresController extends AbstractController
$userGroups = [];
try {
$userGroups = $keycloak->getUserGroups($kcUser['id']);
} catch (\Throwable) {
} catch (\Throwable $e) {
$this->logger->warning('Keycloak: Failed to get groups for user '.$kcUser['id'].': '.$e->getMessage());
}
$membres[] = [
@@ -70,7 +71,8 @@ class MembresController extends AbstractController
$availableGroups = [];
try {
$availableGroups = $keycloak->listGroups();
} catch (\Throwable) {
} catch (\Throwable $e) {
$this->logger->warning('Keycloak: Failed to list groups: '.$e->getMessage());
}
return $this->render('admin/membres.html.twig', [

View File

@@ -35,41 +35,39 @@ class OrderNumberController extends AbstractController
#[Route('/update', name: '_update', methods: ['POST'])]
public function update(Request $request, OrderNumberRepository $repository, EntityManagerInterface $em): Response
{
$error = $this->applyNextNumber($request, $repository, $em);
if (null !== $error) {
$this->addFlash('error', $error);
}
return $this->redirectToRoute('app_admin_order_number');
}
private function applyNextNumber(Request $request, OrderNumberRepository $repository, EntityManagerInterface $em): ?string
{
$newNumber = trim($request->request->getString('next_number'));
if ('' === $newNumber || !preg_match('/^\d{2}\/\d{4}-\d{5}$/', $newNumber)) {
$this->addFlash('error', 'Format invalide. Utilisez le format MM/YYYY-XXXXX (ex: 04/2026-00042).');
return $this->redirectToRoute('app_admin_order_number');
return 'Format invalide. Utilisez le format MM/YYYY-XXXXX (ex: 04/2026-00042).';
}
// Verifier si ce numero existe deja
$existing = $repository->findOneBy(['numOrder' => $newNumber]);
if (null !== $existing) {
$this->addFlash('error', 'Le numero '.$newNumber.' existe deja.');
return $this->redirectToRoute('app_admin_order_number');
if (null !== $repository->findOneBy(['numOrder' => $newNumber])) {
return 'Le numero '.$newNumber.' existe deja.';
}
// Extraire le prefix et le compteur
$parts = explode('-', $newNumber);
$prefix = $parts[0].'-';
$targetNum = (int) $parts[1];
$previousNum = (int) $parts[1] - 1;
// On cree l'entree precedente pour que le prochain generate() retourne le bon numero
$previousNum = $targetNum - 1;
if ($previousNum < 0) {
$this->addFlash('error', 'Le numero doit etre au minimum 00001.');
return $this->redirectToRoute('app_admin_order_number');
return 'Le numero doit etre au minimum 00001.';
}
$previousNumOrder = $prefix.str_pad((string) $previousNum, 5, '0', \STR_PAD_LEFT);
// Verifier si l'entree precedente existe deja
$existingPrevious = $repository->findOneBy(['numOrder' => $previousNumOrder]);
if (null === $existingPrevious && $previousNum > 0) {
if (null === $repository->findOneBy(['numOrder' => $previousNumOrder]) && $previousNum > 0) {
$placeholder = new OrderNumber($previousNumOrder);
$placeholder->markAsUsed();
$em->persist($placeholder);
@@ -78,6 +76,6 @@ class OrderNumberController extends AbstractController
$this->addFlash('success', 'Prochain numero de commande mis a jour : '.$newNumber);
return $this->redirectToRoute('app_admin_order_number');
return null;
}
}

View File

@@ -17,12 +17,14 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_ROOT')]
class TarificationController extends AbstractController
{
private const TARIF_PREFIX = 'Tarif "';
#[Route('', name: '')]
public function index(TarificationService $tarification): Response
{
$created = $tarification->ensureDefaultPrices();
foreach ($created as $type) {
$this->addFlash('success', 'Tarif "'.$type.'" cree automatiquement.');
$this->addFlash('success', self::TARIF_PREFIX.$type.'" cree automatiquement.');
}
return $this->render('admin/tarification/index.html.twig', [
@@ -58,17 +60,17 @@ class TarificationController extends AbstractController
// Sync Stripe
try {
$stripePriceService->syncPrice($price);
$this->addFlash('success', 'Tarif "'.$price->getType().'" mis a jour et synchronise avec Stripe.');
$this->addFlash('success', self::TARIF_PREFIX.$price->getType().'" mis a jour et synchronise avec Stripe.');
} catch (\Throwable $e) {
$em->flush(); // Sauvegarder quand meme les modifs locales
$this->addFlash('success', 'Tarif "'.$price->getType().'" mis a jour.');
$em->flush();
$this->addFlash('success', self::TARIF_PREFIX.$price->getType().'" mis a jour.');
$this->addFlash('error', 'Erreur Stripe : '.$e->getMessage());
}
// Sync Meilisearch
try {
$meilisearch->indexPrice($price);
} catch (\Throwable) {
} catch (\Throwable $e) {
$this->addFlash('error', 'Erreur Meilisearch : '.$e->getMessage());
}
return $this->redirectToRoute('app_admin_tarification');