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,146 @@
<?php
declare(strict_types=1);
namespace App\Administration\Infrastructure\Security;
use App\Administration\Domain\Model\Subject\Subject;
use App\Administration\Domain\Model\User\Role;
use App\Administration\Infrastructure\Api\Resource\SubjectResource;
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;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* Voter pour les autorisations sur les matières.
*
* Règles d'accès :
* - ADMIN et SUPER_ADMIN : accès complet (CRUD)
* - ENSEIGNANT : lecture seule (via affectations)
* - VIE_SCOLAIRE, SECRETARIAT : lecture seule
* - ELEVE et PARENT : pas d'accès direct
*
* @extends Voter<string, Subject|SubjectResource>
*/
final class SubjectVoter extends Voter
{
public const string VIEW = 'SUBJECT_VIEW';
public const string CREATE = 'SUBJECT_CREATE';
public const string EDIT = 'SUBJECT_EDIT';
public const string DELETE = 'SUBJECT_DELETE';
private const array SUPPORTED_ATTRIBUTES = [
self::VIEW,
self::CREATE,
self::EDIT,
self::DELETE,
];
#[Override]
protected function supports(string $attribute, mixed $subject): bool
{
if (!in_array($attribute, self::SUPPORTED_ATTRIBUTES, true)) {
return false;
}
// CREATE, EDIT, DELETE, and VIEW (for collections) don't require a subject
// since authorization is role-based, not object-based
if ($subject === null) {
return true;
}
return $subject instanceof Subject || $subject instanceof SubjectResource;
}
#[Override]
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{
$user = $token->getUser();
if (!$user instanceof UserInterface) {
return false;
}
// Récupérer le rôle depuis les rôles Symfony
$roles = $user->getRoles();
return match ($attribute) {
self::VIEW => $this->canView($roles),
self::CREATE => $this->canCreate($roles),
self::EDIT => $this->canEdit($roles),
self::DELETE => $this->canDelete($roles),
default => false,
};
}
/**
* @param string[] $roles
*/
private function canView(array $roles): bool
{
// Personnel de l'établissement uniquement
return $this->hasAnyRole($roles, [
Role::SUPER_ADMIN->value,
Role::ADMIN->value,
Role::PROF->value,
Role::VIE_SCOLAIRE->value,
Role::SECRETARIAT->value,
]);
}
/**
* @param string[] $roles
*/
private function canCreate(array $roles): bool
{
// Seuls ADMIN et SUPER_ADMIN peuvent créer des matières
return $this->hasAnyRole($roles, [
Role::SUPER_ADMIN->value,
Role::ADMIN->value,
]);
}
/**
* @param string[] $roles
*/
private function canEdit(array $roles): bool
{
// Seuls ADMIN et SUPER_ADMIN peuvent modifier des matières
return $this->hasAnyRole($roles, [
Role::SUPER_ADMIN->value,
Role::ADMIN->value,
]);
}
/**
* @param string[] $roles
*/
private function canDelete(array $roles): bool
{
// Seuls ADMIN et SUPER_ADMIN peuvent supprimer des matières
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;
}
}