-
Notifications
You must be signed in to change notification settings - Fork 1
A drush command to run updates #13
Changes from all commits
58f13f7
d387506
ebe109f
a943e97
ab4dd31
1cb7132
4133364
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
services: | ||
update_helper.commands: | ||
class: Drupal\update_helper\Command\ApplyConfigurationUpdateCommands | ||
tags: | ||
- { name: drush.command } |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ use Drupal\Core\Url; | |
use Drupal\update_helper_checklist\UpdateChecklist; | ||
use Symfony\Component\Yaml\Yaml; | ||
use Drupal\update_helper_checklist\Entity\Update; | ||
use Drupal\update_helper\Updater; | ||
|
||
/** | ||
* Implements hook_checklistapi_checklist_info(). | ||
|
@@ -62,10 +63,10 @@ function _update_helper_checklist_checklistapi_checklist_items() { | |
$entry = Update::load($update_key); | ||
|
||
$status = ($entry && $entry->wasSuccessfulByHook()) ? TRUE : FALSE; | ||
if ($status && !empty($update['#description_successful'])) { | ||
if ($entry && $status && !empty($update['#description_successful'])) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this look like a leftover from PR #12 ? |
||
$update['#description'] .= $update['#description_successful']; | ||
} | ||
elseif (!$status && !empty($update['#description_failed'])) { | ||
elseif ($entry && !$status && !empty($update['#description_failed'])) { | ||
$update['#description'] .= $update['#description_failed']; | ||
} | ||
} | ||
|
@@ -107,6 +108,7 @@ function _update_helper_checklist_checklistapi_checklist_items() { | |
function update_helper_checklist_modules_installed(array $modules) { | ||
/** @var \Drupal\Core\Extension\ModuleHandler $module_handler */ | ||
$module_handler = \Drupal::service('module_handler'); | ||
$updateHelper = \Drupal::service('update_helper.updater'); | ||
|
||
$modules_checklist = []; | ||
$module_directories = $module_handler->getModuleDirectories(); | ||
|
@@ -120,6 +122,10 @@ function update_helper_checklist_modules_installed(array $modules) { | |
foreach ($updates_checklist as $version_items) { | ||
foreach ($version_items as $update_hook_name => $checklist_definition) { | ||
if (is_array($checklist_definition)) { | ||
|
||
$status = $updateHelper->checkUpdate($module_name, $update_hook_name); | ||
if ($status != Updater::CONFIG_ALREADY_APPLIED && $status != Updater::CONFIG_APPLIED_SUCCESSFULLY) continue; | ||
|
||
if (!isset($modules_checklist[$module_name])) { | ||
$modules_checklist[$module_name] = []; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
<?php | ||
|
||
namespace Drupal\update_helper\Command; | ||
|
||
use Drush\Commands\DrushCommands; | ||
use Drupal\update_helper\Utility\CommandHelper; | ||
use Psr\Log\LoggerInterface; | ||
|
||
/** | ||
* Class ApplyConfigurationUpdateCommand | ||
* | ||
* define drush commands for update_helper module | ||
* | ||
* @package Drupal\update_helper\Command | ||
*/ | ||
class ApplyConfigurationUpdateCommands extends DrushCommands { | ||
|
||
/** | ||
* command helper object (inspired by search API module) | ||
* | ||
* @var \Drupal\update_helper\Utility\CommandHelper | ||
*/ | ||
protected $commandHelper; | ||
|
||
/** | ||
* ApplyConfigurationUpdateCommands constructor. | ||
*/ | ||
public function __construct() { | ||
$this->commandHelper = new CommandHelper(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be good to inject this as service. |
||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function setLogger(LoggerInterface $logger) { | ||
parent::setLogger($logger); | ||
$this->commandHelper->setLogger($logger); | ||
} | ||
|
||
/** | ||
* applying an update hook (function) from module install file | ||
* Apply updates by invoking the related update hooks. | ||
* | ||
* @param string $module | ||
* @param string $update_hook | ||
* @param array $options | ||
* @option force | ||
* | ||
* @command update_helper:apply-update | ||
* @aliases uhau | ||
*/ | ||
public function apply_update ($module = '', $update_hook = '', $options = ['force' => FALSE]) { | ||
$force = $options['force']; | ||
$this->commandHelper->apply_update($module, $update_hook, $force); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,12 @@ class Updater implements UpdaterInterface { | |
|
||
use StringTranslationTrait; | ||
|
||
const CONFIG_NOT_FOUND = 0; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as for |
||
const CONFIG_ALREADY_APPLIED = 1; | ||
const CONFIG_NOT_EXPECTED = 2; | ||
const CONFIG_APPLIED_SUCCESSFULLY = 3; | ||
const MODULES_FOUND = 4; | ||
const MODULES_NOT_FOUND = 5; | ||
/** | ||
* Site configFactory object. | ||
* | ||
|
@@ -130,7 +136,7 @@ protected function logInfo($message) { | |
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function executeUpdate($module, $update_definition_name) { | ||
public function executeUpdate($module, $update_definition_name, $force = FALSE) { | ||
$this->warningCount = 0; | ||
|
||
$update_definitions = $this->configHandler->loadUpdate($module, $update_definition_name); | ||
|
@@ -141,7 +147,7 @@ public function executeUpdate($module, $update_definition_name) { | |
} | ||
|
||
if (!empty($update_definitions)) { | ||
$this->executeConfigurationActions($update_definitions); | ||
$this->executeConfigurationActions($update_definitions, $force); | ||
} | ||
|
||
// Dispatch event after update has finished. | ||
|
@@ -151,6 +157,44 @@ public function executeUpdate($module, $update_definition_name) { | |
return $this->warningCount === 0; | ||
} | ||
|
||
/** | ||
* Check update status of configuration from update definitions. | ||
* | ||
* @param string $module | ||
* Module name where update definition is saved. | ||
* @param string $update_definition_name | ||
* Update definition name. Usually same name as update hook. | ||
* | ||
* @return bool | ||
* Returns update status. | ||
*/ | ||
public function checkUpdate($module, $update_definition_name) { | ||
$this->warningCount = 0; | ||
$moduleHandler = \Drupal::service('module_handler'); | ||
$modulesInstalled = []; | ||
$update_definitions = $this->configHandler->loadUpdate($module, $update_definition_name); | ||
if (isset($update_definitions[UpdateDefinitionInterface::GLOBAL_ACTIONS])) { | ||
if (isset($update_definitions[UpdateDefinitionInterface::GLOBAL_ACTIONS][UpdateDefinitionInterface::GLOBAL_ACTION_INSTALL_MODULES])) { | ||
$modules = $update_definitions[UpdateDefinitionInterface::GLOBAL_ACTIONS][UpdateDefinitionInterface::GLOBAL_ACTION_INSTALL_MODULES]; | ||
foreach ($modules as $module) { | ||
if (!$moduleHandler->moduleExists($module)) return Updater::MODULES_NOT_FOUND; | ||
$modulesInstalled[] = $module; | ||
} | ||
} | ||
unset($update_definitions[UpdateDefinitionInterface::GLOBAL_ACTIONS]); | ||
} | ||
|
||
if (!empty($update_definitions)) { | ||
return $this->executeConfigurationActions($update_definitions, FALSE, TRUE); | ||
} | ||
|
||
if (empty($update_definitions) && !empty($modulesInstalled)) { | ||
return Updater::MODULES_FOUND; | ||
} | ||
|
||
return Updater::CONFIG_NOT_FOUND; | ||
} | ||
|
||
/** | ||
* Get array with defined global actions. | ||
* | ||
|
@@ -176,8 +220,15 @@ protected function executeGlobalActions(array $global_actions) { | |
* | ||
* @param array $update_definitions | ||
* List of configurations with update definitions for them. | ||
* @param bool $force | ||
* Force the update. | ||
* @param bool $checkOnly | ||
* Check the update status and don't apply the update. | ||
* | ||
* @return bool | ||
* Returns update status if checkOnly flag is set. | ||
*/ | ||
protected function executeConfigurationActions(array $update_definitions) { | ||
protected function executeConfigurationActions(array $update_definitions, $force = FALSE, $checkOnly = FALSE) { | ||
foreach ($update_definitions as $configName => $configChange) { | ||
$update_actions = $configChange['update_actions']; | ||
|
||
|
@@ -198,15 +249,33 @@ protected function executeConfigurationActions(array $update_definitions) { | |
$new_config = NestedArray::mergeDeep($new_config, $update_actions['add']); | ||
} | ||
|
||
if ($this->updateConfig($configName, $new_config, $configChange['expected_config'], $delete_keys)) { | ||
$this->logInfo($this->t('Configuration @configName has been successfully updated.', ['@configName' => $configName])); | ||
$result = $this->updateConfig($configName, $new_config, $configChange['expected_config'], $delete_keys, $force, $checkOnly); | ||
|
||
if ($checkOnly) { | ||
return $result; | ||
} | ||
else { | ||
$this->logWarning($this->t('Unable to update configuration for @configName.', ['@configName' => $configName])); | ||
|
||
switch ($result) { | ||
case Updater::CONFIG_APPLIED_SUCCESSFULLY: | ||
$this->logInfo($this->t('Configuration @configName has been successfully updated.', ['@configName' => $configName])); | ||
break; | ||
|
||
case Updater::CONFIG_ALREADY_APPLIED: | ||
$this->logWarning($this->t('Configuration @configName is already updated.', ['@configName' => $configName])); | ||
break; | ||
|
||
case Updater::CONFIG_NOT_EXPECTED: | ||
$this->logWarning($this->t('Expected current configuration for @configName is not matching. Unable to apply new config.', ['@configName' => $configName])); | ||
break; | ||
|
||
case Updater::CONFIG_NOT_FOUND: | ||
$this->logWarning($this->t('Unable to find config @configName. Skipping update.', ['@configName' => $configName])); | ||
break; | ||
} | ||
} | ||
} | ||
|
||
|
||
/** | ||
* Installs modules. | ||
* | ||
|
@@ -321,41 +390,54 @@ protected function getFlatKeys(array $nested_array) { | |
* Only if current config is same like old config we are updating. | ||
* @param array $delete_keys | ||
* List of parent keys to remove. @see NestedArray::unsetValue() | ||
* @param bool $force | ||
* Force the update. | ||
* @param bool $checkOnly | ||
* Check the update status and don't apply the update. | ||
* | ||
* @return bool | ||
* Returns TRUE if update of configuration was successful. | ||
*/ | ||
protected function updateConfig($config_name, array $configuration, array $expected_configuration = [], array $delete_keys = []) { | ||
protected function updateConfig($config_name, array $configuration, array $expected_configuration = [], array $delete_keys = [], $force = FALSE, $checkOnly = FALSE) { | ||
$config = $this->configFactory->getEditable($config_name); | ||
|
||
$config_data = $config->get(); | ||
|
||
// Reset expected_config in case of force flag. | ||
if ($force) { | ||
$expected_configuration = []; | ||
} | ||
|
||
// Check that configuration exists before executing update. | ||
if (empty($config_data)) { | ||
return FALSE; | ||
return Updater::CONFIG_NOT_FOUND; | ||
} | ||
|
||
// Check if configuration is already in new state. | ||
$merged_data = NestedArray::mergeDeep($expected_configuration, $configuration); | ||
if (empty(DiffArray::diffAssocRecursive($merged_data, $config_data))) { | ||
return TRUE; | ||
if (!$force && empty(DiffArray::diffAssocRecursive($configuration, $config_data))) { | ||
return Updater::CONFIG_ALREADY_APPLIED; | ||
} | ||
|
||
if (!empty($expected_configuration) && DiffArray::diffAssocRecursive($expected_configuration, $config_data)) { | ||
return FALSE; | ||
} | ||
if (empty($expected_configuration) || !DiffArray::diffAssocRecursive($expected_configuration, $config_data)) { | ||
// Delete configuration keys from config. | ||
if($checkOnly){ | ||
return Updater::CONFIG_APPLIED_SUCCESSFULLY; | ||
} | ||
|
||
// Delete configuration keys from config. | ||
if (!empty($delete_keys)) { | ||
foreach ($delete_keys as $key_path) { | ||
NestedArray::unsetValue($config_data, $key_path); | ||
if (!empty($delete_keys)) { | ||
foreach ($delete_keys as $key_path) { | ||
NestedArray::unsetValue($config_data, $key_path); | ||
} | ||
} | ||
} | ||
|
||
$config->setData(NestedArray::mergeDeep($config_data, $configuration)); | ||
$config->save(); | ||
$config->setData(NestedArray::mergeDeep($config_data, $configuration)); | ||
$config->save(); | ||
|
||
return TRUE; | ||
return Updater::CONFIG_APPLIED_SUCCESSFULLY; | ||
}else{ | ||
return Updater::CONFIG_NOT_EXPECTED; | ||
} | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<?php | ||
|
||
namespace Drupal\update_helper\Utility; | ||
|
||
use Psr\Log\LoggerAwareInterface; | ||
use Psr\Log\LoggerAwareTrait; | ||
|
||
class CommandHelper implements LoggerAwareInterface { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we could make a service from this? Then we could nicely inject |
||
|
||
use LoggerAwareTrait; | ||
|
||
public function __construct() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably we don't need an empty constructor. |
||
|
||
} | ||
|
||
/** | ||
* applying an (optional) update hook (function) from module install file | ||
* | ||
* @param string $module - drupal module name | ||
* @param string $update_hook - name of update_hook to apply | ||
* @param bool $force - force the update | ||
*/ | ||
public function apply_update($module = '', $update_hook = '', $force = FALSE) { | ||
if (!$update_hook || !$module) { | ||
$this->logger->error(dt('Please provide a module name and an update hook. Example: drush uhau <module> <update_hook>')); | ||
return; | ||
} | ||
|
||
$updateHelper = \Drupal::service('update_helper.updater'); | ||
$updateHelper->executeUpdate($module, $update_hook, $force); | ||
return $updateHelper->logger()->output(); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
<?php | ||
|
||
/** | ||
* @file | ||
* Update helper drush command. | ||
*/ | ||
|
||
use Drupal\update_helper\Utility\CommandHelper; | ||
|
||
/** | ||
* Implements hook_drush_command(). | ||
*/ | ||
function update_helper_drush_command() { | ||
|
||
$commands['update-helper-apply-update'] = [ | ||
'description' => 'Apply config updates.', | ||
'aliases' => ['uhau'], | ||
'arguments' => [ | ||
'module' => 'Module name.', | ||
'updateName' => 'Update name.', | ||
], | ||
'options' => [ | ||
'force' => FALSE, | ||
], | ||
'examples' => [ | ||
'drush uhau <module> <updateName>' => 'Apply the update <updateName> from <module>', | ||
'drush uhau --force <module> <updateName>' => 'Force apply the update <updateName> from <module>', | ||
], | ||
]; | ||
|
||
return $commands; | ||
} | ||
|
||
/** | ||
* Drush command logic. | ||
* | ||
* The drush_[MODULE_NAME]_[COMMAND_NAME](). | ||
*/ | ||
function drush_update_helper_apply_update($module = "", $update_hook = "") { | ||
$commandhelper = _update_helper_drush_command_helper(); | ||
$force = drush_get_option('force', FALSE); | ||
$commandhelper->apply_update($module, $update_hook, $force); | ||
} | ||
|
||
/** | ||
* Returns an instance of the command helper. | ||
* | ||
* @return \Drupal\update_helper\Utility\CommandHelper | ||
* An instance of the command helper class. | ||
*/ | ||
function _update_helper_drush_command_helper() { | ||
$command_helper = new CommandHelper(); | ||
$command_helper->setLogger(\Drupal::logger('update_helper')); | ||
return $command_helper; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This service name does not match the class name. Probably class
ApplyConfigurationUpdateCommands
should be renamed into something likeUpdateHelperCommands
if we want one class to handle multiple commands. Or we should rename service name and class to match single command per class approach.I'm not sure what is best practice in case of Drush commands?