feat: Permettre à l'enseignant de créer et gérer ses évaluations
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

Les enseignants avaient besoin de définir les critères de notation
(barème, coefficient) avant de pouvoir saisir des notes. Sans cette
brique, le module Notes & Évaluations (Epic 6) ne pouvait pas démarrer.

L'évaluation est un agrégat du bounded context Scolarité avec deux
Value Objects (GradeScale 1-100, Coefficient 0.1-10). Le barème est
verrouillé dès qu'une note existe pour éviter les incohérences.
Un port EvaluationGradesChecker (stub pour l'instant) sera branché
sur le repository de notes dans la story 6.2.
This commit is contained in:
2026-03-23 23:56:37 +01:00
parent 8d950b0f3c
commit 93baeb1eaa
43 changed files with 4312 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Application\Command\CreateEvaluation;
final readonly class CreateEvaluationCommand
{
public function __construct(
public string $tenantId,
public string $classId,
public string $subjectId,
public string $teacherId,
public string $title,
public ?string $description,
public string $evaluationDate,
public int $gradeScale = 20,
public float $coefficient = 1.0,
) {
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Application\Command\CreateEvaluation;
use App\Administration\Domain\Model\SchoolClass\ClassId;
use App\Administration\Domain\Model\Subject\SubjectId;
use App\Administration\Domain\Model\User\UserId;
use App\Scolarite\Application\Port\EnseignantAffectationChecker;
use App\Scolarite\Domain\Exception\EnseignantNonAffecteException;
use App\Scolarite\Domain\Model\Evaluation\Coefficient;
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
use App\Scolarite\Domain\Model\Evaluation\GradeScale;
use App\Scolarite\Domain\Repository\EvaluationRepository;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler(bus: 'command.bus')]
final readonly class CreateEvaluationHandler
{
public function __construct(
private EvaluationRepository $evaluationRepository,
private EnseignantAffectationChecker $affectationChecker,
private Clock $clock,
) {
}
public function __invoke(CreateEvaluationCommand $command): Evaluation
{
$tenantId = TenantId::fromString($command->tenantId);
$classId = ClassId::fromString($command->classId);
$subjectId = SubjectId::fromString($command->subjectId);
$teacherId = UserId::fromString($command->teacherId);
$now = $this->clock->now();
if (!$this->affectationChecker->estAffecte($teacherId, $classId, $subjectId, $tenantId)) {
throw EnseignantNonAffecteException::pourClasseEtMatiere($teacherId, $classId, $subjectId);
}
$evaluation = Evaluation::creer(
tenantId: $tenantId,
classId: $classId,
subjectId: $subjectId,
teacherId: $teacherId,
title: $command->title,
description: $command->description,
evaluationDate: new DateTimeImmutable($command->evaluationDate),
gradeScale: new GradeScale($command->gradeScale),
coefficient: new Coefficient($command->coefficient),
now: $now,
);
$this->evaluationRepository->save($evaluation);
return $evaluation;
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Application\Command\DeleteEvaluation;
final readonly class DeleteEvaluationCommand
{
public function __construct(
public string $tenantId,
public string $evaluationId,
public string $teacherId,
) {
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Application\Command\DeleteEvaluation;
use App\Administration\Domain\Model\User\UserId;
use App\Scolarite\Domain\Exception\NonProprietaireDeLEvaluationException;
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
use App\Scolarite\Domain\Repository\EvaluationRepository;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler(bus: 'command.bus')]
final readonly class DeleteEvaluationHandler
{
public function __construct(
private EvaluationRepository $evaluationRepository,
private Clock $clock,
) {
}
public function __invoke(DeleteEvaluationCommand $command): Evaluation
{
$tenantId = TenantId::fromString($command->tenantId);
$evaluationId = EvaluationId::fromString($command->evaluationId);
$evaluation = $this->evaluationRepository->get($evaluationId, $tenantId);
$teacherId = UserId::fromString($command->teacherId);
if ((string) $evaluation->teacherId !== (string) $teacherId) {
throw NonProprietaireDeLEvaluationException::withId($evaluationId);
}
$evaluation->supprimer($this->clock->now());
$this->evaluationRepository->save($evaluation);
return $evaluation;
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Application\Command\UpdateEvaluation;
final readonly class UpdateEvaluationCommand
{
public function __construct(
public string $tenantId,
public string $evaluationId,
public string $teacherId,
public string $title,
public ?string $description,
public string $evaluationDate,
public float $coefficient = 1.0,
public ?int $gradeScale = null,
) {
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Application\Command\UpdateEvaluation;
use App\Administration\Domain\Model\User\UserId;
use App\Scolarite\Application\Port\EvaluationGradesChecker;
use App\Scolarite\Domain\Exception\NonProprietaireDeLEvaluationException;
use App\Scolarite\Domain\Model\Evaluation\Coefficient;
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
use App\Scolarite\Domain\Model\Evaluation\GradeScale;
use App\Scolarite\Domain\Repository\EvaluationRepository;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler(bus: 'command.bus')]
final readonly class UpdateEvaluationHandler
{
public function __construct(
private EvaluationRepository $evaluationRepository,
private EvaluationGradesChecker $gradesChecker,
private Clock $clock,
) {
}
public function __invoke(UpdateEvaluationCommand $command): Evaluation
{
$tenantId = TenantId::fromString($command->tenantId);
$evaluationId = EvaluationId::fromString($command->evaluationId);
$now = $this->clock->now();
$evaluation = $this->evaluationRepository->get($evaluationId, $tenantId);
$teacherId = UserId::fromString($command->teacherId);
if ((string) $evaluation->teacherId !== (string) $teacherId) {
throw NonProprietaireDeLEvaluationException::withId($evaluationId);
}
$hasGrades = $this->gradesChecker->hasGrades($evaluationId, $tenantId);
$evaluation->modifier(
title: $command->title,
description: $command->description,
coefficient: new Coefficient($command->coefficient),
evaluationDate: new DateTimeImmutable($command->evaluationDate),
gradeScale: $command->gradeScale !== null ? new GradeScale($command->gradeScale) : null,
hasGrades: $hasGrades,
now: $now,
);
$this->evaluationRepository->save($evaluation);
return $evaluation;
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Application\Port;
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
use App\Shared\Domain\Tenant\TenantId;
interface EvaluationGradesChecker
{
public function hasGrades(EvaluationId $evaluationId, TenantId $tenantId): bool;
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Domain\Event;
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
use App\Shared\Domain\DomainEvent;
use DateTimeImmutable;
use Override;
use Ramsey\Uuid\UuidInterface;
final readonly class EvaluationCreee implements DomainEvent
{
public function __construct(
public EvaluationId $evaluationId,
public string $classId,
public string $subjectId,
public string $teacherId,
public string $title,
public DateTimeImmutable $evaluationDate,
private DateTimeImmutable $occurredOn,
) {
}
#[Override]
public function occurredOn(): DateTimeImmutable
{
return $this->occurredOn;
}
#[Override]
public function aggregateId(): UuidInterface
{
return $this->evaluationId->value;
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Domain\Event;
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
use App\Shared\Domain\DomainEvent;
use DateTimeImmutable;
use Override;
use Ramsey\Uuid\UuidInterface;
final readonly class EvaluationModifiee implements DomainEvent
{
public function __construct(
public EvaluationId $evaluationId,
public string $title,
public DateTimeImmutable $evaluationDate,
private DateTimeImmutable $occurredOn,
) {
}
#[Override]
public function occurredOn(): DateTimeImmutable
{
return $this->occurredOn;
}
#[Override]
public function aggregateId(): UuidInterface
{
return $this->evaluationId->value;
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Domain\Event;
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
use App\Shared\Domain\DomainEvent;
use DateTimeImmutable;
use Override;
use Ramsey\Uuid\UuidInterface;
final readonly class EvaluationSupprimee implements DomainEvent
{
public function __construct(
public EvaluationId $evaluationId,
private DateTimeImmutable $occurredOn,
) {
}
#[Override]
public function occurredOn(): DateTimeImmutable
{
return $this->occurredOn;
}
#[Override]
public function aggregateId(): UuidInterface
{
return $this->evaluationId->value;
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Domain\Exception;
use DomainException;
use function sprintf;
final class BaremeInvalideException extends DomainException
{
public static function avecValeur(int $maxValue): self
{
return new self(sprintf(
'Le barème doit être compris entre 1 et 100, %d donné.',
$maxValue,
));
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Domain\Exception;
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
use DomainException;
use function sprintf;
final class BaremeNonModifiableException extends DomainException
{
public static function carNotesExistantes(EvaluationId $id): self
{
return new self(sprintf(
'Le barème de l\'évaluation "%s" ne peut pas être modifié car des notes ont déjà été saisies.',
$id,
));
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Domain\Exception;
use DomainException;
use function sprintf;
final class CoefficientInvalideException extends DomainException
{
public static function avecValeur(float $value): self
{
return new self(sprintf(
'Le coefficient doit être compris entre 0.1 et 10, %s donné.',
$value,
));
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Domain\Exception;
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
use DomainException;
use function sprintf;
final class EvaluationDejaSupprimeeException extends DomainException
{
public static function withId(EvaluationId $id): self
{
return new self(sprintf(
'L\'évaluation avec l\'ID "%s" est déjà supprimée.',
$id,
));
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Domain\Exception;
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
use DomainException;
use function sprintf;
final class EvaluationNotFoundException extends DomainException
{
public static function withId(EvaluationId $id): self
{
return new self(sprintf(
'L\'évaluation avec l\'ID "%s" n\'a pas été trouvée.',
$id,
));
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Domain\Exception;
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
use DomainException;
use function sprintf;
final class NonProprietaireDeLEvaluationException extends DomainException
{
public static function withId(EvaluationId $id): self
{
return new self(sprintf(
'Vous n\'êtes pas le propriétaire de l\'évaluation "%s".',
$id,
));
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Domain\Model\Evaluation;
use App\Scolarite\Domain\Exception\CoefficientInvalideException;
final readonly class Coefficient
{
public function __construct(
public float $value,
) {
if ($value < 0.1 || $value > 10) {
throw CoefficientInvalideException::avecValeur($value);
}
}
public function equals(self $other): bool
{
return $this->value === $other->value;
}
}

View File

@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Domain\Model\Evaluation;
use App\Administration\Domain\Model\SchoolClass\ClassId;
use App\Administration\Domain\Model\Subject\SubjectId;
use App\Administration\Domain\Model\User\UserId;
use App\Scolarite\Domain\Event\EvaluationCreee;
use App\Scolarite\Domain\Event\EvaluationModifiee;
use App\Scolarite\Domain\Event\EvaluationSupprimee;
use App\Scolarite\Domain\Exception\BaremeNonModifiableException;
use App\Scolarite\Domain\Exception\EvaluationDejaSupprimeeException;
use App\Shared\Domain\AggregateRoot;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
final class Evaluation extends AggregateRoot
{
public private(set) DateTimeImmutable $updatedAt;
private function __construct(
public private(set) EvaluationId $id,
public private(set) TenantId $tenantId,
public private(set) ClassId $classId,
public private(set) SubjectId $subjectId,
public private(set) UserId $teacherId,
public private(set) string $title,
public private(set) ?string $description,
public private(set) DateTimeImmutable $evaluationDate,
public private(set) GradeScale $gradeScale,
public private(set) Coefficient $coefficient,
public private(set) EvaluationStatus $status,
public private(set) DateTimeImmutable $createdAt,
) {
$this->updatedAt = $createdAt;
}
public static function creer(
TenantId $tenantId,
ClassId $classId,
SubjectId $subjectId,
UserId $teacherId,
string $title,
?string $description,
DateTimeImmutable $evaluationDate,
GradeScale $gradeScale,
Coefficient $coefficient,
DateTimeImmutable $now,
): self {
$evaluation = new self(
id: EvaluationId::generate(),
tenantId: $tenantId,
classId: $classId,
subjectId: $subjectId,
teacherId: $teacherId,
title: $title,
description: $description,
evaluationDate: $evaluationDate,
gradeScale: $gradeScale,
coefficient: $coefficient,
status: EvaluationStatus::PUBLISHED,
createdAt: $now,
);
$evaluation->recordEvent(new EvaluationCreee(
evaluationId: $evaluation->id,
classId: (string) $classId,
subjectId: (string) $subjectId,
teacherId: (string) $teacherId,
title: $title,
evaluationDate: $evaluationDate,
occurredOn: $now,
));
return $evaluation;
}
public function modifier(
string $title,
?string $description,
Coefficient $coefficient,
DateTimeImmutable $evaluationDate,
?GradeScale $gradeScale,
bool $hasGrades,
DateTimeImmutable $now,
): void {
if ($this->status === EvaluationStatus::DELETED) {
throw EvaluationDejaSupprimeeException::withId($this->id);
}
if ($gradeScale !== null && !$this->gradeScale->equals($gradeScale) && $hasGrades) {
throw BaremeNonModifiableException::carNotesExistantes($this->id);
}
$this->title = $title;
$this->description = $description;
$this->coefficient = $coefficient;
$this->evaluationDate = $evaluationDate;
if ($gradeScale !== null && !$hasGrades) {
$this->gradeScale = $gradeScale;
}
$this->updatedAt = $now;
$this->recordEvent(new EvaluationModifiee(
evaluationId: $this->id,
title: $title,
evaluationDate: $evaluationDate,
occurredOn: $now,
));
}
public function supprimer(DateTimeImmutable $now): void
{
if ($this->status === EvaluationStatus::DELETED) {
throw EvaluationDejaSupprimeeException::withId($this->id);
}
$this->status = EvaluationStatus::DELETED;
$this->updatedAt = $now;
$this->recordEvent(new EvaluationSupprimee(
evaluationId: $this->id,
occurredOn: $now,
));
}
/**
* @internal Pour usage Infrastructure uniquement
*/
public static function reconstitute(
EvaluationId $id,
TenantId $tenantId,
ClassId $classId,
SubjectId $subjectId,
UserId $teacherId,
string $title,
?string $description,
DateTimeImmutable $evaluationDate,
GradeScale $gradeScale,
Coefficient $coefficient,
EvaluationStatus $status,
DateTimeImmutable $createdAt,
DateTimeImmutable $updatedAt,
): self {
$evaluation = new self(
id: $id,
tenantId: $tenantId,
classId: $classId,
subjectId: $subjectId,
teacherId: $teacherId,
title: $title,
description: $description,
evaluationDate: $evaluationDate,
gradeScale: $gradeScale,
coefficient: $coefficient,
status: $status,
createdAt: $createdAt,
);
$evaluation->updatedAt = $updatedAt;
return $evaluation;
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Domain\Model\Evaluation;
use App\Shared\Domain\EntityId;
final readonly class EvaluationId extends EntityId
{
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Domain\Model\Evaluation;
enum EvaluationStatus: string
{
case PUBLISHED = 'published';
case DELETED = 'deleted';
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Domain\Model\Evaluation;
use App\Scolarite\Domain\Exception\BaremeInvalideException;
use function round;
final readonly class GradeScale
{
public function __construct(
public int $maxValue,
) {
if ($maxValue < 1 || $maxValue > 100) {
throw BaremeInvalideException::avecValeur($maxValue);
}
}
public function convertTo20(float $grade): float
{
return round(($grade / $this->maxValue) * 20, 2);
}
public function equals(self $other): bool
{
return $this->maxValue === $other->maxValue;
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Domain\Repository;
use App\Administration\Domain\Model\SchoolClass\ClassId;
use App\Administration\Domain\Model\User\UserId;
use App\Scolarite\Domain\Exception\EvaluationNotFoundException;
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
use App\Shared\Domain\Tenant\TenantId;
interface EvaluationRepository
{
public function save(Evaluation $evaluation): void;
/** @throws EvaluationNotFoundException */
public function get(EvaluationId $id, TenantId $tenantId): Evaluation;
public function findById(EvaluationId $id, TenantId $tenantId): ?Evaluation;
/** @return array<Evaluation> */
public function findByTeacher(UserId $teacherId, TenantId $tenantId): array;
/** @return array<Evaluation> */
public function findByTeacherAndClass(UserId $teacherId, ClassId $classId, TenantId $tenantId): array;
/** @return array<Evaluation> */
public function findByClass(ClassId $classId, TenantId $tenantId): array;
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Infrastructure\Api\Processor;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Administration\Infrastructure\Security\SecurityUser;
use App\Scolarite\Application\Command\CreateEvaluation\CreateEvaluationCommand;
use App\Scolarite\Application\Command\CreateEvaluation\CreateEvaluationHandler;
use App\Scolarite\Domain\Exception\BaremeInvalideException;
use App\Scolarite\Domain\Exception\CoefficientInvalideException;
use App\Scolarite\Domain\Exception\EnseignantNonAffecteException;
use App\Scolarite\Infrastructure\Api\Resource\EvaluationResource;
use App\Shared\Infrastructure\Tenant\TenantContext;
use Override;
use Ramsey\Uuid\Exception\InvalidUuidStringException;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\Messenger\MessageBusInterface;
/**
* @implements ProcessorInterface<EvaluationResource, EvaluationResource>
*/
final readonly class CreateEvaluationProcessor implements ProcessorInterface
{
public function __construct(
private CreateEvaluationHandler $handler,
private TenantContext $tenantContext,
private MessageBusInterface $eventBus,
private Security $security,
) {
}
/**
* @param EvaluationResource $data
*/
#[Override]
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): EvaluationResource
{
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.');
}
try {
$command = new CreateEvaluationCommand(
tenantId: (string) $this->tenantContext->getCurrentTenantId(),
classId: $data->classId ?? '',
subjectId: $data->subjectId ?? '',
teacherId: $user->userId(),
title: $data->title ?? '',
description: $data->description,
evaluationDate: $data->evaluationDate ?? '',
gradeScale: $data->gradeScale ?? 20,
coefficient: $data->coefficient ?? 1.0,
);
$evaluation = ($this->handler)($command);
foreach ($evaluation->pullDomainEvents() as $event) {
$this->eventBus->dispatch($event);
}
return EvaluationResource::fromDomain($evaluation);
} catch (EnseignantNonAffecteException|BaremeInvalideException|CoefficientInvalideException $e) {
throw new BadRequestHttpException($e->getMessage());
} catch (InvalidUuidStringException $e) {
throw new BadRequestHttpException('UUID invalide : ' . $e->getMessage());
}
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Infrastructure\Api\Processor;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Administration\Infrastructure\Security\SecurityUser;
use App\Scolarite\Application\Command\DeleteEvaluation\DeleteEvaluationCommand;
use App\Scolarite\Application\Command\DeleteEvaluation\DeleteEvaluationHandler;
use App\Scolarite\Domain\Exception\EvaluationDejaSupprimeeException;
use App\Scolarite\Domain\Exception\EvaluationNotFoundException;
use App\Scolarite\Domain\Exception\NonProprietaireDeLEvaluationException;
use App\Scolarite\Infrastructure\Api\Resource\EvaluationResource;
use App\Shared\Infrastructure\Tenant\TenantContext;
use Override;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\Messenger\MessageBusInterface;
/**
* @implements ProcessorInterface<EvaluationResource, EvaluationResource>
*/
final readonly class DeleteEvaluationProcessor implements ProcessorInterface
{
public function __construct(
private DeleteEvaluationHandler $handler,
private TenantContext $tenantContext,
private MessageBusInterface $eventBus,
private Security $security,
) {
}
/**
* @param EvaluationResource $data
*/
#[Override]
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): EvaluationResource
{
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 $id */
$id = $uriVariables['id'];
try {
$command = new DeleteEvaluationCommand(
tenantId: (string) $this->tenantContext->getCurrentTenantId(),
evaluationId: $id,
teacherId: $user->userId(),
);
$evaluation = ($this->handler)($command);
foreach ($evaluation->pullDomainEvents() as $event) {
$this->eventBus->dispatch($event);
}
return EvaluationResource::fromDomain($evaluation);
} catch (EvaluationNotFoundException $e) {
throw new NotFoundHttpException($e->getMessage());
} catch (NonProprietaireDeLEvaluationException $e) {
throw new AccessDeniedHttpException($e->getMessage());
} catch (EvaluationDejaSupprimeeException $e) {
throw new BadRequestHttpException($e->getMessage());
}
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Infrastructure\Api\Processor;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Administration\Infrastructure\Security\SecurityUser;
use App\Scolarite\Application\Command\UpdateEvaluation\UpdateEvaluationCommand;
use App\Scolarite\Application\Command\UpdateEvaluation\UpdateEvaluationHandler;
use App\Scolarite\Domain\Exception\BaremeInvalideException;
use App\Scolarite\Domain\Exception\BaremeNonModifiableException;
use App\Scolarite\Domain\Exception\CoefficientInvalideException;
use App\Scolarite\Domain\Exception\EvaluationDejaSupprimeeException;
use App\Scolarite\Domain\Exception\EvaluationNotFoundException;
use App\Scolarite\Domain\Exception\NonProprietaireDeLEvaluationException;
use App\Scolarite\Infrastructure\Api\Resource\EvaluationResource;
use App\Shared\Infrastructure\Tenant\TenantContext;
use Override;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\Messenger\MessageBusInterface;
/**
* @implements ProcessorInterface<EvaluationResource, EvaluationResource>
*/
final readonly class UpdateEvaluationProcessor implements ProcessorInterface
{
public function __construct(
private UpdateEvaluationHandler $handler,
private TenantContext $tenantContext,
private MessageBusInterface $eventBus,
private Security $security,
) {
}
/**
* @param EvaluationResource $data
*/
#[Override]
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): EvaluationResource
{
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 $id */
$id = $uriVariables['id'];
try {
$command = new UpdateEvaluationCommand(
tenantId: (string) $this->tenantContext->getCurrentTenantId(),
evaluationId: $id,
teacherId: $user->userId(),
title: $data->title ?? '',
description: $data->description,
evaluationDate: $data->evaluationDate ?? '',
coefficient: $data->coefficient ?? 1.0,
gradeScale: $data->gradeScale,
);
$evaluation = ($this->handler)($command);
foreach ($evaluation->pullDomainEvents() as $event) {
$this->eventBus->dispatch($event);
}
return EvaluationResource::fromDomain($evaluation);
} catch (EvaluationNotFoundException $e) {
throw new NotFoundHttpException($e->getMessage());
} catch (NonProprietaireDeLEvaluationException $e) {
throw new AccessDeniedHttpException($e->getMessage());
} catch (EvaluationDejaSupprimeeException|BaremeNonModifiableException|BaremeInvalideException|CoefficientInvalideException $e) {
throw new BadRequestHttpException($e->getMessage());
}
}
}

View File

@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Infrastructure\Api\Provider;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Administration\Domain\Model\SchoolClass\ClassId;
use App\Administration\Domain\Model\Subject\SubjectId;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Infrastructure\Security\SecurityUser;
use App\Scolarite\Domain\Model\Evaluation\Coefficient;
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
use App\Scolarite\Domain\Model\Evaluation\EvaluationStatus;
use App\Scolarite\Domain\Model\Evaluation\GradeScale;
use App\Scolarite\Infrastructure\Api\Resource\EvaluationResource;
use App\Shared\Domain\Tenant\TenantId;
use App\Shared\Infrastructure\Tenant\TenantContext;
use function array_map;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;
use function is_string;
use Override;
use Ramsey\Uuid\Exception\InvalidUuidStringException;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
/**
* @implements ProviderInterface<EvaluationResource>
*/
final readonly class EvaluationCollectionProvider implements ProviderInterface
{
public function __construct(
private Connection $connection,
private TenantContext $tenantContext,
private Security $security,
) {
}
/** @return array<EvaluationResource> */
#[Override]
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array
{
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.');
}
$tenantId = (string) $this->tenantContext->getCurrentTenantId();
$teacherId = $user->userId();
/** @var array<string, mixed> $filters */
$filters = $context['filters'] ?? [];
$classIdFilter = $filters['classId'] ?? null;
$search = $filters['search'] ?? null;
$sql = 'SELECT e.*, c.name AS class_name, s.name AS subject_name
FROM evaluations e
LEFT JOIN school_classes c ON c.id = e.class_id
LEFT JOIN subjects s ON s.id = e.subject_id
WHERE e.teacher_id = :teacher_id
AND e.tenant_id = :tenant_id
AND e.status != :deleted';
$params = [
'teacher_id' => $teacherId,
'tenant_id' => $tenantId,
'deleted' => EvaluationStatus::DELETED->value,
];
if (is_string($classIdFilter) && $classIdFilter !== '') {
try {
ClassId::fromString($classIdFilter);
} catch (InvalidUuidStringException $e) {
throw new BadRequestHttpException('UUID de classe invalide : ' . $e->getMessage());
}
$sql .= ' AND e.class_id = :class_id';
$params['class_id'] = $classIdFilter;
}
if (is_string($search) && $search !== '') {
$sql .= ' AND e.title ILIKE :search';
$params['search'] = '%' . $search . '%';
}
$sql .= ' ORDER BY e.evaluation_date DESC';
$rows = $this->connection->fetchAllAssociative($sql, $params);
return array_map(static function (array $row): EvaluationResource {
/** @var string $className */
$className = $row['class_name'] ?? null;
/** @var string $subjectName */
$subjectName = $row['subject_name'] ?? null;
$evaluation = self::hydrateEvaluation($row);
return EvaluationResource::fromDomain($evaluation, $className, $subjectName);
}, $rows);
}
/** @param array<string, mixed> $row */
private static function hydrateEvaluation(array $row): Evaluation
{
/** @var string $id */
$id = $row['id'];
/** @var string $tenantId */
$tenantId = $row['tenant_id'];
/** @var string $classId */
$classId = $row['class_id'];
/** @var string $subjectId */
$subjectId = $row['subject_id'];
/** @var string $teacherId */
$teacherId = $row['teacher_id'];
/** @var string $title */
$title = $row['title'];
/** @var string|null $description */
$description = $row['description'];
/** @var string $evaluationDate */
$evaluationDate = $row['evaluation_date'];
/** @var string|int $gradeScaleRaw */
$gradeScaleRaw = $row['grade_scale'];
/** @var string|float $coefficientRaw */
$coefficientRaw = $row['coefficient'];
/** @var string $status */
$status = $row['status'];
/** @var string $createdAt */
$createdAt = $row['created_at'];
/** @var string $updatedAt */
$updatedAt = $row['updated_at'];
return Evaluation::reconstitute(
id: EvaluationId::fromString($id),
tenantId: TenantId::fromString($tenantId),
classId: ClassId::fromString($classId),
subjectId: SubjectId::fromString($subjectId),
teacherId: UserId::fromString($teacherId),
title: $title,
description: $description,
evaluationDate: new DateTimeImmutable($evaluationDate),
gradeScale: new GradeScale((int) $gradeScaleRaw),
coefficient: new Coefficient((float) $coefficientRaw),
status: EvaluationStatus::from($status),
createdAt: new DateTimeImmutable($createdAt),
updatedAt: new DateTimeImmutable($updatedAt),
);
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Infrastructure\Api\Provider;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Administration\Domain\Repository\ClassRepository;
use App\Administration\Domain\Repository\SubjectRepository;
use App\Administration\Infrastructure\Security\SecurityUser;
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
use App\Scolarite\Domain\Repository\EvaluationRepository;
use App\Scolarite\Infrastructure\Api\Resource\EvaluationResource;
use App\Shared\Infrastructure\Tenant\TenantContext;
use Override;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
/**
* @implements ProviderInterface<EvaluationResource>
*/
final readonly class EvaluationItemProvider implements ProviderInterface
{
public function __construct(
private EvaluationRepository $evaluationRepository,
private TenantContext $tenantContext,
private Security $security,
private ClassRepository $classRepository,
private SubjectRepository $subjectRepository,
) {
}
#[Override]
public function provide(Operation $operation, array $uriVariables = [], array $context = []): EvaluationResource
{
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 $id */
$id = $uriVariables['id'];
$evaluation = $this->evaluationRepository->findById(
EvaluationId::fromString($id),
$this->tenantContext->getCurrentTenantId(),
);
if ($evaluation === null) {
throw new NotFoundHttpException('Évaluation non trouvée.');
}
if ((string) $evaluation->teacherId !== $user->userId()) {
throw new AccessDeniedHttpException('Vous n\'êtes pas le propriétaire de cette évaluation.');
}
$class = $this->classRepository->findById($evaluation->classId);
$subject = $this->subjectRepository->findById($evaluation->subjectId);
return EvaluationResource::fromDomain(
$evaluation,
$class !== null ? (string) $class->name : null,
$subject !== null ? (string) $subject->name : null,
);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Infrastructure\Api\Provider;
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
final readonly class EvaluationWithNames
{
public function __construct(
public Evaluation $evaluation,
public ?string $className,
public ?string $subjectName,
) {
}
}

View File

@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Infrastructure\Api\Resource;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
use App\Scolarite\Infrastructure\Api\Processor\CreateEvaluationProcessor;
use App\Scolarite\Infrastructure\Api\Processor\DeleteEvaluationProcessor;
use App\Scolarite\Infrastructure\Api\Processor\UpdateEvaluationProcessor;
use App\Scolarite\Infrastructure\Api\Provider\EvaluationCollectionProvider;
use App\Scolarite\Infrastructure\Api\Provider\EvaluationItemProvider;
use DateTimeImmutable;
use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
shortName: 'Evaluation',
operations: [
new GetCollection(
uriTemplate: '/evaluations',
provider: EvaluationCollectionProvider::class,
name: 'get_evaluation_list',
),
new Get(
uriTemplate: '/evaluations/{id}',
provider: EvaluationItemProvider::class,
name: 'get_evaluation',
),
new Post(
uriTemplate: '/evaluations',
processor: CreateEvaluationProcessor::class,
validationContext: ['groups' => ['Default', 'create']],
name: 'create_evaluation',
),
new Patch(
uriTemplate: '/evaluations/{id}',
provider: EvaluationItemProvider::class,
processor: UpdateEvaluationProcessor::class,
validationContext: ['groups' => ['Default', 'update']],
name: 'update_evaluation',
),
new Delete(
uriTemplate: '/evaluations/{id}',
provider: EvaluationItemProvider::class,
processor: DeleteEvaluationProcessor::class,
name: 'delete_evaluation',
),
],
)]
final class EvaluationResource
{
#[ApiProperty(identifier: true)]
public ?string $id = null;
#[Assert\NotBlank(message: 'La classe est requise.', groups: ['create'])]
#[Assert\Uuid(message: 'L\'identifiant de la classe doit être un UUID valide.', groups: ['create'])]
public ?string $classId = null;
#[Assert\NotBlank(message: 'La matière est requise.', groups: ['create'])]
#[Assert\Uuid(message: 'L\'identifiant de la matière doit être un UUID valide.', groups: ['create'])]
public ?string $subjectId = null;
public ?string $teacherId = null;
#[Assert\NotBlank(message: 'Le titre est requis.', groups: ['create', 'update'])]
#[Assert\Length(max: 255, maxMessage: 'Le titre ne peut pas dépasser 255 caractères.')]
public ?string $title = null;
public ?string $description = null;
#[Assert\NotBlank(message: 'La date d\'évaluation est requise.', groups: ['create', 'update'])]
public ?string $evaluationDate = null;
#[Assert\Range(min: 1, max: 100, notInRangeMessage: 'Le barème doit être compris entre 1 et 100.')]
public ?int $gradeScale = null;
#[Assert\Range(min: 0.1, max: 10, notInRangeMessage: 'Le coefficient doit être compris entre 0.1 et 10.')]
public ?float $coefficient = null;
public ?string $status = null;
public ?string $className = null;
public ?string $subjectName = null;
public ?DateTimeImmutable $createdAt = null;
public ?DateTimeImmutable $updatedAt = null;
public static function fromDomain(
Evaluation $evaluation,
?string $className = null,
?string $subjectName = null,
): self {
$resource = new self();
$resource->id = (string) $evaluation->id;
$resource->classId = (string) $evaluation->classId;
$resource->subjectId = (string) $evaluation->subjectId;
$resource->teacherId = (string) $evaluation->teacherId;
$resource->title = $evaluation->title;
$resource->description = $evaluation->description;
$resource->evaluationDate = $evaluation->evaluationDate->format('Y-m-d');
$resource->gradeScale = $evaluation->gradeScale->maxValue;
$resource->coefficient = $evaluation->coefficient->value;
$resource->status = $evaluation->status->value;
$resource->className = $className;
$resource->subjectName = $subjectName;
$resource->createdAt = $evaluation->createdAt;
$resource->updatedAt = $evaluation->updatedAt;
return $resource;
}
}

View File

@@ -0,0 +1,207 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Infrastructure\Persistence\Doctrine;
use App\Administration\Domain\Model\SchoolClass\ClassId;
use App\Administration\Domain\Model\Subject\SubjectId;
use App\Administration\Domain\Model\User\UserId;
use App\Scolarite\Domain\Exception\EvaluationNotFoundException;
use App\Scolarite\Domain\Model\Evaluation\Coefficient;
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
use App\Scolarite\Domain\Model\Evaluation\EvaluationStatus;
use App\Scolarite\Domain\Model\Evaluation\GradeScale;
use App\Scolarite\Domain\Repository\EvaluationRepository;
use App\Shared\Domain\Tenant\TenantId;
use function array_map;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;
use Override;
final readonly class DoctrineEvaluationRepository implements EvaluationRepository
{
public function __construct(
private Connection $connection,
) {
}
#[Override]
public function save(Evaluation $evaluation): void
{
$this->connection->executeStatement(
'INSERT INTO evaluations (id, tenant_id, class_id, subject_id, teacher_id, title, description, evaluation_date, grade_scale, coefficient, status, created_at, updated_at)
VALUES (:id, :tenant_id, :class_id, :subject_id, :teacher_id, :title, :description, :evaluation_date, :grade_scale, :coefficient, :status, :created_at, :updated_at)
ON CONFLICT (id) DO UPDATE SET
title = EXCLUDED.title,
description = EXCLUDED.description,
evaluation_date = EXCLUDED.evaluation_date,
grade_scale = EXCLUDED.grade_scale,
coefficient = EXCLUDED.coefficient,
status = EXCLUDED.status,
updated_at = EXCLUDED.updated_at',
[
'id' => (string) $evaluation->id,
'tenant_id' => (string) $evaluation->tenantId,
'class_id' => (string) $evaluation->classId,
'subject_id' => (string) $evaluation->subjectId,
'teacher_id' => (string) $evaluation->teacherId,
'title' => $evaluation->title,
'description' => $evaluation->description,
'evaluation_date' => $evaluation->evaluationDate->format('Y-m-d'),
'grade_scale' => $evaluation->gradeScale->maxValue,
'coefficient' => $evaluation->coefficient->value,
'status' => $evaluation->status->value,
'created_at' => $evaluation->createdAt->format(DateTimeImmutable::ATOM),
'updated_at' => $evaluation->updatedAt->format(DateTimeImmutable::ATOM),
],
);
}
#[Override]
public function get(EvaluationId $id, TenantId $tenantId): Evaluation
{
$evaluation = $this->findById($id, $tenantId);
if ($evaluation === null) {
throw EvaluationNotFoundException::withId($id);
}
return $evaluation;
}
#[Override]
public function findById(EvaluationId $id, TenantId $tenantId): ?Evaluation
{
$row = $this->connection->fetchAssociative(
'SELECT * FROM evaluations WHERE id = :id AND tenant_id = :tenant_id',
['id' => (string) $id, 'tenant_id' => (string) $tenantId],
);
if ($row === false) {
return null;
}
return $this->hydrate($row);
}
#[Override]
public function findByTeacher(UserId $teacherId, TenantId $tenantId): array
{
$rows = $this->connection->fetchAllAssociative(
'SELECT e.*, c.name AS class_name, s.name AS subject_name
FROM evaluations e
LEFT JOIN school_classes c ON c.id = e.class_id
LEFT JOIN subjects s ON s.id = e.subject_id
WHERE e.teacher_id = :teacher_id
AND e.tenant_id = :tenant_id
AND e.status != :deleted
ORDER BY e.evaluation_date DESC',
[
'teacher_id' => (string) $teacherId,
'tenant_id' => (string) $tenantId,
'deleted' => EvaluationStatus::DELETED->value,
],
);
return array_map($this->hydrate(...), $rows);
}
#[Override]
public function findByTeacherAndClass(UserId $teacherId, ClassId $classId, TenantId $tenantId): array
{
$rows = $this->connection->fetchAllAssociative(
'SELECT e.*, c.name AS class_name, s.name AS subject_name
FROM evaluations e
LEFT JOIN school_classes c ON c.id = e.class_id
LEFT JOIN subjects s ON s.id = e.subject_id
WHERE e.teacher_id = :teacher_id
AND e.class_id = :class_id
AND e.tenant_id = :tenant_id
AND e.status != :deleted
ORDER BY e.evaluation_date DESC',
[
'teacher_id' => (string) $teacherId,
'class_id' => (string) $classId,
'tenant_id' => (string) $tenantId,
'deleted' => EvaluationStatus::DELETED->value,
],
);
return array_map($this->hydrate(...), $rows);
}
#[Override]
public function findByClass(ClassId $classId, TenantId $tenantId): array
{
$rows = $this->connection->fetchAllAssociative(
'SELECT e.*, c.name AS class_name, s.name AS subject_name
FROM evaluations e
LEFT JOIN school_classes c ON c.id = e.class_id
LEFT JOIN subjects s ON s.id = e.subject_id
WHERE e.class_id = :class_id
AND e.tenant_id = :tenant_id
AND e.status != :deleted
ORDER BY e.evaluation_date DESC',
[
'class_id' => (string) $classId,
'tenant_id' => (string) $tenantId,
'deleted' => EvaluationStatus::DELETED->value,
],
);
return array_map($this->hydrate(...), $rows);
}
/** @param array<string, mixed> $row */
private function hydrate(array $row): Evaluation
{
/** @var string $id */
$id = $row['id'];
/** @var string $tenantId */
$tenantId = $row['tenant_id'];
/** @var string $classId */
$classId = $row['class_id'];
/** @var string $subjectId */
$subjectId = $row['subject_id'];
/** @var string $teacherId */
$teacherId = $row['teacher_id'];
/** @var string $title */
$title = $row['title'];
/** @var string|null $description */
$description = $row['description'];
/** @var string $evaluationDate */
$evaluationDate = $row['evaluation_date'];
/** @var string|int $gradeScaleRaw */
$gradeScaleRaw = $row['grade_scale'];
$gradeScale = (int) $gradeScaleRaw;
/** @var string|float $coefficientRaw */
$coefficientRaw = $row['coefficient'];
$coefficient = (float) $coefficientRaw;
/** @var string $status */
$status = $row['status'];
/** @var string $createdAt */
$createdAt = $row['created_at'];
/** @var string $updatedAt */
$updatedAt = $row['updated_at'];
return Evaluation::reconstitute(
id: EvaluationId::fromString($id),
tenantId: TenantId::fromString($tenantId),
classId: ClassId::fromString($classId),
subjectId: SubjectId::fromString($subjectId),
teacherId: UserId::fromString($teacherId),
title: $title,
description: $description,
evaluationDate: new DateTimeImmutable($evaluationDate),
gradeScale: new GradeScale($gradeScale),
coefficient: new Coefficient($coefficient),
status: EvaluationStatus::from($status),
createdAt: new DateTimeImmutable($createdAt),
updatedAt: new DateTimeImmutable($updatedAt),
);
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Infrastructure\Persistence\InMemory;
use App\Administration\Domain\Model\SchoolClass\ClassId;
use App\Administration\Domain\Model\User\UserId;
use App\Scolarite\Domain\Exception\EvaluationNotFoundException;
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
use App\Scolarite\Domain\Model\Evaluation\EvaluationStatus;
use App\Scolarite\Domain\Repository\EvaluationRepository;
use App\Shared\Domain\Tenant\TenantId;
use function array_filter;
use function array_values;
use Override;
final class InMemoryEvaluationRepository implements EvaluationRepository
{
/** @var array<string, Evaluation> */
private array $byId = [];
#[Override]
public function save(Evaluation $evaluation): void
{
$this->byId[(string) $evaluation->id] = $evaluation;
}
#[Override]
public function get(EvaluationId $id, TenantId $tenantId): Evaluation
{
$evaluation = $this->findById($id, $tenantId);
if ($evaluation === null) {
throw EvaluationNotFoundException::withId($id);
}
return $evaluation;
}
#[Override]
public function findById(EvaluationId $id, TenantId $tenantId): ?Evaluation
{
$evaluation = $this->byId[(string) $id] ?? null;
if ($evaluation === null || !$evaluation->tenantId->equals($tenantId)) {
return null;
}
return $evaluation;
}
#[Override]
public function findByTeacher(UserId $teacherId, TenantId $tenantId): array
{
return array_values(array_filter(
$this->byId,
static fn (Evaluation $e): bool => $e->teacherId->equals($teacherId)
&& $e->tenantId->equals($tenantId)
&& $e->status !== EvaluationStatus::DELETED,
));
}
#[Override]
public function findByClass(ClassId $classId, TenantId $tenantId): array
{
return array_values(array_filter(
$this->byId,
static fn (Evaluation $e): bool => $e->classId->equals($classId)
&& $e->tenantId->equals($tenantId)
&& $e->status !== EvaluationStatus::DELETED,
));
}
#[Override]
public function findByTeacherAndClass(UserId $teacherId, ClassId $classId, TenantId $tenantId): array
{
return array_values(array_filter(
$this->byId,
static fn (Evaluation $e): bool => $e->teacherId->equals($teacherId)
&& $e->classId->equals($classId)
&& $e->tenantId->equals($tenantId)
&& $e->status !== EvaluationStatus::DELETED,
));
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Infrastructure\Service;
use App\Scolarite\Application\Port\EvaluationGradesChecker;
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
use App\Shared\Domain\Tenant\TenantId;
use Override;
final readonly class NoGradesEvaluationGradesChecker implements EvaluationGradesChecker
{
#[Override]
public function hasGrades(EvaluationId $evaluationId, TenantId $tenantId): bool
{
return false;
}
}