Completed
Push — master ( 9bb56c...ef52f6 )
by Greg
01:23
created

src/ProcessBase.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Consolidation\SiteProcess;
4
5
use Psr\Log\LoggerInterface;
6
use Symfony\Component\Console\Style\OutputStyle;
7
use Symfony\Component\Process\Process;
8
use Consolidation\SiteProcess\Util\RealtimeOutputHandler;
9
use Consolidation\SiteProcess\Util\Escape;
10
use Symfony\Component\Console\Output\OutputInterface;
11
use Symfony\Component\Console\Output\ConsoleOutputInterface;
12
13
/**
14
 * A wrapper around Symfony Process.
15
 *
16
 * - Supports simulated mode. Typically enabled via a --simulate option.
17
 * - Supports verbose mode - logs all runs.
18
 * - Can convert output json data into php array (convenience method)
19
 * - Provides a "realtime output" helper
20
 */
21
class ProcessBase extends Process
22
{
23
    /**
24
     * @var OutputStyle
25
     */
26
    protected $output;
27
28
    /**
29
     * @var OutputInterface
30
     */
31
    protected $stderr;
32
33
    private $simulated = false;
34
35
    private $verbose = false;
36
37
    /**
38
     * @var LoggerInterface
39
     */
40
    private $logger;
41
42
    /**
43
     * Symfony 4 style constructor for creating Process instances from strings.
44
     * @param string $command The commandline string to run
45
     * @param string|null $cwd     The working directory or null to use the working dir of the current PHP process
46
     * @param array|null $env     The environment variables or null to use the same environment as the current PHP process
47
     * @param mixed|null $input   The input as stream resource, scalar or \Traversable, or null for no input
48
     * @param int|float|null $timeout The timeout in seconds or null to disable
49
     * @return Process
50
     */
51
    public static function fromShellCommandline($command, $cwd = null, array $env = null, $input = null, $timeout = 60)
52
    {
53
        if (method_exists('\Symfony\Component\Process\Process', 'fromShellCommandline')) {
54
            return Process::fromShellCommandline($command, $cwd, $env, $input, $timeout);
0 ignored issues
show
It seems like you code against a specific sub-type and not the parent class Symfony\Component\Process\Process as the method fromShellCommandline() does only exist in the following sub-classes of Symfony\Component\Process\Process: Consolidation\SiteProcess\ProcessBase, Consolidation\SiteProcess\SiteProcess. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
55
        }
56
        return new self($command, $cwd, $env, $input, $timeout);
57
    }
58
59
    /**
60
     * realtimeStdout returns the output stream that realtime output
61
     * should be sent to (if applicable)
62
     *
63
     * @return OutputStyle $output
64
     */
65
    public function realtimeStdout()
66
    {
67
        return $this->output;
68
    }
69
70
    protected function realtimeStderr()
71
    {
72
        if ($this->stderr) {
73
            return $this->stderr;
74
        }
75
        if (method_exists($this->output, 'getErrorStyle')) {
76
            return $this->output->getErrorStyle();
77
        }
78
79
        return $this->realtimeStdout();
80
    }
81
82
    /**
83
     * setRealtimeOutput allows the caller to inject an OutputStyle object
84
     * that will be used to stream realtime output if applicable.
85
     *
86
     * @param OutputStyle $output
87
     */
88
    public function setRealtimeOutput(OutputInterface $output, $stderr = null)
89
    {
90
        $this->output = $output;
91
        $this->stderr = $stderr instanceof ConsoleOutputInterface ? $stderr->getErrorOutput() : $stderr;
92
    }
93
94
    /**
95
     * @return bool
96
     */
97
    public function isVerbose()
98
    {
99
        return $this->verbose;
100
    }
101
102
    /**
103
     * @param bool $verbose
104
     */
105
    public function setVerbose($verbose)
106
    {
107
        $this->verbose = $verbose;
108
    }
109
110
    /**
111
     * @return bool
112
     */
113
    public function isSimulated()
114
    {
115
        return $this->simulated;
116
    }
117
118
    /**
119
     * @param bool $simulated
120
     */
121
    public function setSimulated($simulated)
122
    {
123
        $this->simulated = $simulated;
124
    }
125
126
    /**
127
     * @return LoggerInterface
128
     */
129
    public function getLogger()
130
    {
131
        return $this->logger;
132
    }
133
134
    /**
135
     * @param LoggerInterface $logger
136
     */
137
    public function setLogger($logger)
138
    {
139
        $this->logger = $logger;
140
    }
141
142
    /**
143
     * @inheritDoc
144
     */
145
    public function start(callable $callback = null, $env = array())
146
    {
147
        $cmd = $this->getCommandLine();
148
        if ($this->isSimulated()) {
149
            $this->getLogger()->notice('Simulating: ' . $cmd);
150
            // Run a command that always succeeds (on Linux and Windows).
151
            $this->setCommandLine('true');
152
        } elseif ($this->isVerbose()) {
153
            $this->getLogger()->info('Executing: ' . $cmd);
154
        }
155
        parent::start($callback, $env);
156
        // Set command back to original value in case anyone asks.
157
        if ($this->isSimulated()) {
158
            $this->setCommandLine($cmd);
159
        }
160
    }
161
162
    /**
163
     * Get Process output and decode its JSON.
164
     *
165
     * @return array
166
     *   An associative array.
167
     */
168
    public function getOutputAsJson()
169
    {
170
        $output = trim($this->getOutput());
171
        if (empty($output)) {
172
            throw new \InvalidArgumentException('Output is empty.');
173
        }
174
        if (Escape::isWindows()) {
175
            // Doubled double quotes were converted to \\".
176
            // Revert to double quote.
177
            $output = str_replace('\\"', '"', $output);
178
            // Revert of doubled backslashes.
179
            $output = preg_replace('#\\\\{2}#', '\\', $output);
180
        }
181
        $sanitizedOutput = $this->removeNonJsonJunk($output);
182
        $json = json_decode($sanitizedOutput, true);
183
        if (!isset($json)) {
184
            $msg = 'Unable to decode output into JSON: ' . json_last_error_msg();
185
            if (json_last_error() == JSON_ERROR_SYNTAX) {
186
                $msg .= "\n\n$output";
187
            }
188
            throw new \InvalidArgumentException($msg);
189
        }
190
        return $json;
191
    }
192
193
    /**
194
     * Allow for a certain amount of resiliancy in the output received when
195
     * json is expected.
196
     *
197
     * @param string $data
198
     * @return string
199
     */
200
    protected function removeNonJsonJunk($data)
201
    {
202
        // Exit early if we have no output.
203
        $data = trim($data);
204
        if (empty($data)) {
205
            return $data;
206
        }
207
        // If the data is a simple quoted string, or an array, then exit.
208
        if ((($data[0] == '"') && ($data[strlen($data) - 1] == '"')) ||
209
            (($data[0] == "[") && ($data[strlen($data) - 1] == "]"))
210
        ) {
211
            return $data;
212
        }
213
        // If the json is not a simple string or a simple array, then is must
214
        // be an associative array. We will remove non-json garbage characters
215
        // before and after the enclosing curley-braces.
216
        $start = strpos($data, '{');
217
        $end = strrpos($data, '}') + 1;
218
        $data = substr($data, $start, $end - $start);
219
        return $data;
220
    }
221
222
    /**
223
     * Return a realTime output object.
224
     *
225
     * @return callable
226
     */
227
    public function showRealtime()
228
    {
229
        $realTimeOutput = new RealtimeOutputHandler($this->realtimeStdout(), $this->realtimeStderr());
230
        $realTimeOutput->configure($this);
231
        return $realTimeOutput;
232
    }
233
}
234