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.
181 lines
5.9 KiB
PHP
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,
|
|
);
|
|
}
|
|
}
|