Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
ConsentRepository
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
5 / 5
7
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
 append
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 getCurrentForUser
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getHistoryForUser
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 hydrate
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace App\Domain\Consent\Repository;
6
7use App\Domain\Consent\Data\ConsentRecord;
8use PDO;
9
10/**
11 * PDO-backed access to the consent_records ledger (FSC-87).
12 *
13 * Current state is read from the latest_consent view (most recent row per
14 * consent_type); history is read straight from the table. No update/delete
15 * methods exist by design — the ledger is append-only.
16 */
17final class ConsentRepository implements ConsentRepositoryInterface
18{
19    public function __construct(
20        private readonly PDO $pdo,
21    ) {}
22
23    public function append(
24        int $userId,
25        string $consentType,
26        string $action,
27        string $version,
28        string $languageShown,
29        ?string $ipAddress,
30        ?string $userAgent,
31        string $source,
32    ): int {
33        $sql = '
34            INSERT INTO consent_records (
35                user_id, consent_type, action, version, language_shown,
36                ip_address, user_agent, source
37            )
38            VALUES (
39                :user_id, :consent_type, :action, :version, :language_shown,
40                :ip_address, :user_agent, :source
41            )
42            RETURNING consent_id
43        ';
44
45        $stmt = $this->pdo->prepare($sql);
46        $stmt->execute([
47            'user_id' => $userId,
48            'consent_type' => $consentType,
49            'action' => $action,
50            'version' => $version,
51            'language_shown' => $languageShown,
52            'ip_address' => $ipAddress,
53            'user_agent' => $userAgent !== null ? mb_substr($userAgent, 0, 500) : null,
54            'source' => $source,
55        ]);
56
57        return (int)$stmt->fetchColumn();
58    }
59
60    /**
61     * @param int $userId
62     * @return list<ConsentRecord>
63     */
64    public function getCurrentForUser(int $userId): array
65    {
66        return $this->hydrate('
67            SELECT
68                consent_id     AS "consentId",
69                user_id        AS "userId",
70                consent_type   AS "consentType",
71                action,
72                version,
73                language_shown AS "languageShown",
74                ip_address     AS "ipAddress",
75                user_agent     AS "userAgent",
76                source,
77                created_at     AS "createdAt"
78            FROM latest_consent
79            WHERE user_id = :user_id
80            ORDER BY consent_type
81        ', $userId);
82    }
83
84    /**
85     * @param int $userId
86     * @return list<ConsentRecord>
87     */
88    public function getHistoryForUser(int $userId): array
89    {
90        return $this->hydrate('
91            SELECT
92                consent_id     AS "consentId",
93                user_id        AS "userId",
94                consent_type   AS "consentType",
95                action,
96                version,
97                language_shown AS "languageShown",
98                ip_address     AS "ipAddress",
99                user_agent     AS "userAgent",
100                source,
101                created_at     AS "createdAt"
102            FROM consent_records
103            WHERE user_id = :user_id
104            ORDER BY created_at DESC, consent_id DESC
105        ', $userId);
106    }
107
108    /**
109     * @param string $sql
110     * @param int $userId
111     * @return list<ConsentRecord>
112     */
113    private function hydrate(string $sql, int $userId): array
114    {
115        $stmt = $this->pdo->prepare($sql);
116        $stmt->execute(['user_id' => $userId]);
117
118        $records = [];
119        /** @var array<string, mixed> $row */
120        foreach ($stmt->fetchAll() as $row) {
121            $records[] = new ConsentRecord($row);
122        }
123
124        return $records;
125    }
126}