*/ private array $results = []; public function handle(): int { $this->info('🔍 در حال اجرای بررسی پیش‌از-دیپلوی IFNEX...'); $this->newLine(); $this->checkEnv(); $this->checkSecrets(); $this->checkPayment(); $this->checkDatabase(); $this->checkCors(); $this->checkStoragePermissions(); $this->checkPhpExtensions(); if (! $this->option('skip-db')) { $this->checkCurrenciesSeeded(); } else { $this->addResult('currencies seeded', 'skipped (--skip-db)', true); } $this->renderReport(); $failed = collect($this->results)->filter(fn ($r) => ! $r['passed'])->count(); if ($failed > 0) { $this->newLine(); $this->error("❌ {$failed} مورد ناموفق — دیپلوی را ادامه ندهید تا رفع شوند."); return Command::FAILURE; } $this->newLine(); $this->info('✅ همهٔ بررسی‌ها سبز هستند. آمادهٔ migrate و دیپلوی.'); return Command::SUCCESS; } // ─── محیط ─────────────────────────────────────────────── private function checkEnv(): void { $env = config('app.env'); $debug = config('app.debug'); $key = config('app.key'); $this->addResult( 'APP_ENV = production', "APP_ENV = {$env}", $env === 'production' ); $this->addResult( 'APP_DEBUG = false', 'APP_DEBUG = ' . ($debug ? 'true' : 'false'), $debug === false ); $this->addResult( 'APP_KEY تنظیم شده', $key ? 'present (' . strlen($key) . ' chars)' : 'MISSING', !empty($key) ); } // ─── کلیدهای امنیتی ───────────────────────────────────── private function checkSecrets(): void { $forbidden = ['change-me', 'change-this-secret-key', '', null]; $apiKey = config('ifnex.api_key'); $this->addResult( 'IFNEX_API_KEY چرخش داده شده', in_array($apiKey, $forbidden, true) ? 'STILL DEFAULT / EMPTY' : 'rotated (' . strlen($apiKey) . ' chars)', !in_array($apiKey, $forbidden, true) ); $bridgeKey = config('ifnex.bridge_api_key'); $this->addResult( 'IFNEX_BRIDGE_API_KEY چرخش داده شده', in_array($bridgeKey, $forbidden, true) ? 'STILL DEFAULT / EMPTY' : 'rotated (' . strlen($bridgeKey) . ' chars)', !in_array($bridgeKey, $forbidden, true) ); } // ─── درگاه پرداخت ─────────────────────────────────────── private function checkPayment(): void { $sandbox = (bool) config('ifnex.zarinpal.sandbox'); $merchant = (string) config('ifnex.zarinpal.merchant_id'); $fakeValues = ['fake-merchant-id-for-testing', '', 'YOUR_REAL_MERCHANT_ID', 'xxxx-xxxx-xxxx-xxxx']; $this->addResult( 'ZARINPAL_SANDBOX = false', 'ZARINPAL_SANDBOX = ' . ($sandbox ? 'true' : 'false'), $sandbox === false ); $merchantOk = ! in_array($merchant, $fakeValues, true) && strlen($merchant) >= 8; $this->addResult( 'ZARINPAL_MERCHANT_ID واقعی', in_array($merchant, $fakeValues, true) ? "STILL PLACEHOLDER ({$merchant})" : 'looks real (' . strlen($merchant) . ' chars)', $merchantOk ); } // ─── دیتابیس ──────────────────────────────────────────── private function checkDatabase(): void { $conn = config('database.default'); // مقدار مورد انتظار از config خوانده می‌شود (پیش‌فرض mysql). // در محیط تست می‌توان آن را روی sqlite گذاشت تا بدون اتصال واقعی به MySQL تست شود. $expected = config('ifnex.preflight.expected_db_connection', 'mysql'); $this->addResult( "DB_CONNECTION = {$expected}", "DB_CONNECTION = {$conn}", $conn === $expected ); } // ─── CORS ─────────────────────────────────────────────── private function checkCors(): void { $allowed = config('ifnex.cors_allowed_origins', []); $allowed = is_array($allowed) ? $allowed : [$allowed]; $forbiddenFragments = ['localhost', '127.0.0.1', '0.0.0.0']; $hasLocal = false; foreach ($allowed as $origin) { foreach ($forbiddenFragments as $frag) { if (str_contains((string) $origin, $frag)) { $hasLocal = true; break 2; } } } $display = implode(', ', $allowed); $this->addResult( 'CORS_ALLOWED_ORIGINS بدون localhost', "CORS_ALLOWED_ORIGINS = {$display}", ! $hasLocal && count($allowed) >= 1 ); } // ─── دسترسی فایل ──────────────────────────────────────── private function checkStoragePermissions(): void { $paths = [ base_path('storage'), base_path('storage/logs'), base_path('storage/app'), base_path('storage/framework'), base_path('bootstrap/cache'), ]; foreach ($paths as $path) { $rel = ltrim(str_replace(base_path(), '', $path), '/'); $exists = file_exists($path); $writable = $exists && is_writable($path); $this->addResult( "writable: {$rel}", $exists ? ($writable ? 'writable' : 'NOT writable') : 'MISSING', $writable ); } } // ─── اکستنشن‌های PHP ───────────────────────────────────── private function checkPhpExtensions(): void { // فقط اکستنشن‌های مستقیماً لازم (مطابق DEPLOYMENT_HANDOFF.md §۱) $required = ['pdo_mysql', 'mbstring', 'xml', 'gd', 'zip', 'fileinfo', 'curl', 'ctype', 'tokenizer', 'json']; foreach ($required as $ext) { $loaded = extension_loaded($ext); $this->addResult( "ext: {$ext}", $loaded ? 'loaded' : 'MISSING', $loaded ); } } // ─── ارزها ────────────────────────────────────────────── private function checkCurrenciesSeeded(): void { $required = ['USD', 'AED', 'EUR', 'CNY', 'IRR']; try { $existing = DB::table('currencies')->whereIn('code', $required)->pluck('code')->all(); } catch (\Throwable $e) { $this->addResult( 'currencies table seeded', 'CANNOT QUERY — ' . $e->getMessage(), false ); return; } $missing = array_diff($required, $existing); $this->addResult( 'currencies table seeded (USD/AED/EUR/CNY/IRR)', empty($missing) ? 'all 5 present' : 'MISSING: ' . implode(',', $missing), empty($missing) ); } // ─── Helpers ──────────────────────────────────────────── private function addResult(string $check, string $result, bool $passed): void { $this->results[] = compact('check', 'result', 'passed'); } private function renderReport(): void { $this->table( ['Check', 'Value', 'Status'], collect($this->results)->map(fn ($r) => [ $r['check'], $r['result'], $r['passed'] ? '✓ PASS' : '✗ FAIL', ])->all() ); } }