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 Tweet von drubb, 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.
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:
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 ...