Completed
Push — master ( be53ba...d8ac27 )
by Jan Philipp
11s
created

ProcessExecutor::setUpProcess()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
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
        try {
69
            foreach ($commands as $index => $command) {
70
                $this->executeCommand($command, $index, count($commands));
71
            }
72
        } finally {
73
            $this->waitForDeferredProcesses();
74
        }
75
76
        $this->logger->finishScript($script);
77
    }
78
79
    /**
80
     * @param Command $command
81
     * @param int $index
82
     * @param int $totalCount
83
     */
84
    private function executeCommand(Command $command, int $index, int $totalCount)
85
    {
86
        switch ($command) {
87
            case $command instanceof ProcessCommand:
88
                $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...
89
                $process = $this->environment->createProcess($parsedCommand);
90
                $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...
91
92
                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...
93
                    $this->logger->logStart(
94
                        'Deferring',
95
                        $parsedCommand,
96
                        $command->getLineNumber(),
97
                        $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...
98
                        $index,
99
                        $totalCount
100
                    );
101
                    $this->deferProcess($parsedCommand, $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...
102
                } else {
103
                    $this->logger->logStart(
104
                        'Starting',
105
                        $parsedCommand,
106
                        $command->getLineNumber(),
107
                        $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...
108
                        $index,
109
                        $totalCount
110
                    );
111
112
                    $this->runProcess($process);
113
                    $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...
114
                }
115
116
                break;
117
118
119
            case $command instanceof TemplateCommand:
120
                $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...
121
122
                $this->logger->logStart(
123
                    'Template',
124
                    $template->getDestination(),
125
                    $command->getLineNumber(),
126
                    false,
127
                    $index,
128
                    $totalCount
129
                );
130
131
                $this->renderTemplate($template);
132
                break;
133
134
            case $command instanceof WaitCommand:
135
                $this->logger->logStart(
136
                    'Waiting',
137
                    '',
138
                    $command->getLineNumber(),
139
                    false,
140
                    $index,
141
                    $totalCount
142
                );
143
144
                $this->waitForDeferredProcesses();
145
                break;
146
        }
147
    }
148
149
    private function executeTemplateRendering()
150
    {
151
        foreach ($this->environment->getTemplates() as $template) {
152
            $this->renderTemplate($template);
153
        }
154
    }
155
156
    /**
157
     * @param ProcessCommand $command
158
     * @return string
159
     */
160
    protected function getParsedShellCommand(ProcessCommand $command): string
161
    {
162
        $rawShellCommand = $command->getShellCommand();
163
164
        $parsedCommand = $this->templateEngine->render(
165
            $rawShellCommand,
166
            $this->environment->getAllValues()
167
        );
168
169
        return $parsedCommand;
170
    }
171
172
    /**
173
     * @param Process $process
174
     */
175
    private function setProcessDefaults(ProcessCommand $command, Process $process)
176
    {
177
        $process->setWorkingDirectory($this->applicationDirectory);
178
        $process->setTimeout(0);
179
        $process->setTty($command->isTTy());
180
    }
181
182
    /**
183
     * @param Process $process
184
     */
185
    private function runProcess(Process $process)
186
    {
187
        $process->run(function ($type, $response) {
188
            $this->logger->log(new LogMessage($response, $type === Process::ERR));
189
        });
190
    }
191
192
    /**
193
     * @param ProcessCommand $command
194
     * @param Process $process
195
     */
196
    protected function testProcessResultValid(ProcessCommand $command, Process $process)
197
    {
198
        if (!$this->isProcessResultValid($command, $process)) {
199
            throw new ExecutionErrorException('Command exited with Error');
200
        }
201
    }
202
203
    /**
204
     * @param $template
205
     */
206
    private function renderTemplate(Template $template)
207
    {
208
        $renderedTemplateDestination = $this->templateEngine
209
            ->render($template->getDestination(), $this->environment->getAllValues());
210
211
        $template->setDestination($renderedTemplateDestination);
212
213
        $renderedTemplateContent = $this->templateEngine
214
            ->render($template->getContent(), $this->environment->getAllValues());
215
216
        $template->setContents($renderedTemplateContent);
217
    }
218
219
    private function waitForDeferredProcesses()
220
    {
221
        if (!$this->deferredProcesses) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->deferredProcesses of type Shopware\Psh\ScriptRuntime\DeferredProcess[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
222
            return;
223
        }
224
225
        $this->logger->logWait();
226
227
        foreach ($this->deferredProcesses as $index => $deferredProcess) {
228
            $deferredProcess->getProcess()->wait();
229
230
            $this->logger->logStart(
231
                'Output from',
232
                $deferredProcess->getParsedCommand(),
233
                $deferredProcess->getCommand()->getLineNumber(),
234
                $deferredProcess->getCommand()->isIgnoreError(),
235
                $index,
236
                count($this->deferredProcesses)
237
            );
238
239
            foreach ($deferredProcess->getLog() as $logMessage) {
240
                $this->logger->log($logMessage);
241
            }
242
243
            if ($this->isProcessResultValid($deferredProcess->getCommand(), $deferredProcess->getProcess())) {
244
                $this->logger->logSuccess();
245
            } else {
246
                $this->logger->logFailure();
247
            }
248
        }
249
250
        foreach ($this->deferredProcesses as $deferredProcess) {
251
            $this->testProcessResultValid($deferredProcess->getCommand(), $deferredProcess->getProcess());
252
        }
253
254
        $this->deferredProcesses = [];
255
    }
256
257
    /**
258
     * @param ProcessCommand $command
259
     * @param Process $process
260
     */
261
    private function deferProcess(string $parsedCommand, ProcessCommand $command, Process $process)
262
    {
263
        $deferredProcess = new DeferredProcess($parsedCommand, $command, $process);
264
265
        $process->start(function ($type, $response) use ($deferredProcess) {
266
            $deferredProcess->log(new LogMessage($response, $type === Process::ERR));
267
        });
268
269
        $this->deferredProcesses[] = $deferredProcess;
270
    }
271
272
    /**
273
     * @param ProcessCommand $command
274
     * @param Process $process
275
     * @return bool
276
     */
277
    protected function isProcessResultValid(ProcessCommand $command, Process $process): bool
278
    {
279
        return $command->isIgnoreError() || $process->isSuccessful();
280
    }
281
}
282