feat: Gestion des matières scolaires

Les établissements ont besoin de définir leur référentiel de matières
pour pouvoir ensuite les associer aux enseignants et aux classes.
Cette fonctionnalité permet aux administrateurs de créer, modifier et
archiver les matières avec leurs propriétés (nom, code court, couleur).

L'architecture suit le pattern DDD avec des Value Objects utilisant
les property hooks PHP 8.5 pour garantir l'immutabilité et la validation.
L'isolation multi-tenant est assurée par vérification dans les handlers.
This commit is contained in:
2026-02-05 20:42:31 +01:00
parent 8e09e0abf1
commit 0d5a097c4c
50 changed files with 5882 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
<?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\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Administration\Infrastructure\Api\Processor\CreateSubjectProcessor;
use App\Administration\Infrastructure\Api\Processor\DeleteSubjectProcessor;
use App\Administration\Infrastructure\Api\Processor\UpdateSubjectProcessor;
use App\Administration\Infrastructure\Api\Provider\SubjectCollectionProvider;
use App\Administration\Infrastructure\Api\Provider\SubjectItemProvider;
use DateTimeImmutable;
use Symfony\Component\Validator\Constraints as Assert;
/**
* API Resource pour la gestion des matières.
*
* @see Story 2.2 - Création et Gestion des Matières
* @see FR74 - Structurer l'offre pédagogique
*/
#[ApiResource(
shortName: 'Subject',
operations: [
new GetCollection(
uriTemplate: '/subjects',
provider: SubjectCollectionProvider::class,
name: 'get_subjects',
),
new Get(
uriTemplate: '/subjects/{id}',
provider: SubjectItemProvider::class,
name: 'get_subject',
),
new Post(
uriTemplate: '/subjects',
processor: CreateSubjectProcessor::class,
validationContext: ['groups' => ['Default', 'create']],
name: 'create_subject',
),
new Patch(
uriTemplate: '/subjects/{id}',
provider: SubjectItemProvider::class,
processor: UpdateSubjectProcessor::class,
validationContext: ['groups' => ['Default', 'update']],
name: 'update_subject',
),
new Delete(
uriTemplate: '/subjects/{id}',
provider: SubjectItemProvider::class,
processor: DeleteSubjectProcessor::class,
name: 'delete_subject',
),
],
)]
final class SubjectResource
{
#[ApiProperty(identifier: true)]
public ?string $id = null;
#[Assert\NotBlank(message: 'Le nom de la matière est requis.', groups: ['create'])]
#[Assert\Length(
min: 2,
max: 100,
minMessage: 'Le nom de la matière doit contenir au moins {{ limit }} caractères.',
maxMessage: 'Le nom de la matière ne peut pas dépasser {{ limit }} caractères.',
)]
public ?string $name = null;
#[Assert\NotBlank(message: 'Le code de la matière est requis.', groups: ['create'])]
#[Assert\Regex(
pattern: '/^[A-Za-z0-9]{2,10}$/',
message: 'Le code doit contenir entre 2 et 10 caractères alphanumériques.',
)]
public ?string $code = null;
#[Assert\Regex(
pattern: '/^#[0-9A-Fa-f]{6}$/',
message: 'La couleur doit être au format hexadécimal #RRGGBB.',
)]
public ?string $color = null;
public ?string $description = null;
public ?string $status = null;
public ?DateTimeImmutable $createdAt = null;
public ?DateTimeImmutable $updatedAt = null;
/**
* Statistiques : nombre d'enseignants associés à cette matière.
* Disponible uniquement dans GetCollection.
*/
#[ApiProperty(readable: true, writable: false)]
public ?int $teacherCount = null;
/**
* Statistiques : nombre de classes associées à cette matière.
* Disponible uniquement dans GetCollection.
*/
#[ApiProperty(readable: true, writable: false)]
public ?int $classCount = null;
/**
* Permet de supprimer explicitement la couleur lors d'un PATCH.
* Si true, la couleur sera mise à null même si color n'est pas fourni.
*/
#[ApiProperty(readable: false)]
public ?bool $clearColor = null;
/**
* Permet de supprimer explicitement la description lors d'un PATCH.
* Si true, la description sera mise à null même si description n'est pas fourni.
*/
#[ApiProperty(readable: false)]
public ?bool $clearDescription = null;
/**
* Indique si la matière a des notes associées (pour avertissement avant suppression).
*/
#[ApiProperty(readable: true, writable: false)]
public ?bool $hasGrades = null;
}