Lines 92.85% 52 / 56
Methods 60.00% 3 / 5
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 get 88.46% 23 / 26 0.00% 0 / 1 9.12
 save 100.00% 18 / 18 100.00% 1 / 1 2
 hasRow 88.88% 8 / 9 0.00% 0 / 1 2.01
 reset 100.00% 2 / 2 100.00% 1 / 1 1
29readonly class DashboardLayout
30{
31    public function __construct(
32        private Configuration $configuration,
33    ) {
34    }
35
36    /**
37     * Returns the stored widget layout for a user, or an empty array when none exists.
38     *
39     * @return array<int, array{key: string, position: int, visible: bool}>
40     */
41    public function get(int $userId): array
42    {
43        $query = sprintf(
44            'SELECT config FROM %sfaqadmindashboard WHERE user_id = %d',
45            Database::getTablePrefix(),
46            $userId,
47        );
48
49        $result = $this->configuration->getDb()->query($query);
50        if ($result === false) {
51            return [];
52        }
53
54        $row = $this->configuration->getDb()->fetchObject($result);
55        if (!is_object($row) || !is_string($row->config ?? null) || $row->config === '') {
56            return [];
57        }
58
59        try {
60            $config = json_decode($row->config, associative: true, depth: 16, flags: JSON_THROW_ON_ERROR);
61        } catch (JsonException) {
62            return [];
63        }
64
65        if (!is_array($config)) {
66            return [];
67        }
68
69        $layout = [];
70        foreach ($config as $entry) {
71            if (!is_array($entry)) {
72                continue;
73            }
74
75            $layout[] = [
76                'key' => (string) ($entry['key'] ?? ''),
77                'position' => (int) ($entry['position'] ?? 0),
78                'visible' => (bool) ($entry['visible'] ?? true),
79            ];
80        }
81
82        return $layout;
83    }
84
85    /**
86     * Stores the widget layout for a user, replacing any previous layout.
87     *
88     * @param array<int, array{key: string, position: int, visible: bool}> $config
89     * @throws JsonException
90     */
91    public function save(int $userId, array $config): bool
92    {
93        $database = $this->configuration->getDb();
94        $prefix = Database::getTablePrefix();
95        $encoded = $database->escape(json_encode($config, JSON_THROW_ON_ERROR));
96
97        // Update an existing row in place rather than delete-then-insert: this avoids
98        // a window where a failing insert would leave the user without any layout.
99        if ($this->hasRow($userId)) {
100            $update = sprintf(
101                "UPDATE %sfaqadmindashboard SET config = '%s' WHERE user_id = %d",
102                $prefix,
103                $encoded,
104                $userId,
105            );
106
107            return $database->query($update) !== false;
108        }
109
110        $insert = sprintf(
111            "INSERT INTO %sfaqadmindashboard (user_id, config) VALUES (%d, '%s')",
112            $prefix,
113            $userId,
114            $encoded,
115        );
116
117        return $database->query($insert) !== false;
118    }
119
120    /**
121     * Returns whether a layout row already exists for the given user.
122     */
123    private function hasRow(int $userId): bool
124    {
125        $query = sprintf(
126            'SELECT user_id FROM %sfaqadmindashboard WHERE user_id = %d',
127            Database::getTablePrefix(),
128            $userId,
129        );
130
131        $result = $this->configuration->getDb()->query($query);
132        if ($result === false) {
133            return false;
134        }
135
136        return is_object($this->configuration->getDb()->fetchObject($result));
137    }
138
139    /**
140     * Removes the stored layout for a user, reverting them to the default layout.
141     */
142    public function reset(int $userId): bool
143    {
144        $query = sprintf('DELETE FROM %sfaqadmindashboard WHERE user_id = %d', Database::getTablePrefix(), $userId);
145
146        return $this->configuration->getDb()->query($query) !== false;
147    }
148}