Completed
Push — master ( 8c2e94...7e7633 )
by Greg
01:38
created

ProcessBase::removeNonJsonJunk()   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 8.9777
c 0
b 0
f 0
cc 6
nc 3
nop 1
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
Bug introduced by
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;
0 ignored issues
show
Documentation Bug introduced by
$output is of type object<Symfony\Component...Output\OutputInterface>, but the property $output was declared to be of type object<Symfony\Component...sole\Style\OutputStyle>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
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
        $output = $this->removeNonJsonJunk($output);
182
        $json = json_decode($output, true);
183
        if (!isset($json)) {
184
            throw new \InvalidArgumentException('Unable to decode output into JSON.');
185
        }
186
        return $json;
187
    }
188
189
    /**
190
     * Allow for a certain amount of resiliancy in the output received when
191
     * json is expected.
192
     *
193
     * @param string $data
194
     * @return string
195
     */
196
    protected function removeNonJsonJunk($data)
197
    {
198
        // Exit early if we have no output.
199
        $data = trim($data);
200
        if (empty($data)) {
201
            return $data;
202
        }
203
        // If the data is a simple quoted string, or an array, then exit.
204
        if ((($data[0] == '"') && ($data[strlen($data) - 1] == '"')) ||
205
            (($data[0] == "[") && ($data[strlen($data) - 1] == "]"))
206
        ) {
207
            return $data;
208
        }
209
        // If the json is not a simple string or a simple array, then is must
210
        // be an associative array. We will remove non-json garbage characters
211
        // before and after the enclosing curley-braces.
212
        $data = preg_replace('#^[^{]*#', '', $data);
213
        $data = preg_replace('#[^}]*$#', '', $data);
214
        return $data;
215
    }
216
217
    /**
218
     * Return a realTime output object.
219
     *
220
     * @return callable
221
     */
222
    public function showRealtime()
223
    {
224
        $realTimeOutput = new RealtimeOutputHandler($this->realtimeStdout(), $this->realtimeStderr());
225
        $realTimeOutput->configure($this);
0 ignored issues
show
Unused Code introduced by
The call to the method Consolidation\SiteProces...putHandler::configure() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
226
        return $realTimeOutput;
227
    }
228
}
229