Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
88.41% |
61 / 69 |
|
40.00% |
2 / 5 |
CRAP | |
0.00% |
0 / 1 |
| UniversalUserStructureService | |
89.71% |
61 / 68 |
|
40.00% |
2 / 5 |
21.48 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| fetchUserStructureIds | |
77.78% |
7 / 9 |
|
0.00% |
0 / 1 |
2.04 | |||
| syncUserStructures | |
87.50% |
28 / 32 |
|
0.00% |
0 / 1 |
10.20 | |||
| assertStructureCanBeDeleted | |
94.12% |
16 / 17 |
|
0.00% |
0 / 1 |
6.01 | |||
| assertUserCanBeDeleted | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | /** @license For full copyright and license information, please see the LICENSE.md file. */ |
| 6 | |
| 7 | namespace App\Core\Engine\Application\Service; |
| 8 | |
| 9 | defined('AMMONLY_APP') || exit('Direct script access is forbidden.'); |
| 10 | |
| 11 | use App\Core\Engine\Application\Persistence\UniversalPersistenceManager; |
| 12 | use App\Modules\Structure\Domain\Exception\StructureReassignRequiredException; |
| 13 | use PDO; |
| 14 | |
| 15 | /** |
| 16 | * Manages user assignments to organizational structures and deletion safety checks. |
| 17 | */ |
| 18 | final 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 | } |