feat(core) : RBAC Task 1 - entites Permission et Role + domaine securite

- Entite Permission avec methodes markOrphan/revive/updateMetadata
- Entite Role avec addPermission/removePermission/ensureDeletable
- Constantes SystemRoles (codes admin/user partages)
- Exception SystemRoleDeletionException pour la garde de suppression
- Tests unitaires couvrant le comportement domaine (pas de BDD)

Ticket #343 - 1/7 : fondations RBAC (domaine pur, sans persistence).
Les entites ne portent pas encore repositoryClass (ajoute en Task 2).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-04-14 16:30:15 +02:00
parent e3025bf2c9
commit f0ea9201f5
7 changed files with 459 additions and 0 deletions

View File

@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace App\Module\Core\Domain\Entity;
use App\Module\Core\Domain\Exception\SystemRoleDeletionException;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
/**
* Role RBAC : groupe nomme de permissions assignable a un utilisateur.
*
* Un role peut etre "systeme" (cree et protege par la plateforme) ou
* "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\Table(name: '`role`')]
#[ORM\UniqueConstraint(name: 'uniq_role_code', columns: ['code'])]
#[ORM\Index(name: 'idx_role_is_system', columns: ['is_system'])]
class Role
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 100)]
private string $code;
#[ORM\Column(length: 255)]
private string $label;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $description = null;
#[ORM\Column(name: 'is_system', options: ['default' => false])]
private bool $isSystem = false;
/** @var Collection<int, Permission> */
#[ORM\ManyToMany(targetEntity: Permission::class, fetch: 'EAGER')]
#[ORM\JoinTable(name: 'role_permission')]
private Collection $permissions;
public function __construct(string $code, string $label, bool $isSystem = false, ?string $description = null)
{
$this->code = $code;
$this->label = $label;
$this->isSystem = $isSystem;
$this->description = $description;
$this->permissions = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getCode(): string
{
return $this->code;
}
public function getLabel(): string
{
return $this->label;
}
public function getDescription(): ?string
{
return $this->description;
}
public function isSystem(): bool
{
return $this->isSystem;
}
/** @return Collection<int, Permission> */
public function getPermissions(): Collection
{
return $this->permissions;
}
/**
* Met a jour le libelle affichable du role. Le code reste immuable pour
* garantir la stabilite des references cote fixtures et migrations.
*/
public function setLabel(string $label): self
{
$this->label = $label;
return $this;
}
/**
* Met a jour la description libre du role (champ documentaire).
*/
public function setDescription(?string $description): self
{
$this->description = $description;
return $this;
}
/**
* Ajoute une permission au role. Idempotent : ajouter deux fois la meme
* permission n'entraine pas de doublon dans la collection.
*/
public function addPermission(Permission $permission): self
{
if (!$this->permissions->contains($permission)) {
$this->permissions->add($permission);
}
return $this;
}
/**
* Retire une permission du role. Idempotent : retirer une permission
* absente est un no-op silencieux.
*/
public function removePermission(Permission $permission): self
{
$this->permissions->removeElement($permission);
return $this;
}
/**
* Garde domaine : refuse la suppression d'un role marque comme systeme.
* La traduction HTTP (403) est faite au niveau application / API Platform.
*/
public function ensureDeletable(): void
{
if ($this->isSystem) {
throw SystemRoleDeletionException::forRole($this);
}
}
}