2 namespace Drush\Drupal\Commands\config;
4 use Consolidation\AnnotatedCommand\CommandData;
5 use Drupal\Core\Config\ConfigManagerInterface;
6 use Drupal\Core\Config\StorageComparer;
7 use Drupal\Core\Config\FileStorage;
8 use Drupal\Core\Config\StorageInterface;
9 use Drush\Commands\DrushCommands;
10 use Drush\Exceptions\UserAbortException;
11 use Symfony\Component\Console\Output\BufferedOutput;
12 use Webmozart\PathUtil\Path;
14 class ConfigExportCommands extends DrushCommands
18 * @var ConfigManagerInterface
20 protected $configManager;
23 * @var StorageInterface
25 protected $configStorage;
28 * @var StorageInterface
30 protected $configStorageSync;
33 * @return ConfigManagerInterface
35 public function getConfigManager()
37 return $this->configManager;
41 * @return StorageInterface
43 public function getConfigStorage()
45 return $this->configStorage;
49 * @return StorageInterface
51 public function getConfigStorageSync()
53 return $this->configStorageSync;
58 * @param ConfigManagerInterface $configManager
59 * @param StorageInterface $configStorage
60 * @param StorageInterface $configStorageSync
62 public function __construct(ConfigManagerInterface $configManager, StorageInterface $configStorage, StorageInterface $configStorageSync)
64 parent::__construct();
65 $this->configManager = $configManager;
66 $this->configStorage = $configStorage;
67 $this->configStorageSync = $configStorageSync;
71 * Export Drupal configuration to a directory.
73 * @command config:export
74 * @interact-config-label
75 * @param string $label A config directory label (i.e. a key in $config_directories array in settings.php).
76 * @option add Run `git add -p` after exporting. This lets you choose which config changes to sync for commit.
77 * @option commit Run `git add -A` and `git commit` after exporting. This commits everything that was exported without prompting.
78 * @option message Commit comment for the exported configuration. Optional; may only be used with --commit.
79 * @option destination An arbitrary directory that should receive the exported files. A backup directory is used when no value is provided.
80 * @option diff Show preview as a diff, instead of a change list.
81 * @usage drush config:export --destination
82 * Export configuration; Save files in a backup directory named config-export.
83 * @aliases cex,config-export
85 public function export($label = null, $options = ['add' => false, 'commit' => false, 'message' => self::REQ, 'destination' => self::OPT, 'diff' => false])
87 // Get destination directory.
88 $destination_dir = ConfigCommands::getDirectory($label, $options['destination']);
90 // Do the actual config export operation.
91 $preview = $this->doExport($options, $destination_dir);
93 // Do the VCS operations.
94 $this->doAddCommit($options, $destination_dir, $preview);
97 public function doExport($options, $destination_dir)
99 // Prepare the configuration storage for the export.
100 if ($destination_dir == Path::canonicalize(\config_get_config_directory(CONFIG_SYNC_DIRECTORY))) {
101 $target_storage = $this->getConfigStorageSync();
103 $target_storage = new FileStorage($destination_dir);
106 if (count(glob($destination_dir . '/*')) > 0) {
107 // Retrieve a list of differences between the active and target configuration (if any).
108 $config_comparer = new StorageComparer($this->getConfigStorage(), $target_storage, $this->getConfigManager());
109 if (!$config_comparer->createChangelist()->hasChanges()) {
110 $this->logger()->notice(dt('The active configuration is identical to the configuration in the export directory (!target).', ['!target' => $destination_dir]));
113 $this->output()->writeln("Differences of the active config to the export directory:\n");
115 if ($options['diff']) {
116 $diff = ConfigCommands::getDiff($target_storage, $this->getConfigStorage(), $this->output());
117 $this->output()->writeln($diff);
120 foreach ($config_comparer->getAllCollectionNames() as $collection) {
121 $change_list[$collection] = $config_comparer->getChangelist(null, $collection);
123 // Print a table with changes in color, then re-generate again without
124 // color to place in the commit comment.
125 $bufferedOutput = new BufferedOutput();
126 $table = ConfigCommands::configChangesTable($change_list, $bufferedOutput, false);
128 $preview = $bufferedOutput->fetch();
129 $table = ConfigCommands::configChangesTable($change_list, $this->output(), true);
133 if (!$this->io()->confirm(dt('The .yml files in your export directory (!target) will be deleted and replaced with the active config.', ['!target' => $destination_dir]))) {
134 throw new UserAbortException();
136 // Only delete .yml files, and not .htaccess or .git.
137 $target_storage->deleteAll();
139 // Also delete collections.
140 foreach ($target_storage->getAllCollectionNames() as $collection_name) {
141 $target_collection = $target_storage->createCollection($collection_name);
142 $target_collection->deleteAll();
146 // Write all .yml files.
147 ConfigCommands::copyConfig($this->getConfigStorage(), $target_storage);
149 $this->logger()->success(dt('Configuration successfully exported to !target.', ['!target' => $destination_dir]));
150 drush_backend_set_result($destination_dir);
151 return isset($preview) ? $preview : 'No existing configuration to diff against.';
154 public function doAddCommit($options, $destination_dir, $preview)
156 // Commit or add exported configuration if requested.
157 if ($options['commit']) {
158 // There must be changed files at the destination dir; if there are not, then
159 // we will skip the commit step.
160 $result = drush_shell_cd_and_exec($destination_dir, 'git status --porcelain .');
162 throw new \Exception(dt("`git status` failed."));
164 $uncommitted_changes = drush_shell_exec_output();
165 if (!empty($uncommitted_changes)) {
166 $result = drush_shell_cd_and_exec($destination_dir, 'git add -A .');
168 throw new \Exception(dt("`git add -A` failed."));
170 $comment_file = drush_save_data_to_temp_file($options['message'] ?: 'Exported configuration.'. $preview);
171 $result = drush_shell_cd_and_exec($destination_dir, 'git commit --file=%s', $comment_file);
173 throw new \Exception(dt("`git commit` failed. Output:\n\n!output", ['!output' => implode("\n", drush_shell_exec_output())]));
176 } elseif ($options['add']) {
177 drush_shell_exec_interactive('git add -p %s', $destination_dir);
182 * @hook validate config-export
183 * @param \Consolidation\AnnotatedCommand\CommandData $commandData
185 public function validate(CommandData $commandData)
187 $destination = $commandData->input()->getOption('destination');
189 if ($destination === true) {
190 // We create a dir in command callback. No need to validate.
194 if (!empty($destination)) {
195 // TODO: evaluate %files et. al. in destination
196 // $commandData->input()->setOption('destination', $destination);
197 if (!file_exists($destination)) {
198 $parent = dirname($destination);
199 if (!is_dir($parent)) {
200 throw new \Exception('The destination parent directory does not exist.');
202 if (!is_writable($parent)) {
203 throw new \Exception('The destination parent directory is not writable.');
206 if (!is_dir($destination)) {
207 throw new \Exception('The destination is not a directory.');
209 if (!is_writable($destination)) {
210 throw new \Exception('The destination directory is not writable.');