addOption('debug', null, null, '(deprecated) Enable debug mode'); $this->addOption('no-motd', null, null, 'Disable motd'); } public function initialize(InputInterface $input, OutputInterface $output): void { $this->input = $input; $this->output = $output; // Bind command context to ApplicationContext ApplicationContext::bindCommandContext($input, $output); if ($input->getOption('no-motd')) { $this->no_motd = true; } set_error_handler(static function ($error_no, $error_msg, $error_file, $error_line) { // Respect the @ suppression operator (error_reporting() returns 0 when @ is used) if (error_reporting() === 0) { return true; } $tips = [ E_WARNING => ['PHP Warning: ', 'warning'], E_NOTICE => ['PHP Notice: ', 'notice'], E_USER_ERROR => ['PHP Error: ', 'error'], E_USER_WARNING => ['PHP Warning: ', 'warning'], E_USER_NOTICE => ['PHP Notice: ', 'notice'], E_RECOVERABLE_ERROR => ['PHP Recoverable Error: ', 'error'], E_DEPRECATED => ['PHP Deprecated: ', 'notice'], E_USER_DEPRECATED => ['PHP User Deprecated: ', 'notice'], ]; $level_tip = $tips[$error_no] ?? ['PHP Unknown: ', 'error']; $error = $level_tip[0] . $error_msg . ' in ' . $error_file . ' on ' . $error_line; logger()->{$level_tip[1]}($error); // 如果 return false 则错误会继续递交给 PHP 标准错误处理 return true; }); $version = $this->getVersionWithCommit(); if (!$this->no_motd) { $str = str_replace('{version}', '' . ConsoleColor::none("v{$version}"), '' . ConsoleColor::magenta(self::$motd)); $this->output->writeln($this->input->getOption('no-ansi') ? strip_ansi_colors($str) : $str); } } abstract public function handle(): int; protected function execute(InputInterface $input, OutputInterface $output): int { try { // handle verbose option $level = match ($this->output->getVerbosity()) { OutputInterface::VERBOSITY_VERBOSE => 'info', OutputInterface::VERBOSITY_VERY_VERBOSE, OutputInterface::VERBOSITY_DEBUG => 'debug', default => 'warning', }; $isDebug = false; // if '--debug' is set, override log level to debug if ($this->input->getOption('debug')) { $level = 'debug'; logger()->warning('The --debug option is deprecated and will be removed in future versions. Please use -vv or -vvv to enable debug mode.'); $this->output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); $isDebug = true; } logger()->setLevel($level); // ansi if ($this->input->getOption('no-ansi')) { logger()->setDecorated(false); } // Set debug mode in ApplicationContext $isDebug = $isDebug ?: $this->output->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG; ApplicationContext::setDebug($isDebug); // show raw argv list for logger()->debug logger()->debug('argv: ' . implode(' ', $_SERVER['argv'])); return $this->handle(); } /* @noinspection PhpRedundantCatchClauseInspection */ catch (SPCException $e) { // Handle SPCException and log it return ExceptionHandler::handleSPCException($e); } catch (\Throwable $e) { // Handle any other exceptions return ExceptionHandler::handleDefaultException($e); } } /** * Warn the user if doctor has not been run (or is outdated). * Set SPC_SKIP_DOCTOR_CHECK=1 to suppress. */ protected function checkDoctorCache(): void { if (getenv('SPC_SKIP_DOCTOR_CHECK') || Doctor::isHealthy()) { return; } $this->output->writeln(''); $this->output->writeln('[WARNING] Please run `spc doctor` first to verify your build environment.'); $this->output->writeln(''); sleep(2); } protected function getOption(string $name): mixed { return $this->input->getOption($name); } protected function getArgument(string $name): mixed { return $this->input->getArgument($name); } /** * Get version string with git commit short ID if available. */ private function getVersionWithCommit(): string { $version = $this->getApplication()->getVersion(); // Don't show commit ID when running in phar if (\Phar::running()) { $stable = file_exists(ROOT_DIR . '/src/.release') ? 'stable' : 'unstable'; return "{$version} ({$stable})"; } $commitId = $this->getGitCommitShortId(); if ($commitId) { return "{$version} ({$commitId})"; } return $version; } /** * Get git commit short ID without executing git command. */ private function getGitCommitShortId(): ?string { try { $gitDir = ROOT_DIR . '/.git'; if (!is_dir($gitDir)) { return null; } $headFile = $gitDir . '/HEAD'; if (!file_exists($headFile)) { return null; } $head = trim(file_get_contents($headFile)); // If HEAD contains 'ref:', it's a branch reference if (str_starts_with($head, 'ref: ')) { $ref = substr($head, 5); $refFile = $gitDir . '/' . $ref; if (file_exists($refFile)) { $commit = trim(file_get_contents($refFile)); return substr($commit, 0, 7); } } else { // HEAD contains the commit hash directly (detached HEAD) return substr($head, 0, 7); } } catch (\Throwable) { // Silently fail if we can't read git info } return null; } }