Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
70.89% covered (warning)
70.89%
56 / 79
22.22% covered (danger)
22.22%
2 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
CommandInputProcessor
70.89% covered (warning)
70.89%
56 / 79
22.22% covered (danger)
22.22%
2 / 9
88.63
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
40.91% covered (danger)
40.91%
9 / 22
0.00% covered (danger)
0.00%
0 / 1
35.97
 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    private function inferCommandName(array $context, InputInterface $input, array $arguments): ?string
91    {
92        if (\array_key_exists('command', $context)) {
93            return null;
94        }
95
96        if (\array_key_exists('command', $arguments) && \is_string($arguments['command'])) {
97            return $arguments['command'];
98        }
99
100        $command = $input->getFirstArgument();
101
102        return \is_string($command) ? $command : null;
103    }
104
105    /**
106     * @param InputInterface $input
107     *
108     * @return array<string, mixed>
109     */
110    private function extractProvidedArguments(InputInterface $input): array
111    {
112        $arguments = [];
113        $arrayParameters = $this->resolveArrayParameters($input);
114
115        foreach ($this->resolveArguments($input) as $name => $value) {
116            if (null === $value) {
117                continue;
118            }
119
120            if (\is_array($value)) {
121                if ([] === $value) {
122                    continue;
123                }
124
125                $providedValues = array_values(array_filter(
126                    $value,
127                    static fn(mixed $item): bool => \is_scalar($item) && $input->hasParameterOption(
128                        (string) $item,
129                        true
130                    ),
131                ));
132
133                if ([] !== $providedValues) {
134                    $arguments[$name] = $providedValues;
135                }
136
137                continue;
138            }
139
140            if (
141                (\is_array($arrayParameters) && \array_key_exists($name, $arrayParameters))
142                || (\is_scalar($value) && $input->hasParameterOption((string) $value, true))
143            ) {
144                $arguments[$name] = $value;
145            }
146        }
147
148        return $arguments;
149    }
150
151    /**
152     * @param InputInterface $input
153     *
154     * @return array<string, mixed>
155     */
156    private function resolveArguments(InputInterface $input): array
157    {
158        try {
159            return $input->getArguments();
160        } catch (Throwable) {
161            return [];
162        }
163    }
164
165    /**
166     * @param InputInterface $input
167     *
168     * @return array<string, mixed>
169     */
170    private function extractProvidedOptions(InputInterface $input): array
171    {
172        $definition = $this->resolveDefinition($input);
173
174        if (! $definition instanceof InputDefinition) {
175            return [];
176        }
177
178        $options = [];
179
180        foreach ($definition->getOptions() as $option) {
181            $tokens = $this->optionTokens($option);
182
183            if (! $input->hasParameterOption($tokens, true)) {
184                continue;
185            }
186
187            $options[$option->getName()] = $input->getOption($option->getName());
188        }
189
190        return $options;
191    }
192
193    /**
194     * @param InputInterface $input
195     */
196    private function resolveDefinition(InputInterface $input): ?InputDefinition
197    {
198        if (! $input instanceof Input) {
199            return null;
200        }
201
202        static $property;
203
204        if (! $property instanceof ReflectionProperty) {
205            $property = new ReflectionProperty(Input::class, 'definition');
206        }
207
208        /** @var InputDefinition $definition */
209        $definition = $property->getValue($input);
210
211        return $definition;
212    }
213
214    /**
215     * @param InputInterface $input
216     *
217     * @return array<string|int, mixed>|null
218     */
219    private function resolveArrayParameters(InputInterface $input): ?array
220    {
221        if (! $input instanceof ArrayInput) {
222            return null;
223        }
224
225        static $property;
226
227        if (! $property instanceof ReflectionProperty) {
228            $property = new ReflectionProperty(ArrayInput::class, 'parameters');
229        }
230
231        /** @var array<string|int, mixed> $parameters */
232        $parameters = $property->getValue($input);
233
234        return $parameters;
235    }
236
237    /**
238     * @param InputOption $option
239     *
240     * @return list<string>
241     */
242    private function optionTokens(InputOption $option): array
243    {
244        $tokens = ['--' . $option->getName()];
245        $shortcut = $option->getShortcut();
246
247        if (null !== $shortcut && '' !== $shortcut) {
248            foreach (explode('|', $shortcut) as $alias) {
249                if ('' !== $alias) {
250                    $tokens[] = '-' . $alias;
251                }
252            }
253        }
254
255        return $tokens;
256    }
257}