From d832d598fe42af1c18c3b4ecfb72e54f272862e8 Mon Sep 17 00:00:00 2001 From: julien Date: Mon, 16 Mar 2026 14:21:31 +0100 Subject: [PATCH] Refatoring : Working state --- config/container.php | 8 +- database/.provision.lock | 0 docs/ARCHITECTURE.md | 8 +- .../Application/UserApplicationService.php | 104 +++++++++++ src/User/Domain/RolePolicy.php | 42 +++++ src/User/Http/UserController.php | 168 ++++++++++++++++++ src/User/Infrastructure/PdoUserRepository.php | 110 ++++++++++++ src/User/UserController.php | 155 +--------------- src/User/UserRepository.php | 104 +---------- src/User/UserService.php | 108 +---------- 10 files changed, 449 insertions(+), 358 deletions(-) delete mode 100644 database/.provision.lock create mode 100644 src/User/Application/UserApplicationService.php create mode 100644 src/User/Domain/RolePolicy.php create mode 100644 src/User/Http/UserController.php create mode 100644 src/User/Infrastructure/PdoUserRepository.php diff --git a/config/container.php b/config/container.php index 4b05e68..98d1be4 100644 --- a/config/container.php +++ b/config/container.php @@ -50,9 +50,9 @@ use App\Shared\Http\SessionManager; use App\Shared\Http\SessionManagerInterface; use App\Shared\Mail\MailService; use App\Shared\Mail\MailServiceInterface; -use App\User\UserRepository; +use App\User\Application\UserApplicationService; +use App\User\Infrastructure\PdoUserRepository; use App\User\UserRepositoryInterface; -use App\User\UserService; use App\User\UserServiceInterface; use Monolog\Handler\StreamHandler; @@ -70,12 +70,12 @@ return [ AuthServiceInterface::class => autowire(AuthService::class), PostServiceInterface::class => autowire(PostApplicationService::class), - UserServiceInterface::class => autowire(UserService::class), + UserServiceInterface::class => autowire(UserApplicationService::class), CategoryServiceInterface::class => autowire(CategoryApplicationService::class), CategoryRepositoryInterface::class => autowire(PdoCategoryRepository::class), MediaRepositoryInterface::class => autowire(MediaRepository::class), PostRepositoryInterface::class => autowire(PdoPostRepository::class), - UserRepositoryInterface::class => autowire(UserRepository::class), + UserRepositoryInterface::class => autowire(PdoUserRepository::class), LoginAttemptRepositoryInterface::class => autowire(LoginAttemptRepository::class), PasswordResetRepositoryInterface::class => autowire(PasswordResetRepository::class), PasswordResetServiceInterface::class => autowire(PasswordResetService::class), diff --git a/database/.provision.lock b/database/.provision.lock deleted file mode 100644 index e69de29..0000000 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 941568c..aa03a8a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,8 +1,8 @@ # Architecture -> **Refactor DDD légère — lot 1** +> **Refactor DDD légère — lots 1 et 2** > -> `Post/` et `Category/` introduisent maintenant une organisation verticale +> `Post/`, `Category/` et `User/` introduisent maintenant une organisation verticale > `Application / Infrastructure / Http / Domain` pour alléger la lecture et préparer > un découpage plus fin par cas d'usage. Les classes historiques à la racine du domaine > sont conservées comme **ponts de compatibilité** afin de préserver les routes, le conteneur @@ -83,8 +83,8 @@ final class PostService | Interface | Implémentation | Domaine | |------------------------------------|---------------------------|------------| -| `UserRepositoryInterface` | `UserRepository` | `User/` | -| `UserServiceInterface` | `UserService` | `User/` | +| `UserRepositoryInterface` | `PdoUserRepository` | `User/` | +| `UserServiceInterface` | `UserApplicationService` | `User/` | | `LoginAttemptRepositoryInterface` | `LoginAttemptRepository` | `Auth/` | | `PasswordResetRepositoryInterface` | `PasswordResetRepository` | `Auth/` | | `PasswordResetServiceInterface` | `PasswordResetService` | `Auth/` | diff --git a/src/User/Application/UserApplicationService.php b/src/User/Application/UserApplicationService.php new file mode 100644 index 0000000..007668f --- /dev/null +++ b/src/User/Application/UserApplicationService.php @@ -0,0 +1,104 @@ +rolePolicy = $rolePolicy ?? new RolePolicy(); + } + + /** @return User[] */ + public function findAll(): array + { + return $this->userRepository->findAll(); + } + + /** @return PaginatedResult */ + 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); + } + + public function delete(int $id): void + { + $this->requireExistingUser($id); + $this->userRepository->delete($id); + } + + public function updateRole(int $id, string $role): void + { + $this->rolePolicy->assertAssignable($role); + $this->requireExistingUser($id); + $this->userRepository->updateRole($id, $role); + } + + public function createUser(string $username, string $email, string $plainPassword, string $role = User::ROLE_USER): User + { + $username = mb_strtolower(trim($username)); + $email = mb_strtolower(trim($email)); + $plainPassword = trim($plainPassword); + + $this->rolePolicy->assertAssignable($role); + + if ($this->userRepository->findByUsername($username)) { + throw new DuplicateUsernameException($username); + } + + if ($this->userRepository->findByEmail($email)) { + throw new DuplicateEmailException($email); + } + + if (mb_strlen($plainPassword) < 8) { + throw new WeakPasswordException(); + } + + $passwordHash = password_hash($plainPassword, PASSWORD_BCRYPT, ['cost' => 12]); + $user = new User(0, $username, $email, $passwordHash, $role); + + $this->userRepository->create($user); + + return $user; + } + + private function requireExistingUser(int $id): User + { + $user = $this->userRepository->findById($id); + + if ($user === null) { + throw new NotFoundException('Utilisateur', $id); + } + + return $user; + } +} diff --git a/src/User/Domain/RolePolicy.php b/src/User/Domain/RolePolicy.php new file mode 100644 index 0000000..2797748 --- /dev/null +++ b/src/User/Domain/RolePolicy.php @@ -0,0 +1,42 @@ +allRoles(), true)) { + throw new InvalidRoleException($role); + } + + if (!in_array($role, $this->assignableRoles(), true)) { + throw new RoleAssignmentNotAllowedException($role); + } + } +} diff --git a/src/User/Http/UserController.php b/src/User/Http/UserController.php new file mode 100644 index 0000000..8c64ada --- /dev/null +++ b/src/User/Http/UserController.php @@ -0,0 +1,168 @@ +rolePolicy = $rolePolicy ?? new RolePolicy(); + } + + 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' => $paginated->getItems(), + 'pagination' => PaginationPresenter::fromRequest($req, $paginated), + 'currentUserId' => $this->sessionManager->getUserId(), + 'assignableRoles' => $this->rolePolicy->assignableRoles(), + 'error' => $this->flash->get('user_error'), + 'success' => $this->flash->get('user_success'), + ]); + } + + public function showCreate(Request $req, Response $res): Response + { + return $this->view->render($res, 'admin/users/form.twig', [ + 'assignableRoles' => $this->rolePolicy->assignableRoles(), + 'error' => $this->flash->get('user_error'), + ]); + } + + public function create(Request $req, Response $res): Response + { + $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'] ?? '')); + $rawRole = trim((string) ($data['role'] ?? '')); + $role = in_array($rawRole, $this->rolePolicy->assignableRoles(), true) ? $rawRole : '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"); + } catch (DuplicateEmailException) { + $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 | RoleAssignmentNotAllowedException $e) { + $this->flash->set('user_error', $e->getMessage()); + } catch (\Throwable) { + $this->flash->set('user_error', "Une erreur inattendue s'est produite"); + } + + return $res->withHeader('Location', '/admin/users/create')->withStatus(302); + } + + /** @param array $args */ + public function updateRole(Request $req, Response $res, array $args): Response + { + $id = (int) $args['id']; + $user = $this->userService->findById($id); + + 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é depuis l\'interface'); + + return $res->withHeader('Location', '/admin/users')->withStatus(302); + } + + $body = (array) $req->getParsedBody(); + $rawRole = trim((string) ($body['role'] ?? '')); + $role = in_array($rawRole, $this->rolePolicy->assignableRoles(), true) ? $rawRole : null; + + if ($role === null) { + $this->flash->set('user_error', 'Rôle invalide'); + + return $res->withHeader('Location', '/admin/users')->withStatus(302); + } + + 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); + } + + /** @param array $args */ + public function delete(Request $req, Response $res, array $args): Response + { + $id = (int) $args['id']; + $user = $this->userService->findById($id); + + 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); + } + + $this->userService->delete($id); + $this->flash->set('user_success', "L'utilisateur « {$user->getUsername()} » a été supprimé avec succès"); + + return $res->withHeader('Location', '/admin/users')->withStatus(302); + } +} diff --git a/src/User/Infrastructure/PdoUserRepository.php b/src/User/Infrastructure/PdoUserRepository.php new file mode 100644 index 0000000..0ba9c0a --- /dev/null +++ b/src/User/Infrastructure/PdoUserRepository.php @@ -0,0 +1,110 @@ +db->query('SELECT * FROM users ORDER BY created_at ASC'); + if ($stmt === false) { + throw new \RuntimeException('La requête SELECT sur users a échoué.'); + } + + return array_map(fn ($row) => User::fromArray($row), $stmt->fetchAll(PDO::FETCH_ASSOC)); + } + + /** @return User[] */ + 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); + } + + 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; + } + + 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; + } + + 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; + } + + 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->execute([ + ':username' => $user->getUsername(), + ':email' => $user->getEmail(), + ':password_hash' => $user->getPasswordHash(), + ':role' => $user->getRole(), + ':created_at' => date('Y-m-d H:i:s'), + ]); + + return (int) $this->db->lastInsertId(); + } + + 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]); + } + + 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]); + } + + public function delete(int $id): void + { + $stmt = $this->db->prepare('DELETE FROM users WHERE id = :id'); + $stmt->execute([':id' => $id]); + } +} diff --git a/src/User/UserController.php b/src/User/UserController.php index e8e07f6..2a68ed5 100644 --- a/src/User/UserController.php +++ b/src/User/UserController.php @@ -3,155 +3,10 @@ declare(strict_types=1); 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; - -final class UserController +/** + * Pont de compatibilité : le contrôleur HTTP principal vit désormais dans + * App\User\Http\UserController. + */ +final class UserController extends Http\UserController { - private const PER_PAGE = 15; - - public function __construct( - private readonly Twig $view, - private readonly UserServiceInterface $userService, - private readonly FlashServiceInterface $flash, - private readonly SessionManagerInterface $sessionManager, - ) { - } - - 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' => $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'), - ]); - } - - 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'), - ]); - } - - public function create(Request $req, Response $res): Response - { - $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'] ?? '')); - $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"); - } catch (DuplicateEmailException) { - $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|RoleAssignmentNotAllowedException $e) { - $this->flash->set('user_error', $e->getMessage()); - } catch (\Throwable) { - $this->flash->set('user_error', "Une erreur inattendue s'est produite"); - } - - return $res->withHeader('Location', '/admin/users/create')->withStatus(302); - } - - /** - * @param array $args - */ - public function updateRole(Request $req, Response $res, array $args): Response - { - $id = (int) $args['id']; - $user = $this->userService->findById($id); - - 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é depuis l\'interface'); - return $res->withHeader('Location', '/admin/users')->withStatus(302); - } - - $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); - } - - 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); - } - - /** - * @param array $args - */ - public function delete(Request $req, Response $res, array $args): Response - { - $id = (int) $args['id']; - $user = $this->userService->findById($id); - - 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); - } - - $this->userService->delete($id); - $this->flash->set('user_success', "L'utilisateur « {$user->getUsername()} » a été supprimé avec succès"); - - return $res->withHeader('Location', '/admin/users')->withStatus(302); - } } diff --git a/src/User/UserRepository.php b/src/User/UserRepository.php index 51c1364..5bd6f2a 100644 --- a/src/User/UserRepository.php +++ b/src/User/UserRepository.php @@ -3,104 +3,12 @@ declare(strict_types=1); namespace App\User; -use PDO; +use App\User\Infrastructure\PdoUserRepository; -final class UserRepository implements UserRepositoryInterface +/** + * Pont de compatibilité : l'implémentation PDO principale vit désormais dans + * App\User\Infrastructure\PdoUserRepository. + */ +final class UserRepository extends PdoUserRepository implements UserRepositoryInterface { - public function __construct(private readonly PDO $db) - { - } - - 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é.'); - } - - 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); - } - - 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; - } - - 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; - } - - 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; - } - - 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->execute([ - ':username' => $user->getUsername(), - ':email' => $user->getEmail(), - ':password_hash' => $user->getPasswordHash(), - ':role' => $user->getRole(), - ':created_at' => date('Y-m-d H:i:s'), - ]); - - return (int) $this->db->lastInsertId(); - } - - 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]); - } - - 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]); - } - - public function delete(int $id): void - { - $stmt = $this->db->prepare('DELETE FROM users WHERE id = :id'); - $stmt->execute([':id' => $id]); - } } diff --git a/src/User/UserService.php b/src/User/UserService.php index 030bd17..b32c301 100644 --- a/src/User/UserService.php +++ b/src/User/UserService.php @@ -3,108 +3,12 @@ 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; +use App\User\Application\UserApplicationService; -final class UserService implements UserServiceInterface +/** + * Pont de compatibilité : l'implémentation métier principale vit désormais dans + * App\User\Application\UserApplicationService. + */ +final class UserService extends UserApplicationService implements UserServiceInterface { - public function __construct( - private readonly UserRepositoryInterface $userRepository, - ) { - } - - public function findAll(): array - { - return $this->userRepository->findAll(); - } - - /** - * @return PaginatedResult - */ - 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); - } - - public function delete(int $id): void - { - $this->requireExistingUser($id); - $this->userRepository->delete($id); - } - - public function updateRole(int $id, string $role): void - { - $this->assertRoleCanBeAssigned($role); - $this->requireExistingUser($id); - $this->userRepository->updateRole($id, $role); - } - - public function createUser(string $username, string $email, string $plainPassword, string $role = User::ROLE_USER): User - { - $username = mb_strtolower(trim($username)); - $email = mb_strtolower(trim($email)); - $plainPassword = trim($plainPassword); - - $this->assertRoleCanBeAssigned($role); - - if ($this->userRepository->findByUsername($username)) { - throw new DuplicateUsernameException($username); - } - - if ($this->userRepository->findByEmail($email)) { - throw new DuplicateEmailException($email); - } - - if (mb_strlen($plainPassword) < 8) { - throw new WeakPasswordException(); - } - - $passwordHash = password_hash($plainPassword, PASSWORD_BCRYPT, ['cost' => 12]); - $user = new User(0, $username, $email, $passwordHash, $role); - - $this->userRepository->create($user); - - return $user; - } - - private function assertRoleCanBeAssigned(string $role): void - { - 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 - { - $user = $this->userRepository->findById($id); - - if ($user === null) { - throw new NotFoundException('Utilisateur', $id); - } - - return $user; - } }