NiedersachsenMetall: Ein weiteres Drupal 7 Projekt erfolgreich migriert

Für mich ist es immer ein aufregender und irgendwie auch „beängstigender“ Moment, wenn nach monatelanger Arbeit ein Drupal Projekt final auf eine neue Version migriert wird. Es stellt sich immer die Frage: „Wird alles klappen?“, „Wurde etwas übersehen?“, oder noch schlimmer „Werden alle Daten vollständig und korrekt übernommen?“ Dabei sind diese Gedanken eher unbegründet, wurde doch die Migration zuvor etliche Male durchgeführt und getestet. Ich kann es an dieser Stelle schon einmal vorwegnehmen: Es hat alles wunderbar geklappt.

In diesem Beitrag möchte ich meine persönliche Sicht auf das Projekt, dessen Herausforderungen und Lösungsansätze schildern. Meine Rolle war dabei die Projektsteuerung des Entwicklungsteams, sowie die federführende Entwicklung der Datenmigration und Theme-Umstellung.

Das Projekt

Die bestehende Drupal 7 Installation von NiedersachsenMetall (die bisher von einer Agentur betreut wurde) ist funktionell ziemlich Standard. Es dreht sich alles um Inhalte und es gibt keine besonderen Logiken oder Prozessabbildungen. Von daher war das Projekt an sich keine wirkliche technische Herausforderung. Was das Projekt jedoch etwas speziell gemacht hat, war die schiere Menge an Inhaltstypen, hunderten Feldern und fast 100 Paragraph-Types, mit einer ebenso großen Vielzahl an Templates.

NiedersachsenMetall ist ein Verband der Metall- und Elektroindustrie in Niedersachsen. Der Verband vertritt die Interessen seiner mehr als 300 Mitgliedsunternehmen mit rund 110.000 Beschäftigten. Die Mitgliedsunternehmen reichen von kleinen Familienbetrieben bis hin zu großen Konzernen und decken eine Vielzahl von Branchen ab, darunter Automobilbau, Maschinenbau, Elektrotechnik, Luft- und Raumfahrt, Medizintechnik und viele mehr. Die Website des Verbandes spielt eine zentrale Rolle bei der Informationsvermittlung an die Mitglieder. Daher ist die einfache Pflege und Steuerung der Informationen über die – an das Drupal – angeschlossenen Websites von zentraler Bedeutung.

Website von NiedersachsenMetall dargestellt auf einem Mac

Die Herausforderungen

Wie eingangs bereits erläutert, war die zentrale Herausforderung die schiere Menge an zu migrierenden Strukturen und Themes/Templates. Insgesamt teilten sich diese in folgende Bereiche auf:

  1. Migration der grundlegenden Datenstrukturen und Inhalte
  2. Umstellung der bisherigen Bild- und Dateifelder auf Media-Entities, um die Installation zukunftsfähig zu gestalten
  3. Bändigung der komplexen Abhängigkeiten zwischen den Migrationen, insbesondere für Paragraphs
  4. Umstellung der vorhandenen Themes von PHPTemplate auf Twig, sowie parallel von Bootstrap 3 und 5 und einer besseren Organisation für eine leichtere Wartbarkeit und Weiterentwicklung

Das Ganze wurde begleitet von einer entsprechenden Projektsteuerung und Dokumentation, insbesondere um den Status und die Fortschritte bei der Umstellung der einzelnen Templates zu verwalten. Das normale Ticketsystem wäre hier sehr schnell übersichtlich geworden und die schiere Menge hätte wohl auch zu einer Lähmung des Projektes geführt. Daher wurde das Projekt in verschiedene Pakete und Abschnitte aufgeteilt und dann je nach Bedarf auf zusätzliche Werkzeuge für die Projektverwaltung zurückgegriffen. So sind neben unserer internen Projektverwaltung (Podio), auch Logseq und später Tana zum Einsatz gekommen.

Für die Drupal 7 Migration selbst, haben wir auch wieder auf Basis-Module zurückgegriffen, die ich bereits in einem anderen Beitrag vorgestellt habe.

Unter anderem:

Multi-Domain-Website

Eine Besonderheit bei dem Projekt war, dass aus einer Drupal Installation insgesamt 13 Websites gesteuert werden. Es handelt sich also um eine Multi-Domain-Installation, was insbesondere bei der Migration und der Sicherstellung bisheriger Funktionen dafür gesorgt hat, dass eigene Migrations-Erweiterungen und zusätzliche Logiken entwickelt wurden. Teilweise auch dadurch bedingt, dass eine Vielzahl kleinerer Module aus dem Domain-Modul-Universum (welches für die Realisierung von Multi-Domain-Websites zentral ist) keine Drupal 9 kompatible Version aufweisen. Während der Entwicklung des Projekts sind wir auch auf den einen oder anderen Bug im Zusammenhang mit dem Domain-Modul gestoßen und haben notwendige Patches entwickelt und getestet.

