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