Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
55 / 55
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
AbstractSqlLoanRepository
100.00% covered (success)
100.00%
55 / 55
100.00% covered (success)
100.00%
5 / 5
8
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
 findAllWithDetails
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
3
 countAllWithDetails
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 findByUserIdWithBookDetails
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 quoteTableName
n/a
0 / 0
n/a
0 / 0
0
 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\ReadModel\LoanWithDetails;
13use AppDemo\Loan\Infrastructure\Mapper\LoanMapper;
14use DateTimeImmutable;
15use Override;
16use PDO;
17use Phexium\Domain\Constant\DateTimeFormat;
18use Phexium\Domain\Id\IdInterface;
19use Phexium\Plugin\SqlDriver\Port\SqlDriverInterface;
20
21abstract class AbstractSqlLoanRepository extends AbstractLoanRepository
22{
23    public function __construct(
24        SqlDriverInterface $driver,
25        protected readonly PDO $pdo,
26        LoanMapper $mapper,
27    ) {
28        parent::__construct($driver, $mapper);
29    }
30
31    #[Override]
32    public function findAllWithDetails(?int $offset = null, ?int $limit = null): array
33    {
34        $loan = $this->quoteTableName('loan');
35        $book = $this->quoteTableName('book');
36        $user = $this->quoteTableName('user');
37
38        $sql = "
39            SELECT
40                L.id as loan_id,
41                L.user_id,
42                L.book_id,
43                L.borrowed_at,
44                L.due_at,
45                L.returned_at,
46                L.status as loan_status,
47                B.title as book_title,
48                U.email as user_email
49            FROM {$loan} L
50                LEFT JOIN {$book} B ON L.book_id = B.id
51                LEFT JOIN {$user} U ON L.user_id = U.id
52        ";
53
54        if ($limit !== null) {
55            $sql .= ' LIMIT '.$limit;
56        }
57
58        if ($offset !== null) {
59            $sql .= ' OFFSET '.$offset;
60        }
61
62        $stmt = $this->pdo->prepare($sql);
63        $stmt->execute();
64
65        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
66
67        return array_map(
68            $this->rowToLoanWithDetails(...),
69            $rows
70        );
71    }
72
73    #[Override]
74    public function countAllWithDetails(): int
75    {
76        $loan = $this->quoteTableName('loan');
77
78        $sql = sprintf('SELECT COUNT(*) FROM %s', $loan);
79        $stmt = $this->pdo->prepare($sql);
80        $stmt->execute();
81
82        return (int) $stmt->fetchColumn();
83    }
84
85    #[Override]
86    public function findByUserIdWithBookDetails(IdInterface $userId): array
87    {
88        $loan = $this->quoteTableName('loan');
89        $book = $this->quoteTableName('book');
90        $user = $this->quoteTableName('user');
91
92        $sql = "
93            SELECT
94                L.id as loan_id,
95                L.user_id,
96                L.book_id,
97                L.borrowed_at,
98                L.due_at,
99                L.returned_at,
100                L.status as loan_status,
101                B.title as book_title,
102                U.email as user_email
103            FROM {$loan} L
104                LEFT JOIN {$book} B ON L.book_id = B.id
105                LEFT JOIN {$user} U ON L.user_id = U.id
106            WHERE L.user_id = :user_id
107            ORDER BY L.borrowed_at DESC
108        ";
109
110        $stmt = $this->pdo->prepare($sql);
111        $stmt->execute(['user_id' => $userId->getValue()]);
112
113        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
114
115        return array_map(
116            $this->rowToLoanWithDetails(...),
117            $rows
118        );
119    }
120
121    abstract protected function quoteTableName(string $tableName): string;
122
123    private function rowToLoanWithDetails(array $row): LoanWithDetails
124    {
125        $dueAt = DateTimeImmutable::createFromFormat(DateTimeFormat::SQL_DATETIME, $row['due_at']);
126        $now = new DateTimeImmutable();
127        $isOverdue = $dueAt < $now && $row['returned_at'] === null;
128
129        return new LoanWithDetails(
130            loanId: $row['loan_id'],
131            userId: $row['user_id'],
132            userEmail: $row['user_email'] ?? 'Unknown',
133            bookId: $row['book_id'],
134            bookTitle: $row['book_title'] ?? 'Unknown',
135            borrowedAt: $row['borrowed_at'],
136            dueAt: $row['due_at'],
137            returnedAt: $row['returned_at'],
138            status: $row['loan_status'],
139            isOverdue: $isOverdue,
140        );
141    }
142}