Das Domain-Modul für Drupal 9/10 bietet bereits eine Migration, um die vorhandenen Domains in die neue Drupal-Installation zu migrieren. Allerdings beschränkt sich diese Migration derzeit nur auf die Domain-Informationen selbst und nicht auf die Zuweisungen der Domains zu den einzelnen Inhalten (Nodes) und Benutzerkonten. Um sicherzustellen, dass die Inhalte nur auf den entsprechenden zugeordneten Domains angezeigt werden, haben wir die Migration der Inhaltstypen und Benutzerkonten entsprechend erweitert.

Ein weiteres hilfreiches Modul im Umgang mit solchen Multi-Domain-Websites ist Domain Path. Damit ist es möglich, dass ein Pfad wie /kontakt je nach Domain auf eine andere Seite (Node) verweisen kann. Denn die normalen Drupal Aliases können in der Datenbank nur einmalig vorkommen.

Migration von Paragraph-Types und deren Inhalte

Zusätzliche Komplexität hat die Migration der zahlreichen Paragraph-Types und damit verbundene Inhalte verursacht. Das Paragraph-Modul in Drupal wird sehr häufig verwendet, um Inhalte auf einer Seite in logische Blöcke zu organisieren und somit Redakteuren die Erstellung komplexer strukturierter Inhalte mittels einer intuitiven Benutzeroberfläche zu ermöglichen. Obwohl das Paragraph-Modul direkt passende Drupal 7 Migrationen für die Übernahme der Typen und Inhalte mitbringt, haben diese in unserem Fall nicht ausgereicht.

Ein Problem bestand darin, dass die Standard-Migrationen nicht den Anwendungsfall vollständig berücksichtigen, wenn innerhalb eines Paragraphs weitere Paragraphs enthalten. In unserem Fall konnten solche Abhängigkeiten zwischen Paragraph Entities über mehrere Ebenen vorhanden sein. So werden bei der Migration dieser Inhalte die Referenzen zwischen den Paragraphs versucht aufzulösen (ParagraphLookup) und bei Nichtexistenz werden „Stummel-Paragraphs“ erstellt. Dies führt im Endeffekt dazu, dass bei den Paragraph-IDs ein Chaos entsteht und andere Paragraphs zugeordnet und angezeigt werden.

Wie bei anderen komplexen Drupal Migrationen auch, haben wir die Struktur der Migrationen (Konfiguration) von der eigentlichen Ausführung getrennt. Dies geschieht mittels des Migrate Upgrade Moduls (drush migrate-upgrade —legacy-dB-key=migrate —configure-only). Dabei haben wir einen Hook implementiert, der die notwendigen Paragraph-Abhängigkeiten identifiziert und mit in die generierte Migration Konfiguration schreibt. Dadurch wurden dann die Migrationen für Paragraphs in der richtigen Reihenfolge ausgeführt und die Referenzen zwischen den einzelnen Paragraphs konnten korrekt aufgelöst werden. Die Hook-Implementierung kannst Du am Ende dieses Artikels finden.

Twig Templates und CSS Styles

Neben den technischen Herausforderungen spielte bei diesem Projekt auch die schwierige Menge an Templates und CSS Styles eine wesentliche Rolle. Allein die bestehende, zentrale LESS-Datei in einem Theme umfasste allein 6.000 Zeilen. Insgesamt sind beide Themes auf gute 43.000 Zeilen an LESS-Code gekommen, das verwendete Bootstrap 3 Framework dabei nicht mitgezählt.

Durch eine Neustrukturierung der Themes und die konsequente Anwendung von komponentenbasierten Styles konnten wir die Komplexität erheblich reduzieren und so auch eine wesentlich bessere Weiterentwicklung in Zukunft ermöglichen. Alle 12 Domains verwenden ein Basis-Theme, und eine Domain verwendet ein eigenes Theme. Wir haben ein drittes Theme eingeführt, welches zentrale Templates und Komponenten für die anderen beiden Themes bereitstellt. Zusätzlich weist jede der 12 Domains ihr eigenes Farbschema auf, das wir mithilfe von CSS Variablen umgesetzt haben.

Im Ergebnis konnten wir so allein unsere SCSS Dateien über alle drei Themes hinweg auf knapp 9.000 Zeilen reduzieren und somit fast 35.000 Zeilen einsparen. Das macht sich natürlich erheblich in der Dateigröße der generierten CSS-Dateien bemerkbar und sorgt so für schnelle Zugriffszeiten und Datensparsamkeit.

Zusammenfassung

Die finale Umstellung der Website auf Drupal 9 und Umzug auf eine neue Infrastruktur verlief erfolgreich und nur mit einem kurzzeitigen Ausfall, als die DNS-Einstellungen für die Domains angepasst wurden. Ein Großteil der Migrationen verlief ohne Probleme und Inhaltstypen, Felder, Benutzer und viele weitere Strukturen und Inhalte wurden zuverlässig übernommen. Es gibt fertige Migrationspfade für Module wie Paragraphs, Media (z.B. um auch aus bisherigen Bild/Datei-Feldern entsprechende Media Entities zu machen) und Webform. Damit lässt sich sehr schnell eine Basis für die D7-Migration aufbauen. Die eigentliche Arbeit liegt dann in den Details, die sich meist jedoch durch die Projektbesonderheiten ergeben.

