Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.41% covered (warning)
88.41%
61 / 69
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
UniversalUserStructureService
89.71% covered (warning)
89.71%
61 / 68
40.00% covered (danger)
40.00%
2 / 5
21.48
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
 fetchUserStructureIds
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
2.04
 syncUserStructures
87.50% covered (warning)
87.50%
28 / 32
0.00% covered (danger)
0.00%
0 / 1
10.20
 assertStructureCanBeDeleted
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
6.01
 assertUserCanBeDeleted
100.00% covered (success)
100.00%
9 / 9
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\Core\Engine\Application\Service;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Engine\Application\Persistence\UniversalPersistenceManager;
12use App\Modules\Structure\Domain\Exception\StructureReassignRequiredException;
13use PDO;
14
15/**
16 * Manages user assignments to organizational structures and deletion safety checks.
17 */
18final readonly class UniversalUserStructureService
19{
20    public function __construct(
21        private UniversalPersistenceManager $persistence
22    ) {
23    }
24
25    /**
26     * Fetches assigned structure IDs for a system user.
27     *
28     * @param int $userId User record ID.
29     * @return list<int>
30     */
31    public function fetchUserStructureIds(int $userId): array
32    {
33        try {
34            $pdo = $this->persistence->getPdo();
35            $stStmt = $pdo->prepare(
36                'SELECT `structure_id` FROM `a_rel_users_structure` WHERE `user_id` = :uid ORDER BY `structure_id` ASC'
37            );
38            $stStmt->execute([':uid' => $userId]);
39            /** @var list<int|string> $structCol */
40            $structCol = $stStmt->fetchAll(PDO::FETCH_COLUMN);
41
42            return array_map('intval', $structCol);
43        } catch (\Throwable) {
44            return [];
45        }
46    }
47
48    /**
49     * Synchronizes user assignments in rel_users_structure.
50     *
51     * @param int   $userId User ID.
52     * @param mixed $raw    Raw input structures payload (array, JSON string, etc.).
53     */
54    public function syncUserStructures(int $userId, mixed $raw): void
55    {
56        if ($userId <= 0) {
57            return;
58        }
59
60        if (is_array($raw)) {
61            $list = $raw;
62        } elseif (is_string($raw)) {
63            $list = json_decode($raw, true);
64        } else {
65            $list = [];
66        }
67
68        $targetIds = array_values(array_unique(array_filter(
69            array_map('intval', is_array($list) ? $list : []),
70            static fn(int $id): bool => $id > 0
71        )));
72
73        try {
74            $pdo = $this->persistence->getPdo();
75            $stmtCur = $pdo->prepare('SELECT structure_id FROM `a_rel_users_structure` WHERE user_id = :uid');
76            $stmtCur->execute([':uid' => $userId]);
77            $currentIds = array_map('intval', $stmtCur->fetchAll(PDO::FETCH_COLUMN));
78
79            $toAdd = array_diff($targetIds, $currentIds);
80            $toRemove = array_diff($currentIds, $targetIds);
81
82            if ($toRemove !== []) {
83                $delStmt = $pdo->prepare(
84                    'DELETE FROM `a_rel_users_structure` WHERE user_id = :uid AND structure_id = :sid'
85                );
86                foreach ($toRemove as $remSid) {
87                    $delStmt->execute([':uid' => $userId, ':sid' => $remSid]);
88                }
89            }
90
91            if ($toAdd !== []) {
92                $insStmt = $pdo->prepare(
93                    'INSERT IGNORE INTO `a_rel_users_structure` (`user_id`, `structure_id`, `created_at`) '
94                    . 'VALUES (:uid, :sid, :created_at)'
95                );
96                $now = date('Y-m-d H:i:s');
97                foreach ($toAdd as $addSid) {
98                    $insStmt->execute([':uid' => $userId, ':sid' => $addSid, ':created_at' => $now]);
99                }
100            }
101        } catch (\Throwable) {
102            // Ignore if structure is not supported or relation table is missing
103        }
104    }
105
106    /**
107     * Asserts that an organizational structure node can be deleted without orphaned dependencies (Rule 2).
108     *
109     * @param int $structureId Structure node ID.
110     * @throws StructureReassignRequiredException When structure has children, assigned users or records.
111     */
112    public function assertStructureCanBeDeleted(int $structureId): void
113    {
114        try {
115            $pdo = $this->persistence->getPdo();
116
117            $stmtChild = $pdo->prepare('SELECT COUNT(*) FROM `a_mod_structure_records` WHERE parent_id = :sid');
118            $stmtChild->execute([':sid' => $structureId]);
119            $children = (int) $stmtChild->fetchColumn();
120
121            $stmtUsers = $pdo->prepare('SELECT COUNT(*) FROM `a_rel_users_structure` WHERE structure_id = :sid');
122            $stmtUsers->execute([':sid' => $structureId]);
123            $users = (int) $stmtUsers->fetchColumn();
124
125            $stmtCo = $pdo->prepare(
126                "SELECT COUNT(*) FROM `a_core_record_co_owners` WHERE owner_type = 'structure' AND structure_id = :sid"
127            );
128            $stmtCo->execute([':sid' => $structureId]);
129            $records = (int) $stmtCo->fetchColumn();
130
131            if ($children > 0 || $users > 0 || $records > 0) {
132                throw StructureReassignRequiredException::forStructure($structureId, $records, $users, $children);
133            }
134        } catch (StructureReassignRequiredException $e) {
135            throw $e;
136        } catch (\Throwable) {
137            // Ignore if structure tables do not exist
138        }
139    }
140
141    /**
142     * Asserts that a user can be deleted without orphaned record ownership (Rule 2).
143     *
144     * @param int $userId Target user ID.
145     * @throws StructureReassignRequiredException When user still owns active records.
146     */
147    public function assertUserCanBeDeleted(int $userId): void
148    {
149        $pdo = $this->persistence->getPdo();
150        $stmtCo = $pdo->prepare(
151            "SELECT COUNT(*) FROM `a_core_record_co_owners` "
152            . "WHERE (owner_type = 'user' OR owner_type IS NULL) AND user_id = :uid"
153        );
154        $stmtCo->execute([':uid' => $userId]);
155        $records = (int) $stmtCo->fetchColumn();
156
157        if ($records > 0) {
158            throw StructureReassignRequiredException::forUser($userId, $records);
159        }
160    }
161}