Completed
Pull Request — master (#82)
by Jan Philipp
01:32
created

ProcessExecutor::logTemplateStart()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.9
c 0
b 0
f 0
cc 1
nc 1
nop 4
1
<?php declare(strict_types=1);
2
3
4
namespace Shopware\Psh\ScriptRuntime\Execution;
5
6
use Shopware\Psh\Listing\Script;
7
use Shopware\Psh\ScriptRuntime\BashCommand;
8
use Shopware\Psh\ScriptRuntime\Command;
9
use Shopware\Psh\ScriptRuntime\DeferredProcessCommand;
10
use Shopware\Psh\ScriptRuntime\ProcessCommand;
11
use Shopware\Psh\ScriptRuntime\SynchronusProcessCommand;
12
use Shopware\Psh\ScriptRuntime\TemplateCommand;
13
use Shopware\Psh\ScriptRuntime\WaitCommand;
14
use Symfony\Component\Process\Process;
15
16
/**
17
 * Execute a command in a separate process
18
 */
19
class ProcessExecutor
20
{
21
    /**
22
     * @var ProcessEnvironment
23
     */
24
    private $environment;
25
26
    /**
27
     * @var TemplateEngine
28
     */
29
    private $templateEngine;
30
31
    /**
32
     * @var Logger
33
     */
34
    private $logger;
35
36
    /**
37
     * @var string
38
     */
39
    private $applicationDirectory;
40
41
    /**
42
     * @var DeferredProcess[]
43
     */
44
    private $deferredProcesses = [];
45
46
    /**
47
     * ProcessExecutor constructor.
48
     * @param ProcessEnvironment $environment
49
     * @param TemplateEngine $templateEngine
50
     * @param Logger $logger
51
     * @param string $applicationDirectory
52
     */
53
    public function __construct(
54
        ProcessEnvironment $environment,
55
        TemplateEngine $templateEngine,
56
        Logger $logger,
57
        string $applicationDirectory
58
    ) {
59
        $this->environment = $environment;
60
        $this->templateEngine = $templateEngine;
61
        $this->logger = $logger;
62
        $this->applicationDirectory = $applicationDirectory;
63
    }
64
65
    /**
66
     * @param Script $script
67
     * @param Command[] $commands
68
     */
69
    public function execute(Script $script, array $commands)
70
    {
71
        $this->logger->startScript($script);
72
73
        $this->executeTemplateRendering();
74
75
        try {
76
            foreach ($commands as $index => $command) {
77
                $this->executeCommand($command, $index, count($commands));
78
            }
79
        } finally {
80
            $this->waitForDeferredProcesses();
81
        }
82
83
        $this->logger->finishScript($script);
84
    }
85
86
    /**
87
     * @param Command $command
88
     * @param int $index
89
     * @param int $totalCount
90
     */
91
    private function executeCommand(Command $command, int $index, int $totalCount)
92
    {
93
        switch (true) {
94
            case $command instanceof BashCommand:
95
                $originalContent = file_get_contents($command->getScript()->getPath());
96
97
                try {
98
                    file_put_contents($command->getScript()->getPath(), $this->templateEngine->render($originalContent, $this->environment->getAllValues()));
99
100
                    $process = $this->environment->createProcess($command->getScript()->getPath());
101
                    $this->setProcessDefaults($process, false);
102
                    $this->logBashStart($command, $index, $totalCount);
103
                    $this->runProcess($process);
104
                    $this->testProcessResultValid($process, false);
105
                } finally {
106
                    file_put_contents($command->getScript()->getPath(), $originalContent);
107
                }
108
109
                break;
110 View Code Duplication
            case $command instanceof SynchronusProcessCommand:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
111
                $parsedCommand = $this->getParsedShellCommand($command);
112
                $process = $this->environment->createProcess($parsedCommand);
113
                $this->setProcessDefaults($process, $command->isTTy());
114
                $this->logSynchronousProcessStart($command, $index, $totalCount, $parsedCommand);
115
                $this->runProcess($process);
116
                $this->testProcessResultValid($process, $command->isIgnoreError());
117
118
                break;
119 View Code Duplication
            case $command instanceof DeferredProcessCommand:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
120
                $parsedCommand = $this->getParsedShellCommand($command);
121
                $process = $this->environment->createProcess($parsedCommand);
122
                $this->setProcessDefaults($process, $command->isTTy());
123
                $this->logDeferedStart($command, $index, $totalCount, $parsedCommand);
124
                $this->deferProcess($parsedCommand, $command, $process);
125
126
                break;
127
            case $command instanceof TemplateCommand:
128
                $template = $command->createTemplate();
129
                $this->logTemplateStart($command, $index, $totalCount, $template);
130
                $this->renderTemplate($template);
131
132
                break;
133
            case $command instanceof WaitCommand:
134
                $this->logWaitStart($command, $index, $totalCount);
135
                $this->waitForDeferredProcesses();
136
137
                break;
138
        }
139
    }
140
141
    private function executeTemplateRendering()
142
    {
143
        foreach ($this->environment->getTemplates() as $template) {
144
            $this->renderTemplate($template);
145
        }
146
    }
147
148
    /**
149
     * @param ProcessCommand $command
150
     * @return string
151
     */
152
    protected function getParsedShellCommand(ProcessCommand $command): string
153
    {
154
        $rawShellCommand = $command->getShellCommand();
155
156
        $parsedCommand = $this->templateEngine->render(
157
            $rawShellCommand,
158
            $this->environment->getAllValues()
159
        );
160
161
        return $parsedCommand;
162
    }
163
164
    /**
165
     * @param Process $process
166
     * @param bool $isTty
167
     */
168
    private function setProcessDefaults(Process $process, bool $isTty)
169
    {
170
        $process->setWorkingDirectory($this->applicationDirectory);
171
        $process->setTimeout(0);
172
        $process->setTty($isTty);
173
    }
174
175
    /**
176
     * @param Process $process
177
     */
178
    private function runProcess(Process $process)
179
    {
180
        $process->run(function ($type, $response) {
181
            $this->logger->log(new LogMessage($response, $type === Process::ERR));
182
        });
183
    }
184
185
    /**
186
     * @param Process $process
187
     * @param bool $ignoreError
188
     */
189
    protected function testProcessResultValid(Process $process, bool $ignoreError)
190
    {
191
        if (!$this->isProcessResultValid($process, $ignoreError)) {
192
            throw new ExecutionErrorException('Command exited with Error');
193
        }
194
    }
195
196
    /**
197
     * @param $template
198
     */
199
    private function renderTemplate(Template $template)
200
    {
201
        $renderedTemplateDestination = $this->templateEngine
202
            ->render($template->getDestination(), $this->environment->getAllValues());
203
204
        $template->setDestination($renderedTemplateDestination);
205
206
        $renderedTemplateContent = $this->templateEngine
207
            ->render($template->getContent(), $this->environment->getAllValues());
208
209
        $template->setContents($renderedTemplateContent);
210
    }
211
212
    private function waitForDeferredProcesses()
213
    {
214
        if (count($this->deferredProcesses) === 0) {
215
            return;
216
        }
217
218
        $this->logger->logWait();
219
220
        foreach ($this->deferredProcesses as $index => $deferredProcess) {
221
            $deferredProcess->getProcess()->wait();
222
223
            $this->logger->logStart(
224
                'Output from',
225
                $deferredProcess->getParsedCommand(),
226
                $deferredProcess->getCommand()->getLineNumber(),
227
                $deferredProcess->getCommand()->isIgnoreError(),
228
                $index,
229
                count($this->deferredProcesses)
230
            );
231
232
            foreach ($deferredProcess->getLog() as $logMessage) {
233
                $this->logger->log($logMessage);
234
            }
235
236
            if ($this->isProcessResultValid($deferredProcess->getProcess(), $deferredProcess->getCommand()->isIgnoreError())) {
237
                $this->logger->logSuccess();
238
            } else {
239
                $this->logger->logFailure();
240
            }
241
        }
242
243
        foreach ($this->deferredProcesses as $deferredProcess) {
244
            $this->testProcessResultValid($deferredProcess->getProcess(), $deferredProcess->getCommand()->isIgnoreError());
245
        }
246
247
        $this->deferredProcesses = [];
248
    }
249
250
    /**
251
     * @param string $parsedCommand
252
     * @param DeferredProcessCommand $command
253
     * @param Process $process
254
     */
255
    private function deferProcess(string $parsedCommand, DeferredProcessCommand $command, Process $process)
256
    {
257
        $deferredProcess = new DeferredProcess($parsedCommand, $command, $process);
258
259
        $process->start(function ($type, $response) use ($deferredProcess) {
260
            $deferredProcess->log(new LogMessage($response, $type === Process::ERR));
261
        });
262
263
        $this->deferredProcesses[] = $deferredProcess;
264
    }
265
266
    /**
267
     * @param Process $process
268
     * @param bool $ignoreError
269
     * @return bool
270
     */
271
    protected function isProcessResultValid(Process $process, bool $ignoreError): bool
272
    {
273
        return $ignoreError || $process->isSuccessful();
274
    }
275
276
    /**
277
     * @param Command $command
278
     * @param int $index
279
     * @param int $totalCount
280
     */
281
    private function logWaitStart(Command $command, int $index, int $totalCount)
282
    {
283
        $this->logger->logStart(
284
            'Waiting',
285
            '',
286
            $command->getLineNumber(),
287
            false,
288
            $index,
289
            $totalCount
290
        );
291
    }
292
293
    /**
294
     * @param Command $command
295
     * @param int $index
296
     * @param int $totalCount
297
     * @param Template $template
298
     */
299
    private function logTemplateStart(Command $command, int $index, int $totalCount, Template $template)
300
    {
301
        $this->logger->logStart(
302
            'Template',
303
            $template->getDestination(),
304
            $command->getLineNumber(),
305
            false,
306
            $index,
307
            $totalCount
308
        );
309
    }
310
311
    /**
312
     * @param Command $command
313
     * @param int $index
314
     * @param int $totalCount
315
     * @param string $parsedCommand
316
     */
317
    private function logDeferedStart(Command $command, int $index, int $totalCount, string $parsedCommand)
318
    {
319
        $this->logger->logStart(
320
            'Defering',
321
            $parsedCommand,
322
            $command->getLineNumber(),
323
            $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\DeferredProcessCommand, Shopware\Psh\ScriptRunti...ynchronusProcessCommand.

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...
324
            $index,
325
            $totalCount
326
        );
327
    }
328
329
    /**
330
     * @param Command $command
331
     * @param int $index
332
     * @param int $totalCount
333
     * @param string $parsedCommand
334
     */
335
    private function logSynchronousProcessStart(Command $command, int $index, int $totalCount, string $parsedCommand)
336
    {
337
        $this->logger->logStart(
338
            'Starting',
339
            $parsedCommand,
340
            $command->getLineNumber(),
341
            $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\DeferredProcessCommand, Shopware\Psh\ScriptRunti...ynchronusProcessCommand.

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...
342
            $index,
343
            $totalCount
344
        );
345
    }
346
347
    /**
348
     * @param Command $command
349
     * @param int $index
350
     * @param int $totalCount
351
     */
352
    private function logBashStart(Command $command, int $index, int $totalCount)
353
    {
354
        $this->logger->logStart(
355
            'Executing',
356
            $command->getScript()->getPath(),
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 getScript() does only exist in the following implementations of said interface: Shopware\Psh\ScriptRuntime\BashCommand.

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...
357
            $command->getLineNumber(),
358
            false,
359
            $index,
360
            $totalCount
361
        );
362
    }
363
}
364