$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) {
$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
if ($optional !== null && !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;
}
}
}