Working state
This commit is contained in:
17
src/User/Exception/RoleAssignmentNotAllowedException.php
Normal file
17
src/User/Exception/RoleAssignmentNotAllowedException.php
Normal 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())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user