2 namespace Drush\Commands\core;
4 use Consolidation\Log\ConsoleLogLevel;
5 use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
6 use Drupal\Core\Logger\RfcLogLevel;
7 use Drupal\Core\Utility\Error;
8 use Drupal\Core\Entity\EntityStorageException;
9 use Drush\Commands\DrushCommands;
11 use Drush\Exceptions\UserAbortException;
13 use Symfony\Component\Console\Output\OutputInterface;
15 class UpdateDBCommands extends DrushCommands
17 protected $cache_clear;
19 protected $maintenanceModeOriginalState;
22 * Apply any database updates required (as with running update.php).
25 * @option cache-clear Clear caches upon completion.
26 * @option entity-updates Run automatic entity schema updates at the end of any update hooks.
27 * @option post-updates Run post updates after hook_update_n and entity updates.
32 public function updatedb($options = ['cache-clear' => true, 'entity-updates' => false, 'post-updates' => true])
34 $this->cache_clear = $options['cache-clear'];
35 require_once DRUPAL_ROOT . '/core/includes/install.inc';
36 require_once DRUPAL_ROOT . '/core/includes/update.inc';
37 drupal_load_updates();
39 // Disables extensions that have a lower Drupal core major version, or too high of a PHP requirement.
40 // Those are rare, and this function does a full rebuild. So commenting it out for now.
41 // update_fix_compatibility();
43 // Check requirements before updating.
44 if (!$this->updateCheckRequirements()) {
45 if (!$this->io()->confirm(dt('Requirements check reports errors. Do you wish to continue?'))) {
46 throw new UserAbortException();
50 $return = drush_invoke_process('@self', 'updatedb:status', [], ['entity-updates' => $options['entity-updates'], 'post-updates' => $options['post-updates']]);
51 if ($return['error_status']) {
52 throw new \Exception('Failed getting update status.');
53 } elseif (empty($return['object'])) {
54 // Do nothing. updatedb:status already logged a message.
56 if (!$this->io()->confirm(dt('Do you wish to run the specified pending updates?'))) {
57 throw new UserAbortException();
59 if (Drush::simulate()) {
62 $success = $this->updateBatch($options);
63 // Caches were just cleared in updateFinished callback.
67 drush_set_context('DRUSH_EXIT_CODE', DRUSH_FRAMEWORK_ERROR);
70 $level = $success ? ConsoleLogLevel::SUCCESS : LogLevel::ERROR;
71 $this->logger()->log($level, dt('Finished performing updates.'));
76 * Apply pending entity schema updates.
78 * @command entity:updates
79 * @option cache-clear Set to 0 to suppress normal cache clearing; the caller should then clear if needed.
82 * @aliases entup,entity-updates
83 * @usage drush updatedb:status --entity-updates | grep entity-update
84 * Use updatedb:status to detect pending updates.
87 public function entityUpdates($options = ['cache-clear' => true])
89 if (Drush::simulate()) {
90 throw new \Exception(dt('entity-updates command does not support --simulate option.'));
93 if ($this->entityUpdatesMain() === false) {
94 throw new \Exception('Entity updates not run.');
97 if ($options['cache-clear']) {
98 drush_drupal_cache_clear_all();
101 $this->logger()->success(dt('Finished performing updates.'));
105 * List any pending database updates.
107 * @command updatedb:status
108 * @option entity-updates Show entity schema updates.
109 * @option post-updates Show post updates.
112 * @aliases updbst,updatedb-status
115 * update_id: Update ID
116 * description: Description
118 * @default-fields module,update_id,type,description
119 * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields
121 public function updatedbStatus($options = ['format'=> 'table', 'entity-updates' => true, 'post-updates' => true])
123 require_once DRUSH_DRUPAL_CORE . '/includes/install.inc';
124 drupal_load_updates();
125 list($pending, $start) = $this->getUpdatedbStatus($options);
126 if (empty($pending)) {
127 $this->logger()->success(dt("No database updates required."));
129 return new RowsOfFields($pending);
134 * Process operations in the specified batch set.
136 * @command updatedb:batch-process
137 * @param string $batch_id The batch id that will be processed.
142 public function process($batch_id)
144 // Suppress the output of the batch process command. This is intended to
145 // be passed to the initiating command rather than being output to the
147 $this->output()->setVerbosity(OutputInterface::VERBOSITY_QUIET);
148 return drush_batch_command($batch_id);
152 * Perform one update and store the results which will later be displayed on
155 * An update function can force the current and all later updates for this
156 * module to abort by returning a $ret array with an element like:
157 * $ret['#abort'] = array('success' => FALSE, 'query' => 'What went wrong');
158 * The schema version will not be updated in this case, and all the
159 * aborted updates will continue to appear on update.php as updates that
160 * have not yet been run.
163 * The module whose update will be run.
165 * The update number to run.
167 * The batch context array
169 public function updateDoOne($module, $number, $dependency_map, &$context)
171 $function = $module . '_update_' . $number;
173 // Disable config entity overrides.
174 if (!defined('MAINTENANCE_MODE')) {
175 define('MAINTENANCE_MODE', 'update');
178 // If this update was aborted in a previous step, or has a dependency that
179 // was aborted in a previous step, go no further.
180 if (!empty($context['results']['#abort']) && array_intersect($context['results']['#abort'], array_merge($dependency_map, [$function]))) {
184 $context['log'] = false;
186 \Drupal::moduleHandler()->loadInclude($module, 'install');
189 if (function_exists($function)) {
191 if ($context['log']) {
192 Database::startLog($function);
195 $this->logger()->notice("Update started: $function");
196 $ret['results']['query'] = $function($context['sandbox']);
197 $ret['results']['success'] = true;
198 } catch (\Throwable $e) {
199 // PHP 7 introduces Throwable, which covers both Error and Exception throwables.
200 $ret['#abort'] = ['success' => false, 'query' => $e->getMessage()];
201 $this->logger()->error($e->getMessage());
202 } catch (\Exception $e) {
203 // In order to be compatible with PHP 5 we also catch regular Exceptions.
204 $ret['#abort'] = ['success' => false, 'query' => $e->getMessage()];
205 $this->logger()->error($e->getMessage());
208 if ($context['log']) {
209 $ret['queries'] = Database::getLog($function);
212 $ret['#abort'] = ['success' => false];
213 $this->logger()->warning(dt('Update function @function not found', ['@function' => $function]));
216 if (isset($context['sandbox']['#finished'])) {
217 $context['finished'] = $context['sandbox']['#finished'];
218 unset($context['sandbox']['#finished']);
221 if (!isset($context['results'][$module])) {
222 $context['results'][$module] = [];
224 if (!isset($context['results'][$module][$number])) {
225 $context['results'][$module][$number] = [];
227 $context['results'][$module][$number] = array_merge($context['results'][$module][$number], $ret);
229 // Log the message that was returned.
230 if (!empty($ret['results']['query'])) {
231 $this->logger()->notice(strip_tags((string) $ret['results']['query']));
234 if (!empty($ret['#abort'])) {
235 // Record this function in the list of updates that were aborted.
236 $context['results']['#abort'][] = $function;
237 // Setting this value will output an error message.
238 // @see \DrushBatchContext::offsetSet()
239 $context['error_message'] = "Update failed: $function";
242 // Record the schema update if it was completed successfully.
243 if ($context['finished'] == 1 && empty($ret['#abort'])) {
244 drupal_set_installed_schema_version($module, $number);
245 // Setting this value will output a success message.
246 // @see \DrushBatchContext::offsetSet()
247 $context['message'] = "Update completed: $function";
252 * Batch command that executes a single post-update.
254 * @param string $function
255 * The post-update function to execute.
256 * @param array $context
259 public function updateDoOnePostUpdate($function, &$context)
263 // Disable config entity overrides.
264 if (!defined('MAINTENANCE_MODE')) {
265 define('MAINTENANCE_MODE', 'update');
268 // If this update was aborted in a previous step, or has a dependency that was
269 // aborted in a previous step, go no further.
270 if (!empty($context['results']['#abort'])) {
274 list($module, $name) = explode('_post_update_', $function, 2);
275 module_load_include('php', $module, $module . '.post_update');
276 if (function_exists($function)) {
277 $this->logger()->notice("Update started: $function");
279 $ret['results']['query'] = $function($context['sandbox']);
280 $ret['results']['success'] = true;
282 if (!isset($context['sandbox']['#finished']) || (isset($context['sandbox']['#finished']) && $context['sandbox']['#finished'] >= 1)) {
283 \Drupal::service('update.post_update_registry')->registerInvokedUpdates([$function]);
285 } catch (\Exception $e) {
286 // @TODO We may want to do different error handling for different exception
287 // types, but for now we'll just log the exception and return the message
289 // @see https://www.drupal.org/node/2564311
290 $this->logger()->error($e->getMessage());
292 $variables = Error::decodeException($e);
293 unset($variables['backtrace']);
296 'query' => t('%type: @message in %function (line %line of %file).', $variables),
301 if (isset($context['sandbox']['#finished'])) {
302 $context['finished'] = $context['sandbox']['#finished'];
303 unset($context['sandbox']['#finished']);
305 if (!isset($context['results'][$module][$name])) {
306 $context['results'][$module][$name] = [];
308 $context['results'][$module][$name] = array_merge($context['results'][$module][$name], $ret);
310 // Log the message that was returned.
311 if (!empty($ret['results']['query'])) {
312 $this->logger()->notice(strip_tags((string) $ret['results']['query']));
315 if (!empty($ret['#abort'])) {
316 // Record this function in the list of updates that were aborted.
317 $context['results']['#abort'][] = $function;
318 // Setting this value will output an error message.
319 // @see \DrushBatchContext::offsetSet()
320 $context['error_message'] = "Update failed: $function";
322 // Setting this value will output a success message.
323 // @see \DrushBatchContext::offsetSet()
324 $context['message'] = "Update completed: $function";
329 * Start the database update batch process.
331 public function updateBatch($options)
333 $start = $this->getUpdateList();
334 // Resolve any update dependencies to determine the actual updates that will
335 // be run and the order they will be run in.
336 $updates = update_resolve_dependencies($start);
338 // Store the dependencies for each update function in an array which the
339 // batch API can pass in to the batch operation each time it is called. (We
340 // do not store the entire update dependency array here because it is
341 // potentially very large.)
342 $dependency_map = [];
343 foreach ($updates as $function => $update) {
344 $dependency_map[$function] = !empty($update['reverse_paths']) ? array_keys($update['reverse_paths']) : [];
349 foreach ($updates as $update) {
350 if ($update['allowed']) {
351 // Set the installed version of each module so updates will start at the
352 // correct place. (The updates are already sorted, so we can simply base
353 // this on the first one we come across in the above foreach loop.)
354 if (isset($start[$update['module']])) {
355 drupal_set_installed_schema_version($update['module'], $update['number'] - 1);
356 unset($start[$update['module']]);
358 // Add this update function to the batch.
359 $function = $update['module'] . '_update_' . $update['number'];
360 $operations[] = [[$this, 'updateDoOne'], [$update['module'], $update['number'], $dependency_map[$function]]];
364 // Perform entity definition updates, which will update storage
365 // schema if needed. If module update functions need to work with specific
366 // entity schema they should call the entity update service for the specific
367 // update themselves.
368 // @see \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface::applyEntityUpdate()
369 // @see \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface::applyFieldUpdate()
370 if ($options['entity-updates'] && \Drupal::entityDefinitionUpdateManager()->needsUpdates()) {
371 $operations[] = [[$this, 'updateEntityDefinitions'], []];
374 // Lastly, apply post update hooks if specified.
375 if ($options['post-updates']) {
376 $post_updates = \Drupal::service('update.post_update_registry')->getPendingUpdateFunctions();
379 // Only needed if we performed updates earlier.
380 $operations[] = [[$this, 'cacheRebuild'], []];
382 foreach ($post_updates as $function) {
383 $operations[] = [[$this, 'updateDoOnePostUpdate'], [$function]];
388 $batch['operations'] = $operations;
390 'title' => 'Updating',
391 'init_message' => 'Starting updates',
392 'error_message' => 'An unrecoverable error has occurred. You can find the error message below. It is advised to copy it to the clipboard for reference.',
393 'finished' => [$this, 'updateFinished'],
394 'file' => 'core/includes/update.inc',
398 // See updateFinished() for the restore of maint mode.
399 $this->maintenanceModeOriginalState = \Drupal::service('state')->get('system.maintenance_mode');
400 \Drupal::service('state')->set('system.maintenance_mode', true);
401 $result = drush_backend_batch_process('updatedb:batch-process');
404 if (!is_array($result)) {
405 $this->logger()->error(dt('Batch process did not return a result array. Returned: !type', ['!type' => gettype($result)]));
406 } elseif (!array_key_exists('object', $result)) {
407 $this->logger()->error(dt('Batch process did not return a result object.'));
408 } elseif (!empty($result['object'][0]['#abort'])) {
409 // Whenever an error occurs the batch process does not continue, so
410 // this array should only contain a single item, but we still output
411 // all available data for completeness.
412 $this->logger()->error(dt('Update aborted by: !process', [
413 '!process' => implode(', ', $result['object'][0]['#abort']),
423 * Apply entity schema updates.
425 public function updateEntityDefinitions(&$context)
428 \Drupal::entityDefinitionUpdateManager()->applyupdates();
429 } catch (EntityStorageException $e) {
430 watchdog_exception('update', $e);
431 $variables = Error::decodeException($e);
432 unset($variables['backtrace']);
433 // The exception message is run through
434 // \Drupal\Component\Utility\SafeMarkup::checkPlain() by
435 // \Drupal\Core\Utility\Error::decodeException().
436 $ret['#abort'] = ['success' => false, 'query' => t('%type: !message in %function (line %line of %file).', $variables)];
437 $context['results']['core']['update_entity_definitions'] = $ret;
438 $context['results']['#abort'][] = 'update_entity_definitions';
442 // Copy of protected \Drupal\system\Controller\DbUpdateController::getModuleUpdates.
443 public function getUpdateList()
446 $updates = update_get_update_list();
447 foreach ($updates as $module => $update) {
448 $return[$module] = $update['start'];
455 * Clears caches and rebuilds the container.
457 * This is called in between regular updates and post updates. Do not use
458 * drush_drupal_cache_clear_all() as the cache clearing and container rebuild
459 * must happen in the same process that the updates are run in.
461 * Drupal core's update.php uses drupal_flush_all_caches() directly without
462 * explicitly rebuilding the container as the container is rebuilt on the next
463 * HTTP request of the batch.
465 * @see drush_drupal_cache_clear_all()
466 * @see \Drupal\system\Controller\DbUpdateController::triggerBatch()
468 public function cacheRebuild()
470 drupal_flush_all_caches();
471 \Drupal::service('kernel')->rebuildContainer();
472 // Load the module data which has been removed when the container was
474 $module_handler = \Drupal::moduleHandler();
475 $module_handler->loadAll();
476 $module_handler->invokeAll('rebuild');
480 * Batch update callback, clears the cache if needed, and restores maint mode.
482 * @see \Drupal\system\Controller\DbUpdateController::batchFinished()
483 * @see \Drupal\system\Controller\DbUpdateController::results()
485 * @param boolean $success Whether the batch ended without a fatal error.
486 * @param array $results
487 * @param array $operations
489 public function updateFinished($success, $results, $operations)
491 if (!$this->cache_clear) {
492 $this->logger()->info(dt("Skipping cache-clear operation due to --no-cache-clear option."));
494 drupal_flush_all_caches();
497 \Drupal::service('state')->set('system.maintenance_mode', $this->maintenanceModeOriginalState);
501 * Return a 2 item array with
502 * - an array where each item is a 4 item associative array describing a pending update.
503 * - an array listing the first update to run, keyed by module.
505 public function getUpdatedbStatus(array $options)
507 require_once DRUPAL_ROOT . '/core/includes/update.inc';
508 $pending = \update_get_update_list();
511 // Ensure system module's updates run first.
512 $start['system'] = [];
514 foreach ($pending as $module => $updates) {
515 if (isset($updates['start'])) {
516 foreach ($updates['pending'] as $update_id => $description) {
517 // Strip cruft from front.
518 $description = str_replace($update_id . ' - ', '', $description);
519 $return[$module . "_update_$update_id"] = [
521 'update_id' => $update_id,
522 'description' => $description,
523 'type'=> 'hook_update_n'
526 if (isset($updates['start'])) {
527 $start[$module] = $updates['start'];
532 // Append row(s) for pending entity definition updates.
533 if ($options['entity-updates']) {
534 foreach (\Drupal::entityDefinitionUpdateManager()
535 ->getChangeSummary() as $entity_type_id => $changes) {
536 foreach ($changes as $change) {
538 'module' => dt('@type entity type', ['@type' => $entity_type_id]),
540 'description' => strip_tags($change),
541 'type' => 'entity-update'
547 // Pending hook_post_update_X() implementations.
548 $post_updates = \Drupal::service('update.post_update_registry')->getPendingUpdateInformation();
549 if ($options['post-updates']) {
550 foreach ($post_updates as $module => $post_update) {
551 foreach ($post_update as $key => $list) {
552 if ($key == 'pending') {
553 foreach ($list as $id => $item) {
554 $return[$module . '-post-' . $id] = [
557 'description' => $item,
558 'type' => 'post-update'
566 return [$return, $start];
570 * Apply pending entity schema updates.
572 public function entityUpdatesMain()
574 $change_summary = \Drupal::entityDefinitionUpdateManager()->getChangeSummary();
575 if (!empty($change_summary)) {
576 $this->output()->writeln(dt('The following updates are pending:'));
577 $this->io()->newLine();
579 foreach ($change_summary as $entity_type_id => $changes) {
580 $this->output()->writeln($entity_type_id . ' entity type : ');
581 foreach ($changes as $change) {
582 $this->output()->writeln(strip_tags($change), 2);
586 if (!$this->io()->confirm(dt('Do you wish to run all pending updates?'))) {
587 throw new UserAbortException();
590 $operations[] = [[$this, 'updateEntityDefinitions'], []];
593 $batch['operations'] = $operations;
595 'title' => 'Updating',
596 'init_message' => 'Starting updates',
597 'error_message' => 'An unrecoverable error has occurred. You can find the error message below. It is advised to copy it to the clipboard for reference.',
598 'finished' => [$this, 'updateFinished'],
602 // See updateFinished() for the restore of maint mode.
603 $this->maintenanceModeOriginalState = \Drupal::service('state')->get('system.maintenance_mode');
604 \Drupal::service('state')->set('system.maintenance_mode', true);
605 drush_backend_batch_process();
607 $this->logger()->success(dt("No entity schema updates required"));
612 * Log messages for any requirements warnings/errors.
614 public function updateCheckRequirements()
618 \Drupal::moduleHandler()->resetImplementations();
619 $requirements = update_check_requirements();
620 $severity = drupal_requirements_severity($requirements);
622 // If there are issues, report them.
623 if ($severity != REQUIREMENT_OK) {
624 if ($severity === REQUIREMENT_ERROR) {
627 foreach ($requirements as $requirement) {
628 if (isset($requirement['severity']) && $requirement['severity'] != REQUIREMENT_OK) {
629 $message = isset($requirement['description']) ? $requirement['description'] : '';
630 if (isset($requirement['value']) && $requirement['value']) {
631 $message .= ' (Currently using '. $requirement['title'] .' '. $requirement['value'] .')';
633 $log_level = $requirement['severity'] === REQUIREMENT_ERROR ? LogLevel::ERROR : LogLevel::WARNING;
634 $this->logger()->log($log_level, $message);