['role:read']], denormalizationContext: ['groups' => ['role:write']], )] class Role { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] #[Groups(['role:read'])] private ?int $id = null; #[ORM\Column(length: 100, unique: true, options: ['comment' => 'Immutable role code (snake_case)'])] #[Groups(['role:read', 'role:write'])] private string $code; #[ORM\Column(length: 255, options: ['comment' => 'Human-readable role label'])] #[Groups(['role:read', 'role:write'])] private string $label; #[ORM\Column(type: 'text', nullable: true, options: ['comment' => 'Optional role description'])] #[Groups(['role:read', 'role:write'])] private ?string $description; #[ORM\Column(name: 'is_system', options: ['comment' => 'True for built-in roles that cannot be deleted'])] private bool $isSystem; /** * @var Collection */ #[ORM\ManyToMany(targetEntity: Permission::class, fetch: 'EAGER')] #[ORM\JoinTable(name: 'role_permission')] #[Groups(['role:read', 'role:write'])] private Collection $permissions; public function __construct(string $code, string $label, ?string $description = null, bool $isSystem = false) { if (1 !== preg_match('/^[a-z][a-z0-9_]*$/', $code)) { throw new InvalidArgumentException(sprintf('Code de rôle invalide : "%s" (attendu snake_case).', $code)); } if ('' === trim($label)) { throw new InvalidArgumentException('Le libellé de rôle ne peut pas être vide.'); } $this->code = $code; $this->label = $label; $this->description = $description; $this->isSystem = $isSystem; $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 setLabel(string $label): void { $this->label = $label; } public function getDescription(): ?string { return $this->description; } public function setDescription(?string $description): void { $this->description = $description; } // PropertyInfo strips the `is` prefix and would expose this field as `system`. // An explicit SerializedName guarantees the `isSystem` key expected by API clients. #[Groups(['role:read'])] #[SerializedName('isSystem')] public function isSystem(): bool { return $this->isSystem; } /** * @return Collection */ public function getPermissions(): Collection { return $this->permissions; } public function addPermission(Permission $permission): void { if (!$this->permissions->contains($permission)) { $this->permissions->add($permission); } } public function removePermission(Permission $permission): void { $this->permissions->removeElement($permission); } public function ensureDeletable(): void { if ($this->isSystem) { throw new SystemRoleDeletionException($this->code); } } }