Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.44% covered (success)
99.44%
176 / 177
94.44% covered (success)
94.44%
17 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
SqlProfilePermissionRepository
99.43% covered (success)
99.43%
175 / 176
94.44% covered (success)
94.44%
17 / 18
34
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
 getProfile
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 getAllProfiles
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getModulePermissions
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
2
 getFieldPermissions
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
4
 canViewModule
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 canCreateInModule
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 canEditInModule
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 canDeleteInModule
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFieldPermission
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 setModulePermission
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 bulkSetModulePermission
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 setFieldPermission
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 bulkSetFieldPermissions
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 saveMatrix
96.55% covered (success)
96.55%
28 / 29
0.00% covered (danger)
0.00%
0 / 1
8
 copyPermissions
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
3
 invalidateCache
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 checkModuleAction
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
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\Profiles\Infrastructure\Repository;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Modules\Profiles\Domain\Model\FieldPermissionType;
12use App\Modules\Profiles\Domain\Model\PermissionProfile;
13use App\Modules\Profiles\Domain\Model\ProfileFieldPermission;
14use App\Modules\Profiles\Domain\Model\ProfileModulePermission;
15use App\Modules\Profiles\Domain\Repository\ProfilePermissionRepositoryInterface;
16use PDO;
17
18/**
19 * SQL Profile Permission Repository.
20 *
21 * Persists and retrieves profile functional permissions using direct PDO prepared queries.
22 *
23 * @package App\Modules\Profiles\Infrastructure\Repository
24 */
25final readonly class SqlProfilePermissionRepository implements ProfilePermissionRepositoryInterface
26{
27    private const string SQL_INSERT_MODULES_PREFIX = 'INSERT INTO `a_core_profile_modules` '
28        . '(`profile_id`, `module_name`, `can_view`, `can_create`, `can_edit`, `can_delete`) ';
29
30    private const string SQL_INSERT_FIELDS_PREFIX = 'INSERT INTO `a_core_profile_fields` '
31        . '(`profile_id`, `module_name`, `field_key`, `permission`) ';
32
33    private const string SQL_UPDATE_TS = '`updated_at` = CURRENT_TIMESTAMP(6)';
34
35    private const string SQL_UPSERT_MODULE = self::SQL_INSERT_MODULES_PREFIX
36        . 'VALUES (:pid, :mod, :v, :c, :e, :d) '
37        . 'ON DUPLICATE KEY UPDATE `can_view` = VALUES(`can_view`), `can_create` = VALUES(`can_create`), '
38        . '`can_edit` = VALUES(`can_edit`), `can_delete` = VALUES(`can_delete`), '
39        . self::SQL_UPDATE_TS;
40
41    private const string SQL_UPSERT_FIELD = self::SQL_INSERT_FIELDS_PREFIX
42        . 'VALUES (:pid, :mod, :fld, :perm) '
43        . 'ON DUPLICATE KEY UPDATE `permission` = VALUES(`permission`), '
44        . self::SQL_UPDATE_TS;
45
46    /**
47     * SqlProfilePermissionRepository constructor.
48     *
49     * @param PDO $pdo Database PDO connection.
50     */
51    public function __construct(private PDO $pdo)
52    {
53    }
54
55    /**
56     * @inheritDoc
57     */
58    public function getProfile(int $profileId): ?PermissionProfile
59    {
60        $stmt = $this->pdo->prepare(
61            'SELECT `id`, `name`, `code`, `description`, `status`, `special_access`, '
62            . '`created_at`, `updated_at`, `created_by`, `owner`, `co_owners` '
63            . 'FROM `a_mod_profiles_records` WHERE `id` = :id LIMIT 1'
64        );
65        $stmt->execute([':id' => $profileId]);
66        /** @var array<string, mixed>|false $row */
67        $row = $stmt->fetch(PDO::FETCH_ASSOC);
68
69        return $row ? PermissionProfile::fromArray($row) : null;
70    }
71
72    /**
73     * @inheritDoc
74     */
75    public function getAllProfiles(): array
76    {
77        $stmt = $this->pdo->query(
78            'SELECT `id`, `name`, `code`, `description`, `status`, `special_access`, '
79            . '`created_at`, `updated_at`, `created_by`, `owner`, `co_owners` '
80            . 'FROM `a_mod_profiles_records` WHERE `status` = \'active\' AND `special_access` = 1 ORDER BY `id` ASC'
81        );
82        /** @var list<array<string, mixed>> $rows */
83        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
84
85        return array_map(static fn(array $r): PermissionProfile => PermissionProfile::fromArray($r), $rows);
86    }
87
88    /**
89     * @inheritDoc
90     */
91    public function getModulePermissions(int $profileId): array
92    {
93        $stmt = $this->pdo->prepare(
94            'SELECT `profile_id`, `module_name`, `can_view`, `can_create`, `can_edit`, `can_delete` '
95            . 'FROM `a_core_profile_modules` WHERE `profile_id` = :pid'
96        );
97        $stmt->execute([':pid' => $profileId]);
98        /** @var list<array<string, mixed>> $rows */
99        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
100
101        $result = [];
102        foreach ($rows as $row) {
103            $name = (string) $row['module_name'];
104            $result[$name] = new ProfileModulePermission(
105                profileId: (int) $row['profile_id'],
106                moduleName: $name,
107                canView: (bool) $row['can_view'],
108                canCreate: (bool) $row['can_create'],
109                canEdit: (bool) $row['can_edit'],
110                canDelete: (bool) $row['can_delete'],
111            );
112        }
113
114        return $result;
115    }
116
117    /**
118     * @inheritDoc
119     */
120    public function getFieldPermissions(int $profileId, ?string $moduleName = null): array
121    {
122        $sql = 'SELECT `profile_id`, `module_name`, `field_key`, `permission` '
123            . 'FROM `a_core_profile_fields` WHERE `profile_id` = :pid';
124        $params = [':pid' => $profileId];
125
126        if ($moduleName !== null && $moduleName !== '') {
127            $sql .= ' AND `module_name` = :mod';
128            $params[':mod'] = $moduleName;
129        }
130
131        $stmt = $this->pdo->prepare($sql);
132        $stmt->execute($params);
133        /** @var list<array<string, mixed>> $rows */
134        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
135
136        $result = [];
137        foreach ($rows as $row) {
138            $key = $row['module_name'] . '.' . $row['field_key'];
139            $permType = FieldPermissionType::tryFrom((string) $row['permission']) ?? FieldPermissionType::EDIT;
140            $result[$key] = new ProfileFieldPermission(
141                profileId: (int) $row['profile_id'],
142                moduleName: (string) $row['module_name'],
143                fieldKey: (string) $row['field_key'],
144                permission: $permType,
145            );
146        }
147
148        return $result;
149    }
150
151    /**
152     * @inheritDoc
153     */
154    public function canViewModule(int $profileId, string $moduleName): bool
155    {
156        return $this->checkModuleAction($profileId, $moduleName, 'can_view');
157    }
158
159    /**
160     * @inheritDoc
161     */
162    public function canCreateInModule(int $profileId, string $moduleName): bool
163    {
164        return $this->checkModuleAction($profileId, $moduleName, 'can_create');
165    }
166
167    /**
168     * @inheritDoc
169     */
170    public function canEditInModule(int $profileId, string $moduleName): bool
171    {
172        return $this->checkModuleAction($profileId, $moduleName, 'can_edit');
173    }
174
175    /**
176     * @inheritDoc
177     */
178    public function canDeleteInModule(int $profileId, string $moduleName): bool
179    {
180        return $this->checkModuleAction($profileId, $moduleName, 'can_delete');
181    }
182
183    /**
184     * @inheritDoc
185     */
186    public function getFieldPermission(int $profileId, string $moduleName, string $fieldKey): FieldPermissionType
187    {
188        $stmt = $this->pdo->prepare(
189            'SELECT `permission` FROM `a_core_profile_fields` '
190            . 'WHERE `profile_id` = :pid AND `module_name` = :mod AND `field_key` = :fld LIMIT 1'
191        );
192        $stmt->execute([':pid' => $profileId, ':mod' => $moduleName, ':fld' => $fieldKey]);
193        $val = $stmt->fetchColumn();
194
195        return is_string($val)
196            ? (FieldPermissionType::tryFrom($val) ?? FieldPermissionType::EDIT)
197            : FieldPermissionType::EDIT;
198    }
199
200    /**
201     * @inheritDoc
202     */
203    public function setModulePermission(
204        int    $profileId,
205        string $moduleName,
206        bool   $canView,
207        bool   $canCreate,
208        bool   $canEdit,
209        bool   $canDelete
210    ): void {
211        $stmt = $this->pdo->prepare(self::SQL_UPSERT_MODULE);
212        $stmt->execute([
213            ':pid' => $profileId,
214            ':mod' => $moduleName,
215            ':v'   => (int) $canView,
216            ':c'   => (int) $canCreate,
217            ':e'   => (int) $canEdit,
218            ':d'   => (int) $canDelete,
219        ]);
220    }
221
222    /**
223     * @inheritDoc
224     */
225    public function bulkSetModulePermission(int $profileId, string $action, bool $value): void
226    {
227        $allowed = ['view' => 'can_view', 'create' => 'can_create', 'edit' => 'can_edit', 'delete' => 'can_delete'];
228        $column = $allowed[$action] ?? 'can_view';
229        $intVal = (int) $value;
230
231        // Update existing records
232        $stmt = $this->pdo->prepare(
233            "UPDATE `a_core_profile_modules` SET `{$column}` = :val, "
234            . self::SQL_UPDATE_TS
235            . " WHERE `profile_id` = :pid"
236        );
237        $stmt->execute([':val' => $intVal, ':pid' => $profileId]);
238
239        // Insert missing records for all active modules
240        $stmt2 = $this->pdo->prepare(
241            self::SQL_INSERT_MODULES_PREFIX
242            . "SELECT :pid, `name`, 1, 1, 1, 1 FROM `a_core_module_records` WHERE `is_active` = 1 "
243            . "ON DUPLICATE KEY UPDATE `{$column}` = :val2, " . self::SQL_UPDATE_TS
244        );
245        $stmt2->execute([':pid' => $profileId, ':val2' => $intVal]);
246    }
247
248    /**
249     * @inheritDoc
250     */
251    public function setFieldPermission(
252        int                 $profileId,
253        string              $moduleName,
254        string              $fieldKey,
255        FieldPermissionType $permission
256    ): void {
257        $stmt = $this->pdo->prepare(self::SQL_UPSERT_FIELD);
258        $stmt->execute([
259            ':pid'  => $profileId,
260            ':mod'  => $moduleName,
261            ':fld'  => $fieldKey,
262            ':perm' => $permission->value,
263        ]);
264    }
265
266    /**
267     * @inheritDoc
268     */
269    public function bulkSetFieldPermissions(
270        int                 $profileId,
271        string              $moduleName,
272        FieldPermissionType $permission
273    ): void {
274        $stmt = $this->pdo->prepare(
275            self::SQL_INSERT_FIELDS_PREFIX
276            . 'SELECT :pid, :mod, f.`field_key`, :perm '
277            . 'FROM `a_core_field_records` f '
278            . 'JOIN `a_core_module_records` m ON m.`id` = f.`module_id` '
279            . 'WHERE m.`name` = :mod2 AND f.`special_access` = 1 '
280            . 'ON DUPLICATE KEY UPDATE `permission` = VALUES(`permission`), '
281            . self::SQL_UPDATE_TS
282        );
283        $stmt->execute([
284            ':pid'  => $profileId,
285            ':mod'  => $moduleName,
286            ':perm' => $permission->value,
287            ':mod2' => $moduleName,
288        ]);
289    }
290
291    /**
292     * @inheritDoc
293     */
294    public function saveMatrix(int $profileId, array $modules, array $fields): void
295    {
296        $this->pdo->beginTransaction();
297        try {
298            if (!empty($modules)) {
299                $stmtMod = $this->pdo->prepare(self::SQL_UPSERT_MODULE);
300                foreach ($modules as $m) {
301                    $stmtMod->execute([
302                        ':pid' => $profileId,
303                        ':mod' => (string) ($m['module_name'] ?? ''),
304                        ':v'   => (int) ($m['can_view'] ?? false),
305                        ':c'   => (int) ($m['can_create'] ?? false),
306                        ':e'   => (int) ($m['can_edit'] ?? false),
307                        ':d'   => (int) ($m['can_delete'] ?? false),
308                    ]);
309                }
310            }
311
312            if (!empty($fields)) {
313                $stmtFld = $this->pdo->prepare(self::SQL_UPSERT_FIELD);
314                foreach ($fields as $f) {
315                    $perm = $f['permission'] instanceof FieldPermissionType
316                        ? $f['permission']->value
317                        : (string) ($f['permission'] ?? 'edit');
318                    $stmtFld->execute([
319                        ':pid'  => $profileId,
320                        ':mod'  => (string) ($f['module_name'] ?? ''),
321                        ':fld'  => (string) ($f['field_key'] ?? ''),
322                        ':perm' => $perm,
323                    ]);
324                }
325            }
326
327            $this->pdo->commit();
328        } catch (\Throwable $e) {
329            if ($this->pdo->inTransaction()) {
330                $this->pdo->rollBack();
331            }
332            throw $e;
333        }
334    }
335
336    /**
337     * @inheritDoc
338     */
339    public function copyPermissions(int $sourceProfileId, int $targetProfileId): void
340    {
341        $this->pdo->beginTransaction();
342        try {
343            $copyModulesSql = 'INSERT INTO `a_core_profile_modules` '
344                . '(`profile_id`, `module_name`, `can_view`, `can_create`, `can_edit`, `can_delete`) '
345                . 'SELECT :target_id, `module_name`, `can_view`, `can_create`, `can_edit`, `can_delete` '
346                . 'FROM `a_core_profile_modules` '
347                . 'WHERE `profile_id` = :source_id '
348                . 'ON DUPLICATE KEY UPDATE '
349                . '`can_view` = VALUES(`can_view`), '
350                . '`can_create` = VALUES(`can_create`), '
351                . '`can_edit` = VALUES(`can_edit`), '
352                . '`can_delete` = VALUES(`can_delete`)';
353
354            $stmtMod = $this->pdo->prepare($copyModulesSql);
355            $stmtMod->execute([':target_id' => $targetProfileId, ':source_id' => $sourceProfileId]);
356
357            $copyFieldsSql = 'INSERT INTO `a_core_profile_fields` '
358                . '(`profile_id`, `module_name`, `field_key`, `permission`) '
359                . 'SELECT :target_id, `module_name`, `field_key`, `permission` '
360                . 'FROM `a_core_profile_fields` '
361                . 'WHERE `profile_id` = :source_id '
362                . 'ON DUPLICATE KEY UPDATE '
363                . '`permission` = VALUES(`permission`)';
364
365            $stmtFld = $this->pdo->prepare($copyFieldsSql);
366            $stmtFld->execute([':target_id' => $targetProfileId, ':source_id' => $sourceProfileId]);
367
368            $this->pdo->commit();
369        } catch (\Throwable $e) {
370            if ($this->pdo->inTransaction()) {
371                $this->pdo->rollBack();
372            }
373            throw $e;
374        }
375    }
376
377    /**
378     * @inheritDoc
379     */
380    public function invalidateCache(int $profileId): void
381    {
382        // Direct SQL repository has no persistent cache
383    }
384
385    /**
386     * Executes a single module action check query.
387     *
388     * @param int    $profileId  Profile identifier.
389     * @param string $moduleName Machine module name.
390     * @param string $column     Action column name.
391     * @return bool True if permitted or default true.
392     */
393    private function checkModuleAction(int $profileId, string $moduleName, string $column): bool
394    {
395        $stmt = $this->pdo->prepare(
396            "SELECT `{$column}` FROM `a_core_profile_modules` "
397            . "WHERE `profile_id` = :pid AND `module_name` = :mod LIMIT 1"
398        );
399        $stmt->execute([':pid' => $profileId, ':mod' => $moduleName]);
400        $val = $stmt->fetchColumn();
401
402        return $val === false ? true : (bool) $val;
403    }
404}