feat: Affectation des enseignants aux classes et matières
Permet aux administrateurs d'associer un enseignant à une classe pour une matière donnée au sein d'une année scolaire. Cette brique est nécessaire pour construire les emplois du temps et les carnets de notes par la suite. Le modèle impose l'unicité du triplet enseignant × classe × matière par année scolaire, avec réactivation automatique d'une affectation retirée plutôt que duplication. L'isolation multi-tenant est garantie au niveau du repository (findById/get filtrent par tenant_id).
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Event;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use App\Administration\Domain\Model\Subject\SubjectId;
|
||||
use App\Administration\Domain\Model\TeacherAssignment\TeacherAssignmentId;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Shared\Domain\DomainEvent;
|
||||
use DateTimeImmutable;
|
||||
use Override;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
final readonly class AffectationRetiree implements DomainEvent
|
||||
{
|
||||
public function __construct(
|
||||
public TeacherAssignmentId $assignmentId,
|
||||
public UserId $teacherId,
|
||||
public ClassId $classId,
|
||||
public SubjectId $subjectId,
|
||||
private DateTimeImmutable $occurredOn,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function occurredOn(): DateTimeImmutable
|
||||
{
|
||||
return $this->occurredOn;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function aggregateId(): UuidInterface
|
||||
{
|
||||
return $this->assignmentId->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Event;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use App\Administration\Domain\Model\Subject\SubjectId;
|
||||
use App\Administration\Domain\Model\TeacherAssignment\TeacherAssignmentId;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Shared\Domain\DomainEvent;
|
||||
use DateTimeImmutable;
|
||||
use Override;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
final readonly class EnseignantAffecte implements DomainEvent
|
||||
{
|
||||
public function __construct(
|
||||
public TeacherAssignmentId $assignmentId,
|
||||
public UserId $teacherId,
|
||||
public ClassId $classId,
|
||||
public SubjectId $subjectId,
|
||||
private DateTimeImmutable $occurredOn,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function occurredOn(): DateTimeImmutable
|
||||
{
|
||||
return $this->occurredOn;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function aggregateId(): UuidInterface
|
||||
{
|
||||
return $this->assignmentId->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use App\Administration\Domain\Model\Subject\SubjectId;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class AffectationDejaExistanteException extends DomainException
|
||||
{
|
||||
public static function pourTriple(UserId $teacherId, ClassId $classId, SubjectId $subjectId): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'L\'enseignant "%s" est déjà affecté à la classe "%s" pour la matière "%s".',
|
||||
$teacherId,
|
||||
$classId,
|
||||
$subjectId,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\TeacherAssignment\TeacherAssignmentId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class AffectationNotFoundException extends DomainException
|
||||
{
|
||||
public static function withId(TeacherAssignmentId $id): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'L\'affectation avec l\'ID "%s" n\'a pas été trouvée.',
|
||||
$id,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Model\TeacherAssignment;
|
||||
|
||||
enum AssignmentStatus: string
|
||||
{
|
||||
case ACTIVE = 'active';
|
||||
case REMOVED = 'removed';
|
||||
|
||||
public function estActive(): bool
|
||||
{
|
||||
return $this === self::ACTIVE;
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::ACTIVE => 'Active',
|
||||
self::REMOVED => 'Retirée',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Model\TeacherAssignment;
|
||||
|
||||
use App\Administration\Domain\Event\AffectationRetiree;
|
||||
use App\Administration\Domain\Event\EnseignantAffecte;
|
||||
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use App\Administration\Domain\Model\Subject\SubjectId;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Shared\Domain\AggregateRoot;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
|
||||
/**
|
||||
* Aggregate Root représentant l'affectation d'un enseignant à une classe et une matière.
|
||||
*
|
||||
* Relation ternaire : Teacher × Class × Subject, scopée par année scolaire.
|
||||
* Supporte le multi-matière (un enseignant, plusieurs matières par classe)
|
||||
* et le co-enseignement (plusieurs enseignants, même matière dans une classe).
|
||||
*
|
||||
* @see FR78: Affecter enseignant à classe et matière
|
||||
*/
|
||||
final class TeacherAssignment extends AggregateRoot
|
||||
{
|
||||
public private(set) DateTimeImmutable $updatedAt;
|
||||
|
||||
private function __construct(
|
||||
public private(set) TeacherAssignmentId $id,
|
||||
public private(set) TenantId $tenantId,
|
||||
public private(set) UserId $teacherId,
|
||||
public private(set) ClassId $classId,
|
||||
public private(set) SubjectId $subjectId,
|
||||
public private(set) AcademicYearId $academicYearId,
|
||||
public private(set) DateTimeImmutable $startDate,
|
||||
public private(set) ?DateTimeImmutable $endDate,
|
||||
public private(set) AssignmentStatus $status,
|
||||
public private(set) DateTimeImmutable $createdAt,
|
||||
) {
|
||||
$this->updatedAt = $createdAt;
|
||||
}
|
||||
|
||||
public static function creer(
|
||||
TenantId $tenantId,
|
||||
UserId $teacherId,
|
||||
ClassId $classId,
|
||||
SubjectId $subjectId,
|
||||
AcademicYearId $academicYearId,
|
||||
DateTimeImmutable $createdAt,
|
||||
): self {
|
||||
$assignment = new self(
|
||||
id: TeacherAssignmentId::generate(),
|
||||
tenantId: $tenantId,
|
||||
teacherId: $teacherId,
|
||||
classId: $classId,
|
||||
subjectId: $subjectId,
|
||||
academicYearId: $academicYearId,
|
||||
startDate: $createdAt,
|
||||
endDate: null,
|
||||
status: AssignmentStatus::ACTIVE,
|
||||
createdAt: $createdAt,
|
||||
);
|
||||
|
||||
$assignment->recordEvent(new EnseignantAffecte(
|
||||
assignmentId: $assignment->id,
|
||||
teacherId: $assignment->teacherId,
|
||||
classId: $assignment->classId,
|
||||
subjectId: $assignment->subjectId,
|
||||
occurredOn: $createdAt,
|
||||
));
|
||||
|
||||
return $assignment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retire l'affectation. Les notes existantes sont conservées (historique),
|
||||
* mais l'enseignant ne peut plus en ajouter.
|
||||
*/
|
||||
public function retirer(DateTimeImmutable $at): void
|
||||
{
|
||||
if ($this->status === AssignmentStatus::REMOVED) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->status = AssignmentStatus::REMOVED;
|
||||
$this->endDate = $at;
|
||||
$this->updatedAt = $at;
|
||||
|
||||
$this->recordEvent(new AffectationRetiree(
|
||||
assignmentId: $this->id,
|
||||
teacherId: $this->teacherId,
|
||||
classId: $this->classId,
|
||||
subjectId: $this->subjectId,
|
||||
occurredOn: $at,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Réactive une affectation précédemment retirée.
|
||||
*
|
||||
* Permet de ré-affecter un enseignant au même triplet sans violer
|
||||
* la contrainte d'unicité en base.
|
||||
*/
|
||||
public function reactiver(DateTimeImmutable $at): void
|
||||
{
|
||||
if ($this->status === AssignmentStatus::ACTIVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->status = AssignmentStatus::ACTIVE;
|
||||
$this->endDate = null;
|
||||
$this->startDate = $at;
|
||||
$this->updatedAt = $at;
|
||||
|
||||
$this->recordEvent(new EnseignantAffecte(
|
||||
assignmentId: $this->id,
|
||||
teacherId: $this->teacherId,
|
||||
classId: $this->classId,
|
||||
subjectId: $this->subjectId,
|
||||
occurredOn: $at,
|
||||
));
|
||||
}
|
||||
|
||||
public function estActive(): bool
|
||||
{
|
||||
return $this->status->estActive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstitue une TeacherAssignment depuis le stockage.
|
||||
*
|
||||
* @internal Pour usage Infrastructure uniquement
|
||||
*/
|
||||
public static function reconstitute(
|
||||
TeacherAssignmentId $id,
|
||||
TenantId $tenantId,
|
||||
UserId $teacherId,
|
||||
ClassId $classId,
|
||||
SubjectId $subjectId,
|
||||
AcademicYearId $academicYearId,
|
||||
DateTimeImmutable $startDate,
|
||||
?DateTimeImmutable $endDate,
|
||||
AssignmentStatus $status,
|
||||
DateTimeImmutable $createdAt,
|
||||
DateTimeImmutable $updatedAt,
|
||||
): self {
|
||||
$assignment = new self(
|
||||
id: $id,
|
||||
tenantId: $tenantId,
|
||||
teacherId: $teacherId,
|
||||
classId: $classId,
|
||||
subjectId: $subjectId,
|
||||
academicYearId: $academicYearId,
|
||||
startDate: $startDate,
|
||||
endDate: $endDate,
|
||||
status: $status,
|
||||
createdAt: $createdAt,
|
||||
);
|
||||
|
||||
$assignment->updatedAt = $updatedAt;
|
||||
|
||||
return $assignment;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Model\TeacherAssignment;
|
||||
|
||||
use App\Shared\Domain\EntityId;
|
||||
|
||||
final readonly class TeacherAssignmentId extends EntityId
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Repository;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use App\Administration\Domain\Model\Subject\SubjectId;
|
||||
use App\Administration\Domain\Model\TeacherAssignment\TeacherAssignment;
|
||||
use App\Administration\Domain\Model\TeacherAssignment\TeacherAssignmentId;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
interface TeacherAssignmentRepository
|
||||
{
|
||||
public function save(TeacherAssignment $assignment): void;
|
||||
|
||||
/**
|
||||
* @throws \App\Administration\Domain\Exception\AffectationNotFoundException
|
||||
*/
|
||||
public function get(TeacherAssignmentId $id, TenantId $tenantId): TeacherAssignment;
|
||||
|
||||
public function findById(TeacherAssignmentId $id, TenantId $tenantId): ?TeacherAssignment;
|
||||
|
||||
/**
|
||||
* Vérifie si une affectation active existe déjà pour ce triplet enseignant × classe × matière.
|
||||
*/
|
||||
public function findByTeacherClassSubject(
|
||||
UserId $teacherId,
|
||||
ClassId $classId,
|
||||
SubjectId $subjectId,
|
||||
AcademicYearId $academicYearId,
|
||||
TenantId $tenantId,
|
||||
): ?TeacherAssignment;
|
||||
|
||||
/**
|
||||
* Recherche une affectation retirée pour ce triplet enseignant × classe × matière.
|
||||
*
|
||||
* Utilisé pour réactiver une affectation au lieu d'en créer une nouvelle,
|
||||
* évitant ainsi une violation de la contrainte d'unicité.
|
||||
*/
|
||||
public function findRemovedByTeacherClassSubject(
|
||||
UserId $teacherId,
|
||||
ClassId $classId,
|
||||
SubjectId $subjectId,
|
||||
AcademicYearId $academicYearId,
|
||||
TenantId $tenantId,
|
||||
): ?TeacherAssignment;
|
||||
|
||||
/**
|
||||
* Retourne toutes les affectations actives d'un enseignant.
|
||||
*
|
||||
* @return TeacherAssignment[]
|
||||
*/
|
||||
public function findActiveByTeacher(
|
||||
UserId $teacherId,
|
||||
TenantId $tenantId,
|
||||
): array;
|
||||
|
||||
/**
|
||||
* Retourne tous les enseignants affectés à une classe.
|
||||
*
|
||||
* @return TeacherAssignment[]
|
||||
*/
|
||||
public function findActiveByClass(
|
||||
ClassId $classId,
|
||||
TenantId $tenantId,
|
||||
): array;
|
||||
}
|
||||
Reference in New Issue
Block a user