Wenn es um den Import von Inhalten in eine Drupal-Webseite geht, gibt es verschiedene Möglichkeiten. Eine davon ist die Migration mit Hilfe von csv-Dateien. Diese kann, wenn richtig angewendet, die Erstellung von Inhalten erheblich erleichtern. Das Migrieren von tausenden von Seiten kann mit der nötigen Vorarbeit auf Knopfdruck stattfinden. Doch nicht nur die Migration, auch das Aktualisieren dieser Seiten kann mit dem Management von csv-Dateien vereinfacht werden.
Innerhalb dieses Artikels wird zuerst anhand eines einfachen Beispiels eine Migration über drush behandelt. Anschließend werden wir uns mit den Möglichkeiten beschäftigen, eine Migration ohne drush durchzuführen, und ihren Ablauf dynamisch zu verändern.
Vorbereitung
Bevor wir mit der eigentlichen Migration starten, müssen zuerst einige Vorbereitungen getroffen werden:
-
Die Module
migrate, migrate_tools, migrate_plus und migrate_source_csv
müssen eingeschaltet sein.
-
Zudem sollte sichergestellt werden, dass die aktuellste Version von drush installiert ist.
Nachdem dies passiert ist, können wir uns der Erstellung einer einfachen csv-Datei zuwenden.
Die csv-Datei
Eine csv-Datei gleicht einer Tabelle, in der jeder Spalte ein Wert in den Zeilen darunter zugewiesen wird. Die erste Zeile dient meistens der Benennung der Spalten. Diese Spalten stellen Werte dar die einer beliebigen Entität zugewiesen werden können. Man stelle sich z. B. einen Inhaltstyp “people” vor, für den Felder definiert sind, deren Werte durch die in der csv-Datei angegebenen Spalten befüllt werden können.
Unsere Beispiel csv-Datei "people":
id,first_name,last_name,age,date_of_birth,married,hair_color,picture
1,Paul,Watson,24,1993-01-08,0,brown,paul.png
2,John,Miller,36,1980-06-24,1,black,john.png
Als formatierte Tabelle würde dies so aussehen:
id |
first_name |
last_name |
age |
date_of_birth |
married |
hair_color |
picture |
1 |
Paul |
Watson |
24 |
1993-01-08 |
0 |
brown |
picture.png |
2 |
John |
Miller |
36 |
1980-06-24 |
1 |
black |
picture.png |
Für jede Zeile der csv-Datei die nicht die Überschriftszeile ist, kann jetzt ein solcher Inhalt erstellt werden.
Die yml-Datei
Um die einzelnen Werte der csv-Datei den Feldern des Inhaltstyps zuzuordnen, benötigt man neben der csv-Datei noch eine zusätzliche Konfigurationsdatei. Die Datei für unser “people”-Beispiel könnte folgendermaßen benannt werden:
“migrate_plus.migration.migrate_node_people”
“migrate_node_people” kann hierbei durch jede beliebige Bezeichnung ersetzt werden. Wichtig ist lediglich das “migrate_plus.migration” davor, so dass die Konfiguration diese Datei als Migration erkennt.
Im Folgenden wird der Aufbau einer solchen Datei Schritt für Schritt erklärt.
id: people_csv_import
label: 'Example csv migration'
source:
plugin: csv
path: 'public://csv_file.csv'
header_row_count: 1
keys:
- id
Die ersten zwei Zeilen
id: people_csv_import
label: ‘Example csv migration’
geben einer Migration eine ID und ein Label, um sie einfacher zu identifizieren. In den nächsten Zeilen
source:
plugin: csv
path: 'public://csv_file.csv'
folgt der “source”-Abschnitt. Hier geben wir an, eine csv-Datei innerhalb des public-Verzeichnisses als Quelle für die Migration verwenden zu wollen. Anschließend folgt der Hinweis, dass eine Zeile übersprungen werden muss, da sie die Überschriften der einzelnen Spalten darstellt.
header_row_count: 1
“Keys” sind die Schlüssel, durch die die einzelnen Zeilen voneinander unterschieden werden. In unserem Fall ist dies jedoch nur einer, die id.
keys:
- id
Innerhalb des “process”-Abschnittes
process:
field_complete_name:
plugin: concat
source:
- first_name
- last_name
delimiter: _
field_age: age
field_date_of_birth: date_of_birth
field_married: married
field_hair_color:
plugin: entity_generate
source: hair_color
field_picture:
plugin: entity_generate
source: picture
default_values:
uri: 'public://picture.png'
werden jetzt beispielhaft die Felder des Inhaltstyps “people” den Werten der csv-Datei zugeordnet.
“field_complete_name”
ist ein Textfeld, soll jedoch den kompletten Namen der Person beinhalten. Um dies zu erreichen wird auf das Plugin “concat” zurückgegriffen. Dieses Plugin konkateniert zwei Strings aneinander. Durch die optionale Angabe eines Trennzeichens kann die Konkatenation beeinflusst werden. Die Ausgabe für die erste Zeile der csv-Datei wäre hier also
“Paul_Watson”
Eine vollständige Liste aller Plugins befindet sich hier: https://www.drupal.org/node/2129651.
Das nächste Feld
field_age: age
beinhaltet das Alter der Person als Nummer. Da weiter oben bereits angegeben wurde wo sich die csv-Datei befindet, und wir nichts an dieser Angabe verändern wollen, kann der Name der Spalte direkt zugewiesen werden.
field_date_of_birth: date_of_birth
ist ein Datumsfeld. Auch dieser Wert kann direkt zugewiesen werden. Zu beachten ist lediglich das richtige Format des Datums in der csv-Datei. Jahr-Monat-Tag (z.B. 1999-06-24) für ein Datumsfeld welches lediglich das Datum beinhaltet, und Jahr-Monat-TagTStunde:Minute:Sekunde (z.B. 1999-06-24T12:04:13) für ein Datumsfeld welches das Datum und die Zeit beinhaltet. Eine Möglichkeit ein selbst gewähltes Format anzugeben und trotzdem ein korrektes Datum zu erhalten wird später behandelt.
field_married: married
ist ein Boolean. Hier gilt es lediglich zu beachten, dass in der csv-Datei eine 0 (false) oder eine 1 (true) angegeben wird.
In unserem Beispiel gibt es eine Taxonomie
“hair_color”
welche in dem Feld
“field_hair_color”
referenziert wird. Hier machen wir Gebrauch von “entity_generate”, ein weiteres Plugin, welches ausgehend von dem Namen der angegebenen Quelle nach einem Term der referenzierten Taxonomie sucht. Sollte ein Term mit dem angegebenen Namen noch nicht existieren, wird er erstellt, und dem Feld und der Taxonomie hinzugefügt.
“field_picture”
ist eine Referenz auf ein Bild. Auch hier nutzen wir “entity_generate”, um eine möglicherweise bereits existierende Datei zu verwenden anstatt eine neue zu erstellen. Zusätzlich wird “default_values” verwendet, welches eine Möglichkeit bietet, weitere Felder einer Entität mit Werten zu befüllen, falls sie noch nicht existieren sollte. Hier geben wir den Pfad zu der Datei an, die wir als Bild verwenden wollen.
Wichtig ist hierbei, dass auch Felder, die nicht manuell für eine Entität angelegt worden sind (z. B. “langcode”, “status”, etc.), mit Werten befüllt werden können.
Als nächstes folgt die Information welcher Typ von Inhalt erstellt werden soll:
type:
plugin: default_value
default_value: people
Zum Schluss bleibt im “destination”-Abschnitt nur noch die Art des Inhaltes anzugeben. In unserem Fall wird also eine “node” des Typs “people” erstellt.
destination:
plugin: 'entity:node'
Im Folgenden noch einmal die Konfigurationsdatei als ganzes:
id: people_csv_import
label: 'Example csv migration'
source:
plugin: csv
path: 'public://csv_file.csv'
header_row_count: 1
keys:
- id
process:
field_complete_name:
plugin: concat
source:
- first_name
- last_name
delimiter: _
field_age: age
field_date_of_birth: date_of_birth
field_married: married
field_hair_color:
plugin: entity_generate
source: hair_color
field_picture:
plugin: entity_generate
source: picture
default_values:
uri: 'public://picture.png'
type:
plugin: default_value
default_value: people
destination:
plugin: 'entity:node'
Um die Konfigurationsdatei als Migration nutzen zu können, muss sie zuerst in die aktuelle Konfiguration importiert werden. Dieser Vorgang kann über die Oberfläche unter Administration > Configuration > Development > Synchronize oder
admin/config/development/configuration/single/import
durchgeführt werden.
Importieren der Migration
Durch das migrate_tools Modul sind mehrere drush-Befehle hinzugekommen, die es ermöglichen, die gerade erstellte Migration zu importieren. Durch den Befehl
“drush migrate-status” (“drush ms”)
lässt sich eine Übersicht aller zur Zeit existierenden Migrationen aufrufen.
Durch das Ausführen von
“drush migrate-import people_csv_import” (“drush mi people_csv_import”)
wird durch die Angabe der id eine bestimmte Migration ausgeführt. In unserem Beispiel wurden also zwei “nodes” des Typs “people” erstellt. Weitere nützliche drush-Befehle:
-
“drush migrate-rollback” (“drush mr”)
Dieser Befehl macht eine bereits importierte Migration wieder rückgängig.
-
“drush migrate-stop” (“drush mst”)
Stoppt eine laufende Migration.
-
“drush migrate-reset-status” (“drush mrs”)
Setzt den Status einer Migration wieder auf “idle”. Nur Migrationen, die den Status “idle” haben, können auch importiert werden.
-
“drush migrate-messages” (“drush mmsg”)
Zeigt alle Nachrichten (z. B. Fehlermeldungen) einer Migration an.
Wie bereits erwähnt, gibt es auch die Möglichkeit, bereits importierte Migrationen zu aktualisieren. Wurden Werte in der csv-Datei geändert, kann ein Update mit dem Befehl
“drush mi migration_id --update”
durchgeführt werden.
Sonstiges
id: people_csv_import
migration tags:
- example
- import
migration_group:
- csv
label: 'Example csv migration'
Innerhalb der Konfigurationsdatei ist es möglich, Tags und Gruppen zu definieren. Die gerade aufgelisteten Befehle können nicht nur auf einzelne Migrationen, sondern auch auf verschiedene Tags und Gruppen angewandt werden, um Zeit zu sparen.
Nicht jeder Wert der csv-Datei muss verwendet werden und umgekehrt muss auch nicht jedes Feld der erstellten Entität einen Wert erhalten. Selbst Felder, die “required” sind, müssen nicht unbedingt einen Wert erhalten. Die entsprechenden Entitäten werden trotzdem erstellt. Dies kann zu einem Problem werden, wenn die Entitäten manuell editiert und wieder gespeichert werden. Die Validierung des Formulars wird daraufhin fehlschlagen, da ein benötigtes Feld noch keinen Wert erhalten hat. Es empfiehlt sich also, Feldern, die “required” sind, auf jeden Fall einen Wert zuzuweisen.
Custom Plugins
Durch das Modul “migrate” haben wir die Möglichkeit, eigene Plugins zu schreiben um die Daten der csv-Datei in einer bestimmten Art und Weise zu verändern. Diese Plugins können genauso wie die bereits erwähnten verwendet werden. Im folgenden das Beispiel für ein Plugin welches einen Timestamp in das korrekte Format für ein Datumsfeld formatiert.
<?php
namespace Drupal\csv_import\Plugin\migrate\process;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Formats a timestamp so it can be used in a migration.
*
* @MigrateProcessPlugin(
* id = "format_timestamp",
* )
*/
class FormatTimestampsExample extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface
$migrate_executable, Row $row, $destination_property) {
$date_formatter = \Drupal:service('date.formatter');
$current_date = $date_formatter->format($value, 'custm', 'Y-m-dTH:i:s');
return $current_date;
}
}
Diese Klasse wurde in einem Beispielmodul unter
example_module/src/Plugin/migrate/process/FormatTimestamp
eingefügt. Dies liegt daran, dass es sich hierbei um ein “process”-Plugin handelt. Ein Plugin für “destination” würde also unter
example_module/src/Plugin/migrate/destination/DestinationPluginClass
hinzugefügt werden.
Die id, die unterhalb der Anmerkung “@MigrationProcessPlugin” steht, ist der Name, der später innerhalb der Konfigurationsdatei verwendet werden kann, um dieses Plugin aufzurufen.
Innerhalb der “transform”-Funktion wird der Timestamp der csv-Datei in ein valides Datum formatiert. Diese Funktion schaltet sich also durch das Aufrufen zwischen den Wert der csv-Datei (“value”) und den Wert, der schließlich dem entsprechenden Feld zugewiesen wird (der “return”-Wert). Zusätzlich zu dem Wert der csv-Datei werden weitere Parameter, wie die aktuelle Zeile mitgegeben, so dass die Möglichkeit besteht, Werte basierend auf anderen Informationen zu verändern.
Steht in der csv-Datei an der Stelle, an der vorher noch ein valides Datum stand, jetzt ein Timestamp, so kann das obige Beispiel folgendermaßen verwendet werden:
field_date_of_birth:
plugin: format_timetamp
source: date_of_birth
Migrationen ohne drush
Das bisher Aufgezeigte bietet bereits viele Möglichkeiten, Inhalte in eine Drupal-Webseite zu migrieren. Wenn man jedoch etwas mehr Kontrolle über den Verlauf der Migration haben möchte, lassen sich alle beschriebenen Befehle auch ohne drush innerhalb des Codes ausführen.
// Get plugin manager for migration configs.
$manager = \Drupal::service('plugin.manager.config_entity_migration');
// Get all migrations.
$migrations = $manager->createInstances([]);
// Select people_migration with id of yml file.
$people_migration = $migrations['people_csv_import'];
// Start migration.
$executable = new MigrateExecutable($people_migration,
new MigrateMessage());
$executale->import();
Durch den Plugin-Manager für Migration werden für den Aufruf von “createInstances([])” mit einem leeren Array alle zur Zeit in der Konfiguration befindlichen Migrationen zurückgegeben. Die “import()”-Funktion am Ende ist die gleiche Funktion, die auch “drush mi” aufruft. Dieses Stück Code hat, an der richtigen Stelle eingesetzt, also den gleichen Effekt wie das Ausführen über drush. Ein Update dieser Migration lässt sich mit folgendem Zusatz ausführen:
// Update this migration on next import.
$people_migration->getIdMap()->prepareUpdate();
Dies registriert die Migration dafür beim nächsten Importieren ein Update durchzuführen. Ein wiederholtes Importieren einer Migration ohne diesen Zusatz wird abgebrochen und nicht durchgeführt.
Wer die entsprechende “yml”-Datei der Migration nicht über die Oberfläche importieren will, kann in einem Modul unter
example_module/config/install
diese Datei hinterlegen. Beim Installieren des Moduls wird die Datei dann automatisch der Konfiguration hinzugefügt.
Zu beachten ist hierbei, dass beim Deinstallieren des Moduls die Datei nicht aus der Konfiguration gelöscht wird. Dies führt zu einem Fehler bei einer erneuten Installation, da die zu installierende Datei bereits vorhanden ist. Wichtig ist also folgender Zusatz innerhalb einer “csv_example.install”, der beim Deinstallieren des Moduls die Datei aus der Konfiguration löscht um Fehlermeldungen zu vermeiden:
function migrate:menu_uninstall() {
\Drupal::configFactory()->getEditable
('migrate_plus.migration.migrate_node_people')->delete();
}
Migration Events
Das “migrate”-Modul registriert eine Reihe von Events, die während des Durchlaufs der “import()”-Funktion von einem “EventSubscriber” genutzt werden können, um den Import einer Migration gezielt zu beeinflussen.
Ein solcher “EventSubscriber” kann durch die Einbindung einer “services.yml” genutzt werden.
services:
example_module.event_subscriber:
class: Drupal\example _module\EventSubscriber\MigrationEventSubscriber
tags:
- { name: event_subscriber }
Unter dem angegebenen Pfad kann jetzt ein “EventSubscriber” erstellt werden.
<?php
namespace Drupal\example_module\EventSubscriber;
use ...
/**
* Event Subscriber to handle migration events.
*
* @package Drupal\example_module\EventSubscriber
*/
class MigrationEventSubscriber implements EventSubscriberInterface {
/**
* Event which fires befor the start of the import.
*
* @param \Drupal\migrate\Event\MigrateImportEvent $event
* Current MigrateImportEvent object.
*/
public function migratePreImport(MigrateImportEvent $event) {
$migration = $event->getMigration();
}
/**
* Event which fires before the Row is going to be saved.
*
* @param \Drupal\migrate\Event\MigratePreRowSaveEvent $event
* Current MigrateImportEvent object.
*/
public function migratePreRowSave(MigratePreRowSaveEvent $event) {
$migration = $event->getMigration();
$row = $event->getRow();
}
/**
* Event which fires after the Row has been saved.
*
* @param \Drupal\migrate\Event\MigratePostRowSaveEvent $event
* Current MigrateImportEvent object.
*/
public function migratePostRowSave(MigratePostRowSaveEvent $event) {
$migration = $event->getMigration();
$row = $event->getRow();
}
/**
* Event which fires after the import has been executed.
*
* @param \Drupal\migrate\Event\MigrateImportEvent $event
* Current MigrateImportEvent object.
*/
public function migratePostImport(MigrateImportEvent $event) {
$migration = $event->getMigration();
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[MigrateEvents::PRE_IMPORT][] = ['migratePreImport'];
$events[MigrateEvents::PRE_ROW_SAVE][] = ['migratePreRowSave'];
$events[MigrateEvents::POST_ROW_SAVE][] = ['migratePostRowSave'];
$events[MigrateEvents::POST_IMPORT][] = ['migratePostImport'];
return $events;
}
}
Innerhalb von “getSubscribedEvents()” können für die verschiedenen “MigrationEvents” verschiedene Funktionen definiert werden, die für das jeweilige Event aufgerufen werden sollen. Innerhalb des Events befindet sich die Migration und teilweise sogar das Objekt der aktuellen Zeile der csv-Datei. Diese Objekte können hier nach Belieben geändert werden.
Fazit
Migrationen sind - richtig eingesetzt - ein mächtiges Werkzeug, welches mit etwas Vorarbeit das Erstellen und Aktualisieren von riesigen Datenmengen vereinfachen kann. Ich hoffe, dass diese Anleitung ein paar Fragen zum Thema Migration beantworten konnte, und jedem das Erstellen und Importieren von Migrationen in Zukunft erleichtert.