diff --git a/composer.json b/composer.json index b17f66d9d4d79361a7590e875d00eaa78c97601d..1dd54db9eb890b4d6f3e84a36a3ca293f8ba8ab6 100644 --- a/composer.json +++ b/composer.json @@ -142,8 +142,8 @@ "drupal/menu_breadcrumb": "1.13", "drupal/metatag": "1.13", "drupal/migrate_devel": "2.0-alpha2", - "drupal/migrate_plus": "4.0", - "drupal/migrate_tools": "4.0", + "drupal/migrate_plus": "5.1", + "drupal/migrate_tools": "5.0", "drupal/mobile_detect_twig_extensions": "1.4", "drupal/mobile_device_detection": "3.2", "drupal/module_filter": "3.1", diff --git a/composer.lock b/composer.lock index 313a39a027c083e4137fa667ecfd695aa38c128c..9aff2845f4d894886186ab9c3ac56f8957cff34e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1c62a5829fdd2dcd1c604e5105490765", + "content-hash": "4061d56ed0fe7f80e7e5e4a47a2ae8c5", "packages": [ { "name": "alchemy/zippy", @@ -6118,20 +6118,21 @@ }, { "name": "drupal/migrate_plus", - "version": "4.0.0", + "version": "5.1.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/migrate_plus.git", - "reference": "8.x-4.0" + "reference": "8.x-5.1" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/migrate_plus-8.x-4.0.zip", - "reference": "8.x-4.0", - "shasum": "63dad289defe8298aa5ca5e30062fe9761d19eca" + "url": "https://ftp.drupal.org/files/projects/migrate_plus-8.x-5.1.zip", + "reference": "8.x-5.1", + "shasum": "1257427ab0c64459c3c1e42bb2a98d3114b77163" }, "require": { - "drupal/core": "^8.3" + "drupal/core": "^8.8 || ^9", + "php": ">=7.1" }, "require-dev": { "drupal/migrate_example_advanced_setup": "*", @@ -6143,12 +6144,9 @@ }, "type": "drupal-module", "extra": { - "branch-alias": { - "dev-4.x": "4.x-dev" - }, "drupal": { - "version": "8.x-4.0", - "datestamp": "1536264180", + "version": "8.x-5.1", + "datestamp": "1588261060", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -6157,7 +6155,7 @@ }, "notification-url": "https://packages.drupal.org/8/downloads", "license": [ - "GPL-2.0+" + "GPL-2.0-or-later" ], "authors": [ { @@ -6166,48 +6164,51 @@ "role": "Maintainer" }, { - "name": "mikeryan", - "homepage": "https://www.drupal.org/user/4420" + "name": "Lucas Hedding", + "homepage": "https://www.drupal.org/u/heddn", + "role": "Maintainer" } ], "description": "Enhancements to core migration support.", "homepage": "https://www.drupal.org/project/migrate_plus", "support": { - "source": "https://cgit.drupalcode.org/migrate_plus", + "source": "https://git.drupalcode.org/project/migrate_plus", "issues": "https://www.drupal.org/project/issues/migrate_plus", - "irc": "irc://irc.freenode.org/drupal-migrate" + "slack": "#migrate" } }, { "name": "drupal/migrate_tools", - "version": "4.0.0", + "version": "5.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/migrate_tools.git", - "reference": "8.x-4.0" + "reference": "8.x-5.0" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/migrate_tools-8.x-4.0.zip", - "reference": "8.x-4.0", - "shasum": "016dfb010df76723c5a6a447921fdccd3c885237" + "url": "https://ftp.drupal.org/files/projects/migrate_tools-8.x-5.0.zip", + "reference": "8.x-5.0", + "shasum": "b7c91aa6f7de9d6d548f65f83c8736e47e5926a1" }, "require": { - "drupal/core": "^8.3", - "drupal/migrate_plus": "^4" + "drupal/core": "^8.8 | ^9", + "drupal/migrate_plus": "^5", + "php": ">=7.1" }, "require-dev": { - "drupal/coder": "^8", - "drupal/migrate_source_csv": "^2.2" + "drupal/migrate_plus": "^5", + "drupal/migrate_source_csv": "^3", + "drush/drush": "^10" + }, + "suggest": { + "drush/drush": "^9 || ^10" }, "type": "drupal-module", "extra": { - "branch-alias": { - "dev-4.x": "4.x-dev" - }, "drupal": { - "version": "8.x-4.0", - "datestamp": "1535380084", + "version": "8.x-5.0", + "datestamp": "1588260531", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -6215,34 +6216,32 @@ }, "drush": { "services": { - "drush.services.yml": "^9" + "drush.services.yml": "^9 || ^10" } } }, "notification-url": "https://packages.drupal.org/8/downloads", "license": [ - "GPL-2.0+" + "GPL-2.0-or-later" ], "authors": [ { - "name": "heddn", - "homepage": "https://www.drupal.org/user/1463982" - }, - { - "name": "mikeryan", - "homepage": "https://www.drupal.org/user/4420" + "name": "Mike Ryan", + "homepage": "https://www.drupal.org/u/mikeryan", + "role": "Maintainer" }, { - "name": "moshe weitzman", - "homepage": "https://www.drupal.org/user/23" + "name": "Lucas Hedding", + "homepage": "https://www.drupal.org/u/heddn", + "role": "Maintainer" } ], "description": "Tools to assist in developing and running migrations.", "homepage": "http://drupal.org/project/migrate_tools", "support": { - "source": "http://cgit.drupalcode.org/migrate_tools", - "issues": "http://drupal.org/project/migrate_tools", - "irc": "irc://irc.freenode.org/drupal-migrate" + "source": "https://git.drupalcode.org/project/migrate_tools", + "issues": "https://www.drupal.org/project/issues/migrate_tools", + "slack": "#migrate" } }, { diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 1940910d4a232d4a658dfe7f5dee32bacb14daac..34351e0277447be10058a3ef9647c2180219c920 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -6304,21 +6304,22 @@ }, { "name": "drupal/migrate_plus", - "version": "4.0.0", - "version_normalized": "4.0.0.0", + "version": "5.1.0", + "version_normalized": "5.1.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/migrate_plus.git", - "reference": "8.x-4.0" + "reference": "8.x-5.1" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/migrate_plus-8.x-4.0.zip", - "reference": "8.x-4.0", - "shasum": "63dad289defe8298aa5ca5e30062fe9761d19eca" + "url": "https://ftp.drupal.org/files/projects/migrate_plus-8.x-5.1.zip", + "reference": "8.x-5.1", + "shasum": "1257427ab0c64459c3c1e42bb2a98d3114b77163" }, "require": { - "drupal/core": "^8.3" + "drupal/core": "^8.8 || ^9", + "php": ">=7.1" }, "require-dev": { "drupal/migrate_example_advanced_setup": "*", @@ -6330,12 +6331,9 @@ }, "type": "drupal-module", "extra": { - "branch-alias": { - "dev-4.x": "4.x-dev" - }, "drupal": { - "version": "8.x-4.0", - "datestamp": "1536264180", + "version": "8.x-5.1", + "datestamp": "1588261060", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -6345,7 +6343,7 @@ "installation-source": "dist", "notification-url": "https://packages.drupal.org/8/downloads", "license": [ - "GPL-2.0+" + "GPL-2.0-or-later" ], "authors": [ { @@ -6354,49 +6352,52 @@ "role": "Maintainer" }, { - "name": "mikeryan", - "homepage": "https://www.drupal.org/user/4420" + "name": "Lucas Hedding", + "homepage": "https://www.drupal.org/u/heddn", + "role": "Maintainer" } ], "description": "Enhancements to core migration support.", "homepage": "https://www.drupal.org/project/migrate_plus", "support": { - "source": "https://cgit.drupalcode.org/migrate_plus", + "source": "https://git.drupalcode.org/project/migrate_plus", "issues": "https://www.drupal.org/project/issues/migrate_plus", - "irc": "irc://irc.freenode.org/drupal-migrate" + "slack": "#migrate" } }, { "name": "drupal/migrate_tools", - "version": "4.0.0", - "version_normalized": "4.0.0.0", + "version": "5.0.0", + "version_normalized": "5.0.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/migrate_tools.git", - "reference": "8.x-4.0" + "reference": "8.x-5.0" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/migrate_tools-8.x-4.0.zip", - "reference": "8.x-4.0", - "shasum": "016dfb010df76723c5a6a447921fdccd3c885237" + "url": "https://ftp.drupal.org/files/projects/migrate_tools-8.x-5.0.zip", + "reference": "8.x-5.0", + "shasum": "b7c91aa6f7de9d6d548f65f83c8736e47e5926a1" }, "require": { - "drupal/core": "^8.3", - "drupal/migrate_plus": "^4" + "drupal/core": "^8.8 | ^9", + "drupal/migrate_plus": "^5", + "php": ">=7.1" }, "require-dev": { - "drupal/coder": "^8", - "drupal/migrate_source_csv": "^2.2" + "drupal/migrate_plus": "^5", + "drupal/migrate_source_csv": "^3", + "drush/drush": "^10" + }, + "suggest": { + "drush/drush": "^9 || ^10" }, "type": "drupal-module", "extra": { - "branch-alias": { - "dev-4.x": "4.x-dev" - }, "drupal": { - "version": "8.x-4.0", - "datestamp": "1535380084", + "version": "8.x-5.0", + "datestamp": "1588260531", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -6404,35 +6405,33 @@ }, "drush": { "services": { - "drush.services.yml": "^9" + "drush.services.yml": "^9 || ^10" } } }, "installation-source": "dist", "notification-url": "https://packages.drupal.org/8/downloads", "license": [ - "GPL-2.0+" + "GPL-2.0-or-later" ], "authors": [ { - "name": "heddn", - "homepage": "https://www.drupal.org/user/1463982" - }, - { - "name": "mikeryan", - "homepage": "https://www.drupal.org/user/4420" + "name": "Mike Ryan", + "homepage": "https://www.drupal.org/u/mikeryan", + "role": "Maintainer" }, { - "name": "moshe weitzman", - "homepage": "https://www.drupal.org/user/23" + "name": "Lucas Hedding", + "homepage": "https://www.drupal.org/u/heddn", + "role": "Maintainer" } ], "description": "Tools to assist in developing and running migrations.", "homepage": "http://drupal.org/project/migrate_tools", "support": { - "source": "http://cgit.drupalcode.org/migrate_tools", - "issues": "http://drupal.org/project/migrate_tools", - "irc": "irc://irc.freenode.org/drupal-migrate" + "source": "https://git.drupalcode.org/project/migrate_tools", + "issues": "https://www.drupal.org/project/issues/migrate_tools", + "slack": "#migrate" } }, { diff --git a/web/modules/migrate_plus/README.txt b/web/modules/migrate_plus/README.txt index 797082e388e58656739e88d8611a6991f9bbadf0..9738be925077101ffeb42844aca02cd86a4d8324 100644 --- a/web/modules/migrate_plus/README.txt +++ b/web/modules/migrate_plus/README.txt @@ -7,8 +7,8 @@ Extensions to base API migration configuration. * A MigrationGroup configuration entity is provided, which enables migrations to be organized in groups, and to maintain shared configuration in one place. -* A MigrateEvents::PREPARE_ROW event is provided to dispatch hook_prepare_row() - invocations as events. +* A MigrateEvents::PREPARE_ROW event is provided to dispatch + hook_migrate_prepare_row() invocations as events. * A SourcePluginExtension class is provided, enabling one to define fields and IDs for a source plugin via configuration rather than requiring PHP code. diff --git a/web/modules/migrate_plus/composer.json b/web/modules/migrate_plus/composer.json index 54070eda9559c3bf94a7d5a29aa9ccf0a2214aaf..7dfce263ef156f728cf40a49fdbd2cc51218048b 100644 --- a/web/modules/migrate_plus/composer.json +++ b/web/modules/migrate_plus/composer.json @@ -1,24 +1,32 @@ { - "name": "drupal/migrate_plus", - "description": "Enhancements to core migration support.", - "type": "drupal-module", - "license": "GPL-2.0+", - "homepage": "https://www.drupal.org/project/migrate_plus", - "authors": [ - { - "name": "Mike Ryan", - "homepage":"https://www.drupal.org/u/mikeryan", - "role": "Maintainer" + "name": "drupal/migrate_plus", + "description": "Enhancements to core migration support.", + "type": "drupal-module", + "license": "GPL-2.0-or-later", + "homepage": "https://www.drupal.org/project/migrate_plus", + "authors": [ + { + "name": "Mike Ryan", + "homepage":"https://www.drupal.org/u/mikeryan", + "role": "Maintainer" + }, + { + "name": "Lucas Hedding", + "homepage": "https://www.drupal.org/u/heddn", + "role": "Maintainer" + } + ], + "support": { + "issues": "https://www.drupal.org/project/issues/migrate_plus", + "slack": "#migrate", + "source": "https://git.drupalcode.org/project/migrate_plus" + }, + "minimum-stability": "dev", + "suggest": { + "sainsburys/guzzle-oauth2-plugin": "3.0 required for the OAuth2 authentication plugin", + "ext-soap": "*" + }, + "require": { + "php": ">=7.1" } - ], - "support": { - "issues": "https://www.drupal.org/project/issues/migrate_plus", - "irc": "irc://irc.freenode.org/drupal-migrate", - "source": "https://cgit.drupalcode.org/migrate_plus" - }, - "minimum-stability": "dev", - "suggest": { - "sainsburys/guzzle-oauth2-plugin": "3.0 required for the OAuth2 authentication plugin", - "ext-soap": "*" - } } diff --git a/web/modules/migrate_plus/config/schema/migrate_plus.destination.schema.yml b/web/modules/migrate_plus/config/schema/migrate_plus.destination.schema.yml index 58ad74e53834cd06968ef95a889478a731a0eb27..c76e59d5d3b9363a593ca6244bbcb860e392e412 100644 --- a/web/modules/migrate_plus/config/schema/migrate_plus.destination.schema.yml +++ b/web/modules/migrate_plus/config/schema/migrate_plus.destination.schema.yml @@ -8,6 +8,9 @@ migrate_plus.destination.*: type: boolean label: 'Whether stubbing is allowed.' default: false + default_bundle: + type: string + label: 'The default bundle for content entity destinations.' migrate_plus.destination.config: type: migrate_destination diff --git a/web/modules/migrate_plus/config/schema/migrate_plus.schema.yml b/web/modules/migrate_plus/config/schema/migrate_plus.schema.yml index 99aef5523869dcd4fbbaa9cf1173e9d1765481b3..33dc2e6e1d8cb2033bb1cb9a5500bf57a869e098 100644 --- a/web/modules/migrate_plus/config/schema/migrate_plus.schema.yml +++ b/web/modules/migrate_plus/config/schema/migrate_plus.schema.yml @@ -29,13 +29,13 @@ migrate_plus.migration.*: type: label label: 'Label' source: - type: migrate_plus.source.[plugin] + type: ignore label: 'Source' process: type: ignore label: 'Process' destination: - type: migrate_plus.destination.[plugin] + type: ignore label: 'Destination' migration_dependencies: type: mapping diff --git a/web/modules/migrate_plus/drupalci.yml b/web/modules/migrate_plus/drupalci.yml new file mode 100644 index 0000000000000000000000000000000000000000..7286c3ba56786e1db97341e19cdba85a69fd139c --- /dev/null +++ b/web/modules/migrate_plus/drupalci.yml @@ -0,0 +1,20 @@ +# Learn to make one for your own drupal.org project: +# https://www.drupal.org/drupalorg/docs/drupal-ci/customizing-drupalci-testing +build: + assessment: + validate_codebase: + phplint: + container_composer: + phpcs: + # phpcs will use core's specified version of Coder. + sniff-all-files: true + halt-on-fail: true + testing: + # run_tests task is executed several times in order of performance speeds. + # halt-on-fail can be set on the run_tests tasks in order to fail fast. + # suppress-deprecations is false in order to be alerted to usages of + # deprecated code. + run_tests.standard: + types: 'Simpletest,PHPUnit-Unit,PHPUnit-Kernel,PHPUnit-Functional' + testgroups: '--all' + suppress-deprecations: false diff --git a/web/modules/migrate_plus/migrate_example/README.txt b/web/modules/migrate_plus/migrate_example/README.txt index 431e22e9bac4ed3c4641b26d82a79185f11b6b91..1a616788a65b966cda2e21acbe6d3a9de0a4d5a6 100644 --- a/web/modules/migrate_plus/migrate_example/README.txt +++ b/web/modules/migrate_plus/migrate_example/README.txt @@ -1,7 +1,7 @@ INTRODUCTION ------------ The migrate_example module demonstrates how to implement custom migrations -for Drupal 8. It includes a group of "beer" migrations demonstrating a complete +for Drupal 8+. It includes a group of "beer" migrations demonstrating a complete simple migration scenario. THE BEER SITE @@ -15,7 +15,7 @@ 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 +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 diff --git a/web/modules/migrate_plus/migrate_example/migrate_example.info.yml b/web/modules/migrate_plus/migrate_example/migrate_example.info.yml index 5ff995788d1aa13f41d87401bc6b3e2a1157deff..c2b711a05d1a3ad6c60983058619f1abd2fce221 100644 --- a/web/modules/migrate_plus/migrate_example/migrate_example.info.yml +++ b/web/modules/migrate_plus/migrate_example/migrate_example.info.yml @@ -1,8 +1,8 @@ type: module name: Migrate Example -description: 'Examples of how Drupal 8 migration compares to previous versions.' +description: 'Examples of how Drupal 8+ migration compares to previous versions.' package: Examples -# core: 8.x +core_version_requirement: ^8.8 || ^9 dependencies: - drupal:migrate - migrate_plus:migrate_example_setup @@ -10,8 +10,7 @@ dependencies: - drupal:menu_ui - drupal:path -# Information added by Drupal.org packaging script on 2018-09-06 -version: '8.x-4.0' -core: '8.x' +# Information added by Drupal.org packaging script on 2020-04-30 +version: '8.x-5.1' project: 'migrate_plus' -datestamp: 1536264189 +datestamp: 1588261062 diff --git a/web/modules/migrate_plus/migrate_example/migrate_example_setup/config/install/core.entity_form_display.node.migrate_example_beer.default.yml b/web/modules/migrate_plus/migrate_example/migrate_example_setup/config/install/core.entity_form_display.node.migrate_example_beer.default.yml index 306af0e3fe46e6da77aca91ffaf82ed13912b8cf..d3b47a9cef3bb8fc206a7479f8f878baa2ac9702 100644 --- a/web/modules/migrate_plus/migrate_example/migrate_example_setup/config/install/core.entity_form_display.node.migrate_example_beer.default.yml +++ b/web/modules/migrate_plus/migrate_example/migrate_example_setup/config/install/core.entity_form_display.node.migrate_example_beer.default.yml @@ -39,6 +39,7 @@ content: weight: 7 settings: match_operator: CONTAINS + match_limit: 10 size: 60 placeholder: '' third_party_settings: { } @@ -81,6 +82,7 @@ content: weight: 1 settings: match_operator: CONTAINS + match_limit: 10 size: 60 placeholder: '' third_party_settings: { } diff --git a/web/modules/migrate_plus/migrate_example/migrate_example_setup/migrate_example_setup.info.yml b/web/modules/migrate_plus/migrate_example/migrate_example_setup/migrate_example_setup.info.yml index b9dd2672cdde2bded14b3ec6eadfa77699bc5b2e..2c4afb4340496ad6ffd92d1bbeca171915a6b151 100644 --- a/web/modules/migrate_plus/migrate_example/migrate_example_setup/migrate_example_setup.info.yml +++ b/web/modules/migrate_plus/migrate_example/migrate_example_setup/migrate_example_setup.info.yml @@ -2,8 +2,8 @@ type: module name: Migrate Example Setup description: 'Separate site configuration for the example from the actual migration.' package: Migration -# core: 8.x -hidden: 1 +core_version_requirement: ^8.8 || ^9 +hidden: true dependencies: - drupal:comment - drupal:image @@ -11,8 +11,7 @@ dependencies: - drupal:options - drupal:taxonomy -# Information added by Drupal.org packaging script on 2018-09-06 -version: '8.x-4.0' -core: '8.x' +# Information added by Drupal.org packaging script on 2020-04-30 +version: '8.x-5.1' project: 'migrate_plus' -datestamp: 1536264189 +datestamp: 1588261062 diff --git a/web/modules/migrate_plus/migrate_example/migrate_example_setup/migrate_example_setup.install b/web/modules/migrate_plus/migrate_example/migrate_example_setup/migrate_example_setup.install index 342bc2dcfc529ec24337ede726a6aaa90cf07f27..e0a5fe65f461072e8a39cbea01275d3c2bdaf8e6 100644 --- a/web/modules/migrate_plus/migrate_example/migrate_example_setup/migrate_example_setup.install +++ b/web/modules/migrate_plus/migrate_example/migrate_example_setup/migrate_example_setup.install @@ -316,7 +316,7 @@ function migrate_example_beer_data_node() { 'image_title', 'image_description', ]; - $query = db_insert('migrate_example_beer_node') + $query = \Drupal::database()->insert('migrate_example_beer_node') ->fields($fields); // Use high bid numbers to avoid overwriting an existing node id. $data = [ @@ -381,7 +381,7 @@ function migrate_example_beer_data_account() { 'sex', 'beers', ]; - $query = db_insert('migrate_example_beer_account') + $query = \Drupal::database()->insert('migrate_example_beer_account') ->fields($fields); $data = [ [ @@ -436,7 +436,7 @@ function migrate_example_beer_data_account() { */ function migrate_example_beer_data_comment() { $fields = ['bid', 'cid_parent', 'subject', 'body', 'name', 'mail', 'aid']; - $query = db_insert('migrate_example_beer_comment') + $query = \Drupal::database()->insert('migrate_example_beer_comment') ->fields($fields); $data = [ [99999998, NULL, 'im first', 'full body', 'alice', 'alice@example.com', 0], @@ -464,7 +464,7 @@ function migrate_example_beer_data_comment() { */ function migrate_example_beer_data_topic() { $fields = ['style', 'details', 'style_parent', 'region', 'hoppiness']; - $query = db_insert('migrate_example_beer_topic') + $query = \Drupal::database()->insert('migrate_example_beer_topic') ->fields($fields); $data = [ ['ale', 'traditional', NULL, 'Medieval British Isles', 'Medium'], @@ -488,7 +488,7 @@ function migrate_example_beer_data_topic() { */ function migrate_example_beer_data_topic_node() { $fields = ['bid', 'style']; - $query = db_insert('migrate_example_beer_topic_node') + $query = \Drupal::database()->insert('migrate_example_beer_topic_node') ->fields($fields); $data = [ [99999999, 'pilsner'], diff --git a/web/modules/migrate_plus/migrate_example_advanced/README.txt b/web/modules/migrate_plus/migrate_example_advanced/README.txt index 8d5a25d807a250ed14fc38b95387f8aa8c1f01a0..a791c70a364e3099bf9d5cc30bf56c7dd018aeb1 100644 --- a/web/modules/migrate_plus/migrate_example_advanced/README.txt +++ b/web/modules/migrate_plus/migrate_example_advanced/README.txt @@ -1,15 +1,15 @@ INTRODUCTION ------------ -The migrate_example_advanced module demonstrates some techniques for Drupal 8 +The migrate_example_advanced module demonstrates some techniques for Drupal 8+ migrations beyond the basics in migrate_example. It includes a group of migrations with a wine theme. SETUP ----- To demonstrate XML migrations as realistically as possible, the setup module -provides the source data as REST services. So the migrations' references to these -services can be set up accurately, if you install migrate_example_advanced via -drush be sure to use the --uri parameter, e.g. +provides the source data as REST services. So the migrations' references to +these services can be set up accurately, if you install migrate_example_advanced +via drush be sure to use the --uri parameter, e.g. drush en -y migrate_example_advanced --uri=http://d8.local:8083/ @@ -42,7 +42,7 @@ example: UNDERSTANDING THE MIGRATIONS ---------------------------- Basic techniques demonstrated in the migrate_example module are not rehashed -here - it is expected that if you are learning Drupal 8 migration, you will +here - it is expected that if you are learning Drupal 8+ migration, you will study and understand those examples first, and use migrate_example_advanced to learn about specific techniques beyond those basics. This example doesn't have the narrative form of migrate_example - it's more of a grab-bag demonstrating diff --git a/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_variety_list.yml.txt b/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_variety_list.yml.txt deleted file mode 100644 index ff88862b2fac8c7af304ad6991acd5a7afa84d0f..0000000000000000000000000000000000000000 --- a/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_variety_list.yml.txt +++ /dev/null @@ -1,59 +0,0 @@ -# This migration demonstrates importing from an endpoint listing other endpoints -# containing individual item data. -id: wine_variety_list -label: XML feed of varieties -migration_group: wine -migration_tags: - - advanced example -source: - # We use the XML source plugin. - plugin: xml - # Normally, this is one or more fully-qualified URLs or file paths. Because - # we can't hardcode your local URL, we provide a relative path here which - # hook_install() will rewrite to a full URL for the current site. - urls: - - /migrate_example_advanced_variety_list?_format=xml - item_url: /migrate_example_advanced_variety_list/:id?_format=xml - id_selector: /response/items - # Visit the URL above (relative to your site root) and look at it. You can see - # that <response> is the outer element, and each item we want to import is a - # <position> element. The item_xpath value is the xpath to use to query the - # desired elements. - item_selector: /response/variety - # Under 'fields', we list the data items to be imported. The first level keys - # are the source field names we want to populate (the names to be used as - # sources in the process configuration below). For each field we're importing, - # we provide a label (optional - this is for display in migration tools) and - # an xpath for retrieving that value. It's important to note that this xpath - # is relative to the elements retrieved by item_xpath. - fields: - category_name: - label: - selector: name - category_details: - label: - selector: details - category_parent: - label: 'Unique position identifier' - selector: parent - # Under 'ids', we identify source fields populated above which will uniquely - # identify each imported item. The 'type' makes sure the migration map table - # uses the proper schema type for stored the IDs. - ids: - category_name: - type: string -process: - vid: - plugin: default_value - default_value: migrate_example_wine_varieties - name: category_name - description: category_details - parent: - plugin: migration_lookup - migration: wine_terms - source: category_parent -destination: - plugin: entity:taxonomy_term -migration_dependencies: - require: - - wine_terms diff --git a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced.info.yml b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced.info.yml index 1d396e34bc8382fd2da518eae5297ae11691a203..34b2bb57080726b8547fbf602848ad6a84ab6df8 100644 --- a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced.info.yml +++ b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced.info.yml @@ -1,15 +1,14 @@ type: module name: Migrate Example (Advanced) -description: 'Specialized examples of Drupal 8 migration.' +description: 'Specialized examples of Drupal 8+ migration.' package: Examples -# core: 8.x +core_version_requirement: ^8.8 || ^9 dependencies: - drupal:migrate - migrate_plus:migrate_example_advanced_setup - migrate_plus:migrate_plus -# Information added by Drupal.org packaging script on 2018-09-06 -version: '8.x-4.0' -core: '8.x' +# Information added by Drupal.org packaging script on 2020-04-30 +version: '8.x-5.1' project: 'migrate_plus' -datestamp: 1536264189 +datestamp: 1588261062 diff --git a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/core.entity_form_display.node.migrate_example_producer.default.yml b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/core.entity_form_display.node.migrate_example_producer.default.yml index 89df7690e983cebcc3bf028bd32347912d3a7c10..2df3f91e1d2d1a00b0cd91435835d82b87a56f13 100644 --- a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/core.entity_form_display.node.migrate_example_producer.default.yml +++ b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/core.entity_form_display.node.migrate_example_producer.default.yml @@ -30,6 +30,7 @@ content: weight: 32 settings: match_operator: CONTAINS + match_limit: 10 size: 60 placeholder: '' third_party_settings: { } @@ -63,6 +64,7 @@ content: weight: 5 settings: match_operator: CONTAINS + match_limit: 10 size: 60 placeholder: '' third_party_settings: { } diff --git a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/core.entity_form_display.node.migrate_example_wine.default.yml b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/core.entity_form_display.node.migrate_example_wine.default.yml index 0ef5edb66dc2204585ad25b60b8989c8e228fe0b..d9dff917a97c6d2224aa772574bba2977d388b81 100644 --- a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/core.entity_form_display.node.migrate_example_wine.default.yml +++ b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/core.entity_form_display.node.migrate_example_wine.default.yml @@ -56,6 +56,7 @@ content: weight: 34 settings: match_operator: CONTAINS + match_limit: 10 size: 60 placeholder: '' third_party_settings: { } @@ -70,6 +71,7 @@ content: weight: 33 settings: match_operator: CONTAINS + match_limit: 10 size: 60 placeholder: '' third_party_settings: { } @@ -78,6 +80,7 @@ content: weight: 32 settings: match_operator: CONTAINS + match_limit: 10 size: 60 placeholder: '' third_party_settings: { } @@ -111,6 +114,7 @@ content: weight: 5 settings: match_operator: CONTAINS + match_limit: 10 size: 60 placeholder: '' third_party_settings: { } diff --git a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/migrate_example_advanced_setup.info.yml b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/migrate_example_advanced_setup.info.yml index 87058656836fededbc49a49bad261a323e8a84a6..d02a5afaf4219f2f26332a86ebcef7a98efeed4a 100644 --- a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/migrate_example_advanced_setup.info.yml +++ b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/migrate_example_advanced_setup.info.yml @@ -2,8 +2,8 @@ type: module name: Migrate Advanced Example Setup description: 'Separate site configuration for the example from the actual migration.' package: Migration -# core: 8.x -hidden: 1 +core_version_requirement: ^8.8 || ^9 +hidden: true dependencies: - drupal:comment - drupal:image @@ -11,8 +11,7 @@ dependencies: - drupal:taxonomy - drupal:rest -# Information added by Drupal.org packaging script on 2018-09-06 -version: '8.x-4.0' -core: '8.x' +# Information added by Drupal.org packaging script on 2020-04-30 +version: '8.x-5.1' project: 'migrate_plus' -datestamp: 1536264189 +datestamp: 1588261062 diff --git a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/migrate_example_advanced_setup.install b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/migrate_example_advanced_setup.install index be9031a6beef0910247f48012ad986417eb5f860..6fd5f7926c46cff2ede66e687dd4f1532cd1b62b 100644 --- a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/migrate_example_advanced_setup.install +++ b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/migrate_example_advanced_setup.install @@ -737,7 +737,7 @@ function migrate_example_advanced_data_wine() { 'region', 'rating', ]; - $query = db_insert('migrate_example_wine') + $query = \Drupal::database()->insert('migrate_example_wine') ->fields($fields); $data = [ [ @@ -776,7 +776,7 @@ function migrate_example_advanced_data_wine() { */ function migrate_example_advanced_data_updates() { $fields = ['wineid', 'rating']; - $query = db_insert('migrate_example_advanced_updates') + $query = \Drupal::database()->insert('migrate_example_advanced_updates') ->fields($fields); $data = [ [1, 93], @@ -793,7 +793,7 @@ function migrate_example_advanced_data_updates() { */ function migrate_example_advanced_data_producer() { $fields = ['producerid', 'name', 'body', 'excerpt', 'accountid']; - $query = db_insert('migrate_example_advanced_producer') + $query = \Drupal::database()->insert('migrate_example_advanced_producer') ->fields($fields); $data = [ [1, 'Montes', 'Fine Chilean winery', 'Great!', 9], @@ -824,7 +824,7 @@ function migrate_example_advanced_data_account() { 'imageid', 'positions', ]; - $query = db_insert('migrate_example_advanced_account') + $query = \Drupal::database()->insert('migrate_example_advanced_account') ->fields($fields); $data = [ [ @@ -884,7 +884,7 @@ function migrate_example_advanced_data_account() { */ function migrate_example_advanced_data_account_updates() { $fields = ['accountid', 'sex']; - $query = db_insert('migrate_example_advanced_account_updates') + $query = \Drupal::database()->insert('migrate_example_advanced_account_updates') ->fields($fields); $data = [ [1, NULL], @@ -915,7 +915,7 @@ function migrate_example_advanced_data_comment() { 'posted', 'lastchanged', ]; - $query = db_insert('migrate_example_advanced_comment') + $query = \Drupal::database()->insert('migrate_example_advanced_comment') ->fields($fields); $data = [ [ @@ -1000,7 +1000,7 @@ function migrate_example_advanced_data_comment() { */ function migrate_example_advanced_data_comment_updates() { $fields = ['commentid', 'subject']; - $query = db_insert('migrate_example_advanced_comment_updates') + $query = \Drupal::database()->insert('migrate_example_advanced_comment_updates') ->fields($fields); $data = [ [1, 'I am first'], @@ -1027,7 +1027,7 @@ function migrate_example_advanced_data_categories() { 'details', 'ordering', ]; - $query = db_insert('migrate_example_advanced_categories') + $query = \Drupal::database()->insert('migrate_example_advanced_categories') ->fields($fields); $data = [ [ @@ -1079,7 +1079,7 @@ function migrate_example_advanced_data_categories() { */ function migrate_example_advanced_data_vintages() { $fields = ['wineid', 'vintage']; - $query = db_insert('migrate_example_advanced_vintages') + $query = \Drupal::database()->insert('migrate_example_advanced_vintages') ->fields($fields); $data = [ [1, 2006], @@ -1097,7 +1097,7 @@ function migrate_example_advanced_data_vintages() { */ function migrate_example_advanced_data_variety_updates() { $fields = ['categoryid', 'details']; - $query = db_insert('migrate_example_advanced_variety_updates') + $query = \Drupal::database()->insert('migrate_example_advanced_variety_updates') ->fields($fields); $data = [ [1, 'White wines are simpler and sweeter than red'], @@ -1120,7 +1120,7 @@ function migrate_example_advanced_data_variety_updates() { */ function migrate_example_advanced_data_category_wine() { $fields = ['wineid', 'categoryid']; - $query = db_insert('migrate_example_advanced_category_wine') + $query = \Drupal::database()->insert('migrate_example_advanced_category_wine') ->fields($fields); $data = [ [1, 12], @@ -1138,7 +1138,7 @@ function migrate_example_advanced_data_category_wine() { */ function migrate_example_advanced_data_category_producer() { $fields = ['producerid', 'categoryid']; - $query = db_insert('migrate_example_advanced_category_producer') + $query = \Drupal::database()->insert('migrate_example_advanced_category_producer') ->fields($fields); $data = [ [1, 17], @@ -1154,7 +1154,7 @@ function migrate_example_advanced_data_category_producer() { */ function migrate_example_advanced_data_files() { $fields = ['imageid', 'url', 'image_alt', 'image_title', 'wineid']; - $query = db_insert('migrate_example_advanced_files') + $query = \Drupal::database()->insert('migrate_example_advanced_files') ->fields($fields); $data = [ [ @@ -1192,7 +1192,7 @@ function migrate_example_advanced_data_files() { function migrate_example_advanced_data_blobs() { $blob = file_get_contents('core/misc/druplicon.png'); $fields = ['imageid', 'imageblob']; - $query = db_insert('migrate_example_advanced_blobs') + $query = \Drupal::database()->insert('migrate_example_advanced_blobs') ->fields($fields); $data = [ [1, $blob], @@ -1208,7 +1208,7 @@ function migrate_example_advanced_data_blobs() { */ function migrate_example_advanced_data_table_source() { $fields = ['fooid', 'field1', 'field2']; - $query = db_insert('migrate_example_advanced_table_source') + $query = \Drupal::database()->insert('migrate_example_advanced_table_source') ->fields($fields); $data = [ [3, 'Some sample data', 58], diff --git a/web/modules/migrate_plus/migrate_json_example/README.md b/web/modules/migrate_plus/migrate_json_example/README.md new file mode 100644 index 0000000000000000000000000000000000000000..85f6f87c9118919722f8829406e191967158ac8b --- /dev/null +++ b/web/modules/migrate_plus/migrate_json_example/README.md @@ -0,0 +1,23 @@ +A demonstration of a simple import of a JSON file. + +REQUIREMENTS +============ +You need the contrib modules Migrate Plus and Migrate Tools. +To make the products.json file available for import, the file will be copied +from the artifacts folder to your sites/default/files folder. + +USAGE +===== +Enable the module, check status, import all products and rollback with Drush +drush en migrate_json_example +drush migrate-status +drush migrate-import product +drush migrate-rollback product + +See config/optional/migrate_plus.migration.product.yml for details about the +migration. + +Thanks to Jeff Geerling and Christophe for the original code: +https://www.jeffgeerling.com/blog/2016/migrate-custom-json-feed-drupal-8-migrate-source-json + +https://colorfield.be/blog/drupal-8-json-custom-migration diff --git a/web/modules/migrate_plus/migrate_json_example/artifacts/products.json b/web/modules/migrate_plus/migrate_json_example/artifacts/products.json new file mode 100644 index 0000000000000000000000000000000000000000..def2ddf29cc57af5c2bef162670ecf498c29060c --- /dev/null +++ b/web/modules/migrate_plus/migrate_json_example/artifacts/products.json @@ -0,0 +1,16 @@ +{ + "product": [ + { + "upc": "11111", + "name": "Widget", + "description": "Helpful for many things.", + "price": "14.99" + }, + { + "upc": "22222", + "name": "Sprocket", + "description": "Helpful for things needing sprockets.", + "price": "8.99" + } + ] +} diff --git a/web/modules/migrate_plus/migrate_json_example/config/optional/core.entity_form_display.node.product.default.yml b/web/modules/migrate_plus/migrate_json_example/config/optional/core.entity_form_display.node.product.default.yml new file mode 100644 index 0000000000000000000000000000000000000000..f907862075556fc1987508ef7d0d59cee2885b52 --- /dev/null +++ b/web/modules/migrate_plus/migrate_json_example/config/optional/core.entity_form_display.node.product.default.yml @@ -0,0 +1,89 @@ +langcode: en +status: true +dependencies: + config: + - field.field.node.product.field_description + - field.field.node.product.field_price + - field.field.node.product.field_upc + - node.type.product + module: + - path +id: node.product.default +targetEntityType: node +bundle: product +mode: default +content: + created: + type: datetime_timestamp + weight: 10 + region: content + settings: { } + third_party_settings: { } + field_description: + weight: 123 + settings: + size: 60 + placeholder: '' + third_party_settings: { } + type: string_textfield + region: content + field_price: + weight: 124 + settings: + placeholder: '' + third_party_settings: { } + type: number + region: content + field_upc: + weight: 122 + settings: + placeholder: '' + third_party_settings: { } + type: number + region: content + path: + type: path + weight: 30 + region: content + settings: { } + third_party_settings: { } + promote: + type: boolean_checkbox + settings: + display_label: true + weight: 15 + region: content + third_party_settings: { } + status: + type: boolean_checkbox + settings: + display_label: true + weight: 120 + region: content + third_party_settings: { } + sticky: + type: boolean_checkbox + settings: + display_label: true + weight: 16 + region: content + third_party_settings: { } + title: + type: string_textfield + weight: -5 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } + uid: + type: entity_reference_autocomplete + weight: 5 + settings: + match_operator: CONTAINS + match_limit: 10 + size: 60 + placeholder: '' + region: content + third_party_settings: { } +hidden: { } diff --git a/web/modules/migrate_plus/migrate_json_example/config/optional/core.entity_view_display.node.product.default.yml b/web/modules/migrate_plus/migrate_json_example/config/optional/core.entity_view_display.node.product.default.yml new file mode 100644 index 0000000000000000000000000000000000000000..9e7a9f23cedc30e1bfc6c3fb887fca409143d255 --- /dev/null +++ b/web/modules/migrate_plus/migrate_json_example/config/optional/core.entity_view_display.node.product.default.yml @@ -0,0 +1,49 @@ +langcode: en +status: true +dependencies: + config: + - field.field.node.product.field_description + - field.field.node.product.field_price + - field.field.node.product.field_upc + - node.type.product + module: + - user +id: node.product.default +targetEntityType: node +bundle: product +mode: default +content: + field_description: + weight: 103 + label: above + settings: + link_to_entity: false + third_party_settings: { } + type: string + region: content + field_price: + weight: 104 + label: above + settings: + thousand_separator: '' + decimal_separator: . + scale: 2 + prefix_suffix: true + third_party_settings: { } + type: number_decimal + region: content + field_upc: + weight: 102 + label: above + settings: + thousand_separator: '' + prefix_suffix: true + third_party_settings: { } + type: number_integer + region: content + links: + weight: 100 + settings: { } + third_party_settings: { } + region: content +hidden: { } diff --git a/web/modules/migrate_plus/migrate_json_example/config/optional/field.field.node.product.field_description.yml b/web/modules/migrate_plus/migrate_json_example/config/optional/field.field.node.product.field_description.yml new file mode 100644 index 0000000000000000000000000000000000000000..7bbbd8419a61e0c53f1a2217d469e13661b2b726 --- /dev/null +++ b/web/modules/migrate_plus/migrate_json_example/config/optional/field.field.node.product.field_description.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.field_description + - node.type.product +id: node.product.field_description +field_name: field_description +entity_type: node +bundle: product +label: Description +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: string diff --git a/web/modules/migrate_plus/migrate_json_example/config/optional/field.field.node.product.field_price.yml b/web/modules/migrate_plus/migrate_json_example/config/optional/field.field.node.product.field_price.yml new file mode 100644 index 0000000000000000000000000000000000000000..df23a84ae3433716ea53fde3c8b134cdd7149038 --- /dev/null +++ b/web/modules/migrate_plus/migrate_json_example/config/optional/field.field.node.product.field_price.yml @@ -0,0 +1,22 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.field_price + - node.type.product +id: node.product.field_price +field_name: field_price +entity_type: node +bundle: product +label: Price +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + min: null + max: null + prefix: '' + suffix: '' +field_type: float diff --git a/web/modules/migrate_plus/migrate_json_example/config/optional/field.field.node.product.field_upc.yml b/web/modules/migrate_plus/migrate_json_example/config/optional/field.field.node.product.field_upc.yml new file mode 100644 index 0000000000000000000000000000000000000000..ef0a28ec4f463c2f3e4d7d14a1b4270b377c572f --- /dev/null +++ b/web/modules/migrate_plus/migrate_json_example/config/optional/field.field.node.product.field_upc.yml @@ -0,0 +1,22 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.field_upc + - node.type.product +id: node.product.field_upc +field_name: field_upc +entity_type: node +bundle: product +label: UPC +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + min: null + max: null + prefix: '' + suffix: '' +field_type: integer diff --git a/web/modules/migrate_plus/migrate_json_example/config/optional/field.storage.node.field_description.yml b/web/modules/migrate_plus/migrate_json_example/config/optional/field.storage.node.field_description.yml new file mode 100644 index 0000000000000000000000000000000000000000..a5c39580deed6288f8bbf19b46a45411531546a8 --- /dev/null +++ b/web/modules/migrate_plus/migrate_json_example/config/optional/field.storage.node.field_description.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + module: + - node +id: node.field_description +field_name: field_description +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 diff --git a/web/modules/migrate_plus/migrate_json_example/config/optional/field.storage.node.field_price.yml b/web/modules/migrate_plus/migrate_json_example/config/optional/field.storage.node.field_price.yml new file mode 100644 index 0000000000000000000000000000000000000000..090acad3d9c56acf9992803b94d3c2e6b56c7c01 --- /dev/null +++ b/web/modules/migrate_plus/migrate_json_example/config/optional/field.storage.node.field_price.yml @@ -0,0 +1,17 @@ +langcode: en +status: true +dependencies: + module: + - node +id: node.field_price +field_name: field_price +entity_type: node +type: float +settings: { } +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/web/modules/migrate_plus/migrate_json_example/config/optional/field.storage.node.field_upc.yml b/web/modules/migrate_plus/migrate_json_example/config/optional/field.storage.node.field_upc.yml new file mode 100644 index 0000000000000000000000000000000000000000..149b11da10109edb40ec787f0773d84092efffac --- /dev/null +++ b/web/modules/migrate_plus/migrate_json_example/config/optional/field.storage.node.field_upc.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + module: + - node +id: node.field_upc +field_name: field_upc +entity_type: node +type: integer +settings: + unsigned: false + size: normal +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/web/modules/migrate_plus/migrate_json_example/config/optional/migrate_plus.migration.product.yml b/web/modules/migrate_plus/migrate_json_example/config/optional/migrate_plus.migration.product.yml new file mode 100644 index 0000000000000000000000000000000000000000..16607249464ae612e819c7ab7b4ec01e18b5ce9f --- /dev/null +++ b/web/modules/migrate_plus/migrate_json_example/config/optional/migrate_plus.migration.product.yml @@ -0,0 +1,81 @@ +# This migration demonstrates a simple import from a JSON file. +id: product +label: JSON feed of Products +migration_group: Product +migration_tags: + - json example +source: + # We use the JSON source plugin. + plugin: url + # In this example we get data from a local file, to get data from a URL + # define http as data_fetcher_plugin. + # data_fetcher_plugin: http + data_fetcher_plugin: file + data_parser_plugin: json + # The data_parser normally limits the fields passed on to the source plugin + # to fields configured to be used as part of the migration. To support more + # dynamic migrations, the JSON data parser supports including the original + # data for the current row. Simply include the 'include_raw_data' flag set + # to `true` to enable this. This option is disabled by default to minimize + # memory footprint for migrations that do not need this capability. + # include_raw_data: true + # Flags whether to track changes to incoming data. If TRUE, we will maintain + # hashed source rows to determine whether incoming data has changed. + # track_changes: true + # Copy the example JSON file in artifacts folder to sites/default/files folder. + urls: + - 'public://migrate_json_example/products.json' + # An xpath-like selector corresponding to the items to be imported. + item_selector: product + # Under 'fields', we list the data items to be imported. The first level keys + # are the source field names we want to populate (the names to be used as + # sources in the process configuration below). For each field we're importing, + # we provide a label (optional - this is for display in migration tools) and + # an xpath for retrieving that value. It's important to note that this xpath + # is relative to the elements retrieved by item_selector. + fields: + - + name: upc + label: 'Unique product identifier' + selector: upc + - + name: name + label: 'Product name' + selector: name + - + name: description + label: 'Product description' + selector: description + - + name: price + label: 'Product price' + selector: price + # Under 'ids', we identify source fields populated above which will uniquely + # identify each imported item. The 'type' makes sure the migration map table + # uses the proper schema type for stored the IDs. + ids: + upc: + type: integer +process: + # Note that the source field names here (name, description and price) were + # defined by the 'fields' configuration for the source plugin above. + type: + plugin: default_value + default_value: product + title: name + field_upc: upc + field_description: description + field_price: price + sticky: + plugin: default_value + default_value: 0 + uid: + plugin: default_value + default_value: 0 +destination: + plugin: 'entity:node' +migration_dependencies: { } +dependencies: + enforced: + module: + - migrate_json_example diff --git a/web/modules/migrate_plus/migrate_json_example/config/optional/node.type.product.yml b/web/modules/migrate_plus/migrate_json_example/config/optional/node.type.product.yml new file mode 100644 index 0000000000000000000000000000000000000000..18d8499de906378faa77aa9a97e963a493af162a --- /dev/null +++ b/web/modules/migrate_plus/migrate_json_example/config/optional/node.type.product.yml @@ -0,0 +1,17 @@ +langcode: en +status: true +dependencies: + module: + - menu_ui +third_party_settings: + menu_ui: + available_menus: + - main + parent: 'main:' +name: Product +type: product +description: '' +help: '' +new_revision: true +preview_mode: 1 +display_submitted: true diff --git a/web/modules/migrate_plus/migrate_json_example/migrate_json_example.info.yml b/web/modules/migrate_plus/migrate_json_example/migrate_json_example.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..631788117ac086de800de425fec673bca6c4ad7b --- /dev/null +++ b/web/modules/migrate_plus/migrate_json_example/migrate_json_example.info.yml @@ -0,0 +1,13 @@ +type: module +name: Migrate JSON Example +description: 'Simple JSON Migration example' +package: Examples +core_version_requirement: ^8.8 || ^9 +dependencies: + - drupal:migrate + - migrate_plus:migrate_plus + +# Information added by Drupal.org packaging script on 2020-04-30 +version: '8.x-5.1' +project: 'migrate_plus' +datestamp: 1588261062 diff --git a/web/modules/migrate_plus/migrate_json_example/migrate_json_example.install b/web/modules/migrate_plus/migrate_json_example/migrate_json_example.install new file mode 100644 index 0000000000000000000000000000000000000000..a038a19113e56688d188020dd24b2854b1f24606 --- /dev/null +++ b/web/modules/migrate_plus/migrate_json_example/migrate_json_example.install @@ -0,0 +1,22 @@ +<?php + +/** + * @file + * Install, update, and uninstall functions for migrate_json_example. + */ + +use Drupal\Core\File\FileSystemInterface; + +/** + * Copies the example file to the sites/default/files folder. + */ +function migrate_json_example_install() { + // Create the example file directory and ensure it's writable. + $directory = \Drupal::config('system.file')->get('default_scheme') . '://migrate_json_example'; + \Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); + + // Copy the example file to example directory. + $module_path = drupal_get_path('module', 'migrate_json_example'); + $file_source = $module_path . '/artifacts/products.json'; + \Drupal::service('file_system')->copy($file_source, $directory . '/products.json', FileSystemInterface::EXISTS_REPLACE); +} diff --git a/web/modules/migrate_plus/migrate_plus.info.yml b/web/modules/migrate_plus/migrate_plus.info.yml index c5c0e29c67a6c8c57c7746586b8ec51434e16dd7..f23cac5e66e659697ccaeea8118309a8a59d1b4e 100644 --- a/web/modules/migrate_plus/migrate_plus.info.yml +++ b/web/modules/migrate_plus/migrate_plus.info.yml @@ -2,12 +2,11 @@ type: module name: Migrate Plus description: 'Enhancements to core migration support' package: Migration -# core: 8.x +core_version_requirement: ^8.8 || ^9 dependencies: - - drupal:migrate (>=8.3) + - drupal:migrate -# Information added by Drupal.org packaging script on 2018-09-06 -version: '8.x-4.0' -core: '8.x' +# Information added by Drupal.org packaging script on 2020-04-30 +version: '8.x-5.1' project: 'migrate_plus' -datestamp: 1536264189 +datestamp: 1588261062 diff --git a/web/modules/migrate_plus/migrate_plus.module b/web/modules/migrate_plus/migrate_plus.module index 0dbddc473585b414b9e32191b7f2164e098f9022..6cb976c4dd4b529501874fcdba9cabecc0ed65c4 100644 --- a/web/modules/migrate_plus/migrate_plus.module +++ b/web/modules/migrate_plus/migrate_plus.module @@ -5,8 +5,9 @@ * Provides enhancements for implementing and managing migrations. */ -use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Plugin\MigrateSourceInterface; +use Drupal\migrate\Plugin\Migration; +use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Row; use Drupal\migrate_plus\Entity\MigrationGroup; use Drupal\migrate_plus\Event\MigrateEvents; @@ -16,11 +17,10 @@ * Implements hook_migration_plugins_alter(). */ function migrate_plus_migration_plugins_alter(array &$migrations) { - /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ - foreach ($migrations as $id => $migration) { + foreach (array_keys($migrations) as $id) { // Add the default class where empty. - if (empty($migration['class'])) { - $migrations[$id]['class'] = 'Drupal\migrate\Plugin\Migration'; + if (empty($migrations[$id]['class'])) { + $migrations[$id]['class'] = Migration::class; } // For derived configuration entity-based migrations, strip the deriver @@ -36,16 +36,16 @@ function migrate_plus_migration_plugins_alter(array &$migrations) { } // Integrate shared group configuration into the migration. - if (empty($migration['migration_group'])) { - $migration['migration_group'] = 'default'; + if (empty($migrations[$id]['migration_group'])) { + $migrations[$id]['migration_group'] = 'default'; } - $group = MigrationGroup::load($migration['migration_group']); + $group = MigrationGroup::load($migrations[$id]['migration_group']); if (empty($group)) { // If the specified group does not exist, create it. Provide a little more // for the 'default' group. $group_properties = []; - $group_properties['id'] = $migration['migration_group']; - if ($migration['migration_group'] == 'default') { + $group_properties['id'] = $migrations[$id]['migration_group']; + if ($migrations[$id]['migration_group'] == 'default') { $group_properties['label'] = 'Default'; $group_properties['description'] = 'A container for any migrations not explicitly assigned to a group.'; } @@ -61,7 +61,7 @@ function migrate_plus_migration_plugins_alter(array &$migrations) { continue; } foreach ($shared_configuration as $key => $group_value) { - $migration_value = $migration[$key]; + $migration_value = $migrations[$id][$key]; // Where both the migration and the group provide arrays, replace // recursively (so each key collision is resolved in favor of the // migration). diff --git a/web/modules/migrate_plus/phpcs.xml b/web/modules/migrate_plus/phpcs.xml deleted file mode 100644 index a18054efbc2dc47c2595364bb13a992a0fffbb4a..0000000000000000000000000000000000000000 --- a/web/modules/migrate_plus/phpcs.xml +++ /dev/null @@ -1,207 +0,0 @@ -<?xml version="1.0"?> -<ruleset name="Drupal coding standards"> - <description>Drupal 8 coding standards</description> - - <file>.</file> - <arg name="extensions" value="inc,install,module,php,profile,test,theme"/> - - <!--Exclude third party code.--> - <exclude-pattern>./vendor/*</exclude-pattern> - <!--Run Drupal standards.--> - <rule ref="Drupal.Array"/> - <rule ref="Drupal.Classes"/> - <rule ref="Drupal.Commenting"> - <!-- TagsNotGrouped and ParamGroup have false-positives. - @see https://www.drupal.org/node/2060925 --> - <exclude name="Drupal.Commenting.DocComment.TagsNotGrouped"/> - <exclude name="Drupal.Commenting.DocComment.ParamGroup"/> - </rule> - <rule ref="Drupal.ControlStructures"/> - <rule ref="Drupal.CSS"/> - <rule ref="Drupal.Files"/> - <rule ref="Drupal.Formatting"/> - <rule ref="Drupal.Functions"/> - <rule ref="Drupal.InfoFiles"/> - <rule ref="Drupal.Methods"/> - <rule ref="Drupal.NamingConventions"/> - <rule ref="Drupal.Scope"/> - <rule ref="Drupal.Semantics"/> - <rule ref="Drupal.Strings"/> - <rule ref="Drupal.WhiteSpace"/> - - <!-- Drupal Practice sniffs --> - <rule ref="DrupalPractice.Commenting"/> - - <!-- Generic sniffs --> - <rule ref="Generic.Arrays.DisallowLongArraySyntax"/> - <rule ref="Generic.Files.ByteOrderMark"/> - <rule ref="Generic.Files.LineEndings"/> - <rule ref="Generic.Formatting.SpaceAfterCast"/> - <rule ref="Generic.Functions.FunctionCallArgumentSpacing"/> - <rule ref="Generic.Functions.OpeningFunctionBraceKernighanRitchie"> - <properties> - <property name="checkClosures" value="true"/> - </properties> - </rule> - <rule ref="Generic.NamingConventions.ConstructorName"/> - <rule ref="Generic.NamingConventions.UpperCaseConstantName"/> - <rule ref="Generic.PHP.DeprecatedFunctions"/> - <rule ref="Generic.PHP.DisallowShortOpenTag"/> - <rule ref="Generic.PHP.LowerCaseKeyword"/> - <rule ref="Generic.PHP.UpperCaseConstant"/> - <rule ref="Generic.WhiteSpace.DisallowTabIndent"/> - - <!-- MySource sniffs --> - <rule ref="MySource.Debug.DebugCode"/> - - <!-- PEAR sniffs --> - <rule ref="PEAR.Files.IncludingFile"/> - <!-- Disable some error messages that we do not want. --> - <rule ref="PEAR.Files.IncludingFile.UseIncludeOnce"> - <severity>0</severity> - </rule> - <rule ref="PEAR.Files.IncludingFile.UseInclude"> - <severity>0</severity> - </rule> - <rule ref="PEAR.Files.IncludingFile.UseRequireOnce"> - <severity>0</severity> - </rule> - <rule ref="PEAR.Files.IncludingFile.UseRequire"> - <severity>0</severity> - </rule> - <rule ref="PEAR.Functions.ValidDefaultValue"/> - - <!-- PEAR sniffs --> - <rule ref="PEAR.Functions.FunctionCallSignature"/> - <!-- The sniffs inside PEAR.Functions.FunctionCallSignature silenced below are - also silenced in Drupal CS' ruleset.xml. The code below is a 1-on-1 copy - from that file. --> - <!-- Disable some error messages that we already cover. --> - <rule ref="PEAR.Functions.FunctionCallSignature.SpaceAfterOpenBracket"> - <severity>0</severity> - </rule> - <rule ref="PEAR.Functions.FunctionCallSignature.SpaceBeforeCloseBracket"> - <severity>0</severity> - </rule> - <!-- Disable some error messages that we do not want. --> - <rule ref="PEAR.Functions.FunctionCallSignature.Indent"> - <severity>0</severity> - </rule> - <rule ref="PEAR.Functions.FunctionCallSignature.ContentAfterOpenBracket"> - <severity>0</severity> - </rule> - <rule ref="PEAR.Functions.FunctionCallSignature.CloseBracketLine"> - <severity>0</severity> - </rule> - <rule ref="PEAR.Functions.FunctionCallSignature.EmptyLine"> - <severity>0</severity> - </rule> - - <!-- PSR-2 sniffs --> - <rule ref="PSR2.Classes.PropertyDeclaration"> - <exclude name="PSR2.Classes.PropertyDeclaration.Underscore"/> - </rule> - <rule ref="PSR2.Namespaces.NamespaceDeclaration"/> - <rule ref="PSR2.Namespaces.UseDeclaration"> - <exclude name="PSR2.Namespaces.UseDeclaration.UseAfterNamespace"/> - </rule> - - <!-- Squiz sniffs --> - <rule ref="Squiz.Arrays.ArrayBracketSpacing"/> - <rule ref="Squiz.Arrays.ArrayDeclaration"> - <exclude name="Squiz.Arrays.ArrayDeclaration.NoKeySpecified"/> - <exclude name="Squiz.Arrays.ArrayDeclaration.KeySpecified"/> - </rule> - <!-- Disable some error messages that we do not want. --> - <rule ref="Squiz.Arrays.ArrayDeclaration.CloseBraceNotAligned"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Arrays.ArrayDeclaration.DoubleArrowNotAligned"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Arrays.ArrayDeclaration.FirstValueNoNewline"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Arrays.ArrayDeclaration.KeyNotAligned"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Arrays.ArrayDeclaration.MultiLineNotAllowed"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Arrays.ArrayDeclaration.NoComma"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Arrays.ArrayDeclaration.NoCommaAfterLast"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Arrays.ArrayDeclaration.NotLowerCase"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Arrays.ArrayDeclaration.SingleLineNotAllowed"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Arrays.ArrayDeclaration.ValueNotAligned"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Arrays.ArrayDeclaration.ValueNoNewline"> - <severity>0</severity> - </rule> - <rule ref="Squiz.ControlStructures.ForEachLoopDeclaration"/> - <!-- Disable some error messages that we already cover. --> - <rule ref="Squiz.ControlStructures.ForEachLoopDeclaration.AsNotLower"> - <severity>0</severity> - </rule> - <rule ref="Squiz.ControlStructures.ForEachLoopDeclaration.SpaceAfterOpen"> - <severity>0</severity> - </rule> - <rule ref="Squiz.ControlStructures.ForEachLoopDeclaration.SpaceBeforeClose"> - <severity>0</severity> - </rule> - <rule ref="Squiz.ControlStructures.ForLoopDeclaration"/> - <!-- Disable some error messages that we already cover. --> - <rule ref="Squiz.ControlStructures.ForLoopDeclaration.SpacingAfterOpen"> - <severity>0</severity> - </rule> - <rule ref="Squiz.ControlStructures.ForLoopDeclaration.SpacingBeforeClose"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Functions.MultiLineFunctionDeclaration"/> - <rule ref="Squiz.Functions.MultiLineFunctionDeclaration.BraceOnSameLine"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Functions.MultiLineFunctionDeclaration.ContentAfterBrace"> - <severity>0</severity> - </rule> - <!-- Standard yet to be finalized on this (https://www.drupal.org/node/1539712). --> - <rule ref="Squiz.Functions.MultiLineFunctionDeclaration.FirstParamSpacing"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Functions.MultiLineFunctionDeclaration.Indent"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Functions.MultiLineFunctionDeclaration.CloseBracketLine"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing"> - <properties> - <property name="equalsSpacing" value="1"/> - </properties> - </rule> - <rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing.NoSpaceBeforeArg"> - <severity>0</severity> - </rule> - <rule ref="Squiz.PHP.LowercasePHPFunctions"/> - <rule ref="Squiz.Strings.ConcatenationSpacing"> - <properties> - <property name="spacing" value="1"/> - <property name="ignoreNewlines" value="true"/> - </properties> - </rule> - <rule ref="Squiz.WhiteSpace.LanguageConstructSpacing" /> - <rule ref="Squiz.WhiteSpace.SemicolonSpacing"/> - <rule ref="Squiz.WhiteSpace.SuperfluousWhitespace"/> - - <!-- Zend sniffs --> - <rule ref="Zend.Files.ClosingTag"/> - -</ruleset> diff --git a/web/modules/migrate_plus/src/DataFetcherPluginBase.php b/web/modules/migrate_plus/src/DataFetcherPluginBase.php index 9e413fe106a234a07846408f35fc1dfcbc2c84d6..eb21f301da43f74e05bf93dc8b00ca41627caad8 100644 --- a/web/modules/migrate_plus/src/DataFetcherPluginBase.php +++ b/web/modules/migrate_plus/src/DataFetcherPluginBase.php @@ -15,13 +15,6 @@ */ abstract class DataFetcherPluginBase extends PluginBase implements DataFetcherPluginInterface { - /** - * {@inheritdoc} - */ - public function __construct(array $configuration, $plugin_id, $plugin_definition) { - parent::__construct($configuration, $plugin_id, $plugin_definition); - } - /** * {@inheritdoc} */ diff --git a/web/modules/migrate_plus/src/DataParserPluginBase.php b/web/modules/migrate_plus/src/DataParserPluginBase.php index c74c6c53acbefb03f4812335145229d00fc3cead..3e8fd47a2cd8d1895cdc7f42779243b5066b85cf 100644 --- a/web/modules/migrate_plus/src/DataParserPluginBase.php +++ b/web/modules/migrate_plus/src/DataParserPluginBase.php @@ -80,10 +80,10 @@ public static function create(ContainerInterface $container, array $configuratio * The data fetcher plugin. */ public function getDataFetcherPlugin() { - if (!isset($this->dataFetcherPlugin)) { - $this->dataFetcherPlugin = \Drupal::service('plugin.manager.migrate_plus.data_fetcher')->createInstance($this->configuration['data_fetcher_plugin'], $this->configuration); + if (!isset($this->dataFetcher)) { + $this->dataFetcher = \Drupal::service('plugin.manager.migrate_plus.data_fetcher')->createInstance($this->configuration['data_fetcher_plugin'], $this->configuration); } - return $this->dataFetcherPlugin; + return $this->dataFetcher; } /** @@ -149,13 +149,17 @@ abstract protected function fetchNextRow(); * TRUE if a valid source URL was opened */ protected function nextSource() { + if (empty($this->urls)) { + return FALSE; + } + while ($this->activeUrl === NULL || (count($this->urls) - 1) > $this->activeUrl) { if (is_null($this->activeUrl)) { $this->activeUrl = 0; } else { // Increment the activeUrl so we try to load the next source. - $this->activeUrl = $this->activeUrl + 1; + ++$this->activeUrl; if ($this->activeUrl >= count($this->urls)) { return FALSE; } diff --git a/web/modules/migrate_plus/src/Entity/Migration.php b/web/modules/migrate_plus/src/Entity/Migration.php index 6359825cbecfa3977035b768a887a437003de912..c5a04d2ac80c388787b7c612efd43fd05837289f 100644 --- a/web/modules/migrate_plus/src/Entity/Migration.php +++ b/web/modules/migrate_plus/src/Entity/Migration.php @@ -18,8 +18,23 @@ * entity_keys = { * "id" = "id", * "label" = "label", - * "weight" = "weight" - * } + * "weight" = "weight", + * "status" = "status" + * }, + * config_export = { + * "id", + * "class", + * "field_plugin_method", + * "cck_plugin_method", + * "migration_tags", + * "migration_group", + * "status", + * "label", + * "source", + * "process", + * "destination", + * "migration_dependencies", + * }, * ) */ class Migration extends ConfigEntityBase implements MigrationInterface { @@ -57,6 +72,10 @@ protected static function invalidateTagsOnDelete(EntityTypeInterface $entity_typ /** * Create a configuration entity from a core migration plugin's configuration. * + * Note the list of properties being transplanted from the plugin instance or + * definition into the Migration config entity must remain in sync with the + * keys listed in the "config_export" annotation key of this class. + * * @param string $plugin_id * ID of a migration plugin managed by MigrationPluginManager. * @param string $new_plugin_id @@ -68,16 +87,18 @@ protected static function invalidateTagsOnDelete(EntityTypeInterface $entity_typ public static function createEntityFromPlugin($plugin_id, $new_plugin_id) { /** @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface $plugin_manager */ $plugin_manager = \Drupal::service('plugin.manager.migration'); + /** @var \Drupal\migrate\Plugin\Migration $migration_plugin */ $migration_plugin = $plugin_manager->createInstance($plugin_id); $entity_array['id'] = $new_plugin_id; - $entity_array['migration_tags'] = $migration_plugin->get('migration_tags'); + $plugin_definition = $migration_plugin->getPluginDefinition(); + $migration_details['class'] = $plugin_definition['class']; + $entity_array['migration_tags'] = $migration_plugin->getMigrationTags(); $entity_array['label'] = $migration_plugin->label(); $entity_array['source'] = $migration_plugin->getSourceConfiguration(); $entity_array['destination'] = $migration_plugin->getDestinationConfiguration(); $entity_array['process'] = $migration_plugin->getProcess(); $entity_array['migration_dependencies'] = $migration_plugin->getMigrationDependencies(); - $migration_entity = static::create($entity_array); - return $migration_entity; + return static::create($entity_array); } } diff --git a/web/modules/migrate_plus/src/Entity/MigrationGroup.php b/web/modules/migrate_plus/src/Entity/MigrationGroup.php index 519dbe24132da5b03436c281830a9a66d2325535..96845b0318db9694f97a4aa7fd867b8e1a5dbe2e 100644 --- a/web/modules/migrate_plus/src/Entity/MigrationGroup.php +++ b/web/modules/migrate_plus/src/Entity/MigrationGroup.php @@ -19,7 +19,15 @@ * entity_keys = { * "id" = "id", * "label" = "label" - * } + * }, + * config_export = { + * "id", + * "label", + * "description", + * "source_type", + * "module", + * "shared_configuration", + * }, * ) */ class MigrationGroup extends ConfigEntityBase implements MigrationGroupInterface { diff --git a/web/modules/migrate_plus/src/Event/MigrateEvents.php b/web/modules/migrate_plus/src/Event/MigrateEvents.php index 6ce44ee62e7cd8384a11f8e6d98afefd8809edbf..4d9904bb6eb0ae0b875fa450a845fa00a43d54a8 100644 --- a/web/modules/migrate_plus/src/Event/MigrateEvents.php +++ b/web/modules/migrate_plus/src/Event/MigrateEvents.php @@ -27,4 +27,17 @@ final class MigrateEvents { */ const PREPARE_ROW = 'migrate_plus.prepare_row'; + /** + * Name of the event fired when a source item is missing. + * + * This event allows modules to perform an action whenever a specific item is + * missing from the source. The event listener method receives a + * \Drupal\migrate\Event\MigrateRowDeleteEvent instance. + * + * @Event + * + * @see \Drupal\migrate\Event\MigrateRowDeleteEvent + */ + const MISSING_SOURCE_ITEM = 'migrate_plus.missing_source_item'; + } diff --git a/web/modules/migrate_plus/src/Event/MigratePrepareRowEvent.php b/web/modules/migrate_plus/src/Event/MigratePrepareRowEvent.php index 4727f3ee376ce4d6c7fcfde64a4e9e4e5ca09f8f..0534a20ae7a23a94450ee6365365324558943fce 100644 --- a/web/modules/migrate_plus/src/Event/MigratePrepareRowEvent.php +++ b/web/modules/migrate_plus/src/Event/MigratePrepareRowEvent.php @@ -2,8 +2,8 @@ namespace Drupal\migrate_plus\Event; -use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Plugin\MigrateSourceInterface; +use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Row; use Symfony\Component\EventDispatcher\Event; @@ -38,10 +38,8 @@ class MigratePrepareRowEvent extends Event { * * @param \Drupal\migrate\Row $row * Row of source data to be analyzed/manipulated. - * * @param \Drupal\migrate\Plugin\MigrateSourceInterface $source * Source plugin that is the source of the event. - * * @param \Drupal\migrate\Plugin\MigrationInterface $migration * Migration entity. */ diff --git a/web/modules/migrate_plus/src/Plugin/MigrationConfigDeriver.php b/web/modules/migrate_plus/src/Plugin/MigrationConfigDeriver.php index 0135444974f3da4f93e587d787563b8977e2c0e6..d66892128881e6b5db6124e22f2192938ce12185 100644 --- a/web/modules/migrate_plus/src/Plugin/MigrationConfigDeriver.php +++ b/web/modules/migrate_plus/src/Plugin/MigrationConfigDeriver.php @@ -20,6 +20,10 @@ public function getDerivativeDefinitions($base_plugin_definition) { $migrations = Migration::loadMultiple(); /** @var \Drupal\migrate_plus\Entity\MigrationInterface $migration */ foreach ($migrations as $id => $migration) { + if (!$migration->status()) { + continue; + } + $this->derivatives[$id] = $migration->toArray(); } return $this->derivatives; diff --git a/web/modules/migrate_plus/src/Plugin/migrate/destination/Table.php b/web/modules/migrate_plus/src/Plugin/migrate/destination/Table.php index f04d9f66d75f9bcb5933445e8b25fdea3d1d28ac..00d24159e8f2a0379ef943aa465a48c693fafa85 100644 --- a/web/modules/migrate_plus/src/Plugin/migrate/destination/Table.php +++ b/web/modules/migrate_plus/src/Plugin/migrate/destination/Table.php @@ -5,10 +5,12 @@ use Drupal\Core\Database\Connection; use Drupal\Core\Database\Database; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\migrate\Event\ImportAwareInterface; +use Drupal\migrate\Event\MigrateImportEvent; use Drupal\migrate\MigrateException; use Drupal\migrate\MigrateSkipProcessException; -use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Plugin\migrate\destination\DestinationBase; +use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Row; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -17,11 +19,58 @@ * * Use this plugin for a table not registered with Drupal Schema API. * + * Examples: + * + * @code + * destination: + * plugin: table + * # Key for the database connection to use for inserting records. + * database_key: roads_db + * # DB table for storage. + * table_name: roads + * # Maximum number of rows to insert in one query. + * batch_size: 3 + * # Fields used by migrate to identify table rows uniquely. At least one + * # field is required. + * id_fields: + * name: + * type: string + * suburb: + * type: string + * ward: + * type: string + * # Mapping of column names to values set in migrate process. + * fields: + * name: name + * owner: owner + * suburb: suburb + * ward: ward + * type: type + * @endcode + * + * For numeric id fields, migrate can generate the values on-the-fly, by + * enabling use_auto_increment; in such case, the id field may be ommitted from + * the 'fields' section: + * + * @code + * destination: + * plugin: table + * # ... + * id_fields: + * my_id_field: + * type: integer + * use_auto_increment: true + * # ... + * fields: + * non_my_id_field_1: non_my_id_field_1 + * non_my_id_field_2: non_my_id_field_2 + * @endcode + * * @MigrateDestination( * id = "table" * ) */ -class Table extends DestinationBase implements ContainerFactoryPluginInterface { +class Table extends DestinationBase implements ContainerFactoryPluginInterface, ImportAwareInterface { /** * The name of the destination table. @@ -51,6 +100,27 @@ class Table extends DestinationBase implements ContainerFactoryPluginInterface { */ protected $dbConnection; + /** + * Maximum number of rows to insert in one query. + * + * @var int + */ + protected $batchSize; + + /** + * The query object being built row-by-row. + * + * @var array + */ + protected $rowsToInsert = []; + + /** + * The highest ID seen or created so far on this table. + * + * @var int + */ + protected $lastId = 0; + /** * Constructs a new Table. * @@ -71,6 +141,7 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition $this->tableName = $configuration['table_name']; $this->idFields = $configuration['id_fields']; $this->fields = isset($configuration['fields']) ? $configuration['fields'] : []; + $this->batchSize = isset($configuration['batch_size']) ? $configuration['batch_size'] : 1; $this->supportsRollback = TRUE; } @@ -79,7 +150,6 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) { $db_key = !empty($configuration['database_key']) ? $configuration['database_key'] : NULL; - return new static( $configuration, $plugin_id, @@ -110,23 +180,79 @@ public function fields(MigrationInterface $migration = NULL) { * {@inheritdoc} */ public function import(Row $row, array $old_destination_id_values = []) { - $id = $row->getSourceIdValues(); - if (count($id) != count($this->idFields)) { - throw new MigrateSkipProcessException('All the id fields are required for a table migration.'); + // Skip batching (if configured) for updates. + $batch_inserts = ($this->batchSize > 1 && empty($old_destination_id_values)); + $ids = []; + foreach ($this->idFields as $field => $fieldInfo) { + if ($row->hasDestinationProperty($field)) { + $ids[$field] = $row->getDestinationProperty($field); + } + elseif (!$row->hasDestinationProperty($field) && empty($fieldInfo['use_auto_increment'])) { + throw new MigrateSkipProcessException('All the id fields are required for a table migration.'); + } + // When batching, we do the auto-incrementing ourselves. + elseif ($batch_inserts && $fieldInfo['use_auto_increment']) { + if (count($this->rowsToInsert) === 0) { + // Get the highest existing ID, so we will create IDs above it. + $this->lastId = $this->dbConnection->query("SELECT MAX($field) AS MaxId FROM {{$this->tableName}}") + ->fetchField(); + if (!$this->lastId) { + $this->lastId = 0; + } + } + $id = ++$this->lastId; + $ids[$field] = $id; + $row->setDestinationProperty($field, $id); + } } - $values = $row->getDestination(); + // When batching, make sure we have the same properties in the same order + // every time. + $values = []; + if ($batch_inserts) { + $destination_properties = array_keys($this->migration->getProcess()); + $destination_properties = array_merge($destination_properties, + array_keys($this->idFields)); + sort($destination_properties); + $destination_values = $row->getDestination(); + foreach ($destination_properties as $property_name) { + $values[$property_name] = $destination_values[$property_name] ?? NULL; + } + } + else { + $values = $row->getDestination(); + } if ($this->fields) { $values = array_intersect_key($values, $this->fields); } - $status = $this->dbConnection->merge($this->tableName) - ->key($id) - ->fields($values) - ->execute(); - - return $status ? $id : NULL; + if ($batch_inserts) { + $this->rowsToInsert[] = $values; + if (count($this->rowsToInsert) >= $this->batchSize) { + $this->flushInserts(); + } + $status = TRUE; + } + // Row contains empty id field with use_auto_increment enabled. + elseif (count($ids) < count($this->idFields)) { + $status = $id = $this->dbConnection->insert($this->tableName) + ->fields($values) + ->execute(); + foreach ($this->idFields as $field => $fieldInfo) { + if (isset($fieldInfo['use_auto_increment']) && $fieldInfo['use_auto_increment'] === TRUE && !$row->hasDestinationProperty($field)) { + $row->setDestinationProperty($field, $id); + $ids[$field] = $id; + } + } + } + else { + $status = $this->dbConnection->merge($this->tableName) + ->keys($ids) + ->fields($values) + ->execute(); + } + return $status ? $ids : NULL; } /** @@ -140,4 +266,43 @@ public function rollback(array $destination_identifier) { $delete->execute(); } + /** + * Execute the insert query and reset everything. + */ + public function flushInserts() { + if (count($this->rowsToInsert) > 0) { + $batch_query = $this->dbConnection->insert($this->tableName) + ->fields(array_keys($this->rowsToInsert[0])); + foreach ($this->rowsToInsert as $row) { + $batch_query->values(array_values($row)); + } + // Empty the queue first, so if the statement throws an error we don't + // end up here trying to execute the same statement (plus one row). + $this->rowsToInsert = []; + $batch_query->execute(); + } + } + + /** + * {@inheritDoc} + */ + public function preImport(MigrateImportEvent $event) { + } + + /** + * {@inheritDoc} + */ + public function postImport(MigrateImportEvent $event) { + // At the conclusion of a given migration, make sure batched inserts go in. + $this->flushInserts(); + } + + /** + * Make absolutely sure batched inserts are processed (especially for stubs). + */ + public function __destruct() { + // At the conclusion of a given migration, make sure batched inserts go in. + $this->flushInserts(); + } + } diff --git a/web/modules/migrate_plus/src/Plugin/migrate/process/DefaultEntityValue.php b/web/modules/migrate_plus/src/Plugin/migrate/process/DefaultEntityValue.php new file mode 100644 index 0000000000000000000000000000000000000000..78ef4cadd4682870074038db276e237c1b305392 --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/DefaultEntityValue.php @@ -0,0 +1,46 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate\process; + +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\Row; + +/** + * Returns EntityLookup for a given default value if input is empty. + * + * @see \Drupal\migrate_plus\Plugin\migrate\process\EntityLookup + * + * Example usage with full configuration: + * @code + * process: + * uid: + * - + * plugin: migration_lookup + * migration: users + * source: author + * - + * plugin: default_entity_value + * entity_type: user + * value_key: name + * ignore_case: true + * default_value: editorial + * @endcode + * + * @MigrateProcessPlugin( + * id = "default_entity_value", + * handle_multiples = TRUE + * ) + */ +class DefaultEntityValue extends EntityLookup { + + /** + * {@inheritdoc} + */ + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + if (!empty($value)) { + return $value; + } + return parent::transform($this->configuration['default_value'], $migrate_executable, $row, $destination_property); + } + +} diff --git a/web/modules/migrate_plus/src/Plugin/migrate/process/Dom.php b/web/modules/migrate_plus/src/Plugin/migrate/process/Dom.php new file mode 100644 index 0000000000000000000000000000000000000000..1d1c50fbb766d0b4e5e9cbae88b2bddb307eb7d1 --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/Dom.php @@ -0,0 +1,233 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate\process; + +use Drupal\Component\Utility\Html; +use Drupal\migrate\MigrateException; +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\Plugin\MigrationInterface; +use Drupal\migrate\ProcessPluginBase; +use Drupal\migrate\Row; + +/** + * Handles string to DOM and back conversions. + * + * Available configuration keys: + * - method: Action to perform. Possible values: + * - import: string to DomDocument. + * - export: DomDocument to string. + * - non_root: (optional) Assume the passed HTML is not a complete hierarchy, + * but only a subset inside body element. Defaults to true. + * + * The following keys are only used if the method is 'import': + * - log_messages: (optional) When parsing HTML, libxml may trigger + * warnings. If this option is set to true, it will log them as migration + * messages. Otherwise, it will not handle it in a special way. Defaults to + * true. + * - version: (optional) The version number of the document as part of the XML + * declaration. Defaults to '1.0'. + * - encoding: (optional) The encoding of the document as part of the XML + * declaration. Defaults to 'UTF-8'. + * + * @codingStandardsIgnoreStart + * + * Examples: + * @code + * process: + * 'body/value': + * - + * plugin: dom + * method: import + * source: 'body/0/value' + * - + * plugin: dom + * method: export + * @endcode + * This example above will convert the input string to a DOMDocument object and + * back, with no explicit processing. It should have few noticeable effects. + * + * @code + * process: + * 'body/value': + * - + * plugin: dom + * method: import + * source: 'body/0/value' + * non_root: true + * log_messages: true + * version: '1.0' + * encoding: UTF-8 + * - + * plugin: dom + * method: export + * non_root: true + * @endcode + * This example above will have the same effect as the previous example, since + * it specifies the default values for all the optional parameters. + * + * @codingStandardsIgnoreEnd + * + * @MigrateProcessPlugin( + * id = "dom" + * ) + */ +class Dom extends ProcessPluginBase { + + /** + * If parsing warnings should be logged as migrate messages. + * + * @var bool + */ + protected $logMessages = TRUE; + + /** + * The HTML contains only the piece inside the body element. + * + * @var bool + */ + protected $nonRoot = TRUE; + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition) { + if (!isset($configuration['method'])) { + throw new \InvalidArgumentException('The "method" must be set.'); + } + if (!in_array($configuration['method'], ['import', 'export'])) { + throw new \InvalidArgumentException('The "method" must be "import" or "export".'); + } + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->configuration += $this->defaultValues(); + $this->logMessages = (bool) $this->configuration['log_messages']; + $this->nonRoot = (bool) $this->configuration['non_root']; + } + + /** + * Supply default values of all optional parameters. + * + * @return array + * An array with keys the optional parameters and values the corresponding + * defaults. + */ + protected function defaultValues() { + return [ + 'non_root' => TRUE, + 'log_messages' => TRUE, + 'version' => '1.0', + 'encoding' => 'UTF-8', + ]; + } + + /** + * Converts a HTML string into a DOMDocument. + * + * It is not using \Drupal\Component\Utility\Html::load() because it ignores + * all errors on import, and therefore incompatible with log_messages + * option. + * + * @param mixed $value + * The string to be imported. + * @param \Drupal\migrate\MigrateExecutableInterface $migrate_executable + * The migration in which this process is being executed. + * @param \Drupal\migrate\Row $row + * The row from the source to process. Normally, just transforming the value + * is adequate but very rarely you might need to change two columns at the + * same time or something like that. + * @param string $destination_property + * The destination property currently worked on. This is only used together + * with the $row above. + * + * @return \DOMDocument + * The document object based on the provided string. + * + * @throws \Drupal\migrate\MigrateException + * When the received $value is not a string. + */ + public function import($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + if (!is_string($value)) { + throw new MigrateException('Cannot import a non-string value.'); + } + + if ($this->logMessages) { + set_error_handler(static function ($errno, $errstr) use ($migrate_executable) { + $migrate_executable->saveMessage($errstr, MigrationInterface::MESSAGE_WARNING); + }); + } + + if ($this->nonRoot) { + $html = $this->getNonRootHtml($value); + } + else { + $html = $value; + } + + $document = new \DOMDocument($this->configuration['version'], $this->configuration['encoding']); + $document->loadHTML($html); + + if ($this->logMessages) { + restore_error_handler(); + } + + return $document; + } + + /** + * Converts a DOMDocument into a HTML string. + * + * @param mixed $value + * The document to be exported. + * @param \Drupal\migrate\MigrateExecutableInterface $migrate_executable + * The migration in which this process is being executed. + * @param \Drupal\migrate\Row $row + * The row from the source to process. Normally, just transforming the value + * is adequate but very rarely you might need to change two columns at the + * same time or something like that. + * @param string $destination_property + * The destination property currently worked on. This is only used together + * with the $row above. + * + * @return string + * The HTML string corresponding to the provided document object. + * + * @throws \Drupal\migrate\MigrateException + * When the received $value is not a \DOMDocument. + */ + public function export($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + if (!$value instanceof \DOMDocument) { + $value_description = (gettype($value) == 'object') ? get_class($value) : gettype($value); + throw new MigrateException(sprintf('Cannot export a "%s".', $value_description)); + } + + if ($this->nonRoot) { + return Html::serialize($value); + } + return $value->saveHTML(); + } + + /** + * Builds an full html string based on a partial. + * + * @param string $partial + * A subset of a full html string. For instance the contents of the body + * element. + */ + protected function getNonRootHtml($partial) { + $replacements = [ + "\n" => '', + '!encoding' => strtolower($this->configuration['encoding']), + '!value' => $partial, + ]; + // Prepend the html with a header using the configured source encoding. + // By default, loadHTML() assumes ISO-8859-1. + $html_template = <<<EOD +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head><meta http-equiv="Content-Type" content="text/html; charset=!encoding" /></head> +<body>!value</body> +</html> +EOD; + return strtr($html_template, $replacements); + } + +} diff --git a/web/modules/migrate_plus/src/Plugin/migrate/process/DomApplyStyles.php b/web/modules/migrate_plus/src/Plugin/migrate/process/DomApplyStyles.php new file mode 100644 index 0000000000000000000000000000000000000000..995085c4427cf0ffe5face7ddb5e255de7d9c2ab --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/DomApplyStyles.php @@ -0,0 +1,201 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate\process; + +use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException; +use Drupal\Core\Config\ConfigFactory; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\Plugin\MigrationInterface; +use Drupal\migrate\Row; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Apply Editor styles to configured elements. + * + * Replace HTML elements with elements and classes specified in the Styles menu + * of the WYSIWYG editor. + * + * Available configuration keys: + * - format: the text format to inspect for style options (optional, + * defaults to 'basic_html'). + * - rules: an array of keyed arrays, with the following keys: + * - xpath: an XPath expression for the elements to replace. + * - style: the label of the item in the Styles menu to use. + * - depth: the number of parent elements to remove (optional, defaults to 0). + * + * Example: + * + * @code + * process: + * 'body/value': + * - + * plugin: dom + * method: import + * source: 'body/0/value' + * - + * plugin: dom_apply_styles + * format: full_html + * rules: + * - + * xpath: '//b' + * style: Bold + * - + * xpath: '//span/i' + * style: Italic + * depth: 1 + * - + * plugin: dom + * method: export + * @endcode + * + * This will replace <b>...</b> with whatever style is labeled "Bold" in the + * Full HTML text format, perhaps <strong class="foo">...</strong>. + * It will also replace <span><i>...</i></span> with the style labeled "Italic" + * in that text format, perhaps <em class="foo bar">...</em>. + * You may get unexpected results if there is anything between the two opening + * tags or between the two closing tags. That is, the code assumes that + * '<span><i>' is closed with '</i></span>' exactly. + * + * @MigrateProcessPlugin( + * id = "dom_apply_styles" + * ) + */ +class DomApplyStyles extends DomProcessBase implements ContainerFactoryPluginInterface { + + /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactory + */ + protected $configFactory; + + /** + * Array of styles from the WYSIWYG editor. + * + * @var array + */ + protected $styles = []; + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, ConfigFactory $config_factory) { + $configuration += ['format' => 'basic_html']; + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->configFactory = $config_factory; + $this->setStyles($configuration['format']); + $this->validateRules(); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('config.factory') + ); + } + + /** + * {@inheritdoc} + */ + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + $this->init($value, $destination_property); + + foreach ($this->configuration['rules'] as $rule) { + $this->apply($rule); + } + + return $this->document; + } + + /** + * Retrieve the list of styles based on configuration. + * + * The styles configuration is a string: styles are separated by "\r\n", and + * each one has the format 'element(\.class)*|label'. + * Convert this to an array with 'label' => 'element.class', and save as + * $this->styles. + * + * @param string $format + * The text format from which to get configured styles. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + */ + protected function setStyles($format) { + if (empty($format) || !is_string($format)) { + $message = 'The "format" option must be a non-empty string.'; + throw new InvalidPluginDefinitionException($this->getPluginId(), $message); + } + $editor_styles = $this->configFactory + ->get("editor.editor.$format") + ->get('settings.plugins.stylescombo.styles'); + foreach (explode("\r\n", $editor_styles) as $rule) { + if (preg_match('/(.*)\|(.*)/', $rule, $matches)) { + $this->styles[$matches[2]] = $matches[1]; + } + } + } + + /** + * Validate the configured rules. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + */ + protected function validateRules() { + if (!array_key_exists('rules', $this->configuration) || !is_array($this->configuration['rules'])) { + $message = 'The "rules" option must be an array.'; + throw new InvalidPluginDefinitionException($this->getPluginId(), $message); + } + foreach ($this->configuration['rules'] as $rule) { + if (empty($rule['xpath']) || empty($rule['style'])) { + $message = 'The "xpath" and "style" options are required for each rule.'; + throw new InvalidPluginDefinitionException($this->getPluginId(), $message); + } + if (empty($this->styles[$rule['style']])) { + $message = sprintf('The style "%s" is not defined.', $rule['style']); + throw new InvalidPluginDefinitionException($this->getPluginId(), $message); + } + } + } + + /** + * Apply a rule to the document. + * + * Search $this->document for elements matching 'xpath' and replace them with + * the HTML elements and classes in $this->styles specified by 'style'. + * If 'depth' is positive, then replace additional parent elements as well. + * + * @param string[] $rule + * An array with keys 'xpath', 'style', and (optional) 'depth'. + */ + protected function apply(array $rule) { + // An entry in $this->styles has the format element(\.class)*: for example, + // 'p' or 'a.button' or 'div.col-xs-6.col-md-4'. + // @see setStyles() + [$element, $classes] = explode('.', $this->styles[$rule['style']] . '.', 2); + $classes = trim(str_replace('.', ' ', $classes)); + + foreach ($this->xpath->query($rule['xpath']) as $node) { + $new_node = $this->document->createElement($element); + foreach ($node->childNodes as $child) { + $new_node->appendChild($child->cloneNode(TRUE)); + } + if ($classes) { + $new_node->setAttribute('class', $classes); + } + $old_node = $node; + if (!empty($rule['depth'])) { + for ($i = 0; $i < $rule['depth']; $i++) { + $old_node = $old_node->parentNode; + } + } + $old_node->parentNode->replaceChild($new_node, $old_node); + } + } + +} diff --git a/web/modules/migrate_plus/src/Plugin/migrate/process/DomMigrationLookup.php b/web/modules/migrate_plus/src/Plugin/migrate/process/DomMigrationLookup.php new file mode 100644 index 0000000000000000000000000000000000000000..fa68d175f6e43da7fc4f5f46bdec267106f82e16 --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/DomMigrationLookup.php @@ -0,0 +1,225 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate\process; + +use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\Plugin\MigratePluginManagerInterface; +use Drupal\migrate\Plugin\MigrationInterface; +use Drupal\migrate\Row; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * String replacements on a source dom based on migration lookup. + * + * Meant to be used after dom process plugin. + * + * Available configuration keys: + * - mode: What to modify. Possible values: + * - attribute: One element attribute. + * - xpath: XPath query expression that will produce the \DOMNodeList to walk. + * - attribute_options: A map of options related to the attribute mode. Required + * when mode is attribute. The keys can be: + * - name: Name of the attribute to match and modify. + * - search: Regular expression to use. It should contain at least one + * parenthesized subpattern which will be used as the ID passed to + * migration_lookup process plugin. + * - replace: Default value to use for replacements on migrations, if not + * specified on the migration. It should contain the '[mapped-id]' string + * where the looked-up migration value will be placed. + * - migrations: A map of options indexed by migration machine name. Possible + * option values are: + * - replace: See replace option lines above. + * - no_stub: If TRUE, then do not create stub entities during migration lookup. + * Optional, defaults to TRUE. + * + * Example: + * + * @code + * process: + * 'body/value': + * - + * plugin: dom + * method: import + * source: 'body/0/value' + * - + * plugin: dom_migration_lookup + * mode: attribute + * xpath: '//a' + * attribute_options: + * name: href + * search: '@/user/(\d+)@' + * replace: '/user/[mapped-id]' + * migrations: + * users: + * replace: '/user/[mapped-id]' + * people: + * replace: '/people/[mapped-id]' + * - + * plugin: dom + * method: export + * @endcode + * + * @MigrateProcessPlugin( + * id = "dom_migration_lookup" + * ) + */ +class DomMigrationLookup extends DomStrReplace implements ContainerFactoryPluginInterface { + + /** + * The migration to be executed. + * + * @var \Drupal\migrate\Plugin\MigrationInterface + */ + protected $migration; + + /** + * The process plugin manager. + * + * @var \Drupal\migrate\Plugin\MigratePluginManagerInterface + */ + protected $processPluginManager; + + /** + * Parameters passed to transform method, except the first, value. + * + * This helps to pass values to another process plugin. + * + * @var array + */ + protected $transformParameters; + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, MigratePluginManagerInterface $process_plugin_manager) { + $configuration += ['no_stub' => TRUE]; + $default_replace_missing = empty($configuration['replace']); + if ($default_replace_missing) { + $configuration['replace'] = 'prevent-requirement-fail'; + } + parent::__construct($configuration, $plugin_id, $plugin_definition); + if ($default_replace_missing) { + unset($this->configuration['replace']); + } + $this->migration = $migration; + $this->processPluginManager = $process_plugin_manager; + if (empty($this->configuration['migrations'])) { + throw new InvalidPluginDefinitionException( + $this->getPluginId(), + "Configuration option 'migration' is required." + ); + } + if (!is_array($this->configuration['migrations'])) { + throw new InvalidPluginDefinitionException( + $this->getPluginId(), + "Configuration option 'migration' should be a keyed array." + ); + } + // Add missing values if possible. + $default_replace = isset($this->configuration['replace']) ? $this->configuration['replace'] : NULL; + foreach ($this->configuration['migrations'] as $migration_name => $configuration_item) { + if (!empty($configuration_item['replace'])) { + continue; + } + if (is_null($default_replace)) { + throw new InvalidPluginDefinitionException( + $this->getPluginId(), + "Please define either a global replace for all migrations, or a specific one for 'migrations.$migration_name'." + ); + } + $this->configuration['migrations'][$migration_name]['replace'] = $default_replace; + } + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $migration, + $container->get('plugin.manager.migrate.process') + ); + } + + /** + * {@inheritdoc} + */ + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + $this->init($value, $destination_property); + $this->transformParameters = [ + 'migrate_executable' => $migrate_executable, + 'row' => $row, + 'destination_property' => $destination_property, + ]; + + foreach ($this->xpath->query($this->configuration['xpath']) as $html_node) { + $subject = $this->getSubject($html_node); + if (empty($subject)) { + // Could not find subject, skip processing. + continue; + } + $search = $this->getSearch(); + if (!preg_match($search, $subject, $matches)) { + // No match found, skip processing. + continue; + } + $id = $matches[1]; + // Walk through defined migrations looking for a map. + foreach ($this->configuration['migrations'] as $migration_name => $configuration) { + $mapped_id = $this->migrationLookup($id, $migration_name); + if (!is_null($mapped_id)) { + // Not using getReplace(), since this implementation depends on the + // migration. + $replace = str_replace('[mapped-id]', $mapped_id, $configuration['replace']); + $this->doReplace($html_node, $search, $replace, $subject); + break; + } + } + } + + return $this->document; + } + + /** + * {@inheritdoc} + */ + protected function doReplace(\DOMElement $html_node, $search, $replace, $subject) { + $new_subject = preg_replace($search, $replace, $subject); + $this->postReplace($html_node, $new_subject); + } + + /** + * Lookup the migration mapped ID on one migration. + * + * @param mixed $id + * The ID to search with migration_lookup process plugin. + * @param string $migration_name + * The migration to look into machine name. + * + * @return string|null + * The found mapped ID, or NULL if not found on the provided migration. + */ + protected function migrationLookup($id, $migration_name) { + $mapped_id = NULL; + $parameters = [ + $id, + $this->transformParameters['migrate_executable'], + $this->transformParameters['row'], + $this->transformParameters['destination_property'], + ]; + $plugin_configuration = [ + 'migration' => $migration_name, + 'no_stub' => $this->configuration['no_stub'], + ]; + $migration_lookup_plugin = $this->processPluginManager + ->createInstance('migration_lookup', $plugin_configuration, $this->migration); + $mapped_id = call_user_func_array([$migration_lookup_plugin, 'transform'], $parameters); + return $mapped_id; + } + +} diff --git a/web/modules/migrate_plus/src/Plugin/migrate/process/DomProcessBase.php b/web/modules/migrate_plus/src/Plugin/migrate/process/DomProcessBase.php new file mode 100644 index 0000000000000000000000000000000000000000..05981ba54a44e12a43a9ea2474bfa6332ddfa6df --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/DomProcessBase.php @@ -0,0 +1,56 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate\process; + +use Drupal\migrate\MigrateSkipRowException; +use Drupal\migrate\ProcessPluginBase; + +/** + * Base class for process plugins that work with \DOMDocument objects. + * + * Use Dom::import() to convert a string to a \DOMDocument object, then plugins + * derived from this class to manipulate the object, then Dom::export() to + * convert back to a string. + */ +abstract class DomProcessBase extends ProcessPluginBase { + + /** + * Document to use. + * + * @var \DOMDocument + */ + protected $document; + + /** + * Xpath query object. + * + * @var \DOMXPath + */ + protected $xpath; + + /** + * Initialize the class properties. + * + * @param mixed $value + * Process plugin value. + * @param string $destination_property + * The name of the destination being processed. Used to generate an error + * message. + * + * @throws \Drupal\migrate\MigrateSkipRowException + * If $value is not a \DOMDocument object. + */ + protected function init($value, $destination_property) { + if (!($value instanceof \DOMDocument)) { + $message = sprintf( + 'The %s plugin in the %s process pipeline requires a \DOMDocument object. You can use the dom plugin to convert a string to \DOMDocument.', + $this->getPluginId(), + $destination_property + ); + throw new MigrateSkipRowException($message); + } + $this->document = $value; + $this->xpath = new \DOMXPath($this->document); + } + +} diff --git a/web/modules/migrate_plus/src/Plugin/migrate/process/DomStrReplace.php b/web/modules/migrate_plus/src/Plugin/migrate/process/DomStrReplace.php new file mode 100644 index 0000000000000000000000000000000000000000..fd2a674a213d284b352c8ae4ca230ae0c1584ddd --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/DomStrReplace.php @@ -0,0 +1,206 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate\process; + +use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException; +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\Row; + +/** + * String replacements on a source dom. + * + * Analogous to str_replace process plugin, but based on a \DOMDocument instead + * of a string. + * Meant to be used after dom process plugin. + * + * Available configuration keys: + * - mode: What to modify. Possible values: + * - attribute: One element attribute. + * - xpath: XPath query expression that will produce the \DOMNodeList to walk. + * - attribute_options: A map of options related to the attribute mode. Required + * when mode is attribute. The keys can be: + * - name: Name of the attribute to match and modify. + * - search: pattern to match. + * - replace: value to replace the searched pattern with. + * - regex: Use regular expression replacement. + * - case_insensitive: Case insensitive search. Only valid when regex is false. + * + * Examples: + * + * @code + * process: + * 'body/value': + * - + * plugin: dom + * method: import + * source: 'body/0/value' + * - + * plugin: dom_str_replace + * mode: attribute + * xpath: '//a' + * attribute_options: + * name: href + * search: 'foo' + * replace: 'bar' + * - + * plugin: dom_str_replace + * mode: attribute + * xpath: '//a' + * attribute_options: + * name: href + * regex: true + * search: '/foo/' + * replace: 'bar' + * - + * plugin: dom + * method: export + * @endcode + * + * @MigrateProcessPlugin( + * id = "dom_str_replace" + * ) + */ +class DomStrReplace extends DomProcessBase { + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->configuration += [ + 'case_insensitive' => FALSE, + 'regex' => FALSE, + ]; + $options_validation = [ + 'xpath' => NULL, + 'mode' => ['attribute'], + // @todo Move out once another mode is supported. + // @see https://www.drupal.org/project/migrate_plus/issues/3042833 + 'attribute_options' => NULL, + 'search' => NULL, + 'replace' => NULL, + ]; + foreach ($options_validation as $option_name => $possible_values) { + if (empty($this->configuration[$option_name])) { + throw new InvalidPluginDefinitionException( + $this->getPluginId(), + "Configuration option '$option_name' is required." + ); + } + if (!is_null($possible_values) && !in_array($this->configuration[$option_name], $possible_values)) { + throw new InvalidPluginDefinitionException( + $this->getPluginId(), + sprintf( + 'Configuration option "%s" only accepts the following values: %s.', + $option_name, + implode(', ', $possible_values) + ) + ); + } + } + } + + /** + * {@inheritdoc} + */ + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + $this->init($value, $destination_property); + + foreach ($this->xpath->query($this->configuration['xpath']) as $html_node) { + $subject = $this->getSubject($html_node); + if (empty($subject)) { + // Could not find subject, skip processing. + continue; + } + $search = $this->getSearch(); + $replace = $this->getReplace(); + $this->doReplace($html_node, $search, $replace, $subject); + } + + return $this->document; + } + + /** + * Retrieves the right subject string. + * + * @param \DOMElement $node + * The current element from iteration. + * + * @return string + * The string to use a subject on search. + */ + protected function getSubject(\DOMElement $node) { + switch ($this->configuration['mode']) { + case 'attribute': + return $node->getAttribute($this->configuration['attribute_options']['name']); + } + } + + /** + * Retrieves the right search string based on configuration. + * + * @return string + * The value to be searched. + */ + protected function getSearch() { + switch ($this->configuration['mode']) { + case 'attribute': + return $this->configuration['search']; + } + } + + /** + * Retrieves the right replace string based on configuration. + * + * @return string + * The value to use for replacement. + */ + protected function getReplace() { + switch ($this->configuration['mode']) { + case 'attribute': + return $this->configuration['replace']; + } + } + + /** + * Retrieves the right replace string based on configuration. + * + * @param \DOMElement $html_node + * The current element from iteration. + * @param string $search + * The search string or pattern. + * @param string $replace + * The replacement string. + * @param string $subject + * The string on which to perform the substitution. + */ + protected function doReplace(\DOMElement $html_node, $search, $replace, $subject) { + if ($this->configuration['regex']) { + $function = 'preg_replace'; + } + elseif ($this->configuration['case_insensitive']) { + $function = 'str_ireplace'; + } + else { + $function = 'str_replace'; + } + $new_subject = $function($search, $replace, $subject); + $this->postReplace($html_node, $new_subject); + } + + /** + * Performs post-replace actions. + * + * @param \DOMElement $html_node + * The current element from iteration. + * @param string $new_subject + * The new value to use. + */ + protected function postReplace(\DOMElement $html_node, $new_subject) { + switch ($this->configuration['mode']) { + case 'attribute': + $html_node->setAttribute($this->configuration['attribute_options']['name'], $new_subject); + } + } + +} diff --git a/web/modules/migrate_plus/src/Plugin/migrate/process/EntityGenerate.php b/web/modules/migrate_plus/src/Plugin/migrate/process/EntityGenerate.php index a749a6388cf42c80dd5939f8dd6c6e8fe2f6d749..7311687d6dba25a8a795ad6d6c9eba731dc1aae2 100644 --- a/web/modules/migrate_plus/src/Plugin/migrate/process/EntityGenerate.php +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/EntityGenerate.php @@ -2,10 +2,8 @@ namespace Drupal\migrate_plus\Plugin\migrate\process; -use Drupal\Core\Entity\EntityManagerInterface; -use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface; +use Drupal\Component\Utility\NestedArray; use Drupal\migrate\MigrateExecutableInterface; -use Drupal\migrate\Plugin\MigratePluginManager; use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Row; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -53,57 +51,33 @@ class EntityGenerate extends EntityLookup { protected $row; /** - * The MigrateExecutable instance. + * The migrate executable. * - * @var \Drupal\migrate\MigrateExecutable + * @var \Drupal\migrate\MigrateExecutableInterface */ protected $migrateExecutable; /** - * The get process plugin instance. + * The MigratePluginManager instance. * - * @var \Drupal\migrate\Plugin\migrate\process\Get + * @var \Drupal\migrate\Plugin\MigratePluginManagerInterface */ - protected $getProcessPlugin; + protected $processPluginManager; /** - * EntityGenerate constructor. + * The get process plugin instance. * - * @param array $configuration - * A configuration array containing information about the plugin instance. - * @param string $pluginId - * The plugin_id for the plugin instance. - * @param mixed $pluginDefinition - * The plugin implementation definition. - * @param \Drupal\migrate\Plugin\MigrationInterface $migration - * The migration. - * @param \Drupal\Core\Entity\EntityManagerInterface $entityManager - * The $entityManager instance. - * @param \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selectionPluginManager - * The $selectionPluginManager instance. - * @param \Drupal\migrate\Plugin\MigratePluginManager $migratePluginManager - * The MigratePluginManager instance. + * @var \Drupal\migrate\Plugin\migrate\process\Get */ - public function __construct(array $configuration, $pluginId, $pluginDefinition, MigrationInterface $migration, EntityManagerInterface $entityManager, SelectionPluginManagerInterface $selectionPluginManager, MigratePluginManager $migratePluginManager) { - parent::__construct($configuration, $pluginId, $pluginDefinition, $migration, $entityManager, $selectionPluginManager); - if (isset($configuration['values'])) { - $this->getProcessPlugin = $migratePluginManager->createInstance('get', ['source' => $configuration['values']]); - } - } + protected $getProcessPlugin; /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $pluginId, $pluginDefinition, MigrationInterface $migration = NULL) { - return new static( - $configuration, - $pluginId, - $pluginDefinition, - $migration, - $container->get('entity.manager'), - $container->get('plugin.manager.entity_reference_selection'), - $container->get('plugin.manager.migrate.process') - ); + $instance = parent::create($container, $configuration, $pluginId, $pluginDefinition, $migration); + $instance->processPluginManager = $container->get('plugin.manager.migrate.process'); + return $instance; } /** @@ -131,7 +105,7 @@ public function transform($value, MigrateExecutableInterface $migrateExecutable, */ protected function generateEntity($value) { if (!empty($value)) { - $entity = $this->entityManager + $entity = $this->entityTypeManager ->getStorage($this->lookupEntityType) ->create($this->entity($value)); $entity->save(); @@ -168,8 +142,8 @@ protected function entity($value) { // Gather any additional properties/fields. if (isset($this->configuration['values']) && is_array($this->configuration['values'])) { foreach ($this->configuration['values'] as $key => $property) { - $source_value = $this->getProcessPlugin->transform(NULL, $this->migrateExecutable, $this->row, $property); - $entity_values[$key] = $source_value; + $source_value = $this->row->get($property); + NestedArray::setValue($entity_values, explode(Row::PROPERTY_SEPARATOR, $key), $source_value, TRUE); } } diff --git a/web/modules/migrate_plus/src/Plugin/migrate/process/EntityLookup.php b/web/modules/migrate_plus/src/Plugin/migrate/process/EntityLookup.php index 2ae0d07c1de2a438c830da55412a9c6618b2e7da..7f9595f1aa2bf9bcda4e072b4c26239174d3de3f 100644 --- a/web/modules/migrate_plus/src/Plugin/migrate/process/EntityLookup.php +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/EntityLookup.php @@ -3,12 +3,10 @@ namespace Drupal\migrate_plus\Plugin\migrate\process; use Drupal\Core\Config\Entity\ConfigEntityInterface; -use Drupal\Core\Entity\EntityManagerInterface; -use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\MigrateException; use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\ProcessPluginBase; use Drupal\migrate\Row; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -16,15 +14,16 @@ /** * This plugin looks for existing entities. * - * @MigrateProcessPlugin( - * id = "entity_lookup", - * handle_multiples = TRUE - * ) - * * In its most simple form, this plugin needs no configuration. However, if the * lookup properties cannot be determined through introspection, define them via * configuration. * + * Available configuration keys: + * - access_check: (optional) Indicates if access to the entity for this user + * will be checked. Default is true. + * + * @codingStandardsIgnoreStart + * * Example usage with minimal configuration: * @code * destination: @@ -35,8 +34,10 @@ * default_value: page * field_tags: * plugin: entity_lookup + * access_check: false * source: tags * @endcode + * In this example above, the access check is disabled. * * Example usage with full configuration: * @code @@ -49,15 +50,29 @@ * entity_type: taxonomy_term * ignore_case: true * @endcode + * + * @codingStandardsIgnoreEnd + * + * @MigrateProcessPlugin( + * id = "entity_lookup", + * handle_multiples = TRUE + * ) */ class EntityLookup extends ProcessPluginBase implements ContainerFactoryPluginInterface { /** - * The entity manager. + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The field manager. * - * @var \Drupal\Core\Entity\EntityManagerInterface + * @var \Drupal\Core\Entity\EntityFieldManagerInterface */ - protected $entityManager; + protected $entityFieldManager; /** * The migration. @@ -123,30 +138,29 @@ class EntityLookup extends ProcessPluginBase implements ContainerFactoryPluginIn protected $destinationProperty; /** - * {@inheritdoc} + * The access check flag. + * + * @var string */ - public function __construct(array $configuration, $pluginId, $pluginDefinition, MigrationInterface $migration, EntityManagerInterface $entityManager, SelectionPluginManagerInterface $selectionPluginManager) { - parent::__construct($configuration, $pluginId, $pluginDefinition); - $this->migration = $migration; - $this->entityManager = $entityManager; - $this->selectionPluginManager = $selectionPluginManager; - $pluginIdParts = explode(':', $this->migration->getDestinationPlugin()->getPluginId()); - $this->destinationEntityType = empty($pluginIdParts[1]) ?: $pluginIdParts[1]; - $this->destinationBundleKey = !$this->destinationEntityType ?: $this->entityManager->getDefinition($this->destinationEntityType)->getKey('bundle'); - } + protected $accessCheck = TRUE; /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $pluginId, $pluginDefinition, MigrationInterface $migration = NULL) { - return new static( + $instance = new static( $configuration, $pluginId, - $pluginDefinition, - $migration, - $container->get('entity.manager'), - $container->get('plugin.manager.entity_reference_selection') + $pluginDefinition ); + $instance->migration = $migration; + $instance->entityTypeManager = $container->get('entity_type.manager'); + $instance->entityFieldManager = $container->get('entity_field.manager'); + $instance->selectionPluginManager = $container->get('plugin.manager.entity_reference_selection'); + $pluginIdParts = explode(':', $instance->migration->getDestinationPlugin()->getPluginId()); + $instance->destinationEntityType = empty($pluginIdParts[1]) ? NULL : $pluginIdParts[1]; + $instance->destinationBundleKey = $instance->destinationEntityType ? $instance->entityTypeManager->getDefinition($instance->destinationEntityType)->getKey('bundle') : NULL; + return $instance; } /** @@ -177,6 +191,9 @@ public function transform($value, MigrateExecutableInterface $migrateExecutable, * with the $row above. */ protected function determineLookupProperties($destinationProperty) { + if (isset($this->configuration['access_check'])) { + $this->accessCheck = $this->configuration['access_check']; + } if (!empty($this->configuration['value_key'])) { $this->lookupValueKey = $this->configuration['value_key']; } @@ -194,7 +211,7 @@ protected function determineLookupProperties($destinationProperty) { // See if we can introspect the lookup properties from destination field. if (!empty($this->migration->getProcess()[$this->destinationBundleKey][0]['default_value'])) { $destinationEntityBundle = $this->migration->getProcess()[$this->destinationBundleKey][0]['default_value']; - $fieldConfig = $this->entityManager->getFieldDefinitions($this->destinationEntityType, $destinationEntityBundle)[$destinationProperty]->getConfig($destinationEntityBundle); + $fieldConfig = $this->entityFieldManager->getFieldDefinitions($this->destinationEntityType, $destinationEntityBundle)[$destinationProperty]->getConfig($destinationEntityBundle); switch ($fieldConfig->getType()) { case 'entity_reference': if (empty($this->lookupBundle)) { @@ -211,9 +228,11 @@ protected function determineLookupProperties($destinationProperty) { // Make an assumption that if the selection handler can target more // than one type of entity that we will use the first entity type. - $this->lookupEntityType = $this->lookupEntityType ?: reset($this->selectionPluginManager->createInstance($fieldConfig->getSetting('handler'))->getPluginDefinition()['entity_types']); - $this->lookupValueKey = $this->lookupValueKey ?: $this->entityManager->getDefinition($this->lookupEntityType)->getKey('label'); - $this->lookupBundleKey = $this->lookupBundleKey ?: $this->entityManager->getDefinition($this->lookupEntityType)->getKey('bundle'); + $fieldHandler = $fieldConfig->getSetting('handler'); + $selection = $this->selectionPluginManager->createInstance($fieldHandler); + $this->lookupEntityType = $this->lookupEntityType ?: reset($selection->getPluginDefinition()['entity_types']); + $this->lookupValueKey = $this->lookupValueKey ?: $this->entityTypeManager->getDefinition($this->lookupEntityType)->getKey('label'); + $this->lookupBundleKey = $this->lookupBundleKey ?: $this->entityTypeManager->getDefinition($this->lookupEntityType)->getKey('bundle'); break; case 'file': @@ -223,8 +242,7 @@ protected function determineLookupProperties($destinationProperty) { break; default: - throw new MigrateException('Destination field type ' . - $fieldConfig->getType() . 'is not a recognized reference type.'); + throw new MigrateException(sprintf('Destination field type %s is not a recognized reference type.', $fieldConfig->getType())); } } } @@ -259,9 +277,15 @@ protected function query($value) { $multiple = is_array($value); - $query = $this->entityManager->getStorage($this->lookupEntityType) + $query = $this->entityTypeManager->getStorage($this->lookupEntityType) ->getQuery() + ->accessCheck($this->accessCheck) ->condition($this->lookupValueKey, $value, $multiple ? 'IN' : NULL); + // Sqlite and possibly others returns data in a non-deterministic order. + // Make it deterministic. + if ($multiple) { + $query->sort($this->lookupValueKey, 'DESC'); + } if ($this->lookupBundleKey) { $query->condition($this->lookupBundleKey, $this->lookupBundle); @@ -276,7 +300,7 @@ protected function query($value) { if (!$ignoreCase) { // Returns the entity's identifier. foreach ($results as $k => $identifier) { - $entity = $this->entityManager->getStorage($this->lookupEntityType)->load($identifier); + $entity = $this->entityTypeManager->getStorage($this->lookupEntityType)->load($identifier); $result_value = $entity instanceof ConfigEntityInterface ? $entity->get($this->lookupValueKey) : $entity->get($this->lookupValueKey)->value; if (($multiple && !in_array($result_value, $value, TRUE)) || (!$multiple && $result_value !== $value)) { unset($results[$k]); diff --git a/web/modules/migrate_plus/src/Plugin/migrate/process/FileBlob.php b/web/modules/migrate_plus/src/Plugin/migrate/process/FileBlob.php index 22341b6bc6ab6643fc11c0b1b3ef387079ff08d2..ed581682008f9104dad4cfce0b5903663f83dc4c 100644 --- a/web/modules/migrate_plus/src/Plugin/migrate/process/FileBlob.php +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/FileBlob.php @@ -13,6 +13,61 @@ /** * Copy a file from a blob into a file. * + * The source value is an indexed array of two values: + * - The destination URI, e.g. 'public://example.txt'. + * - The binary blob data. + * + * Available configuration keys: + * - reuse: true + * + * @codingStandardsIgnoreStart + * + * Examples: + * @code + * uri: + * plugin: file_blob + * source: + * - 'public://example.txt' + * - blob + * @endcode + * Above, a basic configuration. + * + * @code + * source: + * constants: + * destination: public://images + * process: + * destination_blob: + * plugin: callback + * callable: base64_decode + * source: + * - blob + * destination_basename: + * plugin: callback + * callable: basename + * source: file_name + * destination_path: + * plugin: concat + * source: + * - constants/destination + * - @destination_basename + * uri: + * plugin: file_blob + * source: + * - @destination_path + * - @destination_blob + * @endcode + In the example above, it is necessary to manipulate the values before they + * are processed by this plugin. This is because this plugin takes a binary blob + * and saves it as a file. In many cases, as in this example, the data is base64 + * encoded and should be decoded first. In destination_blob, the incoming data + * is decoded from base64 to binary. The destination_path element is + * concatenating the base filename with the destination directory set in the + * constants to create the final path. The resulting values are then referenced + * as the source of the file_blob plugin. + * + * @codingStandardsIgnoreEnd + * * @MigrateProcessPlugin( * id = "file_blob" * ) @@ -67,7 +122,7 @@ public function transform($value, MigrateExecutableInterface $migrate_executable if ($row->isStub()) { return NULL; } - list($destination, $blob) = $value; + [$destination, $blob] = $value; // Determine if we going to overwrite existing files or not touch them. $replace = $this->getOverwriteMode(); @@ -78,9 +133,11 @@ public function transform($value, MigrateExecutableInterface $migrate_executable return $destination; } $dir = $this->getDirectory($destination); - if (!file_prepare_directory($dir, FILE_CREATE_DIRECTORY)) { + $success = $this->fileSystem->prepareDirectory($dir, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); + if (!$success) { throw new MigrateSkipProcessException("Could not create directory '$dir'"); } + if ($this->putFile($destination, $blob, $replace)) { return $destination; } @@ -95,14 +152,15 @@ public function transform($value, MigrateExecutableInterface $migrate_executable * @param string $blob * The base64 encoded file contents. * @param int $replace - * (optional) FILE_EXISTS_REPLACE (default) or FILE_EXISTS_ERROR, depending - * on the configuration. + * (optional) either FileSystemInterface::EXISTS_REPLACE; (default) or + * FileSystemInterface::EXISTS_ERROR, depending on the configuration. * * @return bool|string * File path on success, FALSE on failure. */ - protected function putFile($destination, $blob, $replace = FILE_EXISTS_REPLACE) { - if ($path = file_destination($destination, $replace)) { + protected function putFile($destination, $blob, $replace = FileSystemInterface::EXISTS_REPLACE) { + $path = $this->fileSystem->getDestinationFilename($destination, $replace); + if ($path) { if (file_put_contents($path, $blob)) { return $path; } @@ -119,15 +177,14 @@ protected function putFile($destination, $blob, $replace = FILE_EXISTS_REPLACE) * Determines how to handle file conflicts. * * @return int - * Either FILE_EXISTS_REPLACE (default) or FILE_EXISTS_ERROR, depending on - * the configuration. + * Either FileSystemInterface::EXISTS_REPLACE; (default) or + * FileSystemInterface::EXISTS_ERROR, depending on the configuration. */ protected function getOverwriteMode() { - if (!empty($this->configuration['reuse'])) { - return FILE_EXISTS_ERROR; + if (isset($this->configuration['reuse']) && !empty($this->configuration['reuse'])) { + return FileSystemInterface::EXISTS_ERROR; } - - return FILE_EXISTS_REPLACE; + return FileSystemInterface::EXISTS_REPLACE; } /** diff --git a/web/modules/migrate_plus/src/Plugin/migrate/process/Merge.php b/web/modules/migrate_plus/src/Plugin/migrate/process/Merge.php index 463595e9d7b31c13bd6e7e4dc3a799d28ac1dff4..9861bd65d4e961d4d195be2c22fdc73bd51ed510 100644 --- a/web/modules/migrate_plus/src/Plugin/migrate/process/Merge.php +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/Merge.php @@ -2,9 +2,9 @@ namespace Drupal\migrate_plus\Plugin\migrate\process; -use Drupal\migrate\ProcessPluginBase; use Drupal\migrate\MigrateException; use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\ProcessPluginBase; use Drupal\migrate\Row; /** @@ -33,7 +33,7 @@ * temp_images: * plugin: iterator * source: field_image - * process + * process: * target_id: * plugin: migration_lookup * migration: image_entities_to_paragraph @@ -56,13 +56,14 @@ public function transform($value, MigrateExecutableInterface $migrate_executable throw new MigrateException(sprintf('Merge process failed for destination property (%s): input is not an array.', $destination_property)); } $new_value = []; - foreach($value as $i => $item) { + foreach ($value as $i => $item) { if (!is_array($item)) { throw new MigrateException(sprintf('Merge process failed for destination property (%s): index (%s) in the source value is not an array that can be merged.', $destination_property, $i)); } - $new_value = array_merge($new_value, $item); + $new_value[] = $item; } - return $new_value; + + return array_merge(...$new_value); } } diff --git a/web/modules/migrate_plus/src/Plugin/migrate/process/SkipOnValue.php b/web/modules/migrate_plus/src/Plugin/migrate/process/SkipOnValue.php index 1a0c8f545c5996f3c5708da619f918dd4ef60168..bf1632fefc4e23b8a17247ef2d251dff3e361426 100644 --- a/web/modules/migrate_plus/src/Plugin/migrate/process/SkipOnValue.php +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/SkipOnValue.php @@ -20,10 +20,12 @@ * - value: An single value or array of values against which the source value * should be compared. * - not_equals: (optional) If set, skipping occurs when values are not equal. - * - method: What to do if the input value is empty. Possible values: - * - row: Skips the entire row when an empty value is encountered. - * - process: Prevents further processing of the input property when the value - * is empty. + * - method: What to do if the input value equals to value given in + * configuration key value. Possible values: + * - row: Skips the entire row. + * - process: Prevents further processing of the input property + * + * @codingStandardsIgnoreStart * * Examples: * @@ -32,12 +34,11 @@ * type: * plugin: skip_on_value * source: content_type - * method: row + * method: process * value: blog * @endcode - * - * The above example will skip processing the input property if the content_type - * source field equals "blog". + * The above example will skip further processing of the input property if + * the content_type source field equals "blog". * * Example usage with full configuration: * @code @@ -50,9 +51,10 @@ * - article * - testimonial * @endcode - * * The above example will skip processing any row for which the source row's * content type field is not "article" or "testimonial". + * + * @codingStandardsIgnoreEnd */ class SkipOnValue extends ProcessPluginBase { diff --git a/web/modules/migrate_plus/src/Plugin/migrate/process/StrReplace.php b/web/modules/migrate_plus/src/Plugin/migrate/process/StrReplace.php index 2b033bebc3a9b5274415db8e69f26f84c83c7560..d131394148b94eb1549f9bf1dba33c4f6e7a7be9 100644 --- a/web/modules/migrate_plus/src/Plugin/migrate/process/StrReplace.php +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/StrReplace.php @@ -14,8 +14,9 @@ * id = "str_replace" * ) * - * To do a simple hardcoded string replace use the following: + * @codingStandardsIgnoreStart * + * To do a simple hardcoded string replace, use the following: * @code * field_text: * plugin: str_replace @@ -23,7 +24,6 @@ * search: foo * replace: bar * @endcode - * * If the value of text is "vero eos et accusam et justo vero" in source, foo is * "et" in search and bar is "that" in replace, field_text will be "vero eos * that accusam that justo vero". @@ -37,7 +37,6 @@ * search: foo * replace: bar * @endcode - * * If the value of text is "VERO eos et accusam et justo vero" in source, foo is * "vero" in search and bar is "that" in replace, field_text will be "that eos * et accusam et justo that". @@ -51,7 +50,6 @@ * search: foo * replace: bar * @endcode - * * If the value of text is "vero eos et 123 accusam et justo 123 duo" in source, * foo is "/[0-9]{3}/" in search and bar is "the" in replace, field_text will be * "vero eos et the accusam et justo the duo". @@ -59,6 +57,17 @@ * All the rules for * @link http://php.net/manual/function.str-replace.php str_replace @endlink * apply. This means that you can provide arrays as values. + * + * Multiple values can be matched like this: + * @code + * field_text: + * plugin: str_replace + * source: text + * search: ["AT", "CH", "DK"] + * replace: ["Austria", "Switzerland", "Denmark"] + * @endcode + * + * @codingStandardsIgnoreEnd */ class StrReplace extends ProcessPluginBase { @@ -84,7 +93,7 @@ public function transform($value, MigrateExecutableInterface $migrate_executable 'case_insensitive' => FALSE, 'regex' => FALSE, ]; - $function = "str_replace"; + $function = 'str_replace'; if ($this->configuration['case_insensitive']) { $function = 'str_ireplace'; } diff --git a/web/modules/migrate_plus/src/Plugin/migrate/process/Transliteration.php b/web/modules/migrate_plus/src/Plugin/migrate/process/Transliteration.php index 217869e205c6874ba7ed47930a0fac7310cc14a7..f6cd6f7d1353d52d4c0fe8ffadb32a83058008c5 100644 --- a/web/modules/migrate_plus/src/Plugin/migrate/process/Transliteration.php +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/Transliteration.php @@ -5,8 +5,8 @@ use Drupal\Component\Transliteration\TransliterationInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\migrate\ProcessPluginBase; use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\ProcessPluginBase; use Drupal\migrate\Row; use Symfony\Component\DependencyInjection\ContainerInterface; diff --git a/web/modules/migrate_plus/src/Plugin/migrate/source/SourcePluginExtension.php b/web/modules/migrate_plus/src/Plugin/migrate/source/SourcePluginExtension.php index 19ca6a9a2a7b2d70b12db4510ce7813fdc8d944a..5d5f5cb512c9f4bed0b8dd1f093291300faab222 100644 --- a/web/modules/migrate_plus/src/Plugin/migrate/source/SourcePluginExtension.php +++ b/web/modules/migrate_plus/src/Plugin/migrate/source/SourcePluginExtension.php @@ -2,8 +2,8 @@ namespace Drupal\migrate_plus\Plugin\migrate\source; -use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Plugin\migrate\source\SourcePluginBase; +use Drupal\migrate\Plugin\MigrationInterface; /** * Generally-useful extensions to the core SourcePluginBase. diff --git a/web/modules/migrate_plus/src/Plugin/migrate/source/Table.php b/web/modules/migrate_plus/src/Plugin/migrate/source/Table.php new file mode 100644 index 0000000000000000000000000000000000000000..21c43eb7378d8dba3d3a1dc5a4624435b49f13ac --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate/source/Table.php @@ -0,0 +1,80 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate\source; + +use Drupal\Core\State\StateInterface; +use Drupal\migrate\MigrateException; +use Drupal\migrate\Plugin\migrate\source\SqlBase; +use Drupal\migrate\Plugin\MigrationInterface; + +/** + * Source plugin for retrieving data via URLs. + * + * @MigrateSource( + * id = "table" + * ) + */ +class Table extends SqlBase { + + const TABLE_ALIAS = 't'; + + /** + * The name of the destination table. + * + * @var string + */ + protected $tableName; + + /** + * IDMap compatible array of id fields. + * + * @var array + */ + protected $idFields; + + /** + * Array of fields present on the destination table. + * + * @var array + */ + protected $fields; + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, StateInterface $state) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $state); + $this->tableName = $configuration['table_name']; + // Insert alias in id_fields. + foreach ($configuration['id_fields'] as &$field) { + $field['alias'] = static::TABLE_ALIAS; + } + $this->idFields = $configuration['id_fields']; + $this->fields = isset($configuration['fields']) ? $configuration['fields'] : []; + } + + /** + * {@inheritdoc} + */ + public function query() { + return $this->select($this->tableName, static::TABLE_ALIAS)->fields(static::TABLE_ALIAS, $this->fields); + } + + /** + * {@inheritdoc} + */ + public function fields() { + return $this->fields; + } + + /** + * {@inheritdoc} + */ + public function getIds() { + if (empty($this->idFields)) { + throw new MigrateException('Id fields are required for a table source'); + } + return $this->idFields; + } + +} diff --git a/web/modules/migrate_plus/src/Plugin/migrate_plus/authentication/OAuth2.php b/web/modules/migrate_plus/src/Plugin/migrate_plus/authentication/OAuth2.php index 116314d6bcaa2c7e6da502211c8ab4b2c695b5cf..0be3f0810fa80bd167b07e7333fcf49a8b0336c4 100644 --- a/web/modules/migrate_plus/src/Plugin/migrate_plus/authentication/OAuth2.php +++ b/web/modules/migrate_plus/src/Plugin/migrate_plus/authentication/OAuth2.php @@ -41,21 +41,26 @@ public function getAuthenticationOptions() { case 'authorization_code': $grant_type = new AuthorizationCode($client, $this->configuration); break; + case 'client_credentials': $grant_type = new ClientCredentials($client, $this->configuration); break; + case 'urn:ietf:params:oauth:grant-type:jwt-bearer': $grant_type = new JwtBearer($client, $this->configuration); break; + case 'password': $grant_type = new PasswordCredentials($client, $this->configuration); break; + case 'refresh_token': $grant_type = new RefreshToken($client, $this->configuration); break; + default: throw new MigrateException("Unrecognized grant_type {$this->configuration['grant_type']}."); - break; + } $middleware = new OAuthMiddleware($client, $grant_type); diff --git a/web/modules/migrate_plus/src/Plugin/migrate_plus/data_fetcher/File.php b/web/modules/migrate_plus/src/Plugin/migrate_plus/data_fetcher/File.php index dfa02d3bc3d885e8e043c060f3190d5b602fb9fa..33e3cae5c5d665cb0f0e9cbca82bd8f9d8237d61 100644 --- a/web/modules/migrate_plus/src/Plugin/migrate_plus/data_fetcher/File.php +++ b/web/modules/migrate_plus/src/Plugin/migrate_plus/data_fetcher/File.php @@ -34,7 +34,7 @@ public function getRequestHeaders() { * {@inheritdoc} */ public function getResponse($url) { - $response = file_get_contents($url); + $response = @file_get_contents($url); if ($response === FALSE) { throw new MigrateException('file parser plugin: could not retrieve data from ' . $url); } diff --git a/web/modules/migrate_plus/src/Plugin/migrate_plus/data_parser/Json.php b/web/modules/migrate_plus/src/Plugin/migrate_plus/data_parser/Json.php index c1220bc5f4e60d9836fbdaac4cabce4947e70d4f..7ef4467d00b1ac7e963b4d15472e143d4cbfc40b 100644 --- a/web/modules/migrate_plus/src/Plugin/migrate_plus/data_parser/Json.php +++ b/web/modules/migrate_plus/src/Plugin/migrate_plus/data_parser/Json.php @@ -114,8 +114,8 @@ protected function fetchNextRow() { foreach ($field_selectors as $field_selector) { if (is_array($field_data) && array_key_exists($field_selector, $field_data)) { $field_data = $field_data[$field_selector]; - } - else { + } + else { $field_data = ''; } } diff --git a/web/modules/migrate_plus/src/Plugin/migrate_plus/data_parser/SimpleXml.php b/web/modules/migrate_plus/src/Plugin/migrate_plus/data_parser/SimpleXml.php index 8a58d8b075c5d2bf77ed7a2ef634e54e0e1b0699..5e09822287d97f89faeccea88458bea22f6f6371 100644 --- a/web/modules/migrate_plus/src/Plugin/migrate_plus/data_parser/SimpleXml.php +++ b/web/modules/migrate_plus/src/Plugin/migrate_plus/data_parser/SimpleXml.php @@ -45,14 +45,14 @@ protected function openSourceUrl($url) { libxml_clear_errors(); $xml_data = $this->getDataFetcherPlugin()->getResponseContent($url); - $xml = simplexml_load_string($xml_data); - $this->registerNamespaces($xml); - $xpath = $this->configuration['item_selector']; - $this->matches = $xml->xpath($xpath); + $xml = simplexml_load_string(trim($xml_data)); foreach (libxml_get_errors() as $error) { $error_string = self::parseLibXmlError($error); throw new MigrateException($error_string); } + $this->registerNamespaces($xml); + $xpath = $this->configuration['item_selector']; + $this->matches = $xml->xpath($xpath); return TRUE; } diff --git a/web/modules/migrate_plus/src/Plugin/migrate_plus/data_parser/Soap.php b/web/modules/migrate_plus/src/Plugin/migrate_plus/data_parser/Soap.php index 7f8c694e2f5b7a6038ef08ebd96537e91b1c4999..3f8a75421a9e42ba7eefff18e7f31b6bf6f9c63c 100644 --- a/web/modules/migrate_plus/src/Plugin/migrate_plus/data_parser/Soap.php +++ b/web/modules/migrate_plus/src/Plugin/migrate_plus/data_parser/Soap.php @@ -104,12 +104,15 @@ protected function openSourceUrl($url) { $xml = simplexml_load_string($response_value); $this->iterator = new \ArrayIterator($xml->xpath($this->itemSelector)); break; + case 'object': $this->iterator = new \ArrayIterator($response_value->{$this->itemSelector}); break; + case 'array': $this->iterator = new \ArrayIterator($response_value[$this->itemSelector]); break; + } return TRUE; } diff --git a/web/modules/migrate_plus/src/Plugin/migrate_plus/data_parser/Xml.php b/web/modules/migrate_plus/src/Plugin/migrate_plus/data_parser/Xml.php index 6c08069276e886dd3907c0cc22ec3a2e18f2b78b..2737850aa365960a1e8824046885d7cc5ef09c4f 100644 --- a/web/modules/migrate_plus/src/Plugin/migrate_plus/data_parser/Xml.php +++ b/web/modules/migrate_plus/src/Plugin/migrate_plus/data_parser/Xml.php @@ -135,7 +135,7 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition * A \SimpleXmlElement when the document is parseable, or false if a * parsing error occurred. * - * @throws MigrateException + * @throws \Drupal\migrate\MigrateException */ protected function getSimpleXml() { $node = $this->reader->expand(); diff --git a/web/modules/migrate_plus/tests/data/simple_xml_broken_missing_tag.xml b/web/modules/migrate_plus/tests/data/simple_xml_broken_missing_tag.xml new file mode 100644 index 0000000000000000000000000000000000000000..75fe95fb79040c5395ce9c2ae5b9e9e96480d705 --- /dev/null +++ b/web/modules/migrate_plus/tests/data/simple_xml_broken_missing_tag.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<items> + <item id="1"> + <values title="Values"> + <value>Value 1</value> + <value>Value 2</value> + </values> + <!-- item --> + <item id="2"> + <values title="Values"> + <value>Value 1 (single)</value> + </values> + </item> +</items> diff --git a/web/modules/migrate_plus/tests/data/simple_xml_broken_tag_mismatch.xml b/web/modules/migrate_plus/tests/data/simple_xml_broken_tag_mismatch.xml new file mode 100644 index 0000000000000000000000000000000000000000..579540925dbb83420134dde3bc98306c83722c5c --- /dev/null +++ b/web/modules/migrate_plus/tests/data/simple_xml_broken_tag_mismatch.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- unmatched tags --> +<ietems> <!-- wrong tag --> + <item id="1"> + <values title="Values"> + <value>Value 1</value> + <value>Value 2</value> + </values> + </item> + <item id="2"> + <values title="Values"> + <value>Value 1 (single)</value> + </values> + </item> +</items> diff --git a/web/modules/migrate_plus/tests/data/simple_xml_invalid_multi_whitespace.xml b/web/modules/migrate_plus/tests/data/simple_xml_invalid_multi_whitespace.xml new file mode 100644 index 0000000000000000000000000000000000000000..a063482ca716569ee17fbf73c766e5a931904f75 --- /dev/null +++ b/web/modules/migrate_plus/tests/data/simple_xml_invalid_multi_whitespace.xml @@ -0,0 +1,17 @@ + + + +<?xml version="1.0" encoding="utf-8"?> +<items> + <item id="1"> + <values title="Values"> + <value>Value 1</value> + <value>Value 2</value> + </values> + </item> + <item id="2"> + <values title="Values"> + <value>Value 1 (single)</value> + </values> + </item> +</items> diff --git a/web/modules/migrate_plus/tests/data/simple_xml_invalid_single_line.xml b/web/modules/migrate_plus/tests/data/simple_xml_invalid_single_line.xml new file mode 100644 index 0000000000000000000000000000000000000000..86ec1961a50834163a9be7d884090d7b2b5c3327 --- /dev/null +++ b/web/modules/migrate_plus/tests/data/simple_xml_invalid_single_line.xml @@ -0,0 +1,15 @@ + +<?xml version="1.0" encoding="utf-8"?> +<items> + <item id="1"> + <values title="Values"> + <value>Value 1</value> + <value>Value 2</value> + </values> + </item> + <item id="2"> + <values title="Values"> + <value>Value 1 (single)</value> + </values> + </item> +</items> diff --git a/web/modules/migrate_plus/tests/data/simple_xml_non_xml.xml b/web/modules/migrate_plus/tests/data/simple_xml_non_xml.xml new file mode 100644 index 0000000000000000000000000000000000000000..d7447b68393f618d3fd33494f5e83f1f61e8749f --- /dev/null +++ b/web/modules/migrate_plus/tests/data/simple_xml_non_xml.xml @@ -0,0 +1,17 @@ + + + +<? xml version="1.0" encoding="utf-8"?> +<items> + <item id="1"> + <values title="Values"> + <value>Value 1</value> + <value>Value 2</value> + </values> + </item> + <item id="2"> + <values title="Values"> + <value>Value 1 (single)</value> + </values> + </item> +</items> diff --git a/web/modules/migrate_plus/tests/modules/migrate_plus_test/config/install/migrate_plus.migration.dummy.yml b/web/modules/migrate_plus/tests/modules/migrate_plus_test/config/install/migrate_plus.migration.dummy.yml new file mode 100644 index 0000000000000000000000000000000000000000..3a01054ce7a323ea56edbd0978952299c880526c --- /dev/null +++ b/web/modules/migrate_plus/tests/modules/migrate_plus_test/config/install/migrate_plus.migration.dummy.yml @@ -0,0 +1,21 @@ +langcode: en +status: true +dependencies: { } +id: dummy +label: Dummy migration +migration_tags: { } +source: + plugin: embedded_data + data_rows: + - + name: Dummy + ids: + name: + type: string +process: + name: name +destination: + plugin: null +migration_dependencies: + required: { } + optional: { } diff --git a/web/modules/migrate_plus/tests/modules/migrate_plus_test/config/install/migrate_plus.migration.fruit_terms.yml b/web/modules/migrate_plus/tests/modules/migrate_plus_test/config/install/migrate_plus.migration.fruit_terms.yml new file mode 100644 index 0000000000000000000000000000000000000000..7f606d127f01412455ceb8dc28c4b75e0456d454 --- /dev/null +++ b/web/modules/migrate_plus/tests/modules/migrate_plus_test/config/install/migrate_plus.migration.fruit_terms.yml @@ -0,0 +1,32 @@ +langcode: en +status: true +dependencies: { } +id: fruit_terms +label: Fruit Terms +class: null +field_plugin_method: null +cck_plugin_method: null +migration_tags: { } +migration_group: default +source: + plugin: embedded_data + data_rows: + - + name: Apple + - + name: Banana + - + name: Orange + ids: + name: + type: string + constants: + vocabulary: fruit +process: + name: name + vid: constants/vocabulary +destination: + plugin: entity:taxonomy_term +migration_dependencies: + required: { } + optional: { } diff --git a/web/modules/migrate_plus/tests/modules/migrate_plus_test/config/install/migrate_plus.migration_group.default.yml b/web/modules/migrate_plus/tests/modules/migrate_plus_test/config/install/migrate_plus.migration_group.default.yml new file mode 100644 index 0000000000000000000000000000000000000000..0f35a218958af5e973b980f989daeb17b3da19f4 --- /dev/null +++ b/web/modules/migrate_plus/tests/modules/migrate_plus_test/config/install/migrate_plus.migration_group.default.yml @@ -0,0 +1,9 @@ +langcode: en +status: true +dependencies: { } +id: default +label: Default +description: '' +source_type: '' +module: null +shared_configuration: null diff --git a/web/modules/migrate_plus/tests/modules/migrate_plus_test/migrate_plus_test.info.yml b/web/modules/migrate_plus/tests/modules/migrate_plus_test/migrate_plus_test.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..cec43eaa1425117f8e53d37415bbf18b8179c68a --- /dev/null +++ b/web/modules/migrate_plus/tests/modules/migrate_plus_test/migrate_plus_test.info.yml @@ -0,0 +1,13 @@ +type: module +name: Migrate Plus Test +description: 'Test module to test Migrate Plus.' +package: Testing +core_version_requirement: ^8.8 || ^9 +dependencies: + - drupal:migrate + - migrate_plus:migrate_plus + +# Information added by Drupal.org packaging script on 2020-04-30 +version: '8.x-5.1' +project: 'migrate_plus' +datestamp: 1588261062 diff --git a/web/modules/migrate_plus/tests/src/Functional/LoadTest.php b/web/modules/migrate_plus/tests/src/Functional/LoadTest.php index c9aceda72053b8cc05087edf6a12f57e9a560abc..02edda0ee98c3a5c252de00adf4f00ba0ea19661 100644 --- a/web/modules/migrate_plus/tests/src/Functional/LoadTest.php +++ b/web/modules/migrate_plus/tests/src/Functional/LoadTest.php @@ -13,14 +13,15 @@ class LoadTest extends BrowserTestBase { /** - * Modules to enable. - * - * @var array + * {@inheritdoc} */ - public static $modules = [ + protected static $modules = [ 'migrate_plus', 'migrate_example', + 'migrate_example_setup', 'migrate_example_advanced', + 'migrate_example_advanced_setup', + 'migrate_json_example', ]; /** @@ -33,7 +34,12 @@ class LoadTest extends BrowserTestBase { /** * {@inheritdoc} */ - protected function setUp() { + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { parent::setUp(); $this->user = $this->drupalCreateUser(['administer site configuration']); $this->drupalLogin($this->user); @@ -42,7 +48,7 @@ protected function setUp() { /** * Tests that the home page loads with a 200 response. */ - public function testLoad() { + public function testLoad(): void { $this->drupalGet(Url::fromRoute('<front>')); $this->assertSession()->statusCodeEquals(200); } diff --git a/web/modules/migrate_plus/tests/src/Kernel/EntityLookupAccessTest.php b/web/modules/migrate_plus/tests/src/Kernel/EntityLookupAccessTest.php new file mode 100644 index 0000000000000000000000000000000000000000..794484f99371d4192bc7b2bc5c1ed47638a558c1 --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Kernel/EntityLookupAccessTest.php @@ -0,0 +1,121 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Kernel; + +use Drupal\entity_test\Entity\EntityTest; +use Drupal\KernelTests\KernelTestBase; +use Drupal\migrate\MigrateExecutable; +use Drupal\migrate\Row; +use Drupal\Tests\user\Traits\UserCreationTrait; +use Drupal\user\Entity\User; + +/** + * Tests entity lookup access check. + * + * @group migrate_plus + */ +class EntityLookupAccessTest extends KernelTestBase { + use UserCreationTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'entity_test', + 'migrate', + 'migrate_plus', + 'system', + 'user', + ]; + + /** + * A user. + * + * @var \Drupal\user\Entity\User + */ + protected $user; + + /** + * A test entity. + * + * @var \Drupal\entity_test\Entity\EntityTest + */ + protected $entity; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->installSchema('system', ['sequences']); + $this->installEntitySchema('entity_test'); + $this->installEntitySchema('user'); + $this->installConfig('user'); + + $this->user = $this->createUser(['view all entity_test_query_access entities']); + $this->entity = EntityTest::create(['name' => $this->randomMachineName(8)]); + } + + /** + * Tests that access is honored for entity lookups. + */ + public function testEntityLookupAccessCheck(): void { + $definition = [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + ['id' => 1], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + ], + ], + 'process' => [], + 'destination' => [ + 'plugin' => 'entity:entity_test', + ], + ]; + $migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition); + $executable = new MigrateExecutable($migration); + $row = new Row(); + $configuration_base = [ + 'entity_type' => 'entity_test', + 'value_key' => 'id', + ]; + + // Set access_check true. + $configuration = $configuration_base + ['access_check' => TRUE]; + + // Test as anonymous. + $anonymous = User::getAnonymousUser(); + $this->setCurrentUser($anonymous); + $plugin = \Drupal::service('plugin.manager.migrate.process') + ->createInstance('entity_lookup', $configuration, $migration); + // Check the entity is not found. + $value = $plugin->transform($this->entity->id(), $executable, $row, 'id'); + $this->assertNull($value); + + // Test as authenticated user. + $this->setCurrentUser($this->user); + $plugin = \Drupal::service('plugin.manager.migrate.process') + ->createInstance('entity_lookup', $configuration, $migration); + // Check the entity is found. + $value = $plugin->transform($this->entity->id(), $executable, $row, 'id'); + $this->assertSame($this->entity->id(), $value); + + // Retest with access check false. + $configuration = $configuration_base + ['access_check' => FALSE]; + $plugin = \Drupal::service('plugin.manager.migrate.process') + ->createInstance('entity_lookup', $configuration, $migration); + + // Check the entity is found. + $value = $plugin->transform($this->entity->id(), $executable, $row, 'id'); + $this->assertSame($this->entity->id(), $value); + } + +} diff --git a/web/modules/migrate_plus/tests/src/Kernel/MigrateTableBatchTest.php b/web/modules/migrate_plus/tests/src/Kernel/MigrateTableBatchTest.php new file mode 100644 index 0000000000000000000000000000000000000000..4458b23667eb97ca8a933164e2a07a1f7307e5c6 --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Kernel/MigrateTableBatchTest.php @@ -0,0 +1,19 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Kernel; + +/** + * Verifies all tests pass with batching enabled, uneven batches. + * + * @group migrate + */ +class MigrateTableBatchTest extends MigrateTableTest { + + /** + * The batch size to configure (a size of 1 disables batching). + * + * @var int + */ + protected $batchSize = 2; + +} diff --git a/web/modules/migrate_plus/tests/src/Kernel/MigrateTableEvenBatchTest.php b/web/modules/migrate_plus/tests/src/Kernel/MigrateTableEvenBatchTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1c060e4ff3228c7021436f384ccc31cd49414fa6 --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Kernel/MigrateTableEvenBatchTest.php @@ -0,0 +1,19 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Kernel; + +/** + * Verifies all tests pass with batching enabled, even batches. + * + * @group migrate + */ +class MigrateTableEvenBatchTest extends MigrateTableTest { + + /** + * The batch size to configure (a size of 1 disables batching). + * + * @var int + */ + protected $batchSize = 3; + +} diff --git a/web/modules/migrate_plus/tests/src/Kernel/MigrateTableIncrementBatchTest.php b/web/modules/migrate_plus/tests/src/Kernel/MigrateTableIncrementBatchTest.php new file mode 100644 index 0000000000000000000000000000000000000000..32edb2a8f4784bab40f27d4245ddc7928f88ddb8 --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Kernel/MigrateTableIncrementBatchTest.php @@ -0,0 +1,19 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Kernel; + +/** + * Verifies all tests pass with batching enabled, uneven batches. + * + * @group migrate + */ +class MigrateTableIncrementBatchTest extends MigrateTableIncrementTest { + + /** + * The batch size to configure. + * + * @var int + */ + protected $batchSize = 2; + +} diff --git a/web/modules/migrate_plus/tests/src/Kernel/MigrateTableIncrementEvenBatchTest.php b/web/modules/migrate_plus/tests/src/Kernel/MigrateTableIncrementEvenBatchTest.php new file mode 100644 index 0000000000000000000000000000000000000000..66a4879ba81dde932ab07b1df3b133a3e3e23075 --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Kernel/MigrateTableIncrementEvenBatchTest.php @@ -0,0 +1,19 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Kernel; + +/** + * Verifies all tests pass with batching enabled, even batches. + * + * @group migrate + */ +class MigrateTableIncrementEvenBatchTest extends MigrateTableIncrementTest { + + /** + * The batch size to configure. + * + * @var int + */ + protected $batchSize = 3; + +} diff --git a/web/modules/migrate_plus/tests/src/Kernel/MigrateTableIncrementTest.php b/web/modules/migrate_plus/tests/src/Kernel/MigrateTableIncrementTest.php new file mode 100644 index 0000000000000000000000000000000000000000..c0db133e21136eee550d83f9c63401c59db0fd90 --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Kernel/MigrateTableIncrementTest.php @@ -0,0 +1,144 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Kernel; + +use Drupal\migrate\MigrateExecutable; +use Drupal\Tests\migrate\Kernel\MigrateTestBase; + +/** + * Tests migration destination table with auto-increment keys. + * + * @group migrate + */ +class MigrateTableIncrementTest extends MigrateTestBase { + + const TABLE_NAME = 'migrate_test_destination_table'; + + /** + * The database connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $connection; + + /** + * {@inheritdoc} + */ + public static $modules = ['migrate_plus']; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->connection = $this->container->get('database'); + $this->connection->schema()->createTable(static::TABLE_NAME, [ + 'description' => 'Test table', + 'fields' => [ + 'id' => [ + 'type' => 'serial', + 'not null' => TRUE, + ], + 'data1' => [ + 'type' => 'varchar', + 'length' => '32', + 'not null' => TRUE, + ], + 'data2' => [ + 'type' => 'varchar', + 'length' => '32', + 'not null' => TRUE, + ], + ], + 'primary key' => ['id'], + ]); + } + + /** + * {@inheritdoc} + */ + protected function tearDown(): void { + $this->connection->schema()->dropTable(static::TABLE_NAME); + parent::tearDown(); + } + + /** + * Create a minimally valid migration with some source data. + * + * @return array + * The migration definition. + */ + public function tableDestinationMigration(): array { + return [ + 'dummy table' => [ + [ + 'id' => 'migration_table_test', + 'migration_tags' => ['Testing'], + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'data1' => 'dummy1 value1', + 'data2' => 'dummy2 value1', + ], + [ + 'data1' => 'dummy1 value2', + 'data2' => 'dummy2 value2', + ], + [ + 'data1' => 'dummy1 value3', + 'data2' => 'dummy2 value3', + ], + ], + 'ids' => [ + 'data1' => ['type' => 'string'], + ], + ], + 'destination' => [ + 'plugin' => 'table', + 'table_name' => static::TABLE_NAME, + 'id_fields' => [ + 'id' => [ + 'type' => 'integer', + 'use_auto_increment' => TRUE, + ], + ], + ], + 'process' => [ + 'data1' => 'data1', + 'data2' => 'data2', + ], + ], + ], + ]; + } + + /** + * Tests table destination. + * + * @param array $definition + * The migration definition. + * + * @dataProvider tableDestinationMigration + * + * @throws \Drupal\migrate\MigrateException + */ + public function testTableDestination(array $definition) { + $migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition); + + $executable = new MigrateExecutable($migration, $this); + $executable->import(); + + $values = $this->connection->select(static::TABLE_NAME) + ->fields(static::TABLE_NAME) + ->execute() + ->fetchAllAssoc('data1'); + + $this->assertEquals(1, $values['dummy1 value1']->id); + $this->assertEquals(2, $values['dummy1 value2']->id); + $this->assertEquals(3, $values['dummy1 value3']->id); + $this->assertEquals('dummy2 value3', $values['dummy1 value3']->data2); + $this->assertCount(3, $values); + } + +} diff --git a/web/modules/migrate_plus/tests/src/Kernel/MigrateTableTest.php b/web/modules/migrate_plus/tests/src/Kernel/MigrateTableTest.php index dc471770d711668f07e7e8fe2b136388e753b9f2..94d75b8913fe41158152f8790df2ed1a50c21832 100644 --- a/web/modules/migrate_plus/tests/src/Kernel/MigrateTableTest.php +++ b/web/modules/migrate_plus/tests/src/Kernel/MigrateTableTest.php @@ -2,8 +2,9 @@ namespace Drupal\Tests\migrate_plus\Kernel; -use Drupal\Core\Database\Database; use Drupal\migrate\MigrateExecutable; +use Drupal\migrate\Plugin\MigrateIdMapInterface; +use Drupal\migrate\Row; use Drupal\Tests\migrate\Kernel\MigrateTestBase; /** @@ -13,7 +14,8 @@ */ class MigrateTableTest extends MigrateTestBase { - const TABLE_NAME = 'migrate_test_destination_table'; + const SOURCE_TABLE_NAME = 'migrate_test_source_table'; + const DEST_TABLE_NAME = 'migrate_test_destination_table'; /** * The database connection. @@ -22,44 +24,83 @@ class MigrateTableTest extends MigrateTestBase { */ protected $connection; + /** + * The batch size to configure (a size of 1 disables batching). + * + * @var int + */ + protected $batchSize = 1; + + /** + * {@inheritdoc} + */ public static $modules = ['migrate_plus']; /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); - $this->connection = Database::getConnection(); - - $this->connection->schema()->createTable(static::TABLE_NAME, [ - 'description' => 'Test table', - 'fields' => [ - 'data' => [ - 'type' => 'varchar', - 'length' => '32', - 'not null' => TRUE, - ], - 'data2' => [ - 'type' => 'varchar', - 'length' => '32', - 'not null' => TRUE, - ], - 'data3' => [ - 'type' => 'varchar', - 'length' => '32', - 'not null' => TRUE, + $this->connection = $this->container->get('database'); + $connections = [ + static::SOURCE_TABLE_NAME => $this->sourceDatabase, + static::DEST_TABLE_NAME => $this->connection, + ]; + foreach ($connections as $table => $connection) { + $connection->schema()->createTable($table, [ + 'description' => 'Test table', + 'fields' => [ + 'data' => [ + 'type' => 'varchar', + 'length' => '32', + 'not null' => TRUE, + ], + 'data2' => [ + 'type' => 'varchar', + 'length' => '32', + 'not null' => TRUE, + ], + 'data3' => [ + 'type' => 'varchar', + 'length' => '32', + 'not null' => TRUE, + ], ], + 'primary key' => ['data'], + ]); + } + $query = $this->sourceDatabase->insert(static::SOURCE_TABLE_NAME) + ->fields(['data', 'data2', 'data3']); + $values = [ + [ + 'data' => 'dummy value', + 'data2' => 'dummy2 value', + 'data3' => 'dummy3 value', + ], + [ + 'data' => 'dummy value2', + 'data2' => 'dummy2 value2', + 'data3' => 'dummy3 value2', ], - 'primary key' => ['data'], - ]); + [ + 'data' => 'dummy value3', + 'data2' => 'dummy2 value3', + 'data3' => 'dummy3 value3', + ], + ]; + foreach ($values as $record) { + $query->values($record); + } + $query->execute(); } /** * {@inheritdoc} */ - protected function tearDown() { - $this->connection->schema()->dropTable(static::TABLE_NAME); + protected function tearDown(): void { + $this->sourceDatabase->schema()->dropTable(static::SOURCE_TABLE_NAME); + $this->connection->schema()->dropTable(static::DEST_TABLE_NAME); parent::tearDown(); } @@ -69,37 +110,72 @@ protected function tearDown() { * @return array * The migration definition. */ - protected function getTableDestinationMigration() { - $definition = [ - 'id' => 'migration_table_test', - 'migration_tags' => ['Testing'], - 'source' => [ - 'plugin' => 'embedded_data', - 'data_rows' => [ - [ - 'data' => 'dummy value', - 'data2' => 'dummy2 value', - 'data3' => 'dummy3 value', + public function tableDestinationMigration() { + return [ + 'dummy table' => [ + [ + 'id' => 'migration_table_test', + 'migration_tags' => ['Testing'], + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'data' => 'dummy1 value1', + 'data2' => 'dummy2 value1', + ], + [ + 'data' => 'dummy1 value2', + 'data2' => 'dummy2 value2', + ], + [ + 'data' => 'dummy1 value3', + 'data2' => 'dummy2 value3', + ], + ], + 'ids' => [ + 'data' => ['type' => 'string'], + ], ], - [ - 'data' => 'dummy value2', - 'data2' => 'dummy2 value2', - 'data3' => 'dummy3 value2', + 'destination' => [ + 'plugin' => 'table', + 'table_name' => static::DEST_TABLE_NAME, + 'id_fields' => [ + 'data' => [ + 'type' => 'string', + ], + ], ], - [ - 'data' => 'dummy value3', - 'data2' => 'dummy2 value3', - 'data3' => 'dummy3 value3', + 'process' => [ + 'data' => 'data', + 'data2' => 'data2', ], ], - 'ids' => [ + ], + ]; + } + + /** + * Tests table migration. + */ + public function testTableMigration(): void { + $definition = [ + 'id' => 'migration_table_test', + 'migration_tags' => ['Testing'], + 'source' => [ + 'plugin' => 'table', + 'table_name' => static::SOURCE_TABLE_NAME, + 'id_fields' => [ 'data' => ['type' => 'string'], ], + 'ignore_map' => TRUE, ], 'destination' => [ 'plugin' => 'table', - 'table_name' => static::TABLE_NAME, - 'id_fields' => ['data' => ['type' => 'string']], + 'table_name' => static::DEST_TABLE_NAME, + 'id_fields' => [ + 'data' => ['type' => 'string'], + ], + 'batch_size' => $this->batchSize, ], 'process' => [ 'data' => 'data', @@ -107,20 +183,13 @@ protected function getTableDestinationMigration() { 'data3' => 'data3', ], ]; - return $definition; - } - - /** - * Tests table destination. - */ - public function testTableDestination() { - $migration = \Drupal::service('plugin.manager.migration')->createStubMigration($this->getTableDestinationMigration()); + $migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition); $executable = new MigrateExecutable($migration, $this); $executable->import(); - $values = $this->connection->select(static::TABLE_NAME) - ->fields(static::TABLE_NAME) + $values = $this->connection->select(static::DEST_TABLE_NAME) + ->fields(static::DEST_TABLE_NAME) ->execute() ->fetchAllAssoc('data'); @@ -129,35 +198,39 @@ public function testTableDestination() { $this->assertEquals('dummy2 value2', $values['dummy value2']->data2); $this->assertEquals('dummy3 value3', $values['dummy value3']->data3); $this->assertEquals(3, count($values)); - } - - /** - * Tests table rollback. - */ - public function testTableRollback() { - $this->testTableDestination(); - - /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ - $migration = \Drupal::service('plugin.manager.migration')->createStubMigration($this->getTableDestinationMigration()); - $executable = new MigrateExecutable($migration, $this); - $executable->import(); - - $values = $this->connection->select(static::TABLE_NAME) - ->fields(static::TABLE_NAME) - ->execute() - ->fetchAllAssoc('data'); - - $this->assertEquals('dummy value', $values['dummy value']->data); - $this->assertEquals(3, count($values)); // Now rollback. $executable->rollback(); - $values = $this->connection->select(static::TABLE_NAME) - ->fields(static::TABLE_NAME) + $values = $this->connection->select(static::DEST_TABLE_NAME) + ->fields(static::DEST_TABLE_NAME) ->execute() ->fetchAllAssoc('data'); $this->assertEquals(0, count($values)); } + /** + * Tests table update. + * + * @dataProvider tableDestinationMigration + */ + public function testTableUpdate(array $definition): void { + // Make sure migration overwrites the original data for the first row. + $original_values = [ + 'data' => 'dummy value', + 'data2' => 'original value 2', + 'data3' => 'original value 3', + ]; + $this->connection->insert(static::DEST_TABLE_NAME) + ->fields($original_values) + ->execute(); + + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = \Drupal::service('plugin.manager.migration') + ->createStubMigration($definition); + $migration->getIdMap()->saveIdMapping(new Row($original_values, + ['data' => 'dummy value']), ['data' => 'dummy value'], MigrateIdMapInterface::STATUS_NEEDS_UPDATE); + $this->testTableMigration(); + } + } diff --git a/web/modules/migrate_plus/tests/src/Kernel/MigrationConfigEntityTest.php b/web/modules/migrate_plus/tests/src/Kernel/MigrationConfigEntityTest.php index 354da7628af33b165db8bce8239e59661d942b14..b6e2b9b696a4e86eb6bdc9190e43cc0bae41a3e4 100644 --- a/web/modules/migrate_plus/tests/src/Kernel/MigrationConfigEntityTest.php +++ b/web/modules/migrate_plus/tests/src/Kernel/MigrationConfigEntityTest.php @@ -2,17 +2,30 @@ namespace Drupal\Tests\migrate_plus\Kernel; -use Drupal\KernelTests\KernelTestBase; +use Drupal\Component\Plugin\Exception\PluginNotFoundException; +use Drupal\migrate\MigrateExecutable; use Drupal\migrate_plus\Entity\Migration; +use Drupal\Tests\migrate\Kernel\MigrateTestBase; /** * Test migration config entity discovery. * * @group migrate_plus */ -class MigrationConfigEntityTest extends KernelTestBase { +class MigrationConfigEntityTest extends MigrateTestBase { - public static $modules = ['migrate', 'migrate_plus']; + /** + * {@inheritdoc} + */ + public static $modules = [ + 'migrate', + 'migrate_plus', + 'migrate_plus_test', + 'taxonomy', + 'text', + 'system', + 'user', + ]; /** * The plugin manager. @@ -24,17 +37,21 @@ class MigrationConfigEntityTest extends KernelTestBase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); $this->pluginManager = \Drupal::service('plugin.manager.migration'); + $this->installConfig('migrate_plus'); + $this->installEntitySchema('taxonomy_term'); + $this->installSchema('system', ['key_value', 'key_value_expire']); } /** * Tests cache invalidation. */ - public function testCacheInvalidation() { + public function testCacheInvalidation(): void { $config = Migration::create([ 'id' => 'test', + 'status' => TRUE, 'label' => 'Label A', 'migration_tags' => [], 'source' => [], @@ -43,7 +60,7 @@ public function testCacheInvalidation() { ]); $config->save(); - $this->assertTrue($this->pluginManager->getDefinition('test')); + $this->assertNotEmpty($this->pluginManager->getDefinition('test')); $this->assertSame('Label A', $this->pluginManager->getDefinition('test')['label']); // Clear static cache in the plugin manager, the cache tag take care of the @@ -57,4 +74,55 @@ public function testCacheInvalidation() { $this->assertSame('Label B', $this->pluginManager->getDefinition('test')['label']); } + /** + * Tests migration status. + */ + public function testMigrationStatus(): void { + $configs = [ + [ + 'id' => 'test_active', + 'status' => TRUE, + 'label' => 'Label Active', + 'migration_tags' => [], + 'source' => [], + 'destination' => [], + 'migration_dependencies' => [], + ], + [ + 'id' => 'test_inactive', + 'status' => FALSE, + 'label' => 'Label Inactive', + 'migration_tags' => [], + 'source' => [], + 'destination' => [], + 'migration_dependencies' => [], + ], + ]; + + foreach ($configs as $config) { + Migration::create($config)->save(); + } + + $definitions = $this->pluginManager->getDefinitions(); + $this->assertCount(1, $definitions); + $this->assertArrayHasKey('test_active', $definitions); + + $this->expectException(PluginNotFoundException::class); + $this->expectExceptionMessage('The "test_inactive" plugin does not exist.'); + $this->pluginManager->getDefinition('test_inactive'); + } + + /** + * Tests migration from configuration. + */ + public function testImport(): void { + $this->installConfig('migrate_plus_test'); + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = $this->pluginManager->createInstance('fruit_terms'); + $id_map = $migration->getIdMap(); + $executable = new MigrateExecutable($migration, $this); + $executable->import(); + $this->assertSame(3, $id_map->importedCount()); + } + } diff --git a/web/modules/migrate_plus/tests/src/Kernel/MigrationGroupTest.php b/web/modules/migrate_plus/tests/src/Kernel/MigrationGroupTest.php index 53d3ad467d52104f626c74b256e539a0c4a5d087..138cca38fcf0e12f8b1e57d4493f805fb656f708 100644 --- a/web/modules/migrate_plus/tests/src/Kernel/MigrationGroupTest.php +++ b/web/modules/migrate_plus/tests/src/Kernel/MigrationGroupTest.php @@ -13,12 +13,15 @@ */ class MigrationGroupTest extends KernelTestBase { - public static $modules = ['migrate', 'migrate_plus']; + /** + * {@inheritdoc} + */ + public static $modules = ['migrate', 'migrate_plus', 'migrate_plus_test']; /** * Test that group configuration is properly merged into specific migrations. */ - public function testConfigurationMerge() { + public function testConfigurationMerge(): void { $group_id = 'test_group'; /** @var \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group */ @@ -69,10 +72,9 @@ public function testConfigurationMerge() { $migration->save(); $expected_config = [ - 'migration_group' => $group_id, 'label' => 'Unaffected by the group', - 'migration_tags' => ['Drupal 7'], - 'source' => [ + 'getMigrationTags' => ['Drupal 7'], + 'getSourceConfiguration' => [ 'plugin' => 'empty', 'constants' => [ 'entity_type' => 'user', @@ -80,13 +82,13 @@ public function testConfigurationMerge() { 'cardinality' => '3', ], ], - 'destination' => ['plugin' => 'field_storage_config'], + 'getDestinationConfiguration' => ['plugin' => 'field_storage_config'], ]; - /** @var \Drupal\migrate\Plugin\MigrationInterface $loaded_migration */ + /** @var \Drupal\migrate_plus\Plugin\MigrationInterface $loaded_migration */ $loaded_migration = $this->container->get('plugin.manager.migration') ->createInstance('specific_migration'); - foreach ($expected_config as $key => $expected_value) { - $actual_value = $loaded_migration->get($key); + foreach ($expected_config as $method => $expected_value) { + $actual_value = $loaded_migration->$method(); $this->assertEquals($expected_value, $actual_value); } } @@ -94,7 +96,7 @@ public function testConfigurationMerge() { /** * Test that deleting a group deletes its migrations. */ - public function testDelete() { + public function testDelete(): void { /** @var \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group */ $group_configuration = [ 'id' => 'test_group', @@ -125,4 +127,16 @@ public function testDelete() { $this->assertNull($loaded_migration); } + /** + * Test that migrations without a group are assigned to the default group. + */ + public function testDefaultGroup(): void { + $this->installConfig('migrate_plus_test'); + + /** @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface $pluginManager */ + $pluginManager = \Drupal::service('plugin.manager.migration'); + $migration = $pluginManager->getDefinition('dummy'); + $this->assertEqual($migration['migration_group'], 'default', 'Migrations without an explicit group are assigned the default group.'); + } + } diff --git a/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate/process/DefaultEntityValueTest.php b/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate/process/DefaultEntityValueTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d3ee124ddf9809297ea1e116708db67ee277fcc7 --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate/process/DefaultEntityValueTest.php @@ -0,0 +1,76 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Kernel\Plugin\migrate\process; + +use Drupal\KernelTests\KernelTestBase; +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\Plugin\MigrateDestinationInterface; +use Drupal\migrate\Plugin\MigrationInterface; +use Drupal\migrate\Row; +use Drupal\Tests\user\Traits\UserCreationTrait; + +/** + * Tests the default_entity_value plugin. + * + * @coversDefaultClass \Drupal\migrate_plus\Plugin\migrate\process\DefaultEntityValue + * @group migrate_plus + */ +class DefaultEntityValueTest extends KernelTestBase { + + use UserCreationTrait; + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'migrate_plus', + 'migrate', + 'user', + 'system', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->installSchema('system', ['sequences']); + $this->installEntitySchema('user'); + } + + /** + * Tests the lookup when the value is empty. + * + * @covers ::transform + */ + public function testDefaultEntityValue(): void { + // Create a user. + $editorial_user = $this->createUser([], 'editorial'); + $journalist_user = $this->createUser([], 'journalist'); + // Setup test migration objects. + $migration_prophecy = $this->prophesize(MigrationInterface::class); + $migrate_destination_prophecy = $this->prophesize(MigrateDestinationInterface::class); + $migrate_destination_prophecy->getPluginId()->willReturn('user'); + $migrate_destination = $migrate_destination_prophecy->reveal(); + $migration_prophecy->getDestinationPlugin()->willReturn($migrate_destination); + $migration_prophecy->getProcess()->willReturn([]); + $migration = $migration_prophecy->reveal(); + $configuration = [ + 'entity_type' => 'user', + 'value_key' => 'name', + 'ignore_case' => TRUE, + 'default_value' => 'editorial', + ]; + $plugin = \Drupal::service('plugin.manager.migrate.process') + ->createInstance('default_entity_value', $configuration, $migration); + $executable = $this->prophesize(MigrateExecutableInterface::class)->reveal(); + $row = new Row(); + // Check the case default value is not used. + $value = $plugin->transform($journalist_user->id(), $executable, $row, 'name'); + $this->assertSame($journalist_user->id(), $value); + // Check the default value is found. + $value = $plugin->transform('', $executable, $row, 'name'); + $this->assertSame($editorial_user->id(), $value); + } + +} diff --git a/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate/process/EntityGenerateTest.php b/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate/process/EntityGenerateTest.php index 3d46496de479d92ba2507ca6506dcbad65733551..2b3fb728e709353f30d1a79eb9e03e7192df29de 100644 --- a/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate/process/EntityGenerateTest.php +++ b/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate/process/EntityGenerateTest.php @@ -5,12 +5,15 @@ use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Language\LanguageInterface; -use Drupal\field\Tests\EntityReference\EntityReferenceTestTrait; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; use Drupal\KernelTests\KernelTestBase; use Drupal\migrate\MigrateExecutable; use Drupal\migrate\MigrateMessageInterface; use Drupal\node\Entity\NodeType; +use Drupal\taxonomy\Entity\Term; use Drupal\taxonomy\Entity\Vocabulary; +use Drupal\Tests\field\Traits\EntityReferenceTestTrait; /** * Tests the migration plugin. @@ -25,7 +28,7 @@ class EntityGenerateTest extends KernelTestBase implements MigrateMessageInterfa /** * {@inheritdoc} */ - public static $modules = [ + protected static $modules = [ 'migrate_plus', 'migrate', 'user', @@ -68,7 +71,7 @@ class EntityGenerateTest extends KernelTestBase implements MigrateMessageInterfa /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); // Create article content type. $values = [ @@ -84,7 +87,7 @@ protected function setUp() { $this->installEntitySchema('user'); $this->installSchema('system', ['sequences']); $this->installSchema('user', 'users_data'); - $this->installConfig($this->modules); + $this->installConfig(self::$modules); // Create a vocabulary. $vocabulary = Vocabulary::create([ @@ -107,6 +110,18 @@ protected function setUp() { FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED ); + // Create a non-reference field. + FieldStorageConfig::create([ + 'field_name' => 'field_integer', + 'type' => 'integer', + 'entity_type' => 'node', + ])->save(); + FieldConfig::create([ + 'field_name' => 'field_integer', + 'entity_type' => 'node', + 'bundle' => $this->bundle, + ])->save(); + $this->migrationPluginManager = \Drupal::service('plugin.manager.migration'); } @@ -117,7 +132,7 @@ protected function setUp() { * * @covers ::transform */ - public function testTransform(array $definition, array $expected, array $preSeed = []) { + public function testTransform(array $definition, array $expected, array $preSeed = []): void { // Pre seed some test data. foreach ($preSeed as $storageName => $values) { // If the first element of $values is a non-empty array, create multiple @@ -134,8 +149,11 @@ public function testTransform(array $definition, array $expected, array $preSeed /** @var \Drupal\migrate\Plugin\Migration $migration */ $migration = $this->migrationPluginManager->createStubMigration($definition); - /** @var EntityStorageBase $storage */ - $storage = $this->readAttribute($migration->getDestinationPlugin(), 'storage'); + $reflector = new \ReflectionObject($migration->getDestinationPlugin()); + $attribute = $reflector->getProperty('storage'); + $attribute->setAccessible(true); + /** @var \Drupal\Core\Entity\EntityStorageBase $storage */ + $storage = $attribute->getValue($migration->getDestinationPlugin()); $migrationExecutable = (new MigrateExecutable($migration, $this)); $migrationExecutable->import(); @@ -156,14 +174,14 @@ public function testTransform(array $definition, array $expected, array $preSeed foreach ($valueToCheck as $key => $expectedValue) { if (empty($expectedValue)) { if (!$entity->{$property}->isEmpty()) { - $this->assertTrue($entity->{$property}[0]->entity->$key->isEmpty(), "Expected value is empty but field $property.$key is not empty."); + $this->assertTrue($entity->{$property}[0]->entity->{$key}->isEmpty(), "Expected value is empty but field $property.$key is not empty."); } else { - $this->assertTrue($entity->{$property}->isEmpty(), "FOOBAR Expected value is empty but field $property is not empty."); + $this->assertTrue($entity->{$property}->isEmpty(), "Expected value is empty but field $property is not empty."); } } elseif ($entity->{$property}->getValue()) { - $this->assertEquals($expectedValue, $entity->{$property}[$valueID]->entity->$key->value); + $this->assertEquals($expectedValue, $entity->get($property)->offsetGet($valueID)->entity->{$key}->value); } else { $this->fail("Expected value: $expectedValue does not exist in $property."); @@ -177,7 +195,7 @@ public function testTransform(array $definition, array $expected, array $preSeed foreach ($value as $key => $expectedValue) { if (empty($expectedValue)) { if (!$entity->{$property}->isEmpty()) { - $this->assertTrue($entity->{$property}[0]->entity->$key->isEmpty(), "Expected value is empty but field $property.$key is not empty."); + $this->assertTrue($entity->{$property}[0]->entity->{$key}->isEmpty(), "Expected value is empty but field $property.$key is not empty."); } else { $this->assertTrue($entity->{$property}->isEmpty(), "BINBAZ Expected value is empty but field $property is not empty."); @@ -203,6 +221,99 @@ public function testTransform(array $definition, array $expected, array $preSeed } } + /** + * Test lookup without a reference field. + */ + public function testNonReferenceField() { + $values = [ + 'name' => 'Apples', + 'vid' => $this->vocabulary, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ]; + $this->createTestData('taxonomy_term', $values); + + // Not enough context is provided for a non reference field, so error out. + $definition = [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'title' => 'content item 1', + 'term' => 'Apples', + ], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + ], + ], + 'process' => [ + 'id' => 'id', + 'type' => [ + 'plugin' => 'default_value', + 'default_value' => $this->bundle, + ], + 'title' => 'title', + 'field_integer' => [ + 'plugin' => 'entity_generate', + 'source' => 'term', + ], + ], + 'destination' => [ + 'plugin' => 'entity:node', + ], + ]; + /** @var \Drupal\migrate\Plugin\Migration $migration */ + $migration = $this->migrationPluginManager->createStubMigration($definition); + $migrationExecutable = (new MigrateExecutable($migration, $this)); + $migrationExecutable->import(); + $this->assertEquals('Destination field type integer is not a recognized reference type.', $migration->getIdMap()->getMessages()->fetch()->message); + $this->assertSame(1, $migration->getIdMap()->messageCount()); + + // Enough context is provided so this should work. + $definition = [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'title' => 'content item 1', + 'term' => 'Apples', + ], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + ], + ], + 'process' => [ + 'id' => 'id', + 'type' => [ + 'plugin' => 'default_value', + 'default_value' => $this->bundle, + ], + 'title' => 'title', + 'field_integer' => [ + 'plugin' => 'entity_generate', + 'source' => 'term', + 'value_key' => 'name', + 'bundle_key' => 'vid', + 'bundle' => $this->vocabulary, + 'entity_type' => 'taxonomy_term', + ], + ], + 'destination' => [ + 'plugin' => 'entity:node', + ], + ]; + /** @var \Drupal\migrate\Plugin\Migration $migration */ + $migration = $this->migrationPluginManager->createStubMigration($definition); + $migrationExecutable = (new MigrateExecutable($migration, $this)); + $migrationExecutable->import(); + $this->assertEmpty($migration->getIdMap()->messageCount()); + $term = Term::load(1); + $this->assertEquals('Apples', $term->label()); + } + /** * Provides multiple migration definitions for "transform" test. */ @@ -444,6 +555,96 @@ public function transformDataProvider() { ], ], ], + 'provide multiple values' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'title' => 'content item 1', + 'term' => 'Apples', + ], + [ + 'id' => 2, + 'title' => 'content item 2', + 'term' => 'Bananas', + ], + [ + 'id' => 3, + 'title' => 'content item 3', + 'term' => 'Grapes', + ], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + ], + 'constants' => [ + 'foo' => 'bar', + ], + ], + 'process' => [ + 'id' => 'id', + 'type' => [ + 'plugin' => 'default_value', + 'default_value' => $this->bundle, + ], + 'title' => 'title', + 'term_upper' => [ + 'plugin' => 'callback', + 'source' => 'term', + 'callable' => 'strtoupper', + ], + $this->fieldName => [ + 'plugin' => 'entity_generate', + 'source' => 'term', + 'values' => [ + 'name' => '@term_upper', + 'description' => 'constants/foo', + ], + ], + ], + 'destination' => [ + 'plugin' => 'entity:node', + ], + ], + 'expected' => [ + 'row 1' => [ + 'id' => 1, + 'title' => 'content item 1', + $this->fieldName => [ + 'tid' => 2, + 'name' => 'APPLES', + 'description' => 'bar', + ], + ], + 'row 2' => [ + 'id' => 2, + 'title' => 'content item 2', + $this->fieldName => [ + 'tid' => 3, + 'name' => 'BANANAS', + 'description' => 'bar', + ], + ], + 'row 3' => [ + 'id' => 3, + 'title' => 'content item 3', + $this->fieldName => [ + 'tid' => 1, + 'name' => 'Grapes', + 'description' => NULL, + ], + ], + ], + 'pre seed' => [ + 'taxonomy_term' => [ + 'name' => 'Grapes', + 'vid' => $this->vocabulary, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + ], + ], 'lookup single existing term returns correct term' => [ 'definition' => [ 'source' => [ @@ -775,6 +976,9 @@ public function display($message, $type = 'status') { * The storage manager to create. * @param array $values * The values to use when creating the entity. + * + * @return string|int + * The entity identifier. */ private function createTestData($storageName, array $values) { /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ @@ -783,6 +987,7 @@ private function createTestData($storageName, array $values) { ->getStorage($storageName); $entity = $storage->create($values); $entity->save(); + return $entity->id(); } } diff --git a/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate/process/EntityLookupTest.php b/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate/process/EntityLookupTest.php new file mode 100644 index 0000000000000000000000000000000000000000..8793609cfdf095cc9693f821814095696d81f3aa --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate/process/EntityLookupTest.php @@ -0,0 +1,76 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Kernel\Plugin\migrate\process; + +use Drupal\KernelTests\KernelTestBase; +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\Plugin\MigrateDestinationInterface; +use Drupal\migrate\Plugin\MigrationInterface; +use Drupal\migrate\Row; +use Drupal\Tests\user\Traits\UserCreationTrait; + +/** + * Tests the entity_lookup plugin. + * + * @coversDefaultClass \Drupal\migrate_plus\Plugin\migrate\process\EntityLookup + * @group migrate_plus + */ +class EntityLookupTest extends KernelTestBase { + + use UserCreationTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'migrate_plus', + 'migrate', + 'user', + 'system', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->installSchema('system', ['sequences']); + $this->installEntitySchema('user'); + } + + /** + * Lookup an entity without bundles on destination key. + * + * Using user entity as destination entity without bundles as example for + * testing. + * + * @covers ::transform + */ + public function testLookupEntityWithoutBundles(): void { + // Create a user. + $known_user = $this->createUser([], 'lucuma'); + // Setup test migration objects. + $migration_prophecy = $this->prophesize(MigrationInterface::class); + $migrate_destination_prophecy = $this->prophesize(MigrateDestinationInterface::class); + $migrate_destination_prophecy->getPluginId()->willReturn('user'); + $migrate_destination = $migrate_destination_prophecy->reveal(); + $migration_prophecy->getDestinationPlugin()->willReturn($migrate_destination); + $migration_prophecy->getProcess()->willReturn([]); + $migration = $migration_prophecy->reveal(); + $configuration = [ + 'entity_type' => 'user', + 'value_key' => 'name', + ]; + $plugin = \Drupal::service('plugin.manager.migrate.process') + ->createInstance('entity_lookup', $configuration, $migration); + $executable = $this->prophesize(MigrateExecutableInterface::class)->reveal(); + $row = new Row(); + // Check the known user is found. + $value = $plugin->transform('lucuma', $executable, $row, 'name'); + $this->assertSame($known_user->id(), $value); + // Check an unknown user is not found. + $value = $plugin->transform('orange', $executable, $row, 'name'); + $this->assertNull($value); + } + +} diff --git a/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate/process/FileBlobTest.php b/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate/process/FileBlobTest.php new file mode 100644 index 0000000000000000000000000000000000000000..f681628b12e1434a6ecb325002706de104128619 --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate/process/FileBlobTest.php @@ -0,0 +1,224 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Kernel\Plugin\migrate\process; + +use Drupal\Core\File\FileSystemInterface; +use Drupal\KernelTests\KernelTestBase; +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\Row; +use Drupal\Tests\user\Traits\UserCreationTrait; + +/** + * Tests the file_blob plugin. + * + * @coversDefaultClass \Drupal\migrate_plus\Plugin\migrate\process\FileBlob + * @group migrate_plus + */ +class FileBlobTest extends KernelTestBase { + + use UserCreationTrait; + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'migrate', + 'migrate_plus', + 'system', + ]; + + /** + * The process plugin manager. + * + * @var \Drupal\migrate\Plugin\MigratePluginManagerInterface + */ + protected $pluginManager; + + /** + * The blob representation of a cat image. + * + * @var string + */ + protected $blob; + + /** + * The sha1sum of the blob. + * + * @var string + */ + protected $sha1sum; + + /** + * The filesystem interface. + * + * @var \Drupal\Core\File\FileSystemInterface + */ + protected $filesystem; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $this->pluginManager = $this->container->get('plugin.manager.migrate.process'); + $this->filesystem = $this->container->get('file_system'); + $this->sha1sum = 'a8eb2b9a987cfda507356d884f0498289bd0f620'; + $this->blob = <<<EOT +/9j/4AAQSkZJRgABAQEAYABgAAD//gA7Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg +SlBFRyB2NjIpLCBxdWFsaXR5ID0gNjUK/9sAQwALCAgKCAcLCgkKDQwLDREcEhEPDxEiGRoUHCkk +KyooJCcnLTJANy0wPTAnJzhMOT1DRUhJSCs2T1VORlRAR0hF/9sAQwEMDQ0RDxEhEhIhRS4nLkVF +RUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVF/8AAEQgBLADI +AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMF +BQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkq +NDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqi +o6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/E +AB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMR +BAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVG +R0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKz +tLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A +jA4pwFCqRUgHNcZ1iAcVIKQU8CmAoFOFA6UUCFpcUmaWgBRS0lKAaBC0UlGaAHZoBqMnilDcUASi +jFIDTqAExSGnU00AMYCmECnnrSEUhkeKYVqU1GxpDGEU04FDnFNGKB3ExminZzRQFyQJTvLqUKKf +5Z28VRNyALSqM5qTbgc0BMHPrQK5HjBxT9ppzJnFOAxQFxm00u2pNooI4oC5GBTsUxnxxmhHycZo +AcaaalxUbjFMCMkmnAetN46U4ZFAEg6/Sl3UzPNAPrQBJnimg45NNJ4psjYAFIBVGcn1odto96QE +KuSelQNNk5NAx5baMmoWlCjc3WoXuCzBYgXY9hU8WjzSnzLuTywf4R1qlFshzSKTXKl/vU8TA8J+ +Na0Ok2IPyozVK2iwuuI12/Sq5UR7RmRvB78UVNNpT278nIoqGrGik2XhtK80udn+7WbBfo4wGq1H +cCRSpNKwFn5WFMHDbarLcqpKMcGmy3AU9eaYi2SAcVG8oXBNZz6gAcZqJ71WXOeDTsK5rLcruxmm +PcgZGawheHf14oluz0znNPlDmL8t3jnPNNjvOeTWS05YAk8CkabaSN3U07C5joor0OcE81JcXAVO +DXNJc+W5Oc06W9aQgZpWHzG3Hcru5PNSC5UmudW6KjmpftbHPPaiwuY3knDE8055wuBmsKK+2t1z +Tnu9zcnAosHMbnnAgCoZZwZVGayf7QAGF5pyJc3DKUUgE0coc5durwKdgNOtLOe/O7Plxd2NTWml +RxOZbg+Yw7dhWzBDJMBlRHEOnqapIhybIbe1gtFCwrub+8amSAzuWfO0d/WppXtrdP3jgAfwg9aq +3F8hh3SN5UA/An2FVbuSSPIF+SDBxwW7CljZ85Zh9axpdbjCnyUCqOmaoNqzzyqjSEg9hUtlJM6G +6vIlO0fO/wCgorHWbfIeKKzbNYxVik9pHvymUb26U1/tEOGQ7wPSrE6bDxn2zVLz2RuuM+tURchl +vnEmXBFNm1AyJweRVhniuUKSKAR3rFnjeByP4e1UlcltlvzzIMmk3vgjqKggkwfm6VckACZU9uad +hXI0DHkUBmGajjmxle+aWVynXvQAgfP9ajLlnGaieTaPY1HJMARjvTAtq+0nPbrTwSWAHU1TWXIy +asxn+IHHFIYr53EE1JEpZapSSEuTnqKmilJUZoESM+xqUbpeB9KYy72BParlsFjbJ54oA0rCxgto +t8/zP1Aqy1/821cKnYCsgXbPKxycAUiyl5AfSkwSOkhvYYkDzHPovrSS6pcXRwh8tB29vesMXCRc +/efuarT309x+7j+VfQUXK5TWmvIYWySZ5u3PArLu76aZyZGLHsB0FJHavFy/3sc0xzufYqlm9BSG +kiFVeRt8pOOwq9Zqpl3Yy38qqzI0WPOyWP8ACK09OhYnLDaMdKlspFm0iYsWPrRV+KMKuBRUM0Wx +iRSOqbS28Dpmqlwck1IkrluFwfTtT2XzFJAGe4rYwKSNknBNSSKsyDIp0cY/jGOatRRK5wpA+tNE +sy1tsHp0p0xKIO3H51pvEEHrWXfkjtxTEUDJh8mrLfvkA9utUA2489M81fjwIwc8dKYyvP1wKpyt ++8CjGaszSBiRVV1DTL9KEBZTcE6ZqwjZQgdRUDy7IgF71LEp259aGBDMxUY70+BsYyc1DMSW/wAK +bGWY5zgelAGtEQRuPJ96mDYHHU1XRsoPYVZjYMOe1QMVYh5ZGfcmhj5a5FDuQCTgAdqF+Yhn5pDI +vInmwqZGeSa0ra2SyjDODJKegA6VJbgyDCLx6+ta0a29ogkmIZ+wNNA2Z1tpN7qLeZM3kRfqamum +sNIhKRkPJ3aoNS1yeUeXAPLjPU9zWAyy3MmBGx9z3pNroNK+5YN758vyJgn+JutbumREJknJNY1p +psnnAla6i2i8pADxWbNNETqmBRUqlSOooqR3Ofktyv8ABkVVeNVYstXJJpNxDrx6iqj8scEg/wA6 +3OciYhgQPvURtsBwfm9KQ/KckZoDg5yARTQiwxMsWWyuO1ZN6cg7jz7Ve8wqMFj0qleoNuRkiqEY +z/Iwx0PWr0GXjxWbM2DirFtdbByaYx1xEVbPrVebIcY6gYq1dyrImVPJNUrhiMYH40ASFswgnjFX +bRy0eSeKy2lyqgfjV5SsduQDzihgMncgkLgDuabDKCm0D8aqvIzcZ4NPhB4x3oA14ZMR5H61J5hU +Aj9Krj7gUdh2pcnbk8AVmUWPN3cH86licM3JqkTheP1qWOTCZ6e9IZsQ3QTCqOfSpgySvmR/61kx +y45P6VYifqQOTSGaqJaq2fL3k/3quQrCOI4VyfasqKZIRl8ZNRT626ZS3AB9aQWOibbAoZ2RB6Ac +1TOpIWwIyfTPFZENy8qkyuWY9zTCzGXCk07hym9HIzHcSB7Ciq9oJCBmipZaRFJ8jEnkehqrJiQE +g9PelncEZDDFQqSp4NamBFI+Bgnk0wZxkjcKkkG85wc1XYNG2DkGqAemevO30NMusbODgY71J9oA +jCsM1n3EpZSFBxVCMy4AL9aZ5DAbh9aZPIQ2Ca2oIxLZ8DJ29aBmXAS24HpT51zIemDSBGjkcdxR +KcYPr2pAVmBLqnHBp8shBKZpG5YNTSFaQ5NADlj/AHQJGTSxygMM9qtXUXlWQI6Y61lQ5Z8ULUZs +RS4HJ4PpUjvkjceB0ApqWbmESHAA/SqbEiTr8oqbAXydxCoMCnjLH2FVYpcYwB7k1chwRmpZaJFy +PpT/ADiO+MdzTcg8A0rw8deKgYx5JGBIYkVD5mOpyamdTt2jpVZyqdeaYy7bzEDk/lVu1l/eZIrI +jlBPTmrkDNvGKVgbOptpPlHFFUbadlXmipaKTKvldMHP1p4jdedpI9qXax/hOBTi7AYOR9K2Ocqz +cD0x61l3MzFsDJrZ5cEEk/hVa4tkC5HB96pCMITSI+dxBrZtVS5g/hD46VQdOofGDwKLOQ21wCMs +nQg9KYGfqUBjnIPWt7w9bNPbNk8L2NVNUtjdYkgjJ9dvNaWhNJa2JEqEc8Z4ovoDM3UrZorrpxjJ +rMmzxnmurvI0uGPqcGsDUbcQA+9IaM/P7s4HFS29v5jKSOO9ECDIGeGrRhiEbegpN2KSEvo/+Jft +/wA4rEs0/wBIAxxmujkiN03koD8/GPSoZ9Gj0qPzp5BuJwq+tSpWVhtBfTiOFYY/mbHQVjgEk559 +aLq/Lswj5B6k1VE0g6k4+lWk7Es17ddy8ICB3PSriKNvUE98Viw3xHDKGH61o29yHPGQKmSZSZbj +XDcA5PerO3IwCKjRkx8x/AVPE6HhTj1rMopzhlGBxWe4JOTW7JHGwIHJqk1tz04prQCrAjEgmtW3 +UdT1qBLck56CrsUW3gUrgSGbavHSioblNqZzxRSGaqq6delRyxeZwGx9KfLcrtweazJtTWDgNW9j +An5t25INZd/qSLuy2T6AVWu9aEnyqDk1TuIi6xBuGkPPrimkBUmvp52ITOP9kf1piSXkJyPMWu2s +dP07T/D0t3I0ckzfKkR6g+tc2dUXzQpCnccdOlaONiIz5thdL8QSWt1GZkDKD81emTaXBqGnLLCo +AZdwxXmGp2UfliaLAPfFd18Pda+16cbKZsyQnAye1RYvcyGEkE2yT72duD2qrqtuZdhA4710viiz +W2uI7qMfe4IrIRWlhJYDd2qJDRgQw7WGV68irUuNijoat+RsQMcZFU9RcbQFPbgio3LNrw7bB3ed +gfQVheOLjdqscCn7i9Pc11GiMkGlKzdcZNefazd/adcnlY5G7A+lENZjloixZ2cUcLTTjJAzzTIJ +Y55GQoFHalZZLm1IQnp2NUbSGRblQ3GK2Mi7e2arHvT7w9KhjEjRbouvcVevB5dsWdhx0qHSl3Ix +PTBNJ7XHErJdT9CatxXbgAEkUwxjJIApoX5srzU6MrU0Ybp+n86txymUgVmIhUAsOewNXbbf9BUM +ZpxQADJNI8mwmlgOV5NSNEGFSykZd7fbY8EUUt9bIoOTRTVg1K1zcuXI3H86otvlGCTWjLbnyiwG +D71VQFWyRWxiNs7ENcoZORnmrGpN9nvEDDCr0+lOjuEjdTkD1q5qUMWpWgmiI3qMY9aE7MGMlRbi +2/dtwR61zttpsr3mCPlVupqWO7uLMFOSn6intrEhXCLz7DmrfkQk0WNWmWGIRKcnvTfCF9JZawHQ +kAjBqOz0TU9cm/dQOV7nHFaU2mSaGypJGqtjk5zk0nbYtI7jWZje6VkkbsA//qrCtwyhTgbQemet +RaTrn2m2mS4Vc42qOgAFSear7VQD6Cs/JjI7xgQAMYz2rLuYd4DjB56VqXFrNkYHy9eapz5hGCMA +etQykXy3l6SDISqIPmI64rg7uPzbl5YuUJ4robzVnFi1uVDK3FVdFigN4nngFD6miPu3ZT10Mq3u +3tjtYEVZ/tGMfMF59a7W90bQ5U3zSJET74rnrrStIic+XeoQO45q1NPoRy2MKWeW9kAOdvp61tQr +9hsiXwHftnpTC2nWakwt58nbA4rNuLmS4fMjY9FHam7yBaFhpw2cVPax72yen0qggJ+7WnZo54AJ +FS9BouiNDjJzipGwo+UGpEgyRgc1bjtCfvVncorQMfTmrDuyR5PWrEdoFOabdKqoc4FIaOevJXlJ +zRTL24UMQtFWloBvmA7Qp/I1Ru7NMEBtprpFtFcA8ioLrTY3U7Tlq1MLnDzxsjYznFOt72W3P7sg +A9cirt9aNC5AGazmVVf5s/hTGbEV5p8w/wBMtmyerKOtXrfUtAsRvTTnllHQPjFc35nQEADsPWrc +MLyEDYcHucDFLYDs7b4gxRoUGnqgA42muV1bVn1y8aR0Yc/8BUU6OwRCQSM+uOlWWtY1iODjPYd6 +NwMqxUQbyeAOldJpzIse9jknmsRrZoosuc57Yq5YmSRNsfGBzRYLmvcXkWzbnB7Vh3rq+RnnrVt7 +VUceYxbPJIrNvLcByQ+FJ61DSKRkzMHJU8470xQVYMo/I1KqjzSMgnpzU6xiPJ2k+1VewblWZJJc +ZJ9hmoTCyn5xVt5GLnCAD3NW4raKVAd2T6Glew7GPhh90Z/CkCMzc4rSmtChJUfhmq8cZWQZGPYU +7isSW0DORgZA9a2YwsS8hfwqgDIq/IuKmjEjY3Nk/Ss3qWi/HOCflGRV6KXJHNZ8MR75zVjY4Ixm +oGXHm2Dhqx7+5ZsgMTWmqnZ8wNZN/E5zjgUDsZUsLNli4oqvOsoJAziitUI9CkudpySfwqpLqUec +Nu/Grstskg65rPl04Z74qzAgkuIZQQoH/fPSsa9SEZKqCfrWvJp5xhnbHoKZ/Y8bcyOQPQjmkMwE +jJYMi8/3jWtZ+YV2gFj6VdFhGq4jTd/vVo6WnlvtMSfUDmpvdjMeezvGOTC20Dt2pIP3exWRg7Gu +hvvN2lIlZMc8DlqhtZeUE1q5A6ZXNO9hEd/pEk2nGS3yWA/OuZW+ewVkKkNnkNXqllc20kYQqVIH +TFcz4l0SC7LSRKFcd/Wi9ikr6HKNq7SqPkxjk4NULvUHZgPugdqgn3WbtGQQQe9SafavqF2CEJUd +TS21Lsti1pOmz6jchwpWMdW9a1r/AEicIBEp475robKJbO2AiiBYD0qG6S9ugVIEakcYrJzbdyrW +RzcWihnH2mUD1FacOi2kMe6K4O7HQ96b/YhaTMkueOatQ2UUIVfM3EfjQ5N9RWKpstw5x9cVA1os +bZKAj1rXbKjGQB9Krupx8oVqkZR8uN+ij8qmjhjXBCDNP284YYp6Rge/1piHAgD5VFSIrHqAKQYH +A4NQTSyjo1AFiXCrway7qRmyABT2mdvvVVmVmPANOwzNuFOTkiirRgJPzLRVhc7WNVK/L+dL5LN0 +z+NVYZmKjgD6VcjYkc/NWyaOZoha32kkEE1CyIv3mBPoK0Xt2ljzuUj+6DUkmlT+WuI9q4zhepqX +YFcyANzYCgH0xVqBDERtfLH+FFyaa8DRMyiHJHUE9P8AGhJGxt6t/dHAH1pFGrAZnTGYx6A9aY8k +sb4MaMO+KoxuC+UkJfpx0qRxLtx60MC+dQSPC+T+NZepag0qkIoUUx1ZOTk4rOubhgpCqetZyuaR +3Oe8QBZ0LsgVl6Y71Pokqx2oRAASM064t/tHL/lTYLfyOU4xRdWsa26m/FdyKuA340ySRnIDzEH2 +qpavKxwRV+O33cmosK4QwRL8zSE596fsRWIUBT79DS+SEIY9O4ol4XI7DjPcUWJuNcn1x7HkVCYw +Tk/KfUdKPN7g49aRZVJ4IVv0NFguP24GGAx600xoenBpVlXOD8p9O1DxkDI6UxFeRHBqEo/c5FTN +KUPtQsm88cH0oGMjt93U4qcWeB0zU0Meecc1fig3irSE2YzWo7iit/7DntRWliLmOgdSArZFb2lQ +o7L5hBFcxBMrfMj59q2bGZi4wcVnIEdlHDDGAUA/KpZC03yoKp2J8xBk5rRidEcCp3GUrjSo3AaQ +Zb2pYNIt1jJEagt1PrWtKFIzREm5BxxVIDn7jSog+5ECt64qCXT2AwgDcdK6aSIbarxQBic80J2F +Y5g2DFT5i7f96su8035tpGBXbT2wc4YVWudKVgCOoFN6gtDiJdPEcfA5NRwafyS3Oa6K506Z3CIh +Iqs1nJbnMgxis2i1JmULZImOBin+aq8H86mnicjcCMfSsuSUI+0uD7UguXGkByM5Bqs0hwV9OlQF +gWyrfrTydxBpgUXmeKVhnIB/SonuOQV6Gpbq3Pm7gfrUcdockHoaoRYhm8wAOfofSrCzyQHDDcpq +qsQj61PFImdrcqaAHTAEb0PymoUGW44q+kKg4HKN1qX+zFRgyvwadguLaKzEKT+Nb1rZ5AOeayoI +NjDDCt+xztGTVwRLJ0thjpRWhGgIBHWit7EHk8mnvajcASPUVpaRcH+I/nThfQudrkc1LEkJOUI5 +9K46ivsaxfc6vSXLR59a0/IJYNnGKytHwsYGa3cgoKmKuht2ZPGPMUA9BVqLAUgCmW0YENIG2uRW +qViBJUJOBT4EVeD1qZV+XJ71WZsS8Gm9ACeMFwAKe1v8lPYYAY9alRgy5ppAZht8NnFIdPjmB3KD +WkYwahOUOKm1gMW40mBvlKg1gan4Yt3y8ce1/UV2vlb3yajntR1xUtMaZ5Lc6NdxSHEbYFRossYw +QTj1FeqS2CSj7tVf7GhdvmQGhDueYXEhZyQuahE7j+E16bN4etlbcI1/KsXVfDpkG6IAEUMEcY1x +8wJU81YQLIAV61pHQboAbkBAqE2EkTYK4NIYkW4LjOfSn/bHVShB4qPEiSAEcVIYwzA55ppsGhYr +t1kGeh6V0enXBIHFc+tuFwD2PFbmnkAAelXF6kM6W3YkCimWr8DkUV0Ig8cEM6vh/mrb01WOOtMf +Yz5AANaulQgnpXHUZrE2tMdo1ANbKSlsGshAUfpxWtYjeeaiD6Dka9pIfK5pgLPNx0pobYdtXIIx +sz3re1yCYH93+FUgp88fWp/MIYqaesfGe9N6gLKMx4qKFmziptwIxQigDIotdgSUxkBphl2Ng05J +N1O9xETfI1Mll34Ap102FzUMClzms3vYotJENnNV3Xa3FWgwAxTSgbmqaEQCPf1qKe3XbyKtghTi +orhsilbQDNNqGHSqUmkJK+SK3Yo8rzSFQrUrDuc9PoURX7orNbw8u/cAa7N1BFM8lSOlFguca+mF +GGPpU9tYspropLVSelRrbhW4ojowZHbW5Uc0Vb2kCitudE2PI5FdSCK6XQ4iYwTVCKxM0uMZrobG +3FtGBXJe7NNkWbhQseR1FW9IYsMmq4QyECtKCEQqAOKqMdbib0sTtkzLitNDtQCqFuu6TmrUpKit +okMY/MwxVwfdqtCu/wCY095tvFNaAQ7j5hFWEbHBqCMb5M0TEocikMbcctxT0yig1ACXYVYcgR4p +eYEM778AVZt49sdVIYy75PSr24IuKF3ArSPtkxmrCEbM1Sk+aXNSSTBI8UIBs0wV6VT5gzWdJJ5k +vWr8K4SpTuBaTAXioZB81Kj84pzjvVMCP+GmgmnjnigrikAeXkVA67Wq0rcYqF13PRYBuNy0VOI8 +LRRYDirCLbLtxxWnNbkICo5qS3tlVskVPcLheCMVCjoO+pHp8DM2XXGKuTKQwxTdNJVTuORVwojv +watR0FfUW1Qqd1SXLZXipSoSPioFIc1TXQRLb8RVXlyZcCpy2wYqNcFs0MESKQgzUM8oYU+RuKp4 +Lvik30GT29LKxzgVJHGEWmlNzZosBNbphc1HM2Gp4kCjFVpSXbjpQwHds1RuJTnFXQQFxWfOu6Ti +k0AkS/Pk1qRsPLqlHFhcmpo2ycUJWAkXO+rJGUqIJjmn7vlxVCGLwae3So+hqQHNIZCzbadF8xps +g5p8QxQgLIA20Um8AUVQjKeDapqj5hMojrUmPyVlooN4D71EhoveV5SjbxmnQM3mcmi6YhRSW3Iy +afURoSSDy8VFbLlzUUh+WpLPjmq6h0JbgbRmoEYnkVYuv9XUFsPkND3Aaz80sQBfNQyH5zU0HrUo +B8r7TUsWGXNVputWLc/JTQEE7bTSxruXNMuhlqWIkLR1AguHCGq0bh5Kde1Bb9anqM0mYeXxUMDZ +kprsdtFt96qYkaZHy0wdaUE7aYDzQA9uKZvxT3+7Vcnmkxjt25qlHAqJRUjH5aADfzRUGfmooA// +2Q== +EOT; + } + + /** + * Tests file creation. + * + * @covers ::transform + */ + public function testFileCreation(): void { + /** @var \Drupal\migrate\MigrateExecutableInterface $executable */ + $executable = $this->prophesize(MigrateExecutableInterface::class)->reveal(); + $row = new Row([], []); + $value = ['public://cat.jpeg', base64_decode($this->blob, TRUE)]; + /** @var \Drupal\migrate_plus\Plugin\migrate\process\FileBlob $file_blob */ + $file_blob = $this->pluginManager->createInstance('file_blob'); + $file = $file_blob->transform($value, $executable, $row, 'destination_property'); + $this->assertEquals('public://cat.jpeg', $file); + $this->assertEquals($this->sha1sum, sha1_file($file)); + $configuration = [ + 'reuse' => FileSystemInterface::EXISTS_ERROR, + ]; + /** @var \Drupal\migrate_plus\Plugin\migrate\process\FileBlob $file_blob */ + $file_blob = $this->pluginManager->createInstance('file_blob', $configuration); + /** @var \Drupal\migrate\MigrateExecutableInterface $executable */ + $file = $file_blob->transform($value, $executable, $row, 'destination_property'); + $this->assertEquals('public://cat.jpeg', $file); + $this->assertEquals($this->sha1sum, sha1_file($file)); + } + +} diff --git a/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_fetcher/HttpTest.php b/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_fetcher/HttpTest.php index 76a5c1847469f889ec1b175e2ff0827ba69107f5..c3ee25e785f082dc80d4b2aa1e588487465e5253 100644 --- a/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_fetcher/HttpTest.php +++ b/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_fetcher/HttpTest.php @@ -18,7 +18,7 @@ class HttpTest extends KernelTestBase { * * @dataProvider headerDataProvider */ - public function testHttpHeaders(array $definition, array $expected, array $preSeed = []) { + public function testHttpHeaders(array $definition, array $expected, array $preSeed = []): void { $http = new Http($definition, 'http', []); $this->assertEquals($expected, $http->getRequestHeaders()); } @@ -29,7 +29,7 @@ public function testHttpHeaders(array $definition, array $expected, array $preSe * @return array * The test cases */ - public function headerDataProvider() { + public function headerDataProvider(): array { return [ 'dummy headers specified' => [ 'definition' => [ diff --git a/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_parser/JsonTest.php b/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_parser/JsonTest.php index 2fb4f2458dd4b937e75233dc642e90f6468a5939..1216601a915be64eb352713bd363f4fcd6dae1f9 100644 --- a/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_parser/JsonTest.php +++ b/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_parser/JsonTest.php @@ -11,6 +11,9 @@ */ class JsonTest extends KernelTestBase { + /** + * {@inheritdoc} + */ public static $modules = ['migrate', 'migrate_plus']; /** @@ -30,7 +33,7 @@ class JsonTest extends KernelTestBase { * @throws \Drupal\Component\Plugin\Exception\PluginException * @throws \Exception */ - public function testMissingProperties($file, array $ids, array $fields, array $expected) { + public function testMissingProperties($file, array $ids, array $fields, array $expected): void { $path = $this->container ->get('module_handler') ->getModule('migrate_plus') @@ -66,7 +69,7 @@ public function testMissingProperties($file, array $ids, array $fields, array $e * @return array * The test cases. */ - public function jsonBaseDataProvider() { + public function jsonBaseDataProvider(): array { return [ 'missing properties' => [ 'file' => 'missing_properties.json', diff --git a/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_parser/SimpleXmlTest.php b/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_parser/SimpleXmlTest.php index fe6b345e1dfd53383924f7cf0b4ded3e75203ccc..0b93f3a054d024998097001b6afd7a61b8df9cd5 100644 --- a/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_parser/SimpleXmlTest.php +++ b/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_parser/SimpleXmlTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\migrate_plus\Kernel\Plugin\migrate_plus\data_parser; use Drupal\KernelTests\KernelTestBase; +use Drupal\migrate\MigrateException; /** * Test of the data_parser SimpleXml migrate_plus plugin. @@ -11,30 +12,54 @@ */ class SimpleXmlTest extends KernelTestBase { - public static $modules = ['migrate', 'migrate_plus']; + /** + * {@inheritdoc} + */ + protected static $modules = ['migrate', 'migrate_plus']; /** - * Tests reducing single values. + * Path for the xml file. + * + * @var string + */ + protected $path; + + /** + * The plugin manager. + * + * @var \Drupal\migrate_plus\DataParserPluginManager + */ + protected $pluginManager; + + /** + * The plugin configuration. * - * @throws \Drupal\Component\Plugin\Exception\PluginException - * @throws \Exception + * @var array */ - public function testReduceSingleValue() { - $path = $this->container - ->get('module_handler') - ->getModule('migrate_plus') - ->getPath(); - $url = $path . '/tests/data/simple_xml_reduce_single_value.xml'; - - /** @var \Drupal\migrate_plus\DataParserPluginManager $plugin_manager */ - $plugin_manager = $this->container + protected $configuration; + + /** + * The expected result. + * + * @var array + */ + protected $expected; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->path = $this->container->get('module_handler') + ->getModule('migrate_plus')->getPath(); + $this->pluginManager = $this->container ->get('plugin.manager.migrate_plus.data_parser'); - $conf = [ + $this->configuration = [ 'plugin' => 'url', 'data_fetcher_plugin' => 'file', 'data_parser_plugin' => 'simple_xml', 'destination' => 'node', - 'urls' => [$url], + 'urls' => [], 'ids' => ['id' => ['type' => 'integer']], 'fields' => [ [ @@ -50,18 +75,7 @@ public function testReduceSingleValue() { ], 'item_selector' => '/items/item', ]; - $parser = $plugin_manager->createInstance('simple_xml', $conf); - - $data = []; - foreach ($parser as $item) { - $values = []; - foreach ($item['values'] as $value) { - $values[] = (string) $value; - } - $data[] = $values; - } - - $expected = [ + $this->expected = [ [ 'Value 1', 'Value 2', @@ -70,7 +84,121 @@ public function testReduceSingleValue() { 'Value 1 (single)', ], ]; + } + + /** + * Tests reducing single values. + */ + public function testReduceSingleValue(): void { + $url = $this->path . '/tests/data/simple_xml_reduce_single_value.xml'; + $this->configuration['urls'][0] = $url; + $parser = $this->pluginManager->createInstance('simple_xml', $this->configuration); + $this->assertResults($this->expected, $parser); + } + + /** + * Test reading non standard conforming XML. + * + * XML file with lots of different white spaces before the starting tag. + */ + public function testReadNonStandardXmlWhitespace(): void { + $url = $this->path . '/tests/data/simple_xml_invalid_multi_whitespace.xml'; + $this->configuration['urls'][0] = $url; + $parser = $this->pluginManager->createInstance('simple_xml', $this->configuration); + $this->assertResults($this->expected, $parser); + } + + /** + * Test reading non standard conforming XML . + * + * XML file with one empty line before the starting tag. + */ + public function testReadNonStandardXml2(): void { + $url = $this->path . '/tests/data/simple_xml_invalid_single_line.xml'; + $this->configuration['urls'][0] = $url; + + $parser = $this->pluginManager->createInstance('simple_xml', $this->configuration); + $this->assertResults($this->expected, $parser); + } + + /** + * Test reading broken XML (missing closing tag). + * + * @throws \Drupal\Migrate\MigrateException + */ + public function testReadBrokenXmlMissingTag(): void { + $url = $this->path . '/tests/data/simple_xml_broken_missing_tag.xml'; + $this->configuration['urls'][0] = $url; + $this->expectException(MigrateException::class); + $this->expectExceptionMessageRegExp('/^Fatal Error 73/'); + $parser = $this->pluginManager->createInstance('simple_xml', $this->configuration); + $parser->next(); + } + + /** + * Test reading broken XML (tag mismatch). + * + * @throws \Drupal\Migrate\MigrateException + */ + public function testReadBrokenXmlTagMismatch(): void { + $url = $this->path . '/tests/data/simple_xml_broken_tag_mismatch.xml'; + $this->configuration['urls'][0] = $url; + + $this->expectException(MigrateException::class); + $this->expectExceptionMessageRegExp('/^Fatal Error 76/'); + + $parser = $this->pluginManager->createInstance('simple_xml', $this->configuration); + $parser->next(); + } + + /** + * Test reading non XML. + * + * @throws \Drupal\Migrate\MigrateException + */ + public function testReadNonXml(): void { + $url = $this->path . '/tests/data/simple_xml_non_xml.xml'; + $this->configuration['urls'][0] = $url; + + $this->expectException(MigrateException::class); + $this->expectExceptionMessageRegExp('/^Fatal Error 46/'); + $parser = $this->pluginManager->createInstance('simple_xml', $this->configuration); + $parser->next(); + } + + /** + * Tests reading non-existing XML. + * + * @throws \Drupal\Migrate\MigrateException + */ + public function testReadNonExistingXml(): void { + $url = $this->path . '/tests/data/simple_xml_non_existing.xml'; + $this->configuration['urls'][0] = $url; + + $this->expectException(MigrateException::class); + $this->expectExceptionMessage('file parser plugin: could not retrieve data from modules/contrib/migrate_plus/tests/data/simple_xml_non_existing.xml'); + $parser = $this->pluginManager->createInstance('simple_xml', $this->configuration); + $parser->next(); + } + + /** + * Parses and asserts the results match expectations. + * + * @param array|string $expected + * The expected results. + * @param \Traversable $parser + * An iterable data result to parse. + */ + protected function assertResults($expected, \Traversable $parser) { + $data = []; + foreach ($parser as $item) { + $values = []; + foreach ($item['values'] as $value) { + $values[] = (string) $value; + } + $data[] = $values; + } $this->assertEquals($expected, $data); } diff --git a/web/modules/migrate_plus/tests/src/Unit/DataParserPluginBaseTest.php b/web/modules/migrate_plus/tests/src/Unit/DataParserPluginBaseTest.php new file mode 100644 index 0000000000000000000000000000000000000000..3aea50e80b302f5493bc43cbab1a133d8b4fc8fa --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Unit/DataParserPluginBaseTest.php @@ -0,0 +1,99 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Unit; + +use Drupal\migrate_plus\DataParserPluginBase; +use Drupal\Tests\migrate\Unit\MigrateTestCase; + +/** + * @coversDefaultClass \Drupal\migrate_plus\DataParserPluginBase + * + * @group migrate_plus + */ +class DataParserPluginBaseTest extends MigrateTestCase { + + /** + * @covers ::nextSource + */ + public function testNextSourceWithOneUrl(): void { + $parser = $this->getMockedDataParser(); + $parser->expects($this->once()) + ->method('openSourceUrl') + ->willReturn(TRUE); + $this->assertTrue($parser->nextSource()); + } + + /** + * @covers ::nextSource + */ + public function testNextSourceWithoutUrls(): void { + $config = [ + 'urls' => [], + ]; + + $parser = $this->getMockedDataParser($config); + $parser->expects($this->never()) + ->method('openSourceUrl'); + $this->assertFalse($parser->nextSource()); + } + + /** + * @covers ::count + */ + public function testCountWithoutUrls(): void { + $config = [ + 'urls' => [], + ]; + + $parser = $this->getMockedDataParser($config); + $parser->expects($this->never()) + ->method('openSourceUrl'); + $this->assertEquals(0, $parser->count()); + } + + /** + * Returns a mocked data parser. + * + * @param array $configuration + * The configuration to pass to the data parser. + * + * @return \PHPUnit\Framework\MockObject\MockObject|\Drupal\Tests\migrate_plus\Unit\DataParserPluginBaseMock + * An mock instance of DataParserPluginBase. + */ + protected function getMockedDataParser(array $configuration = []) { + // Set constructor arguments. + $configuration += [ + 'urls' => ['http://example.org/data_parser_test'], + 'item_selector' => 0, + ]; + $plugin_id = 'foo'; + $plugin_definition = [ + 'id' => 'foo', + 'title' => 'Foo', + ]; + + return $this->getMockBuilder(DataParserPluginBaseMock::class) + ->setConstructorArgs([$configuration, $plugin_id, $plugin_definition]) + ->setMethods(['openSourceUrl']) + ->getMockForAbstractClass(); + } + +} + +/** + * Mock for abstract class DataParserPluginBase. + * + * This mock is used to make certain methods publicly accessible. + */ +abstract class DataParserPluginBaseMock extends DataParserPluginBase { + + /** + * {@inheritdoc} + * + * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod.Found + */ + public function nextSource() { + return parent::nextSource(); + } + +} diff --git a/web/modules/migrate_plus/tests/src/Unit/data_fetcher/FileTest.php b/web/modules/migrate_plus/tests/src/Unit/data_fetcher/FileTest.php new file mode 100644 index 0000000000000000000000000000000000000000..746f44a66ed51024aba26c82b3e6fb0aea067e07 --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Unit/data_fetcher/FileTest.php @@ -0,0 +1,186 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Unit\data_fetcher; + +use Drupal\migrate\MigrateException; +use Drupal\migrate_plus\Plugin\migrate_plus\data_fetcher\File; +use Drupal\Tests\migrate\Unit\MigrateTestCase; +use org\bovigo\vfs\vfsStream; + +/** + * @file + * PHPUnit tests for the Migrate Plus File 'data fetcher' plugin. + */ + +/** + * @coversDefaultClass \Drupal\migrate_plus\Plugin\migrate_plus\data_fetcher\File + * + * @group migrate_plus + */ +class FileTest extends MigrateTestCase { + + /** + * Directory where test data will be created. + * + * @var string + */ + const BASE_DIRECTORY = 'migration_data'; + + /** + * Minimal migration configuration data. + * + * @var array + */ + private $specificMigrationConfig = [ + 'source' => 'url', + 'data_fetcher_plugin' => 'file', + 'data_parser_plugin' => 'json', + 'item_selector' => 0, + 'fields' => [], + 'ids' => [ + 'id' => [ + 'type' => 'integer', + ], + ], + ]; + + /** + * The data fetcher plugin ID being tested. + * + * @var string + */ + private $dataFetcherPluginId = 'file'; + + /** + * The data fetcher plugin definition. + * + * @var array + */ + private $pluginDefinition = [ + 'id' => 'file', + 'title' => 'File', + ]; + + /** + * Test data to populate a file with. + * + * @var string + */ + private $testData = '[ + { + "id": 1, + "name": "Joe Bloggs" + } + ]'; + + /** + * Define virtual dir where we'll be creating files in/fetching files from. + * + * @var \org\bovigo\vfs\vfsStreamDirectory + */ + private $baseDir; + + /** + * Set up test environment. + */ + public function setUp(): void { + $this->baseDir = vfsStream::setup(self::BASE_DIRECTORY); + } + + /** + * Test fetching a valid file. + */ + public function testFetchFile(): void { + $file_name = 'file.json'; + $file_path = vfsStream::url(implode(DIRECTORY_SEPARATOR, [self::BASE_DIRECTORY, $file_name])); + $migration_config = $this->specificMigrationConfig + [ + 'urls' => [$file_path], + ]; + + $plugin = new File( + $migration_config, + $this->dataFetcherPluginId, + $this->pluginDefinition + ); + + $tree = [ + $file_name => $this->testData, + ]; + + vfsStream::create($tree, $this->baseDir); + + $expected = json_decode($this->testData, TRUE); + $retrieved = json_decode($plugin->getResponseContent($file_path), TRUE); + + $this->assertEquals($expected, $retrieved); + } + + /** + * Test fetching multiple valid files. + */ + public function testFetchMultipleFiles(): void { + $number_of_files = 3; + $file_paths = []; + $file_names = []; + + for ($i = 0; $i < $number_of_files; $i++) { + $file_name = 'file_' . $i . '.json'; + $file_names[] = $file_name; + $file_paths[] = vfsStream::url(implode(DIRECTORY_SEPARATOR, [self::BASE_DIRECTORY, $file_name])); + } + + $migration_config = $this->specificMigrationConfig + [ + 'urls' => $file_paths, + ]; + + $plugin = new File( + $migration_config, + $this->dataFetcherPluginId, + $this->pluginDefinition + ); + + for ($i = 0; $i < $number_of_files; $i++) { + $file_name = $file_names[$i]; + $file_path = $file_paths[$i]; + + $tree = [ + $file_name => $this->testData, + ]; + + vfsStream::create($tree, $this->baseDir); + + $expected = json_decode($this->testData); + $retrieved = json_decode($plugin->getResponseContent($file_path)); + + $this->assertEquals($expected, $retrieved); + } + } + + /** + * Test trying to fetch an unreadable file results in exception. + */ + public function testFetchUnreadableFile(): void { + $file_name = 'file.json'; + $file_path = vfsStream::url(implode(DIRECTORY_SEPARATOR, [self::BASE_DIRECTORY, $file_name])); + $migration_config = $this->specificMigrationConfig + [ + 'urls' => [$file_path], + ]; + + $plugin = new File( + $migration_config, + $this->dataFetcherPluginId, + $this->pluginDefinition + ); + + // Create an unreadable file. + vfsStream::newFile($file_name, 0300) + ->withContent($this->testData) + ->at($this->baseDir); + + // Trigger exception trying to read the non-readable file. + $this->expectException(MigrateException::class); + $this->expectExceptionMessage('file parser plugin: could not retrieve data from vfs://migration_data/file.json'); + $plugin->getResponseContent($file_path); + } + +} diff --git a/web/modules/migrate_plus/tests/src/Unit/data_fetcher/HttpTest.php b/web/modules/migrate_plus/tests/src/Unit/data_fetcher/HttpTest.php new file mode 100644 index 0000000000000000000000000000000000000000..6e5113e943fa57970380973493e97a793d20aab0 --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Unit/data_fetcher/HttpTest.php @@ -0,0 +1,244 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Unit\data_fetcher; + +use Drupal\migrate\MigrateException; +use Drupal\migrate_plus\DataFetcherPluginBase; +use Drupal\migrate_plus\Plugin\migrate_plus\authentication\Basic; +use Drupal\migrate_plus\Plugin\migrate_plus\data_fetcher\Http; +use Drupal\Tests\migrate\Unit\MigrateTestCase; +use GuzzleHttp\Client; +use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Psr7\Response; + +/** + * @file + * PHPUnit tests for the Migrate Plus Http 'data fetcher' plugin. + */ + +/** + * @coversDefaultClass \Drupal\migrate_plus\Plugin\migrate_plus\data_fetcher\Http + * + * @group migrate_plus + */ +class HttpTest extends MigrateTestCase { + + /** + * Minimal migration configuration data. + * + * @var array + */ + private $specificMigrationConfig = [ + 'source' => 'url', + 'urls' => ['http://example.org/http_fetcher_test'], + 'data_fetcher_plugin' => 'http', + 'data_parser_plugin' => 'json', + 'item_selector' => 0, + 'authentication' => [ + 'plugin' => 'basic', + 'username' => 'testing', + 'password' => 'password', + ], + 'fields' => [], + 'ids' => [ + 'id' => [ + 'type' => 'integer', + ], + ], + ]; + + /** + * The data fetcher plugin ID being tested. + * + * @var string + */ + private $dataFetcherPluginId = 'http'; + + /** + * The data fetcher plugin definition. + * + * @var array + */ + private $pluginDefinition = [ + 'id' => 'http', + 'title' => 'HTTP', + ]; + + /** + * Test data to validate an HTTP response against. + * + * @var string + */ + private $testData = ' + { + "id": 1, + "name": "Joe Bloggs" + } + '; + + /** + * Mocked up Basic authentication plugin. + * + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $basicAuthenticator = NULL; + + /** + * Set up test environment. + */ + public function setUp(): void { + // Mock up a Basic authentication plugin that will be used in requests. + $basic_authenticator = $this->getMockBuilder(Basic::class) + ->disableOriginalConstructor() + ->getMock(); + + $basic_authenticator->method('getAuthenticationOptions') + ->will($this->returnValue([ + 'auth' => [ + 'username', + 'password', + ], + ])); + + $this->basicAuthenticator = $basic_authenticator; + } + + /** + * Test 'http' data fetcher (with auth) returns an expected response. + */ + public function testFetchHttpWithAuth(): void { + $migration_config = $this->migrationConfiguration + $this->specificMigrationConfig; + + $plugin = new TestHttp($migration_config, $this->dataFetcherPluginId, $this->pluginDefinition); + $plugin->mockHttpClient([[200, 'application/json', $this->testData]], $this->basicAuthenticator); + + // The Guzzle mock returns an instance of StreamInterface. + // http://docs.guzzlephp.org/en/latest/psr7.html + $stream = $plugin->getResponseContent($migration_config['urls'][0]); + + $body = json_decode((string) $stream, TRUE); + + // Compare what we got back from the parser to what we expected to get. + $expected = json_decode($this->testData, TRUE); + $this->assertArrayEquals($expected, $body); + } + + /** + * Test 'http' data fetcher (without auth) returns an expected response. + */ + public function testFetchHttpNoAuth(): void { + $migration_config = $this->migrationConfiguration + $this->specificMigrationConfig; + unset($migration_config['authentication']); + + $plugin = new TestHttp($migration_config, $this->dataFetcherPluginId, $this->pluginDefinition); + $plugin->mockHttpClient([[200, 'application/json', $this->testData]], NULL); + + $stream = $plugin->getResponseContent($migration_config['urls'][0]); + + $body = json_decode((string) $stream, TRUE); + + $expected = json_decode($this->testData, TRUE); + $this->assertArrayEquals($expected, $body); + } + + /** + * Test 'http' data fetcher (with auth) dies as expected when auth fails. + */ + public function testFetchHttpAuthFailure(): void { + $migration_config = $this->migrationConfiguration + $this->specificMigrationConfig; + + $plugin = new TestHttp($migration_config, $this->dataFetcherPluginId, $this->pluginDefinition); + $plugin->mockHttpClient([[403, 'text/html', 'Forbidden']], $this->basicAuthenticator); + + $this->expectException(MigrateException::class); + $this->expectExceptionMessage('Error message: Client error: `GET http://example.org/http_fetcher_test` resulted in a `403 Forbidden'); + $plugin->getResponseContent($migration_config['urls'][0]); + } + + /** + * Test 'http' data fetcher (with auth) dies as expected when server down. + */ + public function testFetchHttp500Error(): void { + $migration_config = $this->migrationConfiguration + $this->specificMigrationConfig; + + $plugin = new TestHttp($migration_config, $this->dataFetcherPluginId, $this->pluginDefinition); + $plugin->mockHttpClient([[500, 'text/html', 'Internal Server Error']], $this->basicAuthenticator); + + $this->expectException(MigrateException::class); + $this->expectExceptionMessage('GET http://example.org/http_fetcher_test` resulted in a `500 Internal Server Error'); + $plugin->getResponseContent($migration_config['urls'][0]); + } + +} + +/** + * Test class to mock an HTTP request. + */ +class TestHttp extends Http { + + /** + * Mocked authenticator plugin. + * + * @var \PHPUnit_Framework_MockObject_MockObject + */ + public $authenticator = NULL; + + /** + * Mock the HttpClient, so we can control the request/response(s) etc. + * + * @param array $responses + * An array of responses (arrays), with each consisting of properties, + * ordered: response code, content-type and response body. + * @param object $authenticator + * Mocked authenticator plugin. + */ + public function mockHttpClient(array $responses, object $authenticator = NULL) { + // Set mocked authentication plugin to be used for the request auth plugin. + $this->authenticator = $authenticator; + + $handler_responses = []; + foreach ($responses as $response) { + $handler_responses[] = new Response( + $response[0], + ['Content-Type' => $response[1]], + $response[2] + ); + } + + $mock = new MockHandler($handler_responses); + $handler = HandlerStack::create($mock); + + $this->httpClient = new Client(['handler' => $handler]); + } + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition) { + // Skip calling the Http() constructor (that sets the httpClient instance + // variable via \Drupal which we don't want to do), but keep the call to its + // parent class constructor. @see https://bugs.php.net/bug.php?id=42016 + DataFetcherPluginBase::__construct($configuration, $plugin_id, $plugin_definition); + + // This is what the parent class is doing, that we need to override. + $this->httpClient = NULL; + } + + /** + * Override the parent::getAuthenticationPlugin() + * + * So we can mock the authentication plugin. + * + * @return \PHPUnit_Framework_MockObject_MockObject + * A mocked authentication plugin. + */ + public function getAuthenticationPlugin() { + if (!isset($this->authenticationPlugin)) { + $this->authenticationPlugin = $this->authenticator; + } + + return $this->authenticationPlugin; + } + +} diff --git a/web/modules/migrate_plus/tests/src/Unit/process/ArrayPopTest.php b/web/modules/migrate_plus/tests/src/Unit/process/ArrayPopTest.php index 84316d799a0d8ec28eabb0c35b682e8613a71d97..c651f57c6615330ed11454c39d43a0b118309d98 100644 --- a/web/modules/migrate_plus/tests/src/Unit/process/ArrayPopTest.php +++ b/web/modules/migrate_plus/tests/src/Unit/process/ArrayPopTest.php @@ -2,9 +2,9 @@ namespace Drupal\Tests\migrate_plus\Unit\process; -use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase; -use Drupal\migrate_plus\Plugin\migrate\process\ArrayPop; use Drupal\migrate\MigrateException; +use Drupal\migrate_plus\Plugin\migrate\process\ArrayPop; +use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase; /** * Tests the array pop process plugin. @@ -17,7 +17,7 @@ class ArrayPopTest extends MigrateProcessTestCase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { $this->plugin = new ArrayPop([], 'array_pop', []); parent::setUp(); } @@ -28,7 +28,7 @@ protected function setUp() { * @return array * An array containing input values and expected output values. */ - public function arrayPopDataProvider() { + public function arrayPopDataProvider(): array { return [ 'indexed array' => [ 'input' => ['v1', 'v2', 'v3'], @@ -55,7 +55,7 @@ public function arrayPopDataProvider() { * * @dataProvider arrayPopDataProvider */ - public function testArrayPop(array $input, $expected_output) { + public function testArrayPop(array $input, $expected_output): void { $output = $this->plugin->transform($input, $this->migrateExecutable, $this->row, 'destinationproperty'); $this->assertSame($output, $expected_output); } @@ -63,8 +63,9 @@ public function testArrayPop(array $input, $expected_output) { /** * Test invalid input. */ - public function testArrayPopFromString() { - $this->setExpectedException(MigrateException::class, 'Input should be an array.'); + public function testArrayPopFromString(): void { + $this->expectException(MigrateException::class); + $this->expectExceptionMessage('Input should be an array.'); $this->plugin->transform('foo', $this->migrateExecutable, $this->row, 'destinationproperty'); } diff --git a/web/modules/migrate_plus/tests/src/Unit/process/ArrayShiftTest.php b/web/modules/migrate_plus/tests/src/Unit/process/ArrayShiftTest.php index 6c6950fee1109105d8ea9d3eb9001771b7db746a..1a8991a8853c9484c42a8725e361101795db5c48 100644 --- a/web/modules/migrate_plus/tests/src/Unit/process/ArrayShiftTest.php +++ b/web/modules/migrate_plus/tests/src/Unit/process/ArrayShiftTest.php @@ -2,9 +2,9 @@ namespace Drupal\Tests\migrate_plus\Unit\process; -use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase; -use Drupal\migrate_plus\Plugin\migrate\process\ArrayShift; use Drupal\migrate\MigrateException; +use Drupal\migrate_plus\Plugin\migrate\process\ArrayShift; +use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase; /** * Tests the array shift process plugin. @@ -17,7 +17,7 @@ class ArrayShiftTest extends MigrateProcessTestCase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { $this->plugin = new ArrayShift([], 'array_shift', []); parent::setUp(); } @@ -28,7 +28,7 @@ protected function setUp() { * @return array * An array containing input values and expected output values. */ - public function arrayShiftDataProvider() { + public function arrayShiftDataProvider(): array { return [ 'indexed array' => [ 'input' => ['v1', 'v2', 'v3'], @@ -55,7 +55,7 @@ public function arrayShiftDataProvider() { * * @dataProvider arrayShiftDataProvider */ - public function testArrayShift(array $input, $expected_output) { + public function testArrayShift(array $input, $expected_output): void { $output = $this->plugin->transform($input, $this->migrateExecutable, $this->row, 'destinationproperty'); $this->assertSame($output, $expected_output); } @@ -63,8 +63,9 @@ public function testArrayShift(array $input, $expected_output) { /** * Test invalid input. */ - public function testArrayShiftFromString() { - $this->setExpectedException(MigrateException::class, 'Input should be an array.'); + public function testArrayShiftFromString(): void { + $this->expectException(MigrateException::class); + $this->expectExceptionMessage('Input should be an array.'); $this->plugin->transform('foo', $this->migrateExecutable, $this->row, 'destinationproperty'); } diff --git a/web/modules/migrate_plus/tests/src/Unit/process/DomApplyStylesTest.php b/web/modules/migrate_plus/tests/src/Unit/process/DomApplyStylesTest.php new file mode 100644 index 0000000000000000000000000000000000000000..5ed3e5448d94b1812751d342e343c68d6254477f --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Unit/process/DomApplyStylesTest.php @@ -0,0 +1,140 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Unit\process; + +use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException; +use Drupal\Component\Utility\Html; +use Drupal\Core\Config\ConfigFactory; +use Drupal\Core\Config\ImmutableConfig; +use Drupal\migrate\MigrateSkipRowException; +use Drupal\migrate_plus\Plugin\migrate\process\DomApplyStyles; +use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase; + +/** + * Tests the dom_apply_styles process plugin. + * + * @group migrate + * @coversDefaultClass \Drupal\migrate_plus\Plugin\migrate\process\DomApplyStyles + */ +class DomApplyStylesTest extends MigrateProcessTestCase { + + /** + * Example configuration for the dom_apply_styles process plugin. + * + * @var array + */ + protected $exampleConfiguration = [ + 'format' => 'test_format', + 'rules' => [ + [ + 'xpath' => '//b', + 'style' => 'Bold', + ], + [ + 'xpath' => '//span/i', + 'style' => 'Italic', + 'depth' => 1, + ], + ], + ]; + + /** + * Mock a config factory object. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory = NULL; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + // Mock a config object. + $prophecy = $this->prophesize(ImmutableConfig::class); + $prophecy + ->get('settings.plugins.stylescombo.styles') + ->willReturn("strong.foo|Bold\r\nem.foo.bar|Italic\r\n"); + $style_config = $prophecy->reveal(); + // Mock the config factory. + $prophecy = $this->prophesize(ConfigFactory::class); + $prophecy + ->get('editor.editor.test_format') + ->willReturn($style_config); + $this->configFactory = $prophecy->reveal(); + + parent::setUp(); + } + + /** + * @covers ::__construct + * + * @dataProvider providerTestConfig + */ + public function testValidateRules(array $config_overrides, $message): void { + $configuration = $config_overrides + $this->exampleConfiguration; + $value = '<p>A simple paragraph.</p>'; + $this->expectException(InvalidPluginDefinitionException::class); + $this->expectExceptionMessage($message); + (new DomApplyStyles($configuration, 'dom_apply_styles', [], $this->configFactory)) + ->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + } + + /** + * Dataprovider for testValidateRules(). + */ + public function providerTestConfig(): array { + $cases = [ + 'format-empty' => [ + ['format' => ''], + 'The "format" option must be a non-empty string.', + ], + 'format-not-string' => [ + ['format' => [1, 2, 3]], + 'The "format" option must be a non-empty string.', + ], + 'rules-not-array' => [ + ['rules' => 'invalid'], + 'The "rules" option must be an array.', + ], + 'xpath-null' => [ + [ + 'rules' => [['xpath' => NULL, 'style' => 'Bold']], + ], + 'The "xpath" and "style" options are required for each rule.', + ], + 'style-invalid' => [ + [ + 'rules' => [['xpath' => '//b', 'style' => 'invalid-style']], + ], + 'The style "invalid-style" is not defined.', + ], + ]; + + return $cases; + } + + /** + * @covers ::transform + */ + public function testTransformInvalidInput(): void { + $value = 'string'; + $this->expectException(MigrateSkipRowException::class); + $this->expectExceptionMessage('The dom_apply_styles plugin in the destinationproperty process pipeline requires a \DOMDocument object. You can use the dom plugin to convert a string to \DOMDocument.'); + (new DomApplyStyles($this->exampleConfiguration, 'dom_apply_styles', [], $this->configFactory)) + ->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + } + + /** + * @covers ::transform + */ + public function testTransform(): void { + $input_string = '<div><span><b>Bold text</b></span><span><i>Italic text</i></span></div>'; + $output_string = '<div><span><strong class="foo">Bold text</strong></span><em class="foo bar">Italic text</em></div>'; + $value = Html::load($input_string); + $document = (new DomApplyStyles($this->exampleConfiguration, 'dom_apply_styles', [], $this->configFactory)) + ->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertTrue($document instanceof \DOMDocument); + $this->assertEquals($output_string, Html::serialize($document)); + } + +} diff --git a/web/modules/migrate_plus/tests/src/Unit/process/DomMigrationLookupTest.php b/web/modules/migrate_plus/tests/src/Unit/process/DomMigrationLookupTest.php new file mode 100644 index 0000000000000000000000000000000000000000..278eac22b5a4703c99b95c2397e9f7b674bc5f23 --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Unit/process/DomMigrationLookupTest.php @@ -0,0 +1,191 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Unit\process; + +use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException; +use Drupal\Component\Utility\Html; +use Drupal\migrate\MigrateSkipRowException; +use Drupal\migrate\Plugin\MigratePluginManager; +use Drupal\migrate\Plugin\MigrateProcessInterface; +use Drupal\migrate\Plugin\MigrationInterface; +use Drupal\migrate_plus\Plugin\migrate\process\DomMigrationLookup; +use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase; + +/** + * Tests the dom_migration_lookup process plugin. + * + * @group migrate + * @coversDefaultClass \Drupal\migrate_plus\Plugin\migrate\process\DomMigrationLookup + */ +class DomMigrationLookupTest extends MigrateProcessTestCase { + + /** + * Example configuration for the dom_migration_lookup process plugin. + * + * @var array + */ + protected $exampleConfiguration = [ + 'plugin' => 'dom_migration_lookup', + 'mode' => 'attribute', + 'xpath' => '//a', + 'attribute_options' => [ + 'name' => 'href', + ], + 'search' => '@/user/(\d+)@', + 'replace' => '/user/[mapped-id]', + 'migrations' => [ + 'users' => [], + 'people' => [ + 'replace' => '/people/[mapped-id]', + ], + ], + ]; + + /** + * Mock a migration. + * + * @var \Drupal\migrate\Plugin\MigrationInterface + */ + protected $migration; + + /** + * Mock a process plugin manager. + * + * @var \Drupal\migrate\Plugin\MigratePluginManagerInterface + */ + protected $processPluginManager; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + // Mock a migration. + $prophecy = $this->prophesize(MigrationInterface::class); + $this->migration = $prophecy->reveal(); + + // Mock two migration lookup plugins. + $prophecy = $this->prophesize(MigrateProcessInterface::class); + $prophecy + ->transform('123', $this->migrateExecutable, $this->row, 'destinationproperty') + ->willReturn('321'); + $prophecy + ->transform('456', $this->migrateExecutable, $this->row, 'destinationproperty') + ->willReturn(NULL); + $users_lookup_plugin = $prophecy->reveal(); + $prophecy = $this->prophesize(MigrateProcessInterface::class); + $prophecy + ->transform('123', $this->migrateExecutable, $this->row, 'destinationproperty') + ->willReturn('ignored'); + $prophecy + ->transform('456', $this->migrateExecutable, $this->row, 'destinationproperty') + ->willReturn('654'); + $people_lookup_plugin = $prophecy->reveal(); + + // Mock a process plugin manager. + $prophecy = $this->prophesize(MigratePluginManager::class); + $users_configuration = [ + 'migration' => 'users', + 'no_stub' => TRUE, + ]; + $people_configuration = [ + 'migration' => 'people', + 'no_stub' => TRUE, + ]; + $prophecy + ->createInstance('migration_lookup', $users_configuration, $this->migration) + ->willReturn($users_lookup_plugin); + $prophecy + ->createInstance('migration_lookup', $people_configuration, $this->migration) + ->willReturn($people_lookup_plugin); + $this->processPluginManager = $prophecy->reveal(); + } + + /** + * @covers ::__construct + * + * @dataProvider providerTestConfigValidation + */ + public function testConfigValidation(array $config_overrides, $message): void { + $configuration = $config_overrides + $this->exampleConfiguration; + $value = '<p>A simple paragraph.</p>'; + $this->expectException(InvalidPluginDefinitionException::class); + $this->expectExceptionMessage($message); + (new DomMigrationLookup($configuration, 'dom_migration_lookup', [], $this->migration, $this->processPluginManager)) + ->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + } + + /** + * Dataprovider for testConfigValidation(). + */ + public function providerTestConfigValidation(): array { + $cases = [ + 'migrations-empty' => [ + ['migrations' => []], + "Configuration option 'migration' is required.", + ], + 'migrations-invalid' => [ + ['migrations' => 42], + "Configuration option 'migration' should be a keyed array.", + ], + 'replace-null' => [ + ['replace' => NULL], + "Please define either a global replace for all migrations, or a specific one for 'migrations.users'.", + ], + ]; + + return $cases; + } + + /** + * @covers ::transform + */ + public function testTransformInvalidInput(): void { + $value = 'string'; + $this->expectException(MigrateSkipRowException::class); + $this->expectExceptionMessage('The dom_migration_lookup plugin in the destinationproperty process pipeline requires a \DOMDocument object. You can use the dom plugin to convert a string to \DOMDocument.'); + (new DomMigrationLookup($this->exampleConfiguration, 'dom_migration_lookup', [], $this->migration, $this->processPluginManager)) + ->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + } + + /** + * @covers ::transform + * + * @dataProvider providerTestTransform + */ + public function testTransform($config_overrides, $input_string, $output_string): void { + $configuration = $config_overrides + $this->exampleConfiguration; + $value = Html::load($input_string); + $document = (new DomMigrationLookup($configuration, 'dom_migration_lookup', [], $this->migration, $this->processPluginManager)) + ->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertTrue($document instanceof \DOMDocument); + $this->assertEquals($output_string, Html::serialize($document)); + } + + /** + * Dataprovider for testTransform(). + */ + public function providerTestTransform(): array { + $cases = [ + 'users-migration' => [ + [], + '<a href="/user/123">text</a>', + '<a href="/user/321">text</a>', + ], + 'people-migration' => [ + [], + '<a href="https://www.example.com/user/456">text</a>', + '<a href="https://www.example.com/people/654">text</a>', + ], + 'no-match' => [ + ['search' => '@www\.mysite\.com/user/(\d+)@'], + '<a href="https://www.example.com/user/456">text</a>', + '<a href="https://www.example.com/user/456">text</a>', + ], + ]; + + return $cases; + } + +} diff --git a/web/modules/migrate_plus/tests/src/Unit/process/DomStrReplaceTest.php b/web/modules/migrate_plus/tests/src/Unit/process/DomStrReplaceTest.php new file mode 100644 index 0000000000000000000000000000000000000000..fb4a93395499e50755b618b8eaf893e536dbcc5a --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Unit/process/DomStrReplaceTest.php @@ -0,0 +1,145 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Unit\process; + +use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException; +use Drupal\Component\Utility\Html; +use Drupal\migrate\MigrateSkipRowException; +use Drupal\migrate_plus\Plugin\migrate\process\DomStrReplace; +use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase; + +/** + * Tests the dom_str_replace process plugin. + * + * @group migrate + * @coversDefaultClass \Drupal\migrate_plus\Plugin\migrate\process\DomStrReplace + */ +class DomStrReplaceTest extends MigrateProcessTestCase { + + /** + * Example configuration for the dom_str_replace process plugin. + * + * @var array + */ + protected $exampleConfiguration = [ + 'mode' => 'attribute', + 'xpath' => '//a', + 'attribute_options' => [ + 'name' => 'href', + ], + 'search' => 'foo', + 'replace' => 'bar', + ]; + + /** + * @covers ::__construct + * + * @dataProvider providerTestConfigEmpty + */ + public function testConfigValidation(array $config_overrides, $message): void { + $configuration = $config_overrides + $this->exampleConfiguration; + $value = '<p>A simple paragraph.</p>'; + $this->expectException(InvalidPluginDefinitionException::class); + $this->expectExceptionMessage($message); + (new DomStrReplace($configuration, 'dom_str_replace', [])) + ->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + } + + /** + * Dataprovider for testConfigValidation(). + */ + public function providerTestConfigEmpty(): array { + $cases = [ + 'xpath-null' => [ + ['xpath' => NULL], + "Configuration option 'xpath' is required.", + ], + 'mode-null' => [ + ['mode' => NULL], + "Configuration option 'mode' is required.", + ], + 'mode-invalid' => [ + ['mode' => 'invalid'], + 'Configuration option "mode" only accepts the following values: attribute.', + ], + 'attribute_options-null' => [ + ['attribute_options' => NULL], + "Configuration option 'attribute_options' is required.", + ], + 'search-null' => [ + ['search' => NULL], + "Configuration option 'search' is required.", + ], + 'replace-null' => [ + ['replace' => NULL], + "Configuration option 'replace' is required.", + ], + ]; + + return $cases; + } + + /** + * @covers ::transform + */ + public function testTransformInvalidInput(): void { + $configuration = [ + 'xpath' => '//a', + 'mode' => 'attribute', + 'attribute_options' => [ + 'name' => 'href', + ], + 'search' => 'foo', + 'replace' => 'bar', + ]; + $value = 'string'; + $this->expectException(MigrateSkipRowException::class); + $this->expectExceptionMessage('The dom_str_replace plugin in the destinationproperty process pipeline requires a \DOMDocument object. You can use the dom plugin to convert a string to \DOMDocument.'); + (new DomStrReplace($configuration, 'dom_str_replace', [])) + ->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + } + + /** + * @covers ::transform + * + * @dataProvider providerTestTransform + */ + public function testTransform($input_string, $configuration, $output_string): void { + $value = Html::load($input_string); + $document = (new DomStrReplace($configuration, 'dom_str_replace', [])) + ->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertTrue($document instanceof \DOMDocument); + $this->assertEquals($output_string, Html::serialize($document)); + } + + /** + * Dataprovider for testTransform(). + */ + public function providerTestTransform(): array { + $cases = [ + 'string:case_sensitive' => [ + '<a href="/foo/Foo/foo">text</a>', + $this->exampleConfiguration, + '<a href="/bar/Foo/bar">text</a>', + ], + 'string:case_insensitive' => [ + '<a href="/foo/Foo/foo">text</a>', + [ + 'case_insensitive' => TRUE, + ] + $this->exampleConfiguration, + '<a href="/bar/bar/bar">text</a>', + ], + 'regex' => [ + '<a href="/foo/Foo/foo">text</a>', + [ + 'search' => '/(.)\1/', + 'regex' => TRUE, + ] + $this->exampleConfiguration, + '<a href="/fbar/Fbar/fbar">text</a>', + ], + ]; + + return $cases; + } + +} diff --git a/web/modules/migrate_plus/tests/src/Unit/process/DomTest.php b/web/modules/migrate_plus/tests/src/Unit/process/DomTest.php new file mode 100644 index 0000000000000000000000000000000000000000..78abf8587aae04d29186c7fa1eb2864edb285a8b --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Unit/process/DomTest.php @@ -0,0 +1,88 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Unit\process; + +use Drupal\Component\Utility\Html; +use Drupal\migrate\MigrateException; +use Drupal\migrate_plus\Plugin\migrate\process\Dom; +use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase; + +/** + * Tests the dom process plugin. + * + * @group migrate + * @coversDefaultClass \Drupal\migrate_plus\Plugin\migrate\process\Dom + */ +class DomTest extends MigrateProcessTestCase { + + /** + * @covers ::__construct + */ + public function testConfigMethodEmpty(): void { + $configuration = []; + $value = '<p>A simple paragraph.</p>'; + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The "method" must be set.'); + (new Dom($configuration, 'dom', [])) + ->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + } + + /** + * @covers ::__construct + */ + public function testConfigMethodInvalid(): void { + $configuration['method'] = 'invalid'; + $value = '<p>A simple paragraph.</p>'; + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The "method" must be "import" or "export".'); + (new Dom($configuration, 'dom', [])) + ->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + } + + /** + * @covers ::import + */ + public function testImportNonRoot(): void { + $configuration['method'] = 'import'; + $value = '<p>A simple paragraph.</p>'; + $document = (new Dom($configuration, 'dom', [])) + ->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertTrue($document instanceof \DOMDocument); + } + + /** + * @covers ::import + */ + public function testImportNonRootInvalidInput(): void { + $configuration['method'] = 'import'; + $value = [1, 1]; + $this->expectException(MigrateException::class); + $this->expectExceptionMessage('Cannot import a non-string value.'); + (new Dom($configuration, 'dom', [])) + ->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + } + + /** + * @covers ::export + */ + public function testExportNonRoot(): void { + $configuration['method'] = 'export'; + $partial = '<p>A simple paragraph.</p>'; + $document = Html::load($partial); + $value = (new Dom($configuration, 'dom', [])) + ->transform($document, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertEquals($value, $partial); + } + + /** + * @covers ::export + */ + public function testExportNonRootInvalidInput(): void { + $configuration['method'] = 'export'; + $this->expectException(MigrateException::class); + $this->expectExceptionMessage('Cannot export a "string".'); + (new Dom($configuration, 'dom', [])) + ->transform('string is not DOMDocument', $this->migrateExecutable, $this->row, 'destinationproperty'); + } + +} diff --git a/web/modules/migrate_plus/tests/src/Unit/process/MultipleValuesTest.php b/web/modules/migrate_plus/tests/src/Unit/process/MultipleValuesTest.php index 60a520c0dfedfec7a11bf5200253f95b241919e1..604d3c61249b9e09b6bafa318723081943c9245a 100644 --- a/web/modules/migrate_plus/tests/src/Unit/process/MultipleValuesTest.php +++ b/web/modules/migrate_plus/tests/src/Unit/process/MultipleValuesTest.php @@ -2,8 +2,8 @@ namespace Drupal\Tests\migrate_plus\Unit\process; -use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase; use Drupal\migrate_plus\Plugin\migrate\process\MultipleValues; +use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase; /** * @coversDefaultClass \Drupal\migrate_plus\Plugin\migrate\process\MultipleValues @@ -14,7 +14,7 @@ class MultipleValuesTest extends MigrateProcessTestCase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { $this->plugin = new MultipleValues([], 'multiple_values', []); parent::setUp(); } @@ -22,7 +22,7 @@ protected function setUp() { /** * Test input treated as multiple value output. */ - public function testTreatAsMultiple() { + public function testTreatAsMultiple(): void { $value = ['v1', 'v2', 'v3']; $output = $this->plugin->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); $this->assertSame($output, $value); diff --git a/web/modules/migrate_plus/tests/src/Unit/process/SingleValueTest.php b/web/modules/migrate_plus/tests/src/Unit/process/SingleValueTest.php index 203b1c5cb3313fc205ad7f91d02cd2ee9f8e5b77..fe8d0748361a246edb65a7f44d20704cac2e1821 100644 --- a/web/modules/migrate_plus/tests/src/Unit/process/SingleValueTest.php +++ b/web/modules/migrate_plus/tests/src/Unit/process/SingleValueTest.php @@ -2,8 +2,8 @@ namespace Drupal\Tests\migrate_plus\Unit\process; -use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase; use Drupal\migrate_plus\Plugin\migrate\process\SingleValue; +use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase; /** * @coversDefaultClass \Drupal\migrate_plus\Plugin\migrate\process\SingleValue @@ -14,7 +14,7 @@ class SingleValueTest extends MigrateProcessTestCase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { $this->plugin = new SingleValue([], 'single_value', []); parent::setUp(); } @@ -22,7 +22,7 @@ protected function setUp() { /** * Test input treated as single value output. */ - public function testTreatAsSingle() { + public function testTreatAsSingle(): void { $value = ['v1', 'v2', 'v3']; $output = $this->plugin->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); $this->assertSame($output, $value); diff --git a/web/modules/migrate_plus/tests/src/Unit/process/SkipOnValueTest.php b/web/modules/migrate_plus/tests/src/Unit/process/SkipOnValueTest.php index 968e6f0fbc7a2be0ca86f53095dcb230747458e9..e2af7c929864fe77639e0ee18ed81c31cc36c20b 100644 --- a/web/modules/migrate_plus/tests/src/Unit/process/SkipOnValueTest.php +++ b/web/modules/migrate_plus/tests/src/Unit/process/SkipOnValueTest.php @@ -19,10 +19,10 @@ class SkipOnValueTest extends MigrateProcessTestCase { /** * @covers ::process */ - public function testProcessSkipsOnValue() { + public function testProcessSkipsOnValue(): void { $configuration['method'] = 'process'; $configuration['value'] = 86; - $this->setExpectedException(MigrateSkipProcessException::class); + $this->expectException(MigrateSkipProcessException::class); (new SkipOnValue($configuration, 'skip_on_value', [])) ->transform('86', $this->migrateExecutable, $this->row, 'destinationproperty'); } @@ -30,10 +30,10 @@ public function testProcessSkipsOnValue() { /** * @covers ::process */ - public function testProcessSkipsOnMultipleValue() { + public function testProcessSkipsOnMultipleValue(): void { $configuration['method'] = 'process'; $configuration['value'] = [1, 1, 2, 3, 5, 8]; - $this->setExpectedException(MigrateSkipProcessException::class); + $this->expectException(MigrateSkipProcessException::class); (new SkipOnValue($configuration, 'skip_on_value', [])) ->transform('5', $this->migrateExecutable, $this->row, 'destinationproperty'); } @@ -41,7 +41,7 @@ public function testProcessSkipsOnMultipleValue() { /** * @covers ::process */ - public function testProcessBypassesOnNonValue() { + public function testProcessBypassesOnNonValue(): void { $configuration['method'] = 'process'; $configuration['value'] = 'sourcevalue'; $configuration['not_equals'] = TRUE; @@ -57,7 +57,7 @@ public function testProcessBypassesOnNonValue() { /** * @covers ::process */ - public function testProcessSkipsOnMultipleNonValue() { + public function testProcessSkipsOnMultipleNonValue(): void { $configuration['method'] = 'process'; $configuration['value'] = [1, 1, 2, 3, 5, 8]; $value = (new SkipOnValue($configuration, 'skip_on_value', [])) @@ -68,7 +68,7 @@ public function testProcessSkipsOnMultipleNonValue() { /** * @covers ::process */ - public function testProcessBypassesOnMultipleNonValue() { + public function testProcessBypassesOnMultipleNonValue(): void { $configuration['method'] = 'process'; $configuration['value'] = [1, 1, 2, 3, 5, 8]; $configuration['not_equals'] = TRUE; @@ -83,7 +83,7 @@ public function testProcessBypassesOnMultipleNonValue() { /** * @covers ::row */ - public function testRowBypassesOnMultipleNonValue() { + public function testRowBypassesOnMultipleNonValue(): void { $configuration['method'] = 'row'; $configuration['value'] = [1, 1, 2, 3, 5, 8]; $configuration['not_equals'] = TRUE; @@ -98,10 +98,10 @@ public function testRowBypassesOnMultipleNonValue() { /** * @covers ::row */ - public function testRowSkipsOnValue() { + public function testRowSkipsOnValue(): void { $configuration['method'] = 'row'; $configuration['value'] = 86; - $this->setExpectedException(MigrateSkipRowException::class); + $this->expectException(MigrateSkipRowException::class); (new SkipOnValue($configuration, 'skip_on_value', [])) ->transform('86', $this->migrateExecutable, $this->row, 'destinationproperty'); } @@ -109,7 +109,7 @@ public function testRowSkipsOnValue() { /** * @covers ::row */ - public function testRowBypassesOnNonValue() { + public function testRowBypassesOnNonValue(): void { $configuration['method'] = 'row'; $configuration['value'] = 'sourcevalue'; $configuration['not_equals'] = TRUE; @@ -125,9 +125,9 @@ public function testRowBypassesOnNonValue() { /** * @covers ::row */ - public function testRequiredRowConfiguration() { + public function testRequiredRowConfiguration(): void { $configuration['method'] = 'row'; - $this->setExpectedException(MigrateException::class); + $this->expectException(MigrateException::class); (new SkipOnValue($configuration, 'skip_on_value', [])) ->transform('sourcevalue', $this->migrateExecutable, $this->row, 'destinationproperty'); } @@ -135,9 +135,9 @@ public function testRequiredRowConfiguration() { /** * @covers ::process */ - public function testRequiredProcessConfiguration() { + public function testRequiredProcessConfiguration(): void { $configuration['method'] = 'process'; - $this->setExpectedException(MigrateException::class); + $this->expectException(MigrateException::class); (new SkipOnValue($configuration, 'skip_on_value', [])) ->transform('sourcevalue', $this->migrateExecutable, $this->row, 'destinationproperty'); } diff --git a/web/modules/migrate_plus/tests/src/Unit/process/StrReplaceTest.php b/web/modules/migrate_plus/tests/src/Unit/process/StrReplaceTest.php index 7194c711be4e73066bc035136e30c7b99ca6cdf9..c015cf362eb0ee18f2040040429ccfb2a2c086e9 100644 --- a/web/modules/migrate_plus/tests/src/Unit/process/StrReplaceTest.php +++ b/web/modules/migrate_plus/tests/src/Unit/process/StrReplaceTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\migrate_plus\Unit\process; +use Drupal\migrate\MigrateException; use Drupal\migrate_plus\Plugin\migrate\process\StrReplace; use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase; @@ -16,7 +17,7 @@ class StrReplaceTest extends MigrateProcessTestCase { /** * Test for a simple str_replace string. */ - public function testStrReplace() { + public function testStrReplace(): void { $value = 'vero eos et accusam et justo vero'; $configuration['search'] = 'et'; $configuration['replace'] = 'that'; @@ -29,7 +30,7 @@ public function testStrReplace() { /** * Test for case insensitive searches. */ - public function testStrIreplace() { + public function testStrIreplace(): void { $value = 'VERO eos et accusam et justo vero'; $configuration['search'] = 'vero'; $configuration['replace'] = 'that'; @@ -43,7 +44,7 @@ public function testStrIreplace() { /** * Test for regular expressions. */ - public function testPregReplace() { + public function testPregReplace(): void { $value = 'vero eos et 123 accusam et justo 123 duo'; $configuration['search'] = '/[0-9]{3}/'; $configuration['replace'] = 'the'; @@ -56,29 +57,31 @@ public function testPregReplace() { /** * Test for MigrateException for "search" configuration. */ - public function testSearchMigrateException() { + public function testSearchMigrateException(): void { $value = 'vero eos et accusam et justo vero'; $configuration['replace'] = 'that'; $plugin = new StrReplace($configuration, 'str_replace', []); - $this->setExpectedException('\Drupal\migrate\MigrateException', '"search" must be configured.'); + $this->expectException(MigrateException::class); + $this->expectExceptionMessage('"search" must be configured.'); $plugin->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); } /** * Test for MigrateException for "replace" configuration. */ - public function testReplaceMigrateException() { + public function testReplaceMigrateException(): void { $value = 'vero eos et accusam et justo vero'; $configuration['search'] = 'et'; $plugin = new StrReplace($configuration, 'str_replace', []); - $this->setExpectedException('\Drupal\migrate\MigrateException', '"replace" must be configured.'); + $this->expectException(MigrateException::class); + $this->expectExceptionMessage('"replace" must be configured.'); $plugin->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); } /** * Test for multiple. */ - public function testIsMultiple() { + public function testIsMultiple(): void { $value = [ 'vero eos et accusam et justo vero', 'et eos vero accusam vero justo et', diff --git a/web/modules/migrate_plus/tests/src/Unit/process/TransliterationTest.php b/web/modules/migrate_plus/tests/src/Unit/process/TransliterationTest.php index dbfe288570f10bd877473cf2a23dfdc6da681142..2ed635656e6ce38dd5dccab4388fb14263a40d69 100644 --- a/web/modules/migrate_plus/tests/src/Unit/process/TransliterationTest.php +++ b/web/modules/migrate_plus/tests/src/Unit/process/TransliterationTest.php @@ -25,7 +25,7 @@ class TransliterationTest extends MigrateProcessTestCase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { $this->transliteration = new PhpTransliteration(); $this->row = $this->getMockBuilder(Row::class) ->disableOriginalConstructor() @@ -39,7 +39,7 @@ protected function setUp() { /** * Tests transliteration transformation of non-alphanumeric characters. */ - public function testTransform() { + public function testTransform(): void { $actual = '9000004351_53494854_Spøgelsesjægerneáéö'; $expected_result = '9000004351_53494854_Spogelsesjaegerneaeo'; diff --git a/web/modules/migrate_tools/composer.json b/web/modules/migrate_tools/composer.json index d5c8fca8fd46ecb2e9c889dea5fd38082b16baf9..a521cf6f2852f2908956e565aadd8c962536991f 100644 --- a/web/modules/migrate_tools/composer.json +++ b/web/modules/migrate_tools/composer.json @@ -1,27 +1,45 @@ { - "name": "drupal/migrate_tools", - "description": "Tools to assist in developing and running migrations.", - "type": "drupal-module", - "homepage": "http://drupal.org/project/migrate_tools", - "support": { - "issues": "http://drupal.org/project/migrate_tools", - "irc": "irc://irc.freenode.org/drupal-migrate", - "source": "http://cgit.drupalcode.org/migrate_tools" - }, - "license": "GPL-2.0+", - "require": { - "drupal/migrate_plus": "^4" - }, - "require-dev": { - "drupal/coder": "^8", - "drupal/migrate_source_csv": "^2.2" - }, - "minimum-stability": "dev", - "extra": { - "drush": { - "services": { - "drush.services.yml": "^9" - } + "name": "drupal/migrate_tools", + "description": "Tools to assist in developing and running migrations.", + "type": "drupal-module", + "homepage": "http://drupal.org/project/migrate_tools", + "authors": [ + { + "name": "Mike Ryan", + "homepage":"https://www.drupal.org/u/mikeryan", + "role": "Maintainer" + }, + { + "name": "Lucas Hedding", + "homepage": "https://www.drupal.org/u/heddn", + "role": "Maintainer" + } + ], + "support": { + "issues": "https://www.drupal.org/project/issues/migrate_tools", + "slack": "#migrate", + "source": "https://git.drupalcode.org/project/migrate_tools" + }, + "license": "GPL-2.0-or-later", + "require": { + "php": ">=7.1", + "drupal/migrate_plus": "^5", + "drupal/core": "^8.8 | ^9" + }, + "require-dev": { + "drupal/migrate_plus": "^5", + "drupal/migrate_source_csv": "^3", + "drush/drush": "^10" + }, + "suggest": { + "drush/drush": "^9 || ^10" + }, + "minimum-stability": "dev", + "extra": { + "drush": { + "services": { + "drush.services.yml": "^9 || ^10" + } + } } - } } diff --git a/web/modules/migrate_tools/migrate_tools.drush.inc b/web/modules/migrate_tools/migrate_tools.drush.inc deleted file mode 100644 index 73ddec5307cf285b602278ae90d00fd4014908ac..0000000000000000000000000000000000000000 --- a/web/modules/migrate_tools/migrate_tools.drush.inc +++ /dev/null @@ -1,561 +0,0 @@ -<?php - -/** - * @file - * Command-line tools to aid performing and developing migrations. - */ - -use Drupal\Component\Utility\Unicode; -use Drupal\migrate\Exception\RequirementsException; -use Drupal\migrate\Plugin\MigrationInterface; -use Drupal\migrate\Plugin\RequirementsInterface; -use Drupal\migrate_plus\Entity\MigrationGroup; -use Drupal\migrate_tools\DrushLogMigrateMessage; -use Drupal\migrate_tools\MigrateExecutable; - -/** - * Implements hook_drush_command(). - */ -function migrate_tools_drush_command() { - $items['migrate-status'] = [ - 'description' => 'List all migrations with current status.', - 'options' => [ - 'group' => 'A comma-separated list of migration groups to list', - 'tag' => 'Name of the migration tag to list', - 'names-only' => 'Only return names, not all the details (faster)', - ], - 'arguments' => [ - 'migration' => 'Restrict to a comma-separated list of migrations. Optional', - ], - 'examples' => [ - 'migrate-status' => 'Retrieve status for all migrations', - 'migrate-status --group=beer' => 'Retrieve status for all migrations in a given group', - 'migrate-status --tag=user' => 'Retrieve status for all migrations with a given tag', - 'migrate-status --group=beer --tag=user' => 'Retrieve status for all migrations in the beer group and with the user tag', - 'migrate-status beer_term,beer_node' => 'Retrieve status for specific migrations', - ], - 'drupal dependencies' => ['migrate_tools'], - 'aliases' => ['ms'], - ]; - - $items['migrate-import'] = [ - 'description' => 'Perform one or more migration processes.', - 'options' => [ - 'all' => 'Process all migrations.', - 'group' => 'A comma-separated list of migration groups to import', - 'tag' => 'Name of the migration tag to import', - 'limit' => 'Limit on the number of items to process in each migration', - 'feedback' => 'Frequency of progress messages, in items processed', - 'idlist' => 'Comma-separated list of IDs to import', - 'update' => ' In addition to processing unprocessed items from the source, update previously-imported items with the current data', - 'force' => 'Force an operation to run, even if all dependencies are not satisfied', - 'execute-dependencies' => 'Execute all dependent migrations first.', - ], - 'arguments' => [ - 'migration' => 'ID of migration(s) to import. Delimit multiple using commas.', - ], - 'examples' => [ - 'migrate-import --all' => 'Perform all migrations', - 'migrate-import --group=beer' => 'Import all migrations in the beer group', - 'migrate-import --tag=user' => 'Import all migrations with the user tag', - 'migrate-import --group=beer --tag=user' => 'Import all migrations in the beer group and with the user tag', - 'migrate-import beer_term,beer_node' => 'Import new terms and nodes', - 'migrate-import beer_user --limit=2' => 'Import no more than 2 users', - 'migrate-import beer_user --idlist=5' => 'Import the user record with source ID 5', - ], - 'drupal dependencies' => ['migrate_tools'], - 'aliases' => ['mi', 'mim'], - ]; - - $items['migrate-rollback'] = [ - 'description' => 'Rollback one or more migrations.', - 'options' => [ - 'all' => 'Process all migrations.', - 'group' => 'A comma-separated list of migration groups to rollback', - 'tag' => 'ID of the migration tag to rollback', - 'feedback' => 'Frequency of progress messages, in items processed', - ], - 'arguments' => [ - 'migration' => 'Name of migration(s) to rollback. Delimit multiple using commas.', - ], - 'examples' => [ - 'migrate-rollback --all' => 'Perform all migrations', - 'migrate-rollback --group=beer' => 'Rollback all migrations in the beer group', - 'migrate-rollback --tag=user' => 'Rollback all migrations with the user tag', - 'migrate-rollback --group=beer --tag=user' => 'Rollback all migrations in the beer group and with the user tag', - 'migrate-rollback beer_term,beer_node' => 'Rollback imported terms and nodes', - ], - 'drupal dependencies' => ['migrate_tools'], - 'aliases' => ['mr'], - ]; - - $items['migrate-stop'] = [ - 'description' => 'Stop an active migration operation.', - 'arguments' => [ - 'migration' => 'ID of migration to stop', - ], - 'drupal dependencies' => ['migrate_tools'], - 'aliases' => ['mst'], - ]; - - $items['migrate-reset-status'] = [ - 'description' => 'Reset a active migration\'s status to idle.', - 'arguments' => [ - 'migration' => 'ID of migration to reset', - ], - 'drupal dependencies' => ['migrate_tools'], - 'aliases' => ['mrs'], - ]; - - $items['migrate-messages'] = [ - 'description' => 'View any messages associated with a migration.', - 'arguments' => [ - 'migration' => 'ID of the migration', - ], - 'options' => [ - 'csv' => 'Export messages as a CSV', - ], - 'examples' => [ - 'migrate-messages MyNode' => 'Show all messages for the MyNode migration', - ], - 'drupal dependencies' => ['migrate_tools'], - 'aliases' => ['mmsg'], - ]; - - $items['migrate-fields-source'] = [ - 'description' => 'List the fields available for mapping in a source.', - 'arguments' => [ - 'migration' => 'ID of the migration', - ], - 'examples' => [ - 'migrate-fields-source my_node' => 'List fields for the source in the my_node migration', - ], - 'drupal dependencies' => ['migrate_tools'], - 'aliases' => ['mfs'], - ]; - - return $items; -} - -/** - * Display migration status. - * - * @param string $migration_names - * The migration names. - */ -function drush_migrate_tools_migrate_status($migration_names = '') { - $names_only = drush_get_option('names-only'); - - $migrations = drush_migrate_tools_migration_list($migration_names); - - $table = []; - // Take it one group at a time, listing the migrations within each group. - foreach ($migrations as $group_id => $migration_list) { - $group = MigrationGroup::load($group_id); - $group_name = !empty($group) ? "{$group->label()} ({$group->id()})" : $group_id; - if ($names_only) { - $table[] = [ - dt('Group: @name', ['@name' => $group_name]), - ]; - } - else { - $table[] = [ - dt('Group: @name', ['@name' => $group_name]), - dt('Status'), - dt('Total'), - dt('Imported'), - dt('Unprocessed'), - dt('Last imported'), - ]; - } - foreach ($migration_list as $migration_id => $migration) { - try { - $map = $migration->getIdMap(); - $imported = $map->importedCount(); - $source_plugin = $migration->getSourcePlugin(); - } - catch (Exception $e) { - drush_log(dt('Failure retrieving information on @migration: @message', - ['@migration' => $migration_id, '@message' => $e->getMessage()])); - continue; - } - if ($names_only) { - $table[] = [$migration_id]; - } - else { - try { - $source_rows = $source_plugin->count(); - // -1 indicates uncountable sources. - if ($source_rows == -1) { - $source_rows = dt('N/A'); - $unprocessed = dt('N/A'); - } - else { - $unprocessed = $source_rows - $map->processedCount(); - } - } - catch (Exception $e) { - drush_print($e->getMessage()); - drush_log(dt('Could not retrieve source count from @migration: @message', - ['@migration' => $migration_id, '@message' => $e->getMessage()])); - $source_rows = dt('N/A'); - $unprocessed = dt('N/A'); - } - - $status = $migration->getStatusLabel(); - $migrate_last_imported_store = \Drupal::keyValue('migrate_last_imported'); - $last_imported = $migrate_last_imported_store->get($migration->id(), FALSE); - if ($last_imported) { - /** @var \Drupal\Core\Datetime\DateFormatter $date_formatter */ - $date_formatter = \Drupal::service('date.formatter'); - $last_imported = $date_formatter->format($last_imported / 1000, - 'custom', 'Y-m-d H:i:s'); - } - else { - $last_imported = ''; - } - $table[] = [ - $migration_id, - $status, - $source_rows, - $imported, - $unprocessed, - $last_imported, - ]; - } - } - } - drush_print_table($table); -} - -/** - * Import a migration. - * - * @param string $migration_names - * The migration names. - */ -function drush_migrate_tools_migrate_import($migration_names = '') { - $group_names = drush_get_option('group'); - $tag_names = drush_get_option('tag'); - $all = drush_get_option('all'); - - // Display a depreciation message if "mi" alias is used. - $args = drush_get_arguments(); - if ($args[0] === 'mi') { - drush_log('The \'mi\' alias is deprecated and will no longer work with Drush 9. Consider the use of \'mim\' alias instead.', 'warning'); - } - - $options = []; - if (!$all && !$group_names && !$migration_names && !$tag_names) { - drush_set_error('MIGRATE_ERROR', dt('You must specify --all, --group, --tag or one or more migration names separated by commas')); - return; - } - - foreach (['limit', 'feedback', 'idlist', 'update', 'force'] as $option) { - if (drush_get_option($option)) { - $options[$option] = drush_get_option($option); - } - } - - $migrations = drush_migrate_tools_migration_list($migration_names); - if (empty($migrations)) { - drush_log(dt('No migrations found.'), 'error'); - } - - // Take it one group at a time, importing the migrations within each group. - foreach ($migrations as $group_id => $migration_list) { - array_walk($migration_list, '_drush_migrate_tools_execute_migration', $options); - } -} - -/** - * Executes a single migration. - * - * If the --execute-dependencies option was given, the migration's dependencies - * will also be executed first. - * - * @param \Drupal\migrate\Plugin\MigrationInterface $migration - * The migration to execute. - * @param string $migration_id - * The migration ID (not used, just an artifact of array_walk()). - * @param array $options - * Additional options for the migration. - */ -function _drush_migrate_tools_execute_migration(MigrationInterface $migration, $migration_id, array $options = []) { - $log = new DrushLogMigrateMessage(); - - if (drush_get_option('execute-dependencies')) { - if ($required_IDS = $migration->get('requirements')) { - $manager = \Drupal::service('plugin.manager.migration'); - $required_migrations = $manager->createInstances($required_IDS); - $dependency_options = array_merge($options, ['is_dependency' => TRUE]); - array_walk($required_migrations, __FUNCTION__, $dependency_options); - } - } - if (!empty($options['force'])) { - $migration->set('requirements', []); - } - if (!empty($options['update'])) { - $migration->getIdMap()->prepareUpdate(); - } - $executable = new MigrateExecutable($migration, $log, $options); - // Function drush_op() provides --simulate support. - drush_op([$executable, 'import']); - if ($count = $executable->getFailedCount()) { - // Nudge Drush to use a non-zero exit code. - drush_set_error('MIGRATE_ERROR', dt('!name Migration - !count failed.', [ - '!name' => $migration_id, - '!count' => $count, - ])); - } -} - -/** - * Rollback migrations. - * - * @param string $migration_names - * The migration names. - */ -function drush_migrate_tools_migrate_rollback($migration_names = '') { - $group_names = drush_get_option('group'); - $tag_names = drush_get_option('tag'); - $all = drush_get_option('all'); - $options = []; - if (!$all && !$group_names && !$migration_names && !$tag_names) { - drush_set_error('MIGRATE_ERROR', dt('You must specify --all, --group, --tag, or one or more migration names separated by commas')); - return; - } - - if (drush_get_option('feedback')) { - $options['feedback'] = drush_get_option('feedback'); - } - - $log = new DrushLogMigrateMessage(); - - $migrations = drush_migrate_tools_migration_list($migration_names); - if (empty($migrations)) { - drush_log(dt('No migrations found.'), 'error'); - } - - // Take it one group at a time, rolling back the migrations within each group. - foreach ($migrations as $group_id => $migration_list) { - // Roll back in reverse order. - $migration_list = array_reverse($migration_list); - foreach ($migration_list as $migration_id => $migration) { - $executable = new MigrateExecutable($migration, $log, $options); - // drush_op() provides --simulate support. - drush_op([$executable, 'rollback']); - } - } -} - -/** - * Stop a migration. - * - * @param string $migration_id - * The migration id. - */ -function drush_migrate_tools_migrate_stop($migration_id = '') { - /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ - $migration = \Drupal::service('plugin.manager.migration') - ->createInstance($migration_id); - if ($migration) { - $status = $migration->getStatus(); - switch ($status) { - case MigrationInterface::STATUS_IDLE: - drush_log(dt('Migration @id is idle', ['@id' => $migration_id]), 'warning'); - break; - case MigrationInterface::STATUS_DISABLED: - drush_log(dt('Migration @id is disabled', ['@id' => $migration_id]), 'warning'); - break; - case MigrationInterface::STATUS_STOPPING: - drush_log(dt('Migration @id is already stopping', ['@id' => $migration_id]), 'warning'); - break; - default: - $migration->interruptMigration(MigrationInterface::RESULT_STOPPED); - drush_log(dt('Migration @id requested to stop', ['@id' => $migration_id]), 'success'); - break; - } - } - else { - drush_log(dt('Migration @id does not exist', ['@id' => $migration_id]), 'error'); - } -} - -/** - * Reset status. - * - * @param string $migration_id - * The migration id. - */ -function drush_migrate_tools_migrate_reset_status($migration_id = '') { - /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ - $migration = \Drupal::service('plugin.manager.migration') - ->createInstance($migration_id); - if ($migration) { - $status = $migration->getStatus(); - if ($status == MigrationInterface::STATUS_IDLE) { - drush_log(dt('Migration @id is already Idle', ['@id' => $migration_id]), 'warning'); - } - else { - $migration->setStatus(MigrationInterface::STATUS_IDLE); - drush_log(dt('Migration @id reset to Idle', ['@id' => $migration_id]), 'status'); - } - } - else { - drush_log(dt('Migration @id does not exist', ['@id' => $migration_id]), 'error'); - } -} - -/** - * Print messages. - * - * @param string $migration_id - * The migration id. - */ -function drush_migrate_tools_migrate_messages($migration_id) { - /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ - $migration = \Drupal::service('plugin.manager.migration') - ->createInstance($migration_id); - if ($migration) { - $map = $migration->getIdMap(); - $first = TRUE; - $table = []; - foreach ($map->getMessageIterator() as $row) { - unset($row->msgid); - if ($first) { - // @todo: Ideally, replace sourceid* with source key names. Or, should - // getMessageIterator() do that? - foreach ($row as $column => $value) { - $table[0][] = $column; - } - $first = FALSE; - } - $table[] = (array) $row; - } - if (empty($table)) { - drush_log(dt('No messages for this migration'), 'status'); - } - else { - if (drush_get_option('csv')) { - foreach ($table as $row) { - fputcsv(STDOUT, $row); - } - } - else { - $widths = []; - foreach ($table[0] as $header) { - $widths[] = strlen($header) + 1; - } - drush_print_table($table, TRUE, $widths); - } - } - } - else { - drush_log(dt('Migration @id does not exist', ['@id' => $migration_id]), 'error'); - } -} - -/** - * Print source fields. - * - * @param string $migration_id - * The migration id. - */ -function drush_migrate_tools_migrate_fields_source($migration_id) { - /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ - $migration = \Drupal::service('plugin.manager.migration') - ->createInstance($migration_id); - if ($migration) { - $source = $migration->getSourcePlugin(); - $table = []; - foreach ($source->fields() as $machine_name => $description) { - $table[] = [strip_tags($description), $machine_name]; - } - drush_print_table($table); - } - else { - drush_log(dt('Migration @id does not exist', ['@id' => $migration_id]), 'error'); - } -} - -/** - * Retrieve a list of active migrations. - * - * @param string $migration_ids - * Comma-separated list of migrations - if present, return only these - * migrations. - * - * @return \Drupal\migrate\Plugin\MigrationInterface[][] - * An array keyed by migration group, each value containing an array of - * migrations or an empty array if no migrations match the input criteria. - */ -function drush_migrate_tools_migration_list($migration_ids = '') { - // Filter keys must match the migration configuration property name. - $filter['migration_group'] = drush_get_option('group') ? explode(',', drush_get_option('group')) : []; - $filter['migration_tags'] = drush_get_option('tag') ? explode(',', drush_get_option('tag')) : []; - - $manager = \Drupal::service('plugin.manager.migration'); - $plugins = $manager->createInstances([]); - $matched_migrations = []; - - // Get the set of migrations that may be filtered. - if (empty($migration_ids)) { - $matched_migrations = $plugins; - } - else { - // Get the requested migrations. - $migration_ids = explode(',', Unicode::strtolower($migration_ids)); - foreach ($plugins as $id => $migration) { - if (in_array(Unicode::strtolower($id), $migration_ids)) { - $matched_migrations[$id] = $migration; - } - } - } - - // Do not return any migrations which fail to meet requirements. - /** @var \Drupal\migrate\Plugin\Migration $migration */ - foreach ($matched_migrations as $id => $migration) { - if ($migration->getSourcePlugin() instanceof RequirementsInterface) { - try { - $migration->getSourcePlugin()->checkRequirements(); - } - catch (RequirementsException $e) { - unset($matched_migrations[$id]); - } - } - } - - // Filters the matched migrations if a group or a tag has been input. - if (!empty($filter['migration_group']) || !empty($filter['migration_tags'])) { - // Get migrations in any of the specified groups and with any of the - // specified tags. - foreach ($filter as $property => $values) { - if (!empty($values)) { - $filtered_migrations = []; - foreach ($values as $search_value) { - foreach ($matched_migrations as $id => $migration) { - // Cast to array because migration_tags can be an array. - $configured_values = (array) $migration->get($property); - $configured_id = (in_array($search_value, $configured_values)) ? $search_value : 'default'; - if (empty($search_value) || $search_value == $configured_id) { - if (empty($migration_ids) || in_array(Unicode::strtolower($id), $migration_ids)) { - $filtered_migrations[$id] = $migration; - } - } - } - } - $matched_migrations = $filtered_migrations; - } - } - } - - // Sort the matched migrations by group. - if (!empty($matched_migrations)) { - foreach ($matched_migrations as $id => $migration) { - $configured_group_id = empty($migration->get('migration_group')) ? 'default' : $migration->get('migration_group'); - $migrations[$configured_group_id][$id] = $migration; - } - } - return isset($migrations) ? $migrations : []; -} diff --git a/web/modules/migrate_tools/migrate_tools.info.yml b/web/modules/migrate_tools/migrate_tools.info.yml index e3cb18da29ec395afa91997e5ad162061d055c74..ef848aa5f41c23358344cf6d1ba597c4a2882b04 100644 --- a/web/modules/migrate_tools/migrate_tools.info.yml +++ b/web/modules/migrate_tools/migrate_tools.info.yml @@ -2,15 +2,16 @@ type: module name: Migrate Tools description: 'Tools to assist in developing and running migrations.' package: Migration -# core: 8.x +core: 8.x +core_version_requirement: ^8 || ^9 +configure: entity.migration_group.list dependencies: - drupal:migrate (>=8.3) - migrate_plus:migrate_plus test_dependencies: - migrate_source_csv:migrate_source_csv (>=8.x-2.2) -# Information added by Drupal.org packaging script on 2018-08-27 -version: '8.x-4.0' -core: '8.x' +# Information added by Drupal.org packaging script on 2020-04-30 +version: '8.x-5.0' project: 'migrate_tools' -datestamp: 1535380087 +datestamp: 1588260533 diff --git a/web/modules/migrate_tools/migrate_tools.links.task.yml b/web/modules/migrate_tools/migrate_tools.links.task.yml index da508a0ff0e2a1c8bd54cc75615d879a3c5a06f5..df0ae0e0185ac0631e7d2c4e2510eea73ee5454c 100644 --- a/web/modules/migrate_tools/migrate_tools.links.task.yml +++ b/web/modules/migrate_tools/migrate_tools.links.task.yml @@ -40,14 +40,7 @@ entity.migration.delete_form: title: Delete weight: 10 - entity.migration.overview_general_edit: title: Overview route_name: entity.migration.edit_form parent_id: entity.migration.edit_form - -entity.migration.overview_source_edit: - title: Source - route_name: migrate_tools.source_csv - parent_id: entity.migration.edit_form - weight: 1 diff --git a/web/modules/migrate_tools/migrate_tools.module b/web/modules/migrate_tools/migrate_tools.module index b8113fe3d8b2b29514a1a3d585ebaf48742a8aca..6e4dde593f89c323f82cbf4017af9181f2203ec7 100644 --- a/web/modules/migrate_tools/migrate_tools.module +++ b/web/modules/migrate_tools/migrate_tools.module @@ -28,31 +28,3 @@ function migrate_tools_entity_type_build(array &$entity_types) { ->setLinkTemplate('edit-form', '/admin/structure/migrate/manage/{migration_group}') ->setLinkTemplate('delete-form', '/admin/structure/migrate/manage/{migration_group}/delete'); } - -/** - * Implements hook_migration_plugins_alter(). - */ -function migrate_tools_migration_plugins_alter(array &$migrations) { - /** @var \Drupal\Core\TempStore\PrivateTempStoreFactory $store */ - $tempStoreFactory = \Drupal::service('tempstore.private'); - $store = $tempStoreFactory->get('migrate_tools'); - // TODO: remove work-around after - // https://www.drupal.org/project/drupal/issues/2860341 is fixed. - if (!\Drupal::request()->hasSession()) { - $session = \Drupal::service('session'); - \Drupal::request()->setSession($session); - $session->start(); - } - // Get the list of changed migrations. - $migrationsChanged = $store->get('migrations_changed'); - if (isset($store) && (is_array($migrationsChanged))) { - // Alter the source column names for each changed migration. - foreach ($migrationsChanged as $id) { - $data = $store->get($id); - if (isset($data['changed'])) { - $migrations[$id]['source']['column_names'] = $data['changed']; - } - } - } - -} diff --git a/web/modules/migrate_tools/migrate_tools.routing.yml b/web/modules/migrate_tools/migrate_tools.routing.yml index 08f43e5b58488576af25fac906cd8a4cdfe97574..bc9f866a33404a359b35e56f2baca3a913f91064 100644 --- a/web/modules/migrate_tools/migrate_tools.routing.yml +++ b/web/modules/migrate_tools/migrate_tools.routing.yml @@ -157,7 +157,7 @@ migrate_tools.messages: path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/messages' defaults: _controller: '\Drupal\migrate_tools\Controller\MessageController::overview' - _title: 'Messages' + _title_callback: '\Drupal\migrate_tools\Controller\MessageController::title' _migrate_group: true requirements: _permission: 'administer migrations' @@ -181,17 +181,3 @@ migrate_tools.execute: type: entity:migration migration_group: type: entity:migration_group -migrate_tools.source_csv: - path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/source/edit' - defaults: - _form: 'Drupal\migrate_tools\Form\SourceCsvForm' - _migrate_group: true - requirements: - _permission: 'administer migrations' - _custom_access: 'Drupal\migrate_tools\Form\SourceCsvForm::access' - options: - parameters: - migration: - type: entity:migration - migration_group: - type: entity:migration_group diff --git a/web/modules/migrate_tools/migrate_tools.services.yml b/web/modules/migrate_tools/migrate_tools.services.yml index fa61e007f537ee1863af5c91f567664f288ce344..50abdd21d1d96ea63aaa4f359e3c3172f1f7ca8b 100644 --- a/web/modules/migrate_tools/migrate_tools.services.yml +++ b/web/modules/migrate_tools/migrate_tools.services.yml @@ -7,3 +7,14 @@ services: class: Drupal\migrate_tools\Routing\RouteProcessor tags: - { name: route_processor_outbound } + migrate_tools.migration_drush_command_progress: + class: Drupal\migrate_tools\EventSubscriber\MigrationDrushCommandProgress + tags: + - { name: event_subscriber } + arguments: ['@logger.channel.migrate_tools'] + migrate_tools.migration_sync: + class: Drupal\migrate_tools\EventSubscriber\MigrationImportSync + tags: + - { name: event_subscriber } + arguments: + - '@event_dispatcher' diff --git a/web/modules/migrate_tools/phpcs.xml b/web/modules/migrate_tools/phpcs.xml deleted file mode 100644 index a18054efbc2dc47c2595364bb13a992a0fffbb4a..0000000000000000000000000000000000000000 --- a/web/modules/migrate_tools/phpcs.xml +++ /dev/null @@ -1,207 +0,0 @@ -<?xml version="1.0"?> -<ruleset name="Drupal coding standards"> - <description>Drupal 8 coding standards</description> - - <file>.</file> - <arg name="extensions" value="inc,install,module,php,profile,test,theme"/> - - <!--Exclude third party code.--> - <exclude-pattern>./vendor/*</exclude-pattern> - <!--Run Drupal standards.--> - <rule ref="Drupal.Array"/> - <rule ref="Drupal.Classes"/> - <rule ref="Drupal.Commenting"> - <!-- TagsNotGrouped and ParamGroup have false-positives. - @see https://www.drupal.org/node/2060925 --> - <exclude name="Drupal.Commenting.DocComment.TagsNotGrouped"/> - <exclude name="Drupal.Commenting.DocComment.ParamGroup"/> - </rule> - <rule ref="Drupal.ControlStructures"/> - <rule ref="Drupal.CSS"/> - <rule ref="Drupal.Files"/> - <rule ref="Drupal.Formatting"/> - <rule ref="Drupal.Functions"/> - <rule ref="Drupal.InfoFiles"/> - <rule ref="Drupal.Methods"/> - <rule ref="Drupal.NamingConventions"/> - <rule ref="Drupal.Scope"/> - <rule ref="Drupal.Semantics"/> - <rule ref="Drupal.Strings"/> - <rule ref="Drupal.WhiteSpace"/> - - <!-- Drupal Practice sniffs --> - <rule ref="DrupalPractice.Commenting"/> - - <!-- Generic sniffs --> - <rule ref="Generic.Arrays.DisallowLongArraySyntax"/> - <rule ref="Generic.Files.ByteOrderMark"/> - <rule ref="Generic.Files.LineEndings"/> - <rule ref="Generic.Formatting.SpaceAfterCast"/> - <rule ref="Generic.Functions.FunctionCallArgumentSpacing"/> - <rule ref="Generic.Functions.OpeningFunctionBraceKernighanRitchie"> - <properties> - <property name="checkClosures" value="true"/> - </properties> - </rule> - <rule ref="Generic.NamingConventions.ConstructorName"/> - <rule ref="Generic.NamingConventions.UpperCaseConstantName"/> - <rule ref="Generic.PHP.DeprecatedFunctions"/> - <rule ref="Generic.PHP.DisallowShortOpenTag"/> - <rule ref="Generic.PHP.LowerCaseKeyword"/> - <rule ref="Generic.PHP.UpperCaseConstant"/> - <rule ref="Generic.WhiteSpace.DisallowTabIndent"/> - - <!-- MySource sniffs --> - <rule ref="MySource.Debug.DebugCode"/> - - <!-- PEAR sniffs --> - <rule ref="PEAR.Files.IncludingFile"/> - <!-- Disable some error messages that we do not want. --> - <rule ref="PEAR.Files.IncludingFile.UseIncludeOnce"> - <severity>0</severity> - </rule> - <rule ref="PEAR.Files.IncludingFile.UseInclude"> - <severity>0</severity> - </rule> - <rule ref="PEAR.Files.IncludingFile.UseRequireOnce"> - <severity>0</severity> - </rule> - <rule ref="PEAR.Files.IncludingFile.UseRequire"> - <severity>0</severity> - </rule> - <rule ref="PEAR.Functions.ValidDefaultValue"/> - - <!-- PEAR sniffs --> - <rule ref="PEAR.Functions.FunctionCallSignature"/> - <!-- The sniffs inside PEAR.Functions.FunctionCallSignature silenced below are - also silenced in Drupal CS' ruleset.xml. The code below is a 1-on-1 copy - from that file. --> - <!-- Disable some error messages that we already cover. --> - <rule ref="PEAR.Functions.FunctionCallSignature.SpaceAfterOpenBracket"> - <severity>0</severity> - </rule> - <rule ref="PEAR.Functions.FunctionCallSignature.SpaceBeforeCloseBracket"> - <severity>0</severity> - </rule> - <!-- Disable some error messages that we do not want. --> - <rule ref="PEAR.Functions.FunctionCallSignature.Indent"> - <severity>0</severity> - </rule> - <rule ref="PEAR.Functions.FunctionCallSignature.ContentAfterOpenBracket"> - <severity>0</severity> - </rule> - <rule ref="PEAR.Functions.FunctionCallSignature.CloseBracketLine"> - <severity>0</severity> - </rule> - <rule ref="PEAR.Functions.FunctionCallSignature.EmptyLine"> - <severity>0</severity> - </rule> - - <!-- PSR-2 sniffs --> - <rule ref="PSR2.Classes.PropertyDeclaration"> - <exclude name="PSR2.Classes.PropertyDeclaration.Underscore"/> - </rule> - <rule ref="PSR2.Namespaces.NamespaceDeclaration"/> - <rule ref="PSR2.Namespaces.UseDeclaration"> - <exclude name="PSR2.Namespaces.UseDeclaration.UseAfterNamespace"/> - </rule> - - <!-- Squiz sniffs --> - <rule ref="Squiz.Arrays.ArrayBracketSpacing"/> - <rule ref="Squiz.Arrays.ArrayDeclaration"> - <exclude name="Squiz.Arrays.ArrayDeclaration.NoKeySpecified"/> - <exclude name="Squiz.Arrays.ArrayDeclaration.KeySpecified"/> - </rule> - <!-- Disable some error messages that we do not want. --> - <rule ref="Squiz.Arrays.ArrayDeclaration.CloseBraceNotAligned"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Arrays.ArrayDeclaration.DoubleArrowNotAligned"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Arrays.ArrayDeclaration.FirstValueNoNewline"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Arrays.ArrayDeclaration.KeyNotAligned"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Arrays.ArrayDeclaration.MultiLineNotAllowed"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Arrays.ArrayDeclaration.NoComma"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Arrays.ArrayDeclaration.NoCommaAfterLast"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Arrays.ArrayDeclaration.NotLowerCase"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Arrays.ArrayDeclaration.SingleLineNotAllowed"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Arrays.ArrayDeclaration.ValueNotAligned"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Arrays.ArrayDeclaration.ValueNoNewline"> - <severity>0</severity> - </rule> - <rule ref="Squiz.ControlStructures.ForEachLoopDeclaration"/> - <!-- Disable some error messages that we already cover. --> - <rule ref="Squiz.ControlStructures.ForEachLoopDeclaration.AsNotLower"> - <severity>0</severity> - </rule> - <rule ref="Squiz.ControlStructures.ForEachLoopDeclaration.SpaceAfterOpen"> - <severity>0</severity> - </rule> - <rule ref="Squiz.ControlStructures.ForEachLoopDeclaration.SpaceBeforeClose"> - <severity>0</severity> - </rule> - <rule ref="Squiz.ControlStructures.ForLoopDeclaration"/> - <!-- Disable some error messages that we already cover. --> - <rule ref="Squiz.ControlStructures.ForLoopDeclaration.SpacingAfterOpen"> - <severity>0</severity> - </rule> - <rule ref="Squiz.ControlStructures.ForLoopDeclaration.SpacingBeforeClose"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Functions.MultiLineFunctionDeclaration"/> - <rule ref="Squiz.Functions.MultiLineFunctionDeclaration.BraceOnSameLine"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Functions.MultiLineFunctionDeclaration.ContentAfterBrace"> - <severity>0</severity> - </rule> - <!-- Standard yet to be finalized on this (https://www.drupal.org/node/1539712). --> - <rule ref="Squiz.Functions.MultiLineFunctionDeclaration.FirstParamSpacing"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Functions.MultiLineFunctionDeclaration.Indent"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Functions.MultiLineFunctionDeclaration.CloseBracketLine"> - <severity>0</severity> - </rule> - <rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing"> - <properties> - <property name="equalsSpacing" value="1"/> - </properties> - </rule> - <rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing.NoSpaceBeforeArg"> - <severity>0</severity> - </rule> - <rule ref="Squiz.PHP.LowercasePHPFunctions"/> - <rule ref="Squiz.Strings.ConcatenationSpacing"> - <properties> - <property name="spacing" value="1"/> - <property name="ignoreNewlines" value="true"/> - </properties> - </rule> - <rule ref="Squiz.WhiteSpace.LanguageConstructSpacing" /> - <rule ref="Squiz.WhiteSpace.SemicolonSpacing"/> - <rule ref="Squiz.WhiteSpace.SuperfluousWhitespace"/> - - <!-- Zend sniffs --> - <rule ref="Zend.Files.ClosingTag"/> - -</ruleset> diff --git a/web/modules/migrate_tools/src/Commands/MigrateToolsCommands.php b/web/modules/migrate_tools/src/Commands/MigrateToolsCommands.php index 976b3c684db8784b712f308f7c8b25ad6b627100..f07e9be82e3e72c4445d355de99c380d987f2b87 100644 --- a/web/modules/migrate_tools/src/Commands/MigrateToolsCommands.php +++ b/web/modules/migrate_tools/src/Commands/MigrateToolsCommands.php @@ -3,7 +3,7 @@ namespace Drupal\migrate_tools\Commands; use Consolidation\OutputFormatters\StructuredData\RowsOfFields; -use Drupal\Component\Utility\Unicode; +use Drupal\Component\Plugin\Exception\PluginNotFoundException; use Drupal\Core\Datetime\DateFormatter; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; @@ -12,7 +12,9 @@ use Drupal\migrate\Plugin\MigrationPluginManager; use Drupal\migrate\Plugin\RequirementsInterface; use Drupal\migrate_tools\Drush9LogMigrateMessage; +use Drupal\migrate_tools\IdMapFilter; use Drupal\migrate_tools\MigrateExecutable; +use Drupal\migrate_tools\MigrateTools; use Drush\Commands\DrushCommands; /** @@ -88,6 +90,10 @@ public function __construct(MigrationPluginManager $migrationPluginManager, Date * @option group A comma-separated list of migration groups to list * @option tag Name of the migration tag to list * @option names-only Only return names, not all the details (faster) + * @option continue-on-failure When a migration fails, continue processing + * remaining migrations. + * + * @default $options [] * * @usage migrate:status * Retrieve status for all migrations @@ -118,12 +124,18 @@ public function __construct(MigrationPluginManager $migrationPluginManager, Date * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields * Migrations status formatted as table. */ - public function status($migration_names = '', array $options = ['group' => NULL, 'tag' => NULL, 'names-only' => NULL]) { + public function status($migration_names = '', array $options = [ + 'group' => self::REQ, + 'tag' => self::REQ, + 'names-only' => FALSE, + 'continue-on-failure' => FALSE, + ]) { $names_only = $options['names-only']; $migrations = $this->migrationsList($migration_names, $options); $table = []; + $errors = []; // Take it one group at a time, listing the migrations within each group. foreach ($migrations as $group_id => $migration_list) { /** @var \Drupal\migrate_plus\Entity\MigrationGroup $group */ @@ -144,12 +156,12 @@ public function status($migration_names = '', array $options = ['group' => NULL, $source_plugin = $migration->getSourcePlugin(); } catch (\Exception $e) { - $this->logger()->error( - dt( - 'Failure retrieving information on @migration: @message', - ['@migration' => $migration_id, '@message' => $e->getMessage()] - ) + $error = dt( + 'Failure retrieving information on @migration: @message', + ['@migration' => $migration_id, '@message' => $e->getMessage()] ); + $this->logger()->error($error); + $errors[] = $error; continue; } @@ -212,6 +224,11 @@ public function status($migration_names = '', array $options = ['group' => NULL, } } + // If any errors occurred, throw an exception. + if (!empty($errors)) { + throw new \Exception(implode(PHP_EOL, $errors)); + } + return new RowsOfFields($table); } @@ -231,11 +248,19 @@ public function status($migration_names = '', array $options = ['group' => NULL, * @option limit Limit on the number of items to process in each migration * @option feedback Frequency of progress messages, in items processed * @option idlist Comma-separated list of IDs to import + * @option idlist-delimiter The delimiter for records * @option update In addition to processing unprocessed items from the * source, update previously-imported items with the current data * @option force Force an operation to run, even if all dependencies are not * satisfied + * @option continue-on-failure When a migration fails, continue processing + * remaining migrations. * @option execute-dependencies Execute all dependent migrations first. + * @option skip-progress-bar Skip displaying a progress bar. + * @option sync Sync source and destination. Delete destination records that + * do not exist in the source. + * + * @default $options [] * * @usage migrate:import --all * Perform all migrations @@ -251,6 +276,8 @@ public function status($migration_names = '', array $options = ['group' => NULL, * Import no more than 2 users * @usage migrate:import beer_user --idlist=5 * Import the user record with source ID 5 + * @usage migrate:import beer_node_revision --idlist=1:2,2:3,3:5 + * Import the node revision record with source IDs [1,2], [2,3], and [3,5] * * @validate-module-enabled migrate_tools * @@ -259,21 +286,28 @@ public function status($migration_names = '', array $options = ['group' => NULL, * @throws \Exception * If there are not enough parameters to the command. */ - public function import($migration_names = '', array $options = ['all' => NULL, 'group' => NULL, 'tag' => NULL, 'limit' => NULL, 'feedback' => NULL, 'idlist' => NULL, 'update' => NULL, 'force' => NULL, 'execute-dependencies' => NULL]) { + public function import($migration_names = '', array $options = [ + 'all' => FALSE, + 'group' => self::REQ, + 'tag' => self::REQ, + 'limit' => self::REQ, + 'feedback' => self::REQ, + 'idlist' => self::REQ, + 'idlist-delimiter' => MigrateTools::DEFAULT_ID_LIST_DELIMITER, + 'update' => FALSE, + 'force' => FALSE, + 'continue-on-failure' => FALSE, + 'execute-dependencies' => FALSE, + 'skip-progress-bar' => FALSE, + 'sync' => FALSE, + ]) { $group_names = $options['group']; $tag_names = $options['tag']; $all = $options['all']; - $additional_options = []; if (!$all && !$group_names && !$migration_names && !$tag_names) { throw new \Exception(dt('You must specify --all, --group, --tag or one or more migration names separated by commas')); } - foreach (['limit', 'feedback', 'idlist', 'update', 'force', 'execute-dependencies'] as $option) { - if ($options[$option]) { - $additional_options[$option] = $options[$option]; - } - } - $migrations = $this->migrationsList($migration_names, $options); if (empty($migrations)) { $this->logger->error(dt('No migrations found.')); @@ -284,7 +318,7 @@ public function import($migration_names = '', array $options = ['all' => NULL, ' array_walk( $migration_list, [$this, 'executeMigration'], - $additional_options + $options ); } } @@ -303,6 +337,13 @@ public function import($migration_names = '', array $options = ['all' => NULL, ' * @option group A comma-separated list of migration groups to rollback * @option tag ID of the migration tag to rollback * @option feedback Frequency of progress messages, in items processed + * @option idlist Comma-separated list of IDs to rollback + * @option idlist-delimiter The delimiter for records + * @option skip-progress-bar Skip displaying a progress bar. + * @option continue-on-failure When a rollback fails, continue processing + * remaining migrations. + * + * @default $options [] * * @usage migrate:rollback --all * Perform all migrations @@ -314,6 +355,8 @@ public function import($migration_names = '', array $options = ['all' => NULL, ' * Rollback all migrations in the beer group and with the user tag * @usage migrate:rollback beer_term,beer_node * Rollback imported terms and nodes + * @usage migrate:rollback beer_user --idlist=5 + * Rollback imported user record with source ID 5 * @validate-module-enabled migrate_tools * * @aliases mr, migrate-rollback @@ -321,19 +364,23 @@ public function import($migration_names = '', array $options = ['all' => NULL, ' * @throws \Exception * If there are not enough parameters to the command. */ - public function rollback($migration_names = '', array $options = ['all' => NULL, 'group' => NULL, 'tag' => NULL, 'feedback' => NULL]) { + public function rollback($migration_names = '', array $options = [ + 'all' => FALSE, + 'group' => self::REQ, + 'tag' => self::REQ, + 'feedback' => self::REQ, + 'idlist' => self::REQ, + 'idlist-delimiter' => MigrateTools::DEFAULT_ID_LIST_DELIMITER, + 'skip-progress-bar' => FALSE, + 'continue-on-failure' => FALSE, + ]) { $group_names = $options['group']; $tag_names = $options['tag']; $all = $options['all']; - $additional_options = []; if (!$all && !$group_names && !$migration_names && !$tag_names) { throw new \Exception(dt('You must specify --all, --group, --tag, or one or more migration names separated by commas')); } - if ($options['feedback']) { - $additional_options['feedback'] = $options['feedback']; - } - $migrations = $this->migrationsList($migration_names, $options); if (empty($migrations)) { $this->logger()->error(dt('No migrations found.')); @@ -341,17 +388,42 @@ public function rollback($migration_names = '', array $options = ['all' => NULL, // Take it one group at a time, // rolling back the migrations within each group. - foreach ($migrations as $group_id => $migration_list) { + $has_failure = FALSE; + foreach ($migrations as $migration_list) { // Roll back in reverse order. $migration_list = array_reverse($migration_list); foreach ($migration_list as $migration_id => $migration) { + if ($options['skip-progress-bar']) { + $migration->set('skipProgressBar', TRUE); + } + // Initialize the Synmfony Console progress bar. + \Drupal::service('migrate_tools.migration_drush_command_progress')->initializeProgress( + $this->output(), + $migration + ); $executable = new MigrateExecutable( $migration, $this->getMigrateMessage(), - $additional_options + $options ); // drush_op() provides --simulate support. - drush_op([$executable, 'rollback']); + $result = drush_op([$executable, 'rollback']); + if ($result == MigrationInterface::RESULT_FAILED) { + $has_failure = TRUE; + $errored_migration_id = $migration_id; + } + } + } + + // If any rollbacks failed, throw an exception to generate exit status. + if ($has_failure) { + $error_message = dt('!name migration failed.', ['!name' => $errored_migration_id]); + if ($options['continue-on-failure']) { + $this->logger()->error($error_message); + } + else { + // Nudge Drush to use a non-zero exit code. + throw new \Exception($error_message); } } } @@ -402,9 +474,9 @@ public function stop($migration_id = '') { } } else { - $this->logger()->error( - dt('Migration @id does not exist', ['@id' => $migration_id]) - ); + $error = dt('Migration @id does not exist', ['@id' => $migration_id]); + $this->logger()->error($error); + throw new \Exception($error); } } @@ -439,9 +511,9 @@ public function resetStatus($migration_id = '') { } } else { - $this->logger()->error( - dt('Migration @id does not exist', ['@id' => $migration_id]) - ); + $error = dt('Migration @id does not exist', ['@id' => $migration_id]); + $this->logger()->error($error); + throw new \Exception($error); } } @@ -455,7 +527,11 @@ public function resetStatus($migration_id = '') { * * @command migrate:messages * - * @option csv Export messages as a CSV + * @option csv Export messages as a CSV (deprecated) + * @option idlist Comma-separated list of IDs to import + * @option idlist-delimiter The delimiter for records + * + * @default $options [] * * @usage migrate:messages MyNode * Show all messages for the MyNode migration @@ -466,30 +542,55 @@ public function resetStatus($migration_id = '') { * * @field-labels * source_ids_hash: Source IDs Hash + * source_ids: Source ID(s) + * destination_ids: Destination ID(s) * level: Level * message: Message - * @default-fields source_ids_hash,level,message + * @default-fields source_ids_hash,source_ids,destination_ids,level,message * * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields * Source fields of the given migration formatted as a table. */ - public function messages($migration_id, array $options = ['csv' => NULL]) { + public function messages($migration_id, array $options = [ + 'csv' => FALSE, + 'idlist' => self::REQ, + 'idlist-delimiter' => MigrateTools::DEFAULT_ID_LIST_DELIMITER, + ]) { /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ $migration = $this->migrationPluginManager->createInstance( $migration_id ); if (!$migration) { - $this->logger()->error( - dt('Migration @id does not exist', ['@id' => $migration_id]) - ); - return NULL; + $error = dt('Migration @id does not exist', ['@id' => $migration_id]); + $this->logger()->error($error); + throw new \Exception($error); } - - $map = $migration->getIdMap(); + $id_list = MigrateTools::buildIdList($options); + /** @var \Drupal\migrate\Plugin\MigrateIdMapInterface|\Drupal\migrate_tools\IdMapFilter $map */ + $map = new IdMapFilter($migration->getIdMap(), $id_list); + $source_id_keys = $this->getSourceIdKeys($map); $table = []; - foreach ($map->getMessageIterator() as $row) { + // TODO: Remove after 8.7 support goes away. + $iterator_method = method_exists($map, 'getMessages') ? 'getMessages' : 'getMessageIterator'; + foreach ($map->{$iterator_method}() as $row) { unset($row->msgid); - $table[] = (array) $row; + $array_row = (array) $row; + // If the message includes useful IDs don't print the hash. + if (count($source_id_keys) === count(array_intersect_key($source_id_keys, $array_row))) { + unset($array_row['source_ids_hash']); + } + $source_ids = $destination_ids = []; + foreach ($array_row as $name => $item) { + if (substr($name, 0, 4) === 'src_') { + $source_ids[$name] = $item; + } + if (substr($name, 0, 5) === 'dest_') { + $destination_ids[$name] = $item; + } + } + $array_row['source_ids'] = implode(', ', $source_ids); + $array_row['destination_ids'] = implode(', ', $destination_ids); + $table[] = $array_row; } if (empty($table)) { $this->logger()->notice(dt('No messages for this migration')); @@ -501,11 +602,31 @@ public function messages($migration_id, array $options = ['csv' => NULL]) { foreach ($table as $row) { fputcsv(STDOUT, $row); } + $this->logger()->notice('--csv option is deprecated in 4.5 and is removed from 5.0. Use \'--format=csv\' instead.'); + @trigger_error('--csv option is deprecated in migrate_tool:8.x-4.5 and is removed from migrate_tool:8.x-5.0. Use \'--format=csv\' instead. See https://www.drupal.org/node/123', E_USER_DEPRECATED); return NULL; } return new RowsOfFields($table); } + /** + * Get the source ID keys. + * + * @param \Drupal\migrate_tools\IdMapFilter $map + * The migration ID map. + * + * @return array + * The source ID keys. + */ + protected function getSourceIdKeys(IdMapFilter $map) { + $map->rewind(); + $columns = $map->currentSource(); + $source_id_keys = array_map(static function ($id) { + return 'src_' . $id; + }, array_keys($columns)); + return array_combine($source_id_keys, $source_id_keys); + } + /** * List the fields available for mapping in a source. * @@ -546,9 +667,9 @@ public function fieldsSource($migration_id) { return new RowsOfFields($table); } else { - $this->logger()->error( - dt('Migration @id does not exist', ['@id' => $migration_id]) - ); + $error = dt('Migration @id does not exist', ['@id' => $migration_id]); + $this->logger()->error($error); + throw new \Exception($error); } } @@ -561,20 +682,16 @@ public function fieldsSource($migration_id) { * @param array $options * Command options. * + * @default $options [] + * * @return \Drupal\migrate\Plugin\MigrationInterface[][] * An array keyed by migration group, each value containing an array of * migrations or an empty array if no migrations match the input criteria. */ protected function migrationsList($migration_ids = '', array $options = []) { // Filter keys must match the migration configuration property name. - $filter['migration_group'] = $options['group'] ? explode( - ',', - $options['group'] - ) : []; - $filter['migration_tags'] = $options['tag'] ? explode( - ',', - $options['tag'] - ) : []; + $filter['migration_group'] = explode(',', $options['group']); + $filter['migration_tags'] = explode(',', $options['tag']); $manager = $this->migrationPluginManager; $plugins = $manager->createInstances([]); @@ -586,9 +703,9 @@ protected function migrationsList($migration_ids = '', array $options = []) { } else { // Get the requested migrations. - $migration_ids = explode(',', Unicode::strtolower($migration_ids)); + $migration_ids = explode(',', mb_strtolower($migration_ids)); foreach ($plugins as $id => $migration) { - if (in_array(Unicode::strtolower($id), $migration_ids)) { + if (in_array(mb_strtolower($id), $migration_ids)) { $matched_migrations[$id] = $migration; } } @@ -597,13 +714,22 @@ protected function migrationsList($migration_ids = '', array $options = []) { // Do not return any migrations which fail to meet requirements. /** @var \Drupal\migrate\Plugin\Migration $migration */ foreach ($matched_migrations as $id => $migration) { - if ($migration->getSourcePlugin() instanceof RequirementsInterface) { - try { + try { + if ($migration->getSourcePlugin() instanceof RequirementsInterface) { $migration->getSourcePlugin()->checkRequirements(); } - catch (RequirementsException $e) { + } + catch (RequirementsException $e) { + unset($matched_migrations[$id]); + } + catch (PluginNotFoundException $exception) { + if ($options['continue-on-failure']) { + $this->logger()->error($exception->getMessage()); unset($matched_migrations[$id]); } + else { + throw $exception; + } } } @@ -617,15 +743,14 @@ protected function migrationsList($migration_ids = '', array $options = []) { foreach ($values as $search_value) { foreach ($matched_migrations as $id => $migration) { // Cast to array because migration_tags can be an array. - $configured_values = (array) $migration->get($property); - $configured_id = (in_array( - $search_value, - $configured_values - )) ? $search_value : 'default'; - if (empty($search_value) || $search_value == $configured_id) { + $definition = $migration->getPluginDefinition(); + $configured_values = (array) ($definition[$property] ?? NULL); + $configured_id = in_array($search_value, $configured_values, TRUE) ? $search_value : 'default'; + if (empty($search_value) || $search_value === $configured_id) { if (empty($migration_ids) || in_array( - Unicode::strtolower($id), - $migration_ids + mb_strtolower($id), + $migration_ids, + TRUE )) { $filtered_migrations[$id] = $migration; } @@ -640,11 +765,11 @@ protected function migrationsList($migration_ids = '', array $options = []) { // Sort the matched migrations by group. if (!empty($matched_migrations)) { foreach ($matched_migrations as $id => $migration) { - $configured_group_id = empty($migration->get('migration_group')) ? 'default' : $migration->get('migration_group'); + $configured_group_id = empty($migration->migration_group) ? 'default' : $migration->migration_group; $migrations[$configured_group_id][$id] = $migration; } } - return isset($migrations) ? $migrations : []; + return $migrations ?? []; } /** @@ -660,6 +785,8 @@ protected function migrationsList($migration_ids = '', array $options = []) { * @param array $options * Additional options of the command. * + * @default $options [] + * * @throws \Exception * If some migrations failed during execution. */ @@ -668,8 +795,9 @@ protected function executeMigration(MigrationInterface $migration, $migration_id // migration is not run multiple times. static $executed_migrations = []; - if (isset($options['execute-dependencies'])) { - $required_migrations = $migration->get('requirements'); + if ($options['execute-dependencies']) { + $definition = $migration->getPluginDefinition(); + $required_migrations = $definition['requirements'] ?? []; $required_migrations = array_filter($required_migrations, function ($value) use ($executed_migrations) { return !isset($executed_migrations[$value]); }); @@ -682,25 +810,62 @@ protected function executeMigration(MigrationInterface $migration, $migration_id $executed_migrations += $required_migrations; } } - if (!empty($options['force'])) { + if ($options['sync']) { + $migration->set('syncSource', TRUE); + } + if ($options['skip-progress-bar']) { + $migration->set('skipProgressBar', TRUE); + } + if ($options['continue-on-failure']) { + $migration->set('continueOnFailure', TRUE); + } + if ($options['force']) { $migration->set('requirements', []); } - if (!empty($options['update'])) { - $migration->getIdMap()->prepareUpdate(); + if ($options['update']) { + if (!$options['idlist']) { + $migration->getIdMap()->prepareUpdate(); + } + else { + $source_id_values_list = MigrateTools::buildIdList($options); + $keys = array_keys($migration->getSourcePlugin()->getIds()); + foreach ($source_id_values_list as $source_id_values) { + $migration->getIdMap()->setUpdate(array_combine($keys, $source_id_values)); + } + } } + + // Initialize the Synmfony Console progress bar. + \Drupal::service('migrate_tools.migration_drush_command_progress')->initializeProgress( + $this->output(), + $migration + ); + $executable = new MigrateExecutable($migration, $this->getMigrateMessage(), $options); // drush_op() provides --simulate support. - drush_op([$executable, 'import']); + $result = drush_op([$executable, 'import']); $executed_migrations += [$migration_id => $migration_id]; if ($count = $executable->getFailedCount()) { - // Nudge Drush to use a non-zero exit code. - throw new \Exception( - dt( - '!name Migration - !count failed.', - ['!name' => $migration_id, '!count' => $count] - ) + $error_message = dt( + '!name Migration - !count failed.', + ['!name' => $migration_id, '!count' => $count] ); } + elseif ($result == MigrationInterface::RESULT_FAILED) { + $error_message = dt('!name migration failed.', ['!name' => $migration_id]); + } + else { + $error_message = ''; + } + if ($error_message) { + if ($options['continue-on-failure']) { + $this->logger()->error($error_message); + } + else { + // Nudge Drush to use a non-zero exit code. + throw new \Exception($error_message); + } + } } /** diff --git a/web/modules/migrate_tools/src/Controller/MessageController.php b/web/modules/migrate_tools/src/Controller/MessageController.php index fbd347c96d56eb9d5313e72d13991939359d8895..7b16715686d124809050d5817fb297c98f5c0326 100644 --- a/web/modules/migrate_tools/src/Controller/MessageController.php +++ b/web/modules/migrate_tools/src/Controller/MessageController.php @@ -104,18 +104,21 @@ public function overview(MigrationGroupInterface $migration_group, MigratePlusMi 'field' => 'message', ]; + $result = []; $message_table = $migration_plugin->getIdMap()->messageTableName(); - $map_table = $migration_plugin->getIdMap()->mapTableName(); - $query = $this->database->select($message_table, 'msg') - ->extend('\Drupal\Core\Database\Query\PagerSelectExtender') - ->extend('\Drupal\Core\Database\Query\TableSortExtender'); - $query->innerJoin($map_table, 'map', 'msg.source_ids_hash=map.source_ids_hash'); - $query->fields('msg'); - $query->fields('map'); - $result = $query - ->limit(50) - ->orderByHeader($header) - ->execute(); + if ($this->database->schema()->tableExists($message_table)) { + $map_table = $migration_plugin->getIdMap()->mapTableName(); + $query = $this->database->select($message_table, 'msg') + ->extend('\Drupal\Core\Database\Query\PagerSelectExtender') + ->extend('\Drupal\Core\Database\Query\TableSortExtender'); + $query->innerJoin($map_table, 'map', 'msg.source_ids_hash=map.source_ids_hash'); + $query->fields('msg'); + $query->fields('map'); + $result = $query + ->limit(50) + ->orderByHeader($header) + ->execute(); + } foreach ($result as $message_row) { $column_number = 1; @@ -141,4 +144,22 @@ public function overview(MigrationGroupInterface $migration_group, MigratePlusMi return $build; } + /** + * Get the title of the page. + * + * @param \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group + * The migration group. + * @param \Drupal\migrate_plus\Entity\MigrationInterface $migration + * The $migration. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup + * The translated title. + */ + public function title(MigrationGroupInterface $migration_group, MigratePlusMigrationInterface $migration) { + return $this->t( + 'Messages of %migration', + ['%migration' => $migration->label()] + ); + } + } diff --git a/web/modules/migrate_tools/src/Controller/MigrationController.php b/web/modules/migrate_tools/src/Controller/MigrationController.php index 15c90795ca5dc179cacd9044a0d888b5a517bfa3..42fc101402a18fde60107d38ccc943820ab550c7 100644 --- a/web/modules/migrate_tools/src/Controller/MigrationController.php +++ b/web/modules/migrate_tools/src/Controller/MigrationController.php @@ -2,18 +2,18 @@ namespace Drupal\migrate_tools\Controller; -use Drupal\Core\Controller\ControllerBase; -use Drupal\Component\Utility\Xss; use Drupal\Component\Utility\Html; +use Drupal\Component\Utility\Xss; +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Routing\CurrentRouteMatch; +use Drupal\Core\Url; +use Drupal\migrate\MigrateMessage; use Drupal\migrate\Plugin\MigrationPluginManagerInterface; -use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\migrate_plus\Entity\MigrationGroupInterface; use Drupal\migrate_plus\Entity\MigrationInterface; use Drupal\migrate_tools\MigrateBatchExecutable; use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\Url; -use Drupal\migrate\MigrateMessage; /** * Returns responses for migrate_tools migration view routes. @@ -161,8 +161,8 @@ public function source(MigrationGroupInterface $migration_group, MigrationInterf * @param \Drupal\migrate_plus\Entity\MigrationInterface $migration * The $migration. * - * @return array - * A render array as expected by drupal_render(). + * @return \Symfony\Component\HttpFoundation\RedirectResponse|null + * A redirect response if the batch is progressive. Else no return value. */ public function run(MigrationGroupInterface $migration_group, MigrationInterface $migration) { $migrateMessage = new MigrateMessage(); @@ -172,7 +172,6 @@ public function run(MigrationGroupInterface $migration_group, MigrationInterface $executable = new MigrateBatchExecutable($migration_plugin, $migrateMessage, $options); $executable->batchImport(); - $migration_group = $this->currentRouteMatch->getParameter('migration_group'); $route_parameters = [ 'migration_group' => $migration_group, 'migration' => $migration->id(), @@ -211,13 +210,20 @@ public function process(MigrationGroupInterface $migration_group, MigrationInter $row = []; $row[] = ['data' => Html::escape($destination_id)]; if (isset($process_line[0]['source'])) { + if (is_array($process_line[0]['source'])) { + $process_line[0]['source'] = implode(', ', $process_line[0]['source']); + } $row[] = ['data' => Xss::filterAdmin($process_line[0]['source'])]; } else { $row[] = ''; } if (isset($process_line[0]['plugin'])) { - $row[] = ['data' => Xss::filterAdmin($process_line[0]['plugin'])]; + $process_line_plugins = []; + foreach ($process_line as $process_line_row) { + $process_line_plugins[] = Xss::filterAdmin($process_line_row['plugin']); + } + $row[] = ['data' => implode(', ', $process_line_plugins)]; } else { $row[] = ''; diff --git a/web/modules/migrate_tools/src/Controller/MigrationListBuilder.php b/web/modules/migrate_tools/src/Controller/MigrationListBuilder.php index d078764cea5cb26a22b91cd3becd0656968b9a55..a7f2edb515dc7798ead4aae4194011e05ff7d693 100644 --- a/web/modules/migrate_tools/src/Controller/MigrationListBuilder.php +++ b/web/modules/migrate_tools/src/Controller/MigrationListBuilder.php @@ -2,17 +2,16 @@ namespace Drupal\migrate_tools\Controller; -use Drupal\Component\Plugin\Exception\PluginException; use Drupal\Core\Config\Entity\ConfigEntityListBuilder; use Drupal\Core\Entity\EntityHandlerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Logger\LoggerChannelInterface; use Drupal\Core\Routing\CurrentRouteMatch; +use Drupal\Core\Url; use Drupal\migrate\Plugin\MigrationPluginManagerInterface; use Drupal\migrate_plus\Entity\MigrationGroup; -use Drupal\Core\Url; +use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -41,7 +40,7 @@ class MigrationListBuilder extends ConfigEntityListBuilder implements EntityHand /** * The logger service. * - * @var \Drupal\Core\Logger\LoggerChannelInterface + * @var \Psr\Log\LoggerInterface */ protected $logger; @@ -56,10 +55,10 @@ class MigrationListBuilder extends ConfigEntityListBuilder implements EntityHand * The current route match service. * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager * The plugin manager for config entity-based migrations. - * @param \Drupal\Core\Logger\LoggerChannelInterface $logger + * @param \Psr\Log\LoggerInterface $logger * The logger service. */ - public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, CurrentRouteMatch $current_route_match, MigrationPluginManagerInterface $migration_plugin_manager, LoggerChannelInterface $logger) { + public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, CurrentRouteMatch $current_route_match, MigrationPluginManagerInterface $migration_plugin_manager, LoggerInterface $logger) { parent::__construct($entity_type, $storage); $this->currentRouteMatch = $current_route_match; $this->migrationPluginManager = $migration_plugin_manager; @@ -72,7 +71,7 @@ public function __construct(EntityTypeInterface $entity_type, EntityStorageInter public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { return new static( $entity_type, - $container->get('entity.manager')->getStorage($entity_type->id()), + $container->get('entity_type.manager')->getStorage($entity_type->id()), $container->get('current_route_match'), $container->get('plugin.manager.migration'), $container->get('logger.channel.migrate_tools') @@ -142,7 +141,7 @@ public function buildRow(EntityInterface $migration_entity) { try { /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ $migration = $this->migrationPluginManager->createInstance($migration_entity->id()); - $migration_group = $migration->get('migration_group'); + $migration_group = $migration_entity->get('migration_group'); if (!$migration_group) { $migration_group = 'default'; } @@ -160,7 +159,7 @@ public function buildRow(EntityInterface $migration_entity) { $row['machine_name'] = $migration->id(); $row['status'] = $migration->getStatusLabel(); } - catch (PluginException $e) { + catch (\Exception $e) { $this->logger->warning('Migration entity id %id is malformed: %orig', ['%id' => $migration_entity->id(), '%orig' => $e->getMessage()]); return NULL; } @@ -211,20 +210,29 @@ public function buildRow(EntityInterface $migration_entity) { ], ]; } - catch (PluginException $e) { - // Derive the stats. - $row['status'] = $this->t('No data found'); - $row['total'] = $this->t('N/A'); - $row['imported'] = $this->t('N/A'); - $row['unprocessed'] = $this->t('N/A'); - $row['messages'] = $this->t('N/A'); - $row['last_imported'] = $this->t('N/A'); - $row['operations'] = $this->t('N/A'); + catch (\Throwable $throwable) { + $this->handleThrowable($row); } return $row; } + /** + * Derive the row data. + * + * @param array $row + * The table row. + */ + protected function handleThrowable(array &$row) { + $row['status'] = $this->t('No data found'); + $row['total'] = $this->t('N/A'); + $row['imported'] = $this->t('N/A'); + $row['unprocessed'] = $this->t('N/A'); + $row['messages'] = $this->t('N/A'); + $row['last_imported'] = $this->t('N/A'); + $row['operations'] = $this->t('N/A'); + } + /** * Add group route parameter. * diff --git a/web/modules/migrate_tools/src/DrushLogMigrateMessage.php b/web/modules/migrate_tools/src/DrushLogMigrateMessage.php index 4f7e92494c83de372613f8f8c0ce4b934c81e4b5..964a4d99302d5dae9e63d4b1cc671ffe87bc2089 100644 --- a/web/modules/migrate_tools/src/DrushLogMigrateMessage.php +++ b/web/modules/migrate_tools/src/DrushLogMigrateMessage.php @@ -2,6 +2,8 @@ namespace Drupal\migrate_tools; +use Drupal\Core\Logger\RfcLogLevel; +use Drupal\migrate\MigrateMessage; use Drupal\migrate\MigrateMessageInterface; /** @@ -9,7 +11,7 @@ * * @package Drupal\migrate_tools */ -class DrushLogMigrateMessage implements MigrateMessageInterface { +class DrushLogMigrateMessage extends MigrateMessage implements MigrateMessageInterface { /** * Output a message from the migration. @@ -22,7 +24,8 @@ class DrushLogMigrateMessage implements MigrateMessageInterface { * @see drush_log() */ public function display($message, $type = 'status') { - drush_log($message, $type); + $type = isset($this->map[$type]) ? $this->map[$type] : RfcLogLevel::NOTICE; + \Drupal::service(('logger.channel.migrate_tools'))->log($type, $message); } } diff --git a/web/modules/migrate_tools/src/EventSubscriber/MigrationDrushCommandProgress.php b/web/modules/migrate_tools/src/EventSubscriber/MigrationDrushCommandProgress.php new file mode 100644 index 0000000000000000000000000000000000000000..64c2fedb348fa8e2ac884cd4c63ab833aa5fbb47 --- /dev/null +++ b/web/modules/migrate_tools/src/EventSubscriber/MigrationDrushCommandProgress.php @@ -0,0 +1,121 @@ +<?php + +namespace Drupal\migrate_tools\EventSubscriber; + +use Drupal\migrate\Event\MigrateEvents; +use Drupal\migrate\Plugin\MigrationInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Import and rollback progress bar. + */ +class MigrationDrushCommandProgress implements EventSubscriberInterface { + + /** + * The logger service. + * + * @var \Psr\Log\LoggerInterface + */ + protected $logger; + + /** + * MigrationDrushCommandProgress constructor. + * + * @param \Psr\Log\LoggerInterface $logger + * The logger service. + */ + public function __construct(LoggerInterface $logger) { + $this->logger = $logger; + } + + /** + * The progress bar. + * + * @var \Symfony\Component\Console\Helper\ProgressBar + */ + protected $symfonyProgressBar; + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events = []; + $events[MigrateEvents::POST_ROW_SAVE][] = ['updateProgressBar', -10]; + $events[MigrateEvents::MAP_DELETE][] = ['updateProgressBar', -10]; + $events[MigrateEvents::POST_IMPORT][] = ['clearProgress', 10]; + $events[MigrateEvents::POST_ROLLBACK][] = ['clearProgress', 10]; + return $events; + } + + /** + * Initializes the progress bar. + * + * This must be called before the progress bar can be used. + * + * @param \Symfony\Component\Console\Output\OutputInterface $output + * The output. + * @param \Drupal\migrate\Plugin\MigrationInterface $migration + * The migration. + */ + public function initializeProgress(OutputInterface $output, MigrationInterface $migration) { + // Don't display progress bar if explicitly disabled. + if (!empty($migration->skipProgressBar)) { + return; + } + // If the source is configured to skip counts, a progress bar is not + // possible. + if (!empty($migration->getSourceConfiguration()['skip_count'])) { + return; + } + try { + // Clone so that any generators aren't initialized prematurely. + $source = clone $migration->getSourcePlugin(); + $this->symfonyProgressBar = new ProgressBar($output, $source->count()); + } + catch (\Exception $exception) { + if (!empty($migration->continueOnFailure)) { + $this->logger->error($exception->getMessage()); + } + else { + throw $exception; + } + } + } + + /** + * Event callback for advancing the progress bar. + */ + public function updateProgressBar() { + if ($this->isProgressBar()) { + $this->symfonyProgressBar->advance(); + } + } + + /** + * Event callback for removing the progress bar after operation is finished. + */ + public function clearProgress() { + if ($this->isProgressBar()) { + $this->symfonyProgressBar->clear(); + } + } + + /** + * Determine if a progress bar should be displayed. + * + * @return bool + * TRUE if a progress bar should be displayed, FALSE otherwise. + */ + protected function isProgressBar() { + // Can't do anything if the progress bar is not initialised; this probably + // means we're not running as a Drush command and so we should do nothing. + if (!$this->symfonyProgressBar) { + return FALSE; + } + return TRUE; + } + +} diff --git a/web/modules/migrate_tools/src/EventSubscriber/MigrationImportSync.php b/web/modules/migrate_tools/src/EventSubscriber/MigrationImportSync.php new file mode 100644 index 0000000000000000000000000000000000000000..afd5242e651ca1f23a5702f5b18c59cd94d166d0 --- /dev/null +++ b/web/modules/migrate_tools/src/EventSubscriber/MigrationImportSync.php @@ -0,0 +1,97 @@ +<?php + +namespace Drupal\migrate_tools\EventSubscriber; + +use Drupal\migrate\Event\MigrateEvents; +use Drupal\migrate\Event\MigrateImportEvent; +use Drupal\migrate\Event\MigrateRollbackEvent; +use Drupal\migrate\Event\MigrateRowDeleteEvent; +use Drupal\migrate\Plugin\MigrationInterface; +use Drupal\migrate_plus\Event\MigrateEvents as MigratePlusEvents; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Import and sync source and destination. + */ +class MigrationImportSync implements EventSubscriberInterface { + + /** + * The event dispatcher. + * + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface + */ + protected $dispatcher; + + /** + * MigrationImportSync constructor. + * + * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher + * The event dispatcher. + */ + public function __construct(EventDispatcherInterface $dispatcher) { + $this->dispatcher = $dispatcher; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events = []; + $events[MigrateEvents::PRE_IMPORT][] = ['sync']; + return $events; + } + + /** + * Event callback to sync source and destination. + * + * @param \Drupal\migrate\Event\MigrateImportEvent $event + * The migration import event. + */ + public function sync(MigrateImportEvent $event) { + $migration = $event->getMigration(); + if (!empty($migration->syncSource)) { + $id_map = $migration->getIdMap(); + $id_map->prepareUpdate(); + // Clone so that any generators aren't initialized prematurely. + $source = clone $migration->getSourcePlugin(); + $source->rewind(); + $source_id_values = []; + while ($source->valid()) { + $source_id_values[] = $source->current()->getSourceIdValues(); + $source->next(); + } + $id_map->rewind(); + $destination = $migration->getDestinationPlugin(); + while ($id_map->valid()) { + $map_source_id = $id_map->currentSource(); + if (!in_array($map_source_id, $source_id_values, TRUE)) { + $destination_ids = $id_map->currentDestination(); + $this->dispatchRowDeleteEvent(MigrateEvents::PRE_ROW_DELETE, $migration, $destination_ids); + $this->dispatchRowDeleteEvent(MigratePlusEvents::MISSING_SOURCE_ITEM, $migration, $destination_ids); + $destination->rollback($destination_ids); + $this->dispatchRowDeleteEvent(MigrateEvents::POST_ROW_DELETE, $migration, $destination_ids); + $id_map->delete($map_source_id); + } + $id_map->next(); + } + $this->dispatcher->dispatch(MigrateEvents::POST_ROLLBACK, new MigrateRollbackEvent($migration)); + } + } + + /** + * Dispatches MigrateRowDeleteEvent event. + * + * @param string $event_name + * The event name to dispatch. + * @param \Drupal\migrate\Plugin\MigrationInterface $migration + * The active migration. + * @param array $destination_ids + * The destination identifier values of the record. + */ + protected function dispatchRowDeleteEvent($event_name, MigrationInterface $migration, array $destination_ids) { + // Symfony changing dispatcher so implementation could change. + $this->dispatcher->dispatch($event_name, new MigrateRowDeleteEvent($migration, $destination_ids)); + } + +} diff --git a/web/modules/migrate_tools/src/Form/MigrationDeleteForm.php b/web/modules/migrate_tools/src/Form/MigrationDeleteForm.php index 593046dbaeae4b28e8d040b6294fc963c16e9edb..45cb56df126f8ace4a0241abad4d7f11d503a8bb 100644 --- a/web/modules/migrate_tools/src/Form/MigrationDeleteForm.php +++ b/web/modules/migrate_tools/src/Form/MigrationDeleteForm.php @@ -3,8 +3,8 @@ namespace Drupal\migrate_tools\Form; use Drupal\Core\Entity\EntityConfirmFormBase; -use Drupal\Core\Url; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; /** * Provides the delete form for our Migration entity. @@ -60,7 +60,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $this->entity->delete(); // Set a message that the entity was deleted. - drupal_set_message(t('Migration %label was deleted.', [ + $this->messenger()->addStatus($this->t('Migration %label was deleted.', [ '%label' => $this->entity->label(), ])); diff --git a/web/modules/migrate_tools/src/Form/MigrationEditForm.php b/web/modules/migrate_tools/src/Form/MigrationEditForm.php index 214d0ae35d2e8150a3e1ce4d9743ba16112bdf15..15740c60bd1649524d7a921ac2dcc558d3a2cd53 100644 --- a/web/modules/migrate_tools/src/Form/MigrationEditForm.php +++ b/web/modules/migrate_tools/src/Form/MigrationEditForm.php @@ -29,7 +29,7 @@ class MigrationEditForm extends MigrationFormBase { */ public function actions(array $form, FormStateInterface $form_state) { $actions = parent::actions($form, $form_state); - $actions['submit']['#value'] = t('Update Migration'); + $actions['submit']['#value'] = $this->t('Update Migration'); return $actions; } diff --git a/web/modules/migrate_tools/src/Form/MigrationExecuteForm.php b/web/modules/migrate_tools/src/Form/MigrationExecuteForm.php index d05b736ebb71b5833bdc6c9f4151e2af1e3227b9..150f7d1823519b669ce77f6f02ad447a680c85fd 100644 --- a/web/modules/migrate_tools/src/Form/MigrationExecuteForm.php +++ b/web/modules/migrate_tools/src/Form/MigrationExecuteForm.php @@ -2,8 +2,9 @@ namespace Drupal\migrate_tools\Form; -use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Routing\RouteMatchInterface; use Drupal\migrate\MigrateMessage; use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Plugin\MigrationPluginManagerInterface; @@ -27,9 +28,12 @@ class MigrationExecuteForm extends FormBase { * * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager * The plugin manager for config entity-based migrations. + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * The current route match. */ - public function __construct(MigrationPluginManagerInterface $migration_plugin_manager) { + public function __construct(MigrationPluginManagerInterface $migration_plugin_manager, RouteMatchInterface $route_match) { $this->migrationPluginManager = $migration_plugin_manager; + $this->routeMatch = $route_match; } /** @@ -37,7 +41,8 @@ public function __construct(MigrationPluginManagerInterface $migration_plugin_ma */ public static function create(ContainerInterface $container) { return new static( - $container->get('plugin.manager.migration') + $container->get('plugin.manager.migration'), + $container->get('current_route_match') ); } @@ -52,85 +57,105 @@ public function getFormId() { * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { + $form = $form ?: []; - $form = []; + /** @var \Drupal\migrate_plus\Entity\MigrationInterface $migration */ + $migration = $this->getRouteMatch()->getParameter('migration'); + $form['#title'] = $this->t('Execute migration %label', ['%label' => $migration->label()]); - $form['operations'] = $this->migrateMigrateOperations(); + $form = $this->buildFormOperations($form, $form_state); + $form = $this->buildFormOptions($form, $form_state); + + $form['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Execute'), + ]; return $form; } /** - * Get Operations. + * Build the operation form field. + * + * @param array $form + * The execution form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return array + * The execution form updated with the operations. */ - private function migrateMigrateOperations() { - // Build the 'Update options' form. - $form = [ - '#type' => 'fieldset', - '#title' => t('Operations'), - ]; + protected function buildFormOperations(array $form, FormStateInterface $form_state) { + // Build the migration execution form. $options = [ - 'import' => t('Import'), - 'rollback' => t('Rollback'), - 'stop' => t('Stop'), - 'reset' => t('Reset'), + 'import' => $this->t('Import'), + 'rollback' => $this->t('Rollback'), + 'stop' => $this->t('Stop'), + 'reset' => $this->t('Reset'), ]; + $form['operation'] = [ - '#type' => 'select', - '#title' => t('Choose an operation to run'), + '#type' => 'radios', + '#title' => $this->t('Operation'), + '#description' => $this->t('Choose an operation to run.'), '#options' => $options, '#default_value' => 'import', '#required' => TRUE, + 'import' => [ + '#description' => $this->t('Imports all previously unprocessed records from the source, plus any records marked for update, into destination Drupal objects.'), + ], + 'rollback' => [ + '#description' => $this->t('Deletes all Drupal objects created by the import.'), + ], + 'stop' => [ + '#description' => $this->t('Cleanly interrupts any import or rollback processes that may currently be running.'), + ], + 'reset' => [ + '#description' => $this->t('Sometimes a process may fail to stop cleanly, and be left stuck in an Importing or Rolling Back status. Choose Reset to clear the status and permit other operations to proceed.'), + ], ]; - $form['submit'] = [ - '#type' => 'submit', - '#value' => t('Execute'), - ]; - $definitions = []; - $definitions[] = $this->t('Import: Imports all previously unprocessed records from the source, plus any records marked for update, into destination Drupal objects.'); - $definitions[] = $this->t('Rollback: Deletes all Drupal objects created by the import.'); - $definitions[] = $this->t('Stop: Cleanly interrupts any import or rollback processes that may currently be running.'); - $definitions[] = $this->t('Reset: Sometimes a process may fail to stop cleanly, and be left stuck in an Importing or Rolling Back status. Choose Reset to clear the status and permit other operations to proceed.'); - $form['definitions'] = [ - '#theme' => 'item_list', - '#title' => $this->t('Definitions'), - '#list_type' => 'ul', - '#items' => $definitions, - ]; + return $form; + } + + /** + * Build the execution options form field. + * + * @param array $form + * The execution form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return array + * The execution form updated with the execution options. + */ + protected function buildFormOptions(array $form, FormStateInterface $form_state) { $form['options'] = [ - '#type' => 'fieldset', - '#title' => t('Options'), - '#collapsible' => TRUE, - '#collapsed' => TRUE, + '#type' => 'details', + '#title' => $this->t('Additional execution options'), + '#open' => FALSE, ]; + $form['options']['update'] = [ '#type' => 'checkbox', - '#title' => t('Update'), - '#description' => t('Check this box to update all previously-imported content - in addition to importing new content. Leave unchecked to only import - new content'), + '#title' => $this->t('Update'), + '#description' => $this->t('Check this box to update all previously-imported content in addition to importing new content. Leave unchecked to only import new content'), ]; + $form['options']['force'] = [ '#type' => 'checkbox', - '#title' => t('Ignore dependencies'), - '#description' => t('Check this box to ignore dependencies when running imports - - all tasks will run whether or not their dependent tasks have - completed.'), + '#title' => $this->t('Ignore dependencies'), + '#description' => $this->t('Check this box to ignore dependencies when running imports - all tasks will run whether or not their dependent tasks have completed.'), ]; - // @TODO: Limit is not working. Perhaps because of batch? See - // https://www.drupal.org/project/migrate_tools/issues/2924298. - return $form; - } - /** - * {@inheritdoc} - */ - public function validateForm(array &$form, FormStateInterface $form_state) { - if (empty($form_state->getValue('operation'))) { - $form_state->setErrorByName('operation', $this->t('Please select an operation.')); - return; - } + $form['options']['limit'] = [ + '#type' => 'number', + '#title' => $this->t('Limit to:'), + '#size' => 10, + '#description' => $this->t('Set a limit of how many items to process for each migration task.'), + ]; + + return $form; } /** @@ -160,7 +185,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $force = 0; } - $migration = \Drupal::routeMatch()->getParameter('migration'); + $migration = $this->getRouteMatch()->getParameter('migration'); if ($migration) { /** @var \Drupal\migrate\Plugin\MigrationInterface $migration_plugin */ $migration_plugin = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); diff --git a/web/modules/migrate_tools/src/Form/MigrationFormBase.php b/web/modules/migrate_tools/src/Form/MigrationFormBase.php index 3ba6a0f55fd14ccb8044a4da8fa451a8237fa728..fc8f0eb7c0ff69c4899c89a0ef3a4e13f9eec809 100644 --- a/web/modules/migrate_tools/src/Form/MigrationFormBase.php +++ b/web/modules/migrate_tools/src/Form/MigrationFormBase.php @@ -3,10 +3,8 @@ namespace Drupal\migrate_tools\Form; use Drupal\Core\Entity\EntityForm; -use Drupal\Core\Entity\Query\QueryFactory; use Drupal\Core\Form\FormStateInterface; use Drupal\migrate_plus\Entity\MigrationGroup; -use Symfony\Component\DependencyInjection\ContainerInterface; /** * Class MigrationFormBase. @@ -17,35 +15,6 @@ */ class MigrationFormBase extends EntityForm { - /** - * The entity query factory. - * - * @var \Drupal\Core\Entity\Query\QueryFactory - */ - protected $entityQueryFactory; - - /** - * Construct the MigrationGroupFormBase. - * - * For simple entity forms, there's no need for a constructor. Our migration - * form base, however, requires an entity query factory to be injected into it - * from the container. We later use this query factory to build an entity - * query for the exists() method. - * - * @param \Drupal\Core\Entity\Query\QueryFactory $query_factory - * An entity query factory for the migration group entity type. - */ - public function __construct(QueryFactory $query_factory) { - $this->entityQueryFactory = $query_factory; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static($container->get('entity.query')); - } - /** * Overrides Drupal\Core\Entity\EntityFormController::form(). * @@ -97,7 +66,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { foreach ($groups as $group) { $group_options[$group->id()] = $group->label(); } - if (!$migration->get('migration_group') && isset($group_options['default'])) { + if (!$migration->migration_group && isset($group_options['default'])) { $migration->set('migration_group', 'default'); } @@ -105,7 +74,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#type' => 'select', '#title' => $this->t('Migration Group'), '#empty_value' => '', - '#default_value' => $migration->get('migration_group'), + '#default_value' => $migration->migration_group, '#options' => $group_options, '#description' => $this->t('Assign this migration to an existing group.'), ]; @@ -127,8 +96,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { * TRUE if this format already exists, FALSE otherwise. */ public function exists($entity_id, array $element, FormStateInterface $form_state) { - // Use the query factory to build a new migration entity query. - $query = $this->entityQueryFactory->get('migration'); + $query = $this->entityTypeManager->getStorage('migration')->getQuery(); // Query the entity ID to see if its in use. $result = $query->condition('id', $element['#field_prefix'] . $entity_id) @@ -150,7 +118,7 @@ public function exists($entity_id, array $element, FormStateInterface $form_stat * An array of supported actions for the current entity form. */ protected function actions(array $form, FormStateInterface $form_state) { - // Get the basic actins from the base class. + // Get the basic actions from the base class. $actions = parent::actions($form, $form_state); // Change the submit button text. @@ -169,11 +137,11 @@ public function save(array $form, FormStateInterface $form_state) { if ($status == SAVED_UPDATED) { // If we edited an existing entity... - drupal_set_message($this->t('Migration %label has been updated.', ['%label' => $migration->label()])); + $this->messenger()->addStatus($this->t('Migration %label has been updated.', ['%label' => $migration->label()])); } else { // If we created a new entity... - drupal_set_message($this->t('Migration %label has been added.', ['%label' => $migration->label()])); + $this->messenger()->addStatus($this->t('Migration %label has been added.', ['%label' => $migration->label()])); } // Redirect the user back to the listing route after the save operation. diff --git a/web/modules/migrate_tools/src/Form/MigrationGroupDeleteForm.php b/web/modules/migrate_tools/src/Form/MigrationGroupDeleteForm.php index 98d1baf700efc203f3079dde5ce7e23c6feba6df..e50911bf5edf417d2e62b38b1e69de438210089e 100644 --- a/web/modules/migrate_tools/src/Form/MigrationGroupDeleteForm.php +++ b/web/modules/migrate_tools/src/Form/MigrationGroupDeleteForm.php @@ -3,8 +3,8 @@ namespace Drupal\migrate_tools\Form; use Drupal\Core\Entity\EntityConfirmFormBase; -use Drupal\Core\Url; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; /** * Provides the delete form for our Migration Group entity. @@ -60,7 +60,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $this->entity->delete(); // Set a message that the entity was deleted. - drupal_set_message(t('Migration group %label was deleted.', [ + $this->messenger()->addStatus($this->t('Migration group %label was deleted.', [ '%label' => $this->entity->label(), ])); diff --git a/web/modules/migrate_tools/src/Form/MigrationGroupEditForm.php b/web/modules/migrate_tools/src/Form/MigrationGroupEditForm.php index 1eedc2d261d362be898bfca83f7b1e3b43366d84..551950a018dd6e74fa6872e81ae935491cc58079 100644 --- a/web/modules/migrate_tools/src/Form/MigrationGroupEditForm.php +++ b/web/modules/migrate_tools/src/Form/MigrationGroupEditForm.php @@ -28,7 +28,7 @@ class MigrationGroupEditForm extends MigrationGroupFormBase { */ public function actions(array $form, FormStateInterface $form_state) { $actions = parent::actions($form, $form_state); - $actions['submit']['#value'] = t('Update Migration Group'); + $actions['submit']['#value'] = $this->t('Update Migration Group'); return $actions; } diff --git a/web/modules/migrate_tools/src/Form/MigrationGroupFormBase.php b/web/modules/migrate_tools/src/Form/MigrationGroupFormBase.php index d60944a759fd9c55c4fb79957851437c4fce7e7d..91d484679918dda17913ddb6bbfdfddf4da9fb07 100644 --- a/web/modules/migrate_tools/src/Form/MigrationGroupFormBase.php +++ b/web/modules/migrate_tools/src/Form/MigrationGroupFormBase.php @@ -3,9 +3,7 @@ namespace Drupal\migrate_tools\Form; use Drupal\Core\Entity\EntityForm; -use Drupal\Core\Entity\Query\QueryFactory; use Drupal\Core\Form\FormStateInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; /** * Class MigrationGroupFormBase. @@ -16,35 +14,6 @@ */ class MigrationGroupFormBase extends EntityForm { - /** - * The query factory service. - * - * @var \Drupal\Core\Entity\Query\QueryFactory - */ - protected $entityQueryFactory; - - /** - * Construct the MigrationGroupFormBase. - * - * For simple entity forms, there's no need for a constructor. Our migration - * group form base, however, requires an entity query factory to be injected - * into it from the container. We later use this query factory to build an - * entity query for the exists() method. - * - * @param \Drupal\Core\Entity\Query\QueryFactory $query_factory - * An entity query factory for the migration group entity type. - */ - public function __construct(QueryFactory $query_factory) { - $this->entityQueryFactory = $query_factory; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static($container->get('entity.query')); - } - /** * Overrides Drupal\Core\Entity\EntityFormController::form(). * @@ -116,8 +85,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { * TRUE if this format already exists, FALSE otherwise. */ public function exists($entity_id, array $element, FormStateInterface $form_state) { - // Use the query factory to build a new migration group entity query. - $query = $this->entityQueryFactory->get('migration_group'); + $query = $this->entityTypeManager->getStorage('migration_group')->getQuery(); // Query the entity ID to see if its in use. $result = $query->condition('id', $element['#field_prefix'] . $entity_id) @@ -158,11 +126,11 @@ public function save(array $form, FormStateInterface $form_state) { if ($status == SAVED_UPDATED) { // If we edited an existing entity... - drupal_set_message($this->t('Migration group %label has been updated.', ['%label' => $migration_group->label()])); + $this->messenger()->addStatus($this->t('Migration group %label has been updated.', ['%label' => $migration_group->label()])); } else { // If we created a new entity... - drupal_set_message($this->t('Migration group %label has been added.', ['%label' => $migration_group->label()])); + $this->messenger()->addStatus($this->t('Migration group %label has been added.', ['%label' => $migration_group->label()])); } // Redirect the user back to the listing route after the save operation. diff --git a/web/modules/migrate_tools/src/Form/SourceCsvForm.php b/web/modules/migrate_tools/src/Form/SourceCsvForm.php deleted file mode 100644 index 40b5dc3bac4cac7bae0c814e5b75da517582e03d..0000000000000000000000000000000000000000 --- a/web/modules/migrate_tools/src/Form/SourceCsvForm.php +++ /dev/null @@ -1,453 +0,0 @@ -<?php - -namespace Drupal\migrate_tools\Form; - -use Drupal\Component\Plugin\Exception\PluginException; -use Drupal\Core\Access\AccessResult; -use Drupal\Core\Session\AccountInterface; -use Drupal\Core\Database\Connection; -use Drupal\Core\Form\FormBase; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Messenger\MessengerInterface; -use Drupal\Core\TempStore\PrivateTempStoreFactory; -use Drupal\Core\TempStore\TempStoreException; -use Drupal\Core\Url; -use Drupal\migrate_plus\Entity\MigrationInterface; -use Drupal\migrate_source_csv\Plugin\migrate\source\CSV; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\migrate\Plugin\MigrationPluginManagerInterface; - -/** - * Provides an edit form for CSV source plugin column_names configuration. - * - * This means you can tell the migration which columns your data is in and no - * longer edit the CSV to fit the column order set in the migration or edit the - * migration yml itself. - * - * Changes made to the column configuration, or aliases, are stored in the - * private migrate_toools private store keyed by the migration plugin id. The - * data stored for each migrations consists of two arrays, the 'original' column - * aliases and the 'updated' column aliases. - * - * An addtional list of all changed migration id is kept in the store, in the - * key 'migrations_changed' - * - * Private Store Usage: - * migrations_changed: An array of the ids of the migrations that have been - * changed: - * [migration_id]: The original and changed values for this column assignments - * - * Format of the source configuration saved in the store. - * @code - * migration_id - * original - * column_index1 - * property 1 => label 1 - * column_index2 - * property 2 => label 2 - * updated - * column_index1 - * property 2 => label 2 - * column_index2 - * property 1 => label 1 - * @endcode - * - * Example source configuration. - * @code - * custom_migration - * original - * 2 - * title => title - * 3 - * body => foo - * updated - * 8 - * title => new_title - * 9 - * body => new_body - * @endcode - */ -class SourceCsvForm extends FormBase { - - /** - * The database connection. - * - * @var \Drupal\Core\Database\Connection - */ - protected $connection; - - /** - * Plugin manager for migration plugins. - * - * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface - */ - protected $migrationPluginManager; - - /** - * The messenger service. - * - * @var \Drupal\Core\Messenger\MessengerInterface - */ - protected $messenger; - - /** - * Temporary store for column assignment changes. - * - * @var \Drupal\Core\TempStore\PrivateTempStoreFactory - */ - protected $store; - - /** - * The file object that reads the CSV file. - * - * @var \SplFileObject - */ - protected $file = NULL; - - /** - * The migration being examined. - * - * @var \Drupal\migrate\Plugin\MigrationInterface - */ - protected $migration; - - /** - * The migration plugin id. - * - * @var string - */ - protected $id; - - /** - * The array of columns names from the CSV source plugin. - * - * @var array - */ - protected $columnNames; - - /** - * An array of options for the column select form field.. - * - * @var array - */ - protected $options; - - /** - * An array of modified and original column_name source plugin configuration. - * - * @var array - */ - protected $sourceConfiguration; - - /** - * Constructs new SourceCsvForm object. - * - * @param \Drupal\Core\Database\Connection $connection - * The database connection. - * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager - * The plugin manager for config entity-based migrations. - * @param \Drupal\Core\Messenger\MessengerInterface $messenger - * The messenger service. - * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $private_store - * The private store. - */ - public function __construct(Connection $connection, MigrationPluginManagerInterface $migration_plugin_manager, MessengerInterface $messenger, PrivateTempStoreFactory $private_store) { - $this->connection = $connection; - $this->migrationPluginManager = $migration_plugin_manager; - $this->messenger = $messenger; - $this->store = $private_store->get('migrate_tools'); - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('database'), - $container->get('plugin.manager.migration'), - $container->get('messenger'), - $container->get('tempstore.private') - ); - } - - /** - * A custom access check. - * - * @param \Drupal\Core\Session\AccountInterface $account - * Run access checks for this account. - * @param \Drupal\migrate_plus\Entity\MigrationInterface $migration - * The $migration. - * - * @return \Drupal\Core\Access\AccessResult - * Allowed or forbidden, neutral if tempstore is empty. - */ - public function access(AccountInterface $account, MigrationInterface $migration) { - try { - $this->migration = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); - } - catch (PluginException $e) { - return AccessResult::forbidden(); - } - - if ($this->migration) { - if ($source = $this->migration->getSourcePlugin()) { - if (is_a($source, CSV::class)) { - return AccessResult::allowed(); - } - } - } - return AccessResult::forbidden(); - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state, MigrationInterface $migration = NULL) { - try { - // @TODO: remove this horrible config work around after - // https://www.drupal.org/project/drupal/issues/2986665 is fixed. - $this->migration = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); - /** @var \Drupal\migrate_source_csv\Plugin\migrate\source\CSV $source */ - $source = $this->migration->getSourcePlugin(); - $source->setConfiguration($migration->toArray()['source']); - } - catch (PluginException $e) { - return AccessResult::forbidden(); - } - - // Get the source file after the properties are initialized. - $source->initializeIterator(); - $this->file = $source->getFile(); - - // Set the input field options to the header row values or, if there are - // no such values, use an indexed array. - if ($this->file->getHeaderRowCount() > 0) { - $this->options = $this->getHeaderColumnNames(); - } - else { - for ($i = 0; $i < $this->getFileColumnCount(); $i++) { - $this->options[$i] = $i; - } - } - - // Set the store key to the migration id. - $this->id = $this->migration->getPluginId(); - - // Get the column names from the file or from the store, if updated - // values are in the store. - $this->sourceConfiguration = $this->store->get($this->id); - if (isset($this->sourceConfiguration['changed'])) { - if ($config = $this->sourceConfiguration['changed']) { - $this->columnNames = $config; - } - } - else { - // Get the calculated column names. This is either the header rows or - // the configuration column_name value. - $this->columnNames = $this->file->getColumnNames(); - if (!isset($this->sourceConfiguration['original'])) { - // Save as the original values. - $this->sourceConfiguration['original'] = $this->columnNames; - $this->store->set($this->id, $this->sourceConfiguration); - } - } - $form['#title'] = $this->t('Column Aliases'); - - $form['heading'] = [ - '#type' => 'item', - '#title' => $this->t(':label', [':label' => $this->migration->label()]), - '#description' => '<p>' . $this->t('You can change the columns to be used by this migration for each source property.') . '</p>', - ]; - // Create a form field for each column in this migration. - foreach ($this->columnNames as $index => $data) { - $property_name = key($data); - $default_value = $index; - $label = $this->getLabel($this->sourceConfiguration['original'], $property_name); - - $description = $this->t('Select the column where the data for <em>:label</em>, property <em>:property</em>, will be found.', [ - ':label' => $label, - ':property' => $property_name, - ]); - $form['aliases'][$property_name] = [ - '#type' => 'select', - '#title' => $label, - '#description' => $description, - '#options' => $this->options, - '#default_value' => $default_value, - ]; - } - $form['actions'] = ['#type' => 'actions']; - $form['actions']['submit'] = [ - '#type' => 'submit', - '#button_type' => 'primary', - '#value' => $this->t('Submit'), - ]; - $form['actions']['cancel'] = [ - '#type' => 'submit', - '#value' => $this->t('Cancel'), - '#submit' => ['::cancel'], - '#limit_validation_errors' => [], - ]; - - return $form; - } - - /** - * {@inheritdoc} - */ - public function validateForm(array &$form, FormStateInterface $form_state) { - // Display an error message if two properties have the same source column. - $values = []; - foreach ($this->columnNames as $index => $data) { - $property_name = key($data); - $value = $form_state->getValue($property_name); - if (in_array($value, $values)) { - $form_state->setErrorByName($property_name, $this->t('Source properties can not share the same source column.')); - } - $values[] = $value; - } - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - // Create a new column_names configuration. - $new_column_names = []; - foreach ($this->columnNames as $index => $data) { - // Keep the property name as it is used in the process pipeline. - $property_name = key($data); - // Get the new column number from the form alias field for this property. - $new_index = $form_state->getValue($property_name); - // Get the new label from the options array. - $new_label = $this->options[$new_index]; - // Save using the new column number and new label. - $new_column_names[$new_index] = [$property_name => $new_label]; - } - // Update the file columns. - $this->file->setColumnNames($new_column_names); - // Save as updated in the store. - $this->sourceConfiguration['changed'] = $new_column_names; - $this->store->set($this->id, $this->sourceConfiguration); - - $changed = ($this->store->get('migrations_changed')) ? $this->store->get('migrations_changed') : []; - if (!in_array($this->id, $changed)) { - $changed[] = $this->id; - $this->store->set('migrations_changed', $changed); - } - } - - /** - * Form submission handler for the 'cancel' action. - * - * @param array $form - * An associative array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - */ - public function cancel(array $form, FormStateInterface $form_state) { - // Restore the file columns to the original settings. - $this->file->setColumnNames($this->sourceConfiguration['original']); - // Remove this migration from the store. - try { - $this->store->delete($this->id); - } - catch (TempStoreException $e) { - $this->messenger->addError($e->getMessage()); - } - - $migrationsChanged = $this->store->get('migrations_changed'); - unset($migrationsChanged[$this->id]); - try { - $this->store->set('migrations_changed', $migrationsChanged); - } - catch (TempStoreException $e) { - $this->messenger->addError($e->getMessage()); - } - $form_state->setRedirect('entity.migration_group.list'); - } - - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'migrate_tools_source_csv'; - } - - /** - * {@inheritdoc} - */ - public function getQuestion() {} - - /** - * {@inheritdoc} - */ - public function getCancelUrl() { - return new Url('entity.migration_group.list'); - } - - /** - * Returns the header row. - * - * Use a new file handle so that CSVFileObject::current() is not executed. - * - * @return array - * The header row. - */ - public function getHeaderColumnNames() { - $row = []; - $fname = $this->file->getPathname(); - $handle = fopen($fname, 'r'); - if ($handle) { - fseek($handle, $this->file->getHeaderRowCount() - 1); - $row = fgetcsv($handle); - fclose($handle); - } - return $row; - } - - /** - * Returns the count of fields in the header row. - * - * Use a new file handle so that CSVFileObject::current() is not executed. - * - * @return int - * The number of fields in the header row. - */ - public function getFileColumnCount() { - $count = 0; - $fname = $this->file->getPathname(); - $handle = fopen($fname, 'r'); - if ($handle) { - $row = fgetcsv($handle); - $count = count($row); - fclose($handle); - } - return $count; - } - - /** - * Gets the label for a given property from a column_names array. - * - * @param array $column_names - * An array of column_names. - * @param string $property_name - * The property name to find a label for. - * - * @return string - * The label for this property. - */ - protected function getLabel(array $column_names, $property_name) { - $label = ''; - foreach ($column_names as $column) { - foreach ($column as $key => $value) { - if ($key === $property_name) { - $label = $value; - break; - } - } - } - return $label; - } - -} diff --git a/web/modules/migrate_tools/src/IdMapFilter.php b/web/modules/migrate_tools/src/IdMapFilter.php new file mode 100644 index 0000000000000000000000000000000000000000..46518d206dbb3a4b292c8c6bb15f2c1cbc5ca0a7 --- /dev/null +++ b/web/modules/migrate_tools/src/IdMapFilter.php @@ -0,0 +1,42 @@ +<?php + +namespace Drupal\migrate_tools; + +use Drupal\migrate\Plugin\MigrateIdMapInterface; + +/** + * Class to filter ID map by an ID list. + */ +class IdMapFilter extends \FilterIterator { + + /** + * List of specific source IDs to import. + * + * @var array + */ + protected $idList; + + /** + * IdMapFilter constructor. + * + * @param \Drupal\migrate\Plugin\MigrateIdMapInterface $id_map + * The ID map. + * @param array $id_list + * The id list to use in the filter. + */ + public function __construct(MigrateIdMapInterface $id_map, array $id_list) { + parent::__construct($id_map); + $this->idList = $id_list; + } + + /** + * {@inheritdoc} + */ + public function accept() { + // Row is included. + if (empty($this->idList) || in_array(array_values($this->getInnerIterator()->currentSource()), $this->idList)) { + return TRUE; + } + } + +} diff --git a/web/modules/migrate_tools/src/MigrateBatchExecutable.php b/web/modules/migrate_tools/src/MigrateBatchExecutable.php index ee24f64d77352b5b4dd2bb77c8b1470c533aeb93..03e1b40741c527f2e186e0deb0d003975bba91a6 100644 --- a/web/modules/migrate_tools/src/MigrateBatchExecutable.php +++ b/web/modules/migrate_tools/src/MigrateBatchExecutable.php @@ -4,7 +4,6 @@ use Drupal\migrate\MigrateMessage; use Drupal\migrate\MigrateMessageInterface; -use Drupal\migrate\Plugin\Migration; use Drupal\migrate\Plugin\MigrationInterface; /** @@ -65,10 +64,10 @@ public function __construct(MigrationInterface $migration, MigrateMessageInterfa /** * Sets the current batch content so listeners can update the messages. * - * @param array $context + * @param array|\DrushBatchContext $context * The batch context. */ - public function setBatchContext(array &$context) { + public function setBatchContext(&$context) { $this->batchContext = &$context; } @@ -98,10 +97,10 @@ public function batchImport() { if (count($operations) > 0) { $batch = [ 'operations' => $operations, - 'title' => t('Migrating %migrate', ['%migrate' => $this->migration->label()]), - 'init_message' => t('Start migrating %migrate', ['%migrate' => $this->migration->label()]), - 'progress_message' => t('Migrating %migrate', ['%migrate' => $this->migration->label()]), - 'error_message' => t('An error occurred while migrating %migrate.', ['%migrate' => $this->migration->label()]), + 'title' => $this->t('Migrating %migrate', ['%migrate' => $this->migration->label()]), + 'init_message' => $this->t('Start migrating %migrate', ['%migrate' => $this->migration->label()]), + 'progress_message' => $this->t('Migrating %migrate', ['%migrate' => $this->migration->label()]), + 'error_message' => $this->t('An error occurred while migrating %migrate.', ['%migrate' => $this->migration->label()]), 'finished' => '\Drupal\migrate_tools\MigrateBatchExecutable::batchFinishedImport', ]; @@ -138,13 +137,11 @@ protected function batchOperations(array $migrations, $operation, array $options if (!empty($dependencies['required'])) { $required_migrations = $this->migrationPluginManager->createInstances($dependencies['required']); // For dependent migrations will need to be migrate all items. - $dependent_options = $options; - $dependent_options['limit'] = 0; - $operations += $this->batchOperations($required_migrations, $operation, [ + $operations = array_merge($operations, $this->batchOperations($required_migrations, $operation, [ 'limit' => 0, 'update' => $options['update'], 'force' => $options['force'], - ]); + ])); } } @@ -164,10 +161,10 @@ protected function batchOperations(array $migrations, $operation, array $options * The migration id. * @param array $options * The batch executable options. - * @param array $context + * @param array|\DrushBatchContext $context * The sandbox context. */ - public static function batchProcessImport($migration_id, array $options, array &$context) { + public static function batchProcessImport($migration_id, array $options, &$context) { if (empty($context['sandbox'])) { $context['finished'] = 0; $context['sandbox'] = []; @@ -181,6 +178,12 @@ public static function batchProcessImport($migration_id, array $options, array & $message = new MigrateMessage(); /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ $migration = \Drupal::getContainer()->get('plugin.manager.migration')->createInstance($migration_id); + + // Each batch run we need to reinitialize the counter for the migration. + if (!empty($options['limit']) && isset($context['results'][$migration->id()]['@numitems'])) { + $options['limit'] = $options['limit'] - $context['results'][$migration->id()]['@numitems']; + } + $executable = new MigrateBatchExecutable($migration, $message, $options); if (empty($context['sandbox']['total'])) { @@ -249,7 +252,7 @@ public static function batchFinishedImport($success, array $results, array $oper foreach ($results as $migration_id => $result) { $singular_message = "Processed 1 item (@created created, @updated updated, @failures failed, @ignored ignored) - done with '@name'"; $plural_message = "Processed @numitems items (@created created, @updated updated, @failures failed, @ignored ignored) - done with '@name'"; - drupal_set_message(\Drupal::translation()->formatPlural($result['@numitems'], + \Drupal::messenger()->addStatus(\Drupal::translation()->formatPlural($result['@numitems'], $singular_message, $plural_message, $result)); @@ -281,13 +284,13 @@ public function checkStatus() { /** * Calculates how much a single batch iteration will handle. * - * @param array $context + * @param array|\DrushBatchContext $context * The sandbox context. * * @return float * The batch limit. */ - public function calculateBatchLimit(array $context) { + public function calculateBatchLimit($context) { // TODO Maybe we need some other more sophisticated logic here? return ceil($context['sandbox']['total'] / 100); } diff --git a/web/modules/migrate_tools/src/MigrateExecutable.php b/web/modules/migrate_tools/src/MigrateExecutable.php index 2282ab8cbcb0839be8d437426567805917d3c2d5..2cb067a45652567ecabd87d6e42f22e92c96caa3 100644 --- a/web/modules/migrate_tools/src/MigrateExecutable.php +++ b/web/modules/migrate_tools/src/MigrateExecutable.php @@ -2,19 +2,18 @@ namespace Drupal\migrate_tools; +use Drupal\migrate\Event\MigrateEvents; +use Drupal\migrate\Event\MigrateImportEvent; +use Drupal\migrate\Event\MigrateMapDeleteEvent; +use Drupal\migrate\Event\MigrateMapSaveEvent; use Drupal\migrate\Event\MigratePreRowSaveEvent; use Drupal\migrate\Event\MigrateRollbackEvent; use Drupal\migrate\Event\MigrateRowDeleteEvent; use Drupal\migrate\MigrateExecutable as MigrateExecutableBase; use Drupal\migrate\MigrateMessageInterface; -use Drupal\migrate\Plugin\MigrationInterface; -use Drupal\migrate\MigrateSkipRowException; use Drupal\migrate\Plugin\MigrateIdMapInterface; -use Drupal\migrate\Event\MigrateEvents; +use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate_plus\Event\MigrateEvents as MigratePlusEvents; -use Drupal\migrate\Event\MigrateMapSaveEvent; -use Drupal\migrate\Event\MigrateMapDeleteEvent; -use Drupal\migrate\Event\MigrateImportEvent; use Drupal\migrate_plus\Event\MigratePrepareRowEvent; /** @@ -104,14 +103,7 @@ public function __construct(MigrationInterface $migration, MigrateMessageInterfa if (isset($options['feedback'])) { $this->feedback = $options['feedback']; } - if (isset($options['idlist'])) { - if (is_string($options['idlist'])) { - $this->idlist = explode(',', $options['idlist']); - array_walk($this->idlist, function (&$value, $key) { - $value = explode(':', $value); - }); - } - } + $this->idlist = MigrateTools::buildIdList($options); $this->listeners[MigrateEvents::MAP_SAVE] = [$this, 'onMapSave']; $this->listeners[MigrateEvents::MAP_DELETE] = [$this, 'onMapDelete']; @@ -121,7 +113,7 @@ public function __construct(MigrationInterface $migration, MigrateMessageInterfa $this->listeners[MigrateEvents::POST_ROW_DELETE] = [$this, 'onPostRowDelete']; $this->listeners[MigratePlusEvents::PREPARE_ROW] = [$this, 'onPrepareRow']; foreach ($this->listeners as $event => $listener) { - \Drupal::service('event_dispatcher')->addListener($event, $listener); + $this->getEventDispatcher()->addListener($event, $listener); } } @@ -242,7 +234,7 @@ protected function resetCounters() { */ public function onPostImport(MigrateImportEvent $event) { $migrate_last_imported_store = \Drupal::keyValue('migrate_last_imported'); - $migrate_last_imported_store->set($event->getMigration()->id(), round(microtime(TRUE) * 1000)); + $migrate_last_imported_store->set($event->getMigration()->id(), round(\Drupal::time()->getCurrentMicroTime() * 1000)); $this->progressMessage(); $this->removeListeners(); } @@ -252,7 +244,7 @@ public function onPostImport(MigrateImportEvent $event) { */ protected function removeListeners() { foreach ($this->listeners as $event => $listener) { - \Drupal::service('event_dispatcher')->removeListener($event, $listener); + $this->getEventDispatcher()->removeListener($event, $listener); } } @@ -294,8 +286,14 @@ protected function progressMessage($done = TRUE) { * The map event. */ public function onPostRollback(MigrateRollbackEvent $event) { + $migrate_last_imported_store = \Drupal::keyValue('migrate_last_imported'); + $migrate_last_imported_store->set($event->getMigration()->id(), FALSE); $this->rollbackMessage(); - $this->removeListeners(); + // If this is a sync import, then don't remove listeners or post import will + // not be executed. Leave it to post import to remove listeners. + if (empty($event->getMigration()->syncSource)) { + $this->removeListeners(); + } } /** @@ -363,25 +361,7 @@ public function onPostRowDelete(MigrateRowDeleteEvent $event) { * @throws \Drupal\migrate\MigrateSkipRowException */ public function onPrepareRow(MigratePrepareRowEvent $event) { - if (!empty($this->idlist)) { - $row = $event->getRow(); - // TODO: replace for $source_id = $row->getSourceIdValues(); - // when https://www.drupal.org/node/2698023 is fixed. - $migration = $event->getMigration(); - $source_id = array_merge(array_flip(array_keys($migration->getSourcePlugin() - ->getIds())), $row->getSourceIdValues()); - $skip = TRUE; - foreach ($this->idlist as $item) { - if (array_values($source_id) == $item) { - $skip = FALSE; - break; - } - } - if ($skip) { - throw new MigrateSkipRowException(NULL, FALSE); - } - } - if ($this->feedback && ($this->counter) && $this->counter % $this->feedback == 0) { + if ($this->feedback && $this->counter && $this->counter % $this->feedback == 0) { $this->progressMessage(FALSE); $this->resetCounters(); } @@ -389,7 +369,20 @@ public function onPrepareRow(MigratePrepareRowEvent $event) { if ($this->itemLimit && ($this->itemLimitCounter + 1) >= $this->itemLimit) { $event->getMigration()->interruptMigration(MigrationInterface::RESULT_COMPLETED); } + } + /** + * {@inheritdoc} + */ + protected function getSource() { + return new SourceFilter(parent::getSource(), $this->idlist); + } + + /** + * {@inheritdoc} + */ + protected function getIdMap() { + return new IdMapFilter(parent::getIdMap(), $this->idlist); } } diff --git a/web/modules/migrate_tools/src/MigrateTools.php b/web/modules/migrate_tools/src/MigrateTools.php new file mode 100644 index 0000000000000000000000000000000000000000..18b91d8bce850d459cc08b218dcf6a3b0528f1a4 --- /dev/null +++ b/web/modules/migrate_tools/src/MigrateTools.php @@ -0,0 +1,39 @@ +<?php + +namespace Drupal\migrate_tools; + +/** + * Utility functionality for use in migrate_tools. + */ +class MigrateTools { + + /** + * Default ID list delimiter. + */ + const DEFAULT_ID_LIST_DELIMITER = ':'; + + /** + * Build the list of specific source IDs to import. + * + * @param array $options + * The migration executable options. + * + * @return array + * The ID list. + */ + public static function buildIdList(array $options) { + $options += [ + 'idlist' => NULL, + 'idlist-delimiter' => self::DEFAULT_ID_LIST_DELIMITER, + ]; + $id_list = []; + if ($options['idlist']) { + $id_list = explode(',', $options['idlist']); + array_walk($id_list, function (&$value) use ($options) { + $value = str_getcsv($value, $options['idlist-delimiter']); + }); + } + return $id_list; + } + +} diff --git a/web/modules/migrate_tools/src/SourceFilter.php b/web/modules/migrate_tools/src/SourceFilter.php new file mode 100644 index 0000000000000000000000000000000000000000..28a6f9d9e72a8762bf40021b0354adcf5c4e0c68 --- /dev/null +++ b/web/modules/migrate_tools/src/SourceFilter.php @@ -0,0 +1,53 @@ +<?php + +namespace Drupal\migrate_tools; + +use Drupal\migrate\Plugin\migrate\source\SourcePluginBase; +use Drupal\migrate\Plugin\MigrateSourceInterface; + +/** + * Class to filter source by an ID list. + */ +class SourceFilter extends \FilterIterator { + + /** + * List of specific source IDs to import. + * + * @var array + */ + protected $idList; + + /** + * SourceFilter constructor. + * + * @param \Drupal\migrate\Plugin\MigrateSourceInterface $source + * The ID map. + * @param array $id_list + * The id list to use in the filter. + */ + public function __construct(MigrateSourceInterface $source, array $id_list) { + parent::__construct($source); + $this->idList = $id_list; + } + + /** + * {@inheritdoc} + */ + public function accept() { + // No idlist filtering, don't filter. + if (empty($this->idList)) { + return TRUE; + } + // Some source plugins do not extend SourcePluginBase. These cannot be + // filtered so warn and return all values. + if (!$this->getInnerIterator() instanceof SourcePluginBase) { + trigger_error(sprintf('The source plugin %s is not an instance of %s. Extend from %s to support idlist filtering.', $this->getInnerIterator()->getPluginId(), SourcePluginBase::class, SourcePluginBase::class)); + return TRUE; + } + // Row is included. + if (in_array(array_values($this->getInnerIterator()->getCurrentIds()), $this->idList)) { + return TRUE; + } + } + +} diff --git a/web/modules/migrate_tools/tests/modules/csv_source_test/config/install/migrate_plus.migration.csv_source_test.yml b/web/modules/migrate_tools/tests/modules/csv_source_test/config/install/migrate_plus.migration.csv_source_test.yml index 0d600a9ffc0d3e06f716b72f7606bebb0e7449b4..409948c70623ccb0500812f7c9e01ab51da02055 100644 --- a/web/modules/migrate_tools/tests/modules/csv_source_test/config/install/migrate_plus.migration.csv_source_test.yml +++ b/web/modules/migrate_tools/tests/modules/csv_source_test/config/install/migrate_plus.migration.csv_source_test.yml @@ -1,6 +1,5 @@ langcode: en status: true -dependencies: { } id: csv_source_test label: Test edit of column aliases for CSV source plugin class: null @@ -11,17 +10,20 @@ migration_group: csv_test source: plugin: csv path: 'public://test.csv' - header_row_count: 1 + header_offset: 0 enclosure: '"' - keys: + ids: - vid - column_names: - 0: - vid: 'Vocabulary Id' - 1: - name: 'Name' - 2: - description: 'Description' + fields: + - + name: vid + label: Vocabulary Id + - + name: name + label: Name + - + name: description + label: Description process: vid: vid name: name @@ -31,3 +33,7 @@ destination: migration_dependencies: required: { } optional: { } +dependencies: + enforced: + module: + - csv_source_test diff --git a/web/modules/migrate_tools/tests/modules/csv_source_test/config/install/migrate_plus.migration_group.csv_test.yml b/web/modules/migrate_tools/tests/modules/csv_source_test/config/install/migrate_plus.migration_group.csv_test.yml index 1c2b05931e417892a7956abfe0c7bde886fccaed..c18781c86b5af10005ab1457c8fd5371d7ce8343 100644 --- a/web/modules/migrate_tools/tests/modules/csv_source_test/config/install/migrate_plus.migration_group.csv_test.yml +++ b/web/modules/migrate_tools/tests/modules/csv_source_test/config/install/migrate_plus.migration_group.csv_test.yml @@ -1,4 +1,8 @@ id: csv_test label: CSV source plugin edit test -description: Test editting of source plugin via the UI +description: Test editing of source plugin via the UI source_type: CSV +dependencies: + enforced: + module: + - csv_source_test diff --git a/web/modules/migrate_tools/tests/modules/csv_source_test/csv_source_test.info.yml b/web/modules/migrate_tools/tests/modules/csv_source_test/csv_source_test.info.yml index f16e6bbd79a7140bf2db9b130997eea5d900c74f..f39a7876609ff4442e70a3b60d765b9be4e7b35a 100644 --- a/web/modules/migrate_tools/tests/modules/csv_source_test/csv_source_test.info.yml +++ b/web/modules/migrate_tools/tests/modules/csv_source_test/csv_source_test.info.yml @@ -2,14 +2,14 @@ type: module name: CSV Source edit test description: 'Test editing of source plugin via the UI.' package: Testing -# core: 8.x +core: 8.x +core_version_requirement: ^8 || ^9 dependencies: - - drupal:migrate (>=8.3) + - drupal:migrate - migrate_plus:migrate_plus - migrate_plus:migrate_source_csv -# Information added by Drupal.org packaging script on 2018-08-27 -version: '8.x-4.0' -core: '8.x' +# Information added by Drupal.org packaging script on 2020-04-30 +version: '8.x-5.0' project: 'migrate_tools' -datestamp: 1535380087 +datestamp: 1588260533 diff --git a/web/modules/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration.fruit_terms.yml b/web/modules/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration.fruit_terms.yml index 7f606d127f01412455ceb8dc28c4b75e0456d454..498597a524a93261da534b09847afc3d3fc69168 100644 --- a/web/modules/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration.fruit_terms.yml +++ b/web/modules/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration.fruit_terms.yml @@ -1,6 +1,5 @@ langcode: en status: true -dependencies: { } id: fruit_terms label: Fruit Terms class: null @@ -30,3 +29,7 @@ destination: migration_dependencies: required: { } optional: { } +dependencies: + enforced: + module: + - migrate_tools_test diff --git a/web/modules/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration.invalid_plugin.yml b/web/modules/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration.invalid_plugin.yml new file mode 100644 index 0000000000000000000000000000000000000000..7b5656e083bf32ff67f2ea5f6eaff0038c73ef3a --- /dev/null +++ b/web/modules/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration.invalid_plugin.yml @@ -0,0 +1,22 @@ +langcode: en +status: true +id: invalid_plugin +label: Invalid Plugin +class: null +field_plugin_method: null +cck_plugin_method: null +migration_tags: { } +migration_group: default +source: + plugin: does_not_exist +process: + name: name +destination: + plugin: entity:taxonomy_term +migration_dependencies: + required: { } + optional: { } +dependencies: + enforced: + module: + - migrate_tools_test diff --git a/web/modules/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration.source_exception.yml b/web/modules/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration.source_exception.yml new file mode 100644 index 0000000000000000000000000000000000000000..d27144fe9e1a3568f4ad7132d6286ef0fe13b2a9 --- /dev/null +++ b/web/modules/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration.source_exception.yml @@ -0,0 +1,22 @@ +langcode: en +status: true +id: source_exception +label: Source Exception +class: null +field_plugin_method: null +cck_plugin_method: null +migration_tags: { } +migration_group: default +source: + plugin: migrate_exception_source_test +process: + name: name +destination: + plugin: entity:taxonomy_term +migration_dependencies: + required: { } + optional: { } +dependencies: + enforced: + module: + - migrate_tools_test diff --git a/web/modules/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration_group.default.yml b/web/modules/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration_group.default.yml index 0f35a218958af5e973b980f989daeb17b3da19f4..87889152152824cef126a9c8f23a222b21b7e3d9 100644 --- a/web/modules/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration_group.default.yml +++ b/web/modules/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration_group.default.yml @@ -1,9 +1,12 @@ langcode: en status: true -dependencies: { } id: default label: Default description: '' source_type: '' module: null shared_configuration: null +dependencies: + enforced: + module: + - migrate_tools_test diff --git a/web/modules/migrate_tools/tests/modules/migrate_tools_test/drush.services.yml b/web/modules/migrate_tools/tests/modules/migrate_tools_test/drush.services.yml new file mode 100644 index 0000000000000000000000000000000000000000..31955a301b7f2c4c509be6f1d211a603a6e67ef0 --- /dev/null +++ b/web/modules/migrate_tools/tests/modules/migrate_tools_test/drush.services.yml @@ -0,0 +1,6 @@ +services: + migrate_tools_test.commands: + class: \Drupal\migrate_tools_test\Commands\MigrateToolsTestCommands + arguments: ['@plugin.manager.migration'] + tags: + - { name: drush.command } diff --git a/web/modules/migrate_tools/tests/modules/migrate_tools_test/migrate_tools_test.info.yml b/web/modules/migrate_tools/tests/modules/migrate_tools_test/migrate_tools_test.info.yml index bbeb6986786cfa9e561f5572bf743376a79202c9..637362e65ba3ad322051860b4fe3d2a0f10ee12a 100644 --- a/web/modules/migrate_tools/tests/modules/migrate_tools_test/migrate_tools_test.info.yml +++ b/web/modules/migrate_tools/tests/modules/migrate_tools_test/migrate_tools_test.info.yml @@ -2,13 +2,13 @@ type: module name: Migrate Tools Test description: 'Test module to test Migrate Tools.' package: Testing -# core: 8.x +core: 8.x +core_version_requirement: ^8 || ^9 dependencies: - - drupal:migrate (>=8.3) + - drupal:migrate - migrate_plus:migrate_plus -# Information added by Drupal.org packaging script on 2018-08-27 -version: '8.x-4.0' -core: '8.x' +# Information added by Drupal.org packaging script on 2020-04-30 +version: '8.x-5.0' project: 'migrate_tools' -datestamp: 1535380087 +datestamp: 1588260533 diff --git a/web/modules/migrate_tools/tests/modules/migrate_tools_test/src/Commands/MigrateToolsTestCommands.php b/web/modules/migrate_tools/tests/modules/migrate_tools_test/src/Commands/MigrateToolsTestCommands.php new file mode 100644 index 0000000000000000000000000000000000000000..d14fe3ae5d73ef94f9b1280410178d2b2603d8f0 --- /dev/null +++ b/web/modules/migrate_tools/tests/modules/migrate_tools_test/src/Commands/MigrateToolsTestCommands.php @@ -0,0 +1,45 @@ +<?php + +namespace Drupal\migrate_tools_test\Commands; + +use Drupal\migrate\MigrateMessage; +use Drupal\migrate\Plugin\MigrationPluginManager; +use Drupal\migrate_tools\MigrateBatchExecutable; +use Drush\Commands\DrushCommands; + +/** + * Migrate Tools Test drush commands. + */ +class MigrateToolsTestCommands extends DrushCommands { + + /** + * Migration plugin manager service. + * + * @var \Drupal\migrate\Plugin\MigrationPluginManager + */ + protected $migrationPluginManager; + + /** + * MigrateToolsTestCommands constructor. + * + * @param \Drupal\migrate\Plugin\MigrationPluginManager $migrationPluginManager + * The Migration Plugin Manager. + */ + public function __construct(MigrationPluginManager $migrationPluginManager) { + parent::__construct(); + $this->migrationPluginManager = $migrationPluginManager; + } + + /** + * Run a batch import of fruit terms as a test. + * + * @command migrate:batch-import-fruit + */ + public function batchImportFruit() { + $fruit_migration = $this->migrationPluginManager->createInstance('fruit_terms'); + $executable = new MigrateBatchExecutable($fruit_migration, new MigrateMessage()); + $executable->batchImport(); + drush_backend_batch_process(); + } + +} diff --git a/web/modules/migrate_tools/tests/modules/migrate_tools_test/src/Plugin/migrate/source/ExceptionThrowingTestSource.php b/web/modules/migrate_tools/tests/modules/migrate_tools_test/src/Plugin/migrate/source/ExceptionThrowingTestSource.php new file mode 100644 index 0000000000000000000000000000000000000000..b6f4a0dfada5395d813d4a15d8d8eef35a6433d4 --- /dev/null +++ b/web/modules/migrate_tools/tests/modules/migrate_tools_test/src/Plugin/migrate/source/ExceptionThrowingTestSource.php @@ -0,0 +1,52 @@ +<?php + +namespace Drupal\migrate_tools_test\Plugin\migrate\source; + +use Drupal\migrate\Plugin\migrate\source\SourcePluginBase; + +/** + * A simple migrate source for testing exception handling. + * + * @MigrateSource( + * id = "migrate_exception_source_test", + * source_module = "migrate_tools_test" + * ) + */ +class ExceptionThrowingTestSource extends SourcePluginBase { + + /** + * {@inheritdoc} + */ + public function fields() { + return []; + } + + /** + * {@inheritdoc} + */ + public function __toString() { + return ''; + } + + /** + * {@inheritdoc} + */ + public function getIds() { + return []; + } + + /** + * {@inheritdoc} + */ + protected function initializeIterator() { + return new \ArrayIterator(); + } + + /** + * {@inheritdoc} + */ + public function rewind() { + throw new \Exception('Rewind Failure'); + } + +} diff --git a/web/modules/migrate_tools/tests/modules/url_source_test/config/install/migrate_plus.migration.url_404_source_test.yml b/web/modules/migrate_tools/tests/modules/url_source_test/config/install/migrate_plus.migration.url_404_source_test.yml new file mode 100644 index 0000000000000000000000000000000000000000..d7167f3ce1324c96b7877e1941bd0d8d9cc0db41 --- /dev/null +++ b/web/modules/migrate_tools/tests/modules/url_source_test/config/install/migrate_plus.migration.url_404_source_test.yml @@ -0,0 +1,27 @@ +langcode: en +status: true +id: url_404_source_test +label: Test 404 URLs in the UI +class: null +field_plugin_method: null +cck_plugin_method: null +migration_tags: { } +migration_group: url_test +source: + plugin: url + data_fetcher_plugin: file + urls: 'http://localhost/does-not-exist.xml' + data_parser_plugin: simple_xml + item_selector: /rss/channel/item + fields: { } + ids: { } +process: { } +destination: + plugin: 'entity:node' +migration_dependencies: + required: { } + optional: { } +dependencies: + enforced: + module: + - url_source_test diff --git a/web/modules/migrate_tools/tests/modules/url_source_test/config/install/migrate_plus.migration_group.url_test.yml b/web/modules/migrate_tools/tests/modules/url_source_test/config/install/migrate_plus.migration_group.url_test.yml new file mode 100644 index 0000000000000000000000000000000000000000..d1b676365de989b5661650f92ef1b00fd1e401bb --- /dev/null +++ b/web/modules/migrate_tools/tests/modules/url_source_test/config/install/migrate_plus.migration_group.url_test.yml @@ -0,0 +1,8 @@ +id: url_test +label: URL source plugin edit test +description: Test editing of URL source plugin via the UI +source_type: URL +dependencies: + enforced: + module: + - url_source_test diff --git a/web/modules/migrate_tools/tests/modules/url_source_test/url_source_test.info.yml b/web/modules/migrate_tools/tests/modules/url_source_test/url_source_test.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..4343b225b0ca2609926678da332e53c815eee3e5 --- /dev/null +++ b/web/modules/migrate_tools/tests/modules/url_source_test/url_source_test.info.yml @@ -0,0 +1,14 @@ +type: module +name: URL Source edit test +description: 'Test editing of URL source plugin via the UI.' +package: Testing +core: 8.x +core_version_requirement: ^8 || ^9 +dependencies: + - drupal:migrate + - migrate_plus:migrate_plus + +# Information added by Drupal.org packaging script on 2020-04-30 +version: '8.x-5.0' +project: 'migrate_tools' +datestamp: 1588260533 diff --git a/web/modules/migrate_tools/tests/src/Functional/DrushBatchImportTest.php b/web/modules/migrate_tools/tests/src/Functional/DrushBatchImportTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1f97efca9b6751b58d1b5a718aec00ab24a37a93 --- /dev/null +++ b/web/modules/migrate_tools/tests/src/Functional/DrushBatchImportTest.php @@ -0,0 +1,44 @@ +<?php + +namespace Drupal\Tests\migrate_tools\Functional; + +use Drupal\Tests\BrowserTestBase; +use Drush\TestTraits\DrushTestTrait; + +/** + * Test that batch import runs correctly in drush command. + * + * @group migrate_tools + */ +class DrushBatchImportTest extends BrowserTestBase { + use DrushTestTrait; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'migrate_tools_test', + 'migrate_tools', + 'migrate_plus', + 'taxonomy', + 'text', + 'system', + 'user', + ]; + + /** + * Tests that a batch import run from a custom drush command succeeds. + */ + public function testBatchImportInDrushComand(): void { + $this->drush('migrate:batch-import-fruit'); + $migration = \Drupal::service('plugin.manager.migration')->createInstance('fruit_terms'); + $id_map = $migration->getIdMap(); + $this->assertSame(3, $id_map->importedCount()); + } + +} diff --git a/web/modules/migrate_tools/tests/src/Functional/DrushCommandsGeneratorTest.php b/web/modules/migrate_tools/tests/src/Functional/DrushCommandsGeneratorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..0cbc3f6491181c6baf00d703219c45b556043abd --- /dev/null +++ b/web/modules/migrate_tools/tests/src/Functional/DrushCommandsGeneratorTest.php @@ -0,0 +1,100 @@ +<?php + +namespace Drupal\Tests\migrate_tools\Functional; + +use Drupal\Core\StreamWrapper\PublicStream; +use Drupal\Core\StreamWrapper\StreamWrapperInterface; +use Drupal\Tests\BrowserTestBase; +use Drush\TestTraits\DrushTestTrait; + +/** + * Execute drush on fully functional website using source generators. + * + * @group migrate_tools + */ +class DrushCommandsGeneratorTest extends BrowserTestBase { + use DrushTestTrait; + + /** + * The source CSV data. + * + * @var string + */ + protected $sourceData; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'csv_source_test', + 'migrate', + 'migrate_plus', + 'migrate_source_csv', + 'migrate_tools', + 'taxonomy', + ]; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + // Setup the file system so we create the source CSV. + $this->container->get('stream_wrapper_manager')->registerWrapper('public', PublicStream::class, StreamWrapperInterface::NORMAL); + $fs = \Drupal::service('file_system'); + $fs->mkdir('public://sites/default/files', NULL, TRUE); + + // The source data for this test. + $this->sourceData = <<<'EOD' +vid,name,description,hierarchy,weight +tags,Tags,Use tags to group articles,0,0 +forums,Sujet de discussion,Forum navigation vocabulary,1,0 +test_vocabulary,Test Vocabulary,This is the vocabulary description,1,0 +genre,Genre,Genre description,1,0 +EOD; + + // Write the data to the filepath given in the test migration. + file_put_contents('public://test.csv', $this->sourceData); + } + + /** + * Tests synced import. + */ + public function testSyncImport(): void { + $this->drush('mim', ['csv_source_test']); + $this->assertStringContainsString('1/4', $this->getErrorOutput()); + $this->assertStringContainsString('4/4', $this->getErrorOutput()); + $this->assertStringContainsString('[notice] Processed 4 items (4 created, 0 updated, 0 failed, 0 ignored) - done with \'csv_source_test\'', $this->getErrorOutput()); + $this->assertStringNotContainsString('5/5', $this->getErrorOutput()); + $vocabulary = \Drupal::entityTypeManager()->getStorage('taxonomy_vocabulary')->load('genre'); + $this->assertEquals('Genre', $vocabulary->label()); + $this->assertEquals(4, \Drupal::entityTypeManager()->getStorage('taxonomy_vocabulary')->getQuery()->count()->execute()); + + // Remove one vocab and replace with another. + $this->sourceData = str_replace('genre,Genre,Genre description,1,0', 'fruit,Fruit,Fruit description,1,0', $this->sourceData); + file_put_contents('public://test.csv', $this->sourceData); + + // Execute sync migration. + $this->drush('mim', ['csv_source_test'], ['sync' => NULL]); + $this->assertStringContainsString('1/4', $this->getErrorOutput()); + $this->assertStringContainsString('25% [notice] Rolled back 1 item - done with \'csv_source_test\'', $this->getErrorOutput()); + $this->assertStringContainsString('4/4', $this->getErrorOutput()); + $this->assertStringContainsString('5/5', $this->getErrorOutput()); + $this->assertStringContainsString('100% [notice] Processed 4 items (1 created, 3 updated, 0 failed, 0 ignored) - done with \'csv_source_test\'', $this->getErrorOutput()); + $this->assertEquals(4, \Drupal::entityTypeManager()->getStorage('taxonomy_vocabulary')->getQuery()->count()->execute()); + // Flush cache so recently deleted vocabulary actually goes away. + drupal_flush_all_caches(); + $this->assertEmpty(\Drupal::entityTypeManager()->getStorage('taxonomy_vocabulary')->load('genre')); + + /** @var \Drupal\migrate\Plugin\MigrateIdMapInterface $id_map */ + $id_map = $this->container->get('plugin.manager.migration')->createInstance('csv_source_test')->getIdMap(); + $this->assertCount(4, $id_map); + } + +} diff --git a/web/modules/migrate_tools/tests/src/Functional/DrushCommandsTest.php b/web/modules/migrate_tools/tests/src/Functional/DrushCommandsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a863b332ac25dd60fd72f79ead0cb8c8916efe46 --- /dev/null +++ b/web/modules/migrate_tools/tests/src/Functional/DrushCommandsTest.php @@ -0,0 +1,214 @@ +<?php + +namespace Drupal\Tests\migrate_tools\Functional; + +use Drupal\Tests\BrowserTestBase; +use Drush\TestTraits\DrushTestTrait; + +/** + * Execute drush on fully functional website. + * + * @group migrate_tools + */ +class DrushCommandsTest extends BrowserTestBase { + use DrushTestTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'migrate_tools_test', + 'migrate_tools', + 'migrate_plus', + 'taxonomy', + 'text', + 'system', + 'user', + ]; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * Tests migrate:import with feedback. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + */ + public function testFeedback(): void { + $this->drush('mim', ['fruit_terms'], ['feedback' => 2]); + $this->assertStringContainsString('1/3', $this->getErrorOutput()); + $this->assertStringContainsString('[notice] Processed 2 items (2 created, 0 updated, 0 failed, 0 ignored) - continuing with \'fruit_terms\'', $this->getErrorOutput()); + $this->assertStringContainsString('3/3', $this->getErrorOutput()); + $this->assertStringContainsString('[notice] Processed 1 item (1 created, 0 updated, 0 failed, 0 ignored) - done with \'fruit_terms\'', $this->getErrorOutput()); + $this->assertStringNotContainsString('4', $this->getErrorOutput()); + } + + /** + * Tests migrate:import with limit. + */ + public function testLimit(): void { + $this->drush('mim', ['fruit_terms'], ['limit' => 2]); + $this->assertStringContainsString('1/3', $this->getErrorOutput()); + $this->assertStringContainsString('[notice] Processed 2 items (2 created, 0 updated, 0 failed, 0 ignored) - done with \'fruit_terms\'', $this->getErrorOutput()); + $this->assertStringNotContainsString('3/3', $this->getErrorOutput()); + } + + /** + * Test that migrations continue after a failure if the option is set. + */ + public function testContinueOnFailure(): void { + // Option not set, fruit_terms should not run. + $this->drush('mim', ['invalid_plugin,fruit_terms'], [], NULL, NULL, 1); + $this->assertStringNotContainsString("done with 'fruit_terms'", $this->getErrorOutput()); + // Option set, fruit_terms should run. + $this->drush('mim', ['invalid_plugin,fruit_terms'], ['continue-on-failure' => NULL]); + $this->assertStringContainsString("done with 'fruit_terms'", $this->getErrorOutput()); + // Option not set, fruit_terms should not run. + $this->drush('mr', ['invalid_plugin,fruit_terms'], [], NULL, NULL, 1); + $this->assertStringNotContainsString("done with 'fruit_terms'", $this->getErrorOutput()); + // Option set, fruit_terms should run. + $this->drush('mr', ['invalid_plugin,fruit_terms'], ['continue-on-failure' => NULL]); + $this->assertStringContainsString("done with 'fruit_terms'", $this->getErrorOutput()); + // Option not set, fruit_terms should not display. + $this->drush('ms', ['invalid_plugin,fruit_terms'], ['format' => 'json'], NULL, NULL, 1); + // This demonstrates we surface the exception but not as an error. + $this->assertStringNotContainsString('[error] The "does_not_exist" plugin does not exist', $this->getErrorOutput()); + $this->assertStringContainsString('The "does_not_exist" plugin does not exist', $this->getErrorOutput()); + $this->assertStringNotContainsString('fruit_terms Idle 3', $this->getOutput()); + // Option set, fruit_terms should display. + $this->drush('ms', ['invalid_plugin,fruit_terms'], ['continue-on-failure' => NULL]); + $this->assertStringContainsString('[error] The "does_not_exist" plugin does not exist', $this->getErrorOutput()); + $this->assertStringContainsString('fruit_terms Idle 3', $this->getOutput()); + } + + /** + * Tests many of the migrate drush commands. + */ + public function testDrush(): void { + $this->drush('ms', [], [], NULL, NULL, 1); + $this->assertStringContainsString('The "does_not_exist" plugin does not exist.', $this->getErrorOutput()); + $this->container->get('config.factory')->getEditable('migrate_plus.migration.invalid_plugin')->delete(); + // Flush cache so the recently removed invalid migration is cleared. + drupal_flush_all_caches(); + $this->drush('ms', [], ['format' => 'json']); + $expected = [ + [ + 'group' => 'Default (default)', + 'id' => 'fruit_terms', + 'imported' => 0, + 'status' => 'Idle', + 'total' => 3, + 'unprocessed' => 3, + 'last_imported' => '', + ], + [ + 'group' => 'Default (default)', + 'id' => 'source_exception', + 'imported' => 0, + 'status' => 'Idle', + 'total' => 0, + 'unprocessed' => 0, + 'last_imported' => '', + ], + ]; + $this->assertEquals($expected, $this->getOutputFromJSON()); + $this->drush('mim', ['fruit_terms']); + $this->assertStringContainsString('1/3', $this->getErrorOutput()); + $this->assertStringContainsString('3/3', $this->getErrorOutput()); + $this->assertStringContainsString('[notice] Processed 3 items (3 created, 0 updated, 0 failed, 0 ignored) - done with \'fruit_terms\'', $this->getErrorOutput()); + $this->assertStringNotContainsString('4', $this->getErrorOutput()); + $this->drush('mim', ['fruit_terms'], [ + 'update' => NULL, + 'force' => NULL, + 'execute-dependencies' => NULL, + ]); + $this->assertStringContainsString('1/3', $this->getErrorOutput()); + $this->assertStringContainsString('3/3', $this->getErrorOutput()); + $this->assertStringContainsString('[notice] Processed 3 items (0 created, 3 updated, 0 failed, 0 ignored) - done with \'fruit_terms\'', $this->getErrorOutput()); + $this->assertStringNotContainsString('4', $this->getErrorOutput()); + $this->drush('mrs', ['fruit_terms']); + $this->assertErrorOutputEquals('[warning] Migration fruit_terms is already Idle'); + $this->drush('mfs', ['fruit_terms'], ['format' => 'json']); + $expected = [ + [ + 'machine_name' => 'name', + 'description' => 'name', + ], + ]; + $this->assertEquals($expected, $this->getOutputFromJSON()); + $this->drush('mr', ['fruit_terms']); + $this->assertStringContainsString('1/3', $this->getErrorOutput()); + $this->assertStringContainsString('3/3', $this->getErrorOutput()); + $this->assertStringContainsString('[notice] Rolled back 3 items - done with \'fruit_terms\'', $this->getErrorOutput()); + $this->assertStringNotContainsString('4', $this->getErrorOutput()); + $this->drush('migrate:stop', ['fruit_terms']); + $this->assertErrorOutputEquals('[warning] Migration fruit_terms is idle'); + + $this->drush('mim', ['fruit_terms'], ['skip-progress-bar' => NULL]); + $this->assertErrorOutputEquals('[notice] Processed 3 items (3 created, 0 updated, 0 failed, 0 ignored) - done with \'fruit_terms\''); + $this->drush('mr', ['fruit_terms'], ['skip-progress-bar' => NULL]); + $this->assertErrorOutputEquals('[notice] Rolled back 3 items - done with \'fruit_terms\''); + } + + /** + * Fully test migrate messages. + */ + public function testMessages(): void { + $this->drush('mim', ['fruit_terms']); + $this->drush('mmsg', ['fruit_terms']); + $this->assertErrorOutputEquals('[notice] No messages for this migration'); + /** @var \Drupal\migrate\Plugin\MigrateIdMapInterface $id_map */ + $id_map = $this->container->get('plugin.manager.migration')->createInstance('fruit_terms')->getIdMap(); + $id_map->saveMessage(['name' => 'Apple'], 'You picked a bad one.'); + $this->drush('mmsg', ['fruit_terms'], ['format' => 'json']); + $expected = [ + [ + 'level' => '1', + 'message' => 'You picked a bad one.', + 'source_ids' => 'Apple', + 'destination_ids' => '1', + ], + ]; + $this->assertEquals($expected, $this->getOutputFromJSON()); + $this->drush('mmsg', ['fruit_terms'], ['format' => 'csv']); + $expected = <<<EOT +"Source ID(s)","Destination ID(s)",Level,Message +Apple,1,1,"You picked a bad one." +EOT; + $this->assertEquals($expected, $this->getOutput()); + } + + /** + * Tests synced import. + */ + public function testSyncImport(): void { + $this->drush('mim', ['fruit_terms']); + $this->assertStringContainsString('1/3', $this->getErrorOutput()); + $this->assertStringContainsString('3/3', $this->getErrorOutput()); + $this->assertStringContainsString('[notice] Processed 3 items (3 created, 0 updated, 0 failed, 0 ignored) - done with \'fruit_terms\'', $this->getErrorOutput()); + $this->assertStringNotContainsString('4', $this->getErrorOutput()); + $term = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->load(2); + $this->assertEquals('Banana', $term->label()); + $this->assertEquals(3, \Drupal::entityTypeManager()->getStorage('taxonomy_term')->getQuery()->count()->execute()); + $source = $this->container->get('config.factory')->getEditable('migrate_plus.migration.fruit_terms')->get('source'); + unset($source['data_rows'][1]); + $source['data_rows'][] = ['name' => 'Grape']; + $this->container->get('config.factory')->getEditable('migrate_plus.migration.fruit_terms')->set('source', $source)->save(); + // Flush cache so the recently changed migration can be refreshed. + drupal_flush_all_caches(); + $this->drush('mim', ['fruit_terms'], ['sync' => NULL]); + $this->assertStringContainsString('1/3', $this->getErrorOutput()); + $this->assertStringContainsString('4/4', $this->getErrorOutput()); + $this->assertStringContainsString('[notice] Processed 3 items (1 created, 2 updated, 0 failed, 0 ignored) - done with \'fruit_terms\'', $this->getErrorOutput()); + $this->assertStringNotContainsString('5', $this->getErrorOutput()); + $this->assertEquals(3, \Drupal::entityTypeManager()->getStorage('taxonomy_term')->getQuery()->count()->execute()); + $this->assertEmpty(\Drupal::entityTypeManager()->getStorage('taxonomy_term')->load(2)); + + /** @var \Drupal\migrate\Plugin\MigrateIdMapInterface $id_map */ + $id_map = $this->container->get('plugin.manager.migration')->createInstance('fruit_terms')->getIdMap(); + $this->assertCount(3, $id_map); + } + +} diff --git a/web/modules/migrate_tools/tests/src/Functional/MigrateExecutionFormTest.php b/web/modules/migrate_tools/tests/src/Functional/MigrateExecutionFormTest.php index 2f29bf5ba29d3d1790b7438869cec3f59e4f4c35..565a6c9e7955cb661b60866125f5d98b21ddcf51 100644 --- a/web/modules/migrate_tools/tests/src/Functional/MigrateExecutionFormTest.php +++ b/web/modules/migrate_tools/tests/src/Functional/MigrateExecutionFormTest.php @@ -2,7 +2,9 @@ namespace Drupal\Tests\migrate_tools\Functional; +use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\taxonomy\Entity\Vocabulary; +use Drupal\taxonomy\VocabularyInterface; use Drupal\Tests\BrowserTestBase; /** @@ -11,6 +13,7 @@ * @group migrate_tools */ class MigrateExecutionFormTest extends BrowserTestBase { + use StringTranslationTrait; /** * {@inheritdoc} @@ -31,7 +34,7 @@ class MigrateExecutionFormTest extends BrowserTestBase { /** * {@inheritdoc} */ - protected $profile = 'testing'; + protected $defaultTheme = 'stark'; /** * The vocabulary. @@ -50,7 +53,7 @@ class MigrateExecutionFormTest extends BrowserTestBase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); $this->vocabulary = $this->createVocabulary(['vid' => 'fruit', 'name' => 'Fruit']); $this->vocabularyQuery = $this->container->get('entity_type.manager') @@ -65,7 +68,7 @@ protected function setUp() { * * @throws \Behat\Mink\Exception\ExpectationException */ - public function testExecution() { + public function testExecution(): void { $group = 'default'; $migration = 'fruit_terms'; $urlPath = "/admin/structure/migrate/manage/{$group}/migrations/{$migration}/execute"; @@ -77,21 +80,21 @@ public function testExecution() { $edit = [ 'operation' => 'import', ]; - $this->drupalPostForm($urlPath, $edit, t('Execute')); + $this->drupalPostForm($urlPath, $edit, $this->t('Execute')); $real_count = $this->vocabularyQuery->count()->execute(); $expected_count = 3; $this->assertEquals($expected_count, $real_count); $edit = [ 'operation' => 'rollback', ]; - $this->drupalPostForm($urlPath, $edit, t('Execute')); + $this->drupalPostForm($urlPath, $edit, $this->t('Execute')); $real_count = $this->vocabularyQuery->count()->execute(); $expected_count = 0; $this->assertEquals($expected_count, $real_count); $edit = [ 'operation' => 'import', ]; - $this->drupalPostForm($urlPath, $edit, t('Execute')); + $this->drupalPostForm($urlPath, $edit, $this->t('Execute')); $real_count = $this->vocabularyQuery->count()->execute(); $expected_count = 3; $this->assertEquals($expected_count, $real_count); @@ -107,7 +110,7 @@ public function testExecution() { * @return \Drupal\taxonomy\VocabularyInterface * Created vocabulary. */ - protected function createVocabulary(array $values = []) { + protected function createVocabulary(array $values = []): VocabularyInterface { // Find a non-existent random vocabulary name. if (!isset($values['vid'])) { do { diff --git a/web/modules/migrate_tools/tests/src/Functional/SourceCsvFormTest.php b/web/modules/migrate_tools/tests/src/Functional/SourceCsvFormTest.php deleted file mode 100644 index 767a9bfe08cb4b019491d39b0038927051914aae..0000000000000000000000000000000000000000 --- a/web/modules/migrate_tools/tests/src/Functional/SourceCsvFormTest.php +++ /dev/null @@ -1,241 +0,0 @@ -<?php - -namespace Drupal\Tests\migrate_tools\Functional; - -use Drupal\Core\StreamWrapper\PublicStream; -use Drupal\Core\StreamWrapper\StreamWrapperInterface; -use Drupal\Tests\BrowserTestBase; -use Drupal\taxonomy\Entity\Vocabulary; -use Drupal\taxonomy\VocabularyInterface; - -/** - * Test the CSV column alias edit form. - * - * @requires module migrate_source_csv - * - * @group migrate_tools - */ -class SourceCsvFormTest extends BrowserTestBase { - - /** - * Temporary store for column assignment changes. - * - * @var \Drupal\Core\TempStore\PrivateTempStoreFactory - */ - protected $store; - - /** - * {@inheritdoc} - */ - protected static $modules = [ - 'taxonomy', - 'migrate', - 'migrate_plus', - 'migrate_tools', - 'migrate_source_csv', - 'csv_source_test', - ]; - - /** - * {@inheritdoc} - */ - protected $profile = 'testing'; - - /** - * The migration group for the test migration. - * - * @var string - */ - protected $group; - - /** - * The test migration id. - * - * @var string - */ - protected $migration; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - // Log in as user 1. Migrations in the UI can only be performed as user 1. - $this->drupalLogin($this->rootUser); - - // Setup the file system so we create the source CSV. - $this->container->get('stream_wrapper_manager')->registerWrapper('public', PublicStream::class, StreamWrapperInterface::NORMAL); - $fs = \Drupal::service('file_system'); - $fs->mkdir('public://sites/default/files', NULL, TRUE); - - // The source data for this test. - $source_data = <<<'EOD' -vid,name,description,hierarchy,weight -tags,Tags,Use tags to group articles,0,0 -forums,Sujet de discussion,Forum navigation vocabulary,1,0 -test_vocabulary,Test Vocabulary,This is the vocabulary description,1,0 -genre,Genre,Genre description,1,0 -EOD; - - // Write the data to the filepath given in the test migration. - file_put_contents('public://test.csv', $source_data); - - // Get the store. - $tempStoreFactory = \Drupal::service('tempstore.private'); - $this->store = $tempStoreFactory->get('migrate_tools'); - - // Select the group and migration to test. - $this->group = 'csv_test'; - $this->migration = 'csv_source_test'; - } - - /** - * Tests the form to edit CSV column aliases. - * - * @throws \Behat\Mink\Exception\ExpectationException - */ - public function testSourceCsvForm() { - // Define the paths to be used. - $executeUrlPath = "/admin/structure/migrate/manage/{$this->group}/migrations/{$this->migration}/execute"; - $editUrlPath = "/admin/structure/migrate/manage/{$this->group}/migrations/{$this->migration}/source/edit"; - - // Assert the test migration is listed. - $this->drupalGet("/admin/structure/migrate/manage/{$this->group}/migrations"); - $session = $this->assertSession(); - $session->responseContains('Test edit of column aliases for CSV source plugin'); - - // Proceed to the edit page. - $this->drupalGet($editUrlPath); - $session->responseContains('You can change the columns to be used by this migration for each source property.'); - - // Test that there are 3 select fields available which match the number of - // properties in the process pipeline. - $this->assertTrue($session->optionExists('edit-vid', 'vid') - ->isSelected()); - $this->assertTrue($session->optionExists('edit-name', 'name') - ->isSelected()); - $this->assertTrue($session->optionExists('edit-description', 'description') - ->isSelected()); - $session->responseNotContains('edit-hierarchy'); - $session->responseNotContains('edit-weight'); - - // Test that all 5 columns in the CSV source are available as options on - // one of the select fields. - $this->assertTrue($session->optionExists('edit-description', 'vid')); - $this->assertTrue($session->optionExists('edit-description', 'name')); - $this->assertTrue($session->optionExists('edit-description', 'description')); - $this->assertTrue($session->optionExists('edit-description', 'hierarchy')); - $this->assertTrue($session->optionExists('edit-description', 'weight')); - - // Test that two aliases can not be the same. - $edit = [ - 'edit-vid' => 2, - 'edit-name' => 1, - 'edit-description' => 1, - ]; - $this->drupalPostForm($editUrlPath, $edit, t('Submit')); - $session->responseContains('Source properties can not share the same source column.'); - $this->assertTrue($session->optionExists('edit-vid', 'description') - ->isSelected()); - $this->assertTrue($session->optionExists('edit-name', 'name') - ->isSelected()); - $this->assertTrue($session->optionExists('edit-description', 'name') - ->isSelected()); - - // Test that changes to all the column aliases are saved. - $edit = [ - 'edit-vid' => 4, - 'edit-name' => 0, - 'edit-description' => 1, - ]; - $this->drupalPostForm($editUrlPath, $edit, t('Submit')); - $this->assertTrue($session->optionExists('edit-vid', 'weight') - ->isSelected()); - $this->assertTrue($session->optionExists('edit-name', 'vid') - ->isSelected()); - $this->assertTrue($session->optionExists('edit-description', 'name') - ->isSelected()); - - // Test that the changes are saved to store. - $columnConfiguration = $this->store->get('csv_source_test'); - $migrationsChanged = $this->store->get('migrations_changed'); - $this->assertSame(['csv_source_test'], $migrationsChanged); - $expected = - [ - 'original' => - [ - 0 => ['vid' => 'Vocabulary Id'], - 1 => ['name' => 'Name'], - 2 => ['description' => 'Description'], - ], - 'changed' => - [ - 4 => ['vid' => 'weight'], - 0 => ['name' => 'vid'], - 1 => ['description' => 'name'], - ], - ]; - $this->assertSame($expected, $columnConfiguration); - - // Test the migration with incorrect column aliases. Flush the cache to - // ensure the plugin alter is run. - drupal_flush_all_caches(); - $edit = [ - 'operation' => 'import', - ]; - $this->drupalPostForm($executeUrlPath, $edit, t('Execute')); - $session->responseContains("Processed 1 item (1 created, 0 updated, 0 failed, 0 ignored) - done with 'csv_source_test'"); - - // Rollback. - $edit = [ - 'operation' => 'rollback', - ]; - $this->drupalPostForm($executeUrlPath, $edit, t('Execute')); - - // Restore to an order that will succesfully migrate. - $edit = [ - 'edit-vid' => 0, - 'edit-name' => 1, - 'edit-description' => 2, - ]; - $this->drupalPostForm($editUrlPath, $edit, t('Submit')); - $this->assertTrue($session->optionExists('edit-vid', 'vid') - ->isSelected()); - $this->assertTrue($session->optionExists('edit-name', 'name') - ->isSelected()); - $this->assertTrue($session->optionExists('edit-description', 'description') - ->isSelected()); - - // Test the vocabulary migration. - $edit = [ - 'operation' => 'import', - ]; - drupal_flush_all_caches(); - $this->drupalPostForm($executeUrlPath, $edit, t('Execute')); - $session->responseContains("Processed 4 items (4 created, 0 updated, 0 failed, 0 ignored) - done with 'csv_source_test'"); - $this->assertEntity('tags', 'Tags', 'Use tags to group articles'); - $this->assertEntity('forums', 'Sujet de discussion', 'Forum navigation vocabulary'); - $this->assertEntity('test_vocabulary', 'Test Vocabulary', 'This is the vocabulary description'); - $this->assertEntity('genre', 'Genre', 'Genre description'); - } - - /** - * Validate a migrated vocabulary contains the expected values. - * - * @param string $id - * Entity ID to load and check. - * @param string $expected_name - * The name the migrated entity should have. - * @param string $expected_description - * The description the migrated entity should have. - */ - protected function assertEntity($id, $expected_name, $expected_description) { - /** @var \Drupal\taxonomy\VocabularyInterface $entity */ - $entity = Vocabulary::load($id); - $this->assertTrue($entity instanceof VocabularyInterface); - $this->assertSame($expected_name, $entity->label()); - $this->assertSame($expected_description, $entity->getDescription()); - } - -} diff --git a/web/modules/migrate_tools/tests/src/Functional/SourceUrlFormTest.php b/web/modules/migrate_tools/tests/src/Functional/SourceUrlFormTest.php new file mode 100644 index 0000000000000000000000000000000000000000..15cc5a5b810b19a333228cd6fdf4db469e4f8c9f --- /dev/null +++ b/web/modules/migrate_tools/tests/src/Functional/SourceUrlFormTest.php @@ -0,0 +1,71 @@ +<?php + +namespace Drupal\Tests\migrate_tools\Functional; + +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Tests\BrowserTestBase; + +/** + * Test the URL column alias edit form. + * + * @group migrate_tools + */ +class SourceUrlFormTest extends BrowserTestBase { + use StringTranslationTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'migrate', + 'migrate_plus', + 'migrate_tools', + 'url_source_test', + ]; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * The migration group for the test migration. + * + * @var string + */ + protected $group; + + /** + * The test migration id. + * + * @var string + */ + protected $migration; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + // Log in as user 1. Migrations in the UI can only be performed as user 1. + $this->drupalLogin($this->rootUser); + + // Select the group and migration to test. + $this->group = 'url_test'; + $this->migration = 'url_404_source_test'; + } + + /** + * Tests the form ensure graceful 404 handling. + * + * @throws \Behat\Mink\Exception\ExpectationException + */ + public function testSourceUrl404Form(): void { + // Assert the test migration is listed. + $this->drupalGet("/admin/structure/migrate/manage/{$this->group}/migrations"); + $session = $this->assertSession(); + $session->responseContains('Test 404 URLs in the UI'); + } + +} diff --git a/web/modules/migrate_tools/tests/src/Kernel/DrushTest.php b/web/modules/migrate_tools/tests/src/Kernel/DrushTest.php new file mode 100644 index 0000000000000000000000000000000000000000..4f26414f7f269e139102cd7c4ab82dc00172b7c4 --- /dev/null +++ b/web/modules/migrate_tools/tests/src/Kernel/DrushTest.php @@ -0,0 +1,320 @@ +<?php + +namespace Drupal\Tests\migrate_tools\Kernel; + +use Drupal\migrate\Plugin\MigrationInterface; +use Drupal\migrate_tools\Commands\MigrateToolsCommands; +use Drupal\migrate_tools\MigrateTools; +use Drupal\Tests\migrate\Kernel\MigrateTestBase; + +/** + * Tests for the Drush 9 commands. + * + * @group migrate_tools + */ +class DrushTest extends MigrateTestBase { + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'migrate_tools_test', + 'migrate_tools', + 'migrate_plus', + 'taxonomy', + 'text', + 'system', + 'user', + ]; + + /** + * Base options array for import. + * + * @var array + */ + protected $importBaseOptions = [ + 'all' => NULL, + 'group' => NULL, + 'tag' => NULL, + 'limit' => NULL, + 'feedback' => NULL, + 'idlist' => NULL, + 'idlist-delimiter' => MigrateTools::DEFAULT_ID_LIST_DELIMITER, + 'update' => NULL, + 'force' => NULL, + 'execute-dependencies' => NULL, + 'skip-progress-bar' => FALSE, + 'continue-on-failure' => FALSE, + 'sync' => FALSE, + ]; + + /** + * The Migrate Tools Command drush service. + * + * @var \Drupal\migrate_tools\Commands\MigrateToolsCommands + */ + protected $commands; + + /** + * The migration plugin manager. + * + * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface + */ + protected $migrationPluginManager; + + /** + * The logger. + * + * @var \Psr\Log\LoggerInterface + */ + protected $logger; + + /** + * {@inheritdoc} + */ + public function setUp(): void { + parent::setUp(); + $this->installConfig('migrate_plus'); + $this->installConfig('migrate_tools_test'); + $this->installEntitySchema('taxonomy_term'); + $this->installEntitySchema('user'); + $this->installSchema('system', ['key_value', 'key_value_expire']); + $this->installSchema('user', ['users_data']); + $this->migrationPluginManager = $this->container->get('plugin.manager.migration'); + $this->logger = $this->container->get('logger.channel.migrate_tools'); + $this->commands = new MigrateToolsCommands( + $this->migrationPluginManager, + $this->container->get('date.formatter'), + $this->container->get('entity_type.manager'), + $this->container->get('keyvalue')); + $this->commands->setLogger($this->logger); + } + + /** + * Tests drush ms. + */ + public function testStatus(): void { + $this->executeMigration('fruit_terms'); + /** @var \Consolidation\OutputFormatters\StructuredData\RowsOfFields $result */ + $result = $this->commands->status('fruit_terms', [ + 'group' => NULL, + 'tag' => NULL, + 'names-only' => FALSE, + ]); + $rows = $result->getArrayCopy(); + $this->assertCount(1, $rows); + $row = reset($rows); + $this->assertSame('fruit_terms', $row['id']); + $this->assertSame(3, $row['total']); + $this->assertSame(3, $row['imported']); + $this->assertSame('Idle', $row['status']); + + // Migrate status should not display migrate_drupal migrations if no source + // database is defined. + \Drupal::service('module_installer')->uninstall(['migrate_tools_test']); + $this->enableModules(['migrate_drupal']); + \Drupal::configFactory()->getEditable('migrate_plus.migration.fruit_terms')->delete(); + $rows = $this->commands->status(); + $this->assertEmpty($rows); + } + + /** + * Tests that a failing status throws an exception (i.e. exit code). + */ + public function testFailingStatusThrowsException(): void { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('The "does_not_exist" plugin does not exist.'); + $this->commands->status('invalid_plugin'); + } + + /** + * Tests drush mim. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + */ + public function testImport(): void { + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = $this->migrationPluginManager->createInstance('fruit_terms'); + $id_map = $migration->getIdMap(); + $this->commands->import('fruit_terms', array_merge($this->importBaseOptions, ['idlist' => 'Apple'])); + $this->assertSame(1, $id_map->importedCount()); + $this->commands->import('fruit_terms', $this->importBaseOptions); + $this->assertSame(3, $id_map->importedCount()); + $this->commands->import('fruit_terms', array_merge($this->importBaseOptions, ['idlist' => 'Apple', 'update' => TRUE])); + $this->assertCount(0, $id_map->getRowsNeedingUpdate(100)); + } + + /** + * Tests that a failing import throws an exception (i.e. exit code). + */ + public function testFailingImportThrowsException(): void { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('source_exception migration failed.'); + $this->commands->import('source_exception', $this->importBaseOptions); + } + + /** + * Tests drush mmsg. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + */ + public function testMessages(): void { + $this->executeMigration('fruit_terms'); + $message = $this->getRandomGenerator()->string(16); + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = $this->migrationPluginManager->createInstance('fruit_terms'); + $id_map = $migration->getIdMap(); + $id_map->saveMessage(['name' => 'Apple'], $message); + /** @var \Consolidation\OutputFormatters\StructuredData\RowsOfFields $result */ + $result = $this->commands->messages('fruit_terms', [ + 'csv' => FALSE, + 'idlist' => NULL, + 'idlist-delimiter' => MigrateTools::DEFAULT_ID_LIST_DELIMITER, + ]); + $rows = $result->getArrayCopy(); + $this->assertSame($message, $rows[0]['message']); + } + + /** + * Tests that a failing messages throws an exception (i.e. exit code). + */ + public function testFailingMessagesThrowsException(): void { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Migration does_not_exist does not exist'); + $this->commands->messages('does_not_exist', [ + 'csv' => FALSE, + 'idlist' => NULL, + 'idlist-delimiter' => MigrateTools::DEFAULT_ID_LIST_DELIMITER, + ]); + } + + /** + * Tests drush mr. + */ + public function testRollback(): void { + $this->executeMigration('fruit_terms'); + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = $this->migrationPluginManager->createInstance('fruit_terms'); + $id_map = $migration->getIdMap(); + $this->assertSame(3, $id_map->importedCount()); + $this->commands->rollback('fruit_terms', $this->importBaseOptions); + $this->assertSame(0, $id_map->importedCount()); + } + + /** + * Tests that a failing rollback throws an exception (i.e. exit code). + */ + public function testFailingRollbackThrowsException(): void { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('source_exception migration failed'); + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = $this->migrationPluginManager->createInstance('source_exception'); + $migration->setStatus(MigrationInterface::STATUS_IMPORTING); + $this->commands->rollback('source_exception', $this->importBaseOptions); + } + + /** + * Tests drush mrs. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + */ + public function testReset(): void { + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = $this->migrationPluginManager->createInstance('fruit_terms'); + $migration->setStatus(MigrationInterface::STATUS_IMPORTING); + $status = $this->commands->status('fruit_terms', [ + 'group' => NULL, + 'tag' => NULL, + 'names-only' => FALSE, + ])->getArrayCopy()[0]['status']; + $this->assertSame('Importing', $status); + $this->commands->resetStatus('fruit_terms'); + $this->assertSame(MigrationInterface::STATUS_IDLE, $migration->getStatus()); + + } + + /** + * Tests that a failing reset status throws an exception (i.e. exit code). + */ + public function testFailingResetStatusThrowsException(): void { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Migration does_not_exist does not exist'); + $this->commands->resetStatus('does_not_exist'); + } + + /** + * Tests drush mst. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + */ + public function testStop(): void { + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = $this->migrationPluginManager->createInstance('fruit_terms'); + $migration->setStatus(MigrationInterface::STATUS_IMPORTING); + $this->commands->stop('fruit_terms'); + $this->assertSame(MigrationInterface::STATUS_STOPPING, $migration->getStatus()); + } + + /** + * Tests that a failing stop throws an exception (i.e. exit code). + */ + public function testFailingStopThrowsException(): void { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Migration does_not_exist does not exist'); + $this->commands->stop('does_not_exist'); + } + + /** + * Tests drush mfs. + */ + public function testFieldsSource(): void { + /** @var \Consolidation\OutputFormatters\StructuredData\RowsOfFields $result */ + $result = $this->commands->fieldsSource('fruit_terms'); + $rows = $result->getArrayCopy(); + $this->assertCount(1, $rows); + $this->assertSame('name', $rows[0]['machine_name']); + $this->assertSame('name', $rows[0]['description']); + } + + /** + * Tests that a failing fields source throws an exception (i.e. exit code). + */ + public function testFailingFieldsSourceThrowsException(): void { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Migration does_not_exist does not exist'); + $this->commands->fieldsSource('does_not_exist'); + } + +} + +namespace Drupal\migrate_tools\Commands; + +/** + * Stub for drush_op. + * + * @param callable $callable + * The function to call. + */ +function drush_op(callable $callable) { + $args = func_get_args(); + array_shift($args); + return call_user_func_array($callable, $args); +} + +/** + * Stub for dt(). + * + * @param string $text + * The text. + * @param array $args + * An associative array of replacement items. + * + * @return string + * The text. + */ +function dt($text, array $args = []) { + foreach ($args as $before => $after) { + $text = str_replace($before, $after, $text); + } + return $text; +} diff --git a/web/modules/migrate_tools/tests/src/Kernel/MigrateImportTest.php b/web/modules/migrate_tools/tests/src/Kernel/MigrateImportTest.php new file mode 100644 index 0000000000000000000000000000000000000000..38b8c9d296c48f6e65e27055889947619ceb5bd6 --- /dev/null +++ b/web/modules/migrate_tools/tests/src/Kernel/MigrateImportTest.php @@ -0,0 +1,89 @@ +<?php + +namespace Drupal\Tests\migrate_tools\Kernel; + +use Drupal\migrate_tools\MigrateExecutable; +use Drupal\taxonomy\Entity\Vocabulary; +use Drupal\taxonomy\VocabularyInterface; +use Drupal\Tests\migrate\Kernel\MigrateTestBase; + +/** + * Tests imports. + * + * @group migrate + */ +class MigrateImportTest extends MigrateTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = [ + 'field', + 'system', + 'taxonomy', + 'text', + 'user', + 'system', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->installEntitySchema('user'); + $this->installEntitySchema('taxonomy_vocabulary'); + $this->installEntitySchema('taxonomy_term'); + $this->installConfig(['taxonomy']); + } + + /** + * Tests rolling back configuration and content entities. + */ + public function testImport(): void { + // We use vocabularies to demonstrate importing and rolling back + // configuration entities. + $vocabulary_data_rows = [ + ['id' => '1', 'name' => 'categories', 'weight' => '2'], + ['id' => '2', 'name' => 'tags', 'weight' => '1'], + ]; + $ids = ['id' => ['type' => 'integer']]; + $definition = [ + 'id' => 'vocabularies', + 'migration_tags' => ['Import and rollback test'], + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => $vocabulary_data_rows, + 'ids' => $ids, + ], + 'process' => [ + 'vid' => 'id', + 'name' => 'name', + 'weight' => 'weight', + ], + 'destination' => ['plugin' => 'entity:taxonomy_vocabulary'], + ]; + + /** @var \Drupal\migrate\Plugin\MigrationInterface $vocabulary_migration */ + $vocabulary_migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition); + $vocabulary_id_map = $vocabulary_migration->getIdMap(); + + // Test id list import. + $executable = new MigrateExecutable($vocabulary_migration, $this, ['idlist' => 2]); + $executable->import(); + + /** @var \Drupal\taxonomy\Entity\Vocabulary $vocabulary */ + $vocabulary = Vocabulary::load(1); + $this->assertEmpty($vocabulary); + $map_row = $vocabulary_id_map->getRowBySource(['id' => 1]); + $this->assertEmpty($map_row); + /** @var \Drupal\taxonomy\Entity\Vocabulary $vocabulary */ + $vocabulary = Vocabulary::load(2); + $this->assertInstanceOf(VocabularyInterface::class, $vocabulary); + $map_row = $vocabulary_id_map->getRowBySource(['id' => 2]); + $this->assertEqual($map_row['destid1'], $vocabulary->id()); + } + +} diff --git a/web/modules/migrate_tools/tests/src/Kernel/MigrateRollbackTest.php b/web/modules/migrate_tools/tests/src/Kernel/MigrateRollbackTest.php new file mode 100644 index 0000000000000000000000000000000000000000..202d4f54d89b91ccb8203500e57e98a483342054 --- /dev/null +++ b/web/modules/migrate_tools/tests/src/Kernel/MigrateRollbackTest.php @@ -0,0 +1,99 @@ +<?php + +namespace Drupal\Tests\migrate_tools\Kernel; + +use Drupal\migrate_tools\MigrateExecutable; +use Drupal\taxonomy\Entity\Vocabulary; +use Drupal\taxonomy\VocabularyInterface; +use Drupal\Tests\migrate\Kernel\MigrateTestBase; + +/** + * Tests rolling back of imports. + * + * @group migrate + */ +class MigrateRollbackTest extends MigrateTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = [ + 'field', + 'system', + 'taxonomy', + 'text', + 'user', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->installEntitySchema('user'); + $this->installEntitySchema('taxonomy_vocabulary'); + $this->installEntitySchema('taxonomy_term'); + $this->installConfig(['taxonomy']); + } + + /** + * Tests rolling back configuration and content entities. + */ + public function testRollback(): void { + // We use vocabularies to demonstrate importing and rolling back + // configuration entities. + $vocabulary_data_rows = [ + ['id' => '1', 'name' => 'categories', 'weight' => '2'], + ['id' => '2', 'name' => 'tags', 'weight' => '1'], + ]; + $ids = ['id' => ['type' => 'integer']]; + $definition = [ + 'id' => 'vocabularies', + 'migration_tags' => ['Import and rollback test'], + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => $vocabulary_data_rows, + 'ids' => $ids, + ], + 'process' => [ + 'vid' => 'id', + 'name' => 'name', + 'weight' => 'weight', + ], + 'destination' => ['plugin' => 'entity:taxonomy_vocabulary'], + ]; + + /** @var \Drupal\migrate\Plugin\MigrationInterface $vocabulary_migration */ + $vocabulary_migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition); + $vocabulary_id_map = $vocabulary_migration->getIdMap(); + + // Import and validate vocabulary config entities were created. + $executable = new MigrateExecutable($vocabulary_migration, $this, []); + $executable->import(); + foreach ($vocabulary_data_rows as $row) { + /** @var \Drupal\taxonomy\Entity\Vocabulary $vocabulary */ + $vocabulary = Vocabulary::load($row['id']); + $this->assertInstanceOf(VocabularyInterface::class, $vocabulary); + $map_row = $vocabulary_id_map->getRowBySource(['id' => $row['id']]); + $this->assertEqual($map_row['destid1'], $vocabulary->id()); + } + + // Test id list rollback. + $rollback_executable = new MigrateExecutable($vocabulary_migration, $this, ['idlist' => 1]); + $rollback_executable->rollback(); + /** @var \Drupal\taxonomy\Entity\Vocabulary $vocabulary */ + $vocabulary = Vocabulary::load(1); + $this->assertEmpty($vocabulary); + $map_row = $vocabulary_id_map->getRowBySource(['id' => 1]); + $this->assertEmpty($map_row); + + /** @var \Drupal\taxonomy\Entity\Vocabulary $vocabulary */ + $vocabulary = Vocabulary::load(2); + $this->assertInstanceOf(VocabularyInterface::class, $vocabulary); + $map_row = $vocabulary_id_map->getRowBySource(['id' => 2]); + $this->assertEqual($map_row['destid1'], $vocabulary->id()); + } + +} diff --git a/web/modules/migrate_tools/tests/src/Unit/MigrateToolsTest.php b/web/modules/migrate_tools/tests/src/Unit/MigrateToolsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..da243e50955ad9100e4721b1df1d006575395cde --- /dev/null +++ b/web/modules/migrate_tools/tests/src/Unit/MigrateToolsTest.php @@ -0,0 +1,85 @@ +<?php + +namespace Drupal\Tests\migrate_tools\Unit; + +use Drupal\migrate_tools\MigrateTools; +use Drupal\Tests\UnitTestCase; + +/** + * @coversDefaultClass \Drupal\migrate_tools\MigrateTools + * @group migrate_tools + */ +class MigrateToolsTest extends UnitTestCase { + + /** + * @covers ::buildIdList + * + * @dataProvider dataProviderIdList + */ + public function testBuildIdList(array $options, array $expected): void { + $results = MigrateTools::buildIdList($options); + $this->assertEquals($results, $expected); + } + + /** + * Data provider for testBuildIdList. + */ + public function dataProviderIdList(): array { + $cases = []; + $cases[] = [ + 'options' => [], + 'expected' => [], + ]; + $cases['single id'] = [ + 'options' => [ + 'idlist' => 123, + ], + 'expected' => [[123]], + ]; + $cases['multiple ids'] = [ + 'options' => [ + 'idlist' => '123, 456', + ], + 'expected' => [ + [123], [456], + ], + ]; + $cases['default delimiter, composite key'] = [ + 'options' => [ + 'idlist' => '123:456', + ], + 'expected' => [ + [123, 456], + ], + ]; + $cases['special delimiter, single'] = [ + 'options' => [ + 'idlist' => '123:456', + 'idlist-delimiter' => '~', + ], + 'expected' => [ + ['123:456'], + ], + ]; + $cases['special delimiter, multiple'] = [ + 'options' => [ + 'idlist' => '123:456~987:654', + 'idlist-delimiter' => '~', + ], + 'expected' => [ + ['123:456', '987:654'], + ], + ]; + $cases['space delimiter, multiple'] = [ + 'options' => [ + 'idlist' => '123:456 987:654', + 'idlist-delimiter' => ' ', + ], + 'expected' => [ + ['123:456', '987:654'], + ], + ]; + return $cases; + } + +}