Core   B
last analyzed

Complexity

Total Complexity 48

Size/Duplication

Total Lines 362
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 99.27%

Importance

Changes 0
Metric Value
wmc 48
lcom 1
cbo 8
dl 0
loc 362
ccs 136
cts 137
cp 0.9927
rs 8.4864
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
C run() 0 44 11
A runLast() 0 7 1
A runSilently() 0 4 1
A runInFolder() 0 12 2
D processCommands() 0 43 10
A cleanOutput() 0 8 1
A shellCommand() 0 4 1
A sudoCommand() 0 7 2
B processOutput() 0 20 5
A getTimestamp() 0 8 2
A getExtraOutput() 0 10 2
A runLocally() 0 6 1
A runRaw() 0 20 3
B displayCommands() 0 9 5
A getProvided() 0 17 1

How to fix   Complexity   

Complex Class

Complex classes like Core often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Core, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of Rocketeer
5
 *
6
 * (c) Maxime Fabre <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 *
11
 */
12
13
namespace Rocketeer\Services\Connections\Shell\Modules;
14
15
use Illuminate\Support\Arr;
16
use Illuminate\Support\Str;
17
use Symfony\Component\Console\Output\OutputInterface;
18
19
/**
20
 * Core handling of running commands and returning output.
21
 */
