Les enseignants ont besoin de moyennes à jour immédiatement après la publication ou modification des notes, sans attendre un batch nocturne. Le système recalcule via Domain Events synchrones : statistiques d'évaluation (min/max/moyenne/médiane), moyennes matières pondérées (normalisation /20), et moyenne générale par élève. Les résultats sont stockés dans des tables dénormalisées avec cache Redis (TTL 5 min). Trois endpoints API exposent les données avec contrôle d'accès par rôle. Une commande console permet le backfill des données historiques au déploiement.
136 lines
4.4 KiB
PHP
136 lines
4.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Scolarite\Infrastructure\Console;
|
|
|
|
use App\Scolarite\Application\Service\RecalculerMoyennesService;
|
|
use App\Scolarite\Domain\Repository\EvaluationRepository;
|
|
use App\Shared\Infrastructure\Tenant\TenantConfig;
|
|
use App\Shared\Infrastructure\Tenant\TenantContext;
|
|
use App\Shared\Infrastructure\Tenant\TenantDatabaseSwitcher;
|
|
use App\Shared\Infrastructure\Tenant\TenantId;
|
|
use App\Shared\Infrastructure\Tenant\TenantRegistry;
|
|
|
|
use function count;
|
|
|
|
use Override;
|
|
|
|
use function sprintf;
|
|
|
|
use Symfony\Component\Console\Attribute\AsCommand;
|
|
use Symfony\Component\Console\Command\Command;
|
|
use Symfony\Component\Console\Input\InputInterface;
|
|
use Symfony\Component\Console\Input\InputOption;
|
|
use Symfony\Component\Console\Output\OutputInterface;
|
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
use Throwable;
|
|
|
|
/**
|
|
* Recalcule toutes les projections de moyennes à partir des données existantes.
|
|
*
|
|
* Usage:
|
|
* php bin/console app:recalculer-moyennes # Tous les tenants
|
|
* php bin/console app:recalculer-moyennes --tenant=UUID # Un seul tenant
|
|
*/
|
|
#[AsCommand(
|
|
name: 'app:recalculer-moyennes',
|
|
description: 'Recalcule les statistiques évaluations et moyennes élèves depuis les notes publiées',
|
|
)]
|
|
final class RecalculerToutesMoyennesCommand extends Command
|
|
{
|
|
public function __construct(
|
|
private readonly EvaluationRepository $evaluationRepository,
|
|
private readonly TenantRegistry $tenantRegistry,
|
|
private readonly TenantContext $tenantContext,
|
|
private readonly TenantDatabaseSwitcher $databaseSwitcher,
|
|
private readonly RecalculerMoyennesService $service,
|
|
) {
|
|
parent::__construct();
|
|
}
|
|
|
|
#[Override]
|
|
protected function configure(): void
|
|
{
|
|
$this->addOption('tenant', 't', InputOption::VALUE_REQUIRED, 'Limiter à un tenant spécifique (UUID)');
|
|
}
|
|
|
|
#[Override]
|
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
{
|
|
$io = new SymfonyStyle($input, $output);
|
|
$io->title('Recalcul des moyennes et statistiques');
|
|
|
|
/** @var string|null $tenantOption */
|
|
$tenantOption = $input->getOption('tenant');
|
|
|
|
if ($tenantOption !== null) {
|
|
$configs = [$this->tenantRegistry->getConfig(TenantId::fromString($tenantOption))];
|
|
} else {
|
|
$configs = $this->tenantRegistry->getAllConfigs();
|
|
}
|
|
|
|
$totalEvals = 0;
|
|
$totalErrors = 0;
|
|
|
|
foreach ($configs as $config) {
|
|
[$processed, $errors] = $this->processTenant($config, $io);
|
|
$totalEvals += $processed;
|
|
$totalErrors += $errors;
|
|
}
|
|
|
|
if ($totalErrors > 0) {
|
|
$io->warning(sprintf(
|
|
'%d évaluation(s) traitée(s), %d erreur(s).',
|
|
$totalEvals,
|
|
$totalErrors,
|
|
));
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
$io->success(sprintf('%d évaluation(s) traitée(s) avec succès.', $totalEvals));
|
|
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* @return array{int, int} [processed, errors]
|
|
*/
|
|
private function processTenant(TenantConfig $config, SymfonyStyle $io): array
|
|
{
|
|
$this->tenantContext->setCurrentTenant($config);
|
|
$this->databaseSwitcher->useTenantDatabase($config->databaseUrl);
|
|
|
|
$tenantId = \App\Shared\Domain\Tenant\TenantId::fromString((string) $config->tenantId);
|
|
|
|
$evaluations = $this->evaluationRepository->findAllWithPublishedGrades($tenantId);
|
|
|
|
if ($evaluations === []) {
|
|
$io->text(sprintf(' Tenant %s : aucune évaluation publiée.', $config->subdomain));
|
|
|
|
return [0, 0];
|
|
}
|
|
|
|
$io->text(sprintf(' Tenant %s : %d évaluation(s) publiée(s)', $config->subdomain, count($evaluations)));
|
|
|
|
$processed = 0;
|
|
$errors = 0;
|
|
|
|
foreach ($evaluations as $evaluation) {
|
|
try {
|
|
$this->service->recalculerStatistiquesEvaluation($evaluation->id, $tenantId);
|
|
$this->service->recalculerTousElevesPourEvaluation($evaluation->id, $tenantId);
|
|
++$processed;
|
|
} catch (Throwable $e) {
|
|
$io->error(sprintf(' Erreur évaluation %s : %s', $evaluation->id, $e->getMessage()));
|
|
++$errors;
|
|
}
|
|
}
|
|
|
|
$io->text(sprintf(' → %d traitée(s), %d erreur(s)', $processed, $errors));
|
|
|
|
return [$processed, $errors];
|
|
}
|
|
}
|