Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
54 / 54
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
LoginController
100.00% covered (success)
100.00%
54 / 54
100.00% covered (success)
100.00%
4 / 4
6
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 showLoginForm
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 login
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
2
 logout
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3// ╔════════════════════════════════════════════════════════════╗
4// ║ MIT Licence (#Expat) - https://opensource.org/licenses/MIT ║
5// ║ Copyright 2026 Frederic Poeydomenge <dyno@phexium.com>     ║
6// ╚════════════════════════════════════════════════════════════╝
7
8declare(strict_types=1);
9
10namespace AppDemo\User\Application\Http;
11
12use AppDemo\Shared\Application\AbstractHttpController;
13use AppDemo\Shared\Domain\Interface\SessionServiceInterface;
14use AppDemo\User\Application\Command\AuthenticateUserCommand;
15use AppDemo\User\Application\Command\LogoutUserCommand;
16use AppDemo\User\Application\Query\LoginQuery;
17use AppDemo\User\Presentation\LoginPresenter;
18use Exception;
19use Phexium\Application\ControllerInterface;
20use Phexium\Domain\Id\IdInterface;
21use Phexium\Plugin\CommandBus\Port\CommandBusInterface;
22use Phexium\Plugin\Logger\Port\LoggerInterface;
23use Phexium\Plugin\QueryBus\Port\QueryBusInterface;
24use Phexium\Presentation\ResponseBuilderInterface;
25use Psr\Http\Message\ResponseInterface;
26use Psr\Http\Message\ServerRequestInterface;
27use Twig\Environment;
28
29final readonly class LoginController extends AbstractHttpController implements ControllerInterface
30{
31    public function __construct(
32        private CommandBusInterface $commandBus,
33        private SessionServiceInterface $sessionService,
34        private LoginPresenter $presenter,
35        private QueryBusInterface $queryBus,
36        private Environment $twig,
37        private ResponseBuilderInterface $responseBuilder,
38        private LoggerInterface $logger,
39    ) {}
40
41    public function showLoginForm(
42        ServerRequestInterface $request, // NOSONAR - Slim Framework PSR-15 contract
43        ResponseInterface $response
44    ): ResponseInterface {
45        $query = new LoginQuery('');
46
47        $queryResponse = $this->queryBus->dispatch($query);
48
49        $viewModel = $this->presenter->present($queryResponse)->getViewModel();
50
51        $html = $this->twig->render('Login.html.twig', (array) $viewModel);
52
53        return $this->responseBuilder
54            ->withResponse($response)
55            ->withStatus(200)
56            ->withHtml($html)
57            ->build()
58        ;
59    }
60
61    public function login(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
62    {
63        $this->logger->info('LoginController: Processing request');
64
65        $formValues = $this->extractFormData($request, [
66            'email' => '',
67            'password' => '',
68        ]);
69
70        try {
71            $command = new AuthenticateUserCommand($formValues['email'], $formValues['password']);
72
73            $this->commandBus->dispatch($command);
74
75            $this->sessionService->addFlashMessage('success', 'Login successful!');
76
77            return $this->responseBuilder
78                ->withResponse($response)
79                ->withStatus(302)
80                ->withHeader('Location', '/')
81                ->build()
82            ;
83        } catch (Exception $exception) {
84            $this->logger->error('LoginController: Authentication failed', [
85                'email' => $formValues['email'],
86                'error' => $exception->getMessage(),
87            ]);
88
89            $this->sessionService->addFlashMessage('error', 'Invalid credentials, please try again.');
90
91            return $this->responseBuilder
92                ->withResponse($response)
93                ->withStatus(303)
94                ->withHeader('Location', '/auth/login')
95                ->build()
96            ;
97        }
98    }
99
100    public function logout(
101        ServerRequestInterface $request, // NOSONAR - Slim Framework PSR-15 contract
102        ResponseInterface $response
103    ): ResponseInterface {
104        $userId = $this->sessionService->getUserId();
105
106        if (!$userId instanceof IdInterface) {
107            $this->logger->warning('LoginController: Logout attempted without active session');
108
109            return $this->responseBuilder
110                ->withResponse($response)
111                ->withStatus(302)
112                ->withHeader('Location', '/')
113                ->build()
114            ;
115        }
116
117        $this->commandBus->dispatch(new LogoutUserCommand($userId));
118
119        $this->sessionService->addFlashMessage('success', 'You have been logged out successfully.');
120
121        return $this->responseBuilder
122            ->withResponse($response)
123            ->withStatus(302)
124            ->withHeader('Location', '/')
125            ->build()
126        ;
127    }
128}