option('wait'), FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 3600]]); if ($wait === false) { $this->error('--wait deve ser um inteiro entre 0 e 3600 segundos.'); return self::INVALID; } $checks = [ 'PHP >= 8.4.1' => version_compare(PHP_VERSION, '8.4.1', '>='), 'Driver PostgreSQL activo' => extension_loaded('pdo_pgsql'), 'APP_KEY definida' => filled(config('app.key')), 'Base PostgreSQL configurada' => config('database.default') === 'pgsql', 'Cache de vistas gravável' => is_dir(config('view.compiled') ?: '') && is_writable(config('view.compiled') ?: ''), 'Sessões e logs graváveis' => is_writable(storage_path('framework/sessions')) && is_writable(storage_path('logs')), 'Dependências frontend instaladas' => is_file(base_path('node_modules/.bin/vp')) || is_file(base_path('node_modules/.bin/vp.cmd')), ]; if ($checks['Driver PostgreSQL activo'] && $checks['Base PostgreSQL configurada']) { $checks['Ligação à base de dados'] = $this->checkConnection($wait); } if ($checks['Ligação à base de dados'] ?? false) { try { $checks['Extensão PostGIS instalada'] = (bool) DB::selectOne("SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'postgis') AS installed")->installed; $checks['Tabelas principais instaladas'] = collect(['migrations', 'users', 'sectors', 'opportunities', 'sessions', 'cache', 'jobs']) ->every(fn (string $table): bool => Schema::hasTable($table)); } catch (Throwable) { $checks['Esquema da base de dados verificável'] = false; $this->warn('A ligação foi estabelecida, mas a verificação do esquema falhou. Verifique as permissões e a preparação QA.'); } } foreach ($checks as $label => $passed) { $this->line(($passed ? '[OK] ' : '[FALHA] ').$label); } if (in_array(false, $checks, true)) { $this->error('Arranque interrompido. Consulte deploy/local/README.md e corrija os itens acima.'); return self::FAILURE; } return self::SUCCESS; } private function checkConnection(int $wait): bool { $deadline = microtime(true) + $wait; while (true) { try { DB::selectOne('SELECT 1'); return true; } catch (Throwable $error) { [$reason, $retryable] = $this->connectionFailure($error); $remaining = $deadline - microtime(true); if (! $retryable || $remaining <= 0) { $this->warn($reason); if ($retryable) { $this->warn('Inicie Docker Desktop e execute ./deploy/local/start.ps1. Consulte os registos com docker compose --env-file .env -f deploy/local/compose.yaml logs --tail 40 postgres.'); } return false; } $this->line('[AGUARDA] '.$reason.' Nova tentativa; prazo restante: '.(int) ceil($remaining).' s.'); DB::purge(); Sleep::for(min(5, $remaining))->seconds(); } } } /** @return array{string, bool} */ private function connectionFailure(Throwable $error): array { // Inspect the chain internally; never print exception messages, DSNs or credentials. $messages = ''; do { $messages .= ' '.strtolower($error->getMessage()).' '.strtolower((string) $error->getCode()); $error = $error->getPrevious(); } while ($error !== null); if (str_contains($messages, '28p01') || str_contains($messages, '28000') || str_contains($messages, 'password authentication failed') || str_contains($messages, 'no pg_hba.conf entry')) { return ['Autenticação PostgreSQL recusada. Verifique as credenciais e as regras de acesso; aguardar não corrige este erro.', false]; } if (str_contains($messages, '3d000') || preg_match('/database .* does not exist/', $messages)) { return ['A base PostgreSQL configurada não existe. Verifique a configuração e a preparação QA.', false]; } if (str_contains($messages, '57p03') || str_contains($messages, 'recovery mode') || str_contains($messages, 'not yet accepting connections') || str_contains($messages, 'database system is starting up')) { return ['PostgreSQL em arranque ou recuperação; aguarde a conclusão sem remover o volume.', true]; } if (preg_match('/\b08[0-9a-z]{3}\b/', $messages) || str_contains($messages, 'connection refused') || str_contains($messages, '57p01') || str_contains($messages, '57p02')) { return ['PostgreSQL indisponível. Verifique se Docker Desktop e o contentor local estão iniciados.', true]; } return ['Falha PostgreSQL não transitória. Verifique a configuração e os registos do servidor.', false]; } }