Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.34% covered (warning)
75.34%
55 / 73
25.00% covered (danger)
25.00%
2 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
CommandInputProcessor
75.34% covered (warning)
75.34%
55 / 73
25.00% covered (danger)
25.00%
2 / 8
66.20
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
 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;
28
29/**
30 * Expands command input instances into structured context entries.
31 */
32 class CommandInputProcessor implements ContextProcessorInterface
33{
34    /**
35     * @param array<string, mixed> $context
36     *
37     * @return array<string, mixed>
38     */
39    public function process(array $context): array
40    {
41        foreach ($context as $key => $value) {
42            if (! $value instanceof InputInterface) {
43                continue;
44            }
45
46            $context = $this->processInputContext($context, $key, $value);
47        }
48
49        return $context;
50    }
51
52    /**
53     * @param array<string, mixed> $context
54     * @param string|int $key
55     * @param InputInterface $input
56     *
57     * @return array<string, mixed>
58     */
59    private function processInputContext(array $context, string|int $key, InputInterface $input): array
60    {
61        unset($context[$key]);
62
63        $arguments = $this->extractProvidedArguments($input);
64        $command = $this->inferCommandName($context, $input, $arguments);
65
66        if (null !== $command && ! \array_key_exists('command', $context)) {
67            $context['command'] = $command;
68        }
69
70        unset($arguments['command']);
71
72        if ([] !== $arguments && ! \array_key_exists('arguments', $context)) {
73            $context['arguments'] = $arguments;
74        }
75
76        $options = $this->extractProvidedOptions($input);
77        if ([] !== $options && ! \array_key_exists('options', $context)) {
78            $context['options'] = $options;
79        }
80
81        return $context;
82    }
83
84    /**
85     * @param array<string, mixed> $context
86     * @param array<string, mixed> $arguments
87     * @param InputInterface $input
88     *
89     * @return string|null
90     */
91    private function inferCommandName(array $context, InputInterface $input, array $arguments): ?string
92    {
93        if (\array_key_exists('command', $context)) {
94            return null;
95        }
96
97        if (\array_key_exists('command', $arguments) && \is_string($arguments['command'])) {
98            return $arguments['command'];
99        }
100
101        $command = $input->getFirstArgument();
102
103        return \is_string($command) ? $command : null;
104    }
105
106    /**
107     * @param InputInterface $input
108     *
109     * @return array<string, mixed>
110     */
111    private function extractProvidedArguments(InputInterface $input): array
112    {
113        $arguments = [];
114        $arrayParameters = $this->resolveArrayParameters($input);
115
116        foreach ($input->getArguments() as $name => $value) {
117            if (null === $value) {
118                continue;
119            }
120
121            if (\is_array($value)) {
122                if ([] === $value) {
123                    continue;
124                }
125
126                $providedValues = array_values(array_filter(
127                    $value,
128                    fn(mixed $item): bool => \is_scalar($item) && $input->hasParameterOption((string) $item, true),
129                ));
130
131                if ([] !== $providedValues) {
132                    $arguments[$name] = $providedValues;
133                }
134
135                continue;
136            }
137
138            if (
139                (\is_array($arrayParameters) && \array_key_exists($name, $arrayParameters))
140                || (\is_scalar($value) && $input->hasParameterOption((string) $value, true))
141            ) {
142                $arguments[$name] = $value;
143            }
144        }
145
146        return $arguments;
147    }
148
149    /**
150     * @param InputInterface $input
151     *
152     * @return array<string, mixed>
153     */
154    private function extractProvidedOptions(InputInterface $input): array
155    {
156        $definition = $this->resolveDefinition($input);
157
158        if (! $definition instanceof InputDefinition) {
159            return [];
160        }
161
162        $options = [];
163
164        foreach ($definition->getOptions() as $option) {
165            $tokens = $this->optionTokens($option);
166
167            if (! $input->hasParameterOption($tokens, true)) {
168                continue;
169            }
170
171            $options[$option->getName()] = $input->getOption($option->getName());
172        }
173
174        return $options;
175    }
176
177    /**
178     * @param InputInterface $input
179     *
180     * @return InputDefinition|null
181     */
182    private function resolveDefinition(InputInterface $input): ?InputDefinition
183    {
184        if (! $input instanceof Input) {
185            return null;
186        }
187
188        static $property;
189
190        if (! $property instanceof ReflectionProperty) {
191            $property = new ReflectionProperty(Input::class, 'definition');
192        }
193
194        /** @var InputDefinition $definition */
195        $definition = $property->getValue($input);
196
197        return $definition;
198    }
199
200    /**
201     * @param InputInterface $input
202     *
203     * @return array<string|int, mixed>|null
204     */
205    private function resolveArrayParameters(InputInterface $input): ?array
206    {
207        if (! $input instanceof ArrayInput) {
208            return null;
209        }
210
211        static $property;
212
213        if (! $property instanceof ReflectionProperty) {
214            $property = new ReflectionProperty(ArrayInput::class, 'parameters');
215        }
216
217        /** @var array<string|int, mixed> $parameters */
218        $parameters = $property->getValue($input);
219
220        return $parameters;
221    }
222
223    /**
224     * @param InputOption $option
225     *
226     * @return list<string>
227     */
228    private function optionTokens(InputOption $option): array
229    {
230        $tokens = ['--' . $option->getName()];
231        $shortcut = $option->getShortcut();
232
233        if (null !== $shortcut && '' !== $shortcut) {
234            foreach (explode('|', $shortcut) as $alias) {
235                if ('' !== $alias) {
236                    $tokens[] = '-' . $alias;
237                }
238            }
239        }
240
241        return $tokens;
242    }
243}