Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
51 / 51
100.00% covered (success)
100.00%
14 / 14
CRAP
100.00% covered (success)
100.00%
1 / 1
MysqlLoanRepository
100.00% covered (success)
100.00%
51 / 51
100.00% covered (success)
100.00%
14 / 14
18
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%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 findByUserIdWithBookDetails
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 rowToLoanWithDetails
100.00% covered (success)
100.00%
15 / 15
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\Loan\Domain\Loan;
13use AppDemo\Loan\Domain\LoanRepository;
14use AppDemo\Loan\Domain\LoansCollection;
15use AppDemo\Loan\Domain\ReadModel\LoanWithDetails;
16use AppDemo\Loan\Domain\Specification\UserIdSpecification;
17use AppDemo\Loan\Infrastructure\Trait\LoanRepositoryMappingTrait;
18use DateTimeImmutable;
19use InvalidArgumentException;
20use Override;
21use PDO;
22use Phexium\Domain\Constant\DateTimeFormat;
23use Phexium\Domain\Id\IdInterface;
24use Phexium\Domain\Specification\SpecificationInterface;
25use Phexium\Plugin\IdGenerator\Port\IdGeneratorInterface;
26use Phexium\Plugin\SqlDriver\Adapter\MysqlDriver;
27
28final readonly class MysqlLoanRepository implements LoanRepository
29{
30    use LoanRepositoryMappingTrait;
31
32    private const string TABLE = 'loan';
33
34    public function __construct(
35        private MysqlDriver $driver,
36        private PDO $pdo,
37        private IdGeneratorInterface $idGenerator,
38    ) {}
39
40    #[Override]
41    public function findAll(): LoansCollection
42    {
43        $rows = $this->driver->findAll(self::TABLE);
44
45        return LoansCollection::fromMap($rows, fn (array $row): Loan => $this->rowToLoan($row));
46    }
47
48    #[Override]
49    public function findBy(SpecificationInterface $specification, ?array $orderBy = null, ?int $offset = null, ?int $limit = null): LoansCollection
50    {
51        $rows = $this->driver->findBy(self::TABLE, $specification, $orderBy, $offset, $limit);
52
53        return LoansCollection::fromMap($rows, fn (array $row): Loan => $this->rowToLoan($row));
54    }
55
56    #[Override]
57    public function findOneBy(SpecificationInterface $specification): ?Loan
58    {
59        $row = $this->driver->findOneBy(self::TABLE, $specification);
60
61        return $row !== null ? $this->rowToLoan($row) : null;
62    }
63
64    #[Override]
65    public function findById(IdInterface $id): ?Loan
66    {
67        $row = $this->driver->findById(self::TABLE, $id);
68
69        return $row !== null ? $this->rowToLoan($row) : null;
70    }
71
72    #[Override]
73    public function getById(IdInterface $id): Loan
74    {
75        $loan = $this->findById($id);
76
77        if (!$loan instanceof Loan) {
78            throw new InvalidArgumentException('Loan with ID='.$id->getValue().' not found');
79        }
80
81        return $loan;
82    }
83
84    #[Override]
85    public function exists(IdInterface $id): bool
86    {
87        return $this->driver->exists(self::TABLE, $id);
88    }
89
90    #[Override]
91    public function save(Loan $loan): void
92    {
93        $this->driver->save(self::TABLE, $this->loanToRow($loan));
94    }
95
96    #[Override]
97    public function delete(Loan $loan): int
98    {
99        return $this->deleteById($loan->getId());
100    }
101
102    #[Override]
103    public function deleteById(IdInterface $id): int
104    {
105        return $this->driver->deleteById(self::TABLE, $id);
106    }
107
108    #[Override]
109    public function findByUserId(IdInterface $userId): LoansCollection
110    {
111        return $this->findBy(new UserIdSpecification($userId), ['borrowed_at' => 'DESC']);
112    }
113
114    #[Override]
115    public function findAllWithDetails(): array
116    {
117        $sql = '
118            SELECT
119                L.id as loan_id,
120                L.user_id,
121                L.book_id,
122                L.borrowed_at,
123                L.due_at,
124                L.returned_at,
125                L.status as loan_status,
126                B.title as book_title,
127                U.email as user_email
128            FROM `loan` L
129                LEFT JOIN `book` B ON L.book_id = B.id
130                LEFT JOIN `user` U ON L.user_id = U.id
131        ';
132
133        $stmt = $this->pdo->prepare($sql);
134        $stmt->execute();
135
136        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
137
138        return array_map(
139            $this->rowToLoanWithDetails(...),
140            $rows
141        );
142    }
143
144    #[Override]
145    public function findByUserIdWithBookDetails(IdInterface $userId): array
146    {
147        $sql = '
148            SELECT
149                L.id as loan_id,
150                L.user_id,
151                L.book_id,
152                L.borrowed_at,
153                L.due_at,
154                L.returned_at,
155                L.status as loan_status,
156                B.title as book_title,
157                U.email as user_email
158            FROM `loan` L
159                LEFT JOIN `book` B ON L.book_id = B.id
160                LEFT JOIN `user` U ON L.user_id = U.id
161            WHERE L.user_id = :user_id
162            ORDER BY L.borrowed_at DESC
163        ';
164
165        $stmt = $this->pdo->prepare($sql);
166        $stmt->execute(['user_id' => $userId->getValue()]);
167
168        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
169
170        return array_map(
171            $this->rowToLoanWithDetails(...),
172            $rows
173        );
174    }
175
176    private function rowToLoanWithDetails(array $row): LoanWithDetails
177    {
178        $dueAt = DateTimeImmutable::createFromFormat(DateTimeFormat::SQL_DATETIME, $row['due_at']);
179        $now = new DateTimeImmutable();
180        $isOverdue = $dueAt < $now && $row['returned_at'] === null;
181
182        return new LoanWithDetails(
183            loanId: $row['loan_id'],
184            userId: $row['user_id'],
185            userEmail: $row['user_email'] ?? 'Unknown',
186            bookId: $row['book_id'],
187            bookTitle: $row['book_title'] ?? 'Unknown',
188            borrowedAt: $row['borrowed_at'],
189            dueAt: $row['due_at'],
190            returnedAt: $row['returned_at'],
191            status: $row['loan_status'],
192            isOverdue: $isOverdue,
193        );
194    }
195}