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.

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:

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
 3namespace Drupal\up_migrate\Plugin\migrate\source;
 4
 5use ArrayObject;
 6use Drupal\Core\StringTranslation\StringTranslationTrait;
 7use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
 8use Drupal\migrate\Plugin\MigrationInterface;
 9use 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 */
19class 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.

 1id: paragraphtypes_form_display__status
 2label: Add status field to paragraph form display.
 3source:
 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
18process:
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
24destination:
25  plugin: component_entity_form_display
26migration_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:

 1id: paragraphtypes_field_storage_paragraphs_comment
 2label: Define field storage for field_paragraphs_comment.
 3source:
 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
15process:
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
24destination:
25  plugin: entity:field_storage_config
26migration_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:

 1id: paragraphtypes_field_paragraphs_comment
 2label: Adds field_paragraphs_comment to paragraph types.
 3source:
 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'
12process:
13  entity_type: constants/entity_type
14  field_name: constants/field_name
15  bundle: id
16  label: constants/label
17  translatable: constants/translatable
18destination:
19  plugin: entity:field_config
20migration_tags:
21  - up_paragraphstype
22migration_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 ...

Stefan Borchert
  • Geschäftsführung

Stefan ist Co-Geschäftsführer und zuständig für die Qualitätssicherung bei undpaul. Er beherrscht ver­schiedenste Programmiersprachen und hat ein Auge für das User Interface. Er ist Maintainer diverser Module, Mitarbeiter am Drupal Core und Mitglied der ersten Stunde der Drupal User Group Hannover. Acquia Certified Developer seit Juli 2014. Entgegen aller gängigen Vorurteile über Programmierer, zieht er Bewegung an der frischen Luft dem Sofa vor.