Derzeit basiert die Website noch auf Drupal 9, obwohl bereits Drupal 10 veröffentlicht ist. Zu Beginn der Projektumsetzung war dies jedoch noch nicht der Fall und wir wollten die Migration noch mit Drupal 9 durchführen, womit die ganze Zeit auch getestet wurde. Dadurch sollten zusätzliche Überraschungen und Fehler vermieden werden, wenn wir vor der Migration bereits auf Drupal 10 gewechselt wären. Nach der abgeschlossenen Umstellung ist der nächste Schritt nun der Wechsel zu Drupal 10. Das Projekt ist bereits weitestgehend Drupal 10 kompatibel, sodass dieser Wechsel problemlos möglich sein wird.

Anhang: Hook Implementierung um Paragraph Dependencies bei der Migration zu berücksichtigen

/**
 * Implements hook_migration_plugins_alter().
 *
 * Without this implementation, Paragraph Migrations will not take dependencies
 * into account if a Paragraph Type itself contains Paragraphs.
 * This can cause a paragraph migration (A) that contains references to
 * paragraphs of type (B) to be executed before the paragraph migration B.
 * In this case the paragraph entities of B don't exist yet, so ParagraphLookup
 * generates stub paragraph entities and a huge mess with the IDs is created.
 *
 * In this hook we extract the field dependencies from paragraph fields and
 * thus define the dependencies between paragraph types to each other.
 * This ensures that Paragraph Migration B is executed before Migration A.
 */
function YOUR_MODULE_migration_plugins_alter(array &$migrations) {
  // Detect drush.
  // Do this only in the context of drush migrate-upgrade command.
  // Otherwise, during the migration import, the migrate_upgrade_xyz will be
  // executed together with the standard d7 migrations which leads to
  // random errors.
  // This implementation is only relevant when the nigration is generated
  // via via migrate-upgrade.
  $isMigrateUpgradeContext = FALSE;
  if (isset($GLOBALS['argv']) && is_array($GLOBALS['argv'])) {
    foreach ($GLOBALS['argv'] as $arg) {
      if ($arg === 'migrate-upgrade') {
        $isMigrateUpgradeContext = TRUE;
      }
    }
  }
  if (!$isMigrateUpgradeContext) {
    return;
  }

  /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */
  $entity_field_manager = \Drupal::service('entity_field.manager');

  foreach ($migrations as $migration_key => &$migration) {
    $source_entity_type = !empty($migration['source']['entity_type']) ? $migration['source']['entity_type'] : FALSE;
    if (!$source_entity_type && isset($migration['source']['plugin']) && $migration['source']['plugin'] === 'd7_paragraphs_item') {
      $source_entity_type = 'paragraph';
    }
    $source_entity_bundle = !empty($migration['source']['bundle']) ? $migration['source']['bundle'] : FALSE;

    if ($source_entity_type && $source_entity_bundle) {
      // Get the field definitions for the current migration entity type bundle.
      try {
        $field_definitions = $entity_field_manager->getFieldDefinitions($source_entity_type, $source_entity_bundle);
      }
      catch (Exception) {
        continue;
      }

      if (!$field_definitions) {
        continue;
      }

      $relevant_dependencies = [];
      foreach ($field_definitions as $field) {
        // Find fields inside the definitions that contains paragraph entities.
        if ($field instanceof FieldConfig && $field->get('field_type') === 'entity_reference_revisions') {
          $dependencies = $field->getDependencies();

          // Calculate the paragraph migration dependency based on the field
          // dependencies.
          if (isset($dependencies['config'])) {
            foreach ($dependencies['config'] as $dependency) {
              $match = [];
              // [entity_type_id].paragraphs_type.[bundle] .
              // Example: paragraphs.paragraphs_type.adressen_akkordeon_element.
              preg_match('/^(.*)\.paragraphs_type\.(.*)$/', $dependency, $match);

              if (isset($match[1]) && isset($match[2])) {
                // d7_paragraphs:[bundle] .
                $migration_dependency_id = 'd7_paragraphs:' . $match[2];

                if (array_key_exists($migration_dependency_id, $migrations)) {
                  $relevant_dependencies[] = $migration_dependency_id;
                }
              }
            }
          }
        }
      }

      $relevant_dependencies = array_unique($relevant_dependencies);

      // Prevent that the current migration itself is defined as a dependency.
      foreach ($relevant_dependencies as $i => $dependency) {
        if ($dependency === $migration_key) {
          unset($relevant_dependencies[$i]);
        }
      }

      if (!empty($relevant_dependencies)) {
        $migration['migration_dependencies']['required'] = array_merge($migration['migration_dependencies']['required'], $relevant_dependencies);
      }
    }
  }
}
Profile picture for user cbeier
Autor

Christian Beier

Hi, ich bin Christian! Ich unterstütze Unternehmen dabei ihre Geschäftsanwendungen und Prozesse zu digitalisieren. Hier blogge ich über Drupal, Webentwicklung, Digitalisierung, Projektmanagement und andere Themen, die mich interessieren. Per RSS abonnieren. Schreibe mir eine Mail an [email protected] oder folge mir auf LinkedIn oder Mastodon.