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:
@@ -12,6 +12,7 @@ use App\Administration\Domain\Exception\ClasseDejaExistanteException;
|
||||
use App\Administration\Domain\Exception\ClassNameInvalideException;
|
||||
use App\Administration\Infrastructure\Api\Resource\ClassResource;
|
||||
use App\Administration\Infrastructure\Security\ClassVoter;
|
||||
use App\Administration\Infrastructure\Service\CurrentAcademicYearResolver;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
use Override;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
@@ -34,6 +35,7 @@ final readonly class CreateClassProcessor implements ProcessorInterface
|
||||
private TenantContext $tenantContext,
|
||||
private MessageBusInterface $eventBus,
|
||||
private AuthorizationCheckerInterface $authorizationChecker,
|
||||
private CurrentAcademicYearResolver $academicYearResolver,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -53,11 +55,11 @@ final readonly class CreateClassProcessor implements ProcessorInterface
|
||||
|
||||
$tenantId = (string) $this->tenantContext->getCurrentTenantId();
|
||||
|
||||
// TODO: Récupérer school_id et academic_year_id depuis le contexte utilisateur
|
||||
// quand les modules Schools et AcademicYears seront implémentés.
|
||||
// Pour l'instant, on utilise des UUIDs déterministes basés sur le tenant.
|
||||
// TODO: Récupérer school_id depuis le contexte utilisateur
|
||||
// quand le module Schools sera implémenté.
|
||||
$schoolId = Uuid::uuid5(Uuid::NAMESPACE_DNS, "school-{$tenantId}")->toString();
|
||||
$academicYearId = Uuid::uuid5(Uuid::NAMESPACE_DNS, "academic-year-2024-2025-{$tenantId}")->toString();
|
||||
$academicYearId = $this->academicYearResolver->resolve('current')
|
||||
?? throw new BadRequestHttpException('Impossible de résoudre l\'année scolaire courante.');
|
||||
|
||||
try {
|
||||
$command = new CreateClassCommand(
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Infrastructure\Api\Processor;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Administration\Application\Command\AssignTeacher\AssignTeacherCommand;
|
||||
use App\Administration\Application\Command\AssignTeacher\AssignTeacherHandler;
|
||||
use App\Administration\Domain\Exception\AffectationDejaExistanteException;
|
||||
use App\Administration\Domain\Exception\ClasseNotFoundException;
|
||||
use App\Administration\Domain\Exception\SubjectNotFoundException;
|
||||
use App\Administration\Domain\Exception\UserNotFoundException;
|
||||
use App\Administration\Infrastructure\Api\Resource\TeacherAssignmentResource;
|
||||
use App\Administration\Infrastructure\Security\TeacherAssignmentVoter;
|
||||
use App\Administration\Infrastructure\Service\CurrentAcademicYearResolver;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
use Override;
|
||||
use Ramsey\Uuid\Exception\InvalidUuidStringException;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
/**
|
||||
* @implements ProcessorInterface<TeacherAssignmentResource, TeacherAssignmentResource>
|
||||
*/
|
||||
final readonly class CreateTeacherAssignmentProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private AssignTeacherHandler $handler,
|
||||
private TenantContext $tenantContext,
|
||||
private MessageBusInterface $eventBus,
|
||||
private AuthorizationCheckerInterface $authorizationChecker,
|
||||
private CurrentAcademicYearResolver $academicYearResolver,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TeacherAssignmentResource $data
|
||||
*/
|
||||
#[Override]
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): TeacherAssignmentResource
|
||||
{
|
||||
if (!$this->authorizationChecker->isGranted(TeacherAssignmentVoter::CREATE)) {
|
||||
throw new AccessDeniedHttpException('Vous n\'êtes pas autorisé à créer une affectation.');
|
||||
}
|
||||
|
||||
if (!$this->tenantContext->hasTenant()) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Tenant non défini.');
|
||||
}
|
||||
|
||||
$tenantId = (string) $this->tenantContext->getCurrentTenantId();
|
||||
|
||||
$rawAcademicYearId = $this->academicYearResolver->resolve($data->academicYearId ?? 'current');
|
||||
if ($rawAcademicYearId === null) {
|
||||
throw new BadRequestHttpException('Identifiant d\'année scolaire invalide.');
|
||||
}
|
||||
|
||||
try {
|
||||
$command = new AssignTeacherCommand(
|
||||
tenantId: $tenantId,
|
||||
teacherId: $data->teacherId ?? '',
|
||||
classId: $data->classId ?? '',
|
||||
subjectId: $data->subjectId ?? '',
|
||||
academicYearId: $rawAcademicYearId,
|
||||
);
|
||||
|
||||
$assignment = ($this->handler)($command);
|
||||
|
||||
foreach ($assignment->pullDomainEvents() as $event) {
|
||||
$this->eventBus->dispatch($event);
|
||||
}
|
||||
|
||||
return TeacherAssignmentResource::fromDomain($assignment);
|
||||
} catch (AffectationDejaExistanteException $e) {
|
||||
throw new ConflictHttpException($e->getMessage());
|
||||
} catch (UserNotFoundException|ClasseNotFoundException|SubjectNotFoundException $e) {
|
||||
throw new NotFoundHttpException($e->getMessage());
|
||||
} catch (InvalidUuidStringException $e) {
|
||||
throw new BadRequestHttpException('UUID invalide : ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Infrastructure\Api\Processor;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Administration\Application\Command\RemoveAssignment\RemoveAssignmentCommand;
|
||||
use App\Administration\Application\Command\RemoveAssignment\RemoveAssignmentHandler;
|
||||
use App\Administration\Domain\Exception\AffectationNotFoundException;
|
||||
use App\Administration\Infrastructure\Api\Resource\TeacherAssignmentResource;
|
||||
use App\Administration\Infrastructure\Security\TeacherAssignmentVoter;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
use Override;
|
||||
use Ramsey\Uuid\Exception\InvalidUuidStringException;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
/**
|
||||
* @implements ProcessorInterface<TeacherAssignmentResource, null>
|
||||
*/
|
||||
final readonly class RemoveTeacherAssignmentProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private RemoveAssignmentHandler $handler,
|
||||
private TenantContext $tenantContext,
|
||||
private MessageBusInterface $eventBus,
|
||||
private AuthorizationCheckerInterface $authorizationChecker,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TeacherAssignmentResource $data
|
||||
*/
|
||||
#[Override]
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): null
|
||||
{
|
||||
if (!$this->authorizationChecker->isGranted(TeacherAssignmentVoter::DELETE)) {
|
||||
throw new AccessDeniedHttpException('Vous n\'êtes pas autorisé à retirer une affectation.');
|
||||
}
|
||||
|
||||
if (!$this->tenantContext->hasTenant()) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Tenant non défini.');
|
||||
}
|
||||
|
||||
/** @var string|null $assignmentId */
|
||||
$assignmentId = $uriVariables['id'] ?? null;
|
||||
if ($assignmentId === null) {
|
||||
throw new NotFoundHttpException('Affectation non trouvée.');
|
||||
}
|
||||
|
||||
$tenantId = (string) $this->tenantContext->getCurrentTenantId();
|
||||
|
||||
try {
|
||||
$command = new RemoveAssignmentCommand(
|
||||
assignmentId: $assignmentId,
|
||||
tenantId: $tenantId,
|
||||
);
|
||||
|
||||
$assignment = ($this->handler)($command);
|
||||
|
||||
foreach ($assignment->pullDomainEvents() as $event) {
|
||||
$this->eventBus->dispatch($event);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (AffectationNotFoundException|InvalidUuidStringException) {
|
||||
throw new NotFoundHttpException('Affectation non trouvée.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,9 @@ use App\Administration\Application\Query\GetClasses\GetClassesHandler;
|
||||
use App\Administration\Application\Query\GetClasses\GetClassesQuery;
|
||||
use App\Administration\Infrastructure\Api\Resource\ClassResource;
|
||||
use App\Administration\Infrastructure\Security\ClassVoter;
|
||||
use App\Administration\Infrastructure\Service\CurrentAcademicYearResolver;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
use Override;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
@@ -28,6 +28,7 @@ final readonly class ClassCollectionProvider implements ProviderInterface
|
||||
private GetClassesHandler $handler,
|
||||
private TenantContext $tenantContext,
|
||||
private AuthorizationCheckerInterface $authorizationChecker,
|
||||
private CurrentAcademicYearResolver $academicYearResolver,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -48,9 +49,10 @@ final readonly class ClassCollectionProvider implements ProviderInterface
|
||||
|
||||
$tenantId = (string) $this->tenantContext->getCurrentTenantId();
|
||||
|
||||
// TODO: Récupérer academic_year_id depuis le contexte utilisateur
|
||||
// quand le module AcademicYears sera implémenté.
|
||||
$academicYearId = Uuid::uuid5(Uuid::NAMESPACE_DNS, "academic-year-2024-2025-{$tenantId}")->toString();
|
||||
$academicYearId = $this->academicYearResolver->resolve('current') ?? '';
|
||||
if ($academicYearId === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$query = new GetClassesQuery(
|
||||
tenantId: $tenantId,
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Infrastructure\Api\Provider;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Administration\Domain\Model\TeacherAssignment\TeacherAssignmentId;
|
||||
use App\Administration\Domain\Repository\TeacherAssignmentRepository;
|
||||
use App\Administration\Infrastructure\Api\Resource\TeacherAssignmentResource;
|
||||
use App\Administration\Infrastructure\Security\TeacherAssignmentVoter;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
use InvalidArgumentException;
|
||||
use Override;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
/**
|
||||
* @implements ProviderInterface<TeacherAssignmentResource>
|
||||
*/
|
||||
final readonly class TeacherAssignmentItemProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private TeacherAssignmentRepository $repository,
|
||||
private TenantContext $tenantContext,
|
||||
private AuthorizationCheckerInterface $authorizationChecker,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?TeacherAssignmentResource
|
||||
{
|
||||
if (!$this->tenantContext->hasTenant()) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Tenant non défini.');
|
||||
}
|
||||
|
||||
if (!$this->authorizationChecker->isGranted(TeacherAssignmentVoter::DELETE)) {
|
||||
throw new AccessDeniedHttpException('Vous n\'êtes pas autorisé à supprimer cette affectation.');
|
||||
}
|
||||
|
||||
/** @var string $id */
|
||||
$id = $uriVariables['id'] ?? '';
|
||||
|
||||
try {
|
||||
$assignment = $this->repository->findById(
|
||||
TeacherAssignmentId::fromString($id),
|
||||
$this->tenantContext->getCurrentTenantId(),
|
||||
);
|
||||
} catch (InvalidArgumentException) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($assignment === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return TeacherAssignmentResource::fromDomain($assignment);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Infrastructure\Api\Provider;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Administration\Application\Query\GetTeachersForClass\GetTeachersForClassHandler;
|
||||
use App\Administration\Application\Query\GetTeachersForClass\GetTeachersForClassQuery;
|
||||
use App\Administration\Infrastructure\Api\Resource\TeacherAssignmentResource;
|
||||
use App\Administration\Infrastructure\Security\TeacherAssignmentVoter;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
|
||||
use function array_map;
|
||||
|
||||
use Override;
|
||||
use Ramsey\Uuid\Exception\InvalidUuidStringException;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
/**
|
||||
* @implements ProviderInterface<TeacherAssignmentResource>
|
||||
*/
|
||||
final readonly class TeacherAssignmentsByClassProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private GetTeachersForClassHandler $handler,
|
||||
private TenantContext $tenantContext,
|
||||
private AuthorizationCheckerInterface $authorizationChecker,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TeacherAssignmentResource[]
|
||||
*/
|
||||
#[Override]
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array
|
||||
{
|
||||
if (!$this->authorizationChecker->isGranted(TeacherAssignmentVoter::VIEW)) {
|
||||
throw new AccessDeniedHttpException('Vous n\'êtes pas autorisé à voir les enseignants de cette classe.');
|
||||
}
|
||||
|
||||
if (!$this->tenantContext->hasTenant()) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Tenant non défini.');
|
||||
}
|
||||
|
||||
/** @var string $classId */
|
||||
$classId = $uriVariables['classId'] ?? '';
|
||||
$tenantId = (string) $this->tenantContext->getCurrentTenantId();
|
||||
|
||||
try {
|
||||
$query = new GetTeachersForClassQuery(
|
||||
classId: $classId,
|
||||
tenantId: $tenantId,
|
||||
);
|
||||
|
||||
$dtos = ($this->handler)($query);
|
||||
|
||||
return array_map(TeacherAssignmentResource::fromDto(...), $dtos);
|
||||
} catch (InvalidUuidStringException $e) {
|
||||
throw new BadRequestHttpException('Identifiant classe invalide.', $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Infrastructure\Api\Provider;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Administration\Application\Query\GetAssignmentsForTeacher\GetAssignmentsForTeacherHandler;
|
||||
use App\Administration\Application\Query\GetAssignmentsForTeacher\GetAssignmentsForTeacherQuery;
|
||||
use App\Administration\Infrastructure\Api\Resource\TeacherAssignmentResource;
|
||||
use App\Administration\Infrastructure\Security\TeacherAssignmentVoter;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
|
||||
use function array_map;
|
||||
|
||||
use Override;
|
||||
use Ramsey\Uuid\Exception\InvalidUuidStringException;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
/**
|
||||
* @implements ProviderInterface<TeacherAssignmentResource>
|
||||
*/
|
||||
final readonly class TeacherAssignmentsByTeacherProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private GetAssignmentsForTeacherHandler $handler,
|
||||
private TenantContext $tenantContext,
|
||||
private AuthorizationCheckerInterface $authorizationChecker,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TeacherAssignmentResource[]
|
||||
*/
|
||||
#[Override]
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array
|
||||
{
|
||||
if (!$this->tenantContext->hasTenant()) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Tenant non défini.');
|
||||
}
|
||||
|
||||
/** @var string $teacherId */
|
||||
$teacherId = $uriVariables['teacherId'] ?? '';
|
||||
|
||||
// Passer une ressource avec le teacherId pour que le voter puisse
|
||||
// vérifier que l'enseignant ne consulte que ses propres affectations.
|
||||
$subjectForVoter = new TeacherAssignmentResource();
|
||||
$subjectForVoter->teacherId = $teacherId;
|
||||
|
||||
if (!$this->authorizationChecker->isGranted(TeacherAssignmentVoter::VIEW, $subjectForVoter)) {
|
||||
throw new AccessDeniedHttpException('Vous n\'êtes pas autorisé à voir les affectations.');
|
||||
}
|
||||
$tenantId = (string) $this->tenantContext->getCurrentTenantId();
|
||||
|
||||
try {
|
||||
$query = new GetAssignmentsForTeacherQuery(
|
||||
teacherId: $teacherId,
|
||||
tenantId: $tenantId,
|
||||
);
|
||||
|
||||
$dtos = ($this->handler)($query);
|
||||
|
||||
return array_map(TeacherAssignmentResource::fromDto(...), $dtos);
|
||||
} catch (InvalidUuidStringException $e) {
|
||||
throw new BadRequestHttpException('Identifiant enseignant invalide.', $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Infrastructure\Api\Resource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Link;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Administration\Application\Query\GetAssignmentsForTeacher\TeacherAssignmentDto;
|
||||
use App\Administration\Domain\Model\TeacherAssignment\TeacherAssignment;
|
||||
use App\Administration\Infrastructure\Api\Processor\CreateTeacherAssignmentProcessor;
|
||||
use App\Administration\Infrastructure\Api\Processor\RemoveTeacherAssignmentProcessor;
|
||||
use App\Administration\Infrastructure\Api\Provider\TeacherAssignmentItemProvider;
|
||||
use App\Administration\Infrastructure\Api\Provider\TeacherAssignmentsByClassProvider;
|
||||
use App\Administration\Infrastructure\Api\Provider\TeacherAssignmentsByTeacherProvider;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* API Resource pour la gestion des affectations enseignants.
|
||||
*
|
||||
* @see Story 2.8 - Affectation Enseignants aux Classes et Matières
|
||||
* @see FR78 - Affecter enseignant à classe et matière
|
||||
*/
|
||||
#[ApiResource(
|
||||
shortName: 'TeacherAssignment',
|
||||
operations: [
|
||||
new GetCollection(
|
||||
uriTemplate: '/teachers/{teacherId}/assignments',
|
||||
uriVariables: [
|
||||
'teacherId' => new Link(
|
||||
fromClass: self::class,
|
||||
identifiers: ['teacherId'],
|
||||
),
|
||||
],
|
||||
provider: TeacherAssignmentsByTeacherProvider::class,
|
||||
name: 'get_teacher_assignments',
|
||||
),
|
||||
new GetCollection(
|
||||
uriTemplate: '/classes/{classId}/teachers',
|
||||
uriVariables: [
|
||||
'classId' => new Link(
|
||||
fromClass: self::class,
|
||||
identifiers: ['classId'],
|
||||
),
|
||||
],
|
||||
provider: TeacherAssignmentsByClassProvider::class,
|
||||
name: 'get_class_teachers',
|
||||
),
|
||||
new Post(
|
||||
uriTemplate: '/teacher-assignments',
|
||||
processor: CreateTeacherAssignmentProcessor::class,
|
||||
validationContext: ['groups' => ['Default', 'create']],
|
||||
name: 'create_teacher_assignment',
|
||||
),
|
||||
new Delete(
|
||||
uriTemplate: '/teacher-assignments/{id}',
|
||||
provider: TeacherAssignmentItemProvider::class,
|
||||
processor: RemoveTeacherAssignmentProcessor::class,
|
||||
name: 'remove_teacher_assignment',
|
||||
),
|
||||
],
|
||||
)]
|
||||
final class TeacherAssignmentResource
|
||||
{
|
||||
#[ApiProperty(identifier: true)]
|
||||
public ?string $id = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'L\'identifiant de l\'enseignant est requis.', groups: ['create'])]
|
||||
public ?string $teacherId = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'L\'identifiant de la classe est requis.', groups: ['create'])]
|
||||
public ?string $classId = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'L\'identifiant de la matière est requis.', groups: ['create'])]
|
||||
public ?string $subjectId = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'L\'identifiant de l\'année scolaire est requis.', groups: ['create'])]
|
||||
public ?string $academicYearId = null;
|
||||
|
||||
public ?string $status = null;
|
||||
|
||||
public ?DateTimeImmutable $startDate = null;
|
||||
|
||||
public ?DateTimeImmutable $endDate = null;
|
||||
|
||||
public ?DateTimeImmutable $createdAt = null;
|
||||
|
||||
public static function fromDomain(TeacherAssignment $assignment): self
|
||||
{
|
||||
$resource = new self();
|
||||
$resource->id = (string) $assignment->id;
|
||||
$resource->teacherId = (string) $assignment->teacherId;
|
||||
$resource->classId = (string) $assignment->classId;
|
||||
$resource->subjectId = (string) $assignment->subjectId;
|
||||
$resource->academicYearId = (string) $assignment->academicYearId;
|
||||
$resource->status = $assignment->status->value;
|
||||
$resource->startDate = $assignment->startDate;
|
||||
$resource->endDate = $assignment->endDate;
|
||||
$resource->createdAt = $assignment->createdAt;
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
public static function fromDto(TeacherAssignmentDto $dto): self
|
||||
{
|
||||
$resource = new self();
|
||||
$resource->id = $dto->id;
|
||||
$resource->teacherId = $dto->teacherId;
|
||||
$resource->classId = $dto->classId;
|
||||
$resource->subjectId = $dto->subjectId;
|
||||
$resource->academicYearId = $dto->academicYearId;
|
||||
$resource->status = $dto->status;
|
||||
$resource->startDate = $dto->startDate;
|
||||
$resource->endDate = $dto->endDate;
|
||||
$resource->createdAt = $dto->createdAt;
|
||||
|
||||
return $resource;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Infrastructure\Persistence\Doctrine;
|
||||
|
||||
use App\Administration\Domain\Exception\AffectationDejaExistanteException;
|
||||
use App\Administration\Domain\Exception\AffectationNotFoundException;
|
||||
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\AssignmentStatus;
|
||||
use App\Administration\Domain\Model\TeacherAssignment\TeacherAssignment;
|
||||
use App\Administration\Domain\Model\TeacherAssignment\TeacherAssignmentId;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Administration\Domain\Repository\TeacherAssignmentRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
|
||||
use Override;
|
||||
|
||||
final readonly class DoctrineTeacherAssignmentRepository implements TeacherAssignmentRepository
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function save(TeacherAssignment $assignment): void
|
||||
{
|
||||
try {
|
||||
$this->connection->executeStatement(
|
||||
'INSERT INTO teacher_assignments (id, tenant_id, teacher_id, school_class_id, subject_id, academic_year_id, start_date, end_date, status, created_at, updated_at)
|
||||
VALUES (:id, :tenant_id, :teacher_id, :school_class_id, :subject_id, :academic_year_id, :start_date, :end_date, :status, :created_at, :updated_at)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
start_date = EXCLUDED.start_date,
|
||||
end_date = EXCLUDED.end_date,
|
||||
status = EXCLUDED.status,
|
||||
updated_at = EXCLUDED.updated_at',
|
||||
[
|
||||
'id' => (string) $assignment->id,
|
||||
'tenant_id' => (string) $assignment->tenantId,
|
||||
'teacher_id' => (string) $assignment->teacherId,
|
||||
'school_class_id' => (string) $assignment->classId,
|
||||
'subject_id' => (string) $assignment->subjectId,
|
||||
'academic_year_id' => (string) $assignment->academicYearId,
|
||||
'start_date' => $assignment->startDate->format(DateTimeImmutable::ATOM),
|
||||
'end_date' => $assignment->endDate?->format(DateTimeImmutable::ATOM),
|
||||
'status' => $assignment->status->value,
|
||||
'created_at' => $assignment->createdAt->format(DateTimeImmutable::ATOM),
|
||||
'updated_at' => $assignment->updatedAt->format(DateTimeImmutable::ATOM),
|
||||
],
|
||||
);
|
||||
} catch (UniqueConstraintViolationException) {
|
||||
throw AffectationDejaExistanteException::pourTriple(
|
||||
$assignment->teacherId,
|
||||
$assignment->classId,
|
||||
$assignment->subjectId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function get(TeacherAssignmentId $id, TenantId $tenantId): TeacherAssignment
|
||||
{
|
||||
$assignment = $this->findById($id, $tenantId);
|
||||
|
||||
if ($assignment === null) {
|
||||
throw AffectationNotFoundException::withId($id);
|
||||
}
|
||||
|
||||
return $assignment;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findById(TeacherAssignmentId $id, TenantId $tenantId): ?TeacherAssignment
|
||||
{
|
||||
$row = $this->connection->fetchAssociative(
|
||||
'SELECT * FROM teacher_assignments 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 findByTeacherClassSubject(
|
||||
UserId $teacherId,
|
||||
ClassId $classId,
|
||||
SubjectId $subjectId,
|
||||
AcademicYearId $academicYearId,
|
||||
TenantId $tenantId,
|
||||
): ?TeacherAssignment {
|
||||
$row = $this->connection->fetchAssociative(
|
||||
'SELECT * FROM teacher_assignments
|
||||
WHERE tenant_id = :tenant_id
|
||||
AND teacher_id = :teacher_id
|
||||
AND school_class_id = :school_class_id
|
||||
AND subject_id = :subject_id
|
||||
AND academic_year_id = :academic_year_id
|
||||
AND status = :status',
|
||||
[
|
||||
'tenant_id' => (string) $tenantId,
|
||||
'teacher_id' => (string) $teacherId,
|
||||
'school_class_id' => (string) $classId,
|
||||
'subject_id' => (string) $subjectId,
|
||||
'academic_year_id' => (string) $academicYearId,
|
||||
'status' => AssignmentStatus::ACTIVE->value,
|
||||
],
|
||||
);
|
||||
|
||||
if ($row === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->hydrate($row);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findRemovedByTeacherClassSubject(
|
||||
UserId $teacherId,
|
||||
ClassId $classId,
|
||||
SubjectId $subjectId,
|
||||
AcademicYearId $academicYearId,
|
||||
TenantId $tenantId,
|
||||
): ?TeacherAssignment {
|
||||
$row = $this->connection->fetchAssociative(
|
||||
'SELECT * FROM teacher_assignments
|
||||
WHERE tenant_id = :tenant_id
|
||||
AND teacher_id = :teacher_id
|
||||
AND school_class_id = :school_class_id
|
||||
AND subject_id = :subject_id
|
||||
AND academic_year_id = :academic_year_id
|
||||
AND status = :status',
|
||||
[
|
||||
'tenant_id' => (string) $tenantId,
|
||||
'teacher_id' => (string) $teacherId,
|
||||
'school_class_id' => (string) $classId,
|
||||
'subject_id' => (string) $subjectId,
|
||||
'academic_year_id' => (string) $academicYearId,
|
||||
'status' => AssignmentStatus::REMOVED->value,
|
||||
],
|
||||
);
|
||||
|
||||
if ($row === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->hydrate($row);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findActiveByTeacher(
|
||||
UserId $teacherId,
|
||||
TenantId $tenantId,
|
||||
): array {
|
||||
$rows = $this->connection->fetchAllAssociative(
|
||||
'SELECT * FROM teacher_assignments
|
||||
WHERE teacher_id = :teacher_id
|
||||
AND tenant_id = :tenant_id
|
||||
AND status = :status
|
||||
ORDER BY created_at ASC',
|
||||
[
|
||||
'teacher_id' => (string) $teacherId,
|
||||
'tenant_id' => (string) $tenantId,
|
||||
'status' => AssignmentStatus::ACTIVE->value,
|
||||
],
|
||||
);
|
||||
|
||||
return array_map(fn ($row) => $this->hydrate($row), $rows);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findActiveByClass(
|
||||
ClassId $classId,
|
||||
TenantId $tenantId,
|
||||
): array {
|
||||
$rows = $this->connection->fetchAllAssociative(
|
||||
'SELECT * FROM teacher_assignments
|
||||
WHERE school_class_id = :school_class_id
|
||||
AND tenant_id = :tenant_id
|
||||
AND status = :status
|
||||
ORDER BY created_at ASC',
|
||||
[
|
||||
'school_class_id' => (string) $classId,
|
||||
'tenant_id' => (string) $tenantId,
|
||||
'status' => AssignmentStatus::ACTIVE->value,
|
||||
],
|
||||
);
|
||||
|
||||
return array_map(fn ($row) => $this->hydrate($row), $rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
*/
|
||||
private function hydrate(array $row): TeacherAssignment
|
||||
{
|
||||
/** @var string $id */
|
||||
$id = $row['id'];
|
||||
/** @var string $tenantId */
|
||||
$tenantId = $row['tenant_id'];
|
||||
/** @var string $teacherId */
|
||||
$teacherId = $row['teacher_id'];
|
||||
/** @var string $classId */
|
||||
$classId = $row['school_class_id'];
|
||||
/** @var string $subjectId */
|
||||
$subjectId = $row['subject_id'];
|
||||
/** @var string $academicYearId */
|
||||
$academicYearId = $row['academic_year_id'];
|
||||
/** @var string $startDate */
|
||||
$startDate = $row['start_date'];
|
||||
/** @var string|null $endDate */
|
||||
$endDate = $row['end_date'];
|
||||
/** @var string $status */
|
||||
$status = $row['status'];
|
||||
/** @var string $createdAt */
|
||||
$createdAt = $row['created_at'];
|
||||
/** @var string $updatedAt */
|
||||
$updatedAt = $row['updated_at'];
|
||||
|
||||
return TeacherAssignment::reconstitute(
|
||||
id: TeacherAssignmentId::fromString($id),
|
||||
tenantId: TenantId::fromString($tenantId),
|
||||
teacherId: UserId::fromString($teacherId),
|
||||
classId: ClassId::fromString($classId),
|
||||
subjectId: SubjectId::fromString($subjectId),
|
||||
academicYearId: AcademicYearId::fromString($academicYearId),
|
||||
startDate: new DateTimeImmutable($startDate),
|
||||
endDate: $endDate !== null ? new DateTimeImmutable($endDate) : null,
|
||||
status: AssignmentStatus::from($status),
|
||||
createdAt: new DateTimeImmutable($createdAt),
|
||||
updatedAt: new DateTimeImmutable($updatedAt),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Infrastructure\Persistence\InMemory;
|
||||
|
||||
use App\Administration\Domain\Exception\AffectationNotFoundException;
|
||||
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\AssignmentStatus;
|
||||
use App\Administration\Domain\Model\TeacherAssignment\TeacherAssignment;
|
||||
use App\Administration\Domain\Model\TeacherAssignment\TeacherAssignmentId;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Administration\Domain\Repository\TeacherAssignmentRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use Override;
|
||||
|
||||
final class InMemoryTeacherAssignmentRepository implements TeacherAssignmentRepository
|
||||
{
|
||||
/** @var array<string, TeacherAssignment> */
|
||||
private array $byId = [];
|
||||
|
||||
#[Override]
|
||||
public function save(TeacherAssignment $assignment): void
|
||||
{
|
||||
$this->byId[(string) $assignment->id] = $assignment;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function get(TeacherAssignmentId $id, TenantId $tenantId): TeacherAssignment
|
||||
{
|
||||
$assignment = $this->findById($id, $tenantId);
|
||||
|
||||
if ($assignment === null) {
|
||||
throw AffectationNotFoundException::withId($id);
|
||||
}
|
||||
|
||||
return $assignment;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findById(TeacherAssignmentId $id, TenantId $tenantId): ?TeacherAssignment
|
||||
{
|
||||
$assignment = $this->byId[(string) $id] ?? null;
|
||||
|
||||
if ($assignment !== null && !$assignment->tenantId->equals($tenantId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $assignment;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findByTeacherClassSubject(
|
||||
UserId $teacherId,
|
||||
ClassId $classId,
|
||||
SubjectId $subjectId,
|
||||
AcademicYearId $academicYearId,
|
||||
TenantId $tenantId,
|
||||
): ?TeacherAssignment {
|
||||
foreach ($this->byId as $assignment) {
|
||||
if ($assignment->tenantId->equals($tenantId)
|
||||
&& $assignment->teacherId->equals($teacherId)
|
||||
&& $assignment->classId->equals($classId)
|
||||
&& $assignment->subjectId->equals($subjectId)
|
||||
&& $assignment->academicYearId->equals($academicYearId)
|
||||
&& $assignment->status === AssignmentStatus::ACTIVE
|
||||
) {
|
||||
return $assignment;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findRemovedByTeacherClassSubject(
|
||||
UserId $teacherId,
|
||||
ClassId $classId,
|
||||
SubjectId $subjectId,
|
||||
AcademicYearId $academicYearId,
|
||||
TenantId $tenantId,
|
||||
): ?TeacherAssignment {
|
||||
foreach ($this->byId as $assignment) {
|
||||
if ($assignment->tenantId->equals($tenantId)
|
||||
&& $assignment->teacherId->equals($teacherId)
|
||||
&& $assignment->classId->equals($classId)
|
||||
&& $assignment->subjectId->equals($subjectId)
|
||||
&& $assignment->academicYearId->equals($academicYearId)
|
||||
&& $assignment->status === AssignmentStatus::REMOVED
|
||||
) {
|
||||
return $assignment;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findActiveByTeacher(
|
||||
UserId $teacherId,
|
||||
TenantId $tenantId,
|
||||
): array {
|
||||
$result = [];
|
||||
|
||||
foreach ($this->byId as $assignment) {
|
||||
if ($assignment->teacherId->equals($teacherId)
|
||||
&& $assignment->tenantId->equals($tenantId)
|
||||
&& $assignment->status === AssignmentStatus::ACTIVE
|
||||
) {
|
||||
$result[] = $assignment;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findActiveByClass(
|
||||
ClassId $classId,
|
||||
TenantId $tenantId,
|
||||
): array {
|
||||
$result = [];
|
||||
|
||||
foreach ($this->byId as $assignment) {
|
||||
if ($assignment->classId->equals($classId)
|
||||
&& $assignment->tenantId->equals($tenantId)
|
||||
&& $assignment->status === AssignmentStatus::ACTIVE
|
||||
) {
|
||||
$result[] = $assignment;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Infrastructure\Security;
|
||||
|
||||
use App\Administration\Domain\Model\TeacherAssignment\TeacherAssignment;
|
||||
use App\Administration\Domain\Model\User\Role;
|
||||
use App\Administration\Infrastructure\Api\Resource\TeacherAssignmentResource;
|
||||
|
||||
use function in_array;
|
||||
|
||||
use Override;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
|
||||
/**
|
||||
* Voter pour les autorisations sur les affectations enseignants.
|
||||
*
|
||||
* Règles d'accès :
|
||||
* - ADMIN et SUPER_ADMIN : accès complet (CRUD)
|
||||
* - ENSEIGNANT : lecture seule (ses propres affectations)
|
||||
* - VIE_SCOLAIRE, SECRETARIAT : lecture seule
|
||||
* - ELEVE et PARENT : pas d'accès direct
|
||||
*
|
||||
* @extends Voter<string, TeacherAssignment|TeacherAssignmentResource>
|
||||
*/
|
||||
final class TeacherAssignmentVoter extends Voter
|
||||
{
|
||||
public const string VIEW = 'TEACHER_ASSIGNMENT_VIEW';
|
||||
public const string CREATE = 'TEACHER_ASSIGNMENT_CREATE';
|
||||
public const string DELETE = 'TEACHER_ASSIGNMENT_DELETE';
|
||||
|
||||
private const array SUPPORTED_ATTRIBUTES = [
|
||||
self::VIEW,
|
||||
self::CREATE,
|
||||
self::DELETE,
|
||||
];
|
||||
|
||||
#[Override]
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
if (!in_array($attribute, self::SUPPORTED_ATTRIBUTES, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($subject === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $subject instanceof TeacherAssignment || $subject instanceof TeacherAssignmentResource;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
|
||||
{
|
||||
$user = $token->getUser();
|
||||
|
||||
if (!$user instanceof SecurityUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$roles = $user->getRoles();
|
||||
|
||||
return match ($attribute) {
|
||||
self::VIEW => $this->canView($roles, $user, $subject),
|
||||
self::CREATE => $this->canCreate($roles),
|
||||
self::DELETE => $this->canDelete($roles),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $roles
|
||||
*/
|
||||
private function canView(array $roles, SecurityUser $user, mixed $subject): bool
|
||||
{
|
||||
// Admins et personnel administratif : accès complet en lecture
|
||||
if ($this->hasAnyRole($roles, [
|
||||
Role::SUPER_ADMIN->value,
|
||||
Role::ADMIN->value,
|
||||
Role::VIE_SCOLAIRE->value,
|
||||
Role::SECRETARIAT->value,
|
||||
])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Enseignant : lecture seule de ses propres affectations
|
||||
if ($this->hasAnyRole($roles, [Role::PROF->value])) {
|
||||
return $this->isOwnResource($user, $subject);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie que la ressource appartient à l'enseignant connecté.
|
||||
*/
|
||||
private function isOwnResource(SecurityUser $user, mixed $subject): bool
|
||||
{
|
||||
if ($subject instanceof TeacherAssignment) {
|
||||
return (string) $subject->teacherId === $user->userId();
|
||||
}
|
||||
|
||||
if ($subject instanceof TeacherAssignmentResource) {
|
||||
return $subject->teacherId === $user->userId();
|
||||
}
|
||||
|
||||
// Pas de sujet (collection sans filtre) : refuser par défaut
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $roles
|
||||
*/
|
||||
private function canCreate(array $roles): bool
|
||||
{
|
||||
return $this->hasAnyRole($roles, [
|
||||
Role::SUPER_ADMIN->value,
|
||||
Role::ADMIN->value,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $roles
|
||||
*/
|
||||
private function canDelete(array $roles): bool
|
||||
{
|
||||
return $this->hasAnyRole($roles, [
|
||||
Role::SUPER_ADMIN->value,
|
||||
Role::ADMIN->value,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $userRoles
|
||||
* @param string[] $allowedRoles
|
||||
*/
|
||||
private function hasAnyRole(array $userRoles, array $allowedRoles): bool
|
||||
{
|
||||
foreach ($userRoles as $role) {
|
||||
if (in_array($role, $allowedRoles, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Infrastructure\Service;
|
||||
|
||||
use App\Administration\Application\Port\TeacherAssignmentChecker;
|
||||
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\Administration\Domain\Repository\TeacherAssignmentRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use Override;
|
||||
|
||||
final readonly class RepositoryTeacherAssignmentChecker implements TeacherAssignmentChecker
|
||||
{
|
||||
public function __construct(
|
||||
private TeacherAssignmentRepository $assignmentRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function estAffecte(
|
||||
UserId $teacherId,
|
||||
ClassId $classId,
|
||||
SubjectId $subjectId,
|
||||
AcademicYearId $academicYearId,
|
||||
TenantId $tenantId,
|
||||
): bool {
|
||||
$assignment = $this->assignmentRepository->findByTeacherClassSubject(
|
||||
$teacherId,
|
||||
$classId,
|
||||
$subjectId,
|
||||
$academicYearId,
|
||||
$tenantId,
|
||||
);
|
||||
|
||||
return $assignment !== null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user