Migrate als Ersatz für Update-Hooks

Als Entwickler neige ich dazu, faul zu sein. Ich bin immer auf der Suche nach Tools oder Shortcuts, die mir das Leben einfacher machen.

Neulich stolperte ich über einen , in dem er die Verwendung von Migrate anstelle eines benutzerdefinierten Update-Skripts zum Aktualisieren bestehender Entitäten zeigt. Da ich zu dem Zeitpunkt noch keine Verwendung dafür hatte, speicherte ich es in dem "vielleicht später hilfreich"-Teil meines Gehirns ... und vergaß es.

Letzte Woche musste ich jedoch in einem Projekt ein bestehendes Feld zur Formularanzeige aller Paragraphs-Typen hinzufügen. Zusätzlich sollte ein neues Feld hinzugefügt und konfiguriert werden. Da ich die Felder nicht für alle Paragraphs-Typen (es gibt etwa 80 davon in diesem Projekt) manuell konfigurieren wollte (wie Du weisst, bin ich ein fauler Entwickler), hätte ich üblicherweise einen Update-Hook für diese Aufgabe geschrieben. Da fiel mir aber der Tweet wieder ein und ich dachte "Wäre es nicht auch möglich, die Konfiguration einfach mit Migrate zu aktualisieren?".

Was brauchen wir?

Der erste Teil der Aufgabe ist einfach: um ein vorhandenes Feld im Formular einer Entität (in diesem Fall Paragraphs) anzuzeigen, wird es einfach aus dem "ausgeblendeten" Bereich in den Inhaltsbereich gezogen.

Paragraphs form display
Formularanzeige eines Paragraphs-Typen.

Nachdem ich das Feld "Published" in den Inhaltsbereich verschoben hatte, exportierte ich die Konfigurationsänderungen, um zu sehen, was geschieht und erhielt das folgende Ergebnis für core.entity_form_display.paragraph.text.default.yml:

Configuration changes in Paragraphs form display
Konfigurationsänderungen in der Anzeige des Formulars für Paragraphs

In meiner Migration muss ich also genau diese Konfigurationsänderung für alle Paragraphs-Typen replizieren.

Einstellungen für die Formularanzeige migrieren

