Completed
Pull Request — master (#80)
by Jan Philipp
01:46
created

ProcessExecutor::execute()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 74

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 74
rs 7.945
c 0
b 0
f 0
cc 6
nc 6
nop 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types=1);
2
3
4
namespace Shopware\Psh\ScriptRuntime;
5
6
use Shopware\Psh\Listing\Script;
7
use Symfony\Component\Process\Process;
8
9
/**
10
 * Execute a command in a separate process
11
 */
12
class ProcessExecutor
13
{
14
    /**
15
     * @var ProcessEnvironment
16
     */
17
    private $environment;
18
19
    /**
20
     * @var TemplateEngine
21
     */
22
    private $templateEngine;
23
24
    /**
25
     * @var Logger
26
     */
27
    private $logger;
28
29
    /**
30
     * @var string
31
     */
32
    private $applicationDirectory;
33
34
    /**
35
     * @var DeferredProcess[]
36
     */
37
    private $deferredProcesses = [];
38
39
    /**
40
     * ProcessExecutor constructor.
41
     * @param ProcessEnvironment $environment
42
     * @param TemplateEngine $templateEngine
43
     * @param Logger $logger
44
     * @param string $applicationDirectory
45
     */
46
    public function __construct(
47
        ProcessEnvironment $environment,
48
        TemplateEngine $templateEngine,
49
        Logger $logger,
50
        string $applicationDirectory
51
    ) {
52
        $this->environment = $environment;
53
        $this->templateEngine = $templateEngine;
54
        $this->logger = $logger;
55
        $this->applicationDirectory = $applicationDirectory;
56
    }
57
58
    /**
59
     * @param Script $script
60
     * @param Command[] $commands
61
     */
62
    public function execute(Script $script, array $commands)
63
    {
64
        $this->logger->startScript($script);
65
66
        $this->executeTemplateRendering();
67
68
        foreach ($commands as $index => $command) {
69
            switch ($command) {
70
                case $command instanceof ProcessCommand:
71
                    $parsedCommand = $this->getParsedShellCommand($command);
0 ignored issues
show
Compatibility introduced by
$command of type object<Shopware\Psh\ScriptRuntime\Command> is not a sub-type of object<Shopware\Psh\ScriptRuntime\ProcessCommand>. It seems like you assume a concrete implementation of the interface Shopware\Psh\ScriptRuntime\Command to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
72
                    $process = $this->environment->createProcess($parsedCommand);
73
                    $this->setProcessDefaults($command, $process);
0 ignored issues
show
Compatibility introduced by
$command of type object<Shopware\Psh\ScriptRuntime\Command> is not a sub-type of object<Shopware\Psh\ScriptRuntime\ProcessCommand>. It seems like you assume a concrete implementation of the interface Shopware\Psh\ScriptRuntime\Command to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
74
75
                    if ($command->isDeferred()) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Shopware\Psh\ScriptRuntime\Command as the method isDeferred() does only exist in the following implementations of said interface: Shopware\Psh\ScriptRuntime\ProcessCommand.

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...
76
                        $this->logger->logStart(
77
                            'Defering',
78
                            $parsedCommand,
79
                            $command->getLineNumber(),
80
                            $command->isIgnoreError(),
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Shopware\Psh\ScriptRuntime\Command as the method isIgnoreError() does only exist in the following implementations of said interface: Shopware\Psh\ScriptRuntime\ProcessCommand.

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...
81
                            $index,
82
                            count($commands)
83
                        );
84
                        $this->deferProcess($command, $process);
0 ignored issues
show
Compatibility introduced by
$command of type object<Shopware\Psh\ScriptRuntime\Command> is not a sub-type of object<Shopware\Psh\ScriptRuntime\ProcessCommand>. It seems like you assume a concrete implementation of the interface Shopware\Psh\ScriptRuntime\Command to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
85
                    } else {
86
                        $this->logger->logStart(
87
                            'Starting',
88
                            $parsedCommand,
89
                            $command->getLineNumber(),
90
                            $command->isIgnoreError(),
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Shopware\Psh\ScriptRuntime\Command as the method isIgnoreError() does only exist in the following implementations of said interface: Shopware\Psh\ScriptRuntime\ProcessCommand.

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...
91
                            $index,
92
                            count($commands)
93
                        );
94
95
                        $this->runProcess($process);
96
                        $this->testProcessResultValid($command, $process);
0 ignored issues
show
Compatibility introduced by
$command of type object<Shopware\Psh\ScriptRuntime\Command> is not a sub-type of object<Shopware\Psh\ScriptRuntime\ProcessCommand>. It seems like you assume a concrete implementation of the interface Shopware\Psh\ScriptRuntime\Command to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
97
                    }
98
99
                    break;
100
101
102
                case $command instanceof TemplateCommand:
103
                    $template = $command->getTemplate();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Shopware\Psh\ScriptRuntime\Command as the method getTemplate() does only exist in the following implementations of said interface: Shopware\Psh\ScriptRuntime\TemplateCommand.

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...
104
105
                    $this->logger->logStart(
106
                        'Template',
107
                        $template->getDestination(),
108
                        $command->getLineNumber(),
109
                        false,
110
                        $index,
111
                        count($commands)
112
                    );
113
114
                    $this->renderTemplate($template);
115
                    break;
116
117
                case $command instanceof WaitCommand:
118
                    $this->logger->logStart(
119
                        'Waiting',
120
                        '',
121
                        $command->getLineNumber(),
122
                        false,
123
                        $index,
124
                        count($commands)
125
                    );
126
127
                    $this->waitForDeferredProcesses();
128
                    break;
129
            }
130
        }
131
132
        $this->waitForDeferredProcesses();
133
134
        $this->logger->finishScript($script);
135
    }
