From c84d32ac400ed92355b5e491be0c247dd892b52c Mon Sep 17 00:00:00 2001
From: Andreas Hennings <andreas@dqxtech.net>
Date: Fri, 18 Mar 2022 05:36:47 +0100
Subject: [PATCH] OEL-1281: Add upgrade path from
 oe_bootstrap_theme_paragraphs.

---
 .../oe_whitelabel_paragraphs.install          | 162 +++++++++++++++++-
 1 file changed, 159 insertions(+), 3 deletions(-)

diff --git a/modules/oe_whitelabel_paragraphs/oe_whitelabel_paragraphs.install b/modules/oe_whitelabel_paragraphs/oe_whitelabel_paragraphs.install
index a6ba792d..d9a575e6 100644
--- a/modules/oe_whitelabel_paragraphs/oe_whitelabel_paragraphs.install
+++ b/modules/oe_whitelabel_paragraphs/oe_whitelabel_paragraphs.install
@@ -7,19 +7,49 @@
 
 declare(strict_types = 1);
 
+use Drupal\field\Entity\FieldConfig;
 use Drupal\oe_bootstrap_theme\ConfigImporter;
 
 /**
  * Implements hook_install().
  *
- * Customise fields for whitelabel paragraphs.
+ * Customize paragraphs fields and display configuration.
  */