Bei der Migration benötige ich zunächst ein Quell-Plugin für alle verfügbaren Paragraphs-Typen. Da ich bereits die notwendigen Änderungen an der Formulardarstellung des Paragraphs-Typs "Text" vorgenommen hatte, braucht das Quell-Plugin auch die Möglichkeit, bestimmte Elemente auszuschließen (naja, ich hätte eventuell die bisherigen Konfigurationsänderungen rückgängig machen und mit einer aktuellen Datenbanksicherung neu beginnen können, aber ...).

  1. <?php
  2.  
  3. namespace Drupal\up_migrate\Plugin\migrate\source;
  4.  
  5. use ArrayObject;
  6. use Drupal\Core\StringTranslation\StringTranslationTrait;
  7. use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
  8. use Drupal\migrate\Plugin\MigrationInterface;
  9. use Drupal\paragraphs\Entity\ParagraphsType;
  10.  
  11. /**
  12.  * Source plugin for ParagraphsType.
  13.  *
  14.  * @MigrateSource(
  15.  *   id = "up_paragraphs_type",
  16.  *   source_module = "up_migrate"
  17.  * )
  18.  */
  19. class UpParagraphsType extends SourcePluginBase {
  20.  
  21.   use StringTranslationTrait {
  22.     t as t_original;
  23.   }
  24.  
  25.   /**
  26.    * List of paragraphs types to exclude.
  27.    *
  28.    * @var array
  29.    */
  30.   protected $exclude = [];
  31.  
  32.   /**
  33.    * List of paragraph types.
  34.    *
  35.    * @var array
  36.    */
  37.   protected $items = [];
  38.  
  39.   /**
  40.    * {@inheritdoc}
  41.    */
  42.   protected function t($string, array $args = [], array $options = []) {
  43.     if (empty($options['context'])) {
  44.       $options['context'] = 'up_migrate';
  45.     }
  46.  
  47.     return $this->t_original($string, $args, $options);
  48.   }
  49.  
  50.   /**
  51.    * {@inheritdoc}
  52.    */
  53.   public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration) {
  54.     parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
  55.  
  56.     if (isset($configuration['exclude'])) {
  57.       $this->exclude = $configuration['exclude'];
  58.     }
  59.   }
  60.  
  61.   /**
  62.    * {@inheritdoc}
  63.    */
  64.   public function fields() {
  65.     return [
  66.       'id' => $this->t('ID'),
  67.       'label' => $this->t('Label'),
  68.     ];
  69.   }
  70.  
  71.   /**
  72.    * {@inheritdoc}
  73.    */
  74.   public function getIds() {
  75.     $ids['id']['type'] = 'string';
  76.     return $ids;
  77.   }
  78.  
  79.   /**
  80.    * Return a comma-separated list of paragraph type ids.
  81.    */
  82.   public function __toString() {
  83.     return implode(', ', array_column($this->items, 'id'));
  84.   }
  85.  
  86.   /**
  87.    * {@inheritdoc}
  88.    */
  89.   protected function initializeIterator() {
  90.     $this->items = [];
  91.     $paragraphs_types = ParagraphsType::loadMultiple();
  92.     /** @var \Drupal\paragraphs\ParagraphsTypeInterface $paragraphs_type */
  93.     foreach ($paragraphs_types as $paragraphs_type) {
  94.       $this->items[$paragraphs_type->id()] = [
  95.         'id' => $paragraphs_type->id(),
  96.         'label' => $paragraphs_type->label(),
  97.       ];
  98.     }
  99.  
  100.     if (!empty($this->exclude)) {
  101.       $this->items = array_diff_key($this->items, array_flip($this->exclude));
  102.     }
  103.  
  104.     return (new ArrayObject($this->items))->getIterator();
  105.   }
  106.  
  107.   /**
  108.    * {@inheritdoc}
  109.    */
  110.   public function count($refresh = FALSE) {
  111.     parent::count($this->items);
  112.   }
  113.  
  114. }
  115.  

Wie im obigen Gist deutlich wird, ist das Quell-Plugin sehr einfach. Es holt sich eine Liste aller verfügbaren Paragraphs-Typen und entfernt die Typen, die ausgeschlossen werden sollen.

Der folgende Schritt ist, eine Migration zu schreiben, die die Konfiguration für die Formularanzeige aktualisiert.

  1. id: paragraphtypes_form_display__status
  2. label: Add status field to paragraph form display.
  3. source:
  4.   plugin: up_paragraphs_type
  5.   exclude:
  6.    - text
  7.   constants:
  8.     entity_type: paragraph
  9.     field_name: status
  10.     form_mode: default
  11.     options:
  12.       region: content
  13.       settings:
  14.         display_label: true
  15.       third_party_settings: {  }
  16.       type: boolean_checkbox
  17.       weight: 5
  18. process:
  19.   bundle: id
  20.   entity_type: constants/entity_type
  21.   field_name: constants/field_name
  22.   form_mode: constants/form_mode
  23.   options: constants/options
  24. destination:
  25.   plugin: component_entity_form_display
  26. migration_tags:
  27.  - up_paragraphstype
  28.  

Die Migration verwendet das neue Quell-Plugin "up_paragraphs_type" und schließt den Paragraphs-Typ "Text" aus der zu verarbeitenden Liste aus. In Zeile 11..17 werden genau die gleichen Anzeigeeinstellungen gesetzt wie im Screenshot, der die Konfigurationsänderungen für den Paragraphs-Typ "Text" zeigt.

