Skip to content
This repository has been archived by the owner on Apr 8, 2022. It is now read-only.

A drush command to run updates #13

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions drush.services.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
services:
update_helper.commands:
Copy link
Contributor

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 like UpdateHelperCommands 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?

class: Drupal\update_helper\Command\ApplyConfigurationUpdateCommands
tags:
- { name: drush.command }
10 changes: 8 additions & 2 deletions modules/update_helper_checklist/update_helper_checklist.module
Original file line number Diff line number Diff line change
Expand Up @@ -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().
Expand Down Expand Up @@ -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'])) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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'];
}
}
Expand Down Expand Up @@ -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();
Expand All @@ -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] = [];
}
Expand Down
56 changes: 56 additions & 0 deletions src/Command/ApplyConfigurationUpdateCommands.php
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();
Copy link
Contributor

Choose a reason for hiding this comment

The 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);
}
}
126 changes: 104 additions & 22 deletions src/Updater.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ class Updater implements UpdaterInterface {

use StringTranslationTrait;

const CONFIG_NOT_FOUND = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as for update_helper_checklist.module, it's again similar to changes in PR #12.

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.
*
Expand Down Expand Up @@ -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);
Expand All @@ -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.
Expand All @@ -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.
*
Expand All @@ -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'];

Expand All @@ -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.
*
Expand Down Expand Up @@ -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;
}
}

}
34 changes: 34 additions & 0 deletions src/Utility/CommandHelper.php
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The 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 update_helper.updater and also we could inject it in ApplyConfigurationUpdateCommands for Drush services.


use LoggerAwareTrait;

public function __construct() {
Copy link
Contributor

Choose a reason for hiding this comment

The 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();
}

}
55 changes: 55 additions & 0 deletions update_helper.drush.inc
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;
}