checkPrerequisites(); $allOk = collect($checks)->every(fn ($c) => $c['ok'] || ($c['optional'] ?? false)); return view('setup.index', compact('checks', 'allOk')); } public function database(Request $request) { return view('setup.database', [ 'saved' => $request->session()->get('setup.database', []), ]); } public function saveDatabase(Request $request) { $data = $request->validate([ 'driver' => 'required|in:pgsql,mysql', 'host' => 'required|string|max:255', 'port' => 'required|integer|min:1|max:65535', 'database' => 'required|string|max:255', 'username' => 'required|string|max:255', 'password' => 'nullable|string|max:255', ]); $request->session()->put('setup.database', $data); return redirect()->route('setup.application'); } public function testDatabase(Request $request) { $data = $request->validate([ 'driver' => 'required|in:pgsql,mysql', 'host' => 'required|string', 'port' => 'required|integer', 'database' => 'required|string', 'username' => 'required|string', 'password' => 'nullable|string', ]); try { $dsn = $data['driver'] === 'pgsql' ? "pgsql:host={$data['host']};port={$data['port']};dbname={$data['database']}" : "mysql:host={$data['host']};port={$data['port']};dbname={$data['database']};charset=utf8mb4"; new PDO($dsn, $data['username'], $data['password'] ?? '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 5, ]); return response()->json(['ok' => true, 'message' => 'Connexion réussie !']); } catch (PDOException $e) { return response()->json(['ok' => false, 'message' => $e->getMessage()], 422); } } public function application(Request $request) { if (! $request->session()->has('setup.database')) { return redirect()->route('setup.database'); } return view('setup.application', [ 'saved' => $request->session()->get('setup.application', [ 'app_name' => 'MesRelevés', 'app_url' => url('/'), 'registration_enabled' => false, ]), ]); } public function saveApplication(Request $request) { $data = $request->validate([ 'app_name' => 'required|string|max:100', 'app_url' => 'required|url|max:255', ]); $data['registration_enabled'] = $request->boolean('registration_enabled'); $request->session()->put('setup.application', $data); return redirect()->route('setup.admin'); } public function admin(Request $request) { if (! $request->session()->has('setup.database')) { return redirect()->route('setup.database'); } if (! $request->session()->has('setup.application')) { return redirect()->route('setup.application'); } return view('setup.admin'); } public function install(Request $request) { $adminData = $request->validate([ 'name' => 'required|string|max:255', 'email' => 'required|email|max:255', 'password' => 'required|string|min:8|confirmed', ]); $dbData = $request->session()->get('setup.database'); $appData = $request->session()->get('setup.application'); if (! $dbData || ! $appData) { return redirect()->route('setup.index') ->withErrors(['error' => 'Données manquantes, veuillez recommencer.']); } $steps = []; $success = true; // 1. Écriture du .env try { $this->writeEnv($dbData, $appData); $steps[] = ['ok' => true, 'label' => 'Écriture du fichier de configuration (.env)']; } catch (\Exception $e) { $steps[] = ['ok' => false, 'label' => 'Écriture du fichier de configuration (.env)', 'error' => $e->getMessage()]; $success = false; } // 2. Génération de la clé APP_KEY — directement en PHP, sans passer par key:generate. // // Artisan key:generate remplace APP_KEY= dans le .env grâce à un // pattern regex. Mais writeEnv() vient d'écrire APP_KEY= (vide) alors qu'en mémoire // la clé est celle de l'auto-création (TEMP_KEY) → le pattern ne matche pas → la // clé reste vide dans le .env et la config:cache en hérite. // Solution : générer la clé nous-mêmes, l'écrire directement dans le .env, et la // propager en mémoire + env OS dès maintenant. $appKey = null; if ($success) { try { $appKey = 'base64:' . base64_encode(random_bytes(32)); $envPath = base_path('.env'); $env = file_get_contents($envPath); $env = preg_replace('/^APP_KEY=.*/m', 'APP_KEY=' . $appKey, $env); file_put_contents($envPath, $env); config(['app.key' => $appKey]); $steps[] = ['ok' => true, 'label' => 'Génération de la clé de chiffrement (APP_KEY)']; } catch (\Exception $e) { $steps[] = ['ok' => false, 'label' => 'Génération de la clé de chiffrement (APP_KEY)', 'error' => $e->getMessage()]; $success = false; } } // 2b. Reconfiguration de la connexion BDD — processus courant ET sous-processus. // // putenv() écrase l'env OS hérité au boot (pgsql + TEMP_KEY) pour que tous les // sous-processus futurs (config:cache interne à optimize…) reçoivent les bonnes // valeurs. config() + DB::purge() reconfigure le processus courant en mémoire. if ($success) { putenv("APP_KEY={$appKey}"); putenv("DB_CONNECTION={$dbData['driver']}"); putenv("DB_HOST={$dbData['host']}"); putenv("DB_PORT={$dbData['port']}"); putenv("DB_DATABASE={$dbData['database']}"); putenv("DB_USERNAME={$dbData['username']}"); putenv('DB_PASSWORD=' . ($dbData['password'] ?? '')); $connConfig = $dbData['driver'] === 'pgsql' ? ['driver' => 'pgsql', 'host' => $dbData['host'], 'port' => (int) $dbData['port'], 'database' => $dbData['database'], 'username' => $dbData['username'], 'password' => $dbData['password'] ?? '', 'charset' => 'utf8', 'prefix' => '', 'schema' => 'public', 'sslmode' => 'prefer'] : ['driver' => 'mysql', 'host' => $dbData['host'], 'port' => (int) $dbData['port'], 'database' => $dbData['database'], 'username' => $dbData['username'], 'password' => $dbData['password'] ?? '', 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_unicode_ci', 'prefix' => '', 'strict' => true]; config([ 'database.default' => $dbData['driver'], "database.connections.{$dbData['driver']}" => $connConfig, ]); DB::purge($dbData['driver']); } // 3. Migrations (dans le processus courant, config BDD déjà écrasée ci-dessus) if ($success) { try { $exitCode = Artisan::call('migrate', ['--force' => true]); $out = trim(Artisan::output()); $ok = $exitCode === 0; $steps[] = ['ok' => $ok, 'label' => 'Migration de la base de données', 'error' => $ok ? null : $out]; if (! $ok) $success = false; } catch (\Exception $e) { $steps[] = ['ok' => false, 'label' => 'Migration de la base de données', 'error' => $e->getMessage()]; $success = false; } } // 4. Création du compte administrateur if ($success) { try { $this->createAdminUser($dbData, $adminData); $steps[] = ['ok' => true, 'label' => 'Création du compte administrateur']; } catch (\Exception $e) { $steps[] = ['ok' => false, 'label' => 'Création du compte administrateur', 'error' => $e->getMessage()]; $success = false; } } // 4b. Lien de stockage public (symlink public/storage → storage/app/public) // Non bloquant : l'installation continue même si le serveur interdit les symlinks. if ($success) { try { \Illuminate\Support\Facades\Artisan::call('storage:link'); $steps[] = ['ok' => true, 'label' => 'Lien de stockage public créé']; } catch (\Exception $e) { $steps[] = ['ok' => false, 'label' => 'Lien de stockage public (non bloquant — créez-le manuellement via « Administration → Paramètres »)', 'error' => $e->getMessage()]; } } // 5. Paramètres du site if ($success) { try { $dir = storage_path('app'); if (! is_dir($dir)) mkdir($dir, 0755, true); $settings = ['registration_enabled' => (bool) ($appData['registration_enabled'] ?? false)]; file_put_contents(storage_path('app/site_settings.json'), json_encode($settings, JSON_PRETTY_PRINT)); $steps[] = ['ok' => true, 'label' => 'Paramètres du site enregistrés']; } catch (\Exception $e) { $steps[] = ['ok' => false, 'label' => 'Paramètres du site', 'error' => $e->getMessage()]; } } // 6. Nettoyage des caches // optimize:clear supprime tout cache résiduel (config, routes, vues, events). // On n'appelle PAS optimize : config:cache re-boostrappe l'app depuis bootstrap/app.php // dans un contexte qui peut ne pas avoir accès à notre APP_KEY via putenv, ce qui // provoque MissingAppKeyException. Laravel reconstruit ses caches à la première // requête — pas besoin de les préchauffer pendant l'installation. if ($success) { Artisan::call('optimize:clear'); } // 7. Marquage installation if ($success) { file_put_contents(storage_path('installed'), date('Y-m-d H:i:s') . PHP_EOL); $steps[] = ['ok' => true, 'label' => 'Application marquée comme installée']; $request->session()->forget('setup'); } return view('setup.complete', compact('steps', 'success')); } // ─── Private ───────────────────────────────────────────────────────────── private function checkPrerequisites(): array { $checks = []; $checks[] = [ 'label' => 'PHP 8.2 ou supérieur', 'ok' => PHP_VERSION_ID >= 80200, 'value' => PHP_VERSION, 'optional' => false, ]; $extensions = [ 'pdo' => false, 'mbstring' => false, 'tokenizer' => false, 'xml' => false, 'ctype' => false, 'json' => false, 'bcmath' => false, 'openssl' => false, 'fileinfo' => false, 'zip' => false, 'pdo_pgsql' => true, 'pdo_mysql' => true, ]; foreach ($extensions as $ext => $optional) { $loaded = extension_loaded($ext); $checks[] = [ 'label' => "Extension PHP : {$ext}", 'ok' => $loaded, 'value' => $loaded ? 'Présente' : 'Manquante', 'optional' => $optional, ]; } $dirs = [ 'storage/' => storage_path(), 'bootstrap/cache/' => base_path('bootstrap/cache'), 'Racine (écriture .env)' => base_path(), ]; foreach ($dirs as $label => $path) { $writable = is_writable($path); $checks[] = [ 'label' => "Répertoire accessible en écriture : {$label}", 'ok' => $writable, 'value' => $writable ? 'OK' : 'Non accessible', 'optional' => false, ]; } return $checks; } private function writeEnv(array $db, array $app): void { $envPath = base_path('.env'); $envExamplePath = base_path('.env.example'); $overrides = [ 'APP_NAME' => '"' . str_replace('"', '\\"', $app['app_name']) . '"', 'APP_URL' => rtrim($app['app_url'], '/'), 'APP_ENV' => 'production', 'APP_DEBUG' => 'false', 'APP_KEY' => '', 'DB_CONNECTION' => $db['driver'], 'DB_HOST' => $db['host'], 'DB_PORT' => (string) $db['port'], 'DB_DATABASE' => $db['database'], 'DB_USERNAME' => $db['username'], 'DB_PASSWORD' => '"' . str_replace('"', '\\"', $db['password'] ?? '') . '"', 'SESSION_DRIVER' => 'database', 'CACHE_STORE' => 'database', 'QUEUE_CONNECTION' => 'database', ]; $env = file_exists($envExamplePath) ? file_get_contents($envExamplePath) : ''; foreach ($overrides as $key => $value) { $pattern = '/^' . preg_quote($key, '/') . '=.*/m'; if (preg_match($pattern, $env)) { $env = preg_replace($pattern, "{$key}={$value}", $env); } else { $env .= "\n{$key}={$value}"; } } if (file_put_contents($envPath, $env) === false) { throw new \RuntimeException("Impossible d'écrire le fichier .env — vérifiez les permissions du dossier."); } } private function createAdminUser(array $db, array $admin): void { $dsn = $db['driver'] === 'pgsql' ? "pgsql:host={$db['host']};port={$db['port']};dbname={$db['database']}" : "mysql:host={$db['host']};port={$db['port']};dbname={$db['database']};charset=utf8mb4"; $pdo = new PDO($dsn, $db['username'], $db['password'] ?? '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, ]); $isActive = $db['driver'] === 'pgsql' ? 'true' : '1'; $stmt = $pdo->prepare( "INSERT INTO users (name, email, password, role, is_active, email_verified_at, created_at, updated_at) VALUES (:name, :email, :password, 'admin', {$isActive}, NOW(), NOW(), NOW())" ); $stmt->execute([ ':name' => $admin['name'], ':email' => $admin['email'], ':password' => Hash::make($admin['password']), ]); } private function artisanRun(string $artisan, string $command): array { $out = []; $code = 0; exec("{$artisan} {$command} 2>&1", $out, $code); return [$code === 0, implode("\n", $out)]; } private function phpBinary(): string { $bin = PHP_BINARY; return is_executable($bin) ? $bin : 'php'; } }