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