Files
Classeo/backend/src/Scolarite/Infrastructure/Api/Provider/StudentAveragesProvider.php
Mathias STRASSER b7dc27f2a5
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled
feat: Calculer automatiquement les moyennes après chaque saisie de notes
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.
2026-04-04 02:25:00 +02:00

116 lines
3.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Scolarite\Infrastructure\Api\Provider;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Administration\Domain\Model\User\Role;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Infrastructure\Security\SecurityUser;
use App\Scolarite\Domain\Repository\StudentAverageRepository;
use App\Scolarite\Infrastructure\Api\Resource\StudentAveragesResource;
use App\Shared\Infrastructure\Tenant\TenantContext;
use function in_array;
use InvalidArgumentException;
use Override;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
/**
* @implements ProviderInterface<StudentAveragesResource>
*/
final readonly class StudentAveragesProvider implements ProviderInterface
{
public function __construct(
private StudentAverageRepository $studentAverageRepository,
private TenantContext $tenantContext,
private Security $security,
) {
}
#[Override]
public function provide(Operation $operation, array $uriVariables = [], array $context = []): StudentAveragesResource
{
if (!$this->tenantContext->hasTenant()) {
throw new UnauthorizedHttpException('Bearer', 'Tenant non défini.');
}
$user = $this->security->getUser();
if (!$user instanceof SecurityUser) {
throw new UnauthorizedHttpException('Bearer', 'Authentification requise.');
}
/** @var string $studentId */
$studentId = $uriVariables['studentId'];
try {
$userId = UserId::fromString($studentId);
} catch (InvalidArgumentException) {
throw new BadRequestHttpException('Identifiant d\'élève invalide.');
}
// L'élève peut voir ses propres moyennes, les enseignants et admins aussi
$isOwner = $user->userId() === $studentId;
$isStaff = $this->hasAnyRole($user->getRoles(), [
Role::ADMIN->value,
Role::PROF->value,
Role::VIE_SCOLAIRE->value,
]);
if (!$isOwner && !$isStaff) {
throw new AccessDeniedHttpException('Accès non autorisé aux moyennes de cet élève.');
}
$tenantId = $this->tenantContext->getCurrentTenantId();
/** @var array<string, mixed> $filters */
$filters = $context['filters'] ?? [];
/** @var string|null $periodId */
$periodId = $filters['periodId'] ?? null;
$resource = new StudentAveragesResource();
$resource->studentId = $studentId;
$resource->periodId = $periodId;
if ($periodId === null) {
return $resource;
}
$resource->subjectAverages = $this->studentAverageRepository->findDetailedSubjectAveragesForStudent(
$userId,
$periodId,
$tenantId,
);
$resource->generalAverage = $this->studentAverageRepository->findGeneralAverageForStudent(
$userId,
$periodId,
$tenantId,
);
return $resource;
}
/**
* @param list<string> $userRoles
* @param list<string> $allowedRoles
*/
private function hasAnyRole(array $userRoles, array $allowedRoles): bool
{
foreach ($userRoles as $role) {
if (in_array($role, $allowedRoles, true)) {
return true;
}
}
return false;
}
}