Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
73.68% covered (warning)
73.68%
56 / 76
22.22% covered (danger)
22.22%
2 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
CommandInputProcessor
73.68% covered (warning)
73.68%
56 / 76
22.22% covered (danger)
22.22%
2 / 9
76.70
0.00% covered (danger)
0.00%
0 / 1
 process
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 processInputContext
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
7
 inferCommandName
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
5.93
 extractProvidedArguments
47.37% covered (danger)
47.37%
9 / 19
0.00% covered (danger)
0.00%
0 / 1
28.64
 resolveArguments
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
3.19
 extractProvidedOptions
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
4.02
 resolveDefinition
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 resolveArrayParameters
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 optionTokens
57.14% covered (warning)
57.14%
4 / 7
0.00% covered (danger)
0.00%
0 / 1
6.97
1<?php
2
3declare(strict_types=1);
4
5/**
6 * Fast Forward Development Tools for PHP projects.
7 *
8 * This file is part of fast-forward/dev-tools project.
9 *
10 * @author   Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
11 * @license  https://opensource.org/licenses/MIT MIT License
12 *
13 * @see      https://github.com/php-fast-forward/
14 * @see      https://github.com/php-fast-forward/dev-tools
15 * @see      https://github.com/php-fast-forward/dev-tools/issues
16 * @see      https://php-fast-forward.github.io/dev-tools/
17 * @see      https://datatracker.ietf.org/doc/html/rfc2119
18 */
19
20namespace FastForward\DevTools\Console\Logger\Processor;
21
22use ReflectionProperty;
23use Symfony\Component\Console\Input\ArrayInput;
24use Symfony\Component\Console\Input\Input;
25use Symfony\Component\Console\Input\InputDefinition;
26use Symfony\Component\Console\Input\InputInterface;
27use Symfony\Component\Console\Input\InputOption;
28use Throwable;
29
30/**
31 * Expands command input instances into structured context entries.
32 */
33 class CommandInputProcessor implements ContextProcessorInterface
34{
35    /**
36     * @param array<string, mixed> $context
37     *
38     * @return array<string, mixed>
39     */
40    public function process(array $context): array
41    {
42        foreach ($context as $key => $value) {
43            if (! $value instanceof InputInterface) {
44                continue;
45            }
46
47            $context = $this->processInputContext($context, $key, $value);
48        }
49
50        return $context;
51    }
52
53    /**
54     * @param array<string, mixed> $context
55     * @param string|int $key
56     * @param InputInterface $input
57     *
58     * @return array<string, mixed>
59     */
60    private function processInputContext(array $context, string|int $key, InputInterface $input): array
61    {
62        unset($context[$key]);
63
64        $arguments = $this->extractProvidedArguments($input);
65        $command = $this->inferCommandName($context, $input, $arguments);
66
67        if (null !== $command && ! \array_key_exists('command', $context)) {
68            $context['command'] = $command;
69        }
70
71        unset($arguments['command']);
72
73        if ([] !== $arguments && ! \array_key_exists('arguments', $context)) {
74            $context['arguments'] = $arguments;
75        }
76
77        $options = $this->extractProvidedOptions($input);
78        if ([] !== $options && ! \array_key_exists('options', $context)) {
79            $context['options'] = $options;
80        }
81
82        return $context;
83    }
84
85    /**
86     * @param array<string, mixed> $context
87     * @param array<string, mixed> $arguments
88     * @param InputInterface $input
89     *
90     * @return string|null
91     */
92    private function inferCommandName(array $context, InputInterface $input, array $arguments): ?string
93    {
94        if (\array_key_exists('command', $context)) {
95            return null;
96        }
97
98        if (\array_key_exists('command', $arguments) && \is_string($arguments['command'])) {
99            return $arguments['command'];
100        }
101
102        $command = $input->getFirstArgument();
103
104        return \is_string($command) ? $command : null;
105    }
106
107    /**
108     * @param InputInterface $input
109     *
110     * @return array<string, mixed>
111     */
112    private function extractProvidedArguments(InputInterface $input): array
113    {
114        $arguments = [];
115        $arrayParameters = $this->resolveArrayParameters($input);
116
117        foreach ($this->resolveArguments($input) as $name => $value) {
118            if (null === $value) {
119                continue;
120            }
121
122            if (\is_array($value)) {
123                if ([] === $value) {
124                    continue;
125                }
126
127                $providedValues = array_values(array_filter(
128                    $value,
129                    fn(mixed $item): bool => \is_scalar($item) && $input->hasParameterOption((string) $item, true),
130                ));
131
132                if ([] !== $providedValues) {
133                    $arguments[$name] = $providedValues;
134                }
135
136                continue;
137            }
138
139            if (
140                (\is_array($arrayParameters) && \array_key_exists($name, $arrayParameters))
141                || (\is_scalar($value) && $input->hasParameterOption((string) $value, true))
142            ) {
143                $arguments[$name] = $value;
144            }
145        }
146
147        return $arguments;
148    }
149
150    /**
151     * @param InputInterface $input
152     *
153     * @return array<string, mixed>
154     */
155    private function resolveArguments(InputInterface $input): array
156    {
157        try {
158            return $input->getArguments();
159        } catch (Throwable) {
160            return [];
161        }
162    }
163
164    /**
165     * @param InputInterface $input
166     *
167     * @return array<string, mixed>
168     */
169    private function extractProvidedOptions(InputInterface $input): array
170    {
171        $definition = $this->resolveDefinition($input);
172
173        if (! $definition instanceof InputDefinition) {
174            return [];
175        }
176
177        $options = [];
178
179        foreach ($definition->getOptions() as $option) {
180            $tokens = $this->optionTokens($option);
181
182            if (! $input->hasParameterOption($tokens, true)) {
183                continue;
184            }
185
186            $options[$option->getName()] = $input->getOption($option->getName());
187        }
188
189        return $options;
190    }
191
192    /**
193     * @param InputInterface $input
194     *
195     * @return InputDefinition|null
196     */
197    private function resolveDefinition(InputInterface $input): ?InputDefinition
198    {
199        if (! $input instanceof Input) {
200            return null;
201        }
202
203        static $property;
204
205        if (! $property instanceof ReflectionProperty) {
206            $property = new ReflectionProperty(Input::class, 'definition');
207        }
208
209        /** @var InputDefinition $definition */
210        $definition = $property->getValue($input);
211
212        return $definition;
213    }
214
215    /**
216     * @param InputInterface $input
217     *
218     * @return array<string|int, mixed>|null
219     */
220    private function resolveArrayParameters(InputInterface $input): ?array
221    {
222        if (! $input instanceof ArrayInput) {
223            return null;
224        }
225
226        static $property;
227
228        if (! $property instanceof ReflectionProperty) {
229            $property = new ReflectionProperty(ArrayInput::class, 'parameters');
230        }
231
232        /** @var array<string|int, mixed> $parameters */
233        $parameters = $property->getValue($input);
234
235        return $parameters;
236    }
237
238    /**
239     * @param InputOption $option
240     *
241     * @return list<string>
242     */
243    private function optionTokens(InputOption $option): array
244    {
245        $tokens = ['--' . $option->getName()];
246        $shortcut = $option->getShortcut();
247
248        if (null !== $shortcut && '' !== $shortcut) {
249            foreach (explode('|', $shortcut) as $alias) {
250                if ('' !== $alias) {
251                    $tokens[] = '-' . $alias;
252                }
253            }
254        }
255
256        return $tokens;
257    }
258}