feat(core) : RBAC Task 2 - repositories Permission et Role

- PermissionRepositoryInterface avec findByCode et findAllCodes (pour le sync
  command et le futur PermissionVoter)
- RoleRepositoryInterface avec findByCode
- Implementations Doctrine alignees sur DoctrineUserRepository
- Alias DI dans config/services.yaml
- Rebranchement de repositoryClass sur les entites Permission et Role

Ticket #343 - 2/7 : couche persistence RBAC.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-04-14 16:40:44 +02:00
parent 0fc0b57e37
commit 3b34d00872
8 changed files with 179 additions and 5 deletions

View File

@@ -16,5 +16,11 @@ services:
App\:
resource: '../src/'
App\Module\Core\Domain\Repository\PermissionRepositoryInterface:
alias: App\Module\Core\Infrastructure\Doctrine\DoctrinePermissionRepository
App\Module\Core\Domain\Repository\RoleRepositoryInterface:
alias: App\Module\Core\Infrastructure\Doctrine\DoctrineRoleRepository
App\Module\Core\Domain\Repository\UserRepositoryInterface:
alias: App\Module\Core\Infrastructure\Doctrine\DoctrineUserRepository

View File

@@ -4,11 +4,11 @@ declare(strict_types=1);
namespace App\Module\Core\Domain\Entity;
use App\Module\Core\Infrastructure\Doctrine\DoctrinePermissionRepository;
use Doctrine\ORM\Mapping as ORM;
use InvalidArgumentException;
// TODO: brancher repositoryClass au ticket 343 partie 2 (Task 2).
#[ORM\Entity]
#[ORM\Entity(repositoryClass: DoctrinePermissionRepository::class)]
#[ORM\Table(name: 'permission')]
#[ORM\UniqueConstraint(name: 'uniq_permission_code', columns: ['code'])]
#[ORM\Index(name: 'idx_permission_module', columns: ['module'])]

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Module\Core\Domain\Entity;
use App\Module\Core\Domain\Exception\SystemRoleDeletionException;
use App\Module\Core\Infrastructure\Doctrine\DoctrineRoleRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
@@ -17,8 +18,7 @@ use Doctrine\ORM\Mapping as ORM;
* "personnalise" (cree par un administrateur). Seuls les roles personnalises
* peuvent etre supprimes.
*/
// TODO: brancher repositoryClass au ticket 343 partie 2 (Task 2).
#[ORM\Entity]
#[ORM\Entity(repositoryClass: DoctrineRoleRepository::class)]
#[ORM\Table(name: '`role`')]
#[ORM\UniqueConstraint(name: 'uniq_role_code', columns: ['code'])]
#[ORM\Index(name: 'idx_role_is_system', columns: ['is_system'])]

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Module\Core\Domain\Repository;
use App\Module\Core\Domain\Entity\Permission;
/**
* Contrat du catalogue de permissions RBAC.
*
* Utilise par la commande de synchronisation (app:sync-permissions), les
* fixtures, et — a terme (ticket #345) — par le PermissionVoter pour valider
* que les codes verifies existent bien dans le catalogue.
*/
interface PermissionRepositoryInterface
{
public function findById(int $id): ?Permission;
public function findByCode(string $code): ?Permission;
/**
* @return array<int, Permission>
*/
public function findAll(): array;
/**
* @return array<int, string> liste des codes connus, pour la commande de sync et le futur voter
*/
public function findAllCodes(): array;
public function save(Permission $permission): void;
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Module\Core\Domain\Repository;
use App\Module\Core\Domain\Entity\Role;
/**
* Contrat des roles RBAC.
*
* Utilise par les fixtures, la future API d'administration (ticket #344) et
* le PermissionVoter pour resoudre les permissions effectives d'un role.
*/
interface RoleRepositoryInterface
{
public function findById(int $id): ?Role;
public function findByCode(string $code): ?Role;
/**
* @return array<int, Role>
*/
public function findAll(): array;
public function save(Role $role): void;
}

View File

@@ -6,6 +6,7 @@ namespace App\Module\Core\Infrastructure\ApiPlatform\State\Provider;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use Symfony\Bundle\SecurityBundle\Security;
/**
* @implements ProviderInterface<object>
@@ -13,7 +14,7 @@ use ApiPlatform\State\ProviderInterface;
class MeProvider implements ProviderInterface
{
public function __construct(
private readonly \Symfony\Bundle\SecurityBundle\Security $security,
private readonly Security $security,
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?object

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace App\Module\Core\Infrastructure\Doctrine;
use App\Module\Core\Domain\Entity\Permission;
use App\Module\Core\Domain\Repository\PermissionRepositoryInterface;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Permission>
*/
class DoctrinePermissionRepository extends ServiceEntityRepository implements PermissionRepositoryInterface
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Permission::class);
}
public function findById(int $id): ?Permission
{
return $this->find($id);
}
public function findByCode(string $code): ?Permission
{
return $this->findOneBy(['code' => $code]);
}
/**
* @return array<int, Permission>
*/
public function findAll(): array
{
return parent::findAll();
}
/**
* @return array<int, string>
*/
public function findAllCodes(): array
{
// Requete legere : on ne selectionne que la colonne code (pas d'hydratation
// d'entites Permission) car findAllCodes() est appelee par la commande de
// sync et le futur voter qui n'ont besoin que des chaines.
$rows = $this->createQueryBuilder('p')
->select('p.code')
->getQuery()
->getArrayResult()
;
return array_column($rows, 'code');
}
public function save(Permission $permission): void
{
$this->getEntityManager()->persist($permission);
$this->getEntityManager()->flush();
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Module\Core\Infrastructure\Doctrine;
use App\Module\Core\Domain\Entity\Role;
use App\Module\Core\Domain\Repository\RoleRepositoryInterface;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Role>
*/
class DoctrineRoleRepository extends ServiceEntityRepository implements RoleRepositoryInterface
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Role::class);
}
public function findById(int $id): ?Role
{
return $this->find($id);
}
public function findByCode(string $code): ?Role
{
return $this->findOneBy(['code' => $code]);
}
/**
* @return array<int, Role>
*/
public function findAll(): array
{
return parent::findAll();
}
public function save(Role $role): void
{
$this->getEntityManager()->persist($role);
$this->getEntityManager()->flush();
}
}