Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
15 / 15
CRAP
100.00% covered (success)
100.00%
1 / 1
InMemoryLoanRepository
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
15 / 15
22
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
 findAll
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 findBy
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 findOneBy
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 findById
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getById
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 exists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 save
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 delete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 deleteById
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findByUserId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findAllWithDetails
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 findByUserIdWithBookDetails
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 reset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createLoanWithDetails
100.00% covered (success)
100.00%
13 / 13
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\Loan\Infrastructure;
11
12use AppDemo\Library\Domain\Book;
13use AppDemo\Library\Domain\BookRepository;
14use AppDemo\Loan\Domain\Loan;
15use AppDemo\Loan\Domain\LoanRepository;
16use AppDemo\Loan\Domain\LoansCollection;
17use AppDemo\Loan\Domain\ReadModel\LoanWithDetails;
18use AppDemo\Loan\Domain\Specification\UserIdSpecification;
19use AppDemo\Loan\Infrastructure\Trait\LoanRepositoryMappingTrait;
20use AppDemo\User\Domain\User;
21use AppDemo\User\Domain\UserRepository;
22use InvalidArgumentException;
23use Override;
24use Phexium\Domain\Constant\DateTimeFormat;
25use Phexium\Domain\Id\IdInterface;
26use Phexium\Domain\Specification\SpecificationInterface;
27use Phexium\Plugin\Clock\Port\ClockInterface;
28use Phexium\Plugin\IdGenerator\Port\IdGeneratorInterface;
29use Phexium\Plugin\SqlDriver\Adapter\InMemoryDriver;
30
31final readonly class InMemoryLoanRepository implements LoanRepository
32{
33    use LoanRepositoryMappingTrait;
34
35    private const string TABLE = 'loan';
36
37    public function __construct(
38        private InMemoryDriver $driver,
39        private IdGeneratorInterface $idGenerator,
40        private BookRepository $bookRepository,
41        private UserRepository $userRepository,
42        private ClockInterface $clock,
43    ) {}
44
45    #[Override]
46    public function findAll(): LoansCollection
47    {
48        $rows = $this->driver->findAll(self::TABLE);
49
50        return LoansCollection::fromMap($rows, fn (array $row): Loan => $this->rowToLoan($row));
51    }
52
53    #[Override]
54    public function findBy(SpecificationInterface $specification, ?array $orderBy = null, ?int $offset = null, ?int $limit = null): LoansCollection
55    {
56        $rows = $this->driver->findBy(self::TABLE, $specification, $orderBy, $offset, $limit);
57
58        return LoansCollection::fromMap($rows, fn (array $row): Loan => $this->rowToLoan($row));
59    }
60
61    #[Override]
62    public function findOneBy(SpecificationInterface $specification): ?Loan
63    {
64        $row = $this->driver->findOneBy(self::TABLE, $specification);
65
66        return $row !== null ? $this->rowToLoan($row) : null;
67    }
68
69    #[Override]
70    public function findById(IdInterface $id): ?Loan
71    {
72        $row = $this->driver->findById(self::TABLE, $id);
73
74        return $row !== null ? $this->rowToLoan($row) : null;
75    }
76
77    #[Override]
78    public function getById(IdInterface $id): Loan
79    {
80        $loan = $this->findById($id);
81
82        if (!$loan instanceof Loan) {
83            throw new InvalidArgumentException('Loan with ID='.$id->getValue().' not found');
84        }
85
86        return $loan;
87    }
88
89    #[Override]
90    public function exists(IdInterface $id): bool
91    {
92        return $this->driver->exists(self::TABLE, $id);
93    }
94
95    #[Override]
96    public function save(Loan $loan): void
97    {
98        $this->driver->save(self::TABLE, $this->loanToRow($loan));
99    }
100
101    #[Override]
102    public function delete(Loan $loan): int
103    {
104        return $this->deleteById($loan->getId());
105    }
106
107    #[Override]
108    public function deleteById(IdInterface $id): int
109    {
110        return $this->driver->deleteById(self::TABLE, $id);
111    }
112
113    #[Override]
114    public function findByUserId(IdInterface $userId): LoansCollection
115    {
116        return $this->findBy(new UserIdSpecification($userId), ['borrowed_at' => 'DESC']);
117    }
118
119    #[Override]
120    public function findAllWithDetails(): array
121    {
122        $loans = $this->findAll();
123        $loanItems = [];
124
125        foreach ($loans as $loan) {
126            $user = $this->userRepository->findById($loan->getUserId());
127            $userEmail = $user instanceof User ? $user->getEmail()->getValue() : 'Unknown';
128
129            $loanItems[] = $this->createLoanWithDetails($loan, $userEmail);
130        }
131
132        return $loanItems;
133    }
134
135    #[Override]
136    public function findByUserIdWithBookDetails(IdInterface $userId): array
137    {
138        $loans = $this->findByUserId($userId);
139        $loanItems = [];
140
141        foreach ($loans as $loan) {
142            $loanItems[] = $this->createLoanWithDetails($loan, '');
143        }
144
145        return $loanItems;
146    }
147
148    public function reset(): void
149    {
150        $this->driver->reset(self::TABLE);
151    }
152
153    private function createLoanWithDetails(Loan $loan, string $userEmail): LoanWithDetails
154    {
155        $book = $this->bookRepository->findById($loan->getBookId());
156
157        return new LoanWithDetails(
158            loanId: (string) $loan->getId()->getValue(),
159            userId: (string) $loan->getUserId()->getValue(),
160            userEmail: $userEmail,
161            bookId: (string) $loan->getBookId()->getValue(),
162            bookTitle: $book instanceof Book ? $book->getTitle()->getValue() : 'Unknown',
163            borrowedAt: $loan->getBorrowedAt()->format(DateTimeFormat::SQL_DATETIME),
164            dueAt: $loan->getDueAt()->format(DateTimeFormat::SQL_DATETIME),
165            returnedAt: $loan->getReturnedAt()?->format(DateTimeFormat::SQL_DATETIME),
166            status: $loan->getStatus()->value,
167            isOverdue: $loan->isOverdue($this->clock->now()),
168        );
169    }
170}