Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.59% covered (success)
94.59%
35 / 37
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
BorrowBookController
94.59% covered (success)
94.59%
35 / 37
50.00% covered (danger)
50.00%
2 / 4
8.01
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
 showBorrowForm
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 borrowBook
95.65% covered (success)
95.65%
22 / 23
0.00% covered (danger)
0.00%
0 / 1
4
 renderFormResponse
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
2.00
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\Loan\Presentation\Http;
11
12use AppDemo\Loan\Application\Command\BorrowBookCommand;
13use AppDemo\Loan\Application\Query\BorrowBookQuery;
14use AppDemo\Loan\Application\Query\BorrowBookResponse;
15use AppDemo\Loan\Domain\Exception\BookNotAvailableException;
16use AppDemo\Loan\Domain\LoanPeriod;
17use AppDemo\Loan\Presentation\BorrowBookHtmlPresenter;
18use AppDemo\Loan\Presentation\Request\BorrowBookRequest;
19use AppDemo\Shared\Domain\Interface\SessionServiceInterface;
20use AppDemo\Shared\Presentation\AbstractHttpController;
21use InvalidArgumentException;
22use Phexium\Domain\Id\IdInterface;
23use Phexium\Plugin\CommandBus\Port\CommandBusInterface;
24use Phexium\Plugin\IdGenerator\Port\IdGeneratorInterface;
25use Phexium\Plugin\Logger\Port\LoggerInterface;
26use Phexium\Plugin\QueryBus\Port\Exception\UnexpectedQueryResponseException;
27use Phexium\Plugin\QueryBus\Port\QueryBusInterface;
28use Phexium\Presentation\ControllerInterface;
29use Phexium\Presentation\ResponseBuilderInterface;
30use Psr\Http\Message\ResponseInterface;
31use Psr\Http\Message\ServerRequestInterface;
32use RuntimeException;
33use Twig\Environment;
34
35final readonly class BorrowBookController extends AbstractHttpController implements ControllerInterface
36{
37    public function __construct(
38        private CommandBusInterface $commandBus,
39        private QueryBusInterface $queryBus,
40        private SessionServiceInterface $sessionService,
41        private BorrowBookHtmlPresenter $presenter,
42        private Environment $twig,
43        private ResponseBuilderInterface $responseBuilder,
44        private IdGeneratorInterface $idGenerator,
45        private LoggerInterface $logger,
46    ) {}
47
48    public function showBorrowForm(
49        ServerRequestInterface $request, // NOSONAR - Slim Framework PSR-15 contract
50        ResponseInterface $response
51    ): ResponseInterface {
52        return $this->renderFormResponse($response);
53    }
54
55    public function borrowBook(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
56    {
57        $this->logger->info('BorrowBookController: Processing request');
58
59        $requestDto = BorrowBookRequest::fromHttpRequest($request);
60        $errors = $requestDto->validate();
61
62        if ($errors === []) {
63            try {
64                $loanId = $this->idGenerator->generate();
65                $userId = $this->getUserId($request);
66
67                if (!$userId instanceof IdInterface) {
68                    throw new RuntimeException('No authenticated user found in session.');
69                }
70                $bookId = $this->idGenerator->from($requestDto->bookId);
71                $loanPeriod = LoanPeriod::fromInt((int) $requestDto->loanPeriod);
72
73                $command = new BorrowBookCommand($loanId, $userId, $bookId, $loanPeriod);
74
75                $this->commandBus->dispatch($command);
76
77                $this->sessionService->addFlashMessage('success', 'Book borrowed successfully!');
78
79                return $this->responseBuilder
80                    ->withResponse($response)
81                    ->withStatus(302)
82                    ->withHeader('Location', '/loans/my')
83                    ->build()
84                ;
85            } catch (BookNotAvailableException|InvalidArgumentException $e) {
86                $errors['book_id'] = $e->getMessage();
87            }
88        }
89
90        $this->logger->warning('BorrowBookController: Validation errors', ['errors' => $errors]);
91
92        return $this->renderFormResponse($response, $requestDto->toArray(), $errors);
93    }
94
95    private function renderFormResponse(
96        ResponseInterface $response,
97        array $formValues = [],
98        array $errors = []
99    ): ResponseInterface {
100        $query = new BorrowBookQuery($formValues, $errors);
101        $queryResponse = $this->queryBus->dispatch($query);
102
103        if (!$queryResponse instanceof BorrowBookResponse) {
104            throw UnexpectedQueryResponseException::withExpectedType(BorrowBookResponse::class, $queryResponse);
105        }
106
107        $viewModel = $this->presenter->present($queryResponse)->getViewModel();
108        $html = $this->twig->render('BorrowBook.html.twig', (array) $viewModel);
109
110        return $this->responseBuilder
111            ->withResponse($response)
112            ->withStatus(200)
113            ->withHtml($html)
114            ->build()
115        ;
116    }
117}