22
class Core extends AbstractBashModule
23
{
24
    /**
25
     * @var string
26
     */
27
    protected $extraOutput = null;
28
29
    //////////////////////////////////////////////////////////////////////
30
    /////////////////////////////// LOCAL ////////////////////////////////
31
    //////////////////////////////////////////////////////////////////////
32
33
    /**
34
     * Rune actions locally.
35
     *
36
     * @param string|array $commands
37
     *
38
     * @return string|null
0 ignored issues
show
Documentation introduced by Maxime Fabre
Should the return type not be boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
39
     */
40 14
    public function runLocally($commands)
41
    {
42
        return $this->modulable->on('local', function () use ($commands) {
43 14
            return $this->run($commands);
44 14
        });
45
    }
46
47
    ////////////////////////////////////////////////////////////////////
48
    ///////////////////////////// CORE METHODS /////////////////////////
49
    ////////////////////////////////////////////////////////////////////
50
51
    /**
52
     * Run actions on the remote server and gather the ouput.
53
     *
54
     * @param string|array $commands One or more commands
55
     * @param bool         $silent   Whether the command should stay silent no matter what
56
     * @param bool         $array    Whether the output should be returned as an array
57
     *
58
     * @return string|null
59
     */
60 114
    public function run($commands, $silent = false, $array = false)
61
    {
62 114
        $commands = $this->processCommands($commands);
63 114
        $verbose = $this->getOption('verbose') && !$silent;
64 114
        $pretend = $this->getOption('pretend');
65
66
        // Gather any extra output of the server
67
        // to be able to clean it up after
68 114
        if ($this->extraOutput === null && !$pretend) {
69 49
            $this->extraOutput = $this->getExtraOutput();
70 42
        }
71
72
        // Log the commands
73 114
        if (!$silent) {
74 99
            $this->modulable->toHistory($commands);
75 99
        }
76
77
        // Display the commands if necessary
78 114
        if ($verbose || ($pretend && !$silent)) {
79 71
            $this->modulable->toOutput($commands);
80 71
            $this->displayCommands($commands);
81
82 71
            if ($pretend) {
83 70
                return count($commands) === 1 ? $commands[0] : $commands;
84 70
            }
85 1
        }
86
87
        // Run commands
88 79
        $output = null;
89
        $this->modulable->getConnection()->run($commands, function ($results) use (&$output, $verbose) {
90 69
            $output .= $results;
91
92 69
            if ($verbose) {
93 1
                $display = $this->cleanOutput($results);
94 1
                $this->explainer->server(trim($display));
95 1
            }
96 79
        });
97
98
        // Process and log the output and commands
99 79
        $output = $this->processOutput($output, $array, true);
0 ignored issues
show
Bug Compatibility introduced by Maxime Fabre
The expression $this->processOutput($output, $array, true); of type array|string adds the type array to the return on line 102 which is incompatible with the return type documented by Rocketeer\Services\Conne...Shell\Modules\Core::run of type string|null.
Loading history...
100 79
        $this->modulable->toOutput($output);
101
102 79
        return $output;
103
    }
104
105
    /**
106
     * Run a command get the last line output to
107
     * prevent noise.
108
     *
109
     * @param string $commands
110
     *
111
     * @return string
112
     */
113 22
    public function runLast($commands)
114
    {
115 22
        $results = $this->runRaw($commands, true);
116 22
        $results = end($results);
117
118 22
        return $results;
119
    }
120
121
    /**
122
     * Run a raw command, without any processing, and
123
     * get its output as a string or array.
124
     *
125
     * @param string $commands
126
     * @param bool   $array    Whether the output should be returned as an array
127
     * @param bool   $trim     Whether the output should be trimmed
128
     *
129
     * @return string|string[]
0 ignored issues
show
Documentation introduced by Maxime Fabre
Should the return type not be string|array?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
130
     */
131 120
    public function runRaw($commands, $array = false, $trim = false)
132
    {
133 74
        $pretend = $this->getOption('pretend');
134 120
        if ($pretend) {
135 72
            return $array ? [$commands] : 'true';
136
        }
137
138 50
        $this->displayCommands($commands, OutputInterface::VERBOSITY_VERY_VERBOSE);
139
140
        // Run commands
141 50
        $output = null;
142 50
        $this->modulable->getConnection()->run($commands, function ($results) use (&$output) {
143 50
            $output .= $results;
144 50
        });
145
146
        // Process the output
147 50
        $output = $this->processOutput($output, $array, $trim);
148
149 50
        return $output;
150
    }
151
152
    /**
153
     * Run commands silently.
154
     *
155
     * @param string|array $commands
156
     * @param bool         $array
157
     *
158
     * @return string|null
159
     */
160 60
    public function runSilently($commands, $array = false)
161
    {
162 60
        return $this->run($commands, true, $array);
163
    }
164
165
    /**
166
     * Run commands in a folder.
167
     *
168
     * @param string|null  $folder
169
     * @param string|array $tasks
170
     *
171
     * @return string|null
172
     */
173 50
    public function runInFolder($folder = null, $tasks = [])
174
    {
175
        // Convert to array
176 50
        if (!is_array($tasks)) {
177 36
            $tasks = [$tasks];
178 36
        }
179
180
        // Prepend folder
181 50
        array_unshift($tasks, 'cd '.$this->paths->getFolder($folder));
0 ignored issues
show
Documentation Bug introduced by Maxime Fabre
The method getFolder does not exist on object<Rocketeer\Services\Environment\Pathfinder>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
182
183 50
        return $this->run($tasks);
184
    }
185
186
    ////////////////////////////////////////////////////////////////////
187
    /////////////////////////////// HELPERS ////////////////////////////
188
    ////////////////////////////////////////////////////////////////////
189
190
    /**
191
     * Get the current timestamp on the server.
192
     *
193
     * @return string
194
     */
195 122
    public function getTimestamp()
196
    {
197 20
        $timestamp = $this->runLast('date +"%Y%m%d%H%M%S"');
198 20
        $timestamp = trim($timestamp);
199 122
        $timestamp = preg_match('/^[0-9]{14}$/', $timestamp) ? $timestamp : date('YmdHis');
200
201 20
        return $timestamp;
202 120
    }
203
204
    /**
205
     * @return string
206
     */
207 42
    public function getExtraOutput()
208
    {
209 42
        $output = $this->runRaw([$this->shellCommand('echo ROCKETEER')]);
0 ignored issues
show
Documentation introduced by Maxime Fabre
array($this->shellCommand('echo ROCKETEER')) is of type array<integer,string,{"0":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
210 42
        $output = str_replace('ROCKETEER', null, $output);
211 42
        if ($output === "\n") {
212
            return '';
213
        }
214
215 42
        return str_replace("\n\n", "\n", $output);
216
    }
217
218
    ////////////////////////////////////////////////////////////////////
219
    ///////////////////////////// PROCESSORS ///////////////////////////
220
    ////////////////////////////////////////////////////////////////////
221
222
    /**
223
     * Display the passed commands.
224
     *
225
     * @param string|array $commands
226
     * @param int          $verbosity
227
     */
228 120
    protected function displayCommands($commands, $verbosity = 1)
229
    {
230
        // Print out command if verbosity level allows it
231 120
        if ($verbosity && $this->hasCommand() && ($this->command->getVerbosity() >= $verbosity)) {
0 ignored issues
show
Documentation Bug introduced by Maxime Fabre
The method getVerbosity does not exist on object<Rocketeer\Console...mmands\AbstractCommand>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
232 72
            foreach ((array) $commands as $command) {
233 72
                $this->explainer->line('$ '.$command, 'magenta');
234 72
            }
235 72
        }
236 120
    }
237
238
    /**
239
     * Process an array of commands.
240
     *
241
     * @param string|array $commands
242
     *
243
     * @return array
244
     */
245 120
    public function processCommands($commands)
246
    {
247 120
        $separator = $this->environment->getSeparator();
248 120
        $shell = $this->config->getContextually('remote.shell');
0 ignored issues
show
Bug introduced by Maxime Fabre
It seems like you code against a concrete implementation and not the interface Rocketeer\Services\Config\ConfigurationInterface as the method getContextually() does only exist in the following implementations of said interface: Rocketeer\Services\Config\ContextualConfiguration.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
249 120
        $shelled = $this->config->getContextually('remote.shelled');
0 ignored issues
show
Bug introduced by Maxime Fabre
It seems like you code against a concrete implementation and not the interface Rocketeer\Services\Config\ConfigurationInterface as the method getContextually() does only exist in the following implementations of said interface: Rocketeer\Services\Config\ContextualConfiguration.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
250 120
        $sudo = $this->config->getContextually('remote.sudo');
0 ignored issues
show
Bug introduced by Maxime Fabre
It seems like you code against a concrete implementation and not the interface Rocketeer\Services\Config\ConfigurationInterface as the method getContextually() does only exist in the following implementations of said interface: Rocketeer\Services\Config\ContextualConfiguration.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
251 120
        $sudoed = $this->config->getContextually('remote.sudoed');
0 ignored issues
show
Bug introduced by Maxime Fabre
It seems like you code against a concrete implementation and not the interface Rocketeer\Services\Config\ConfigurationInterface as the method getContextually() does only exist in the following implementations of said interface: Rocketeer\Services\Config\ContextualConfiguration.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
252
253
        // Prepare paths replacer
254 120
        $pattern = sprintf('#\%s([\w\d\s])#', DS);
255 120
        $replacement = sprintf('\%s$1', $separator);
256
257
        // Cast commands to array
258 120
        if (!is_array($commands)) {
259 94
            $commands = [$commands];
260 94
        }
261
262
        // Flatten and process commands
263 120
        $commands = Arr::flatten($commands);
264 120
        foreach ($commands as &$command) {
265
            // Replace directory separators
266 120
            if (DS !== $separator) {
267 2
                $command = preg_replace($pattern, $replacement, $command);
268 2
            }
269
270
            // Let framework process commands
271 120
            if ($framework = $this->getFramework()) {
272 6
                $command = $framework->processCommand($command);
273 6
            }
274
275
            // Create shell if asked
276 120
            $forceShell = $this->modulable->getOption('shelled', true);
277 120
            if ($forceShell || $shell && Str::contains($command, $shelled)) {
278 1
                $command = $this->shellCommand($command);
279 1
            }
280
281 120
            if ($sudo && Str::contains($command, $sudoed)) {
282 2
                $command = $this->sudoCommand($sudo, $command);
283 2
            }
284 120
        }
285
286 120
        return $commands;
287
    }
288
289
    /**
290
     * Clean the output of various intruding bits.
291
     *
292
     * @param string $output
293
     *
294
     * @return string
295
     */
296 87
    protected function cleanOutput($output)
297
    {
298 87
        $output = str_replace($this->extraOutput, null, $output);
299
300 87
        return strtr($output, [
301 87
            'stdin: is not a tty' => null,
302 87
        ]);
303
    }
304
305
    /**
306
     * Pass a command through shell execution.
307
     *
308
     * @param string $command
309
     *
310
     * @return string
311
     */
312 46
    public function shellCommand($command)
313
    {
314 46
        return "bash --login -c '".$command."'";
315
    }
316
317
    /**
318
     * Execute a command as a sudo user.
319
     *
320
     * @param string|bool $sudo
321
     * @param string      $command
322
     *
323
     * @return string
324
     */
325 2
    protected function sudoCommand($sudo, $command)
326
    {
327 2
        $sudo = is_bool($sudo) ? 'sudo' : 'sudo -u '.$sudo;
328 2
        $command = $sudo.' '.$command;
329
330 2
        return $command;
331
    }
332
333
    /**
334
     * Process the output of a command.
335
     *
336
     * @param string $output
337
     * @param bool   $array  Whether to return an array or a string
338
     * @param bool   $trim   Whether to trim the output or not
339
     *
340
     * @return string|array
341
     */
342 87
    protected function processOutput($output, $array = false, $trim = true)
343
    {
344
        // Remove polluting strings
345 87
        $output = $this->cleanOutput($output);
346
347
        // Explode output if necessary
348 87
        if ($array) {
349 8
            $delimiter = $this->environment->getLineEndings() ?: PHP_EOL;
350 8
            $output = explode($delimiter, $output);
351 8
        }
352
353
        // Trim output
354 87
        if ($trim) {
355 81
            $output = is_array($output)
356 81
                ? array_filter($output)
357 81
                : trim($output);
358 81
        }
359
360 87
        return $output;
361
    }
362
363
    /**
364
     * @return string[]
365
     */
366 442
    public function getProvided()
367
    {
368
        return [
369 442
            'getExtraOutput',
370 442
            'getProvided  ',
371 442
            'getTimestamp',
372 442
            'processCommands',
373 442
            'run',
374 442
            'runInFolder',
375 442
            'runLast',
376 442
            'runLocally',
377 442
            'runRaw',
378 442
            'runSilently',
379 442
            'shellCommand',
380 442
            'status',
381 442
        ];
382
    }
383
}
384