Working state

This commit is contained in:
julien
2026-03-16 13:40:18 +01:00
parent dec76fa2c7
commit 557360dfde
57 changed files with 1044 additions and 1668 deletions

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\User\Exception;
use App\User\User;
final class RoleAssignmentNotAllowedException extends \InvalidArgumentException
{
public function __construct(string $role)
{
parent::__construct(
"Le rôle '{$role}' ne peut pas être attribué depuis l'interface. Rôles autorisés : "
. implode(', ', User::assignableRoles())
);
}
}

View File

@@ -6,38 +6,14 @@ namespace App\User;
use App\Shared\Util\DateParser;
use DateTime;
/**
* Modèle représentant un utilisateur de l'application.
*
* Encapsule les données et la validation d'un compte utilisateur.
* Ce modèle est immuable : toutes les propriétés sont en lecture seule
* après construction.
*/
final class User
{
/**
* @var DateTime Date de création — toujours non nulle après construction
* (le constructeur accepte ?DateTime mais affecte `new DateTime()` si null)
*/
private readonly DateTime $createdAt;
/**
* Rôles valides pour un utilisateur.
*/
public const ROLE_USER = 'user';
public const ROLE_EDITOR = 'editor';
public const ROLE_ADMIN = 'admin';
/**
* @param int $id Identifiant en base (0 pour un nouvel utilisateur)
* @param string $username Nom d'utilisateur (normalisé en minuscules)
* @param string $email Adresse e-mail (normalisée en minuscules)
* @param string $passwordHash Hash bcrypt du mot de passe
* @param string $role Rôle de l'utilisateur : 'user', 'editor' ou 'admin'
* @param DateTime|null $createdAt Date de création (défaut : maintenant)
*
* @throws \InvalidArgumentException Si les données ne passent pas la validation
*/
public function __construct(
private readonly int $id,
private readonly string $username,
@@ -51,11 +27,7 @@ final class User
}
/**
* Crée une instance depuis un tableau associatif (ligne de base de données).
*
* @param array<string, mixed> $data Données issues de la base de données
*
* @return self L'instance hydratée
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
@@ -70,105 +42,69 @@ final class User
}
/**
* Retourne l'identifiant de l'utilisateur.
*
* @return int L'identifiant en base (0 si non encore persisté)
* @return string[]
*/
public static function allRoles(): array
{
return [self::ROLE_USER, self::ROLE_EDITOR, self::ROLE_ADMIN];
}
/**
* @return string[]
*/
public static function assignableRoles(): array
{
return [self::ROLE_USER, self::ROLE_EDITOR];
}
public function getId(): int
{
return $this->id;
}
/**
* Retourne le nom d'utilisateur.
*
* @return string Le nom d'utilisateur normalisé en minuscules
*/
public function getUsername(): string
{
return $this->username;
}
/**
* Retourne l'adresse e-mail.
*
* @return string L'adresse e-mail normalisée en minuscules
*/
public function getEmail(): string
{
return $this->email;
}
/**
* Retourne le hash bcrypt du mot de passe.
*
* @return string Le hash bcrypt
*/
public function getPasswordHash(): string
{
return $this->passwordHash;
}
/**
* Retourne le rôle de l'utilisateur.
*
* @return string 'user', 'editor' ou 'admin'
*/
public function getRole(): string
{
return $this->role;
}
/**
* Indique si l'utilisateur a le rôle administrateur.
*
* @return bool True si l'utilisateur est administrateur
*/
public function isAdmin(): bool
{
return $this->role === self::ROLE_ADMIN;
}
/**
* Indique si l'utilisateur a le rôle éditeur.
*
* @return bool True si l'utilisateur est éditeur
*/
public function isEditor(): bool
{
return $this->role === self::ROLE_EDITOR;
}
/**
* Retourne la date de création du compte.
*
* @return DateTime La date de création
*/
public function getCreatedAt(): DateTime
{
return $this->createdAt;
}
/**
* Valide les données de l'utilisateur.
*
* @throws \InvalidArgumentException Si le nom d'utilisateur fait moins de 3 ou plus de 50 caractères
* @throws \InvalidArgumentException Si l'adresse e-mail est invalide ou vide
* @throws \InvalidArgumentException Si le hash du mot de passe est vide
* @throws \InvalidArgumentException Si le rôle n'est pas une valeur autorisée
*/
private function validate(): void
{
if (mb_strlen($this->username) < 3) {
throw new \InvalidArgumentException(
"Le nom d'utilisateur doit contenir au moins 3 caractères"
);
throw new \InvalidArgumentException("Le nom d'utilisateur doit contenir au moins 3 caractères");
}
if (mb_strlen($this->username) > 50) {
throw new \InvalidArgumentException(
"Le nom d'utilisateur ne peut pas dépasser 50 caractères"
);
throw new \InvalidArgumentException("Le nom d'utilisateur ne peut pas dépasser 50 caractères");
}
if (!filter_var($this->email, FILTER_VALIDATE_EMAIL)) {
@@ -176,15 +112,12 @@ final class User
}
if ($this->passwordHash === '') {
throw new \InvalidArgumentException(
'Le hash du mot de passe ne peut pas être vide'
);
throw new \InvalidArgumentException('Le hash du mot de passe ne peut pas être vide');
}
$validRoles = [self::ROLE_USER, self::ROLE_EDITOR, self::ROLE_ADMIN];
if (!in_array($this->role, $validRoles, true)) {
if (!in_array($this->role, self::allRoles(), true)) {
throw new \InvalidArgumentException(
"Le rôle '{$this->role}' est invalide. Valeurs autorisées : " . implode(', ', $validRoles)
"Le rôle '{$this->role}' est invalide. Valeurs autorisées : " . implode(', ', self::allRoles())
);
}
}

View File

@@ -5,33 +5,20 @@ namespace App\User;
use App\Shared\Http\FlashServiceInterface;
use App\Shared\Http\SessionManagerInterface;
use App\Shared\Pagination\PaginationPresenter;
use App\User\Exception\DuplicateEmailException;
use App\User\Exception\DuplicateUsernameException;
use App\User\Exception\InvalidRoleException;
use App\User\Exception\RoleAssignmentNotAllowedException;
use App\User\Exception\WeakPasswordException;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Views\Twig;
/**
* Contrôleur pour la gestion des utilisateurs en administration.
*
* Accessible uniquement aux administrateurs (AdminMiddleware).
* Gère la liste, la création, la modification de rôle et la suppression des comptes.
* Toute la logique de persistance est déléguée à UserService.
*
* Règles de protection communes :
* - Le compte administrateur (role = 'admin') ne peut pas être supprimé ni rétrogradé
* - Un administrateur ne peut pas supprimer son propre compte ni changer son propre rôle
*/
final class UserController
{
/**
* @param Twig $view Moteur de templates Twig
* @param UserServiceInterface $userService Service de gestion des utilisateurs
* @param FlashServiceInterface $flash Service de messages flash
* @param SessionManagerInterface $sessionManager Gestionnaire de session
*/
private const PER_PAGE = 15;
public function __construct(
private readonly Twig $view,
private readonly UserServiceInterface $userService,
@@ -40,84 +27,47 @@ final class UserController
) {
}
/**
* Affiche la liste de tous les utilisateurs.
*
* Passe l'identifiant de l'utilisateur courant à la vue
* pour conditionner l'affichage du bouton de suppression.
*
* @param Request $req La requête HTTP
* @param Response $res La réponse HTTP
*
* @return Response La vue admin/users/index.twig
*/
public function index(Request $req, Response $res): Response
{
$page = PaginationPresenter::resolvePage($req->getQueryParams());
$paginated = $this->userService->findPaginated($page, self::PER_PAGE);
return $this->view->render($res, 'admin/users/index.twig', [
'users' => $this->userService->findAll(),
'users' => $paginated->getItems(),
'pagination' => PaginationPresenter::fromRequest($req, $paginated),
'currentUserId' => $this->sessionManager->getUserId(),
'assignableRoles' => User::assignableRoles(),
'error' => $this->flash->get('user_error'),
'success' => $this->flash->get('user_success'),
]);
}
/**
* Affiche le formulaire de création d'un utilisateur.
*
* @param Request $req La requête HTTP
* @param Response $res La réponse HTTP
*
* @return Response La vue admin/users/form.twig
*/
public function showCreate(Request $req, Response $res): Response
{
return $this->view->render($res, 'admin/users/form.twig', [
'assignableRoles' => User::assignableRoles(),
'error' => $this->flash->get('user_error'),
]);
}
/**
* Traite la soumission du formulaire de création d'utilisateur.
*
* Vérifie que les mots de passe correspondent avant de déléguer
* la création à UserService. En cas d'erreur, redirige vers le
* formulaire avec un message flash.
*
* @param Request $req La requête HTTP
* @param Response $res La réponse HTTP
*
* @return Response Redirection vers /admin/users en cas de succès,
* ou vers /admin/users/create en cas d'erreur
*/
public function create(Request $req, Response $res): Response
{
/** @var array<string, mixed> $data */
$data = (array) $req->getParsedBody();
$username = trim((string) ($data['username'] ?? ''));
$email = trim((string) ($data['email'] ?? ''));
$password = trim((string) ($data['password'] ?? ''));
$confirm = trim((string) ($data['password_confirm'] ?? ''));
// Restreindre les rôles assignables depuis le formulaire.
// Le rôle 'admin' est exclu : il ne peut être attribué que directement
// en base de données, pour éviter qu'un admin ne crée d'autres admins
// en manipulant la requête HTTP.
$allowedRoles = [User::ROLE_USER, User::ROLE_EDITOR];
$rawRole = trim((string) ($data['role'] ?? ''));
$role = in_array($rawRole, $allowedRoles, true)
? $rawRole
: User::ROLE_USER;
$rawRole = trim((string) ($data['role'] ?? ''));
$role = in_array($rawRole, User::assignableRoles(), true) ? $rawRole : User::ROLE_USER;
if ($password !== $confirm) {
$this->flash->set('user_error', 'Les mots de passe ne correspondent pas');
return $res->withHeader('Location', '/admin/users/create')->withStatus(302);
}
try {
$this->userService->createUser($username, $email, $password, $role);
$this->flash->set('user_success', "L'utilisateur « {$username} » a été créé avec succès");
return $res->withHeader('Location', '/admin/users')->withStatus(302);
} catch (DuplicateUsernameException) {
$this->flash->set('user_error', "Ce nom d'utilisateur est déjà pris");
@@ -125,7 +75,7 @@ final class UserController
$this->flash->set('user_error', 'Cette adresse e-mail est déjà utilisée');
} catch (WeakPasswordException) {
$this->flash->set('user_error', 'Le mot de passe doit contenir au moins 8 caractères');
} catch (InvalidRoleException $e) {
} catch (InvalidRoleException|RoleAssignmentNotAllowedException $e) {
$this->flash->set('user_error', $e->getMessage());
} catch (\Throwable) {
$this->flash->set('user_error', "Une erreur inattendue s'est produite");
@@ -135,18 +85,7 @@ final class UserController
}
/**
* Met à jour le rôle d'un utilisateur.
*
* La modification est refusée dans trois cas :
* - l'utilisateur cible est introuvable
* - l'administrateur connecté tente de modifier son propre rôle
* - l'utilisateur cible est déjà administrateur
*
* @param Request $req La requête HTTP
* @param Response $res La réponse HTTP
* @param array<string, string> $args Les paramètres de route (id)
*
* @return Response Redirection vers /admin/users dans tous les cas
* @param array<string, mixed> $args
*/
public function updateRole(Request $req, Response $res, array $args): Response
{
@@ -155,53 +94,40 @@ final class UserController
if ($user === null) {
$this->flash->set('user_error', 'Utilisateur introuvable');
return $res->withHeader('Location', '/admin/users')->withStatus(302);
}
if ($id === $this->sessionManager->getUserId()) {
$this->flash->set('user_error', 'Vous ne pouvez pas modifier votre propre rôle');
return $res->withHeader('Location', '/admin/users')->withStatus(302);
}
if ($user->isAdmin()) {
$this->flash->set('user_error', 'Le rôle d\'un administrateur ne peut pas être modifié');
$this->flash->set('user_error', 'Le rôle d\'un administrateur ne peut pas être modifié depuis l\'interface');
return $res->withHeader('Location', '/admin/users')->withStatus(302);
}
$allowedRoles = [User::ROLE_USER, User::ROLE_EDITOR, User::ROLE_ADMIN];
/** @var array<string, mixed> $body */
$body = (array) $req->getParsedBody();
$rawRole = trim((string) ($body['role'] ?? ''));
$role = in_array($rawRole, $allowedRoles, true) ? $rawRole : null;
$body = (array) $req->getParsedBody();
$rawRole = trim((string) ($body['role'] ?? ''));
$role = in_array($rawRole, User::assignableRoles(), true) ? $rawRole : null;
if ($role === null) {
$this->flash->set('user_error', 'Rôle invalide');
return $res->withHeader('Location', '/admin/users')->withStatus(302);
}
$this->userService->updateRole($id, $role);
$this->flash->set('user_success', "Le rôle de « {$user->getUsername()} » a été mis à jour");
try {
$this->userService->updateRole($id, $role);
$this->flash->set('user_success', "Le rôle de « {$user->getUsername()} » a été mis à jour");
} catch (InvalidRoleException|RoleAssignmentNotAllowedException $e) {
$this->flash->set('user_error', $e->getMessage());
}
return $res->withHeader('Location', '/admin/users')->withStatus(302);
}
/**
* Supprime un utilisateur.
*
* La suppression est refusée dans trois cas :
* - l'utilisateur cible est introuvable
* - l'utilisateur cible est administrateur (role = 'admin')
* - l'administrateur connecté tente de supprimer son propre compte
*
* @param Request $req La requête HTTP
* @param Response $res La réponse HTTP
* @param array<string, string> $args Les paramètres de route (id)
*
* @return Response Redirection vers /admin/users dans tous les cas
* @param array<string, mixed> $args
*/
public function delete(Request $req, Response $res, array $args): Response
{
@@ -210,19 +136,16 @@ final class UserController
if ($user === null) {
$this->flash->set('user_error', 'Utilisateur introuvable');
return $res->withHeader('Location', '/admin/users')->withStatus(302);
}
if ($user->isAdmin()) {
$this->flash->set('user_error', 'Le compte administrateur ne peut pas être supprimé');
return $res->withHeader('Location', '/admin/users')->withStatus(302);
}
if ($id === $this->sessionManager->getUserId()) {
$this->flash->set('user_error', 'Vous ne pouvez pas supprimer votre propre compte');
return $res->withHeader('Location', '/admin/users')->withStatus(302);
}

View File

@@ -5,101 +5,75 @@ namespace App\User;
use PDO;
/**
* Dépôt pour la persistance des utilisateurs.
*
* Responsabilité unique : exécuter les requêtes SQL liées à la table `users`
* et retourner des instances de User hydratées.
*/
final class UserRepository implements UserRepositoryInterface
{
/**
* @param PDO $db Instance de connexion à la base de données
*/
public function __construct(private readonly PDO $db)
{
}
/**
* Retourne tous les utilisateurs triés par date de création.
*
* @return User[] La liste des utilisateurs
*/
public function findAll(): array
{
$stmt = $this->db->query('SELECT * FROM users ORDER BY created_at ASC');
if ($stmt === false) {
throw new \RuntimeException('La requête SELECT sur users a échoué.');
}
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
return array_map(fn ($row) => User::fromArray($row), $rows);
return array_map(fn ($row) => User::fromArray($row), $stmt->fetchAll(PDO::FETCH_ASSOC));
}
public function findPage(int $limit, int $offset): array
{
$stmt = $this->db->prepare('SELECT * FROM users ORDER BY created_at ASC LIMIT :limit OFFSET :offset');
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
return array_map(fn ($row) => User::fromArray($row), $stmt->fetchAll(PDO::FETCH_ASSOC));
}
public function countAll(): int
{
$stmt = $this->db->query('SELECT COUNT(*) FROM users');
if ($stmt === false) {
throw new \RuntimeException('La requête COUNT sur users a échoué.');
}
return (int) ($stmt->fetchColumn() ?: 0);
}
/**
* Trouve un utilisateur par son identifiant.
*
* @param int $id Identifiant de l'utilisateur
*
* @return User|null L'utilisateur trouvé, ou null s'il n'existe pas
*/
public function findById(int $id): ?User
{
$stmt = $this->db->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute([':id' => $id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ? User::fromArray($row) : null;
}
/**
* Trouve un utilisateur par son nom d'utilisateur (insensible à la casse).
*
* @param string $username Nom d'utilisateur normalisé en minuscules
*
* @return User|null L'utilisateur trouvé, ou null s'il n'existe pas
*/
public function findByUsername(string $username): ?User
{
$stmt = $this->db->prepare('SELECT * FROM users WHERE username = :username');
$stmt->execute([':username' => $username]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ? User::fromArray($row) : null;
}
/**
* Trouve un utilisateur par son adresse e-mail (insensible à la casse).
*
* @param string $email Adresse e-mail normalisée en minuscules
*
* @return User|null L'utilisateur trouvé, ou null s'il n'existe pas
*/
public function findByEmail(string $email): ?User
{
$stmt = $this->db->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute([':email' => $email]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ? User::fromArray($row) : null;
}
/**
* Persiste un nouvel utilisateur en base de données.
*
* @param User $user L'utilisateur à créer
*
* @return int L'identifiant généré par la base de données
*/
public function create(User $user): int
{
$stmt = $this->db->prepare('
INSERT INTO users (username, email, password_hash, role, created_at)
VALUES (:username, :email, :password_hash, :role, :created_at)
');
$stmt = $this->db->prepare(
'INSERT INTO users (username, email, password_hash, role, created_at)
VALUES (:username, :email, :password_hash, :role, :created_at)'
);
$stmt->execute([
':username' => $user->getUsername(),
@@ -112,36 +86,18 @@ final class UserRepository implements UserRepositoryInterface
return (int) $this->db->lastInsertId();
}
/**
* Met à jour le hash du mot de passe d'un utilisateur.
*
* @param int $id Identifiant de l'utilisateur
* @param string $newHash Nouveau hash bcrypt
*/
public function updatePassword(int $id, string $newHash): void
{
$stmt = $this->db->prepare('UPDATE users SET password_hash = :password_hash WHERE id = :id');
$stmt->execute([':password_hash' => $newHash, ':id' => $id]);
}
/**
* Met à jour le rôle d'un utilisateur.
*
* @param int $id Identifiant de l'utilisateur
* @param string $role Nouveau rôle : 'user', 'editor' ou 'admin'
*/
public function updateRole(int $id, string $role): void
{
$stmt = $this->db->prepare('UPDATE users SET role = :role WHERE id = :id');
$stmt->execute([':role' => $role, ':id' => $id]);
}
/**
* Supprime un utilisateur de la base de données.
*
* @param int $id Identifiant de l'utilisateur à supprimer
* @return void
*/
public function delete(int $id): void
{
$stmt = $this->db->prepare('DELETE FROM users WHERE id = :id');

View File

@@ -3,78 +3,27 @@ declare(strict_types=1);
namespace App\User;
/**
* Contrat de persistance des utilisateurs.
*
* Découple les services métier de l'implémentation concrète (PDO/SQLite),
* facilitant les mocks dans les tests et un éventuel changement de stockage.
*/
interface UserRepositoryInterface
{
/**
* Retourne tous les utilisateurs triés par date de création.
*
* @return User[]
*/
/** @return User[] */
public function findAll(): array;
/**
* Trouve un utilisateur par son identifiant.
*
* @param int $id Identifiant de l'utilisateur
*
* @return User|null L'utilisateur trouvé, ou null s'il n'existe pas
*/
/** @return User[] */
public function findPage(int $limit, int $offset): array;
public function countAll(): int;
public function findById(int $id): ?User;
/**
* Trouve un utilisateur par son nom d'utilisateur.
*
* @param string $username Nom d'utilisateur normalisé en minuscules
*
* @return User|null L'utilisateur trouvé, ou null s'il n'existe pas
*/
public function findByUsername(string $username): ?User;
/**
* Trouve un utilisateur par son adresse e-mail.
*
* @param string $email Adresse e-mail normalisée en minuscules
*
* @return User|null L'utilisateur trouvé, ou null s'il n'existe pas
*/
public function findByEmail(string $email): ?User;
/**
* Persiste un nouvel utilisateur en base de données.
*
* @param User $user L'utilisateur à créer
*
* @return int L'identifiant généré par la base de données
*/
public function create(User $user): int;
/**
* Met à jour le hash du mot de passe d'un utilisateur.
*
* @param int $id Identifiant de l'utilisateur
* @param string $newHash Nouveau hash bcrypt
*/
public function updatePassword(int $id, string $newHash): void;
/**
* Met à jour le rôle d'un utilisateur.
*
* @param int $id Identifiant de l'utilisateur
* @param string $role Nouveau rôle : 'user', 'editor' ou 'admin'
*/
public function updateRole(int $id, string $role): void;
/**
* Supprime un utilisateur de la base de données.
*
* @param int $id Identifiant de l'utilisateur à supprimer
* @return void
*/
public function delete(int $id): void;
}

View File

@@ -4,9 +4,11 @@ declare(strict_types=1);
namespace App\User;
use App\Shared\Exception\NotFoundException;
use App\Shared\Pagination\PaginatedResult;
use App\User\Exception\DuplicateEmailException;
use App\User\Exception\DuplicateUsernameException;
use App\User\Exception\InvalidRoleException;
use App\User\Exception\RoleAssignmentNotAllowedException;
use App\User\Exception\WeakPasswordException;
final class UserService implements UserServiceInterface
@@ -21,6 +23,23 @@ final class UserService implements UserServiceInterface
return $this->userRepository->findAll();
}
/**
* @return PaginatedResult<User>
*/
public function findPaginated(int $page, int $perPage): PaginatedResult
{
$page = max(1, $page);
$total = $this->userRepository->countAll();
$offset = ($page - 1) * $perPage;
return new PaginatedResult(
$this->userRepository->findPage($perPage, $offset),
$total,
$page,
$perPage,
);
}
public function findById(int $id): ?User
{
return $this->userRepository->findById($id);
@@ -34,7 +53,7 @@ final class UserService implements UserServiceInterface
public function updateRole(int $id, string $role): void
{
$this->assertValidRole($role);
$this->assertRoleCanBeAssigned($role);
$this->requireExistingUser($id);
$this->userRepository->updateRole($id, $role);
}
@@ -45,7 +64,7 @@ final class UserService implements UserServiceInterface
$email = mb_strtolower(trim($email));
$plainPassword = trim($plainPassword);
$this->assertValidRole($role);
$this->assertRoleCanBeAssigned($role);
if ($this->userRepository->findByUsername($username)) {
throw new DuplicateUsernameException($username);
@@ -67,13 +86,15 @@ final class UserService implements UserServiceInterface
return $user;
}
private function assertValidRole(string $role): void
private function assertRoleCanBeAssigned(string $role): void
{
$validRoles = [User::ROLE_USER, User::ROLE_EDITOR, User::ROLE_ADMIN];
if (!in_array($role, $validRoles, true)) {
if (!in_array($role, User::allRoles(), true)) {
throw new InvalidRoleException($role);
}
if (!in_array($role, User::assignableRoles(), true)) {
throw new RoleAssignmentNotAllowedException($role);
}
}
private function requireExistingUser(int $id): User

View File

@@ -3,67 +3,36 @@ declare(strict_types=1);
namespace App\User;
use App\Shared\Pagination\PaginatedResult;
use App\User\Exception\DuplicateEmailException;
use App\User\Exception\DuplicateUsernameException;
use App\User\Exception\InvalidRoleException;
use App\User\Exception\WeakPasswordException;
/**
* Contrat du service de gestion des utilisateurs.
*
* Permet de mocker le service dans les tests unitaires sans dépendre
* de la classe concrète finale UserService.
*/
interface UserServiceInterface
{
/**
* Retourne tous les utilisateurs triés par date de création (ordre croissant).
*
* @return User[]
*/
/** @return User[] */
public function findAll(): array;
/**
* Trouve un utilisateur par son identifiant.
*
* @param int $id Identifiant de l'utilisateur
*
* @return User|null L'utilisateur trouvé, ou null s'il n'existe pas
* @return PaginatedResult<User>
*/
public function findPaginated(int $page, int $perPage): PaginatedResult;
public function findById(int $id): ?User;
/**
* Supprime un utilisateur de la base de données.
*
* @param int $id Identifiant de l'utilisateur à supprimer
*/
public function delete(int $id): void;
/**
* Crée un nouveau compte utilisateur.
*
* @param string $username Nom d'utilisateur souhaité (min. 3 caractères)
* @param string $email Adresse e-mail valide
* @param string $plainPassword Mot de passe en clair (min. 8 caractères)
* @param string $role Rôle attribué : 'user', 'editor' ou 'admin' (défaut : 'user')
*
* @return User L'utilisateur créé (sans mot de passe en clair)
*
* @throws DuplicateUsernameException Si le nom d'utilisateur est déjà pris
* @throws DuplicateEmailException Si l'adresse e-mail est déjà utilisée
* @throws WeakPasswordException Si le mot de passe est trop court
* @throws InvalidRoleException Si le rôle est invalide
* @throws \InvalidArgumentException Si le nom ou l'email ne passent pas la validation
* @throws DuplicateUsernameException
* @throws DuplicateEmailException
* @throws WeakPasswordException
* @throws InvalidRoleException
*/
public function createUser(string $username, string $email, string $plainPassword, string $role = User::ROLE_USER): User;
/**
* Met à jour le rôle d'un utilisateur.
*
* @param int $id Identifiant de l'utilisateur
* @param string $role Nouveau rôle : 'user', 'editor' ou 'admin'
*
* @throws InvalidRoleException Si le rôle est invalide
* @throws InvalidRoleException
*/
public function updateRole(int $id, string $role): void;
}