Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
SystemStateService
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 9
506
0.00% covered (danger)
0.00%
0 / 1
 get
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 set
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 delete
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 has
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 all
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 flush
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 load
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
30
 persist
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
42
 resolvePath
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace App\Services;
4
5/**
6 * SystemStateService
7 *
8 * Persiste l'état du processus d'installation dans un fichier JSON sur disque.
9 * Résiste aux redémarrages serveur et aux vidages de cache.
10 *
11 * Fichier : writable/install_state.json
12 */
13class SystemStateService
14{
15    /** Chemin absolu vers le fichier d'état */
16    private static string $path = '';
17
18    /** État chargé en mémoire (évite les lectures disque répétées) */
19    private static ?array $state = null;
20
21    // -------------------------------------------------------------------------
22    // API PUBLIQUE
23    // -------------------------------------------------------------------------
24
25    /**
26     * Lire une valeur
27     *
28     * @param  string $key
29     * @param  mixed  $default  Valeur retournée si la clé est absente
30     * @return mixed
31     */
32    public static function get(string $key, mixed $default = null): mixed
33    {
34        $state = self::load();
35
36        return array_key_exists($key, $state) ? $state[$key] : $default;
37    }
38
39    /**
40     * Écrire une valeur
41     *
42     * @param string $key
43     * @param mixed  $value  Toute valeur sérialisable en JSON
44     */
45    public static function set(string $key, mixed $value): void
46    {
47        $state       = self::load();
48        $state[$key] = $value;
49
50        self::$state = $state;
51        self::persist($state);
52    }
53
54    /**
55     * Supprimer une clé
56     */
57    public static function delete(string $key): void
58    {
59        $state = self::load();
60
61        if (!array_key_exists($key, $state)) {
62            return;
63        }
64
65        unset($state[$key]);
66
67        self::$state = $state;
68        self::persist($state);
69    }
70
71    /**
72     * Vérifier si une clé existe
73     */
74    public static function has(string $key): bool
75    {
76        return array_key_exists($key, self::load());
77    }
78
79    /**
80     * Retourner tout l'état (lecture seule)
81     */
82    public static function all(): array
83    {
84        return self::load();
85    }
86
87    /**
88     * Effacer complètement l'état (reset installation)
89     */
90    public static function flush(): void
91    {
92        self::$state = [];
93        self::persist([]);
94    }
95
96    // -------------------------------------------------------------------------
97    // GESTION DU FICHIER
98    // -------------------------------------------------------------------------
99
100    /**
101     * Charger l'état depuis le disque (ou depuis le cache mémoire)
102     */
103    private static function load(): array
104    {
105        // Déjà chargé dans cette requête
106        if (self::$state !== null) {
107            return self::$state;
108        }
109
110        $path = self::resolvePath();
111
112        // Fichier absent → état vide, on le crée
113        if (!file_exists($path)) {
114            self::persist([]);
115            self::$state = [];
116            return [];
117        }
118
119        $raw = file_get_contents($path);
120
121        if ($raw === false) {
122            throw new \RuntimeException(
123                "SystemStateService : impossible de lire le fichier d'état ({$path})."
124            );
125        }
126
127        $decoded = json_decode($raw, true);
128
129        if (json_last_error() !== JSON_ERROR_NONE) {
130            // Fichier corrompu → on repart d'un état vide
131            self::persist([]);
132            self::$state = [];
133            return [];
134        }
135
136        self::$state = $decoded;
137        return self::$state;
138    }
139
140    /**
141     * Écrire l'état sur le disque (JSON indenté pour lisibilité)
142     *
143     * @throws \RuntimeException Si l'écriture échoue
144     */
145    private static function persist(array $state): void
146    {
147        $path    = self::resolvePath();
148        $dir     = dirname($path);
149        $content = json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
150
151        // Créer le dossier si nécessaire
152        if (!is_dir($dir) && !mkdir($dir, 0755, true)) {
153            throw new \RuntimeException(
154                "SystemStateService : impossible de créer le dossier ({$dir})."
155            );
156        }
157
158        if (!is_writable($dir)) {
159            throw new \RuntimeException(
160                "SystemStateService : le dossier n'est pas accessible en écriture ({$dir})."
161            );
162        }
163
164        // Écriture atomique : fichier temporaire + rename
165        $tmp = $path . '.tmp.' . getmypid();
166
167        if (file_put_contents($tmp, $content, LOCK_EX) === false) {
168            throw new \RuntimeException(
169                "SystemStateService : échec de l'écriture du fichier temporaire ({$tmp})."
170            );
171        }
172
173        if (!rename($tmp, $path)) {
174            @unlink($tmp);
175            throw new \RuntimeException(
176                "SystemStateService : échec du rename atomique vers ({$path})."
177            );
178        }
179    }
180
181    /**
182     * Résoudre le chemin absolu du fichier d'état
183     * Utilise WRITEPATH de CI4, avec un fallback manuel.
184     */
185    private static function resolvePath(): string
186    {
187        if (self::$path !== '') {
188            return self::$path;
189        }
190
191        $base = defined('WRITEPATH')
192            ? rtrim(WRITEPATH, DIRECTORY_SEPARATOR)
193            : dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'writable';
194
195        self::$path = $base . DIRECTORY_SEPARATOR . 'install_state.json';
196
197        return self::$path;
198    }
199}