Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
114 / 114
100.00% covered (success)
100.00%
9 / 9
CRAP
100.00% covered (success)
100.00%
1 / 1
SqlRecordCoOwnerRepository
100.00% covered (success)
100.00%
113 / 113
100.00% covered (success)
100.00%
9 / 9
21
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
 findCoOwnerIds
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 findCoOwnerStructureIds
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 syncStructureCoOwners
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
5
 syncCoOwners
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
5
 addCoOwner
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 removeCoOwner
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 isCoOwner
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 deleteForRecord
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
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\Core\Engine\Infrastructure\Repository;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Engine\Domain\Repository\RecordCoOwnerRepositoryInterface;
12use PDO;
13
14/**
15 * SQL Implementation of Record Co-Owner Repository.
16 *
17 * Persists and manages relational co-ownership assignments in a_core_record_co_owners.
18 *
19 * @package App\Core\Engine\Infrastructure\Repository
20 */
21final readonly class SqlRecordCoOwnerRepository implements RecordCoOwnerRepositoryInterface
22{
23    private const string SQL_DELETE_CO_OWNERS = 'DELETE FROM `a_core_record_co_owners` ';
24    private const string SQL_INSERT_CO_OWNERS = 'INSERT IGNORE INTO `a_core_record_co_owners` '
25        . '(`module_name`, `record_id`, `owner_type`, `user_id`, `structure_id`) '
26        . "VALUES (:module_name, :record_id, 'user', :user_id, NULL)";
27    private const string SQL_INSERT_STRUCT_CO_OWNERS = 'INSERT IGNORE INTO `a_core_record_co_owners` '
28        . '(`module_name`, `record_id`, `owner_type`, `user_id`, `structure_id`) '
29        . "VALUES (:module_name, :record_id, 'structure', NULL, :structure_id)";
30    private const string SQL_WHERE_MODULE_AND_RECORD = 'WHERE `module_name` = :module_name '
31        . 'AND `record_id` = :record_id ';
32
33    /**
34     * SqlRecordCoOwnerRepository constructor.
35     *
36     * @param PDO $pdo Database connection.
37     */
38    public function __construct(private PDO $pdo)
39    {
40    }
41
42    /**
43     * {@inheritdoc}
44     */
45    public function findCoOwnerIds(string $moduleName, int $recordId): array
46    {
47        $sql = 'SELECT `user_id` FROM `a_core_record_co_owners` '
48            . self::SQL_WHERE_MODULE_AND_RECORD
49            . "AND (`owner_type` = 'user' OR `owner_type` IS NULL) "
50            . 'ORDER BY `user_id` ASC';
51
52        $stmt = $this->pdo->prepare($sql);
53        $stmt->execute([
54            ':module_name' => $moduleName,
55            ':record_id'   => $recordId,
56        ]);
57
58        $ids = [];
59        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
60            $ids[] = (int) $row['user_id'];
61        }
62
63        return $ids;
64    }
65
66    /**
67     * Retrieves list of co-owner structure IDs for a given module record.
68     *
69     * @param string $moduleName Module machine name.
70     * @param int    $recordId   Target record ID.
71     * @return array<int> List of structure IDs.
72     */
73    public function findCoOwnerStructureIds(string $moduleName, int $recordId): array
74    {
75        $sql = 'SELECT `structure_id` FROM `a_core_record_co_owners` '
76            . self::SQL_WHERE_MODULE_AND_RECORD
77            . "AND `owner_type` = 'structure' AND `structure_id` IS NOT NULL "
78            . 'ORDER BY `structure_id` ASC';
79
80        $stmt = $this->pdo->prepare($sql);
81        $stmt->execute([
82            ':module_name' => $moduleName,
83            ':record_id'   => $recordId,
84        ]);
85
86        $ids = [];
87        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
88            $ids[] = (int) $row['structure_id'];
89        }
90
91        return $ids;
92    }
93
94    /**
95     * Synchronizes co-owner structure IDs for a module record.
96     *
97     * @param string     $moduleName   Module machine name.
98     * @param int        $recordId     Target record ID.
99     * @param array<int> $structureIds Desired list of structure IDs.
100     * @return void
101     */
102    public function syncStructureCoOwners(string $moduleName, int $recordId, array $structureIds): void
103    {
104        $currentIds = $this->findCoOwnerStructureIds($moduleName, $recordId);
105        $targetIds  = array_values(array_unique(array_filter(
106            array_map(static fn(mixed $id): int => (int) $id, $structureIds),
107            static fn(int $id): bool => $id > 0
108        )));
109
110        $toAdd    = array_diff($targetIds, $currentIds);
111        $toRemove = array_diff($currentIds, $targetIds);
112
113        if ($toRemove !== []) {
114            $deleteSql = self::SQL_DELETE_CO_OWNERS
115                . self::SQL_WHERE_MODULE_AND_RECORD
116                . "AND `owner_type` = 'structure' AND `structure_id` = :structure_id";
117            $delStmt = $this->pdo->prepare($deleteSql);
118            foreach ($toRemove as $delId) {
119                $delStmt->execute([
120                    ':module_name'   => $moduleName,
121                    ':record_id'     => $recordId,
122                    ':structure_id'  => $delId,
123                ]);
124            }
125        }
126
127        if ($toAdd !== []) {
128            $insStmt = $this->pdo->prepare(self::SQL_INSERT_STRUCT_CO_OWNERS);
129            foreach ($toAdd as $addId) {
130                $insStmt->execute([
131                    ':module_name'   => $moduleName,
132                    ':record_id'     => $recordId,
133                    ':structure_id'  => $addId,
134                ]);
135            }
136        }
137    }
138
139    /**
140     * {@inheritdoc}
141     */
142    public function syncCoOwners(string $moduleName, int $recordId, array $userIds): void
143    {
144        $currentIds = $this->findCoOwnerIds($moduleName, $recordId);
145        $targetIds  = array_values(array_unique(array_filter(
146            array_map(static fn(mixed $id): int => (int) $id, $userIds),
147            static fn(int $id): bool => $id > 0
148        )));
149
150        $toAdd    = array_diff($targetIds, $currentIds);
151        $toRemove = array_diff($currentIds, $targetIds);
152
153        if ($toRemove !== []) {
154            $deleteSql = self::SQL_DELETE_CO_OWNERS
155                . 'WHERE `module_name` = :module_name AND `record_id` = :record_id AND `user_id` = :user_id';
156            $delStmt = $this->pdo->prepare($deleteSql);
157            foreach ($toRemove as $delId) {
158                $delStmt->execute([
159                    ':module_name' => $moduleName,
160                    ':record_id'   => $recordId,
161                    ':user_id'     => $delId,
162                ]);
163            }
164        }
165
166        if ($toAdd !== []) {
167            $insStmt = $this->pdo->prepare(self::SQL_INSERT_CO_OWNERS);
168            foreach ($toAdd as $addId) {
169                $insStmt->execute([
170                    ':module_name' => $moduleName,
171                    ':record_id'   => $recordId,
172                    ':user_id'     => $addId,
173                ]);
174            }
175        }
176    }
177
178    /**
179     * {@inheritdoc}
180     */
181    public function addCoOwner(string $moduleName, int $recordId, int $userId): void
182    {
183        if ($userId <= 0) {
184            return;
185        }
186
187        $stmt = $this->pdo->prepare(self::SQL_INSERT_CO_OWNERS);
188        $stmt->execute([
189            ':module_name' => $moduleName,
190            ':record_id'   => $recordId,
191            ':user_id'     => $userId,
192        ]);
193    }
194
195    /**
196     * {@inheritdoc}
197     */
198    public function removeCoOwner(string $moduleName, int $recordId, int $userId): void
199    {
200        $sql = self::SQL_DELETE_CO_OWNERS
201            . 'WHERE `module_name` = :module_name AND `record_id` = :record_id AND `user_id` = :user_id';
202
203        $stmt = $this->pdo->prepare($sql);
204        $stmt->execute([
205            ':module_name' => $moduleName,
206            ':record_id'   => $recordId,
207            ':user_id'     => $userId,
208        ]);
209    }
210
211    /**
212     * {@inheritdoc}
213     */
214    public function isCoOwner(string $moduleName, int $recordId, int $userId): bool
215    {
216        if ($userId <= 0) {
217            return false;
218        }
219
220        $sql = 'SELECT 1 FROM `a_core_record_co_owners` '
221            . 'WHERE `module_name` = :module_name AND `record_id` = :record_id AND `user_id` = :user_id '
222            . 'LIMIT 1';
223
224        $stmt = $this->pdo->prepare($sql);
225        $stmt->execute([
226            ':module_name' => $moduleName,
227            ':record_id'   => $recordId,
228            ':user_id'     => $userId,
229        ]);
230
231        return (bool) $stmt->fetchColumn();
232    }
233
234    /**
235     * {@inheritdoc}
236     */
237    public function deleteForRecord(string $moduleName, int $recordId): void
238    {
239        $sql = self::SQL_DELETE_CO_OWNERS
240            . 'WHERE `module_name` = :module_name AND `record_id` = :record_id';
241
242        $stmt = $this->pdo->prepare($sql);
243        $stmt->execute([
244            ':module_name' => $moduleName,
245            ':record_id'   => $recordId,
246        ]);
247    }
248}