1
|
|
|
<?php declare(strict_types=1); |
2
|
|
|
|
3
|
|
|
namespace Shopware\Psh\Application; |
4
|
|
|
|
5
|
|
|
use InvalidArgumentException; |
6
|
|
|
use Khill\Duration\Duration; |
7
|
|
|
use League\CLImate\CLImate; |
8
|
|
|
use Shopware\Psh\Config\Config; |
9
|
|
|
use Shopware\Psh\Config\RequiredValue; |
10
|
|
|
use Shopware\Psh\Listing\Script; |
11
|
|
|
use Shopware\Psh\Listing\ScriptFinder; |
12
|
|
|
use Shopware\Psh\Listing\ScriptNotFoundException; |
13
|
|
|
use Shopware\Psh\ScriptRuntime\Execution\ExecutionErrorException; |
14
|
|
|
use function array_key_exists; |
15
|
|
|
use function array_map; |
16
|
|
|
use function array_merge; |
17
|
|
|
use function count; |
18
|
|
|
use function explode; |
19
|
|
|
use function implode; |
20
|
|
|
use function mb_strlen; |
21
|
|
|
use function sprintf; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* Main application entry point. moves the requested data around and outputs user information. |
25
|
|
|
*/ |
26
|
|
|
class Application |
27
|
|
|
{ |
28
|
|
|
const MIN_PADDING_SIZE = 30; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @var CLImate |
32
|
|
|
*/ |
33
|
|
|
public $cliMate; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* @var string |
37
|
|
|
*/ |
38
|
|
|
private $rootDirectory; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* @var ApplicationFactory |
42
|
|
|
*/ |
43
|
|
|
private $applicationFactory; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* @var Duration |
47
|
|
|
*/ |
48
|
|
|
private $duration; |
49
|
|
|
|
50
|
|
|
public function __construct(string $rootDirectory) |
51
|
|
|
{ |
52
|
|
|
$this->rootDirectory = $rootDirectory; |
53
|
|
|
$this->applicationFactory = new ApplicationFactory(); |
54
|
|
|
$this->cliMate = new CLImate(); |
55
|
|
|
$this->duration = new Duration(); |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* Main entry point to execute the application. |
60
|
|
|
* |
61
|
|
|
* @return int exit code |
62
|
|
|
*/ |
63
|
|
|
public function run(array $inputArgs): int |
64
|
|
|
{ |
65
|
|
|
try { |
66
|
|
|
$config = $this->prepare($inputArgs); |
67
|
|
|
|
68
|
|
|
$scriptFinder = $this->applicationFactory->createScriptFinder($config); |
69
|
|
|
|
70
|
|
|
$this->executeScript($inputArgs, $scriptFinder, $config); |
71
|
|
|
|
72
|
|
|
$this->showListing($scriptFinder->getAllVisibleScripts()); |
73
|
|
|
|
74
|
|
|
throw ExitSignal::success(); |
75
|
|
|
} catch (ExitSignal $signal) { |
76
|
|
|
return $signal->signal(); |
77
|
|
|
} |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* @param Script[] $scripts |
82
|
|
|
*/ |
83
|
|
|
public function showListing(array $scripts): void |
84
|
|
|
{ |
85
|
|
|
$this->cliMate->green()->bold('Available commands:')->br(); |
86
|
|
|
|
87
|
|
|
if (!count($scripts)) { |
88
|
|
|
$this->cliMate->yellow()->bold('-> Currently no scripts available'); |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
$paddingSize = $this->getPaddingSize($scripts); |
92
|
|
|
$padding = $this->cliMate->padding($paddingSize)->char(' '); |
93
|
|
|
|
94
|
|
|
$scriptEnvironment = false; |
95
|
|
|
|
96
|
|
|
foreach ($scripts as $script) { |
97
|
|
|
if ($script->getEnvironment() !== $scriptEnvironment) { |
98
|
|
|
$scriptEnvironment = $script->getEnvironment(); |
99
|
|
|
$this->cliMate->green()->br()->bold(($scriptEnvironment ?? 'default') . ':'); |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
$padding |
103
|
|
|
->label('<bold> - ' . $script->getName() . '</bold>') |
104
|
|
|
->result('<dim>' . $script->getDescription() . '</dim>'); |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
$this->cliMate->green()->bold("\n" . count($scripts) . " script(s) available\n"); |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
private function extractScriptNames(array $inputArgs): array |
111
|
|
|
{ |
112
|
|
|
if (!isset($inputArgs[1])) { |
113
|
|
|
return []; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
return explode(',', $inputArgs[1]); |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
private function execute(Script $script, Config $config, ScriptFinder $scriptFinder): void |
120
|
|
|
{ |
121
|
|
|
$commands = $this->applicationFactory |
122
|
|
|
->createCommands($script, $scriptFinder); |
123
|
|
|
|
124
|
|
|
$logger = new ClimateLogger($this->cliMate, $this->duration); |
125
|
|
|
$executor = $this->applicationFactory |
126
|
|
|
->createProcessExecutor($script, $config, $logger, $this->rootDirectory); |
127
|
|
|
|
128
|
|
|
try { |
129
|
|
|
$executor->execute($script, $commands); |
130
|
|
|
} catch (ExecutionErrorException $e) { |
131
|
|
|
$this->notifyError("\nExecution aborted, a subcommand failed!\n"); |
132
|
|
|
|
133
|
|
|
throw ExitSignal::error(); |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
$this->notifySuccess("All commands successfully executed!\n"); |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* @param $string |
141
|
|
|
*/ |
142
|
|
|
private function notifySuccess(string $string): void |
143
|
|
|
{ |
144
|
|
|
$this->cliMate->bold()->green($string); |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* @param $string |
149
|
|
|
*/ |
150
|
|
|
public function notifyError(string $string): void |
151
|
|
|
{ |
152
|
|
|
$this->cliMate->bold()->red($string); |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
private function getPaddingSize(array $scripts): int |
156
|
|
|
{ |
157
|
|
|
$maxScriptNameLength = 0; |
158
|
|
|
foreach ($scripts as $script) { |
159
|
|
|
if (mb_strlen($script->getName()) > $maxScriptNameLength) { |
160
|
|
|
$maxScriptNameLength = mb_strlen($script->getName()); |
161
|
|
|
} |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
return $maxScriptNameLength + self::MIN_PADDING_SIZE; |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
private function showAutocompleteListing(Config $config): void |
168
|
|
|
{ |
169
|
|
|
$scriptFinder = $this->applicationFactory |
170
|
|
|
->createScriptFinder($config); |
171
|
|
|
|
172
|
|
|
$scripts = $scriptFinder->getAllVisibleScripts(); |
173
|
|
|
|
174
|
|
|
$commands = array_map(function (Script $script) { |
175
|
|
|
return $script->getName(); |
176
|
|
|
}, $scripts); |
177
|
|
|
|
178
|
|
|
$this->cliMate->out(implode(' ', $commands)); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
private function showScriptNotFoundListing(ScriptNotFoundException $ex, array $scriptNames, ScriptFinder $scriptFinder): void |
182
|
|
|
{ |
183
|
|
|
$this->notifyError("Script with name {$ex->getScriptName()} not found\n"); |
184
|
|
|
|
185
|
|
|
$scripts = []; |
186
|
|
|
foreach ($scriptNames as $scriptName) { |
187
|
|
|
$newScripts = $scriptFinder->findScriptsByPartialName($scriptName); |
188
|
|
|
$scripts = array_merge($scripts, $newScripts); |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
if (count($scripts) > 0) { |
192
|
|
|
$this->cliMate->yellow()->bold('Have you been looking for this?'); |
193
|
|
|
$this->showListing($scripts); |
194
|
|
|
} |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
private function printHead(Config $config, ApplicationConfigLogger $logger): void |
198
|
|
|
{ |
199
|
|
|
$this->cliMate->green()->bold()->out("\n###################"); |
200
|
|
|
|
201
|
|
|
if ($config->getHeader()) { |
202
|
|
|
$this->cliMate->out("\n" . $config->getHeader()); |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
$logger->printOut($this->cliMate); |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
private function validateConfig(Config $config, ?string $environment = null): void |
209
|
|
|
{ |
210
|
|
|
$allPlaceholders = $config->getAllPlaceholders($environment); |
211
|
|
|
|
212
|
|
|
$missing = []; |
213
|
|
|
foreach ($config->getRequiredVariables($environment) as $requiredVariable) { |
214
|
|
|
if (!array_key_exists($requiredVariable->getName(), $allPlaceholders)) { |
215
|
|
|
$missing[] = $requiredVariable; |
216
|
|
|
$this->printMissingRequiredVariable($requiredVariable); |
217
|
|
|
} |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
if (count($missing)) { |
221
|
|
|
$this->cliMate->error("\n<bold>Please define the missing value(s) first</bold>\n"); |
222
|
|
|
throw ExitSignal::error(); |
223
|
|
|
} |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
private function printMissingRequiredVariable(RequiredValue $requiredVariable): void |
227
|
|
|
{ |
228
|
|
|
if ($requiredVariable->hasDescription()) { |
229
|
|
|
$this->cliMate->error(sprintf( |
230
|
|
|
"\t - <bold>Missing required const or var named <underline>%s</underline></bold> <dim>(%s)</dim>", |
231
|
|
|
$requiredVariable->getName(), |
232
|
|
|
$requiredVariable->getDescription() |
233
|
|
|
)); |
234
|
|
|
} else { |
235
|
|
|
$this->cliMate->error(sprintf( |
236
|
|
|
"\t - <bold>Missing required const or var named <underline>%s</underline></bold>", |
237
|
|
|
$requiredVariable->getName() |
238
|
|
|
)); |
239
|
|
|
} |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
private function prepare(array $inputArgs): Config |
243
|
|
|
{ |
244
|
|
|
$configLogger = new ApplicationConfigLogger($this->rootDirectory, $this->cliMate); |
|
|
|
|
245
|
|
|
|
246
|
|
|
try { |
247
|
|
|
$config = $this->applicationFactory |
248
|
|
|
->createConfig($configLogger, $this->rootDirectory, $inputArgs); |
249
|
|
|
} catch (InvalidParameterException | InvalidArgumentException $e) { |
250
|
|
|
$this->notifyError("\n" . $e->getMessage() . "\n"); |
251
|
|
|
|
252
|
|
|
throw ExitSignal::error(); |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
if (count($inputArgs) > 1 && $inputArgs[1] === 'bash_autocompletion_dump') { |
256
|
|
|
$this->showAutocompleteListing($config); |
257
|
|
|
|
258
|
|
|
throw ExitSignal::success(); |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
$this->printHead($config, $configLogger); |
262
|
|
|
$this->validateConfig($config); |
263
|
|
|
|
264
|
|
|
return $config; |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
private function executeScript(array $inputArgs, ScriptFinder $scriptFinder, Config $config): void |
268
|
|
|
{ |
269
|
|
|
$scriptNames = $this->extractScriptNames($inputArgs); |
270
|
|
|
|
271
|
|
|
if (!count($scriptNames)) { |
272
|
|
|
return; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
try { |
276
|
|
|
foreach ($scriptNames as $scriptName) { |
277
|
|
|
$this->execute($scriptFinder->findScriptByName($scriptName), $config, $scriptFinder); |
278
|
|
|
} |
279
|
|
|
} catch (ScriptNotFoundException $e) { |
280
|
|
|
$this->showScriptNotFoundListing($e, $scriptNames, $scriptFinder); |
281
|
|
|
|
282
|
|
|
throw ExitSignal::error(); |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
throw ExitSignal::success(); |
286
|
|
|
} |
287
|
|
|
} |
288
|
|
|
|
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignore
PhpDoc annotation to the duplicate definition and it will be ignored.