Files
Classeo/backend/src/Administration/Infrastructure/Persistence/Doctrine/DoctrineSubjectRepository.php
Mathias STRASSER f19d0ae3ef feat: Gestion des périodes scolaires
L'administration d'un établissement nécessite de découper l'année
scolaire en trimestres ou semestres avant de pouvoir saisir les notes
et générer les bulletins.

Ce module permet de configurer les périodes par année scolaire
(current/previous/next résolus en UUID v5 déterministes), de modifier
les dates individuelles avec validation anti-chevauchement, et de
consulter la période en cours avec le décompte des jours restants.

Les dates par défaut de février s'adaptent aux années bissextiles.
Le repository utilise UPSERT transactionnel pour garantir l'intégrité
lors du changement de mode (trimestres ↔ semestres). Les domain events
de Subject sont étendus pour couvrir toutes les mutations (code,
couleur, description) en plus du renommage.
2026-02-06 14:27:55 +01:00

181 lines
5.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Administration\Infrastructure\Persistence\Doctrine;
use App\Administration\Domain\Exception\SubjectNotFoundException;
use App\Administration\Domain\Model\SchoolClass\SchoolId;
use App\Administration\Domain\Model\Subject\Subject;
use App\Administration\Domain\Model\Subject\SubjectCode;
use App\Administration\Domain\Model\Subject\SubjectColor;
use App\Administration\Domain\Model\Subject\SubjectId;
use App\Administration\Domain\Model\Subject\SubjectName;
use App\Administration\Domain\Model\Subject\SubjectStatus;
use App\Administration\Domain\Repository\SubjectRepository;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;
use Override;
final readonly class DoctrineSubjectRepository implements SubjectRepository
{
public function __construct(
private Connection $connection,
) {
}
#[Override]
public function save(Subject $subject): void
{
$this->connection->executeStatement(
'INSERT INTO subjects (id, tenant_id, school_id, name, code, color, status, description, created_at, updated_at, deleted_at)
VALUES (:id, :tenant_id, :school_id, :name, :code, :color, :status, :description, :created_at, :updated_at, :deleted_at)
ON CONFLICT (id) DO UPDATE SET
name = EXCLUDED.name,
code = EXCLUDED.code,
color = EXCLUDED.color,
status = EXCLUDED.status,
description = EXCLUDED.description,
updated_at = EXCLUDED.updated_at,
deleted_at = EXCLUDED.deleted_at',
[
'id' => (string) $subject->id,
'tenant_id' => (string) $subject->tenantId,
'school_id' => (string) $subject->schoolId,
'name' => (string) $subject->name,
'code' => (string) $subject->code,
'color' => $subject->color !== null ? (string) $subject->color : null,
'status' => $subject->status->value,
'description' => $subject->description,
'created_at' => $subject->createdAt->format(DateTimeImmutable::ATOM),
'updated_at' => $subject->updatedAt->format(DateTimeImmutable::ATOM),
'deleted_at' => $subject->deletedAt?->format(DateTimeImmutable::ATOM),
],
);
}
#[Override]
public function get(SubjectId $id): Subject
{
$subject = $this->findById($id);
if ($subject === null) {
throw SubjectNotFoundException::withId($id);
}
return $subject;
}
#[Override]
public function findById(SubjectId $id): ?Subject
{
$row = $this->connection->fetchAssociative(
'SELECT * FROM subjects WHERE id = :id',
['id' => (string) $id],
);
if ($row === false) {
return null;
}
return $this->hydrate($row);
}
#[Override]
public function findByCode(
SubjectCode $code,
TenantId $tenantId,
SchoolId $schoolId,
): ?Subject {
$row = $this->connection->fetchAssociative(
'SELECT * FROM subjects
WHERE tenant_id = :tenant_id
AND school_id = :school_id
AND code = :code
AND deleted_at IS NULL',
[
'tenant_id' => (string) $tenantId,
'school_id' => (string) $schoolId,
'code' => (string) $code,
],
);
if ($row === false) {
return null;
}
return $this->hydrate($row);
}
#[Override]
public function findActiveByTenantAndSchool(
TenantId $tenantId,
SchoolId $schoolId,
): array {
$rows = $this->connection->fetchAllAssociative(
'SELECT * FROM subjects
WHERE tenant_id = :tenant_id
AND school_id = :school_id
AND status = :status
ORDER BY name ASC',
[
'tenant_id' => (string) $tenantId,
'school_id' => (string) $schoolId,
'status' => SubjectStatus::ACTIVE->value,
],
);
return array_map(fn ($row) => $this->hydrate($row), $rows);
}
#[Override]
public function delete(SubjectId $id): void
{
$this->connection->delete('subjects', ['id' => (string) $id]);
}
/**
* @param array<string, mixed> $row
*/
private function hydrate(array $row): Subject
{
/** @var string $id */
$id = $row['id'];
/** @var string $tenantId */
$tenantId = $row['tenant_id'];
/** @var string $schoolId */
$schoolId = $row['school_id'];
/** @var non-empty-string $name */
$name = $row['name'];
/** @var non-empty-string $code */
$code = $row['code'];
/** @var non-empty-string|null $color */
$color = $row['color'];
/** @var string $status */
$status = $row['status'];
/** @var string|null $description */
$description = $row['description'];
/** @var string $createdAt */
$createdAt = $row['created_at'];
/** @var string $updatedAt */
$updatedAt = $row['updated_at'];
/** @var string|null $deletedAt */
$deletedAt = $row['deleted_at'];
return Subject::reconstitute(
id: SubjectId::fromString($id),
tenantId: TenantId::fromString($tenantId),
schoolId: SchoolId::fromString($schoolId),
name: new SubjectName($name),
code: new SubjectCode($code),
color: $color !== null ? new SubjectColor($color) : null,
status: SubjectStatus::from($status),
description: $description,
createdAt: new DateTimeImmutable($createdAt),
updatedAt: new DateTimeImmutable($updatedAt),
deletedAt: $deletedAt !== null ? new DateTimeImmutable($deletedAt) : null,
);
}
}