-
Notifications
You must be signed in to change notification settings - Fork 0
/
BackendCommands.php
520 lines (467 loc) · 17.4 KB
/
BackendCommands.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
<?php
namespace Drush\Commands\BurdaStyleGroup;
use Consolidation\AnnotatedCommand\AnnotationData;
use Drupal\Core\Site\Settings;
use Consolidation\AnnotatedCommand\CommandData;
use Drush\Commands\DrushCommands;
use Drush\Drush;
use Drush\SiteAlias\SiteAliasManagerAwareInterface;
use Drush\Sql\SqlBase;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
include "BackendCommandsTrait.php";
/**
* Backend drush commands.
*/
class BackendCommands extends DrushCommands implements SiteAliasManagerAwareInterface
{
use BackendCommandsTrait;
/**
* @var \Symfony\Component\Filesystem\Filesystem
*/
protected $filesystem;
/**
* BackendCommands constructor.
*/
public function __construct()
{
parent::__construct();
$this->filesystem = new Filesystem();
}
/**
* @hook init @options-backend
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Consolidation\AnnotatedCommand\AnnotationData $annotationData
*/
public function initCommands(InputInterface $input, AnnotationData $annotationData)
{
$this->projectDirectory = $input->getOption('project-directory') ?: Drush::bootstrapManager()->getComposerRoot();
}
/**
* Define default options for most backend commands.
*
* @hook option @options-backend
*
* @option project-directory The base directory of the project. Defaults to composer root of project.
*
* @param \Symfony\Component\Console\Command\Command $command
* @param \Consolidation\AnnotatedCommand\AnnotationData $annotationData
*/
public function optionsBackend(Command $command, AnnotationData $annotationData)
{
$command->addOption(
'project-directory',
'',
InputOption::VALUE_NONE,
'The base directory of the project. Defaults to composer root of project. Option added by burdastyle backend commands.'
);
}
/**
* Prepare file system and code to be ready for install.
*
* @hook pre-command backend:install
*
* @param \Consolidation\AnnotatedCommand\CommandData $commandData
*/
public function preInstallCommand(CommandData $commandData)
{
$this->populateConfigSyncDirectory();
}
/**
* Install a BurdaStyle backend site from existing configuration.
*
* @command backend:install
*
* @aliases backend:si
*
* @options-backend
*
* @usage drush @elle backend:install-site
* Installs elle project from config.
*
* @bootstrap config
*
* @kernel installer
*/
public function install()
{
// Do the site install.
$this->drush($this->selfRecord(), 'site:install', [], ['existing-config' => true, 'yes' => $this->input()->getOption('yes')]);
// Clear caches.
$this->drush($this->selfRecord(), 'cache:rebuild');
}
/**
* Clean-up installation side-effects.
*
* @hook post-command backend:install
*
* @param $result
* @param \Consolidation\AnnotatedCommand\CommandData $commandData
*/
public function postInstallCommand($result, CommandData $commandData)
{
$this->process(['git', 'checkout', $this->siteDirectory().'/settings.php'], $this->projectDirectory());
}
/**
* Update the BurdaStyle backend code including translations.
*
* @command backend:update-code
*
* @options-backend
*
* @usage drush backend:update-code
* Update code and translation files.
*/
public function updateCode()
{
$this->process(['composer', 'update'], $this->projectDirectory());
$this->process(['scripts/update-po.sh'], $this->projectDirectory());
}
/**
* Update a BurdaStyle backend database for a specific site.
*
* @command backend:update-database
*
* @aliases backend:updb
*
* @options-backend
*
* @usage drush @elle backend:update-databases
* Update the database for elle.
*/
public function updateDatabase()
{
$this->drush($this->selfRecord(), 'updatedb', [], ['yes' => $this->input()->getOption('yes')]);
$this->drush($this->selfRecord(), 'cache:rebuild');
$this->drush($this->selfRecord(), 'locale-update', [], ['yes' => $this->input()->getOption('yes')]);
}
/**
* Add option to command.
*
* @hook option config:export
*
* @param \Symfony\Component\Console\Command\Command $command
* @param \Consolidation\AnnotatedCommand\AnnotationData $annotationData
*/
public function additionalConfigExportOptions(Command $command, AnnotationData $annotationData)
{
$command->addOption(
'project-directory',
'',
InputOption::VALUE_NONE,
'The base directory of the project. Defaults to composer root of project. Option added by burdastyle backend commands.'
);
}
/**
* @hook init config:export
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Consolidation\AnnotatedCommand\AnnotationData $annotationData
*/
public function initConfigExportCommands(InputInterface $input, AnnotationData $annotationData)
{
$this->initCommands($input, $annotationData);
}
/**
* Add option to command.
*
* @hook option config:import
*
* @param \Symfony\Component\Console\Command\Command $command
* @param \Consolidation\AnnotatedCommand\AnnotationData $annotationData
*/
public function additionalConfigImportOptions(Command $command, AnnotationData $annotationData)
{
$command->addOption(
'project-directory',
'',
InputOption::VALUE_NONE,
'The base directory of the project. Defaults to composer root of project. Option added by burdastyle backend commands.'
);
}
/**
* @hook init config:import
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Consolidation\AnnotatedCommand\AnnotationData $annotationData
*/
public function initConfigImportCommands(InputInterface $input, AnnotationData $annotationData)
{
$this->initCommands($input, $annotationData);
}
/**
* Runs populateConfigSyncDirectory() for config:export.
*
* @hook pre-command config:export
*
* @param \Consolidation\AnnotatedCommand\CommandData $commandData
*/
public function preConfigExportCommand(CommandData $commandData)
{
$this->populateConfigSyncDirectory();
}
/**
* Move files from sync folder to shared or override folders.
*
* @hook post-command config:export
*
* @param $result
* @param \Consolidation\AnnotatedCommand\CommandData $commandData
*/
public function postConfigExportCommand($result, CommandData $commandData)
{
$exportedFiles = $this->getConfigFilesInDirectory($this->siteConfigSyncDirectory());
$overrideFiles = $this->getConfigFilesInDirectory($this->siteConfigOverrideDirectory());
$sharedFiles = $this->getConfigFilesInDirectory($this->configSharedDirectory());
$modifiedFiles = [];
foreach ($exportedFiles as $fileName => $fullPath) {
// First check, if the file should be put into the override directory.
// otherwise check, if the file should be put into the shared directory.
// Finally, if file is new (neither in shared nor in override),
// put it into override and inform user of new config.
if (isset($overrideFiles[$fileName])) {
if (!$this->filesAreEqual($overrideFiles[$fileName], $fullPath)) {
$this->filesystem->copy($fullPath, $overrideFiles[$fileName], true);
$modifiedFiles[$fileName] = $fullPath;
}
} elseif (isset($sharedFiles[$fileName])) {
if (!$this->filesAreEqual($sharedFiles[$fileName], $fullPath)) {
$this->filesystem->copy($fullPath, $sharedFiles[$fileName], true);
$modifiedFiles[$fileName] = $fullPath;
}
} else {
$this->filesystem->copy($fullPath, $this->siteConfigOverrideDirectory().'/'.$fileName);
$this->io()->block('New configuration file "'.$fileName.'" was added to override folder. Please check, if that is the correct location.', 'INFO', 'fg=yellow');
$modifiedFiles[$fileName] = $fullPath;
}
}
// Remove files from override, that were not exported anymore.
foreach ($overrideFiles as $fileName => $fullPath) {
if (!isset($exportedFiles[$fileName])) {
$this->filesystem->remove($fullPath);
}
}
// Remove files from shared, that were not exported anymore.
foreach ($sharedFiles as $fileName => $fullPath) {
if (!isset($exportedFiles[$fileName])) {
$this->filesystem->remove($fullPath);
$this->io()->block('Configuration file "'.$fileName.'" was removed from the shared folder. Please check, if overridden config in other sub-sites has to be manually modified or deleted.', 'INFO', 'fg=yellow');
}
}
if (count($modifiedFiles)) {
$this->io()->block('Check all config files if they have been moved to the correct location!', 'INFO', 'fg=yellow');
}
}
/**
* Runs populateConfigSyncDirectory() for config:import.
*
* @hook pre-command config:import
*
* @param \Consolidation\AnnotatedCommand\CommandData $commandData
*/
public function preConfigImportCommand(CommandData $commandData)
{
$this->populateConfigSyncDirectory();
}
/**
* Prepare an update branch. Does code update, database update and config export.
*
* @command backend:prepare-update-branch
*
* @options-backend
*
* @usage drush backend:prepare-update-branch
* Updates code and all configurations for all sites.
* @usage drush @elle backend:prepare-update-branch
* Updates code and configurations for the site elle only.
*/
public function prepareUpdateBranch()
{
if ($this->selfRecord()->name() === '@self') {
$aliases = $this->supportedAliases();
} else {
$aliases = [$this->selfRecord()->name()];
}
// First install all sites with current code base.
$this->process(['composer', 'install'], $this->projectDirectory());
// We use ::process() instead of ::drush() in the following drush calls
// to be able to provide the @alias. with ::drush, this would be translated
// into --uri=http://domain.site, which we do not handle in code.
foreach ($aliases as $alias) {
// Install from config.
$this->process(['drush', $alias, 'backend:install'] + $this->getOptions(), $this->projectDirectory());
}
// Update codebase and translation files
$this->process(['drush', 'backend:update-code'] + $this->getOptions(), $this->projectDirectory());
// Update database and export config for all sites.
foreach ($aliases as $alias) {
$this->process(['drush', $alias, 'backend:update-database'] + $this->getOptions(), $this->projectDirectory());
$this->process(['drush', $alias, 'config:export'] + $this->getOptions(), $this->projectDirectory());
}
}
/**
* Creates a phpunit database generation script.
*
* @command backend:create-testing-dump
*
* @aliases backend:dump
*
* @options-backend
*
* @usage drush @elle backend:create-testing-dump
* Creates a phpunit database generation script for the site elle.
*
* @bootstrap config
*/
public function createTestingDump()
{
$sql = SqlBase::create();
$dbSpec = $sql->getDbSpec();
// Prepare settings file.
$defaultSettingsFile = $this->drupalRootDirectory().'/sites/default/settings.php';
if (file_exists($defaultSettingsFile)) {
$tmpName = tempnam($this->drupalRootDirectory().'/sites/default/', 'settings.tmp');
rename($defaultSettingsFile, $tmpName);
}
$this->prepareSettingsFile($defaultSettingsFile, $dbSpec);
$this->process(['php', 'core/scripts/db-tools.php', 'dump-database-d8-mysql'], $this->drupalRootDirectory());
// Cleanup settings file.
if (!empty($tmpName)) {
rename($tmpName, $defaultSettingsFile);
} else {
unlink($defaultSettingsFile);
}
}
/**
* Enable modules that are excluded from config export.
*
* @command backend:enable-dev-modules
*
* @options-backend
*
* @bootstrap full
*/
public function enableDevModules()
{
$modules = Settings::get('config_exclude_modules', []);
if (!count($modules)) {
$this->logger()->warning('No modules defined in $settings[\'config_exclude_modules\'].');
return;
}
$process = $this->processManager()->drush($this->siteAliasManager()->getSelf(), 'pm:enable', $modules, Drush::redispatchOptions());
$process->mustRun($process->showRealtime());
}
/**
* Gets a cleaned up array of $key=$value strings from the input options.
*
* @return string[]
*/
protected function getOptions()
{
$options = [];
foreach ($this->input()->getOptions() as $key => $value) {
if (!empty($value) && 'root' !== $key) {
$options[] = '--'.$key.'='.$value;
}
}
return $options;
}
/**
* Prepare the config/{site}/sync for being used by site-install, config-export and config-import.
*
* This is necessary, because config files are distributed to different folders.
*/
protected function populateConfigSyncDirectory(): void
{
$syncDirectory = $this->siteConfigSyncDirectory();
$syncFiles = $this->getConfigFilesInDirectory($syncDirectory);
$sharedFiles = $this->getConfigFilesInDirectory($this->configSharedDirectory());
$overrideFiles = $this->getConfigFilesInDirectory($this->siteConfigOverrideDirectory());
// Prepare config-sync directory for site install with existing config.
// First clean up the directory and copy shared config into
// config/{site}/sync, then overwrite this with files from config/{site}/override.
$this->filesystem->remove($syncFiles);
foreach ($sharedFiles as $fileName => $fullPath) {
$this->filesystem->copy($fullPath, $syncDirectory.'/'.$fileName, true);
}
foreach ($overrideFiles as $fileName => $fullPath) {
$this->filesystem->copy($fullPath, $syncDirectory.'/'.$fileName, true);
}
}
/**
* Get all config files in a given directory.
*
* @param $directory
* The directory to find config files in.
* @return string[]
* The filenames of found config files.
*/
private function getConfigFilesInDirectory($directory)
{
if ($this->filesystem->exists($directory) === false) {
return [];
}
$configFiles = [];
// Finder does not reset its internal state, we need a new instance
// everytime we use it.
$finder = new Finder();
foreach ($finder->files()->name('*.yml')->in($directory) as $file) {
$configFiles[$file->getFilename()] = $file->getPath().DIRECTORY_SEPARATOR.$file->getFilename();
}
return $configFiles;
}
/**
* Check if two file have the same content.
*
* @param $firstFile
* @param $secondFile
*
* @return bool
*/
private function filesAreEqual($firstFile, $secondFile): bool
{
if (filesize($firstFile) !== filesize($secondFile)) {
return false;
}
$firstFileHandler = fopen($firstFile, 'rb');
$secondFileHandler = fopen($secondFile, 'rb');
while (!feof($firstFileHandler)) {
if (fread($firstFileHandler, 1024) != fread($secondFileHandler, 1024)) {
return false;
}
}
fclose($firstFileHandler);
fclose($secondFileHandler);
return true;
}
/**
* Generates default settings file with current db params.
*
* @param $defaultSettingsFile
* @param $dbSpec
*
* @return void
*/
private function prepareSettingsFile($defaultSettingsFile, $dbSpec)
{
$fileString = <<<EOF
<?php
\$databases['default']['default'] = [
'database' => '{{ database }}',
'username' => '{{ username }}',
'password' => '{{ password }}',
'prefix' => '',
'host' => '{{ host }}',
'port' => '{{ port }}',
'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
'driver' => '{{ driver }}',
];
EOF;
$fileString = str_replace(['{{ database }}', '{{ username }}', '{{ password }}', '{{ host }}', '{{ port }}', '{{ driver }}'], [$dbSpec['database'], $dbSpec['username'], $dbSpec['password'], $dbSpec['host'], $dbSpec['port'], $dbSpec['driver']], $fileString);
file_put_contents($defaultSettingsFile, $fileString, FILE_APPEND);
}
}