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