Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
XdebugDisablingProcessEnvironmentConfigurator
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
6 / 6
16
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 configure
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 shouldDisableXdebug
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 requiresCoverage
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 containsCommandLineArgument
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 isTruthyEnvironmentFlag
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
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\Environment\EnvironmentInterface;
23use FastForward\DevTools\Php\ExtensionInterface;
24use Symfony\Component\Console\Output\OutputInterface;
25use Symfony\Component\Process\Process;
26use function Safe\preg_match;
27
28/**
29 * Disables Xdebug for child processes unless coverage still needs it.
30 */
31  class XdebugDisablingProcessEnvironmentConfigurator implements ProcessEnvironmentConfiguratorInterface
32{
33    /**
34     * @var list<string>
35     */
36    private const array COVERAGE_ARGUMENT_PATTERNS = [
37        '--coverage',
38        '--coverage-clover',
39        '--coverage-cobertura',
40        '--coverage-crap4j',
41        '--coverage-html',
42        '--coverage-php',
43        '--coverage-text',
44        '--coverage-xml',
45        '--min-coverage',
46    ];
47
48    /**
49     * @param EnvironmentInterface $environment reads parent process environment variables
50     * @param ExtensionInterface $extension checks PHP extension availability
51     */
52    public function __construct(
53        private EnvironmentInterface $environment,
54        private ExtensionInterface $extension,
55    ) {}
56
57    /**
58     * Configures Xdebug-related environment variables for nested commands.
59     *
60     * @param Process $process the queued process that will be started
61     * @param OutputInterface $output the parent output used to infer console capabilities
62     */
63    public function configure(Process $process, OutputInterface $output): void
64    {
65        unset($output);
66
67        if (! $this->shouldDisableXdebug($process)) {
68            return;
69        }
70
71        $env = $process->getEnv();
72
73        if (\array_key_exists('XDEBUG_MODE', $env)) {
74            return;
75        }
76
77        $env['XDEBUG_MODE'] = 'off';
78        $process->setEnv($env);
79    }
80
81    /**
82     * Determines whether Xdebug can be disabled for the child process.
83     *
84     * @param Process $process the queued process that will be started
85     *
86     * @return bool true when Xdebug should be disabled for the child process
87     */
88    private function shouldDisableXdebug(Process $process): bool
89    {
90        if (! $this->extension->isLoaded('xdebug')) {
91            return false;
92        }
93
94        if ($this->isTruthyEnvironmentFlag('COMPOSER_ALLOW_XDEBUG')) {
95            return false;
96        }
97
98        if (null !== $this->environment->get('XDEBUG_MODE')) {
99            return false;
100        }
101
102        if (! $this->requiresCoverage($process)) {
103            return true;
104        }
105
106        return $this->extension->isLoaded('pcov');
107    }
108
109    /**
110     * Determines whether the child process command line requests coverage.
111     *
112     * @param Process $process the queued process that will be started
113     *
114     * @return bool true when coverage arguments are present
115     */
116    private function requiresCoverage(Process $process): bool
117    {
118        $commandLine = $process->getCommandLine();
119
120        foreach (self::COVERAGE_ARGUMENT_PATTERNS as $argument) {
121            if ($this->containsCommandLineArgument($commandLine, $argument)) {
122                return true;
123            }
124        }
125
126        return false;
127    }
128
129    /**
130     * Determines whether a command line contains an exact long option.
131     *
132     * @param string $commandLine the shell-escaped command line
133     * @param string $argument the long option to find
134     *
135     * @return bool true when the exact option is present
136     */
137    private function containsCommandLineArgument(string $commandLine, string $argument): bool
138    {
139        return 1 === preg_match(
140            \sprintf('/(?:^|[\\s\'"])%s(?:=|[\\s\'"]|$)/', preg_quote($argument, '/')),
141            $commandLine
142        );
143    }
144
145    /**
146     * Determines whether an environment flag is set to a truthy value.
147     *
148     * @param string $name the environment variable name
149     *
150     * @return bool true when the environment variable is truthy
151     */
152    private function isTruthyEnvironmentFlag(string $name): bool
153    {
154        $value = $this->environment->get($name, '');
155
156        return null !== $value && '' !== $value && '0' !== $value;
157    }
158}