Completed
Push — master ( 209e5d...97f947 )
by Greg
01:23
created

ProcessBase::fromShellCommandline()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 5
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 Symfony\Component\Console\Output\OutputInterface;
10
use Symfony\Component\Console\Output\ConsoleOutputInterface;
11
12
/**
13
 * A wrapper around Symfony Process.
14
 *
15
 * - Supports simulated mode. Typically enabled via a --simulate option.
16
 * - Supports verbose mode - logs all runs.
17
 * - Can convert output json data into php array (convenience method)
18
 * - Provides a "realtime output" helper
19
 */
20
class ProcessBase extends Process
21
{
22
    /**
23
     * @var OutputStyle
24
     */
25
    protected $output;
26
27
    /**
28
     * @var OutputInterface
29
     */
30
    protected $stderr;
31
32
    private $simulated = false;
33
34
    private $verbose = false;
35
36
    /**
37
     * @var LoggerInterface
38
     */
39
    private $logger;
40
41
    /**
42
     * Symfony 4 style constructor for creating Process instances from strings.
43
     * @param string $command The commandline string to run
44
     * @param string|null $cwd     The working directory or null to use the working dir of the current PHP process
45
     * @param array|null $env     The environment variables or null to use the same environment as the current PHP process
46
     * @param mixed|null $input   The input as stream resource, scalar or \Traversable, or null for no input
47
     * @param int|float|null $timeout The timeout in seconds or null to disable
48
     * @return Process
49
     */
50
    public static function fromShellCommandline($command, $cwd = null, array $env = null, $input = null, $timeout = 60)
51
    {
52
        if (method_exists('\Symfony\Component\Process\Process', 'fromShellCommandline')) {
53
            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...
54
        }
55
        return new Process($command, $cwd, $env, $input, $timeout);
56
    }
57
58
    /**
59
     * realtimeStdout returns the output stream that realtime output
60
     * should be sent to (if applicable)
61
     *
62
     * @return OutputStyle $output
63
     */
64
    public function realtimeStdout()
65
    {
66
        return $this->output;
67
    }
68
69
    protected function realtimeStderr()
70
    {
71
        if ($this->stderr) {
72
            return $this->stderr;
73
        }
74
        if (method_exists($this->output, 'getErrorStyle')) {
75
            return $this->output->getErrorStyle();
76
        }
77
78
        return $this->realtimeStdout();
79
    }
80
81
    /**
82
     * setRealtimeOutput allows the caller to inject an OutputStyle object
83
     * that will be used to stream realtime output if applicable.
84
     *
85
     * @param OutputStyle $output
86
     */
87
    public function setRealtimeOutput(OutputInterface $output, $stderr = null)
88
    {
89
        $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...
90
        $this->stderr = $stderr instanceof ConsoleOutputInterface ? $stderr->getErrorOutput() : $stderr;
91
    }
92
93
    /**
94
     * @return bool
95
     */
96
    public function isVerbose()
97
    {
98
        return $this->verbose;
99
    }
100
101
    /**
102
     * @param bool $verbose
103
     */
104
    public function setVerbose($verbose)
105
    {
106
        $this->verbose = $verbose;
107
    }
108
109
    /**
110
     * @return bool
111
     */
112
    public function isSimulated()
113
    {
114
        return $this->simulated;
115
    }
116
117
    /**
118
     * @param bool $simulated
119
     */
120
    public function setSimulated($simulated)
121
    {
122
        $this->simulated = $simulated;
123
    }
124
125
    /**
126
     * @return LoggerInterface
127
     */
128
    public function getLogger()
129
    {
130
        return $this->logger;
131
    }
132
133
    /**
134
     * @param LoggerInterface $logger
135
     */
136
    public function setLogger($logger)
137
    {
138
        $this->logger = $logger;
139
    }
140
141
    /**
142
     * @inheritDoc
143
     */
144
    public function start(callable $callback = null, $env = array())
145
    {
146
        $cmd = $this->getCommandLine();
147
        if ($this->isSimulated()) {
148
            $this->getLogger()->notice('Simulating: ' . $cmd);
149
            // Run a command that always succeeds.
150
            $this->setCommandLine('exit 0');
151
        } elseif ($this->isVerbose()) {
152
            $this->getLogger()->info('Executing: ' . $cmd);
153
        }
154
        parent::start($callback, $env);
155
        // Set command back to original value in case anyone asks.
156
        if ($this->isSimulated()) {
157
            $this->setCommandLine($cmd);
158
        }
159
    }
160
161
    /**
162
     * Get Process output and decode its JSON.
163
     *
164
     * @return array
165
     *   An associative array.
166
     */
167
    public function getOutputAsJson()
168
    {
169
        $output = trim($this->getOutput());
170
        if (empty($output)) {
171
            throw new \InvalidArgumentException('Output is empty.');
172
        }
173
        $output = preg_replace('#^[^{]*#', '', $output);
174
        $output = preg_replace('#[^}]*$#', '', $output);
175
        if (!$json = json_decode($output, true)) {
176
            throw new \InvalidArgumentException('Unable to decode output into JSON.');
177
        }
178
        return $json;
179
    }
180
181
    /**
182
     * Return a realTime output object.
183
     *
184
     * @return callable
185
     */
186
    public function showRealtime()
187
    {
188
        $realTimeOutput = new RealtimeOutputHandler($this->realtimeStdout(), $this->realtimeStderr());
189
        $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...
190
        return $realTimeOutput;
191
    }
192
}
193