Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.30% covered (success)
91.30%
21 / 23
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ProcessBuilder
91.30% covered (success)
91.30%
21 / 23
66.67% covered (warning)
66.67%
4 / 6
13.11
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 withArgument
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getArguments
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 build
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 shouldAddLogoSuppressionArgument
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 prependLogoSuppressionArgument
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
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\Process;
21
22use FastForward\DevTools\Path\DevToolsPathResolver;
23use Symfony\Component\Process\Process;
24
25/**
26 * Builds immutable process definitions from a command and a collection of arguments.
27 *
28 * This builder SHALL preserve previously supplied arguments by returning a new
29 * instance on each mutation-like operation. Implementations of this concrete
30 * builder MUST keep argument ordering stable so that the generated process
31 * reflects the exact sequence in which arguments were provided.
32 */
33  class ProcessBuilder implements ProcessBuilderInterface
34{
35    private const string NO_LOGO_ARGUMENT = '--no-logo';
36
37    /**
38     * Creates a new immutable process builder instance.
39     *
40     * The provided arguments SHALL be stored in the same order in which they are
41     * received and MUST be used when building the final process instance.
42     *
43     * @param list<string> $arguments the arguments already collected by the builder
44     */
45    public function __construct(
46        private array $arguments = [],
47    ) {}
48
49    /**
50     * Returns a new builder instance with an additional argument appended.
51     *
52     * When a value is provided, the argument SHALL be normalized to the
53     * "{argument}={value}" format before being appended. When no value is
54     * provided, the raw argument token MUST be appended as-is.
55     *
56     * This method MUST NOT mutate the current builder instance and SHALL return
57     * a new instance containing the accumulated arguments.
58     *
59     * @param string $argument the argument name or token to append
60     * @param string|null $value the optional value associated with the argument
61     *
62     * @return ProcessBuilderInterface a new builder instance containing the appended argument
63     */
64    public function withArgument(string $argument, ?string $value = null): ProcessBuilderInterface
65    {
66        if (null !== $value) {
67            $argument = \sprintf('%s=%s', $argument, $value);
68        }
69
70        return new self([...$this->arguments, $argument]);
71    }
72
73    /**
74     * Returns the arguments currently collected by the builder.
75     *
76     * The returned list SHALL preserve insertion order and MAY be used for
77     * inspection, debugging, or testing purposes.
78     *
79     * @return list<string> the collected process arguments
80     */
81    public function getArguments(): array
82    {
83        return $this->arguments;
84    }
85
86    /**
87     * Builds a process instance for the specified command.
88     *
89     * The command string SHALL be split into tokens using a space separator and
90     * combined with all previously collected builder arguments. The resulting
91     * process MUST preserve the final token order exactly as assembled by this
92     * method.
93     *
94     * @param string|array $command the base command used to initialize the process
95     *
96     * @return Process the configured process instance ready for execution
97     */
98    public function build(string|array $command): Process
99    {
100        if (\is_array($command)) {
101            $command = array_values($command);
102        }
103
104        if (\is_string($command)) {
105            $command = explode(' ', $command);
106        }
107
108        if ($this->shouldAddLogoSuppressionArgument($command)) {
109            $command = $this->prependLogoSuppressionArgument($command);
110        }
111
112        return new Process(command: [...$command, ...$this->arguments], timeout: 0);
113    }
114
115    /**
116     * @param list<string> $command
117     */
118    private function shouldAddLogoSuppressionArgument(array $command): bool
119    {
120        if (\in_array(self::NO_LOGO_ARGUMENT, $this->arguments, true)) {
121            return false;
122        }
123
124        if ([] === $command) {
125            return false;
126        }
127
128        $binary = str_replace('\\', '/', $command[0]);
129        $packageBinaryPath = str_replace('\\', '/', DevToolsPathResolver::getBinaryPath());
130
131        return $binary === $packageBinaryPath;
132    }
133
134    /**
135     * @param list<string> $command
136     *
137     * @return list<string>
138     */
139    private function prependLogoSuppressionArgument(array $command): array
140    {
141        if ([] === $command) {
142            return $command;
143        }
144
145        $binary = array_shift($command);
146
147        return [$binary, self::NO_LOGO_ARGUMENT, ...$command];
148    }
149}