-function oe_whitelabel_paragraphs_install($is_syncing): void {
-  // If we are installing from config, we bail out.
+function oe_whitelabel_paragraphs_install(bool $is_syncing): void {
+  // Find legacy fields from oe_bootstrap_theme_paragraphs.
+  // This needs to happen at the start, to allow for early abort.
+  $field_names_by_bundle = _oe_whitelabel_paragraphs_install_get_legacy_fields_map();
+
   if ($is_syncing) {
+    // The module is being installed as part of a config import.
+    if ($field_names_by_bundle) {
+      // There is data to be migrated. This should not happen as a side effect
+      // of config-import. Instead, the installation should be enacted from an
+      // update hook.
+      throw new \Exception('This module should be installed through an update hook, not through config-import, if there is still leftover data from oe_bootstrap_theme_paragraphs to migrate.');
+    }
+    // No data needs to be migrated, but still, no configuration should be
+    // imported in hook_install() during config-import.
+    return;
+  }
+
+  // The module is being installed explicitly, e.g. via a hook_update_N().
+  // Configuration needs to be imported explicitly.
+  _oe_whitelabel_paragraphs_install_config();
+
+  if (!$field_names_by_bundle) {
+    // No fields to migrate and clean up - finished.
     return;
   }
 
+  _oe_whitelabel_paragraphs_install_migrate_field_data($field_names_by_bundle);
+  _oe_whitelabel_paragraphs_install_drop_legacy_fields($field_names_by_bundle);
+}
+
+/**
+ * Imports configuration on module install.
+ */
+function _oe_whitelabel_paragraphs_install_config(): void {
   $configs = [
     'core.entity_form_display.paragraph.oe_accordion_item.default',
     'core.entity_form_display.paragraph.oe_description_list.default',
@@ -39,3 +69,129 @@ function oe_whitelabel_paragraphs_install($is_syncing): void {
 
   ConfigImporter::importMultiple('oe_whitelabel_paragraphs', '/config/overrides/', $configs);
 }
+
+/**
+ * Gets a map of legacy fields to be migrated on install.
+ *
+ * @return string[][]
+ *   Format: $[$bundle][$dest_field_name] = $legacy_field_name.
+ *   Empty, if oe_bootstrap_theme_paragraphs was not installed in the past.
+ */
+function _oe_whitelabel_paragraphs_install_get_legacy_fields_map(): array {
+  /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */
+  $entity_field_manager = \Drupal::service('entity_field.manager');
+  /**
+   * @var array[] $fields_map
+   *   Format: $[$field_name] = [
+   *     'type' => $field_type,
+   *     'bundles' => [$bundle_name => $bundle_name, ...],
+   *   ].
+   */
+  $fields_map = $entity_field_manager->getFieldMap()['paragraph'] ?? [];
+
+  $field_names_map = [
+    'oe_w_links_block_background' => 'oe_bt_links_block_background',
+    'oe_w_links_block_orientation' => 'oe_bt_links_block_orientation',
+    'oe_w_n_columns' => 'oe_bt_n_columns',
+    'oe_w_orientation' => 'oe_bt_orientation',
+  ];
+
+  /**
+   * @var string[][] $field_names_by_bundle
+   *   Format: $[$bundle][$dest_field_name] = $source_field_name.
+   */
+  $field_names_by_bundle = [];
+  foreach ($field_names_map as $dest_field_name => $source_field_name) {
+    if (!isset($fields_map[$source_field_name])) {
+      // No field to migrate from.
+      continue;
+    }
+    if (!isset($fields_map[$dest_field_name])) {
+      // No target field to migrate to - this is unexpected.
+      continue;
+    }
+    // Find bundles where both source and destination fields exist.
+    foreach (array_intersect_key(
+      $fields_map[$dest_field_name]['bundles'],
+      $fields_map[$source_field_name]['bundles'],
+    ) as $bundle) {
+      $field_names_by_bundle[$bundle][$dest_field_name] = $source_field_name;
+    }
+  }
+
+  return $field_names_by_bundle;
+}
+
+/**
+ * Migrates field data from the old oe_bootstrap_theme_paragraphs module.
+ *
+ * Normally this should happen through a batch process, e.g. via $sandbox in a
+ * hook_update_N(). Unfortunately, hook_install() does not support batch
+ * processes.
+ * For the known projects where this will be used, we assume that not too many
+ * paragraphs exist yet - fingers crossed.
+ *
+ * See https://atendesigngroup.com/articles/programmatically-copy-field-data-drupal-8.
+ *
+ * @param string[][] $field_names_by_bundle
+ *   Format: $[$bundle][$dest_field_name] = $source_field_name.
+ */
+function _oe_whitelabel_paragraphs_install_migrate_field_data(array $field_names_by_bundle): void {
+  $paragraphs_storage = \Drupal::entityTypeManager()->getStorage('paragraph');
+
+  // Load all the paragraph ids.
+  $query = $paragraphs_storage->getQuery();
+  $query->allRevisions();
+  $query->condition('type', array_keys($field_names_by_bundle), 'IN');
+  $paragraph_ids = $query->execute();
+
+  foreach ($paragraph_ids as $revision_id => $paragraph_id) {
+    $paragraph_revision = $paragraphs_storage->loadRevision($revision_id);
+    if (!$paragraph_revision) {
+      // Revision not found - this is unexpected, but survivable.
+      continue;
+    }
+    $modified = FALSE;
+    foreach ($field_names_by_bundle[$paragraph_revision->bundle()] ?? [] as $dest_field_name => $source_field_name) {
+      if ($paragraph_revision->get($source_field_name)->isEmpty()) {
+        // Source field has no data.
+        continue;
+      }
+      if (!$paragraph_revision->get($dest_field_name)->isEmpty()) {
+        // Destination already has data.
+        continue;
+      }
+      // Copy the field value.
+      // For these simple field types, magic __set() does the job.
+      $paragraph_revision->$dest_field_name = $paragraph_revision->$source_field_name;
+      // Do not unset the old field, because it might be required.
+      // Remember that the revision needs saving.
+      $modified = TRUE;
+    }
+    if (!$modified) {
+      // No saving is needed.
+      continue;
+    }
+    $paragraph_revision->setNewRevision(FALSE);
+    $paragraph_revision->save();
+  }
+}
+
+/**
+ * Removes legacy field instances from oe_bootstrap_theme_paragraphs module.
+ *
+ * @param string[][] $field_names_by_bundle
+ *   Format: $[$bundle][*] = $source_field_name.
+ */
+function _oe_whitelabel_paragraphs_install_drop_legacy_fields(array $field_names_by_bundle): void {
+  foreach ($field_names_by_bundle as $bundle => $field_names) {
+    foreach ($field_names as $field_name) {
+      $definition = FieldConfig::loadByName('paragraph', $bundle, $field_name);
+      if ($definition === NULL) {
+        // Field no longer exists. This is unexpected, but can be ignored.
+        continue;
+      }
+      $definition->delete();
+    }
+  }
+}
-- 
GitLab