L'admin doit pouvoir voir en un coup d'œil quelles matières sont actives (notes saisies) pour décider lesquelles peuvent être supprimées sans perte de données. Auparavant, la suppression d'une matière était silencieuse : elle cascade-deletait évaluations et notes sans avertir. La liste des matières affiche désormais les compteurs d'enseignants, classes, évaluations et notes. La suppression déclenche une confirmation explicite quand la matière contient des notes, avec récapitulatif des volumes impactés, pour rendre l'action irréversible consciente. Côté tests, un endpoint de seeding HTTP remplace les appels docker exec dans les E2E (gain ~30-60s → 5-10s par test), et un trait partagé factorise le SQL de seeding entre les deux suites fonctionnelles.
186 lines
6.1 KiB
PHP
186 lines
6.1 KiB
PHP
<?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\Application\Query\GetSubjects\SubjectDto;
|
|
use App\Administration\Domain\Model\Subject\Subject;
|
|
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;
|
|
|
|
/**
|
|
* Statistiques : nombre d'évaluations créées pour cette matière.
|
|
*/
|
|
#[ApiProperty(readable: true, writable: false)]
|
|
public ?int $evaluationCount = null;
|
|
|
|
/**
|
|
* Statistiques : nombre de notes saisies pour cette matière.
|
|
*/
|
|
#[ApiProperty(readable: true, writable: false)]
|
|
public ?int $gradeCount = 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;
|
|
|
|
/**
|
|
* Crée un SubjectResource à partir du domain model.
|
|
*/
|
|
public static function fromDomain(Subject $subject): self
|
|
{
|
|
$resource = new self();
|
|
$resource->id = (string) $subject->id;
|
|
$resource->name = (string) $subject->name;
|
|
$resource->code = (string) $subject->code;
|
|
$resource->color = $subject->color !== null ? (string) $subject->color : null;
|
|
$resource->description = $subject->description;
|
|
$resource->status = $subject->status->value;
|
|
$resource->createdAt = $subject->createdAt;
|
|
$resource->updatedAt = $subject->updatedAt;
|
|
|
|
return $resource;
|
|
}
|
|
|
|
/**
|
|
* Crée un SubjectResource à partir d'un DTO de query.
|
|
*/
|
|
public static function fromDto(SubjectDto $dto): self
|
|
{
|
|
$resource = new self();
|
|
$resource->id = $dto->id;
|
|
$resource->name = $dto->name;
|
|
$resource->code = $dto->code;
|
|
$resource->color = $dto->color;
|
|
$resource->description = $dto->description;
|
|
$resource->status = $dto->status;
|
|
$resource->createdAt = $dto->createdAt;
|
|
$resource->updatedAt = $dto->updatedAt;
|
|
$resource->teacherCount = $dto->teacherCount;
|
|
$resource->classCount = $dto->classCount;
|
|
$resource->evaluationCount = $dto->evaluationCount;
|
|
$resource->gradeCount = $dto->gradeCount;
|
|
$resource->hasGrades = $dto->hasGrades();
|
|
|
|
return $resource;
|
|
}
|
|
}
|