Im Abschnitt "process" verarbeitet die Migration die Ergebnisse aus dem Quell-Plugin, wobei nur die vom jeweiligen Paragraphs-Typ gelieferte ID verwendet wird, und ansonsten die zuvor definierten Konstanten genutzt werden. Da die Form-Display-Konfiguration aktualisiert werden soll, wird das " component_entity_form_display"-Plugin als Ziel gewählt, das glücklicherweise direkt von Drupal Core bereitgestellt wird.

Nach dem Durchführen der Migration sind alle auf der Website verfügbaren Paragraphs-Typen so konfiguriert, dass sie das Kontrollkästchen "Veröffentlicht" anzeigen. Yeah!

Was ist mit neuen Feldern?

Aber was ist mit dem neuen Feld, das ich erstellen musste? Im Grunde unterscheidet sich die Migration nicht wirklich von der Obigen. Das Einzige, was hinzukommen muss, ist eine zusätzliche Migration, die die Feldkonfiguration für jeden Paragraphs-Typ erstellt.

Sagen wir, wir möchten ein Textfeld mit dem Namen "Kommentar" für alle Paragraphs-Typen erstellen. Dann muss die Field-Storage für dieses Feld etwa wie folgt erstellt werden:

  1. id: paragraphtypes_field_storage_paragraphs_comment
  2. label: Define field storage for field_paragraphs_comment.
  3. source:
  4.   plugin: empty
  5.   constants:
  6.     entity_type: paragraph
  7.     id: paragraph.field_paragraphs_comment
  8.     field_name: field_paragraphs_comment
  9.   type: string
  10.   cardinality: 1
  11.   settings:
  12.     max_length: 255
  13.   langcode: en
  14.   translatable: true
  15. process:
  16.   entity_type: constants/entity_type
  17.   id: constants/id
  18.   field_name: constants/field_name
  19.   type: constants/type
  20.   cardinality: constants/cardinality
  21.   settings: constants/settings
  22.   langcode: constants/langcode
  23.   translatable: constants/translatable
  24. destination:
  25.   plugin: entity:field_storage_config
  26. migration_tags:
  27.  - up_paragraphstype
  28.  

Hinweis: Wenn das Feld manuell erstellt wurde (wie bei mir für den Absatztyp "Text"), kann diese Migration übersprungen werden, da der Feldspeicher bereits vorhanden ist und es ansonsten zu Fehlern kommt.

Um das neu erstellte Feld hinzuzufügen, muss für jeden Paragraphs-Typ eine Feldinstanz erstellt werden. Dies kann über das Migrationsziel "entity:field_config" durchgeführt werden:

  1. id: paragraphtypes_field_paragraphs_comment
  2. label: Adds field_paragraphs_comment to paragraph types.
  3. source:
  4.   plugin: up_paragraphs_type
  5.   exclude:
  6.    - text
  7.   constants:
  8.     entity_type: paragraph
  9.     field_name: field_paragraphs_comment
  10.     translatable: true
  11.     label: 'Comment'
  12. process:
  13.   entity_type: constants/entity_type
  14.   field_name: constants/field_name
  15.   bundle: id
  16.   label: constants/label
  17.   translatable: constants/translatable
  18. destination:
  19.   plugin: entity:field_config
  20. migration_tags:
  21.  - up_paragraphstype
  22. migration_dependencies:
  23.   required:
  24.    - paragraphtypes_field_storage_paragraphs_comment
  25.  

Ziemlich einfach, oder?

Wie geht es weiter?

Nachdem wir gesehen haben, wie einfach das Erstellen und Aktualisieren der Feldkonfiguration mittels Migrate ist, kamen uns gleich mehrere neue Ideen in den Sinn. Es müsste so beispielsweise auch möglich sein, Entity-Typen mit allen erforderlichen Feldern zu erstellen und die Konfiguration über Migrate zu erzeugen. Dies wäre eine großartige Option, um zusätzliche Funktionen auf einer Website einfach per Mausklick zu aktivieren (natürlich muss die Konfiguration exportiert und eventuell noch Weiteres getan werden).

Aber das ist ein Thema für einen weiteren Artikel ...