Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.24% covered (success)
90.24%
74 / 82
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
SqlDavDeviceRepository
90.12% covered (success)
90.12%
73 / 81
71.43% covered (warning)
71.43%
5 / 7
20.39
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
 listByUser
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 create
78.79% covered (warning)
78.79%
26 / 33
0.00% covered (danger)
0.00%
0 / 1
7.47
 delete
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 findByToken
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 touchSync
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 mapRowToDevice
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
6.01
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Modules\Dav\Infrastructure\Repository;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Modules\Dav\Domain\Model\DavDevice;
12use App\Modules\Dav\Domain\Repository\DavDeviceRepositoryInterface;
13use DateTimeImmutable;
14use PDO;
15
16/**
17 * SQL Implementation of DAV Device Repository.
18 *
19 * Provides database persistence operations for mobile and desktop DAV paired devices.
20 *
21 * @package App\Modules\Dav\Infrastructure\Repository
22 */
23final readonly class SqlDavDeviceRepository implements DavDeviceRepositoryInterface
24{
25    /**
26     * SqlDavDeviceRepository constructor.
27     *
28     * @param PDO $pdo PDO database connection instance.
29     * @param string $tablePrefix Database table prefix (default 'a_').
30     */
31    public function __construct(
32        private PDO $pdo,
33        private string $tablePrefix = 'a_'
34    ) {
35    }
36
37    /**
38     * {@inheritdoc}
39     */
40    public function listByUser(int $userId): array
41    {
42        $tableName = $this->tablePrefix . 'mod_dav_devices';
43        $sql = "SELECT id, user_id, device_name, device_type, sync_token, last_sync_at, is_active, created_at
44                FROM {$tableName}
45                WHERE user_id = :uid AND is_active = 1
46                ORDER BY id DESC";
47
48        $stmt = $this->pdo->prepare($sql);
49        $stmt->execute([':uid' => $userId]);
50
51        /** @var array<int, array<string, mixed>> $rows */
52        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
53
54        $devices = [];
55        foreach ($rows as $row) {
56            $devices[] = $this->mapRowToDevice($row);
57        }
58
59        return $devices;
60    }
61
62    /**
63     * {@inheritdoc}
64     */
65    public function create(int $userId, string $deviceName, string $deviceType): DavDevice
66    {
67        $tableName = $this->tablePrefix . 'mod_dav_devices';
68        $token = bin2hex(random_bytes(24));
69        $cleanName = trim($deviceName);
70        if ($cleanName === '') {
71            $cleanName = match (strtolower($deviceType)) {
72                'ios'     => 'Apple iPhone / iPad',
73                'android' => 'Android Smartphone',
74                'macos'   => 'Apple Mac',
75                default   => 'Personal Device',
76            };
77        }
78
79        $cleanType = strtolower(trim($deviceType));
80        if (!in_array($cleanType, ['ios', 'android', 'macos', 'other'], true)) {
81            $cleanType = 'other';
82        }
83
84        $sql = "INSERT INTO {$tableName}
85                (user_id, device_name, device_type, sync_token, is_active, created_at, updated_at)
86                VALUES (:uid, :dname, :dtype, :token, 1, NOW(6), NOW(6))";
87
88        $stmt = $this->pdo->prepare($sql);
89        $stmt->execute([
90            ':uid'   => $userId,
91            ':dname' => $cleanName,
92            ':dtype' => $cleanType,
93            ':token' => $token,
94        ]);
95
96        $id = (int) $this->pdo->lastInsertId();
97
98        return new DavDevice(
99            id: $id,
100            userId: $userId,
101            deviceName: $cleanName,
102            deviceType: $cleanType,
103            syncToken: $token,
104            lastSyncAt: null,
105            isActive: true,
106            createdAt: new DateTimeImmutable()
107        );
108    }
109
110    /**
111     * {@inheritdoc}
112     */
113    public function delete(int $deviceId, int $userId): bool
114    {
115        $tableName = $this->tablePrefix . 'mod_dav_devices';
116        $sql = "DELETE FROM {$tableName} WHERE id = :id AND user_id = :uid";
117        $stmt = $this->pdo->prepare($sql);
118        $stmt->execute([
119            ':id'  => $deviceId,
120            ':uid' => $userId,
121        ]);
122
123        return $stmt->rowCount() > 0;
124    }
125
126    /**
127     * {@inheritdoc}
128     */
129    public function findByToken(string $token): ?DavDevice
130    {
131        $tableName = $this->tablePrefix . 'mod_dav_devices';
132        $sql = "SELECT id, user_id, device_name, device_type, sync_token, last_sync_at, is_active, created_at
133                FROM {$tableName}
134                WHERE sync_token = :token AND is_active = 1
135                LIMIT 1";
136
137        $stmt = $this->pdo->prepare($sql);
138        $stmt->execute([':token' => $token]);
139
140        /** @var array<string, mixed>|false $row */
141        $row = $stmt->fetch(PDO::FETCH_ASSOC);
142
143        return $row !== false ? $this->mapRowToDevice($row) : null;
144    }
145
146    /**
147     * {@inheritdoc}
148     */
149    public function touchSync(int $deviceId): bool
150    {
151        $tableName = $this->tablePrefix . 'mod_dav_devices';
152        $sql = "UPDATE {$tableName} SET last_sync_at = NOW(), updated_at = NOW(6) WHERE id = :id";
153        $stmt = $this->pdo->prepare($sql);
154
155        return $stmt->execute([':id' => $deviceId]);
156    }
157
158    /**
159     * Maps database row to DavDevice domain model.
160     *
161     * @param array<string, mixed> $row Raw associative database row.
162     * @return DavDevice Hydrated domain entity.
163     */
164    private function mapRowToDevice(array $row): DavDevice
165    {
166        $lastSync = isset($row['last_sync_at']) && is_string($row['last_sync_at']) && $row['last_sync_at'] !== ''
167            ? new DateTimeImmutable($row['last_sync_at'])
168            : null;
169
170        $createdAt = isset($row['created_at']) && is_string($row['created_at'])
171            ? new DateTimeImmutable($row['created_at'])
172            : new DateTimeImmutable();
173
174        return new DavDevice(
175            id: (int)$row['id'],
176            userId: (int)$row['user_id'],
177            deviceName: (string)$row['device_name'],
178            deviceType: (string)$row['device_type'],
179            syncToken: (string)$row['sync_token'],
180            lastSyncAt: $lastSync,
181            isActive: (bool)$row['is_active'],
182            createdAt: $createdAt
183        );
184    }
185}