136
137
    private function executeTemplateRendering()
138
    {
139
        foreach ($this->environment->getTemplates() as $template) {
140
            $this->renderTemplate($template);
141
        }
142
    }
143
144
    /**
145
     * @param ProcessCommand $command
146
     * @return string
147
     */
148
    protected function getParsedShellCommand(ProcessCommand $command): string
149
    {
150
        $rawShellCommand = $command->getShellCommand();
151
152
        $parsedCommand = $this->templateEngine->render(
153
            $rawShellCommand,
154
            $this->environment->getAllValues()
155
        );
156
157
        return $parsedCommand;
158
    }
159
160
    /**
161
     * @param Process $process
162
     */
163
    private function setProcessDefaults(ProcessCommand $command, Process $process)
164
    {
165
        $process->setWorkingDirectory($this->applicationDirectory);
166
        $process->setTimeout(0);
167
        $process->setTty($command->isTTy());
168
    }
169
170
    /**
171
     * @param Process $process
172
     */
173
    private function runProcess(Process $process)
174
    {
175
        $process->run(function ($type, $response) {
176
            $this->logger->log(new LogMessage($response, $type === Process::ERR));
177
        });
178
    }
179
180
    /**
181
     * @param ProcessCommand $command
182
     * @param Process $process
183
     */
184
    protected function testProcessResultValid(ProcessCommand $command, Process $process)
185
    {
186
        if (!$this->isProcessResultValid($command, $process)) {
187
            throw new ExecutionErrorException('Command exited with Error');
188
        }
189
    }
190
191
    /**
192
     * @param $template
193
     */
194
    private function renderTemplate(Template $template)
195
    {
196
        $renderedTemplateDestination = $this->templateEngine
197
            ->render($template->getDestination(), $this->environment->getAllValues());
198
199
        $template->setDestination($renderedTemplateDestination);
200
201
        $renderedTemplateContent = $this->templateEngine
202
            ->render($template->getContent(), $this->environment->getAllValues());
203
204
        $template->setContents($renderedTemplateContent);
205
    }
206
207
    private function waitForDeferredProcesses()
208
    {
209
        $this->logger->logWait();
210
211
        foreach ($this->deferredProcesses as $index => $deferredProcess) {
212
            $deferredProcess->getProcess()->wait();
213
214
            $this->logger->logStart(
215
                'Output from',
216
                $deferredProcess->getCommand()->getShellCommand(),
217
                $deferredProcess->getCommand()->getLineNumber(),
218
                $deferredProcess->getCommand()->isIgnoreError(),
219
                $index,
220
                count($this->deferredProcesses)
221
            );
222
223
            foreach ($deferredProcess->getLog() as $logMessage) {
224
                $this->logger->log($logMessage);
225
            }
226
227
            if ($this->isProcessResultValid($deferredProcess->getCommand(), $deferredProcess->getProcess())) {
228
                $this->logger->logSuccess();
229
            } else {
230
                $this->logger->logFailure();
231
            }
232
        }
233
234
        foreach ($this->deferredProcesses as $deferredProcess) {
235
            $this->testProcessResultValid($deferredProcess->getCommand(), $deferredProcess->getProcess());
236
        }
237
238
        $this->deferredProcesses = [];
239
    }
240
241
    private function deferProcess(ProcessCommand $command, Process $process)
242
    {
243
        $deferredProcess = new DeferredProcess($command, $process);
244
245
        $process->start(function ($type, $response) use ($deferredProcess) {
246
            $deferredProcess->log(new LogMessage($response, $type === Process::ERR));
247
        });
248
249
        $this->deferredProcesses[] = $deferredProcess;
250
    }
251
252
    /**
253
     * @param ProcessCommand $command
254
     * @param Process $process
255
     * @return bool
256
     */
257
    protected function isProcessResultValid(ProcessCommand $command, Process $process): bool
258
    {
259
        return $command->isIgnoreError() || $process->isSuccessful();
260
    }
261
}
262