clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

View File

@@ -0,0 +1,111 @@
INTRODUCTION
------------
The migrate_example module demonstrates how to implement custom migrations
for Drupal 8+. It includes a group of "beer" migrations demonstrating a complete
simple migration scenario.
THE BEER SITE
-------------
In this scenario, we have a beer aficionado site which stores its data in MySQL
tables - there are content items for each beer on the site, user accounts with
profile data, categories to classify the beers, and user-generated comments on
the beers. We want to convert this site to Drupal with just a few modifications
to the basic structure.
To make the example as simple as to run as possible, the source data is placed
in tables directly in your Drupal database - in most real-world scenarios, your
source data will be in an external database. The migrate_example_setup submodule
creates and populates these tables, as well as configuring your Drupal 8+ site
(creating a node type, vocabulary, fields, etc.) to receive the data.
STRUCTURE
---------
There are two primary components to this example:
1. Migration configuration, in the migrations and config/install directories.
These YAML files describe the migration process and provide the mappings from
the source data to Drupal's destination entities. The difference between the
two possible directories:
a. Files in the migrations directory provide configuration directly for the
migration plugins. The filenames are of the form <migration ID>.yml. This
approach is recommended when your migration configuration is fully hardcoded
and does not need to be overridden (e.g., you don't need to change the URL to
a source web service through an admin UI). While developing migrations,
changes to these files require at most a 'drush cr' to load your changes.
b. Files in the config/install directory provide migration configuration as
configuration entities, and have names of the form
migrate_plus.migration.<migration ID>.yml ("migration" because they define
entities of the "migration" type, and "migrate_plus" because that is the
module which implements the "migration" type). Migrations defined in this way
may have their configuration modified (in particular, through a web UI) by
loading the configuration entity, modifying its configuration, and saving the
entity. When developing, to get edits to the .yml files in config/install to
take effect in active configuration, use the config_devel module.
Configuration in either type of file is identical - the only differences are
the directories and filenames.
2. Source plugins, in src/Plugin/migrate/source. These are referenced from the
configuration files, and provide the source data to the migration processing
pipeline, as well as manipulating that data where necessary to put it into
a canonical form for migrations.
UNDERSTANDING THE MIGRATIONS
----------------------------
The YAML and PHP files are copiously documented in-line. To best understand
the concepts described in a more-or-less narrative form, it is recommended you
read the files in the following order:
1. migrate_plus.migration_group.beer.yml
2. migrate_plus.migration.beer_term.yml
3. BeerTerm.php
4. migrate_plus.migration.beer_user.yml
5. BeerUser.php
6. migrate_plus.migration.beer_node.yml
7. BeerNode.php
8. beer_comment.yml
9. BeerComment.php
RUNNING THE MIGRATIONS
----------------------
The migrate_tools module (https://www.drupal.org/project/migrate_tools) provides
the tools you need to perform migration processes. At this time, the web UI only
provides status information - to perform migration operations, you need to use
the drush commands.
# Enable the tools and the example module if you haven't already.
drush en -y migrate_tools,migrate_example
# Look at the migrations. Just look at them. Notice that they are displayed in
# the order they will be run, which reflects their dependencies. For example,
# because the node migration references the imported terms and users, it must
# run after those migrations have been run.
drush ms # Abbreviation for migrate-status
# Run the import operation for all the beer migrations.
drush mi --group=beer # Abbreviation for migrate-import
# Look at what you've done! Also, visit the site and see the imported content,
# user accounts, etc.
drush ms
# Look at the duplicate username message.
drush mmsg beer_user # Abbreviation for migrate-messages
# Run the rollback operation for all the migrations (removing all the imported
# content, user accounts, etc.). Note that it will rollback the migrations in
# the opposite order as they were imported.
drush mr --group=beer # Abbreviation for migrate-rollback
# You can import specific migrations.
drush mi beer_term,beer_user
# At this point, go look at your content listing - you'll see beer nodes named
# "Stub", generated from the user's favbeers references.
drush mi beer_node,beer_comment
# Refresh your content listing - the stub nodes have been filled with real beer!
# You can rollback specific migrations.
drush mr beer_comment,beer_node

View File

@@ -0,0 +1,55 @@
# Migration configuration for beer content.
id: beer_node
label: Beers of the world
migration_group: beer
migration_tags:
- example
source:
plugin: beer_node
destination:
plugin: entity:node
process:
# Hardcode the destination node type (bundle) as 'migrate_example_beer'.
type:
plugin: default_value
default_value: migrate_example_beer
title: name
nid: bid
uid:
plugin: migration_lookup
migration: beer_user
source: aid
sticky:
plugin: default_value
default_value: 0
field_migrate_example_country: countries
field_migrate_example_beer_style:
plugin: migration_lookup
migration: beer_term
source: terms
# Some Drupal fields may have multiple components we may want to set
# separately. For example, text fields may have summaries (teasers) in
# addition to the full text value. We use / to separate the field name from
# the internal field value being set, and put it in quotes because / is a
# YAML special character.
'body/value': body
'body/summary': excerpt
# Our beer nodes have references to terms and users, so we want those to be
# imported first. We make that dependency explicit here - by putting those
# migrations under the 'required' key, we ensure that the tools will prevent
# us from running the beer_node migration unless the beer_term and beer_user
# migrations are complete (although we can override the dependency check by
# passing --force to the drush migrate-import command). We can also add
# 'optional' dependencies - these affect the order in which migrations are
# displayed, and run by default, but does not force you run them in that
# order.
# The general rule of thumb is that any migrations referenced by migration
# process plugins should be required here.
migration_dependencies:
required:
- beer_term
- beer_user
dependencies:
enforced:
module:
- migrate_example

View File

@@ -0,0 +1,91 @@
# A "migration" is, in technical terms, a plugin whose configuration describes
# how to read source data, process it (generally by mapping source fields to
# destination fields), and write it to Drupal.
# The machine name for a migration, used to uniquely identify it.
id: beer_term
# A human-friendly description of the migration.
label: Migrate style categories from the source database to taxonomy terms
# The machine name of the group containing this migration (which contains shared
# configuration to be merged with our own configuration here).
migration_group: beer
# The category or tag for the migration.
migration_tags:
- example
# Every migration must have a source plugin, which controls the delivery of our
# source data. In this case, our source plugin has the name "beer_term", which
# Drupal resolves to the PHP class defined in
# src/Plugin/migrate/source/BeerTerm.php.
source:
plugin: beer_term
# Every migration must also have a destination plugin, which handles writing
# the migrated data in the appropriate form for that particular kind of data.
# Most Drupal content is an "entity" of one type or another, and we need to
# specify what entity type we are populating (in this case, taxonomy terms).
# Unlike the source plugin (which is specific to our particular scenario), this
# destination plugin is implemented in Drupal itself.
destination:
plugin: entity:taxonomy_term
# Here's the meat of the migration - the processing pipeline. This describes how
# each destination field is to be populated based on the source data. For each
# destination field, one or more process plugins may be invoked.
process:
# The simplest process plugin is named 'get' - it is the default plugin, so
# does not need to be explicitly named. It simply copies the source value
# (the 'style' field from the source database in this case) to the destination
# field (the taxonomy term 'name' field). You can see we simply copy the
# source 'details' field to destination 'description' field in the same way.
name: style
description: details
# Here is a new plugin - default_value. In its simplest usage here, it is used
# to hard-code a destination value, the vid (vocabulary ID) our taxonomy terms
# should be assigned to. It's important to note that while above the right
# side of the mappings was a source field name, here the right side of the
# 'default_value:' line is an actual value.
vid:
plugin: default_value
default_value: migrate_example_beer_styles
# Here's another new plugin - migration. When importing data from another
# system, typically the unique identifiers for items on the destination side
# are not the same as the identifiers were on the source side. For example, in
# our style data the term names are the unique identifiers for each term,
# while in Drupal each term is assigned a unique integer term ID (tid). When
# any such items are referenced in Drupal, the reference needs to be
# translated from the old ID ('ale') to the new ID (1). The migration
# framework keeps track of the relationships between source and destination
# IDs in map tables, and the migration plugin is the means of performing a
# lookup in those map tables during processing.
parent:
plugin: migration_lookup
# Here we reference the migration whose map table we're performing a lookup
# against. You'll note that in this case we're actually referencing this
# migration itself, since category parents are imported by the same
# migration. This works best when we're sure the parents are imported
# before the children, and in this case our source plugin is guaranteeing
# that.
migration: beer_term
# 'style_parent' is the parent reference field from the source data. The
# result of this plugin is that the destination 'parent' field is populated
# with the Drupal term ID of the referenced style (or NULL if style_parent
# was empty).
source: style_parent
# We'll learn more about dependencies in beer_node - here, we leave them empty.
migration_dependencies: {}
# By default, configuration entities (like this migration) are not automatically
# removed when the migration which installed them is uninstalled. To have your
# migrations uninstalled with your migration module, add an enforced dependency
# on your module.
dependencies:
enforced:
module:
- migrate_example

View File

@@ -0,0 +1,107 @@
# Migration configuration for user accounts. We've described most of what goes
# into migration configuration in migrate_plus.migration.beer_term.yml, so won't
# repeat that here.
id: beer_user
label: Beer Drinkers of the world
migration_group: beer
migration_tags:
- example
source:
plugin: beer_user
destination:
plugin: entity:user
process:
pass: password
mail: email
init: email
status: status
roles:
plugin: default_value
default_value: 2
# Here's a new process plugin - make_unique_entity_field. Our source site allowed there
# to be multiple user accounts with the same username, but Drupal wants
# usernames to be unique. This plugin allows us to automatically generate
# unique usernames when we detect collisions.
name:
plugin: make_unique_entity_field
# The name of the source field containing the username.
source: username
# These next two settings identify the destination-side field to check for
# duplicates. They say "see if the incoming 'name' matches any existing
# 'name' field in any 'user' entity".
entity_type: user
field: name
# Finally, this specifies a string to use between the original value and the
# sequence number appended to make the value unique. Thus, the first 'alice'
# account gets the name 'alice' in Drupal, and the second one gets the name
# 'alice_1'.
postfix: _
# Another new process plugin - callback. This allows us to filter an incoming
# source value through an arbitrary PHP function. The function called must
# have one required argument.
created:
plugin: callback
# The 'registered' timestamp in the source data is a string of the form
# 'yyyy-mm-dd hh:mm:ss', but Drupal wants a UNIX timestamp for 'created'.
source: registered
callable: strtotime
# Our source data only has a single timestamp value, 'registered', which we
# want to use for all four of Drupal's user timestamp fields. We could
# duplicate the callback plugin we used for 'created' above - but we have a
# shortcut. Putting an @ sign at the beginning of the source value indicates
# that it is to be interpreted as a *destination* field name instead of a
# *source* field name. Thus, if a value we need in more than one place
# requires some processing beyond simply copying it directly, we can perform
# that processing a single time and use the result in multiple places.
changed: '@created'
access: '@created'
login: '@created'
# Yet another new process plugin - static_map. We're making a transformation
# in how we represent gender data - formerly it was integer values 0 for male
# and 1 for female, but in our modern Drupal site we will be making this a
# free-form text field, so we want to replace the obscure integers with
# simple strings.
field_migrate_example_gender:
plugin: static_map
# Specify the source field we're reading (containing 0's and 1's).
source: sex
# Tell it to transform 0 to 'Male', and 1 to 'Female'.
map:
0: Male
1: Female
# If the input is missing, leave the field empty. Without this, an empty
# or invalid source value would cause the user record to be skipped
# entirely.
bypass: true
# This looks like a simple migration process plugin, but there's magic
# happening here. We import nodes after terms and users, because they have
# references to terms and users, so of course the terms and users must be
# migrated first - right? However, the favbeers field is a reference to the
# beer nodes which haven't yet been migrated - we have a circular relationship
# between users and nodes. The way the migration system resolves this
# situation is by creating "stubs". In this case, because no beer nodes have
# been created, each time a beer is looked up against the beer_node migration
# nothing is found, and by default the migration process plugin creates an
# empty stub node as a placeholder so the favbeers reference field has
# something to point to. The stub is recorded in the beer_node map table, so
# when that migration runs it knows that each incoming beer should overwrite
# its stub instead of creating a new node.
field_migrate_example_favbeers:
plugin: migration_lookup
source: beers
migration: beer_node
migration_dependencies: {}
# When a module is creating a custom content type it needs to add an
# enforced dependency to itself, otherwise the content type will persist
# after the module is disabled. See: https://www.drupal.org/node/2629516.
dependencies:
enforced:
module:
- migrate_example

View File

@@ -0,0 +1,38 @@
# A "migration group" is - surprise! - a group of migrations. It is used to
# group migrations for display by our tools, and to perform operations on a
# specific set of migrations. It can also be used to hold any configuration
# common to those migrations, so it doesn't have to be duplicated in each one.
# The machine name of the group, by which it is referenced in individual
# migrations.
id: beer
# A human-friendly label for the group.
label: Beer Imports
# More information about the group.
description: A few simple beer-related imports, to demonstrate how to implement migrations.
# Short description of the type of source, e.g. "Drupal 6" or "WordPress".
source_type: Custom tables
# Here we add any default configuration settings to be shared among all
# migrations in the group. For this example, the source tables are in the
# Drupal (default) database, but usually if your source data is in a
# database it will be external.
shared_configuration:
# Specifying 'source' here means that this configuration will be merged into
# the 'source' configuration of each migration.
source:
# A better practice for real-world migrations would be to add a database
# connection to your external database in settings.php and reference its
# key here.
key: default
# As with the migration configuration (see beer_term), we add an enforced
# dependency so the migration_group configuration will be removed on module
# uninstall.
dependencies:
enforced:
module:
- migrate_example

View File

@@ -0,0 +1,16 @@
type: module
name: Migrate Example
description: 'Examples of how Drupal 8+ migration compares to previous versions.'
package: Examples
core_version_requirement: '>=9.1'
dependencies:
- drupal:migrate
- migrate_plus:migrate_example_setup
- migrate_plus:migrate_plus
- drupal:menu_ui
- drupal:path
# Information added by Drupal.org packaging script on 2024-11-20
version: '6.0.5'
project: 'migrate_plus'
datestamp: 1732124626

View File

@@ -0,0 +1,10 @@
langcode: en
status: true
dependencies:
enforced:
module:
- migrate_example_setup
id: node_comments
label: 'Node comments'
target_entity_type_id: node
description: ''

View File

@@ -0,0 +1,30 @@
langcode: en
status: true
dependencies:
config:
- comment.type.node_comments
- field.field.comment.node_comments.comment_body
module:
- text
id: comment.node_comments.default
targetEntityType: comment
bundle: node_comments
mode: default
content:
author:
weight: -2
comment_body:
type: text_textarea
weight: 11
settings:
rows: 5
placeholder: ''
third_party_settings: { }
subject:
type: string_textfield
weight: 10
settings:
size: 60
placeholder: ''
third_party_settings: { }
hidden: { }

View File

@@ -0,0 +1,89 @@
langcode: en
status: true
dependencies:
config:
- field.field.node.migrate_example_beer.body
- field.field.node.migrate_example_beer.field_comments
- field.field.node.migrate_example_beer.field_migrate_example_beer_style
- field.field.node.migrate_example_beer.field_migrate_example_country
- field.field.node.migrate_example_beer.field_migrate_example_image
- node.type.migrate_example_beer
module:
- comment
- image
- text
id: node.migrate_example_beer.default
targetEntityType: node
bundle: migrate_example_beer
mode: default
content:
body:
type: text_textarea_with_summary
weight: 6
settings:
rows: 9
summary_rows: 3
placeholder: ''
third_party_settings: { }
created:
type: datetime_timestamp
weight: 2
settings: { }
third_party_settings: { }
field_comments:
weight: 10
settings: { }
third_party_settings: { }
type: comment_default
field_migrate_example_beer_style:
weight: 7
settings:
match_operator: CONTAINS
match_limit: 10
size: 60
placeholder: ''
third_party_settings: { }
type: entity_reference_autocomplete
field_migrate_example_country:
weight: 8
settings:
size: 60
placeholder: ''
third_party_settings: { }
type: string_textfield
field_migrate_example_image:
weight: 9
settings:
progress_indicator: throbber
preview_image_style: thumbnail
third_party_settings: { }
type: image_image
promote:
type: boolean_checkbox
settings:
display_label: true
weight: 3
third_party_settings: { }
sticky:
type: boolean_checkbox
settings:
display_label: true
weight: 4
third_party_settings: { }
title:
type: string_textfield
weight: 0
settings:
size: 60
placeholder: ''
third_party_settings: { }
uid:
type: entity_reference_autocomplete
weight: 1
settings:
match_operator: CONTAINS
match_limit: 10
size: 60
placeholder: ''
third_party_settings: { }
hidden: { }

View File

@@ -0,0 +1,22 @@
langcode: en
status: true
dependencies:
config:
- comment.type.node_comments
- field.field.comment.node_comments.comment_body
module:
- text
id: comment.node_comments.default
targetEntityType: comment
bundle: node_comments
mode: default
content:
comment_body:
label: hidden
type: text_default
weight: 0
settings: { }
third_party_settings: { }
links:
weight: 100
hidden: { }

View File

@@ -0,0 +1,60 @@
langcode: en
status: true
dependencies:
config:
- field.field.node.migrate_example_beer.body
- field.field.node.migrate_example_beer.field_comments
- field.field.node.migrate_example_beer.field_migrate_example_beer_style
- field.field.node.migrate_example_beer.field_migrate_example_country
- field.field.node.migrate_example_beer.field_migrate_example_image
- node.type.migrate_example_beer
module:
- comment
- image
- text
- user
id: node.migrate_example_beer.default
targetEntityType: node
bundle: migrate_example_beer
mode: default
content:
body:
label: hidden
type: text_default
weight: 2
settings: { }
third_party_settings: { }
field_comments:
weight: 5
label: above
settings:
pager_id: 0
third_party_settings: { }
type: comment_default
field_migrate_example_beer_style:
weight: 3
label: above
settings:
link: true
third_party_settings: { }
type: entity_reference_label
field_migrate_example_country:
weight: 4
label: above
settings:
link_to_entity: false
third_party_settings: { }
type: string
field_migrate_example_image:
weight: 1
label: above
settings:
image_style: ''
image_link: ''
third_party_settings: { }
type: image
links:
weight: 0
settings: { }
third_party_settings: { }
hidden: { }

View File

@@ -0,0 +1,35 @@
langcode: en
status: true
dependencies:
config:
- core.entity_view_mode.node.teaser
- field.field.node.migrate_example_beer.body
- field.field.node.migrate_example_beer.field_comments
- field.field.node.migrate_example_beer.field_migrate_example_beer_style
- field.field.node.migrate_example_beer.field_migrate_example_country
- field.field.node.migrate_example_beer.field_migrate_example_image
- node.type.migrate_example_beer
module:
- text
- user
id: node.migrate_example_beer.teaser
targetEntityType: node
bundle: migrate_example_beer
mode: teaser
content:
body:
label: hidden
type: text_summary_or_trimmed
weight: 1
settings:
trim_length: 600
third_party_settings: { }
links:
weight: 0
settings: { }
third_party_settings: { }
hidden:
field_comments: true
field_migrate_example_beer_style: true
field_migrate_example_country: true
field_migrate_example_image: true

View File

@@ -0,0 +1,20 @@
langcode: en
status: true
dependencies:
config:
- comment.type.node_comments
- field.storage.comment.comment_body
module:
- text
id: comment.node_comments.comment_body
field_name: comment_body
entity_type: comment
bundle: node_comments
label: Comment
description: ''
required: true
translatable: true
default_value: { }
default_value_callback: ''
settings: { }
field_type: text_long

View File

@@ -0,0 +1,21 @@
langcode: en
status: true
dependencies:
config:
- field.storage.node.body
- node.type.migrate_example_beer
module:
- text
id: node.migrate_example_beer.body
field_name: body
entity_type: node
bundle: migrate_example_beer
label: Body
description: ''
required: false
translatable: true
default_value: { }
default_value_callback: ''
settings:
display_summary: true
field_type: text_with_summary

View File

@@ -0,0 +1,32 @@
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_comments
- node.type.migrate_example_beer
module:
- comment
id: node.migrate_example_beer.field_comments
field_name: field_comments
entity_type: node
bundle: migrate_example_beer
label: Comments
description: ''
required: false
translatable: false
default_value:
-
status: 2
cid: 0
last_comment_timestamp: 0
last_comment_name: null
last_comment_uid: 0
comment_count: 0
default_value_callback: ''
settings:
default_mode: 1
per_page: 50
anonymous: 0
form_location: true
preview: 1
field_type: comment

View File

@@ -0,0 +1,26 @@
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_migrate_example_beer_style
- node.type.migrate_example_beer
- taxonomy.vocabulary.migrate_example_beer_styles
id: node.migrate_example_beer.field_migrate_example_beer_style
field_name: field_migrate_example_beer_style
entity_type: node
bundle: migrate_example_beer
label: 'Migrate Example Beer Styles'
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings:
handler: 'default:taxonomy_term'
handler_settings:
target_bundles:
migrate_example_beer_styles: migrate_example_beer_styles
sort:
field: _none
auto_create: false
field_type: entity_reference

View File

@@ -0,0 +1,18 @@
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_migrate_example_country
- node.type.migrate_example_beer
id: node.migrate_example_beer.field_migrate_example_country
field_name: field_migrate_example_country
entity_type: node
bundle: migrate_example_beer
label: Countries
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: string

View File

@@ -0,0 +1,37 @@
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_migrate_example_image
- node.type.migrate_example_beer
module:
- image
id: node.migrate_example_beer.field_migrate_example_image
field_name: field_migrate_example_image
entity_type: node
bundle: migrate_example_beer
label: Image
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings:
file_directory: ''
file_extensions: 'png gif jpg jpeg'
max_filesize: ''
max_resolution: ''
min_resolution: ''
alt_field: true
alt_field_required: false
title_field: false
title_field_required: false
default_image:
uuid: ''
alt: ''
title: ''
width: null
height: null
handler: 'default:file'
handler_settings: { }
field_type: image

View File

@@ -0,0 +1,27 @@
langcode: en
status: true
dependencies:
config:
- field.storage.user.field_migrate_example_favbeers
- node.type.migrate_example_beer
module:
- user
id: user.user.field_migrate_example_favbeers
field_name: field_migrate_example_favbeers
entity_type: user
bundle: user
label: 'Favorite Beers'
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings:
handler: 'default:node'
handler_settings:
target_bundles:
migrate_example_beer: migrate_example_beer
sort:
field: title
direction: ASC
field_type: entity_reference

View File

@@ -0,0 +1,19 @@
langcode: en
status: true
dependencies:
config:
- field.storage.user.field_migrate_example_gender
module:
- user
id: user.user.field_migrate_example_gender
field_name: field_migrate_example_gender
entity_type: user
bundle: user
label: Gender
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: string

View File

@@ -0,0 +1,22 @@
langcode: en
status: true
dependencies:
enforced:
module:
- migrate_example_setup
module:
- comment
- node
id: node.field_comments
field_name: field_comments
entity_type: node
type: comment
settings:
comment_type: node_comments
module: comment
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,22 @@
langcode: en
status: true
dependencies:
enforced:
module:
- migrate_example_setup
module:
- node
- taxonomy
id: node.field_migrate_example_beer_style
field_name: field_migrate_example_beer_style
entity_type: node
type: entity_reference
settings:
target_type: taxonomy_term
module: core
locked: false
cardinality: -1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,23 @@
langcode: en
status: true
dependencies:
enforced:
module:
- migrate_example_setup
module:
- node
id: node.field_migrate_example_country
field_name: field_migrate_example_country
entity_type: node
type: string
settings:
max_length: 255
is_ascii: false
case_sensitive: false
module: core
locked: false
cardinality: -1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,32 @@
langcode: en
status: true
dependencies:
enforced:
module:
- migrate_example_setup
module:
- file
- image
- node
id: node.field_migrate_example_image
field_name: field_migrate_example_image
entity_type: node
type: image
settings:
uri_scheme: public
default_image:
uuid: ''
alt: ''
title: ''
width: null
height: null
target_type: file
display_field: false
display_default: false
module: image
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,22 @@
langcode: en
status: true
dependencies:
enforced:
module:
- migrate_example_setup
module:
- node
- user
id: user.field_migrate_example_favbeers
field_name: field_migrate_example_favbeers
entity_type: user
type: entity_reference
settings:
target_type: node
module: core
locked: false
cardinality: -1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,23 @@
langcode: en
status: true
dependencies:
enforced:
module:
- migrate_example_setup
module:
- user
id: user.field_migrate_example_gender
field_name: field_migrate_example_gender
entity_type: user
type: string
settings:
max_length: 255
is_ascii: false
case_sensitive: false
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,13 @@
langcode: en
status: true
dependencies:
enforced:
module:
- migrate_example_setup
name: Beer
type: migrate_example_beer
description: 'Beer is what we drink.'
help: ''
new_revision: false
preview_mode: 1
display_submitted: true

View File

@@ -0,0 +1,11 @@
langcode: en
status: true
dependencies:
enforced:
module:
- migrate_example_setup
name: 'Migrate Example Beer Styles'
vid: migrate_example_beer_styles
description: 'Use tags to group beers on similar topics into categories.'
hierarchy: 0
weight: 0

View File

@@ -0,0 +1,17 @@
type: module
name: Migrate Example Setup
description: 'Separate site configuration for the example from the actual migration.'
package: Migration
core_version_requirement: '>=9.1'
hidden: true
dependencies:
- drupal:comment
- drupal:image
- drupal:text
- drupal:options
- drupal:taxonomy
# Information added by Drupal.org packaging script on 2024-11-20
version: '6.0.5'
project: 'migrate_plus'
datestamp: 1732124626

View File

@@ -0,0 +1,502 @@
<?php
declare(strict_types = 1);
/**
* @file
* Install file for migrate example module.
*
* Set up source data and destination configuration for the migration example
* module. We do this in a separate module so migrate_example itself is a pure
* migration module.
*/
/**
* Implements hook_schema().
*/
function migrate_example_setup_schema(): array {
$schema = [];
$schema['migrate_example_beer_account'] = migrate_example_beer_schema_account();
$schema['migrate_example_beer_node'] = migrate_example_beer_schema_node();
$schema['migrate_example_beer_comment'] = migrate_example_beer_schema_comment();
$schema['migrate_example_beer_topic'] = migrate_example_beer_schema_topic();
$schema['migrate_example_beer_topic_node'] = migrate_example_beer_schema_topic_node();
return $schema;
}
/**
* Implements hook_install().
*/
function migrate_example_setup_install(): void {
// Populate our tables.
migrate_example_beer_data_account();
migrate_example_beer_data_node();
migrate_example_beer_data_comment();
migrate_example_beer_data_topic();
migrate_example_beer_data_topic_node();
}
/**
* The hook_schema definition for node.
*
* The schema definition.
*/
function migrate_example_beer_schema_node(): array {
return [
'description' => 'Beers of the world.',
'fields' => [
'bid' => [
'type' => 'serial',
'not null' => TRUE,
'description' => 'Beer ID.',
],
'name' => [
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
],
'body' => [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'description' => 'Full description of the beer.',
],
'excerpt' => [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'description' => 'Abstract for this beer.',
],
'countries' => [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'description' => 'Countries of origin. Multiple values, delimited by pipe',
],
'aid' => [
'type' => 'int',
'not null' => FALSE,
'description' => 'Account Id of the author.',
],
'image' => [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'description' => 'Image path',
],
'image_alt' => [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'description' => 'Image ALT',
],
'image_title' => [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'description' => 'Image title',
],
'image_description' => [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'description' => 'Image description',
],
],
'primary key' => ['bid'],
];
}
/**
* The hook_schema definition for topic.
*
* The schema definition.
*/
function migrate_example_beer_schema_topic(): array {
return [
'description' => 'Categories',
'fields' => [
'style' => [
'type' => 'varchar_ascii',
'length' => 255,
'not null' => TRUE,
],
'details' => [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
],
'style_parent' => [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'description' => 'Parent topic, if any',
],
'region' => [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'description' => 'Region first associated with this style',
],
'hoppiness' => [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'description' => 'Relative hoppiness of the beer',
],
],
'primary key' => ['style'],
];
}
/**
* The hook_schema definition for topic node.
*
* The schema definition.
*/
function migrate_example_beer_schema_topic_node(): array {
return [
'description' => 'Beers topic pairs.',
'fields' => [
'bid' => [
'type' => 'int',
'not null' => TRUE,
'description' => 'Beer ID.',
],
'style' => [
'type' => 'varchar_ascii',
'length' => 255,
'not null' => TRUE,
'description' => 'Topic name',
],
],
'primary key' => ['style', 'bid'],
];
}
/**
* The hook_schema definition for comment.
*
* The schema definition.
*/
function migrate_example_beer_schema_comment(): array {
return [
'description' => 'Beers comments.',
'fields' => [
'cid' => [
'type' => 'serial',
'not null' => TRUE,
'description' => 'Comment ID.',
],
'bid' => [
'type' => 'int',
'not null' => TRUE,
'description' => 'Beer ID that is being commented upon',
],
'cid_parent' => [
'type' => 'int',
'not null' => FALSE,
'description' => 'Parent comment ID in case of comment replies.',
],
'subject' => [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'description' => 'Comment subject',
],
'body' => [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'description' => 'Comment body',
],
'name' => [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'description' => 'Comment name (if anon)',
],
'mail' => [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'description' => 'Comment email (if anon)',
],
'aid' => [
'type' => 'int',
'not null' => FALSE,
'description' => 'Account ID (if any).',
],
],
'primary key' => ['cid'],
];
}
/**
* The hook_schema definition for account.
*
* The schema definition.
*/
function migrate_example_beer_schema_account(): array {
return [
'description' => 'Beers accounts.',
'fields' => [
'aid' => [
'type' => 'serial',
'not null' => TRUE,
'description' => 'Account ID',
],
'status' => [
'type' => 'int',
'not null' => TRUE,
'description' => 'Blocked_Allowed',
],
'registered' => [
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'description' => 'Registration date',
],
'username' => [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'description' => 'Account name (for login)',
],
'nickname' => [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'description' => 'Account name (for display)',
],
'password' => [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'description' => 'Account password (raw)',
],
'email' => [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'description' => 'Account email',
],
'sex' => [
'type' => 'int',
'not null' => FALSE,
'description' => 'Gender (0 for male, 1 for female)',
],
'beers' => [
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'description' => 'Favorite Beers',
],
],
'primary key' => ['aid'],
];
}
/**
* Populate node table.
*/
function migrate_example_beer_data_node(): void {
$fields = [
'bid',
'name',
'body',
'excerpt',
'countries',
'aid',
'image',
'image_alt',
'image_title',
'image_description',
];
$query = \Drupal::database()->insert('migrate_example_beer_node')
->fields($fields);
// Use high bid numbers to avoid overwriting an existing node id.
$data = [
// Comes with migrate_example project.
[
99999999,
'Heineken',
'Blab Blah Blah Green',
'Green',
'Netherlands|Belgium',
1,
'heineken.jpg',
'Heinekin alt',
'Heinekin title',
'Heinekin description',
],
[
99999998,
'Miller Lite',
'We love Miller Brewing',
'Tasteless',
'USA|Canada',
2,
NULL,
NULL,
NULL,
NULL,
],
[
99999997,
'Boddington',
'English occasionally get something right',
'A treat',
'United Kingdom',
2,
NULL,
NULL,
NULL,
NULL,
],
];
foreach ($data as $row) {
$query->values(array_combine($fields, $row));
}
$query->execute();
}
/**
* Populate account table.
*
* Note that alice has duplicate username. Exercises make_unique_entity_field
* plugin.
*
* @todo Duplicate email also.
*/
function migrate_example_beer_data_account(): void {
$fields = [
'status',
'registered',
'username',
'nickname',
'password',
'email',
'sex',
'beers',
];
$query = \Drupal::database()->insert('migrate_example_beer_account')
->fields($fields);
$data = [
[
1,
'2010-03-30 10:31:05',
'alice',
'alice in beerland',
'alicepass',
'alice@example.com',
'1',
'99999999|99999998|99999997',
],
[
1,
'2010-04-04 10:31:05',
'alice',
'alice in aleland',
'alicepass',
'alice2@example.com',
'1',
'99999999|99999998|99999997',
],
[
0,
'2007-03-15 10:31:05',
'bob',
'rebob',
'bobpass',
'bob@example.com',
'0',
'99999999|99999997',
],
[
1,
'2004-02-29 10:31:05',
'charlie',
'charlie chocolate',
'mykids',
'charlie@example.com',
'0',
'99999999|99999998',
],
];
foreach ($data as $row) {
$query->values(array_combine($fields, $row));
}
$query->execute();
}
/**
* Populate comment table.
*/
function migrate_example_beer_data_comment(): void {
$fields = ['bid', 'cid_parent', 'subject', 'body', 'name', 'mail', 'aid'];
$query = \Drupal::database()->insert('migrate_example_beer_comment')
->fields($fields);
$data = [
[99999998, NULL, 'im first', 'full body', 'alice', 'alice@example.com', 1],
[99999998, NULL, 'im second', 'aromatic', 'alice', 'alice@example.com', 1],
[99999999, NULL, 'im parent', 'malty', 'alice', 'alice@example.com', 1],
[99999999, 1, 'im child', 'cold body', 'bob', NULL, 2],
[
99999999,
4,
'im grandchild',
'bitter body',
'charlie@example.com',
NULL,
1,
],
];
foreach ($data as $row) {
$query->values(array_combine($fields, $row));
}
$query->execute();
}
/**
* Populate topic table.
*/
function migrate_example_beer_data_topic(): void {
$fields = ['style', 'details', 'style_parent', 'region', 'hoppiness'];
$query = \Drupal::database()->insert('migrate_example_beer_topic')
->fields($fields);
$data = [
['ale', 'traditional', NULL, 'Medieval British Isles', 'Medium'],
['red ale', 'colorful', 'ale', NULL, NULL],
[
'pilsner',
'refreshing',
NULL,
'Pilsen, Bohemia (now Czech Republic)',
'Low',
],
];
foreach ($data as $row) {
$query->values(array_combine($fields, $row));
}
$query->execute();
}
/**
* Populate topic node table.
*/
function migrate_example_beer_data_topic_node(): void {
$fields = ['bid', 'style'];
$query = \Drupal::database()->insert('migrate_example_beer_topic_node')
->fields($fields);
$data = [
[99999999, 'pilsner'],
[99999999, 'red ale'],
[99999998, 'red ale'],
];
foreach ($data as $row) {
$query->values(array_combine($fields, $row));
}
$query->execute();
}

View File

@@ -0,0 +1,45 @@
# Migration configuration for beer comments. No new concepts here.
id: beer_comment
label: Comments on beers
migration_group: beer
source:
plugin: beer_comment
destination:
plugin: entity:comment
process:
pid:
plugin: migration_lookup
migration: beer_comment
source: cid_parent
entity_id:
plugin: migration_lookup
migration: beer_node
source: bid
entity_type:
plugin: default_value
default_value: node
field_name:
plugin: default_value
default_value: field_comments
comment_type:
plugin: default_value
default_value: node_comments
subject: subject
uid:
plugin: migration_lookup
migration: beer_user
source: aid
name: name
mail: mail
status:
plugin: default_value
default_value: 1
'comment_body/value': body
migration_dependencies:
required:
- beer_node
- beer_user
dependencies:
enforced:
module:
- migrate_example

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_example\Plugin\migrate\source;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
/**
* Source plugin for beer comments.
*
* @MigrateSource(
* id = "beer_comment"
* )
*/
final class BeerComment extends SqlBase {
/**
* {@inheritdoc}
*/
public function query(): SelectInterface {
$fields = [
'cid',
'cid_parent',
'name',
'mail',
'aid',
'body',
'bid',
'subject',
];
return $this->select('migrate_example_beer_comment', 'mec')
->fields('mec', $fields)
->orderBy('cid_parent', 'ASC');
}
/**
* {@inheritdoc}
*/
public function fields(): array {
return [
'cid' => $this->t('Comment ID'),
'cid_parent' => $this->t('Parent comment ID in case of comment replies'),
'name' => $this->t('Comment name (if anon)'),
'mail' => $this->t('Comment email (if anon)'),
'aid' => $this->t('Account ID (if any)'),
'bid' => $this->t('Beer ID that is being commented upon'),
'subject' => $this->t('Comment subject'),
];
}
/**
* {@inheritdoc}
*/
public function getIds(): array {
return [
'cid' => [
'type' => 'integer',
'alias' => 'mec',
],
];
}
}

View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_example\Plugin\migrate\source;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
use Drupal\migrate\Row;
/**
* Source plugin for beer content.
*
* @MigrateSource(
* id = "beer_node"
* )
*/
final class BeerNode extends SqlBase {
/**
* {@inheritdoc}
*/
public function query(): SelectInterface {
// An important point to note is that your query *must* return a single row
// for each item to be imported. Here we might be tempted to add a join to
// migrate_example_beer_topic_node in our query, to pull in the
// relationships to our categories. Doing this would cause the query to
// return multiple rows for a given node, once per related value, thus
// processing the same node multiple times, each time with only one of the
// multiple values that should be imported. To avoid that, we simply query
// the base node data here, and pull in the relationships in prepareRow()
// below.
$fields = [
'bid',
'name',
'body',
'excerpt',
'aid',
'countries',
'image',
'image_alt',
'image_title',
'image_description',
];
return $this->select('migrate_example_beer_node', 'b')
->fields('b', $fields);
}
/**
* {@inheritdoc}
*/
public function fields(): array {
return [
'bid' => $this->t('Beer ID'),
'name' => $this->t('Name of beer'),
'body' => $this->t('Full description of the beer'),
'excerpt' => $this->t('Abstract for this beer'),
'aid' => $this->t('Account ID of the author'),
'countries' => $this->t('Countries of origin. Multiple values, delimited by pipe'),
'image' => $this->t('Image path'),
'image_alt' => $this->t('Image ALT'),
'image_title' => $this->t('Image title'),
'image_description' => $this->t('Image description'),
// Note that this field is not part of the query above - it is populated
// by prepareRow() below. You should document all source properties that
// are available for mapping after prepareRow() is called.
'terms' => $this->t('Applicable styles'),
];
}
/**
* {@inheritdoc}
*/
public function getIds(): array {
return [
'bid' => [
'type' => 'integer',
'alias' => 'b',
],
];
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row): bool {
// As explained above, we need to pull the style relationships into our
// source row here, as an array of 'style' values (the unique ID for
// the beer_term migration).
$terms = $this->select('migrate_example_beer_topic_node', 'bt')
->fields('bt', ['style'])
->condition('bid', $row->getSourceProperty('bid'))
->execute()
->fetchCol();
$row->setSourceProperty('terms', $terms);
// As we did for favorite beers in the user migration, we need to explode
// the multi-value country names.
if ($value = $row->getSourceProperty('countries')) {
$row->setSourceProperty('countries', explode('|', $value));
}
return parent::prepareRow($row);
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_example\Plugin\migrate\source;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
/**
* This is an example of a simple SQL-based source plugin.
*
* Source plugins are classes which deliver source data to the processing
* pipeline. For SQL sources, the SqlBase class provides most of the
* functionality needed - for a specific migration, you are required to
* implement the three simple public methods you see below.
*
* This annotation tells Drupal that the name of the MigrateSource plugin
* implemented by this class is "beer_term". This is the name that the migration
* configuration references with the source "plugin" key.
*
* @MigrateSource(
* id = "beer_term"
* )
*/
final class BeerTerm extends SqlBase {
/**
* {@inheritdoc}
*/
public function query(): SelectInterface {
// The most important part of a SQL source plugin is the SQL query to
// retrieve the data to be imported. Note that the query is not executed
// here - the migration process will control execution of the query. Also
// note that it is constructed from a $this->select() call - this ensures
// that the query is executed against the database configured for this
// source plugin.
$fields = ['style', 'details', 'style_parent', 'region', 'hoppiness'];
return $this->select('migrate_example_beer_topic', 'met')
->fields('met', $fields)
// We sort this way to ensure parent terms are imported first.
->orderBy('style_parent', 'ASC');
}
/**
* {@inheritdoc}
*/
public function fields(): array {
// This method simply documents the available source fields provided by the
// source plugin, for use by front-end tools. It returns an array keyed by
// field/column name, with the value being a translated string explaining
// to humans what the field represents.
return [
'style' => $this->t('Beer style'),
'details' => $this->t('Style details'),
'style_parent' => $this->t('Parent style'),
// These values are not currently migrated - it's OK to skip fields you
// don't need.
'region' => $this->t('Region the style is associated with'),
'hoppiness' => $this->t('Hoppiness of the style'),
];
}
/**
* {@inheritdoc}
*/
public function getIds(): array {
// This method indicates what field(s) from the source row uniquely identify
// that source row, and what their types are. This is critical information
// for managing the migration. The keys of the returned array are the field
// names from the query which comprise the unique identifier. The values are
// arrays indicating the type of the field, used for creating compatible
// columns in the map tables that track processed items.
return [
'style' => [
'type' => 'string',
// 'alias' is the alias for the table containing 'style' in the query
// defined above. Optional in this case, but necessary if the same
// column may occur in multiple tables in a join.
'alias' => 'met',
],
];
}
}

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types = 1);
namespace Drupal\migrate_example\Plugin\migrate\source;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
use Drupal\migrate\Row;
/**
* Source plugin for beer user accounts.
*
* @MigrateSource(
* id = "beer_user"
* )
*/
final class BeerUser extends SqlBase {
/**
* {@inheritdoc}
*/
public function query(): SelectInterface {
$fields = [
'aid',
'status',
'registered',
'username',
'nickname',
'password',
'email',
'sex',
'beers',
];
return $this->select('migrate_example_beer_account', 'mea')
->fields('mea', $fields);
}
/**
* {@inheritdoc}
*/
public function fields(): array {
return [
'aid' => $this->t('Account ID'),
'status' => $this->t('Blocked/Allowed'),
'registered' => $this->t('Registered date'),
'username' => $this->t('Account name (for login)'),
'nickname' => $this->t('Account name (for display)'),
'password' => $this->t('Account password (raw)'),
'email' => $this->t('Account email'),
'sex' => $this->t('Gender'),
'beers' => $this->t('Favorite beers, pipe-separated'),
];
}
/**
* {@inheritdoc}
*/
public function getIds(): array {
return [
'aid' => [
'type' => 'integer',
'alias' => 'mea',
],
];
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row): bool {
// A prepareRow() is the most common place to perform custom run-time
// processing that isn't handled by an existing process plugin. It is called
// when the raw data has been pulled from the source, and provides the
// opportunity to modify or add to that data, creating the canonical set of
// source data that will be fed into the processing pipeline.
// In our particular case, the list of a user's favorite beers is a pipe-
// separated list of beer IDs. The processing pipeline deals with arrays
// representing multi-value fields naturally, so we want to explode that
// string to an array of individual beer IDs.
if ($value = $row->getSourceProperty('beers')) {
$row->setSourceProperty('beers', explode('|', $value));
}
// Always call your parent! Essential processing is performed in the base
// class. Be mindful that prepareRow() returns a boolean status - if FALSE
// that indicates that the item being processed should be skipped. Unless
// we're deciding to skip an item ourselves, let the parent class decide.
return parent::prepareRow($row);
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types = 1);
namespace Drupal\Tests\migrate_example\Kernel;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\migrate_drupal\Kernel\MigrateDrupalTestBase;
/**
* Tests migrate_example migrations.
*
* @group migrate_plus
*/
final class MigrateExampleTest extends MigrateDrupalTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'taxonomy',
'comment',
'text',
'migrate_plus',
'migrate_example',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig([
'node',
'comment',
'migrate_example',
]);
$this->installSchema('comment', ['comment_entity_statistics']);
$this->installSchema('node', ['node_access']);
$this->installSchema('user', ['users_data']);
// Install the module via installer to trigger hook_install.
\Drupal::service('module_installer')->install(['migrate_example_setup']);
$this->installConfig(['migrate_example_setup']);
$this->startCollectingMessages();
// Execute "beer" migrations from 'migrate_example' module.
$this->executeMigration('beer_user');
$this->executeMigrations([
'beer_term',
'beer_node',
'beer_comment',
]);
}
/**
* Tests the results of "Beer" example migration.
*/
public function testBeerMigration(): void {
$users = \Drupal::entityTypeManager()->getStorage('user')->loadMultiple();
$this->assertCount(4, $users);
$terms = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadMultiple();
$this->assertCount(3, $terms);
$nodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple();
$this->assertCount(3, $nodes);
$comments = \Drupal::entityTypeManager()->getStorage('comment')->loadMultiple();
$this->assertCount(5, $comments);
}
/**
* Tests whether the module can be uninstalled and installed again.
*
* Also, checks whether the example configs are removed after uninstall.
*/
public function testModuleCleanup(): void {
$test_node_type = 'migrate_example_beer';
// Prove that test content type existed before the uninstall process.
$this->assertInstanceOf(NodeType::class, NodeType::load($test_node_type));
\Drupal::service('module_installer')->uninstall(['migrate_example_setup']);
// Make sure the test content type was removed.
$this->assertNull(NodeType::load($test_node_type));
// Check whether the module can be installed again.
\Drupal::service('module_installer')->install(['migrate_example_setup']);
}
}