packages/deriver/src/Command/DeriveCommand.php
1<?php
2
3declare(strict_types=1);
4
5namespace Deriver\Command;
6
7use Deriver\Configuration\ReportFormat;
8use Deriver\Deriver;
9use Deriver\Deriver\DeriverConfig;
10use Deriver\Exception\CollectorException;
11use Deriver\Exception\ConfigurationException;
12use Deriver\Exception\InvalidInputException;
13use Deriver\Exception\ParserException;
14use Deriver\Exception\ReportingException;
15use Deriver\Exception\ResolutionException;
16use Deriver\Finder;
17use Deriver\Reporter\JsonReporter;
18use Deriver\Reporter\TextReporter;
19use Override;
20use Symfony\Component\Console\Command\Command;
21use Symfony\Component\Console\Input\InputArgument;
22use Symfony\Component\Console\Input\InputInterface;
23use Symfony\Component\Console\Input\InputOption;
24use Symfony\Component\Console\Output\ConsoleOutputInterface;
25use Symfony\Component\Console\Output\OutputInterface;
26
27/**
28 * Collects selected paths, derives one selector, and writes a report to standard output.
29 * @visibility namespace
30 */
31final class DeriveCommand extends Command
32{
33 /**
34 * Registers the single command exposed by the deliver executable.
35 */
36 public function __construct()
37 {
38 parent::__construct('deliver');
39 }
40
41 /**
42 * Defines source selection, configuration, and report options.
43 */
44 #[Override]
45 protected function configure(): void
46 {
47 $this->setDescription('Derive candidate values from PHP source without executing it.')
48 ->addArgument('query', InputArgument::REQUIRED, 'Selector, optionally prefixed by callers: entry -> ... -> App\\answer#return.')
49 ->addOption('config', 'c', InputOption::VALUE_REQUIRED, 'Load a YAML configuration file.')
50 ->addOption('path', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Search root; repeat for multiple directories. Defaults to the current directory.')
51 ->addOption('include', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Root-relative include wildcard; repeat to combine patterns. Defaults to **/*.php.')
52 ->addOption('exclude', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Root-relative exclude wildcard; exclusions win.')
53 ->addOption('php-version', null, InputOption::VALUE_REQUIRED, 'Target PHP major.minor version; defaults to the library setting.')
54 ->addOption('format', null, InputOption::VALUE_REQUIRED, 'Report format: text or json. Defaults to text.');
55 }
56
57 /**
58 * Uses exit code 0 for completed exploration, 1 for interruption or execution failure, and 2 for invalid input.
59 */
60 #[Override]
61 protected function execute(InputInterface $input, OutputInterface $output): int
62 {
63 try {
64 $query = $input->getArgument('query');
65 if (!is_string($query) || $query === '') {
66 throw new InvalidInputException('A nonempty selector is required.');
67 }
68 $configuration = (new Input())->configuration($input);
69 $finder = new Finder();
70 foreach ($configuration->paths as $path) {
71 $finder->in($path);
72 }
73 foreach ($configuration->include as $pattern) {
74 $finder->include($pattern);
75 }
76 foreach ($configuration->exclude as $pattern) {
77 $finder->exclude($pattern);
78 }
79 $files = $finder->files();
80 if ($files === []) {
81 throw new InvalidInputException('No source files matched the selected paths and patterns.');
82 }
83 $settings = new DeriverConfig($configuration->phpVersion ?? DeriverConfig::default()->phpVersion, $configuration->integerBits, $configuration->floatPrecision, $configuration->environment, $configuration->standardEvaluators);
84 $candidates = Deriver::prepare($files, $settings)->derive($query);
85 $reporter = match ($configuration->format) {
86 ReportFormat::Text => new TextReporter(),
87 ReportFormat::Json => new JsonReporter(),
88 };
89 $output->write($reporter->report($candidates), false, OutputInterface::OUTPUT_RAW);
90 return $candidates->interrupted ? self::FAILURE : self::SUCCESS;
91 } catch (ConfigurationException | InvalidInputException $error) {
92 $status = self::INVALID;
93 } catch (CollectorException | ParserException | ResolutionException | ReportingException $error) {
94 $status = self::FAILURE;
95 }
96 $errors = $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output;
97 $errors->writeln('Error: ' . $error->getMessage(), OutputInterface::OUTPUT_RAW);
98 return $status;
99 }
100}
101