Skip to content

Instantly share code, notes, and snippets.

@jhedstrom
Last active March 23, 2021 08:07
Show Gist options
  • Save jhedstrom/0a1723a94484e3a0e46e2692d4698fe6 to your computer and use it in GitHub Desktop.
Save jhedstrom/0a1723a94484e3a0e46e2692d4698fe6 to your computer and use it in GitHub Desktop.
Custom content moderation update hooks from 8.2.x to 8.3.x
<?php
/**
* @file
* Install file for content_moderation.
*/
use Drupal\content_moderation\Entity\ContentModerationState;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Database\Database;
use Drupal\Core\Utility\UpdateException;
/**
* Install the workflows module.
*/
function content_moderation_update_8301() {
\Drupal::service('module_installer')->install(['workflows']);
}
/**
* Update content_moderation_state schema for 8.3.
*/
function content_moderation_update_8302() {
// Update max_length of the content_entity_type_id so the unique index can
// be added without being too long.
// @see https://www.drupal.org/node/2779931
$definition = \Drupal::entityDefinitionUpdateManager()->getFieldStorageDefinition('content_entity_type_id', 'content_moderation_state');
// Calling `setSetting` with a new `max_length` here doesn't appear to work,
// so the database columns are directly updated.
$spec = $definition->getColumns();
$spec['value']['length'] = EntityTypeInterface::ID_MAX_LENGTH;
db_change_field('content_moderation_state_field_data', 'content_entity_type_id', 'content_entity_type_id', $spec['value']);
db_change_field('content_moderation_state_field_revision', 'content_entity_type_id', 'content_entity_type_id', $spec['value']);
// Install the new workflow entity reference field.
$new_workflow_field = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Workflow'))
->setDescription(t('The workflow the moderation state is in.'))
->setSetting('target_type', 'workflow')
->setRequired(TRUE)
->setRevisionable(TRUE)
->setTranslatable(TRUE);
\Drupal::entityDefinitionUpdateManager()->installFieldStorageDefinition('workflow', 'content_moderation_state', 'content_moderation', $new_workflow_field);
}
/**
* Create the new workflow entities and migrate states/transitions.
*/
function content_moderation_update_8303() {
// Most of the logic for this is copied from the simpler use-case in
// https://www.drupal.org/node/2846618.
$new_workflows = [
// Machine name => label.
// Update these as needed for your project. It assumes that states and
// transitions in the old 8.2 version were prefixed with these machine
// names. Logic for multiple workflows utilizing a different strategy would
// need to be tweaked below.
'cusalert' => t('Customer alert'),
'escal' => t('Escalation'),
'general' => t('General'),
];
// Create workflow entities for each.
$workflows = [];
foreach ($new_workflows as $id => $label) {
// During update hooks, it is best practice to work with the raw data,
// rather than the config entity objects themselves.
$workflows[$id] = [
'id' => $id,
'label' => $label,
'type' => 'content_moderation',
'states' => [],
'transitions' => [],
'type_settings' => [
'states' => [],
],
];
}
// Loop through all states and add them to the appropriate workflow.
foreach (\Drupal::configFactory()->listAll('content_moderation.state.') as $id) {
$state = \Drupal::configFactory()->getEditable($id)->get();
list($workflow_id, $state_id) = explode('_', $state['id'], 2);
if (!isset($workflows[$workflow_id])) {
throw new UpdateException('Workflow ID "' . $workflow_id . '"" does not exist.');
}
$workflows[$workflow_id]['states'][$state_id] = [
'label' => $state['label'],
'weight' => 0,
];
$workflows[$workflow_id]['type_settings'][$state_id] = [
'published' => $state['published'],
'default_revision' => $state['default_revision'],
];
}
// Loop through all transitions and add them to the appropriate workflow.
foreach (\Drupal::configFactory()->listAll('content_moderation.state_transition.') as $id) {
$transition = \Drupal::configFactory()->getEditable($id)->get();
list ($workflow_id, $transition_id) = explode('_', $transition['id'], 2);
// Special handling for the 'archive' transition.
if ($workflow_id === 'archive') {
$workflow_id = 'general';
$transition_id = 'archive';
}
// Special handling for the 'keep_in_review' transition.
elseif ($workflow_id === 'keep') {
$workflow_id = 'general';
$transition_id = 'keep_in_review';
}
if (!isset($workflows[$workflow_id])) {
throw new UpdateException('Workflow ID "' . $workflow_id . '"" does not exist.');
}
// Pull out the old workflow ID from the to and from state IDs.
$from_state = str_replace($workflow_id . '_', '', $transition['stateFrom']);
$to_state = str_replace($workflow_id . '_', '', $transition['stateTo']);
$workflows[$workflow_id]['transitions'][$transition_id] = [
'label' => $transition['label'],
'from' => [$from_state],
'to' => $to_state,
'weight' => 0,
];
}
// Get enabled node types for old state transitions.
$enabled_workflows = _content_moderation_update_8300_get_bundle_info();
// Save all the new workflows.
foreach ($workflows as $id => $workflow) {
$config = \Drupal::configFactory()->getEditable('workflows.workflow.' . $id);
// Set enabled entity types.
$workflow['type_settings']['entity_types'] = isset($enabled_workflows[$id]) ? $enabled_workflows[$id] : [];
$config->setData($workflow);
$config->save();
}
// Delete the old states and transitions.
foreach (\Drupal::configFactory()->listAll('content_moderation.state_transition.') as $id) {
$transition = \Drupal::configFactory()->getEditable($id);
$transition->delete();
}
foreach (\Drupal::configFactory()->listAll('content_moderation.state.') as $id) {
$state = \Drupal::configFactory()->getEditable($id);
$state->delete();
}
}
/**
* Update existing content to the new workflows.
*/
function content_moderation_update_8304(&$sandbox) {
// Work in batches.
$moderation_state_storage = \Drupal::entityTypeManager()->getStorage('content_moderation_state');
if (!isset($sandbox['progress'])) {
$sandbox['progress'] = 0;
$sandbox['current_item_id'] = 0;
$sandbox['max'] = Database::getConnection()->query('SELECT COUNT(DISTINCT id, revision_id, langcode, default_langcode, moderation_state) FROM {content_moderation_state_field_revision} WHERE default_langcode = 1')->fetchField();
// Copy data over to a temporary table. This is necessary because we need
// to remove the old field, and add the new base field.
// @see https://www.drupal.org/node/2846618
Database::getConnection()->query('CREATE TABLE {wdc_workflow_update_tracker} AS (SELECT DISTINCT id, revision_id, langcode, default_langcode, moderation_state) FROM {content_moderation_state_field_revision} ORDER BY revision_id');
Database::getConnection()->query('CREATE INDEX content_moderation_update_index ON {content_moderation_update_tracker} (revision_id, default_langcode)');
// Remove old field, first purging data.
Database::getConnection()
->update('content_moderation_state_field_data')
->fields(['moderation_state' => NULL])
->execute();
Database::getConnection()
->update('content_moderation_state_field_revision')
->fields(['moderation_state' => NULL])
->execute();
$entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
$old_field = $entity_definition_update_manager->getFieldStorageDefinition('moderation_state', 'content_moderation_state');
$entity_definition_update_manager->uninstallFieldStorageDefinition($old_field);
// Add the new string field.
$new_field = BaseFieldDefinition::create('string')
->setLabel(t('Moderation state'))
->setDescription(t('The moderation state of the referenced content.'))
->setRequired(TRUE)
->setTranslatable(TRUE)
->setRevisionable(TRUE);
$entity_definition_update_manager->installFieldStorageDefinition('moderation_state', 'content_moderation_state', 'content_moderation', $new_field);
}
// Groups of 200.
$moderation_states = Database::getConnection()
->select('content_moderation_update_tracker', 'ut')
->fields('ut', ['id', 'revision_id', 'moderation_state'])
->condition('revision_id', $sandbox['current_item_id'], '>')
->condition('default_langcode', 1)
->range(0, 200)
->orderBy('revision_id', 'ASC')
->execute();
foreach ($moderation_states as $record) {
list($workflow_id, $state_id) = explode('_', $record->moderation_state, 2);
/** @var \Drupal\content_moderation\ContentModerationStateInterface $moderation_state */
$moderation_state = $moderation_state_storage->loadRevision($record->revision_id);
$moderation_state->workflow->target_id = $workflow_id;
$moderation_state->moderation_state->value = $state_id;
ContentModerationState::updateOrCreateFromEntity($moderation_state);
// Check for and process translations.
_content_moderation_update_8304_process_translations($record, $moderation_state);
$sandbox['progress']++;
$sandbox['current_item_id'] = $record->revision_id;
}
$sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']);
if ($sandbox['#finished'] >= 1) {
// Drop the update table.
Database::getConnection()->query('DROP TABLE {content_moderation_update_tracker}');
return t('Updated @count content moderation state records.', ['@count' => $sandbox['max']]);
}
// Print more useful status if running from drush.
if (function_exists('drush_print')) {
drush_print(t('Finished updating @percent percent of the content moderation state records', ['@percent' => round($sandbox['#finished'] * 100, 1)]), 5);
}
}
/**
* Process any translated workflows.
*
* @param \stdClass $record
* Database result record containing id, revision_id, and moderation_state.
* @param \Drupal\content_moderation\Entity\ContentModerationState $moderation_state
* The default language moderation state.
*/
function _content_moderation_update_8304_process_translations(stdClass $record, ContentModerationState $moderation_state) {
$translations = Database::getConnection()
->select('content_moderation_update_tracker', 'ut')
->fields('ut', ['moderation_state', 'langcode'])
->condition('revision_id', $record->revision_id)
->condition('default_langcode', 1, '<>')
->execute();
foreach ($translations as $translation) {
list($workflow_id, $state_id) = explode('_', $translation->moderation_state, 2);
$moderation_state = $moderation_state->getTranslation($translation->langcode);
$moderation_state->workflow->target_id = $workflow_id;
$moderation_state->moderation_state->value = $state_id;
ContentModerationState::updateOrCreateFromEntity($moderation_state);
}
}
/**
* Helper function to gather which workflow is enabled for node types.
*
* @return array
* An array of node types keyed by workflow ID.
*/
function _content_moderation_update_8300_get_bundle_info() {
$enabled_bundles = [];
// Loop through node type configs and find enabled workflows.
// Note, we can hard-code node here since moderation is not enabled on other
// entity types.
foreach (\Drupal::configFactory()->listAll('node.type.') as $node_type_id) {
$bundle_config = \Drupal::configFactory()->getEditable($node_type_id);
if (!$third_party_settings = $bundle_config->get('third_party_settings')) {
continue;
}
// Remove content moderation from the third party settings if it exists.
$third_party_settings_updated = array_diff_key($third_party_settings, array_flip(['content_moderation']));
if (count($third_party_settings) !== $third_party_settings_updated) {
$content_moderation = $third_party_settings['content_moderation'];
if (!empty($content_moderation['enabled'])) {
// Infer the workflow from the default state ID.
list($workflow_id,) = explode('_', $content_moderation['default_moderation_state'], 2);
$enabled_bundles[$workflow_id]['node'][] = $bundle_config->get('type');
}
}
// Update third party settings.
if (empty($third_party_settings_updated)) {
$bundle_config->clear('third_party_settings');
}
else {
$bundle_config->set('third_party_settings', $third_party_settings_updated);
}
$bundle_config->save();
}
return $enabled_bundles;
}
/**
* Implements hook_update_dependencies().
*/
function content_moderation_update_dependencies() {
$dependencies = [];
// The workflows module must be enabled for many of the 8.3 update hooks
// to properly complete.
$dependencies['system'][8300] = [
'content_moderation' => 8301,
];
$dependencies['comment'][8300] = [
'content_moderation' => 8301,
];
$dependencies['block_content'][8300] = [
'content_moderation' => 8301,
];
return $dependencies;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment