$i->item_name, array_map(fn ($x) => $x[0], $items)); logger()->debug("Loaded doctor check items:\n\t" . implode("\n\t", $names)); } /** * Check all valid check items. * @return bool true if all checks passed, false otherwise */ public function checkAll(bool $interactive = true): bool { if ($interactive) { InteractiveTerm::notice('Starting doctor checks ...'); } foreach ($this->getValidCheckList() as $check) { if (!$this->checkItem($check, $interactive)) { return false; } } return true; } /** * Check a single check item. * * @param CheckItem|string $check The check item to be checked * @return bool True if the check passed or was fixed, false otherwise */ public function checkItem(CheckItem|string $check, bool $interactive = true): bool { if (is_string($check)) { $found = null; foreach (DoctorLoader::getDoctorItems() as $item) { if ($item[0]->item_name === $check) { $found = $item[0]; break; } } if ($found === null) { $this->output?->writeln("Check item '{$check}' not found."); return false; } $check = $found; } $prepend = $interactive ? ' - ' : ''; $this->output?->write("{$prepend}Checking {$check->item_name} ... "); // call check $result = call_user_func($check->callback); if ($result === null) { $this->output?->writeln('skipped'); return true; } if (!$result instanceof CheckResult) { $this->output?->writeln('Skipped due to invalid return value'); return true; } if ($result->isOK()) { /* @phpstan-ignore-next-line */ $this->output?->writeln($result->getMessage() ?? (string) ConsoleColor::green('✓')); return true; } $this->output?->writeln('' . $result->getMessage() . ''); // if the check item is not fixable, fail immediately if ($result->getFixItem() === '') { $this->output?->writeln('This check item can not be fixed automatically !'); return false; } // unknown fix item if (!DoctorLoader::getFixItem($result->getFixItem())) { $this->output?->writeln("Internal error: Unknown fix item: {$result->getFixItem()}"); return false; } // skip fix if ($this->auto_fix === FIX_POLICY_DIE) { $this->output?->writeln('Auto-fix is disabled. Please fix this issue manually.'); return false; } // prompt for fix if ($this->auto_fix === FIX_POLICY_PROMPT && !confirm('Do you want to try to fix this issue now?')) { $this->output?->writeln('You canceled fix.'); return false; } // perform fix InteractiveTerm::indicateProgress("Fixing {$result->getFixItem()} ... "); Shell::passthruCallback(function () { InteractiveTerm::advance(); }); // $this->output?->writeln("Fixing {$check->item_name} ... "); if ($this->emitFix($result->getFixItem(), $result->getFixParams())) { InteractiveTerm::finish('Fix applied successfully!'); return true; } InteractiveTerm::finish('Failed to apply fix!', false); return false; } private function emitFix(string $fix_item, array $fix_item_params = []): bool { keyboard_interrupt_register(function () { $this->output?->writeln('You cancelled fix'); }); try { return ApplicationContext::invoke(DoctorLoader::getFixItem($fix_item), $fix_item_params); } catch (SPCException $e) { $this->output?->writeln('Fix failed: ' . $e->getMessage() . ''); return false; } catch (\Throwable $e) { logger()->debug('Error: ' . $e->getMessage() . " at {$e->getFile()}:{$e->getLine()}\n" . $e->getTraceAsString()); $this->output?->writeln('Fix failed with an unexpected error: ' . $e->getMessage() . ''); return false; } finally { keyboard_interrupt_unregister(); } } /** * Get a list of valid check items for current environment. */ private function getValidCheckList(): iterable { foreach (DoctorLoader::getDoctorItems() as [$item, $optional]) { /* @var CheckItem $item */ // optional check /* @phpstan-ignore-next-line */ if (is_callable($optional) && !call_user_func($optional)) { continue; // skip this when the optional check is false } // limit_os check if ($item->limit_os !== null && $item->limit_os !== PHP_OS_FAMILY) { continue; } // skipped items by env $skip_items = array_filter(explode(',', getenv('SPC_SKIP_DOCTOR_CHECK_ITEMS') ?: '')); if (in_array($item->item_name, $skip_items)) { continue; // skip this item } yield $item; } } }