From 91ec35d57f1437a00529ded41ca3765eacf89ea3 Mon Sep 17 00:00:00 2001 From: bcweaver <brianweaver@gmail.com> Date: Wed, 17 Oct 2018 14:10:05 -0400 Subject: [PATCH] Adding 'migrate_tools' module (and upgrade 'migrate_plus') --- composer.json | 3 +- composer.lock | 95 +- vendor/composer/installed.json | 97 +- web/modules/migrate_plus/README.txt | 71 +- web/modules/migrate_plus/composer.json | 5 +- .../schema/migrate_plus.data_types.schema.yml | 30 +- .../schema/migrate_plus.process.schema.yml | 4 +- .../config/schema/migrate_plus.schema.yml | 9 + .../migrate_plus/migrate_example/README.txt | 32 +- .../migrate_plus.migration.beer_node.yml | 6 +- .../migrate_plus.migration.beer_term.yml | 6 +- .../migrate_plus.migration.beer_user.yml | 4 +- .../migrate_example/migrate_example.info.yml | 14 +- ...play.node.migrate_example_beer.default.yml | 6 - .../node.type.migrate_example_beer.yml | 8 - .../migrate_example_setup.info.yml | 16 +- .../migrate_example_setup.install | 419 ++++--- .../beer_comment.yml} | 6 +- .../src/Plugin/migrate/source/BeerComment.php | 15 +- .../src/Plugin/migrate/source/BeerNode.php | 46 +- .../src/Plugin/migrate/source/BeerTerm.php | 58 +- .../src/Plugin/migrate/source/BeerUser.php | 45 +- .../migrate_plus.migration.weather_soap.yml | 24 +- .../migrate_plus.migration.wine_role_json.yml | 17 +- .../migrate_plus.migration.wine_role_xml.yml | 5 +- .../migrate_plus.migration.wine_terms.yml | 4 +- ...e_plus.migration.wine_variety_list.yml.txt | 7 +- ..._plus.migration.wine_variety_multi_xml.yml | 4 +- .../migrate_example_advanced.info.yml | 12 +- .../migrate_example_advanced.install | 24 +- ...urce.migrate_example_advanced_position.yml | 10 + ...migrate_example_advanced_variety_items.yml | 10 + ....migrate_example_advanced_variety_list.yml | 10 + ...rate_example_advanced_variety_multiple.yml | 10 + .../migrate_example_advanced_setup.info.yml | 16 +- .../migrate_example_advanced_setup.install | 1034 +++++++++++------ .../Plugin/rest/resource/PositionResource.php | 8 + .../src/Plugin/rest/resource/VarietyItems.php | 20 +- .../src/Plugin/rest/resource/VarietyList.php | 8 + .../rest/resource/VarietyMultiFiles.php | 20 +- .../src/Plugin/migrate/source/WineTerm.php | 13 +- .../migrate_plus/migrate_plus.info.yml | 9 +- web/modules/migrate_plus/migrate_plus.module | 18 + .../migrate_plus/migrate_plus.services.yml | 6 +- .../migrations/migration_config_deriver.yml | 6 + web/modules/migrate_plus/phpcs.xml | 207 ++++ .../src/Annotation/Authentication.php | 37 + .../src/Annotation/DataFetcher.php | 2 +- .../src/Annotation/DataParser.php | 2 +- .../src/AuthenticationPluginBase.php | 25 + .../src/AuthenticationPluginInterface.php | 25 + .../src/AuthenticationPluginManager.php | 37 + .../src/DataFetcherPluginInterface.php | 7 +- .../src/DataFetcherPluginManager.php | 2 +- .../migrate_plus/src/DataParserPluginBase.php | 13 +- .../src/DataParserPluginManager.php | 2 +- .../migrate_plus/src/Event/MigrateEvents.php | 3 +- .../src/Event/MigratePrepareRowEvent.php | 2 +- .../Discovery/ConfigEntityDiscovery.php | 49 - .../src/Plugin/MigrationConfigDeriver.php | 28 + .../MigrationConfigEntityPluginManager.php | 25 - .../src/Plugin/migrate/destination/Table.php | 143 +++ .../src/Plugin/migrate/process/ArrayPop.php | 44 + .../src/Plugin/migrate/process/ArrayShift.php | 44 + .../Plugin/migrate/process/EntityGenerate.php | 125 +- .../Plugin/migrate/process/EntityLookup.php | 157 ++- .../src/Plugin/migrate/process/FileBlob.php | 155 +++ .../src/Plugin/migrate/process/Merge.php | 68 ++ .../Plugin/migrate/process/MultipleValues.php | 58 + .../Plugin/migrate/process/SingleValue.php | 45 + .../Plugin/migrate/process/SkipOnValue.php | 135 +++ .../src/Plugin/migrate/process/StrReplace.php | 104 ++ .../migrate/process/Transliteration.php | 83 ++ .../src/Plugin/migrate/source/Url.php | 61 - .../migrate_plus/authentication/Basic.php | 30 + .../migrate_plus/authentication/Digest.php | 31 + .../migrate_plus/authentication/OAuth2.php | 69 ++ .../Plugin/migrate_plus/data_fetcher/File.php | 52 + .../Plugin/migrate_plus/data_fetcher/Http.php | 53 +- .../Plugin/migrate_plus/data_parser/Json.php | 113 +- .../migrate_plus/data_parser/SimpleXml.php | 87 ++ .../Plugin/migrate_plus/data_parser/Soap.php | 9 +- .../Plugin/migrate_plus/data_parser/Xml.php | 94 +- .../migrate_plus/data_parser/XmlTrait.php | 62 + .../tests/data/missing_properties.json | 21 + .../data/simple_xml_reduce_single_value.xml | 14 + .../tests/src/Functional/LoadTest.php | 50 + .../tests/src/Kernel/MigrateTableTest.php | 163 +++ .../src/Kernel}/MigrationConfigEntityTest.php | 29 +- .../src/Kernel}/MigrationGroupTest.php | 66 +- .../migrate/process/EntityGenerateTest.php | 788 +++++++++++++ .../migrate_plus/data_fetcher/HttpTest.php | 59 + .../migrate_plus/data_parser/JsonTest.php | 112 ++ .../data_parser/SimpleXmlTest.php | 77 ++ .../tests/src/Unit/process/ArrayPopTest.php | 71 ++ .../tests/src/Unit/process/ArrayShiftTest.php | 71 ++ .../src/Unit/process/MultipleValuesTest.php | 32 + .../src/Unit/process/SingleValueTest.php | 32 + .../src/Unit/process/SkipOnValueTest.php | 145 +++ .../tests/src/Unit/process/StrReplaceTest.php | 100 ++ .../src/Unit/process/TransliterationTest.php | 51 + web/modules/migrate_tools/LICENSE.txt | 339 ++++++ web/modules/migrate_tools/README.txt | 15 + web/modules/migrate_tools/composer.json | 27 + web/modules/migrate_tools/drush.services.yml | 6 + .../migrate_tools/migrate_tools.drush.inc | 561 +++++++++ .../migrate_tools/migrate_tools.info.yml | 16 + .../migrate_tools.links.action.yml | 11 + .../migrate_tools.links.menu.yml | 5 + .../migrate_tools.links.task.yml | 53 + .../migrate_tools/migrate_tools.module | 58 + .../migrate_tools.permissions.yml | 3 + .../migrate_tools/migrate_tools.routing.yml | 197 ++++ .../migrate_tools/migrate_tools.services.yml | 9 + web/modules/migrate_tools/phpcs.xml | 207 ++++ .../src/Commands/MigrateToolsCommands.php | 719 ++++++++++++ .../src/Controller/MessageController.php | 144 +++ .../src/Controller/MigrationController.php | 300 +++++ .../Controller/MigrationGroupListBuilder.php | 68 ++ .../src/Controller/MigrationListBuilder.php | 244 ++++ .../src/Drush9LogMigrateMessage.php | 48 + .../src/DrushLogMigrateMessage.php | 28 + .../src/Form/MigrationAddForm.php | 37 + .../src/Form/MigrationDeleteForm.php | 71 ++ .../src/Form/MigrationEditForm.php | 50 + .../src/Form/MigrationExecuteForm.php | 212 ++++ .../src/Form/MigrationFormBase.php | 184 +++ .../src/Form/MigrationGroupAddForm.php | 37 + .../src/Form/MigrationGroupDeleteForm.php | 71 ++ .../src/Form/MigrationGroupEditForm.php | 35 + .../src/Form/MigrationGroupFormBase.php | 172 +++ .../migrate_tools/src/Form/SourceCsvForm.php | 453 ++++++++ .../src/MigrateBatchExecutable.php | 295 +++++ .../migrate_tools/src/MigrateExecutable.php | 395 +++++++ .../src/Routing/RouteProcessor.php | 27 + ...migrate_plus.migration.csv_source_test.yml | 33 + .../migrate_plus.migration_group.csv_test.yml | 4 + .../csv_source_test/csv_source_test.info.yml | 15 + .../migrate_plus.migration.fruit_terms.yml | 32 + .../migrate_plus.migration_group.default.yml | 9 + .../migrate_tools_test.info.yml | 14 + .../Functional/MigrateExecutionFormTest.php | 132 +++ .../src/Functional/SourceCsvFormTest.php | 241 ++++ 143 files changed, 10940 insertions(+), 1096 deletions(-) rename web/modules/migrate_plus/migrate_example/{config/install/migrate_plus.migration.beer_comment.yml => migrations/beer_comment.yml} (90%) create mode 100644 web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/rest.resource.migrate_example_advanced_position.yml create mode 100644 web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/rest.resource.migrate_example_advanced_variety_items.yml create mode 100644 web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/rest.resource.migrate_example_advanced_variety_list.yml create mode 100644 web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/rest.resource.migrate_example_advanced_variety_multiple.yml create mode 100644 web/modules/migrate_plus/migrations/migration_config_deriver.yml create mode 100644 web/modules/migrate_plus/phpcs.xml create mode 100644 web/modules/migrate_plus/src/Annotation/Authentication.php create mode 100644 web/modules/migrate_plus/src/AuthenticationPluginBase.php create mode 100644 web/modules/migrate_plus/src/AuthenticationPluginInterface.php create mode 100644 web/modules/migrate_plus/src/AuthenticationPluginManager.php delete mode 100644 web/modules/migrate_plus/src/Plugin/Discovery/ConfigEntityDiscovery.php create mode 100644 web/modules/migrate_plus/src/Plugin/MigrationConfigDeriver.php delete mode 100644 web/modules/migrate_plus/src/Plugin/MigrationConfigEntityPluginManager.php create mode 100644 web/modules/migrate_plus/src/Plugin/migrate/destination/Table.php create mode 100644 web/modules/migrate_plus/src/Plugin/migrate/process/ArrayPop.php create mode 100644 web/modules/migrate_plus/src/Plugin/migrate/process/ArrayShift.php create mode 100644 web/modules/migrate_plus/src/Plugin/migrate/process/FileBlob.php create mode 100644 web/modules/migrate_plus/src/Plugin/migrate/process/Merge.php create mode 100644 web/modules/migrate_plus/src/Plugin/migrate/process/MultipleValues.php create mode 100644 web/modules/migrate_plus/src/Plugin/migrate/process/SingleValue.php create mode 100644 web/modules/migrate_plus/src/Plugin/migrate/process/SkipOnValue.php create mode 100644 web/modules/migrate_plus/src/Plugin/migrate/process/StrReplace.php create mode 100644 web/modules/migrate_plus/src/Plugin/migrate/process/Transliteration.php create mode 100644 web/modules/migrate_plus/src/Plugin/migrate_plus/authentication/Basic.php create mode 100644 web/modules/migrate_plus/src/Plugin/migrate_plus/authentication/Digest.php create mode 100644 web/modules/migrate_plus/src/Plugin/migrate_plus/authentication/OAuth2.php create mode 100644 web/modules/migrate_plus/src/Plugin/migrate_plus/data_fetcher/File.php create mode 100644 web/modules/migrate_plus/src/Plugin/migrate_plus/data_parser/SimpleXml.php create mode 100644 web/modules/migrate_plus/src/Plugin/migrate_plus/data_parser/XmlTrait.php create mode 100644 web/modules/migrate_plus/tests/data/missing_properties.json create mode 100644 web/modules/migrate_plus/tests/data/simple_xml_reduce_single_value.xml create mode 100644 web/modules/migrate_plus/tests/src/Functional/LoadTest.php create mode 100644 web/modules/migrate_plus/tests/src/Kernel/MigrateTableTest.php rename web/modules/migrate_plus/{src/Tests => tests/src/Kernel}/MigrationConfigEntityTest.php (50%) rename web/modules/migrate_plus/{src/Tests => tests/src/Kernel}/MigrationGroupTest.php (65%) create mode 100644 web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate/process/EntityGenerateTest.php create mode 100644 web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_fetcher/HttpTest.php create mode 100644 web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_parser/JsonTest.php create mode 100644 web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_parser/SimpleXmlTest.php create mode 100644 web/modules/migrate_plus/tests/src/Unit/process/ArrayPopTest.php create mode 100644 web/modules/migrate_plus/tests/src/Unit/process/ArrayShiftTest.php create mode 100644 web/modules/migrate_plus/tests/src/Unit/process/MultipleValuesTest.php create mode 100644 web/modules/migrate_plus/tests/src/Unit/process/SingleValueTest.php create mode 100644 web/modules/migrate_plus/tests/src/Unit/process/SkipOnValueTest.php create mode 100644 web/modules/migrate_plus/tests/src/Unit/process/StrReplaceTest.php create mode 100644 web/modules/migrate_plus/tests/src/Unit/process/TransliterationTest.php create mode 100644 web/modules/migrate_tools/LICENSE.txt create mode 100644 web/modules/migrate_tools/README.txt create mode 100644 web/modules/migrate_tools/composer.json create mode 100644 web/modules/migrate_tools/drush.services.yml create mode 100644 web/modules/migrate_tools/migrate_tools.drush.inc create mode 100644 web/modules/migrate_tools/migrate_tools.info.yml create mode 100644 web/modules/migrate_tools/migrate_tools.links.action.yml create mode 100644 web/modules/migrate_tools/migrate_tools.links.menu.yml create mode 100644 web/modules/migrate_tools/migrate_tools.links.task.yml create mode 100644 web/modules/migrate_tools/migrate_tools.module create mode 100644 web/modules/migrate_tools/migrate_tools.permissions.yml create mode 100644 web/modules/migrate_tools/migrate_tools.routing.yml create mode 100644 web/modules/migrate_tools/migrate_tools.services.yml create mode 100644 web/modules/migrate_tools/phpcs.xml create mode 100644 web/modules/migrate_tools/src/Commands/MigrateToolsCommands.php create mode 100644 web/modules/migrate_tools/src/Controller/MessageController.php create mode 100644 web/modules/migrate_tools/src/Controller/MigrationController.php create mode 100644 web/modules/migrate_tools/src/Controller/MigrationGroupListBuilder.php create mode 100644 web/modules/migrate_tools/src/Controller/MigrationListBuilder.php create mode 100644 web/modules/migrate_tools/src/Drush9LogMigrateMessage.php create mode 100644 web/modules/migrate_tools/src/DrushLogMigrateMessage.php create mode 100644 web/modules/migrate_tools/src/Form/MigrationAddForm.php create mode 100644 web/modules/migrate_tools/src/Form/MigrationDeleteForm.php create mode 100644 web/modules/migrate_tools/src/Form/MigrationEditForm.php create mode 100644 web/modules/migrate_tools/src/Form/MigrationExecuteForm.php create mode 100644 web/modules/migrate_tools/src/Form/MigrationFormBase.php create mode 100644 web/modules/migrate_tools/src/Form/MigrationGroupAddForm.php create mode 100644 web/modules/migrate_tools/src/Form/MigrationGroupDeleteForm.php create mode 100644 web/modules/migrate_tools/src/Form/MigrationGroupEditForm.php create mode 100644 web/modules/migrate_tools/src/Form/MigrationGroupFormBase.php create mode 100644 web/modules/migrate_tools/src/Form/SourceCsvForm.php create mode 100644 web/modules/migrate_tools/src/MigrateBatchExecutable.php create mode 100644 web/modules/migrate_tools/src/MigrateExecutable.php create mode 100644 web/modules/migrate_tools/src/Routing/RouteProcessor.php create mode 100644 web/modules/migrate_tools/tests/modules/csv_source_test/config/install/migrate_plus.migration.csv_source_test.yml create mode 100644 web/modules/migrate_tools/tests/modules/csv_source_test/config/install/migrate_plus.migration_group.csv_test.yml create mode 100644 web/modules/migrate_tools/tests/modules/csv_source_test/csv_source_test.info.yml create mode 100644 web/modules/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration.fruit_terms.yml create mode 100644 web/modules/migrate_tools/tests/modules/migrate_tools_test/config/install/migrate_plus.migration_group.default.yml create mode 100644 web/modules/migrate_tools/tests/modules/migrate_tools_test/migrate_tools_test.info.yml create mode 100644 web/modules/migrate_tools/tests/src/Functional/MigrateExecutionFormTest.php create mode 100644 web/modules/migrate_tools/tests/src/Functional/SourceCsvFormTest.php diff --git a/composer.json b/composer.json index 17e5f17ba4..3cb96d7820 100644 --- a/composer.json +++ b/composer.json @@ -121,7 +121,8 @@ "drupal/media_entity_browser": "2.0-alpha1", "drupal/media_entity_twitter": "2.0-alpha2", "drupal/menu_block": "1.4", - "drupal/migrate_plus": "^2.0", + "drupal/migrate_plus": "4.0", + "drupal/migrate_tools": "4.0", "drupal/nice_menus": "1.0-beta2", "drupal/paragraphs": "1.3", "drupal/pathauto": "1.0", diff --git a/composer.lock b/composer.lock index 7fa6e4bd7a..3309f1b75c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "549076b42f1930243fcc38aef6b64e85", + "content-hash": "1765d49ab9251383f8e8c2d4b540106a", "packages": [ { "name": "alchemy/zippy", @@ -4971,36 +4971,40 @@ }, { "name": "drupal/migrate_plus", - "version": "2.0.0-beta2", + "version": "4.0.0", "source": { "type": "git", "url": "https://git.drupal.org/project/migrate_plus", - "reference": "8.x-2.0-beta2" + "reference": "8.x-4.0" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/migrate_plus-8.x-2.0-beta2.zip", - "reference": "8.x-2.0-beta2", - "shasum": "0b29113a3c00c7ab3ba73e811da3450edca4c0bb" + "url": "https://ftp.drupal.org/files/projects/migrate_plus-8.x-4.0.zip", + "reference": "8.x-4.0", + "shasum": "63dad289defe8298aa5ca5e30062fe9761d19eca" }, "require": { - "drupal/core": "^8.1" + "drupal/core": "^8.3" }, "require-dev": { "drupal/migrate_example_advanced_setup": "*", "drupal/migrate_example_setup": "*" }, + "suggest": { + "ext-soap": "*", + "sainsburys/guzzle-oauth2-plugin": "3.0 required for the OAuth2 authentication plugin" + }, "type": "drupal-module", "extra": { "branch-alias": { - "dev-2.x": "2.x-dev" + "dev-4.x": "4.x-dev" }, "drupal": { - "version": "8.x-2.0-beta2", - "datestamp": "1476307439", + "version": "8.x-4.0", + "datestamp": "1536264180", "security-coverage": { - "status": "not-covered", - "message": "Beta releases are not covered by Drupal security advisories." + "status": "covered", + "message": "Covered by Drupal's security advisory policy" } } }, @@ -5027,6 +5031,73 @@ "irc": "irc://irc.freenode.org/drupal-migrate" } }, + { + "name": "drupal/migrate_tools", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://git.drupal.org/project/migrate_tools", + "reference": "8.x-4.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" + }, + "require": { + "drupal/core": "^8.3", + "drupal/migrate_plus": "^4" + }, + "require-dev": { + "drupal/coder": "^8", + "drupal/migrate_source_csv": "^2.2" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-4.x": "4.x-dev" + }, + "drupal": { + "version": "8.x-4.0", + "datestamp": "1535380084", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + }, + "drush": { + "services": { + "drush.services.yml": "^9" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "heddn", + "homepage": "https://www.drupal.org/user/1463982" + }, + { + "name": "mikeryan", + "homepage": "https://www.drupal.org/user/4420" + }, + { + "name": "moshe weitzman", + "homepage": "https://www.drupal.org/user/23" + } + ], + "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" + } + }, { "name": "drupal/nice_menus", "version": "1.0.0-beta2", diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 15b97d7f3c..87c4b83ce1 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -5125,37 +5125,41 @@ }, { "name": "drupal/migrate_plus", - "version": "2.0.0-beta2", - "version_normalized": "2.0.0.0-beta2", + "version": "4.0.0", + "version_normalized": "4.0.0.0", "source": { "type": "git", "url": "https://git.drupal.org/project/migrate_plus", - "reference": "8.x-2.0-beta2" + "reference": "8.x-4.0" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/migrate_plus-8.x-2.0-beta2.zip", - "reference": "8.x-2.0-beta2", - "shasum": "0b29113a3c00c7ab3ba73e811da3450edca4c0bb" + "url": "https://ftp.drupal.org/files/projects/migrate_plus-8.x-4.0.zip", + "reference": "8.x-4.0", + "shasum": "63dad289defe8298aa5ca5e30062fe9761d19eca" }, "require": { - "drupal/core": "^8.1" + "drupal/core": "^8.3" }, "require-dev": { "drupal/migrate_example_advanced_setup": "*", "drupal/migrate_example_setup": "*" }, + "suggest": { + "ext-soap": "*", + "sainsburys/guzzle-oauth2-plugin": "3.0 required for the OAuth2 authentication plugin" + }, "type": "drupal-module", "extra": { "branch-alias": { - "dev-2.x": "2.x-dev" + "dev-4.x": "4.x-dev" }, "drupal": { - "version": "8.x-2.0-beta2", - "datestamp": "1476307439", + "version": "8.x-4.0", + "datestamp": "1536264180", "security-coverage": { - "status": "not-covered", - "message": "Beta releases are not covered by Drupal security advisories." + "status": "covered", + "message": "Covered by Drupal's security advisory policy" } } }, @@ -5183,6 +5187,75 @@ "irc": "irc://irc.freenode.org/drupal-migrate" } }, + { + "name": "drupal/migrate_tools", + "version": "4.0.0", + "version_normalized": "4.0.0.0", + "source": { + "type": "git", + "url": "https://git.drupal.org/project/migrate_tools", + "reference": "8.x-4.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" + }, + "require": { + "drupal/core": "^8.3", + "drupal/migrate_plus": "^4" + }, + "require-dev": { + "drupal/coder": "^8", + "drupal/migrate_source_csv": "^2.2" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-4.x": "4.x-dev" + }, + "drupal": { + "version": "8.x-4.0", + "datestamp": "1535380084", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + }, + "drush": { + "services": { + "drush.services.yml": "^9" + } + } + }, + "installation-source": "dist", + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "heddn", + "homepage": "https://www.drupal.org/user/1463982" + }, + { + "name": "mikeryan", + "homepage": "https://www.drupal.org/user/4420" + }, + { + "name": "moshe weitzman", + "homepage": "https://www.drupal.org/user/23" + } + ], + "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" + } + }, { "name": "drupal/nice_menus", "version": "1.0.0-beta2", diff --git a/web/modules/migrate_plus/README.txt b/web/modules/migrate_plus/README.txt index 8e46d4ced4..797082e388 100644 --- a/web/modules/migrate_plus/README.txt +++ b/web/modules/migrate_plus/README.txt @@ -4,26 +4,69 @@ and additional functionality, as well as providing practical examples. Extensions to base API ====================== * A Migration configuration entity is provided, enabling persistance of dynamic -migration configuration. -* A ConfigEntityDiscovery class is implemented which enables plugin configuration -to be based on configuration entities. This is fully general - it can be used -for any configuration entity type, not just migrations. -* A MigrationConfigEntityPluginManager class and corresponding -plugin.manager.config_entity_migration service is provided, to enable discovery -and instantiation of migration plugins based on the Migration configuration -entity. + migration configuration. * A MigrationGroup configuration entity is provided, which enables migrations to -be organized in groups, and to maintain shared configuration in one place. + 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. + 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. + IDs for a source plugin via configuration rather than requiring PHP code. + +Plugin types +============ +migrate_plus provides the following plugin types, for use with the url source +plugin. + +* A data_parser type, for parsing different formats on behalf of the url source + plugin. +* A data_fetcher type, for fetching data to feed into a data_parser plugin. +* An authentication type, for adding authentication headers with the http + data_fetcher plugin. Plugins ======= -* A Url source plugin is provided, implementing a common structure for -file-based data providers. -* XML and JSON fetchers and parsers for the Url source plugin are provided. + +Process +------- +* The entity_lookup process plugin allows you to populate references to entities + which already exist in Drupal, whether they were migrated or not. +* The entity_generate process plugin extends entity_lookup to also create the + desired entity when it doesn't already exist. +* The file_blob process plugin supports creating file entities from blob data. +* The merge process plugin allows the merging of multiple arrays into a single + field. +* The skip_on_value process plugin allows you to skip a row, or a given field, + for specific source values. + +Source +------ +* A url source plugin is provided, implementing a common structure for + file-based data providers. + +Data parsers +------------ +* The xml parser plugin uses PHP's XMLReader interface to incrementally parse + XML files. This should be used for XML sources which are potentially very + large. +* The simple_xml parser plugin uses PHP's SimpleXML interface to fully parse + XML files. This should be used for XML sources where you need to be able to + use complex xpaths for your item selectors, or have to access elements outside + of the current item element via xpaths. +* The json parser plugin supports JSON sources. +* The soap parser plugin supports SOAP sources. + +Data fetchers +------------- +* The file fetcher plugin works for most URLs regardless of protocol, as well as + local filesystems. +* The http fetcher plugin provides the ability to add headers to an HTTP + request (particularly through authentication plugins). + +Authentication +-------------- +* The basic authentication plugin provides HTTP Basic authentication. +* The digest authentication plugin provides HTTP Digest authentication. +* The oauth2 authentication plugin provides OAuth2 authentication over HTTP. Examples ======== diff --git a/web/modules/migrate_plus/composer.json b/web/modules/migrate_plus/composer.json index c4ec1be44f..54070eda95 100644 --- a/web/modules/migrate_plus/composer.json +++ b/web/modules/migrate_plus/composer.json @@ -17,5 +17,8 @@ "source": "https://cgit.drupalcode.org/migrate_plus" }, "minimum-stability": "dev", - "require": {} + "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.data_types.schema.yml b/web/modules/migrate_plus/config/schema/migrate_plus.data_types.schema.yml index 27c8006f4a..92e8288ec1 100644 --- a/web/modules/migrate_plus/config/schema/migrate_plus.data_types.schema.yml +++ b/web/modules/migrate_plus/config/schema/migrate_plus.data_types.schema.yml @@ -6,7 +6,6 @@ migrate_plugin: plugin: type: string label: 'Plugin' - migrate_destination: type: migrate_plugin label: 'Destination' @@ -25,6 +24,35 @@ migrate_source: constants: type: ignore label: 'Constants' + ids: + type: ignore + label: 'Source IDs schema definition for migrate mapping table' + urls: + type: sequence + label: 'URLs from which to fetch' + sequence: + type: string + data_fetcher_plugin: + type: string + label: 'Fetcher plugin' + data_parser_plugin: + type: string + label: 'Parser plugin' + fields: + type: ignore + label: Mapping of field names to selectors + function: + type: string + label: 'Function to call on the service' + parameters: + type: ignore + label: 'Parameters to pass to function on the service' + response_type: + type: string + label: 'Type of response; XML string, object or array' + item_selector: + type: string + label: 'XPath selector' migrate_process: type: migrate_plugin diff --git a/web/modules/migrate_plus/config/schema/migrate_plus.process.schema.yml b/web/modules/migrate_plus/config/schema/migrate_plus.process.schema.yml index b1f123bc33..7b90a3d4ed 100644 --- a/web/modules/migrate_plus/config/schema/migrate_plus.process.schema.yml +++ b/web/modules/migrate_plus/config/schema/migrate_plus.process.schema.yml @@ -71,9 +71,9 @@ migrate_plus.process.get: type: string label: 'Source key' -migrate_plus.process.iterator: +migrate_plus.process.sub_process: type: migrate_process - label: 'Iterator process' + label: 'Sub process' mapping: process: type: ignore 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 304e4cc3b3..99aef55238 100644 --- a/web/modules/migrate_plus/config/schema/migrate_plus.schema.yml +++ b/web/modules/migrate_plus/config/schema/migrate_plus.schema.yml @@ -7,6 +7,15 @@ migrate_plus.migration.*: id: type: string label: 'ID' + class: + type: string + label: 'Class' + field_plugin_method: + type: string + label: 'Field Plugin Method' + cck_plugin_method: + type: string + label: 'BC layer for Field Plugin Method' migration_tags: type: sequence label: 'Migration Tags' diff --git a/web/modules/migrate_plus/migrate_example/README.txt b/web/modules/migrate_plus/migrate_example/README.txt index 125e3b0f63..431e22e9ba 100644 --- a/web/modules/migrate_plus/migrate_example/README.txt +++ b/web/modules/migrate_plus/migrate_example/README.txt @@ -22,12 +22,30 @@ STRUCTURE --------- There are two primary components to this example: -1. Migration configuration, in the config/install directory. These YAML files - describe the migration process and provide the mappings from the source data - to Drupal's destination entities. The YAML file names are prefixed with - 'migrate_plus.migration.' (because, reading from right to left, they define - "migration" configuration entities, and the configuration entity type is - defined by the "migrate_plus" module). +1. Migration configuration, in the migrations and config/install directories. + These YAML files describe the migration process and provide the mappings from + the source data to Drupal's destination entities. The difference between the + two possible directories: + + a. Files in the migrations directory provide configuration directly for the + migration plugins. The filenames are of the form <migration ID>.yml. This + approach is recommended when your migration configuration is fully hardcoded + and does not need to be overridden (e.g., you don't need to change the URL to + a source web service through an admin UI). While developing migrations, + changes to these files require at most a 'drush cr' to load your changes. + + b. Files in the config/install directory provide migration configuration as + configuration entities, and have names of the form + migrate_plus.migration.<migration ID>.yml ("migration" because they define + entities of the "migration" type, and "migrate_plus" because that is the + module which implements the "migration" type). Migrations defined in this way + may have their configuration modified (in particular, through a web UI) by + loading the configuration entity, modifying its configuration, and saving the + entity. When developing, to get edits to the .yml files in config/install to + take effect in active configuration, use the config_devel module. + + Configuration in either type of file is identical - the only differences are + the directories and filenames. 2. Source plugins, in src/Plugin/migrate/source. These are referenced from the configuration files, and provide the source data to the migration processing @@ -47,7 +65,7 @@ read the files in the following order: 5. BeerUser.php 6. migrate_plus.migration.beer_node.yml 7. BeerNode.php -8. migrate_plus.migration.beer_comment.yml +8. beer_comment.yml 9. BeerComment.php RUNNING THE MIGRATIONS diff --git a/web/modules/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_node.yml b/web/modules/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_node.yml index e2a9aefcb3..7494d1f408 100644 --- a/web/modules/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_node.yml +++ b/web/modules/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_node.yml @@ -2,6 +2,8 @@ id: beer_node label: Beers of the world migration_group: beer +migration_tags: + - example source: plugin: beer_node destination: @@ -14,7 +16,7 @@ process: title: name nid: bid uid: - plugin: migration + plugin: migration_lookup migration: beer_user source: aid sticky: @@ -22,7 +24,7 @@ process: default_value: 0 field_migrate_example_country: countries field_migrate_example_beer_style: - plugin: migration + plugin: migration_lookup migration: beer_term source: terms # Some Drupal fields may have multiple components we may want to set diff --git a/web/modules/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_term.yml b/web/modules/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_term.yml index a7ca1858cb..c091b63d79 100644 --- a/web/modules/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_term.yml +++ b/web/modules/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_term.yml @@ -12,6 +12,10 @@ label: Migrate style categories from the source database to taxonomy terms # configuration to be merged with our own configuration here). migration_group: beer +# The category or tag for the migration. +migration_tags: + - example + # Every migration must have a source plugin, which controls the delivery of our # source data. In this case, our source plugin has the name "beer_term", which # Drupal resolves to the PHP class defined in @@ -60,7 +64,7 @@ process: # IDs in map tables, and the migration plugin is the means of performing a # lookup in those map tables during processing. parent: - plugin: migration + plugin: migration_lookup # Here we reference the migration whose map table we're performing a lookup # against. You'll note that in this case we're actually referencing this # migration itself, since category parents are imported by the same diff --git a/web/modules/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_user.yml b/web/modules/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_user.yml index 66b08682bd..d31b7253e7 100644 --- a/web/modules/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_user.yml +++ b/web/modules/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_user.yml @@ -4,6 +4,8 @@ id: beer_user label: Beer Drinkers of the world migration_group: beer +migration_tags: + - example source: plugin: beer_user destination: @@ -90,7 +92,7 @@ process: # when that migration runs it knows that each incoming beer should overwrite # its stub instead of creating a new node. field_migrate_example_favbeers: - plugin: migration + plugin: migration_lookup source: beers migration: beer_node 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 cf80ea9c69..5ff995788d 100644 --- a/web/modules/migrate_plus/migrate_example/migrate_example.info.yml +++ b/web/modules/migrate_plus/migrate_example/migrate_example.info.yml @@ -4,12 +4,14 @@ description: 'Examples of how Drupal 8 migration compares to previous versions.' package: Examples # core: 8.x dependencies: - - migrate - - migrate_example_setup - - migrate_plus + - drupal:migrate + - migrate_plus:migrate_example_setup + - migrate_plus:migrate_plus + - drupal:menu_ui + - drupal:path -# Information added by Drupal.org packaging script on 2016-08-05 -version: '8.x-2.0-beta2' +# Information added by Drupal.org packaging script on 2018-09-06 +version: '8.x-4.0' core: '8.x' project: 'migrate_plus' -datestamp: 1470428640 +datestamp: 1536264189 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 18ad64c0d1..306af0e3fe 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 @@ -11,7 +11,6 @@ dependencies: module: - comment - image - - path - text id: node.migrate_example_beer.default targetEntityType: node @@ -58,11 +57,6 @@ content: preview_image_style: thumbnail third_party_settings: { } type: image_image - path: - type: path - weight: 5 - settings: { } - third_party_settings: { } promote: type: boolean_checkbox settings: diff --git a/web/modules/migrate_plus/migrate_example/migrate_example_setup/config/install/node.type.migrate_example_beer.yml b/web/modules/migrate_plus/migrate_example/migrate_example_setup/config/install/node.type.migrate_example_beer.yml index afa605210e..24c2e1da59 100644 --- a/web/modules/migrate_plus/migrate_example/migrate_example_setup/config/install/node.type.migrate_example_beer.yml +++ b/web/modules/migrate_plus/migrate_example/migrate_example_setup/config/install/node.type.migrate_example_beer.yml @@ -1,13 +1,5 @@ langcode: en status: true -dependencies: - module: - - menu_ui -third_party_settings: - menu_ui: - available_menus: - - main - parent: 'main:' name: Beer type: migrate_example_beer description: 'Beer is what we drink.' 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 6b305596ca..b9dd2672cd 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 @@ -5,14 +5,14 @@ package: Migration # core: 8.x hidden: 1 dependencies: - - comment - - image - - text - - options - - taxonomy + - drupal:comment + - drupal:image + - drupal:text + - drupal:options + - drupal:taxonomy -# Information added by Drupal.org packaging script on 2016-08-05 -version: '8.x-2.0-beta2' +# Information added by Drupal.org packaging script on 2018-09-06 +version: '8.x-4.0' core: '8.x' project: 'migrate_plus' -datestamp: 1470428640 +datestamp: 1536264189 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 8e450baf08..342bc2dcfc 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 @@ -2,11 +2,16 @@ /** * @file + * Install file for migrate example module. + * * Set up source data and destination configuration for the migration example * module. We do this in a separate module so migrate_example itself is a pure * migration module. */ +/** + * Implements hook_schema(). + */ function migrate_example_setup_schema() { $schema['migrate_example_beer_account'] = migrate_example_beer_schema_account(); $schema['migrate_example_beer_node'] = migrate_example_beer_schema_node(); @@ -17,6 +22,9 @@ function migrate_example_setup_schema() { return $schema; } +/** + * Implements hook_install(). + */ function migrate_example_setup_install() { // Populate our tables. migrate_example_beer_data_account(); @@ -26,318 +34,467 @@ function migrate_example_setup_install() { migrate_example_beer_data_topic_node(); } +/** + * The hook_schema definition for node. + * + * @return array + * The schema definition. + */ function migrate_example_beer_schema_node() { - return array( + return [ 'description' => 'Beers of the world.', - 'fields' => array( - 'bid' => array( + 'fields' => [ + 'bid' => [ 'type' => 'serial', 'not null' => TRUE, 'description' => 'Beer ID.', - ), - 'name' => array( + ], + 'name' => [ 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, - ), - 'body' => array( + ], + 'body' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Full description of the beer.', - ), - 'excerpt' => array( + ], + 'excerpt' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Abstract for this beer.', - ), - 'countries' => array( + ], + 'countries' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Countries of origin. Multiple values, delimited by pipe', - ), - 'aid' => array( + ], + 'aid' => [ 'type' => 'int', 'not null' => FALSE, 'description' => 'Account Id of the author.', - ), - 'image' => array( + ], + 'image' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Image path', - ), - 'image_alt' => array( + ], + 'image_alt' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Image ALT', - ), - 'image_title' => array( + ], + 'image_title' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Image title', - ), - 'image_description' => array( + ], + 'image_description' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Image description', - ), - ), - 'primary key' => array('bid'), - ); + ], + ], + 'primary key' => ['bid'], + ]; } +/** + * The hook_schema definition for topic. + * + * @return array + * The schema definition. + */ function migrate_example_beer_schema_topic() { - return array( + return [ 'description' => 'Categories', - 'fields' => array( - 'style' => array( + 'fields' => [ + 'style' => [ 'type' => 'varchar_ascii', 'length' => 255, 'not null' => TRUE, - ), - 'details' => array( + ], + 'details' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, - ), - 'style_parent' => array( + ], + 'style_parent' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Parent topic, if any', - ), - 'region' => array( + ], + 'region' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Region first associated with this style', - ), - 'hoppiness' => array( + ], + 'hoppiness' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Relative hoppiness of the beer', - ), - ), - 'primary key' => array('style'), - ); + ], + ], + 'primary key' => ['style'], + ]; } +/** + * The hook_schema definition for topic node. + * + * @return array + * The schema definition. + */ function migrate_example_beer_schema_topic_node() { - return array( + return [ 'description' => 'Beers topic pairs.', - 'fields' => array( - 'bid' => array( + 'fields' => [ + 'bid' => [ 'type' => 'int', 'not null' => TRUE, 'description' => 'Beer ID.', - ), - 'style' => array( + ], + 'style' => [ 'type' => 'varchar_ascii', 'length' => 255, 'not null' => TRUE, 'description' => 'Topic name', - ), - ), - 'primary key' => array('style', 'bid'), - ); + ], + ], + 'primary key' => ['style', 'bid'], + ]; } +/** + * The hook_schema definition for comment. + * + * @return array + * The schema definition. + */ function migrate_example_beer_schema_comment() { - return array( + return [ 'description' => 'Beers comments.', - 'fields' => array( - 'cid' => array( + 'fields' => [ + 'cid' => [ 'type' => 'serial', 'not null' => TRUE, 'description' => 'Comment ID.', - ), - 'bid' => array( + ], + 'bid' => [ 'type' => 'int', 'not null' => TRUE, 'description' => 'Beer ID that is being commented upon', - ), - 'cid_parent' => array( + ], + 'cid_parent' => [ 'type' => 'int', 'not null' => FALSE, 'description' => 'Parent comment ID in case of comment replies.', - ), - 'subject' => array( + ], + 'subject' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Comment subject', - ), - 'body' => array( + ], + 'body' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Comment body', - ), - 'name' => array( + ], + 'name' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Comment name (if anon)', - ), - 'mail' => array( + ], + 'mail' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Comment email (if anon)', - ), - 'aid' => array( + ], + 'aid' => [ 'type' => 'int', 'not null' => FALSE, 'description' => 'Account ID (if any).', - ), - ), - 'primary key' => array('cid'), - ); + ], + ], + 'primary key' => ['cid'], + ]; } +/** + * The hook_schema definition for account. + * + * @return array + * The schema definition. + */ function migrate_example_beer_schema_account() { - return array( + return [ 'description' => 'Beers accounts.', - 'fields' => array( - 'aid' => array( + 'fields' => [ + 'aid' => [ 'type' => 'serial', - //'not null' => TRUE, + 'not null' => TRUE, 'description' => 'Account ID', - ), - 'status' => array( + ], + 'status' => [ 'type' => 'int', 'not null' => TRUE, 'description' => 'Blocked_Allowed', - ), - 'registered' => array( + ], + 'registered' => [ 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'description' => 'Registration date', - ), - 'username' => array( + ], + 'username' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Account name (for login)', - ), - 'nickname' => array( + ], + 'nickname' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Account name (for display)', - ), - 'password' => array( + ], + 'password' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Account password (raw)', - ), - 'email' => array( + ], + 'email' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Account email', - ), - 'sex' => array( + ], + 'sex' => [ 'type' => 'int', 'not null' => FALSE, 'description' => 'Gender (0 for male, 1 for female)', - ), - 'beers' => array( + ], + 'beers' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Favorite Beers', - ), - ), - 'primary key' => array('aid'), - ); + ], + ], + 'primary key' => ['aid'], + ]; } +/** + * Populate node table. + */ function migrate_example_beer_data_node() { - $fields = array('bid', 'name', 'body', 'excerpt', 'countries', 'aid', 'image', - 'image_alt', 'image_title', 'image_description'); + $fields = [ + 'bid', + 'name', + 'body', + 'excerpt', + 'countries', + 'aid', + 'image', + 'image_alt', + 'image_title', + 'image_description', + ]; $query = db_insert('migrate_example_beer_node') - ->fields($fields); + ->fields($fields); // Use high bid numbers to avoid overwriting an existing node id. - $data = array( - array(99999999, 'Heineken', 'Blab Blah Blah Green', 'Green', 'Netherlands|Belgium', 0, 'heineken.jpg', 'Heinekin alt', 'Heinekin title', 'Heinekin description'), // comes with migrate_example project. - array(99999998, 'Miller Lite', 'We love Miller Brewing', 'Tasteless', 'USA|Canada', 1, NULL, NULL, NULL, NULL), - array(99999997, 'Boddington', 'English occasionally get something right', 'A treat', 'United Kingdom', 1, NULL, NULL, NULL, NULL), - ); + $data = [ + // Comes with migrate_example project. + [ + 99999999, + 'Heineken', + 'Blab Blah Blah Green', + 'Green', + 'Netherlands|Belgium', + 0, + 'heineken.jpg', + 'Heinekin alt', + 'Heinekin title', + 'Heinekin description', + ], + [ + 99999998, + 'Miller Lite', + 'We love Miller Brewing', + 'Tasteless', + 'USA|Canada', + 1, + NULL, + NULL, + NULL, + NULL, + ], + [ + 99999997, + 'Boddington', + 'English occasionally get something right', + 'A treat', + 'United Kingdom', + 1, + NULL, + NULL, + NULL, + NULL, + ], + ]; foreach ($data as $row) { $query->values(array_combine($fields, $row)); } $query->execute(); } -// Note that alice has duplicate username. Exercises dedupe_entity plugin. -// @TODO duplicate email also. +/** + * Populate account table. + * + * Note that alice has duplicate username. Exercises dedupe_entity plugin. + * TODO duplicate email also. + */ function migrate_example_beer_data_account() { - $fields = array('status', 'registered', 'username', 'nickname', 'password', 'email', 'sex', 'beers'); + $fields = [ + 'status', + 'registered', + 'username', + 'nickname', + 'password', + 'email', + 'sex', + 'beers', + ]; $query = db_insert('migrate_example_beer_account') ->fields($fields); - $data = array( - array(1, '2010-03-30 10:31:05', 'alice', 'alice in beerland', 'alicepass', 'alice@example.com', '1', '99999999|99999998|99999997'), - array(1, '2010-04-04 10:31:05', 'alice', 'alice in aleland', 'alicepass', 'alice2@example.com', '1', '99999999|99999998|99999997'), - array(0, '2007-03-15 10:31:05', 'bob', 'rebob', 'bobpass', 'bob@example.com', '0', '99999999|99999997'), - array(1, '2004-02-29 10:31:05', 'charlie', 'charlie chocolate', 'mykids', 'charlie@example.com', '0', '99999999|99999998'), - ); + $data = [ + [ + 1, + '2010-03-30 10:31:05', + 'alice', + 'alice in beerland', + 'alicepass', + 'alice@example.com', + '1', + '99999999|99999998|99999997', + ], + [ + 1, + '2010-04-04 10:31:05', + 'alice', + 'alice in aleland', + 'alicepass', + 'alice2@example.com', + '1', + '99999999|99999998|99999997', + ], + [ + 0, + '2007-03-15 10:31:05', + 'bob', + 'rebob', + 'bobpass', + 'bob@example.com', + '0', + '99999999|99999997', + ], + [ + 1, + '2004-02-29 10:31:05', + 'charlie', + 'charlie chocolate', + 'mykids', + 'charlie@example.com', + '0', + '99999999|99999998', + ], + ]; foreach ($data as $row) { $query->values(array_combine($fields, $row)); } $query->execute(); } +/** + * Populate comment table. + */ function migrate_example_beer_data_comment() { - $fields = array('bid', 'cid_parent', 'subject', 'body', 'name', 'mail', 'aid'); + $fields = ['bid', 'cid_parent', 'subject', 'body', 'name', 'mail', 'aid']; $query = db_insert('migrate_example_beer_comment') ->fields($fields); - $data = array( - array(99999998, NULL, 'im first', 'full body', 'alice', 'alice@example.com', 0), - array(99999998, NULL, 'im second', 'aromatic', 'alice', 'alice@example.com', 0), - array(99999999, NULL, 'im parent', 'malty', 'alice', 'alice@example.com', 0), - array(99999999, 1, 'im child', 'cold body', 'bob', NULL, 1), - array(99999999, 4, 'im grandchild', 'bitter body', 'charlie@example.com', NULL, 1), - ); + $data = [ + [99999998, NULL, 'im first', 'full body', 'alice', 'alice@example.com', 0], + [99999998, NULL, 'im second', 'aromatic', 'alice', 'alice@example.com', 0], + [99999999, NULL, 'im parent', 'malty', 'alice', 'alice@example.com', 0], + [99999999, 1, 'im child', 'cold body', 'bob', NULL, 1], + [ + 99999999, + 4, + 'im grandchild', + 'bitter body', + 'charlie@example.com', + NULL, + 1, + ], + ]; foreach ($data as $row) { $query->values(array_combine($fields, $row)); } $query->execute(); } +/** + * Populate topic table. + */ function migrate_example_beer_data_topic() { - $fields = array('style', 'details', 'style_parent', 'region', 'hoppiness'); + $fields = ['style', 'details', 'style_parent', 'region', 'hoppiness']; $query = db_insert('migrate_example_beer_topic') ->fields($fields); - $data = array( - array('ale', 'traditional', NULL, 'Medieval British Isles', 'Medium'), - array('red ale', 'colorful', 'ale', NULL, NULL), - array('pilsner', 'refreshing', NULL, 'Pilsen, Bohemia (now Czech Republic)', 'Low'), - ); + $data = [ + ['ale', 'traditional', NULL, 'Medieval British Isles', 'Medium'], + ['red ale', 'colorful', 'ale', NULL, NULL], + [ + 'pilsner', + 'refreshing', + NULL, + 'Pilsen, Bohemia (now Czech Republic)', + 'Low', + ], + ]; foreach ($data as $row) { $query->values(array_combine($fields, $row)); } $query->execute(); } +/** + * Populate topic node table. + */ function migrate_example_beer_data_topic_node() { - $fields = array('bid', 'style'); + $fields = ['bid', 'style']; $query = db_insert('migrate_example_beer_topic_node') ->fields($fields); - $data = array( - array(99999999, 'pilsner'), - array(99999999, 'red ale'), - array(99999998, 'red ale'), - ); + $data = [ + [99999999, 'pilsner'], + [99999999, 'red ale'], + [99999998, 'red ale'], + ]; foreach ($data as $row) { $query->values(array_combine($fields, $row)); } diff --git a/web/modules/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_comment.yml b/web/modules/migrate_plus/migrate_example/migrations/beer_comment.yml similarity index 90% rename from web/modules/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_comment.yml rename to web/modules/migrate_plus/migrate_example/migrations/beer_comment.yml index e631e9ac86..3540ab69d0 100644 --- a/web/modules/migrate_plus/migrate_example/config/install/migrate_plus.migration.beer_comment.yml +++ b/web/modules/migrate_plus/migrate_example/migrations/beer_comment.yml @@ -8,11 +8,11 @@ destination: plugin: entity:comment process: pid: - plugin: migration + plugin: migration_lookup migration: beer_comment source: cid_parent entity_id: - plugin: migration + plugin: migration_lookup migration: beer_node source: bid entity_type: @@ -26,7 +26,7 @@ process: default_value: node_comments subject: subject uid: - plugin: migration + plugin: migration_lookup migration: beer_user source: aid name: name diff --git a/web/modules/migrate_plus/migrate_example/src/Plugin/migrate/source/BeerComment.php b/web/modules/migrate_plus/migrate_example/src/Plugin/migrate/source/BeerComment.php index 217fad262f..b5d446a3ff 100644 --- a/web/modules/migrate_plus/migrate_example/src/Plugin/migrate/source/BeerComment.php +++ b/web/modules/migrate_plus/migrate_example/src/Plugin/migrate/source/BeerComment.php @@ -17,10 +17,19 @@ class BeerComment extends SqlBase { * {@inheritdoc} */ public function query() { + $fields = [ + 'cid', + 'cid_parent', + 'name', + 'mail', + 'aid', + 'body', + 'bid', + 'subject', + ]; $query = $this->select('migrate_example_beer_comment', 'mec') - ->fields('mec', ['cid', 'cid_parent', 'name', 'mail', 'aid', - 'body', 'bid', 'subject']) - ->orderBy('cid_parent', 'ASC'); + ->fields('mec', $fields) + ->orderBy('cid_parent', 'ASC'); return $query; } diff --git a/web/modules/migrate_plus/migrate_example/src/Plugin/migrate/source/BeerNode.php b/web/modules/migrate_plus/migrate_example/src/Plugin/migrate/source/BeerNode.php index 8d9f0bbfdc..40ac4df29d 100644 --- a/web/modules/migrate_plus/migrate_example/src/Plugin/migrate/source/BeerNode.php +++ b/web/modules/migrate_plus/migrate_example/src/Plugin/migrate/source/BeerNode.php @@ -18,21 +18,29 @@ class BeerNode extends SqlBase { * {@inheritdoc} */ public function query() { - /** - * An important point to note is that your query *must* return a single row - * for each item to be imported. Here we might be tempted to add a join to - * migrate_example_beer_topic_node in our query, to pull in the - * relationships to our categories. Doing this would cause the query to - * return multiple rows for a given node, once per related value, thus - * processing the same node multiple times, each time with only one of the - * multiple values that should be imported. To avoid that, we simply query - * the base node data here, and pull in the relationships in prepareRow() - * below. - */ + // An important point to note is that your query *must* return a single row + // for each item to be imported. Here we might be tempted to add a join to + // migrate_example_beer_topic_node in our query, to pull in the + // relationships to our categories. Doing this would cause the query to + // return multiple rows for a given node, once per related value, thus + // processing the same node multiple times, each time with only one of the + // multiple values that should be imported. To avoid that, we simply query + // the base node data here, and pull in the relationships in prepareRow() + // below. + $fields = [ + 'bid', + 'name', + 'body', + 'excerpt', + 'aid', + 'countries', + 'image', + 'image_alt', + 'image_title', + 'image_description', + ]; $query = $this->select('migrate_example_beer_node', 'b') - ->fields('b', ['bid', 'name', 'body', 'excerpt', 'aid', - 'countries', 'image', 'image_alt', 'image_title', - 'image_description']); + ->fields('b', $fields); return $query; } @@ -76,13 +84,11 @@ public function getIds() { * {@inheritdoc} */ public function prepareRow(Row $row) { - /** - * As explained above, we need to pull the style relationships into our - * source row here, as an array of 'style' values (the unique ID for - * the beer_term migration). - */ + // As explained above, we need to pull the style relationships into our + // source row here, as an array of 'style' values (the unique ID for + // the beer_term migration). $terms = $this->select('migrate_example_beer_topic_node', 'bt') - ->fields('bt', ['style']) + ->fields('bt', ['style']) ->condition('bid', $row->getSourceProperty('bid')) ->execute() ->fetchCol(); diff --git a/web/modules/migrate_plus/migrate_example/src/Plugin/migrate/source/BeerTerm.php b/web/modules/migrate_plus/migrate_example/src/Plugin/migrate/source/BeerTerm.php index f8ea51efe9..5eb3e70f30 100644 --- a/web/modules/migrate_plus/migrate_example/src/Plugin/migrate/source/BeerTerm.php +++ b/web/modules/migrate_plus/migrate_example/src/Plugin/migrate/source/BeerTerm.php @@ -5,11 +5,12 @@ use Drupal\migrate\Plugin\migrate\source\SqlBase; /** - * This is an example of a simple SQL-based source plugin. Source plugins are - * classes which deliver source data to the processing pipeline. For SQL - * sources, the SqlBase class provides most of the functionality needed - for - * a specific migration, you are required to implement the three simple public - * methods you see below. + * This is an example of a simple SQL-based source plugin. + * + * Source plugins are classes which deliver source data to the processing + * pipeline. For SQL sources, the SqlBase class provides most of the + * functionality needed - for a specific migration, you are required to + * implement the three simple public methods you see below. * * This annotation tells Drupal that the name of the MigrateSource plugin * implemented by this class is "beer_term". This is the name that the migration @@ -25,16 +26,15 @@ class BeerTerm extends SqlBase { * {@inheritdoc} */ public function query() { - /** - * The most important part of a SQL source plugin is the SQL query to - * retrieve the data to be imported. Note that the query is not executed - * here - the migration process will control execution of the query. Also - * note that it is constructed from a $this->select() call - this ensures - * that the query is executed against the database configured for this - * source plugin. - */ + // The most important part of a SQL source plugin is the SQL query to + // retrieve the data to be imported. Note that the query is not executed + // here - the migration process will control execution of the query. Also + // note that it is constructed from a $this->select() call - this ensures + // that the query is executed against the database configured for this + // source plugin. + $fields = ['style', 'details', 'style_parent', 'region', 'hoppiness']; return $this->select('migrate_example_beer_topic', 'met') - ->fields('met', ['style', 'details', 'style_parent', 'region', 'hoppiness']) + ->fields('met', $fields) // We sort this way to ensure parent terms are imported first. ->orderBy('style_parent', 'ASC'); } @@ -43,16 +43,14 @@ public function query() { * {@inheritdoc} */ public function fields() { - /** - * This method simply documents the available source fields provided by - * the source plugin, for use by front-end tools. It returns an array keyed - * by field/column name, with the value being a translated string explaining - * to humans what the field represents. You should always - */ + // This method simply documents the available source fields provided by the + // source plugin, for use by front-end tools. It returns an array keyed by + // field/column name, with the value being a translated string explaining + // to humans what the field represents. $fields = [ - 'style' => $this->t('Account ID'), - 'details' => $this->t('Blocked/Allowed'), - 'style_parent' => $this->t('Registered date'), + 'style' => $this->t('Beer style'), + 'details' => $this->t('Style details'), + 'style_parent' => $this->t('Parent style'), // These values are not currently migrated - it's OK to skip fields you // don't need. 'region' => $this->t('Region the style is associated with'), @@ -66,14 +64,12 @@ public function fields() { * {@inheritdoc} */ public function getIds() { - /** - * This method indicates what field(s) from the source row uniquely identify - * that source row, and what their types are. This is critical information - * for managing the migration. The keys of the returned array are the field - * names from the query which comprise the unique identifier. The values are - * arrays indicating the type of the field, used for creating compatible - * columns in the map tables that track processed items. - */ + // This method indicates what field(s) from the source row uniquely identify + // that source row, and what their types are. This is critical information + // for managing the migration. The keys of the returned array are the field + // names from the query which comprise the unique identifier. The values are + // arrays indicating the type of the field, used for creating compatible + // columns in the map tables that track processed items. return [ 'style' => [ 'type' => 'string', diff --git a/web/modules/migrate_plus/migrate_example/src/Plugin/migrate/source/BeerUser.php b/web/modules/migrate_plus/migrate_example/src/Plugin/migrate/source/BeerUser.php index b0d963d3fd..398f87c696 100644 --- a/web/modules/migrate_plus/migrate_example/src/Plugin/migrate/source/BeerUser.php +++ b/web/modules/migrate_plus/migrate_example/src/Plugin/migrate/source/BeerUser.php @@ -18,9 +18,19 @@ class BeerUser extends SqlBase { * {@inheritdoc} */ public function query() { + $fields = [ + 'aid', + 'status', + 'registered', + 'username', + 'nickname', + 'password', + 'email', + 'sex', + 'beers', + ]; return $this->select('migrate_example_beer_account', 'mea') - ->fields('mea', ['aid', 'status', 'registered', 'username', 'nickname', - 'password', 'email', 'sex', 'beers']); + ->fields('mea', $fields); } /** @@ -58,27 +68,22 @@ public function getIds() { * {@inheritdoc} */ public function prepareRow(Row $row) { - /** - * prepareRow() is the most common place to perform custom run-time - * processing that isn't handled by an existing process plugin. It is called - * when the raw data has been pulled from the source, and provides the - * opportunity to modify or add to that data, creating the canonical set of - * source data that will be fed into the processing pipeline. - * - * In our particular case, the list of a user's favorite beers is a pipe- - * separated list of beer IDs. The processing pipeline deals with arrays - * representing multi-value fields naturally, so we want to explode that - * string to an array of individual beer IDs. - */ + // A prepareRow() is the most common place to perform custom run-time + // processing that isn't handled by an existing process plugin. It is called + // when the raw data has been pulled from the source, and provides the + // opportunity to modify or add to that data, creating the canonical set of + // source data that will be fed into the processing pipeline. + // In our particular case, the list of a user's favorite beers is a pipe- + // separated list of beer IDs. The processing pipeline deals with arrays + // representing multi-value fields naturally, so we want to explode that + // string to an array of individual beer IDs. if ($value = $row->getSourceProperty('beers')) { $row->setSourceProperty('beers', explode('|', $value)); } - /** - * Always call your parent! Essential processing is performed in the base - * class. Be mindful that prepareRow() returns a boolean status - if FALSE - * that indicates that the item being processed should be skipped. Unless - * we're deciding to skip an item ourselves, let the parent class decide. - */ + // Always call your parent! Essential processing is performed in the base + // class. Be mindful that prepareRow() returns a boolean status - if FALSE + // that indicates that the item being processed should be skipped. Unless + // we're deciding to skip an item ourselves, let the parent class decide. return parent::prepareRow($row); } diff --git a/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.weather_soap.yml b/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.weather_soap.yml index da4252153e..328bcca84a 100644 --- a/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.weather_soap.yml +++ b/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.weather_soap.yml @@ -2,19 +2,33 @@ id: weather_soap label: SOAP service providing weather. migration_group: wine +migration_tags: + - advanced example source: # We use the SOAP parser source plugin. plugin: url data_fetcher_plugin: http # Ignored - SoapClient does the fetching. data_parser_plugin: soap # URL of a WSDL endpoint. - urls: http://www.webservicex.net/globalweather.asmx?WSDL - # The function to call on the service, and the parameters to pass. + urls: + - http://www.webservicex.net/globalweather.asmx?WSDL + # The function to call on the service, and the parameters to pass. See + # http://www.webservicex.net/New/Home/ServiceDetail/56 for the XML structure + # of this feed - how CountryName is passed within the GetCitiesByCountry + # XML element. function: GetCitiesByCountry parameters: CountryName: Spain + # Responses may be returned as an XML string, an object, or an array - specify + # the type of response here. response_type: xml + # Looking at the XML response at http://www.webservicex.net/globalweather.asmx/GetCitiesByCountry, + # we see that the data items we want are within <NewDataSet><Table>. item_selector: /NewDataSet/Table + # For each field, 'name' is the source property name to be used in the process + # steps below, 'label' is optional (to document the property), and selector + # is an xpath (-like, for array and object returns) string relative to the + # item_selector for retrieving that data value. fields: - name: Country @@ -24,6 +38,9 @@ source: name: City label: City selector: City + # 'ids' tells us what source property ('City') holds the unique identifying + # value for each imported item, and what schema type to use to hold that + # value in the migration map an message tables. ids: City: type: string @@ -34,3 +51,6 @@ process: name: City destination: plugin: entity:taxonomy_term +migration_dependencies: + required: {} + optional: {} diff --git a/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_role_json.yml b/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_role_json.yml index 4824eab032..d7d3953af3 100644 --- a/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_role_json.yml +++ b/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_role_json.yml @@ -2,22 +2,33 @@ id: wine_role_json label: JSON feed of roles (positions) migration_group: wine +migration_tags: + - advanced example source: # We use the JSON source plugin. plugin: url data_fetcher_plugin: http 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 # 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_position?_format=json - item_selector: 1 + urls: + - /migrate_example_advanced_position?_format=json + # An xpath-like selector corresponding to the items to be imported. + item_selector: position # 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. + # is relative to the elements retrieved by item_selector. fields: - name: machine_name diff --git a/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_role_xml.yml b/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_role_xml.yml index 6b7234c248..b5032a4f15 100644 --- a/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_role_xml.yml +++ b/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_role_xml.yml @@ -2,6 +2,8 @@ id: wine_role_xml label: XML feed of roles (positions) migration_group: wine +migration_tags: + - advanced example source: # We use the XML data parser plugin. plugin: url @@ -10,7 +12,8 @@ source: # 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_position?_format=xml + urls: + - /migrate_example_advanced_position?_format=xml # 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 diff --git a/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_terms.yml b/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_terms.yml index f11b82f844..56eb273867 100644 --- a/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_terms.yml +++ b/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_terms.yml @@ -1,6 +1,8 @@ id: wine_terms label: Migrate all categories into Drupal taxonomy terms migration_group: wine +migration_tags: + - advanced example source: plugin: wine_term destination: @@ -26,7 +28,7 @@ process: region: migrate_example_wine_regions variety: migrate_example_wine_varieties parent: - plugin: migration + plugin: migration_lookup migration: wine_terms source: category_parent weight: ordering 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 index ae4d2b7471..ff88862b2f 100644 --- 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 @@ -3,13 +3,16 @@ 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 + 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 @@ -46,7 +49,7 @@ process: name: category_name description: category_details parent: - plugin: migration + plugin: migration_lookup migration: wine_terms source: category_parent destination: diff --git a/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_variety_multi_xml.yml b/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_variety_multi_xml.yml index e26579e71a..a501a7e752 100644 --- a/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_variety_multi_xml.yml +++ b/web/modules/migrate_plus/migrate_example_advanced/config/install/migrate_plus.migration.wine_variety_multi_xml.yml @@ -2,6 +2,8 @@ id: wine_variety_multi_xml label: XML feed of varieties migration_group: wine +migration_tags: + - advanced example source: # We use the XML source plugin. plugin: url @@ -54,7 +56,7 @@ process: name: category_name description: category_details parent: - plugin: migration + plugin: migration_lookup migration: wine_terms source: category_parent field_variety_attributes: category_attributes 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 22e24506b2..1d396e34bc 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 @@ -4,12 +4,12 @@ description: 'Specialized examples of Drupal 8 migration.' package: Examples # core: 8.x dependencies: - - migrate - - migrate_example_advanced_setup - - migrate_plus + - drupal:migrate + - migrate_plus:migrate_example_advanced_setup + - migrate_plus:migrate_plus -# Information added by Drupal.org packaging script on 2016-08-05 -version: '8.x-2.0-beta2' +# Information added by Drupal.org packaging script on 2018-09-06 +version: '8.x-4.0' core: '8.x' project: 'migrate_plus' -datestamp: 1470428640 +datestamp: 1536264189 diff --git a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced.install b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced.install index b2506e4a9a..2023bdee16 100644 --- a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced.install +++ b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced.install @@ -2,26 +2,30 @@ /** * @file - * Install, update and uninstall functions for the migrate_example_advanced module. + * Install, update and uninstall functions for migrate_example_advanced module. */ use Drupal\migrate_plus\Entity\Migration; /** * Implements hook_install(). + * + * We need the urls to be absolute for the XML source plugin to read them, but + * the static configuration files on disk can't know the server and port to + * use. So, in the .yml files we provide the REST resources relative to the + * site root and here rewrite them to fully-qualified paths. */ function migrate_example_advanced_install() { - // We need the urls to be absolute for the XML source plugin to read them, but - // the static configuration files on disk can't know the server and port to - // use. So, in the .yml files we provide the REST resources relative to the - // site root and here rewrite them to fully-qualified paths. - /** @var \Drupal\migrate_plus\Entity\MigrationInterface $wine_role_xml_migration */ $wine_role_xml_migration = Migration::load('wine_role_xml'); if ($wine_role_xml_migration) { $source = $wine_role_xml_migration->get('source'); $request = \Drupal::request(); - $source['urls'] = 'http://' . $request->getHttpHost() . $source['urls']; + $urls = []; + foreach ($source['urls'] as $url) { + $urls[] = 'http://' . $request->getHttpHost() . $url; + } + $source['urls'] = $urls; $wine_role_xml_migration->set('source', $source); $wine_role_xml_migration->save(); } @@ -30,7 +34,11 @@ function migrate_example_advanced_install() { if ($wine_role_json_migration) { $source = $wine_role_json_migration->get('source'); $request = \Drupal::request(); - $source['urls'] = 'http://' . $request->getHttpHost() . $source['urls']; + $urls = []; + foreach ($source['urls'] as $url) { + $urls[] = 'http://' . $request->getHttpHost() . $url; + } + $source['urls'] = $urls; $wine_role_json_migration->set('source', $source); $wine_role_json_migration->save(); } diff --git a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/rest.resource.migrate_example_advanced_position.yml b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/rest.resource.migrate_example_advanced_position.yml new file mode 100644 index 0000000000..6a79fc90f4 --- /dev/null +++ b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/rest.resource.migrate_example_advanced_position.yml @@ -0,0 +1,10 @@ +id: migrate_example_advanced_position +plugin_id: 'migrate_example_advanced_position' +granularity: method +configuration: + GET: + supported_formats: + - json + - xml + supported_auth: + - cookie diff --git a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/rest.resource.migrate_example_advanced_variety_items.yml b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/rest.resource.migrate_example_advanced_variety_items.yml new file mode 100644 index 0000000000..bf2518a5f5 --- /dev/null +++ b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/rest.resource.migrate_example_advanced_variety_items.yml @@ -0,0 +1,10 @@ +id: migrate_example_advanced_variety_items +plugin_id: 'migrate_example_advanced_variety_items' +granularity: method +configuration: + GET: + supported_formats: + - json + - xml + supported_auth: + - cookie diff --git a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/rest.resource.migrate_example_advanced_variety_list.yml b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/rest.resource.migrate_example_advanced_variety_list.yml new file mode 100644 index 0000000000..f19dc60fe7 --- /dev/null +++ b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/rest.resource.migrate_example_advanced_variety_list.yml @@ -0,0 +1,10 @@ +id: migrate_example_advanced_variety_list +plugin_id: 'migrate_example_advanced_variety_list' +granularity: method +configuration: + GET: + supported_formats: + - json + - xml + supported_auth: + - cookie diff --git a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/rest.resource.migrate_example_advanced_variety_multiple.yml b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/rest.resource.migrate_example_advanced_variety_multiple.yml new file mode 100644 index 0000000000..c8960369ff --- /dev/null +++ b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/config/install/rest.resource.migrate_example_advanced_variety_multiple.yml @@ -0,0 +1,10 @@ +id: migrate_example_advanced_variety_multiple +plugin_id: 'migrate_example_advanced_variety_multiple' +granularity: method +configuration: + GET: + supported_formats: + - json + - xml + supported_auth: + - cookie 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 c5c48696a0..8705865683 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 @@ -5,14 +5,14 @@ package: Migration # core: 8.x hidden: 1 dependencies: - - comment - - image - - text - - taxonomy - - rest + - drupal:comment + - drupal:image + - drupal:text + - drupal:taxonomy + - drupal:rest -# Information added by Drupal.org packaging script on 2016-08-05 -version: '8.x-2.0-beta2' +# Information added by Drupal.org packaging script on 2018-09-06 +version: '8.x-4.0' core: '8.x' project: 'migrate_plus' -datestamp: 1470428640 +datestamp: 1536264189 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 8c92cba356..be9031a6be 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 @@ -1,8 +1,9 @@ <?php -use Drupal\user\RoleInterface; /** * @file + * Install setup for the migration example module. + * * Set up source data and destination configuration for the migration example * module. We do this in a separate module so migrate_example_advanced itself is * a pure migration module. @@ -36,39 +37,6 @@ function migrate_example_advanced_setup_schema() { * Implements hook_install(). */ function migrate_example_advanced_setup_install() { - // Enable and configure REST resources providing source data. - $config = \Drupal::configFactory()->getEditable('rest.settings'); - $resources = $config->get('resources'); - $resources['migrate_example_advanced_position']['GET'] = [ - 'supported_formats' => ['json', 'xml'], - 'supported_auth' => ['cookie'], - ]; - $resources['migrate_example_advanced_variety_multiple']['GET'] = [ - 'supported_formats' => ['json', 'xml'], - 'supported_auth' => ['cookie'], - ]; - $resources['migrate_example_advanced_variety_list']['GET'] = [ - 'supported_formats' => ['json', 'xml'], - 'supported_auth' => ['cookie'], - ]; - $resources['migrate_example_advanced_variety_items']['GET'] = [ - 'supported_formats' => ['json', 'xml'], - 'supported_auth' => ['cookie'], - ]; - $config->set('resources', $resources); - $config->save(); - - // Don't require authentication for the services, so the migrations can easily - // be run from drush. - user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, - ['restful get migrate_example_advanced_position']); - user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, - ['restful get migrate_example_advanced_variety_multiple']); - user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, - ['restful get migrate_example_advanced_variety_list']); - user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, - ['restful get migrate_example_advanced_variety_items']); - // Populate our tables. migrate_example_advanced_data_account(); migrate_example_advanced_data_account_updates(); @@ -87,6 +55,12 @@ function migrate_example_advanced_setup_install() { migrate_example_advanced_data_table_source(); } +/** + * The hook_schema definition for wine. + * + * @return array + * The schema definition. + */ function migrate_example_advanced_schema_wine() { return [ 'description' => 'Wines of the world', @@ -155,8 +129,14 @@ function migrate_example_advanced_schema_wine() { ]; } +/** + * The hook_schema definition for updates. + * + * @return array + * The schema definition. + */ function migrate_example_advanced_schema_updates() { - return array( + return [ 'description' => 'Updated wine ratings', 'fields' => [ 'wineid' => [ @@ -173,754 +153,1068 @@ function migrate_example_advanced_schema_updates() { ], ], 'primary key' => ['wineid'], - ); + ]; } +/** + * The hook_schema definition for producer. + * + * @return array + * The schema definition. + */ function migrate_example_advanced_schema_producer() { - return array( + return [ 'description' => 'Wine producers of the world', - 'fields' => array( - 'producerid' => array( + 'fields' => [ + 'producerid' => [ 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'description' => 'Producer ID', - ), - 'name' => array( + ], + 'name' => [ 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, - ), - 'body' => array( + ], + 'body' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Full description of the producer.', - ), - 'excerpt' => array( + ], + 'excerpt' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Abstract for this producer.', - ), - 'accountid' => array( + ], + 'accountid' => [ 'type' => 'int', 'unsigned' => TRUE, 'not null' => FALSE, 'description' => 'Account ID of the author.', - ), - ), - 'primary key' => array('producerid'), - ); + ], + ], + 'primary key' => ['producerid'], + ]; } +/** + * The hook_schema definition for categories. + * + * @return array + * The schema definition. + */ function migrate_example_advanced_schema_categories() { - return array( + return [ 'description' => 'Categories', - 'fields' => array( - 'categoryid' => array( + 'fields' => [ + 'categoryid' => [ 'type' => 'int', 'not null' => TRUE, 'unsigned' => TRUE, 'description' => 'Category ID', - ), - 'type' => array( + ], + 'type' => [ 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'description' => 'Type of category: variety, region, best_with', - ), - 'name' => array( + ], + 'name' => [ 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, - ), - 'details' => array( + ], + 'details' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, - ), - 'category_parent' => array( + ], + 'category_parent' => [ 'type' => 'int', 'unsigned' => TRUE, 'not null' => FALSE, 'description' => 'Parent category, if any', - ), - 'ordering' => array( + ], + 'ordering' => [ 'type' => 'int', 'unsigned' => FALSE, 'not null' => FALSE, 'description' => 'Order in which to display categories', - ), - ), - 'primary key' => array('categoryid'), - ); + ], + ], + 'primary key' => ['categoryid'], + ]; } +/** + * The hook_schema definition for vintages. + * + * @return array + * The schema definition. + */ function migrate_example_advanced_schema_vintages() { - return array( + return [ 'description' => 'Wine vintages', - 'fields' => array( - 'wineid' => array( + 'fields' => [ + 'wineid' => [ 'type' => 'int', 'not null' => TRUE, 'description' => 'Wine ID', - ), - 'vintage' => array( + ], + 'vintage' => [ 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'description' => 'Vintage (year)', - ), - ), - 'primary key' => array('wineid', 'vintage'), - ); + ], + ], + 'primary key' => ['wineid', 'vintage'], + ]; } +/** + * The hook_schema definition for variety updates. + * + * @return array + * The schema definition. + */ function migrate_example_advanced_schema_variety_updates() { - return array( + return [ 'description' => 'Variety updates', - 'fields' => array( - 'categoryid' => array( + 'fields' => [ + 'categoryid' => [ 'type' => 'int', 'not null' => TRUE, 'unsigned' => TRUE, 'description' => 'Category ID', - ), - 'details' => array( + ], + 'details' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, - ), - ), - 'primary key' => array('categoryid'), - ); + ], + ], + 'primary key' => ['categoryid'], + ]; } +/** + * The hook_schema definition for category wine. + * + * @return array + * The schema definition. + */ function migrate_example_advanced_schema_category_wine() { - return array( + return [ 'description' => 'Wine category assignments', - 'fields' => array( - 'wineid' => array( + 'fields' => [ + 'wineid' => [ 'type' => 'int', 'not null' => TRUE, 'description' => 'Wine ID', - ), - 'categoryid' => array( + ], + 'categoryid' => [ 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'description' => 'Category ID', - ), - ), - 'primary key' => array('categoryid', 'wineid'), - ); + ], + ], + 'primary key' => ['categoryid', 'wineid'], + ]; } +/** + * The hook_schema definition for category producer. + * + * @return array + * The schema definition. + */ function migrate_example_advanced_schema_category_producer() { - return array( + return [ 'description' => 'Producer category assignments', - 'fields' => array( - 'producerid' => array( + 'fields' => [ + 'producerid' => [ 'type' => 'int', 'not null' => TRUE, 'description' => 'Producer ID', - ), - 'categoryid' => array( + ], + 'categoryid' => [ 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'description' => 'Category ID', - ), - ), - 'primary key' => array('categoryid', 'producerid'), - ); + ], + ], + 'primary key' => ['categoryid', 'producerid'], + ]; } +/** + * The hook_schema definition for comment. + * + * @return array + * The schema definition. + */ function migrate_example_advanced_schema_comment() { - return array( + return [ 'description' => 'Wine comments', - 'fields' => array( - 'commentid' => array( + 'fields' => [ + 'commentid' => [ 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'description' => 'Comment ID', - ), - 'wineid' => array( + ], + 'wineid' => [ 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'description' => 'Wine ID that is being commented upon', - ), - 'comment_parent' => array( + ], + 'comment_parent' => [ 'type' => 'int', 'unsigned' => TRUE, 'not null' => FALSE, 'description' => 'Parent comment ID in case of comment replies.', - ), - 'subject' => array( + ], + 'subject' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Comment subject', - ), - 'body' => array( + ], + 'body' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Comment body', - ), - 'name' => array( + ], + 'name' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Comment name (if anon)', - ), - 'mail' => array( + ], + 'mail' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Comment email (if anon)', - ), - 'accountid' => array( + ], + 'accountid' => [ 'type' => 'int', 'unsigned' => TRUE, 'not null' => FALSE, 'description' => 'Account ID (if any).', - ), - 'commenthost' => array( + ], + 'commenthost' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'IP/domain of host posted from', - ), - 'userpage' => array( + ], + 'userpage' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'User homepage', - ), - 'posted' => array( + ], + 'posted' => [ 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'description' => 'Date comment posted', - ), - 'lastchanged' => array( + ], + 'lastchanged' => [ 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'description' => 'Date comment last changed', - ), - ), - 'primary key' => array('commentid'), - ); + ], + ], + 'primary key' => ['commentid'], + ]; } +/** + * The hook_schema definition for comment updates. + * + * @return array + * The schema definition. + */ function migrate_example_advanced_schema_comment_updates() { - return array( + return [ 'description' => 'Wine comment updates', - 'fields' => array( - 'commentid' => array( + 'fields' => [ + 'commentid' => [ 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'description' => 'Comment ID', - ), - 'subject' => array( + ], + 'subject' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Comment subject', - ), - ), - 'primary key' => array('commentid'), - ); + ], + ], + 'primary key' => ['commentid'], + ]; } +/** + * The hook_schema definition for account. + * + * @return array + * The schema definition. + */ function migrate_example_advanced_schema_account() { - return array( + return [ 'description' => 'Wine accounts.', - 'fields' => array( - 'accountid' => array( + 'fields' => [ + 'accountid' => [ 'type' => 'serial', 'not null' => TRUE, 'description' => 'Account ID', - ), - 'status' => array( + ], + 'status' => [ 'type' => 'int', 'not null' => TRUE, 'description' => 'Blocked_Allowed', - ), - 'posted' => array( + ], + 'posted' => [ 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'description' => 'Registration date', - ), - 'last_access' => array( + ], + 'last_access' => [ 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'description' => 'Last access date', - ), - 'last_login' => array( + ], + 'last_login' => [ 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'description' => 'Last login date', - ), - 'name' => array( + ], + 'name' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Account name (for login)', - ), - 'sex' => array( + ], + 'sex' => [ 'type' => 'char', 'length' => 1, 'not null' => FALSE, 'description' => 'Gender', - ), - 'password' => array( + ], + 'password' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Account password (raw)', - ), - 'mail' => array( + ], + 'mail' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Account email', - ), - 'original_mail' => array( + ], + 'original_mail' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Original account email', - ), - 'sig' => array( + ], + 'sig' => [ 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'description' => 'Signature for comments', - ), - 'imageid' => array( + ], + 'imageid' => [ 'type' => 'int', 'unsigned' => TRUE, 'not null' => FALSE, 'description' => 'Image ID', - ), - 'positions' => array( + ], + 'positions' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Positions held', - ), - ), - 'primary key' => array('accountid'), - ); + ], + ], + 'primary key' => ['accountid'], + ]; } +/** + * The hook_schema definition for account updates. + * + * @return array + * The schema definition. + */ function migrate_example_advanced_schema_account_updates() { - return array( + return [ 'description' => 'Wine account updates', - 'fields' => array( - 'accountid' => array( + 'fields' => [ + 'accountid' => [ 'type' => 'serial', 'not null' => TRUE, 'description' => 'Account ID', - ), - 'sex' => array( + ], + 'sex' => [ 'type' => 'char', 'length' => 1, 'not null' => FALSE, 'description' => 'Gender', - ), - ), - 'primary key' => array('accountid'), - ); + ], + ], + 'primary key' => ['accountid'], + ]; } +/** + * The hook_schema definition for blobs. + * + * @return array + * The schema definition. + */ function migrate_example_advanced_schema_blobs() { - return array( + return [ 'description' => 'Wine blobs to be migrated to file entities', - 'fields' => array( - 'imageid' => array( + 'fields' => [ + 'imageid' => [ 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'description' => 'Image ID', - ), - 'imageblob' => array( + ], + 'imageblob' => [ 'type' => 'blob', 'size' => 'normal', 'description' => 'binary image data', - ), - ), - 'primary key' => array('imageid'), - ); + ], + ], + 'primary key' => ['imageid'], + ]; } +/** + * The hook_schema definition for files. + * + * @return array + * The schema definition. + */ function migrate_example_advanced_schema_files() { - return array( + return [ 'description' => 'Wine and account files', - 'fields' => array( - 'imageid' => array( + 'fields' => [ + 'imageid' => [ 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'description' => 'Image ID', - ), - 'url' => array( + ], + 'url' => [ 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'description' => 'Image URL', - ), - 'image_alt' => array( + ], + 'image_alt' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Image alt', - ), - 'image_title' => array( + ], + 'image_title' => [ 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'description' => 'Image title', - ), - 'wineid' => array( + ], + 'wineid' => [ 'type' => 'int', 'unsigned' => TRUE, 'not null' => FALSE, 'description' => 'Wine node this is associated with', - ), - ), - 'primary key' => array('imageid'), - ); + ], + ], + 'primary key' => ['imageid'], + ]; } +/** + * The hook_schema definition for table source. + * + * @return array + * The schema definition. + */ function migrate_example_advanced_schema_table_source() { - return array( + return [ 'description' => 'Source data to go into a custom Drupal table', - 'fields' => array( - 'fooid' => array( + 'fields' => [ + 'fooid' => [ 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'description' => 'Primary key', - ), - 'field1' => array( + ], + 'field1' => [ 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'description' => 'First field', - ), - 'field2' => array( + ], + 'field2' => [ 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'description' => 'Second field', - ), - ), - 'primary key' => array('fooid'), - ); + ], + ], + 'primary key' => ['fooid'], + ]; } +/** + * The hook_schema definition for table destination. + * + * @return array + * The schema definition. + */ function migrate_example_advanced_schema_table_dest() { - return array( + return [ 'description' => 'Custom Drupal table to receive source data directly', - 'fields' => array( - 'recordid' => array( + 'fields' => [ + 'recordid' => [ 'type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE, 'description' => 'Primary key', - ), - 'drupal_text' => array( + ], + 'drupal_text' => [ 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'description' => 'First field', - ), - 'drupal_int' => array( + ], + 'drupal_int' => [ 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'description' => 'Second field', - ), - ), - 'primary key' => array('recordid'), - ); + ], + ], + 'primary key' => ['recordid'], + ]; } +/** + * Populate wine table. + */ function migrate_example_advanced_data_wine() { - $fields = array('wineid', 'name', 'body', 'excerpt', 'accountid', - 'posted', 'last_changed', 'variety', 'region', 'rating'); + $fields = [ + 'wineid', + 'name', + 'body', + 'excerpt', + 'accountid', + 'posted', + 'last_changed', + 'variety', + 'region', + 'rating', + ]; $query = db_insert('migrate_example_wine') - ->fields($fields); - $data = array( - array(1, 'Montes Classic Cabernet Sauvignon', 'Intense ruby-red color', 'Great!', 9, - strtotime('2010-01-02 03:04:05'), strtotime('2010-03-04 05:06:07'), 25, 17, 95), - array(2, 'Archeo Ruggero di Tasso Nero d\'Avola', 'Lots of berry character', 'Pair with red sauced dishes', 3, - strtotime('2010-09-03 18:23:58'), strtotime('2010-09-03 18:23:58'), 26, 2, 85), - ); + ->fields($fields); + $data = [ + [ + 1, + 'Montes Classic Cabernet Sauvignon', + 'Intense ruby-red color', + 'Great!', + 9, + strtotime('2010-01-02 03:04:05'), + strtotime('2010-03-04 05:06:07'), + 25, + 17, + 95, + ], + [ + 2, + 'Archeo Ruggero di Tasso Nero d\'Avola', + 'Lots of berry character', + 'Pair with red sauced dishes', + 3, + strtotime('2010-09-03 18:23:58'), + strtotime('2010-09-03 18:23:58'), + 26, + 2, + 85, + ], + ]; foreach ($data as $row) { $query->values(array_combine($fields, $row)); } $query->execute(); } +/** + * Populate updates table. + */ function migrate_example_advanced_data_updates() { - $fields = array('wineid', 'rating'); + $fields = ['wineid', 'rating']; $query = db_insert('migrate_example_advanced_updates') - ->fields($fields); - $data = array( - array(1, 93), - array(2, NULL), - ); + ->fields($fields); + $data = [ + [1, 93], + [2, NULL], + ]; foreach ($data as $row) { $query->values(array_combine($fields, $row)); } $query->execute(); } +/** + * Populate producer table. + */ function migrate_example_advanced_data_producer() { - $fields = array('producerid', 'name', 'body', 'excerpt', 'accountid'); + $fields = ['producerid', 'name', 'body', 'excerpt', 'accountid']; $query = db_insert('migrate_example_advanced_producer') - ->fields($fields); - $data = array( - array(1, 'Montes', 'Fine Chilean winery', 'Great!', 9), - array(2, 'Archeo', 'Sicilia!', NULL, 3), - ); + ->fields($fields); + $data = [ + [1, 'Montes', 'Fine Chilean winery', 'Great!', 9], + [2, 'Archeo', 'Sicilia!', NULL, 3], + ]; foreach ($data as $row) { $query->values(array_combine($fields, $row)); } $query->execute(); } +/** + * Populate account table. + */ function migrate_example_advanced_data_account() { - $fields = array('accountid', 'status', 'posted', 'last_access', 'last_login', - 'name', 'sex', 'password', 'mail', 'original_mail', 'sig', 'imageid', 'positions'); + $fields = [ + 'accountid', + 'status', + 'posted', + 'last_access', + 'last_login', + 'name', + 'sex', + 'password', + 'mail', + 'original_mail', + 'sig', + 'imageid', + 'positions', + ]; $query = db_insert('migrate_example_advanced_account') ->fields($fields); - $data = array( - array(1, 1, '2010-03-30 10:31:05', '2010-04-30 18:25:24', '2010-04-30 14:01:02', - 'darren', 'M', 'dpass', 'ddarren@example.com', 'darren@example.com', - 'All about the Australians', NULL, '5'), - array(3, 0, '2007-03-15 10:31:05', '2007-06-10 04:11:38', '2007-06-10 04:11:38', - 'emily', 'F', 'insecure', 'emily@example.com', 'emily@example.com', - 'Sommelier to the stars', NULL, '18'), - array(9, 1, '2004-02-29 10:31:05', '2004-02-29 10:31:05', '2004-02-29 10:31:05', - 'fonzie', NULL, 'bike', 'thefonz@example.com', 'arthur@example.com', - 'Aaay!', 1, '5,18'), - ); + $data = [ + [ + 1, + 1, + '2010-03-30 10:31:05', + '2010-04-30 18:25:24', + '2010-04-30 14:01:02', + 'darren', + 'M', + 'dpass', + 'ddarren@example.com', + 'darren@example.com', + 'All about the Australians', + NULL, + '5', + ], + [ + 3, + 0, + '2007-03-15 10:31:05', + '2007-06-10 04:11:38', + '2007-06-10 04:11:38', + 'emily', + 'F', + 'insecure', + 'emily@example.com', + 'emily@example.com', + 'Sommelier to the stars', + NULL, + '18', + ], + [ + 9, + 1, + '2004-02-29 10:31:05', + '2004-02-29 10:31:05', + '2004-02-29 10:31:05', + 'fonzie', + NULL, + 'bike', + 'thefonz@example.com', + 'arthur@example.com', + 'Aaay!', + 1, + '5,18', + ], + ]; foreach ($data as $row) { $query->values(array_combine($fields, $row)); } $query->execute(); } +/** + * Populate account updates table. + */ function migrate_example_advanced_data_account_updates() { - $fields = array('accountid', 'sex'); + $fields = ['accountid', 'sex']; $query = db_insert('migrate_example_advanced_account_updates') - ->fields($fields); - $data = array( - array(1, NULL), - array(3, 'M'), - array(9, 'F'), - ); + ->fields($fields); + $data = [ + [1, NULL], + [3, 'M'], + [9, 'F'], + ]; foreach ($data as $row) { $query->values(array_combine($fields, $row)); } $query->execute(); } +/** + * Populate comment table. + */ function migrate_example_advanced_data_comment() { - $fields = array('commentid', 'wineid', 'comment_parent', 'subject', 'body', - 'name', 'mail', 'accountid', 'commenthost', 'userpage', 'posted', 'lastchanged'); + $fields = [ + 'commentid', + 'wineid', + 'comment_parent', + 'subject', + 'body', + 'name', + 'mail', + 'accountid', + 'commenthost', + 'userpage', + 'posted', + 'lastchanged', + ]; $query = db_insert('migrate_example_advanced_comment') ->fields($fields); - $data = array( - array(1, 1, NULL, 'im first', 'Tasty', 'grace', 'grace@example.com', 0, - '123.456.78.9', 'http:://grace.example.com/', - strtotime('2010-01-02 03:04:05'), strtotime('2010-04-05 06:07:08')), - array(2, 1, NULL, 'im second', 'Delicious', 'horace', 'horace@example.com', 0, - 'example.com', NULL, - strtotime('2010-02-02 03:04:05'), strtotime('2010-05-05 06:07:08')), - array(3, 1, NULL, 'im parent', 'Don\'t care for it', 'irene', 'irene@example.com', 0, - '254.0.2.5', 'http:://www.example.com/irene', - strtotime('2010-03-02 03:04:05'), strtotime('2010-03-02 03:04:05')), - array(4, 1, 3, 'im child', 'But it\'s so good!', 'emily', NULL, 3, - '58.29.126.1', 'http:://www.wine.com/', - strtotime('2010-01-02 03:04:05'), strtotime('2010-01-02 03:04:05')), - array(5, 1, 4, 'im grandchild', 'Right on, Emily!', 'thefonz@example.com', NULL, 9, - '123.456.78.9', NULL, - strtotime('2010-06-02 03:04:05'), strtotime('2010-06-02 03:04:05')), - ); + $data = [ + [ + 1, + 1, + NULL, + 'im first', + 'Tasty', + 'grace', + 'grace@example.com', + 0, + '123.456.78.9', + 'http:://grace.example.com/', + strtotime('2010-01-02 03:04:05'), + strtotime('2010-04-05 06:07:08'), + ], + [ + 2, + 1, + NULL, + 'im second', + 'Delicious', + 'horace', + 'horace@example.com', + 0, + 'example.com', + NULL, + strtotime('2010-02-02 03:04:05'), + strtotime('2010-05-05 06:07:08'), + ], + [ + 3, + 1, + NULL, + 'im parent', + 'Don\'t care for it', + 'irene', + 'irene@example.com', + 0, + '254.0.2.5', + 'http:://www.example.com/irene', + strtotime('2010-03-02 03:04:05'), + strtotime('2010-03-02 03:04:05'), + ], + [ + 4, + 1, + 3, + 'im child', + 'But it\'s so good!', + 'emily', + NULL, + 3, + '58.29.126.1', + 'http:://www.wine.com/', + strtotime('2010-01-02 03:04:05'), + strtotime('2010-01-02 03:04:05'), + ], + [ + 5, + 1, + 4, + 'im grandchild', + 'Right on, Emily!', + 'thefonz@example.com', + NULL, + 9, + '123.456.78.9', + NULL, + strtotime('2010-06-02 03:04:05'), + strtotime('2010-06-02 03:04:05'), + ], + ]; foreach ($data as $row) { $query->values(array_combine($fields, $row)); } $query->execute(); } +/** + * Populate comment updates table. + */ function migrate_example_advanced_data_comment_updates() { - $fields = array('commentid', 'subject'); + $fields = ['commentid', 'subject']; $query = db_insert('migrate_example_advanced_comment_updates') - ->fields($fields); - $data = array( - array(1, 'I am first'), - array(2, 'I am second'), - array(3, 'I am parent'), - array(4, ''), - array(5, 'I am Spartacus'), - ); + ->fields($fields); + $data = [ + [1, 'I am first'], + [2, 'I am second'], + [3, 'I am parent'], + [4, ''], + [5, 'I am Spartacus'], + ]; foreach ($data as $row) { $query->values(array_combine($fields, $row)); } $query->execute(); } +/** + * Populate categories table. + */ function migrate_example_advanced_data_categories() { - $fields = array('categoryid', 'type', 'name', 'category_parent', 'details', 'ordering'); + $fields = [ + 'categoryid', + 'type', + 'name', + 'category_parent', + 'details', + 'ordering', + ]; $query = db_insert('migrate_example_advanced_categories') - ->fields($fields); - $data = array( - array(1, 'variety', 'White wine', NULL, 'White wines are generally simpler and sweeter than red', 3), - array(3, 'variety', 'Red wine', NULL, 'Red wines are generally more complex and "dry" than white', 1), - array(8, 'variety', 'Riesling', 1, 'Associated with Germany', 2), - array(9, 'variety', 'Chardonnay', 1, 'One of the most popular whites', 1), - array(13, 'variety', 'Merlot', 3, 'Very drinkable', 4), - array(14, 'variety', 'Syrah', 3, 'A.k.a. shiraz', -3), - array(25, 'variety', 'Cabernet Sauvignon', 3, 'A basic', -5), - array(26, 'variety', "Nero d'Avola", 3, 'Sicilian specialty', 2), - array(2, 'region', 'Italy', NULL, 'Largest producer of wine', 5), - array(11, 'region', 'Tuscany', 2, NULL, 2), - array(18, 'region', 'Chianti', 11, NULL, -1), - array(19, 'region', 'Elba', 11, NULL, 5), - array(4, 'region', 'France', NULL, 'C\'est bon', 6), - array(5, 'region', 'Bordeaux', 4, NULL, 1), - array(6, 'region', 'Barsac', 5, NULL, 3), - array(7, 'region', 'Pomerol', 5, NULL, 2), - array(16, 'region', 'Chile', NULL, NULL, 3), - array(17, 'region', 'Colchagua Valley', 16, NULL, 1), - array(20, 'region', 'California', NULL, NULL, 5), - array(21, 'region', 'Redwood Valley', 20, NULL, 1), - array(10, 'best_with', 'Beef', NULL, NULL, 5), - array(12, 'best_with', 'Pork', NULL, NULL, -3), - array(15, 'best_with', 'Chicken', NULL, NULL, -5), - ); + ->fields($fields); + $data = [ + [ + 1, + 'variety', + 'White wine', + NULL, + 'White wines are generally simpler and sweeter than red', + 3, + ], + [ + 3, + 'variety', + 'Red wine', + NULL, + 'Red wines are generally more complex and "dry" than white', + 1, + ], + [8, 'variety', 'Riesling', 1, 'Associated with Germany', 2], + [9, 'variety', 'Chardonnay', 1, 'One of the most popular whites', 1], + [13, 'variety', 'Merlot', 3, 'Very drinkable', 4], + [14, 'variety', 'Syrah', 3, 'A.k.a. shiraz', -3], + [25, 'variety', 'Cabernet Sauvignon', 3, 'A basic', -5], + [26, 'variety', "Nero d'Avola", 3, 'Sicilian specialty', 2], + [2, 'region', 'Italy', NULL, 'Largest producer of wine', 5], + [11, 'region', 'Tuscany', 2, NULL, 2], + [18, 'region', 'Chianti', 11, NULL, -1], + [19, 'region', 'Elba', 11, NULL, 5], + [4, 'region', 'France', NULL, 'C\'est bon', 6], + [5, 'region', 'Bordeaux', 4, NULL, 1], + [6, 'region', 'Barsac', 5, NULL, 3], + [7, 'region', 'Pomerol', 5, NULL, 2], + [16, 'region', 'Chile', NULL, NULL, 3], + [17, 'region', 'Colchagua Valley', 16, NULL, 1], + [20, 'region', 'California', NULL, NULL, 5], + [21, 'region', 'Redwood Valley', 20, NULL, 1], + [10, 'best_with', 'Beef', NULL, NULL, 5], + [12, 'best_with', 'Pork', NULL, NULL, -3], + [15, 'best_with', 'Chicken', NULL, NULL, -5], + ]; foreach ($data as $row) { $query->values(array_combine($fields, $row)); } $query->execute(); } +/** + * Populate vintages table. + */ function migrate_example_advanced_data_vintages() { - $fields = array('wineid', 'vintage'); + $fields = ['wineid', 'vintage']; $query = db_insert('migrate_example_advanced_vintages') - ->fields($fields); - $data = array( - array(1, 2006), - array(1, 2007), - array(2, 2001), - ); + ->fields($fields); + $data = [ + [1, 2006], + [1, 2007], + [2, 2001], + ]; foreach ($data as $row) { $query->values(array_combine($fields, $row)); } $query->execute(); } +/** + * Populate variety updates table. + */ function migrate_example_advanced_data_variety_updates() { - $fields = array('categoryid', 'details'); + $fields = ['categoryid', 'details']; $query = db_insert('migrate_example_advanced_variety_updates') - ->fields($fields); - $data = array( - array(1, 'White wines are simpler and sweeter than red'), - array(3, 'Red wines are generally more complex and dry than white'), - array(8, 'Usually associated with Germany'), - array(9, NULL), - array(13, 'Common, very drinakable'), - array(14, 'AKA Shiraz'), - array(25, 'Basic'), - array(26, 'A specialty of Sicily'), - ); + ->fields($fields); + $data = [ + [1, 'White wines are simpler and sweeter than red'], + [3, 'Red wines are generally more complex and dry than white'], + [8, 'Usually associated with Germany'], + [9, NULL], + [13, 'Common, very drinakable'], + [14, 'AKA Shiraz'], + [25, 'Basic'], + [26, 'A specialty of Sicily'], + ]; foreach ($data as $row) { $query->values(array_combine($fields, $row)); } $query->execute(); } +/** + * Populate category wine table. + */ function migrate_example_advanced_data_category_wine() { - $fields = array('wineid', 'categoryid'); + $fields = ['wineid', 'categoryid']; $query = db_insert('migrate_example_advanced_category_wine') ->fields($fields); - $data = array( - array(1, 12), - array(1, 15), - array(2, 10), - ); + $data = [ + [1, 12], + [1, 15], + [2, 10], + ]; foreach ($data as $row) { $query->values(array_combine($fields, $row)); } $query->execute(); } +/** + * Populate category producer table. + */ function migrate_example_advanced_data_category_producer() { - $fields = array('producerid', 'categoryid'); + $fields = ['producerid', 'categoryid']; $query = db_insert('migrate_example_advanced_category_producer') ->fields($fields); - $data = array( - array(1, 17), - ); + $data = [ + [1, 17], + ]; foreach ($data as $row) { $query->values(array_combine($fields, $row)); } $query->execute(); } +/** + * Populate files table. + */ function migrate_example_advanced_data_files() { - $fields = array('imageid', 'url', 'image_alt', 'image_title', 'wineid'); + $fields = ['imageid', 'url', 'image_alt', 'image_title', 'wineid']; $query = db_insert('migrate_example_advanced_files') ->fields($fields); - $data = array( - array(1, 'http://placekitten.com/200/200', NULL, NULL, NULL), - array(2, 'http://cyrve.com/files/penguin.jpeg', 'Penguin alt', 'Penguin title', 1), - array(3, 'http://cyrve.com/files/rioja.jpeg', 'Rioja alt', 'Rioja title', 2), - array(4, 'http://cyrve.com/files/boutisse_0.jpeg', 'Boutisse alt', 'Boutisse title', 2), - ); + $data = [ + [ + 1, + 'http://placekitten.com/200/200', + NULL, + NULL, + NULL, + ], + [ + 2, + 'http://cyrve.com/files/penguin.jpeg', + 'Penguin alt', + 'Penguin title', + 1, + ], + [3, 'http://cyrve.com/files/rioja.jpeg', 'Rioja alt', 'Rioja title', 2], + [ + 4, + 'http://cyrve.com/files/boutisse_0.jpeg', + 'Boutisse alt', + 'Boutisse title', + 2, + ], + ]; foreach ($data as $row) { $query->values(array_combine($fields, $row)); } $query->execute(); } +/** + * Populate blobs table. + */ function migrate_example_advanced_data_blobs() { $blob = file_get_contents('core/misc/druplicon.png'); - $fields = array('imageid', 'imageblob'); + $fields = ['imageid', 'imageblob']; $query = db_insert('migrate_example_advanced_blobs') ->fields($fields); - $data = array( - array(1, $blob), - ); + $data = [ + [1, $blob], + ]; foreach ($data as $row) { $query->values(array_combine($fields, $row)); } $query->execute(); } +/** + * Populate table source table. + */ function migrate_example_advanced_data_table_source() { - $fields = array('fooid', 'field1', 'field2'); + $fields = ['fooid', 'field1', 'field2']; $query = db_insert('migrate_example_advanced_table_source') ->fields($fields); - $data = array( - array(3, 'Some sample data', 58), - array(15, 'Whatever', 2), - array(646, 'More sample data', 34989), - ); + $data = [ + [3, 'Some sample data', 58], + [15, 'Whatever', 2], + [646, 'More sample data', 34989], + ]; foreach ($data as $row) { $query->values(array_combine($fields, $row)); } diff --git a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/src/Plugin/rest/resource/PositionResource.php b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/src/Plugin/rest/resource/PositionResource.php index f57e0c20ae..20fe34aadd 100644 --- a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/src/Plugin/rest/resource/PositionResource.php +++ b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/src/Plugin/rest/resource/PositionResource.php @@ -33,4 +33,12 @@ public function get() { return $response; } + /** + * {@inheritdoc} + */ + public function permissions() { + // Remove permissions so the resource is available to all. + return []; + } + } diff --git a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/src/Plugin/rest/resource/VarietyItems.php b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/src/Plugin/rest/resource/VarietyItems.php index f1045040a0..670134c345 100644 --- a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/src/Plugin/rest/resource/VarietyItems.php +++ b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/src/Plugin/rest/resource/VarietyItems.php @@ -31,22 +31,26 @@ public function get($variety = NULL) { $varieties = [ 'retsina' => [ 'name' => 'Retsina', - 'parent' => 1, // categoryid for 'white'. + // The categoryid for 'white'. + 'parent' => 1, 'details' => 'Greek', ], 'trebbiano' => [ 'name' => 'Trebbiano', - 'parent' => 1, // categoryid for 'white'. + // The categoryid for 'white'. + 'parent' => 1, 'details' => 'Italian', ], 'valpolicella' => [ 'name' => 'Valpolicella', - 'parent' => 3, // categoryid for 'red'. + // The categoryid for 'red'. + 'parent' => 3, 'details' => 'Italian Venoto region', ], 'bardolino' => [ 'name' => 'Bardolino', - 'parent' => 3, // categoryid for 'red'. + // The categoryid for 'red'. + 'parent' => 3, 'details' => 'Italian Venoto region', ], ]; @@ -61,4 +65,12 @@ public function get($variety = NULL) { return $response; } + /** + * {@inheritdoc} + */ + public function permissions() { + // Remove permissions so the resource is available to all. + return []; + } + } diff --git a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/src/Plugin/rest/resource/VarietyList.php b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/src/Plugin/rest/resource/VarietyList.php index 11847676cc..bb4bb60cbc 100644 --- a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/src/Plugin/rest/resource/VarietyList.php +++ b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/src/Plugin/rest/resource/VarietyList.php @@ -31,4 +31,12 @@ public function get() { return $response; } + /** + * {@inheritdoc} + */ + public function permissions() { + // Remove permissions so the resource is available to all. + return []; + } + } diff --git a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/src/Plugin/rest/resource/VarietyMultiFiles.php b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/src/Plugin/rest/resource/VarietyMultiFiles.php index 829dfa89fe..5db35b5ce8 100644 --- a/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/src/Plugin/rest/resource/VarietyMultiFiles.php +++ b/web/modules/migrate_plus/migrate_example_advanced/migrate_example_advanced_setup/src/Plugin/rest/resource/VarietyMultiFiles.php @@ -32,7 +32,8 @@ public function get($type = NULL) { if (strtolower($type) != 'white') { $data['variety'][] = [ 'name' => 'Amarone', - 'parent' => 3, // categoryid for 'red'. + // The categoryid for 'red'. + 'parent' => 3, 'details' => 'Italian Venoto region', 'attributes' => [ 'rich', @@ -41,7 +42,8 @@ public function get($type = NULL) { ]; $data['variety'][] = [ 'name' => 'Barbaresco', - 'parent' => 3, // categoryid for 'red'. + // The categoryid for 'red'. + 'parent' => 3, 'details' => 'Italian Piedmont region', 'attributes' => [ 'smoky', @@ -52,13 +54,15 @@ public function get($type = NULL) { if (strtolower($type) != 'red') { $data['variety'][] = [ 'name' => 'Kir', - 'parent' => 1, // categoryid for 'white'. + // The categoryid for 'white'. + 'parent' => 1, 'details' => 'French Burgundy region', 'attributes' => [], ]; $data['variety'][] = [ 'name' => 'Pinot Grigio', - 'parent' => 1, // categoryid for 'white'. + // The categoryid for 'white'. + 'parent' => 1, 'details' => 'From the northeast of Italy', 'attributes' => [ 'fruity', @@ -72,4 +76,12 @@ public function get($type = NULL) { return $response; } + /** + * {@inheritdoc} + */ + public function permissions() { + // Remove permissions so the resource is available to all. + return []; + } + } diff --git a/web/modules/migrate_plus/migrate_example_advanced/src/Plugin/migrate/source/WineTerm.php b/web/modules/migrate_plus/migrate_example_advanced/src/Plugin/migrate/source/WineTerm.php index 4bc143fcdf..3d3d235dd2 100644 --- a/web/modules/migrate_plus/migrate_example_advanced/src/Plugin/migrate/source/WineTerm.php +++ b/web/modules/migrate_plus/migrate_example_advanced/src/Plugin/migrate/source/WineTerm.php @@ -5,8 +5,7 @@ use Drupal\migrate\Plugin\migrate\source\SqlBase; /** - * A straight-forward SQL-based source plugin, to retrieve category data from - * the source database. + * A SQL-based source plugin, to retrieve category data from a source database. * * @MigrateSource( * id = "wine_term" @@ -18,8 +17,16 @@ class WineTerm extends SqlBase { * {@inheritdoc} */ public function query() { + $fields = [ + 'categoryid', + 'type', + 'name', + 'details', + 'category_parent', + 'ordering', + ]; return $this->select('migrate_example_advanced_categories', 'wc') - ->fields('wc', ['categoryid', 'type', 'name', 'details', 'category_parent', 'ordering']) + ->fields('wc', $fields) // This sort assures that parents are saved before children. ->orderBy('category_parent', 'ASC'); } diff --git a/web/modules/migrate_plus/migrate_plus.info.yml b/web/modules/migrate_plus/migrate_plus.info.yml index 7f2eb4fd11..c5c0e29c67 100644 --- a/web/modules/migrate_plus/migrate_plus.info.yml +++ b/web/modules/migrate_plus/migrate_plus.info.yml @@ -4,11 +4,10 @@ description: 'Enhancements to core migration support' package: Migration # core: 8.x dependencies: - - system (>=8.1) - - migrate + - drupal:migrate (>=8.3) -# Information added by Drupal.org packaging script on 2016-08-05 -version: '8.x-2.0-beta2' +# Information added by Drupal.org packaging script on 2018-09-06 +version: '8.x-4.0' core: '8.x' project: 'migrate_plus' -datestamp: 1470428640 +datestamp: 1536264189 diff --git a/web/modules/migrate_plus/migrate_plus.module b/web/modules/migrate_plus/migrate_plus.module index ce7f5bee5e..0dbddc4735 100644 --- a/web/modules/migrate_plus/migrate_plus.module +++ b/web/modules/migrate_plus/migrate_plus.module @@ -18,6 +18,24 @@ function migrate_plus_migration_plugins_alter(array &$migrations) { /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ foreach ($migrations as $id => $migration) { + // Add the default class where empty. + if (empty($migration['class'])) { + $migrations[$id]['class'] = 'Drupal\migrate\Plugin\Migration'; + } + + // For derived configuration entity-based migrations, strip the deriver + // prefix so we can reference migrations by the IDs they specify (i.e., + // the migration that specifies "id: temp" can be referenced as "temp" + // rather than "migration_config_deriver:temp"). + $prefix = 'migration_config_deriver:'; + if (strpos($id, $prefix) === 0) { + $new_id = substr($id, strlen($prefix)); + $migrations[$new_id] = $migrations[$id]; + unset($migrations[$id]); + $id = $new_id; + } + + // Integrate shared group configuration into the migration. if (empty($migration['migration_group'])) { $migration['migration_group'] = 'default'; } diff --git a/web/modules/migrate_plus/migrate_plus.services.yml b/web/modules/migrate_plus/migrate_plus.services.yml index 013fa2ca59..2ca3363c19 100644 --- a/web/modules/migrate_plus/migrate_plus.services.yml +++ b/web/modules/migrate_plus/migrate_plus.services.yml @@ -1,10 +1,10 @@ services: + plugin.manager.migrate_plus.authentication: + class: Drupal\migrate_plus\AuthenticationPluginManager + parent: default_plugin_manager plugin.manager.migrate_plus.data_fetcher: class: Drupal\migrate_plus\DataFetcherPluginManager parent: default_plugin_manager plugin.manager.migrate_plus.data_parser: class: Drupal\migrate_plus\DataParserPluginManager parent: default_plugin_manager - plugin.manager.config_entity_migration: - class: Drupal\migrate_plus\Plugin\MigrationConfigEntityPluginManager - parent: plugin.manager.migration diff --git a/web/modules/migrate_plus/migrations/migration_config_deriver.yml b/web/modules/migrate_plus/migrations/migration_config_deriver.yml new file mode 100644 index 0000000000..9fc597e7da --- /dev/null +++ b/web/modules/migrate_plus/migrations/migration_config_deriver.yml @@ -0,0 +1,6 @@ +id: migration_config_deriver +deriver: Drupal\migrate_plus\Plugin\MigrationConfigDeriver +# @todo: Remove if/when https://www.drupal.org/node/2797421 is fixed. +# Unused source configuration must be added to prevent errors. +source: + plugin: embedded_data diff --git a/web/modules/migrate_plus/phpcs.xml b/web/modules/migrate_plus/phpcs.xml new file mode 100644 index 0000000000..a18054efbc --- /dev/null +++ b/web/modules/migrate_plus/phpcs.xml @@ -0,0 +1,207 @@ +<?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/Annotation/Authentication.php b/web/modules/migrate_plus/src/Annotation/Authentication.php new file mode 100644 index 0000000000..5b63e1af84 --- /dev/null +++ b/web/modules/migrate_plus/src/Annotation/Authentication.php @@ -0,0 +1,37 @@ +<?php + +namespace Drupal\migrate_plus\Annotation; + +use Drupal\Component\Annotation\Plugin; + +/** + * Defines an authentication annotation object. + * + * Plugin namespace: Plugin\migrate_plus\authentication. + * + * @see \Drupal\migrate_plus\AuthenticationPluginBase + * @see \Drupal\migrate_plus\AuthenticationPluginInterface + * @see \Drupal\migrate_plus\AuthenticationPluginManager + * @see plugin_api + * + * @Annotation + */ +class Authentication extends Plugin { + + /** + * The plugin ID. + * + * @var string + */ + public $id; + + /** + * The title of the plugin. + * + * @var \Drupal\Core\Annotation\Translation + * + * @ingroup plugin_translatable + */ + public $title; + +} diff --git a/web/modules/migrate_plus/src/Annotation/DataFetcher.php b/web/modules/migrate_plus/src/Annotation/DataFetcher.php index 5dba1df8b8..5c6e24ccb9 100644 --- a/web/modules/migrate_plus/src/Annotation/DataFetcher.php +++ b/web/modules/migrate_plus/src/Annotation/DataFetcher.php @@ -7,7 +7,7 @@ /** * Defines a data fetcher annotation object. * - * Plugin Namespace: Plugin\migrate_plus\data_fetcher + * Plugin namespace: Plugin\migrate_plus\data_fetcher. * * @see \Drupal\migrate_plus\DataFetcherPluginBase * @see \Drupal\migrate_plus\DataFetcherPluginInterface diff --git a/web/modules/migrate_plus/src/Annotation/DataParser.php b/web/modules/migrate_plus/src/Annotation/DataParser.php index 6f9de0aecc..6bc01e24aa 100644 --- a/web/modules/migrate_plus/src/Annotation/DataParser.php +++ b/web/modules/migrate_plus/src/Annotation/DataParser.php @@ -7,7 +7,7 @@ /** * Defines a data parser annotation object. * - * Plugin Namespace: Plugin\migrate_plus\data_parser + * Plugin namespace: Plugin\migrate_plus\data_parser. * * @see \Drupal\migrate_plus\DataParserPluginBase * @see \Drupal\migrate_plus\DataParserPluginInterface diff --git a/web/modules/migrate_plus/src/AuthenticationPluginBase.php b/web/modules/migrate_plus/src/AuthenticationPluginBase.php new file mode 100644 index 0000000000..8833512acd --- /dev/null +++ b/web/modules/migrate_plus/src/AuthenticationPluginBase.php @@ -0,0 +1,25 @@ +<?php + +namespace Drupal\migrate_plus; + +use Drupal\Core\Plugin\PluginBase; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Defines a base authentication implementation. + * + * @see \Drupal\migrate_plus\Annotation\Authentication + * @see \Drupal\migrate_plus\AuthenticationPluginInterface + * @see \Drupal\migrate_plus\AuthenticationPluginManager + * @see plugin_api + */ +abstract class AuthenticationPluginBase extends PluginBase implements AuthenticationPluginInterface { + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static($configuration, $plugin_id, $plugin_definition); + } + +} diff --git a/web/modules/migrate_plus/src/AuthenticationPluginInterface.php b/web/modules/migrate_plus/src/AuthenticationPluginInterface.php new file mode 100644 index 0000000000..5bdc60309f --- /dev/null +++ b/web/modules/migrate_plus/src/AuthenticationPluginInterface.php @@ -0,0 +1,25 @@ +<?php + +namespace Drupal\migrate_plus; + +/** + * Defines an interface for authenticaion handlers. + * + * @see \Drupal\migrate_plus\Annotation\Authentication + * @see \Drupal\migrate_plus\AuthenticationPluginBase + * @see \Drupal\migrate_plus\AuthenticationPluginManager + * @see plugin_api + */ +interface AuthenticationPluginInterface { + + /** + * Performs authentication, returning any options to be added to the request. + * + * @return array + * Options (such as Authentication headers) to be added to the request. + * + * @link http://docs.guzzlephp.org/en/latest/request-options.html + */ + public function getAuthenticationOptions(); + +} diff --git a/web/modules/migrate_plus/src/AuthenticationPluginManager.php b/web/modules/migrate_plus/src/AuthenticationPluginManager.php new file mode 100644 index 0000000000..041094e4d7 --- /dev/null +++ b/web/modules/migrate_plus/src/AuthenticationPluginManager.php @@ -0,0 +1,37 @@ +<?php + +namespace Drupal\migrate_plus; + +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Plugin\DefaultPluginManager; + +/** + * Provides a plugin manager for authentication handlers. + * + * @see \Drupal\migrate_plus\Annotation\DataFetcher + * @see \Drupal\migrate_plus\DataFetcherPluginBase + * @see \Drupal\migrate_plus\DataFetcherPluginInterface + * @see plugin_api + */ +class AuthenticationPluginManager extends DefaultPluginManager { + + /** + * Constructs a new AuthenticationPluginManager. + * + * @param \Traversable $namespaces + * An object that implements \Traversable which contains the root paths + * keyed by the corresponding namespace to look for plugin implementations. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * Cache backend instance to use. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler to invoke the alter hook with. + */ + public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) { + parent::__construct('Plugin/migrate_plus/authentication', $namespaces, $module_handler, 'Drupal\migrate_plus\AuthenticationPluginInterface', 'Drupal\migrate_plus\Annotation\Authentication'); + + $this->alterInfo('authentication_info'); + $this->setCacheBackend($cache_backend, 'migrate_plus_plugins_authentication'); + } + +} diff --git a/web/modules/migrate_plus/src/DataFetcherPluginInterface.php b/web/modules/migrate_plus/src/DataFetcherPluginInterface.php index a64b47923c..f35042fcff 100644 --- a/web/modules/migrate_plus/src/DataFetcherPluginInterface.php +++ b/web/modules/migrate_plus/src/DataFetcherPluginInterface.php @@ -15,7 +15,7 @@ interface DataFetcherPluginInterface { /** * Set the client headers. * - * @param $headers + * @param array $headers * An array of the headers to set on the HTTP request. */ public function setRequestHeaders(array $headers); @@ -28,7 +28,7 @@ public function getRequestHeaders(); /** * Return content. * - * @param $url + * @param string $url * URL to retrieve from. * * @return string @@ -39,10 +39,11 @@ public function getResponseContent($url); /** * Return Http Response object for a given url. * - * @param $url + * @param string $url * URL to retrieve from. * * @return \Psr\Http\Message\ResponseInterface + * The HTTP response message. */ public function getResponse($url); diff --git a/web/modules/migrate_plus/src/DataFetcherPluginManager.php b/web/modules/migrate_plus/src/DataFetcherPluginManager.php index 9e9e238e7a..941252ed71 100644 --- a/web/modules/migrate_plus/src/DataFetcherPluginManager.php +++ b/web/modules/migrate_plus/src/DataFetcherPluginManager.php @@ -21,7 +21,7 @@ class DataFetcherPluginManager extends DefaultPluginManager { * * @param \Traversable $namespaces * An object that implements \Traversable which contains the root paths - * keyed by the corresponding namespace to look for plugin implementations, + * keyed by the corresponding namespace to look for plugin implementations. * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend * Cache backend instance to use. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler diff --git a/web/modules/migrate_plus/src/DataParserPluginBase.php b/web/modules/migrate_plus/src/DataParserPluginBase.php index d133ba386f..c74c6c53ac 100644 --- a/web/modules/migrate_plus/src/DataParserPluginBase.php +++ b/web/modules/migrate_plus/src/DataParserPluginBase.php @@ -73,7 +73,6 @@ public static function create(ContainerInterface $container, array $configuratio return new static($configuration, $plugin_id, $plugin_definition); } - /** * Returns the initialized data fetcher plugin. * @@ -111,8 +110,11 @@ public function next() { $this->fetchNextRow(); // If there was no valid row there, try the next url (if any). if (is_null($this->currentItem)) { - if ($this->nextSource()) { + while ($this->nextSource()) { $this->fetchNextRow(); + if ($this->valid()) { + break; + } } } if ($this->valid()) { @@ -125,7 +127,7 @@ public function next() { /** * Opens the specified URL. * - * @param $url + * @param string $url * URL to open. * * @return bool @@ -134,8 +136,9 @@ public function next() { abstract protected function openSourceUrl($url); /** - * Retrieves the next row of data from the open source URL, populating - * currentItem. + * Retrieves the next row of data. populating currentItem. + * + * Retrieves from the open source URL. */ abstract protected function fetchNextRow(); diff --git a/web/modules/migrate_plus/src/DataParserPluginManager.php b/web/modules/migrate_plus/src/DataParserPluginManager.php index b69cc148d7..65d92b1b71 100644 --- a/web/modules/migrate_plus/src/DataParserPluginManager.php +++ b/web/modules/migrate_plus/src/DataParserPluginManager.php @@ -21,7 +21,7 @@ class DataParserPluginManager extends DefaultPluginManager { * * @param \Traversable $namespaces * An object that implements \Traversable which contains the root paths - * keyed by the corresponding namespace to look for plugin implementations, + * keyed by the corresponding namespace to look for plugin implementations. * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend * Cache backend instance to use. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler diff --git a/web/modules/migrate_plus/src/Event/MigrateEvents.php b/web/modules/migrate_plus/src/Event/MigrateEvents.php index 90a2155b8f..6ce44ee62e 100644 --- a/web/modules/migrate_plus/src/Event/MigrateEvents.php +++ b/web/modules/migrate_plus/src/Event/MigrateEvents.php @@ -16,7 +16,8 @@ final class MigrateEvents { * has read the inital source data into a Row object. Typically, this would be * used to add data to the row, manipulate the data into a canonical form, or * signal by exception that the row should be skipped. The event listener - * method receives a \Drupal\migrate_plus\Event\MigratePrepareRowEvent instance. + * method receives a \Drupal\migrate_plus\Event\MigratePrepareRowEvent + * instance. * * @Event * diff --git a/web/modules/migrate_plus/src/Event/MigratePrepareRowEvent.php b/web/modules/migrate_plus/src/Event/MigratePrepareRowEvent.php index 1c2032b28e..4727f3ee37 100644 --- a/web/modules/migrate_plus/src/Event/MigratePrepareRowEvent.php +++ b/web/modules/migrate_plus/src/Event/MigratePrepareRowEvent.php @@ -64,7 +64,7 @@ public function getRow() { /** * Gets the source plugin. * - * @return \Drupal\migrate\Plugin\MigrateSourceInterface $source + * @return \Drupal\migrate\Plugin\MigrateSourceInterface * The source plugin firing the event. */ public function getSource() { diff --git a/web/modules/migrate_plus/src/Plugin/Discovery/ConfigEntityDiscovery.php b/web/modules/migrate_plus/src/Plugin/Discovery/ConfigEntityDiscovery.php deleted file mode 100644 index 551c8a7a3c..0000000000 --- a/web/modules/migrate_plus/src/Plugin/Discovery/ConfigEntityDiscovery.php +++ /dev/null @@ -1,49 +0,0 @@ -<?php - -namespace Drupal\migrate_plus\Plugin\Discovery; - -use Drupal\Component\Plugin\Discovery\DiscoveryInterface; -use Drupal\Component\Plugin\Discovery\DiscoveryTrait; - -/** - * Allows configuration entities to define plugin definitions. - */ -class ConfigEntityDiscovery implements DiscoveryInterface { - - use DiscoveryTrait; - - /** - * Entity type to query. - * - * @var string - */ - protected $entityType; - - /** - * Construct a YamlDiscovery object. - * - * @param string $entity_type - * The entity type to query for. - */ - function __construct($entity_type) { - $this->entityType = $entity_type; - } - - /** - * {@inheritdoc} - */ - public function getDefinitions() { - $definition = \Drupal::entityTypeManager()->getDefinition($this->entityType); - $prefix = $definition->getConfigPrefix() . '.'; - $storage = \Drupal::service('config.storage'); - $query = \Drupal::entityQuery($this->entityType); - $ids = $query->execute(); - $definitions = []; - foreach ($ids as $id) { - $definitions[$id] = $storage->read($prefix . $id); - } - - return $definitions; - } - -} diff --git a/web/modules/migrate_plus/src/Plugin/MigrationConfigDeriver.php b/web/modules/migrate_plus/src/Plugin/MigrationConfigDeriver.php new file mode 100644 index 0000000000..0135444974 --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/MigrationConfigDeriver.php @@ -0,0 +1,28 @@ +<?php + +namespace Drupal\migrate_plus\Plugin; + +use Drupal\Component\Plugin\Derivative\DeriverBase; +use Drupal\migrate_plus\Entity\Migration; + +/** + * Expose migration entities in the active config store as derivative plugins. + */ +class MigrationConfigDeriver extends DeriverBase { + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + // Always rederive from scratch, because changes may have been made without + // clearing our internal cache. + $this->derivatives = []; + $migrations = Migration::loadMultiple(); + /** @var \Drupal\migrate_plus\Entity\MigrationInterface $migration */ + foreach ($migrations as $id => $migration) { + $this->derivatives[$id] = $migration->toArray(); + } + return $this->derivatives; + } + +} diff --git a/web/modules/migrate_plus/src/Plugin/MigrationConfigEntityPluginManager.php b/web/modules/migrate_plus/src/Plugin/MigrationConfigEntityPluginManager.php deleted file mode 100644 index 25009758d0..0000000000 --- a/web/modules/migrate_plus/src/Plugin/MigrationConfigEntityPluginManager.php +++ /dev/null @@ -1,25 +0,0 @@ -<?php - -namespace Drupal\migrate_plus\Plugin; - -use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator; -use Drupal\migrate\Plugin\MigrationPluginManager; -use Drupal\migrate_plus\Plugin\Discovery\ConfigEntityDiscovery; - -/** - * Plugin manager for migration plugins. - */ -class MigrationConfigEntityPluginManager extends MigrationPluginManager { - - /** - * {@inheritdoc} - */ - protected function getDiscovery() { - if (!isset($this->discovery)) { - $discovery = new ConfigEntityDiscovery('migration'); - $this->discovery = new ContainerDerivativeDiscoveryDecorator($discovery); - } - return $this->discovery; - } - -} diff --git a/web/modules/migrate_plus/src/Plugin/migrate/destination/Table.php b/web/modules/migrate_plus/src/Plugin/migrate/destination/Table.php new file mode 100644 index 0000000000..f04d9f66d7 --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate/destination/Table.php @@ -0,0 +1,143 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate\destination; + +use Drupal\Core\Database\Connection; +use Drupal\Core\Database\Database; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\migrate\MigrateException; +use Drupal\migrate\MigrateSkipProcessException; +use Drupal\migrate\Plugin\MigrationInterface; +use Drupal\migrate\Plugin\migrate\destination\DestinationBase; +use Drupal\migrate\Row; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Provides table destination plugin. + * + * Use this plugin for a table not registered with Drupal Schema API. + * + * @MigrateDestination( + * id = "table" + * ) + */ +class Table extends DestinationBase implements ContainerFactoryPluginInterface { + + /** + * 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; + + /** + * The database connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $dbConnection; + + /** + * Constructs a new Table. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\migrate\Plugin\MigrationInterface $migration + * The migration. + * @param \Drupal\Core\Database\Connection $connection + * The database connection. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, Connection $connection) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $migration); + $this->dbConnection = $connection; + $this->tableName = $configuration['table_name']; + $this->idFields = $configuration['id_fields']; + $this->fields = isset($configuration['fields']) ? $configuration['fields'] : []; + $this->supportsRollback = TRUE; + } + + /** + * {@inheritdoc} + */ + 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, + $plugin_definition, + $migration, + Database::getConnection('default', $db_key) + ); + } + + /** + * {@inheritdoc} + */ + public function getIds() { + if (empty($this->idFields)) { + throw new MigrateException('Id fields are required for a table destination'); + } + return $this->idFields; + } + + /** + * {@inheritdoc} + */ + public function fields(MigrationInterface $migration = NULL) { + return $this->fields; + } + + /** + * {@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.'); + } + + $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; + } + + /** + * {@inheritdoc} + */ + public function rollback(array $destination_identifier) { + $delete = $this->dbConnection->delete($this->tableName); + foreach ($destination_identifier as $field => $value) { + $delete->condition($field, $value); + } + $delete->execute(); + } + +} diff --git a/web/modules/migrate_plus/src/Plugin/migrate/process/ArrayPop.php b/web/modules/migrate_plus/src/Plugin/migrate/process/ArrayPop.php new file mode 100644 index 0000000000..d4749caff3 --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/ArrayPop.php @@ -0,0 +1,44 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate\process; + +use Drupal\migrate\MigrateException; +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\ProcessPluginBase; +use Drupal\migrate\Row; + +/** + * Performs an array_pop() on a source array. + * + * @MigrateProcessPlugin( + * id = "array_pop", + * handle_multiples = TRUE + * ) + * + * The "extract" plugin in core can extract array values when indexes are + * already known. This plugin helps extract the last value in an array by + * performing a "pop" operation. + * + * Example: Say, the migration source has an associative array of names in + * a property called "authors" and the keys in the array can vary, you + * can extract the last value like this: + * + * @code + * last_author: + * plugin: array_pop + * source: authors + * @endcode + */ +class ArrayPop extends ProcessPluginBase { + + /** + * {@inheritdoc} + */ + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + if (!is_array($value)) { + throw new MigrateException('Input should be an array.'); + } + return array_pop($value); + } + +} diff --git a/web/modules/migrate_plus/src/Plugin/migrate/process/ArrayShift.php b/web/modules/migrate_plus/src/Plugin/migrate/process/ArrayShift.php new file mode 100644 index 0000000000..02ef190b56 --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/ArrayShift.php @@ -0,0 +1,44 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate\process; + +use Drupal\migrate\MigrateException; +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\ProcessPluginBase; +use Drupal\migrate\Row; + +/** + * Performs an array_shift() on a source array. + * + * @MigrateProcessPlugin( + * id = "array_shift", + * handle_multiples = TRUE + * ) + * + * The "extract" plugin in core can extract array values when indexes are + * already known. This plugin helps extract the first value in an array by + * performing a "shift" operation. + * + * Example: Say, the migration source has an associative array of names in + * a property called "authors" and the keys in the array can vary, you + * can extract the first value like this: + * + * @code + * first_author: + * plugin: array_shift + * source: authors + * @endcode + */ +class ArrayShift extends ProcessPluginBase { + + /** + * {@inheritdoc} + */ + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + if (!is_array($value)) { + throw new MigrateException('Input should be an array.'); + } + return array_shift($value); + } + +} 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 ff14736ae8..a749a6388c 100644 --- a/web/modules/migrate_plus/src/Plugin/migrate/process/EntityGenerate.php +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/EntityGenerate.php @@ -1,17 +1,17 @@ <?php -/** - * @file - * Contains Drupal\migrate_plus\Plugin\migrate\process\EntityGenerate. - */ - namespace Drupal\migrate_plus\Plugin\migrate\process; +use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface; use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\Plugin\MigratePluginManager; +use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Row; +use Symfony\Component\DependencyInjection\ContainerInterface; /** - * This plugin generates entity stubs. + * This plugin generates entities within the process plugin. * * @MigrateProcessPlugin( * id = "entity_generate" @@ -19,12 +19,12 @@ * * @see EntityLookup * - * All the configuration from the lookup plugin applies here. In it's most + * All the configuration from the lookup plugin applies here. In its most * simple form, this plugin needs no configuration. If there are fields on the - * stub entity that are required or need some default value, that can be - * provided via a default_values configuration option. + * generated entity that are required or need some value, their values can be + * provided via values and/or default_values configuration options. * - * Example usage with default_values configuration: + * Example usage with values and default_values configuration: * @code * destination: * plugin: 'entity:node' @@ -32,21 +32,87 @@ * type: * plugin: default_value * default_value: page + * foo: bar * field_tags: * plugin: entity_generate * source: tags * default_values: - * description: Stub description - * field_long_description: Stub long description + * description: Default description + * values: + * field_long_description: some_source_field + * field_foo: '@foo' * @endcode */ class EntityGenerate extends EntityLookup { + /** + * The row from the source to process. + * + * @var \Drupal\migrate\Row + */ + protected $row; + + /** + * The MigrateExecutable instance. + * + * @var \Drupal\migrate\MigrateExecutable + */ + protected $migrateExecutable; + + /** + * The get process plugin instance. + * + * @var \Drupal\migrate\Plugin\migrate\process\Get + */ + protected $getProcessPlugin; + + /** + * EntityGenerate constructor. + * + * @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. + */ + 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']]); + } + } + + /** + * {@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') + ); + } + /** * {@inheritdoc} */ public function transform($value, MigrateExecutableInterface $migrateExecutable, Row $row, $destinationProperty) { - // Creates a stub entity if one doesn't exist. + $this->row = $row; + $this->migrateExecutable = $migrateExecutable; + // Creates an entity if the lookup determines it doesn't exist. if (!($result = parent::transform($value, $migrateExecutable, $row, $destinationProperty))) { $result = $this->generateEntity($value); } @@ -55,19 +121,19 @@ public function transform($value, MigrateExecutableInterface $migrateExecutable, } /** - * Generates stub entity for a given value. + * Generates an entity for a given value. * * @param string $value - * Value to use in creation of stub entity. + * Value to use in creation of the entity. * * @return int|string * The entity id of the generated entity. */ protected function generateEntity($value) { - if(!empty($value)) { + if (!empty($value)) { $entity = $this->entityManager ->getStorage($this->lookupEntityType) - ->create($this->stub($value)); + ->create($this->entity($value)); $entity->save(); return $entity->id(); @@ -75,32 +141,39 @@ protected function generateEntity($value) { } /** - * Fabricate a stub entity. + * Fabricate an entity. * * This is intended to be extended by implementing classes to provide for more * dynamic default values, rather than just static ones. * - * @param $value - * Value to use in creation of stub entity. + * @param mixed $value + * Primary value to use in creation of the entity. * * @return array - * The stub entity. + * Entity value array. */ - protected function stub($value) { - $stub = [$this->lookupValueKey => $value]; + protected function entity($value) { + $entity_values = [$this->lookupValueKey => $value]; if ($this->lookupBundleKey) { - $stub[$this->lookupBundleKey] = $this->lookupBundle; + $entity_values[$this->lookupBundleKey] = $this->lookupBundle; } // Gather any static default values for properties/fields. if (isset($this->configuration['default_values']) && is_array($this->configuration['default_values'])) { foreach ($this->configuration['default_values'] as $key => $value) { - $stub[$key] = $value; + $entity_values[$key] = $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; } } - return $stub; + return $entity_values; } } 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 674f5bfac2..2ae0d07c1d 100644 --- a/web/modules/migrate_plus/src/Plugin/migrate/process/EntityLookup.php +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/EntityLookup.php @@ -1,12 +1,8 @@ <?php -/** - * @file - * Contains Drupal\migrate_plus\Plugin\migrate\process\EntityGenerate. - */ - 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; @@ -56,34 +52,74 @@ */ class EntityLookup extends ProcessPluginBase implements ContainerFactoryPluginInterface { - /** @var \Drupal\Core\Entity\EntityManagerInterface */ + /** + * The entity manager. + * + * @var \Drupal\Core\Entity\EntityManagerInterface + */ protected $entityManager; - /** @var \Drupal\migrate\Plugin\MigrationInterface */ + /** + * The migration. + * + * @var \Drupal\migrate\Plugin\MigrationInterface + */ protected $migration; - /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface */ + /** + * The selection plugin. + * + * @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface + */ protected $selectionPluginManager; - /** @var string */ + /** + * The destination type. + * + * @var string + */ protected $destinationEntityType; - /** @var string|bool */ + /** + * The destination bundle. + * + * @var string|bool + */ protected $destinationBundleKey; - /** @var string */ + /** + * The lookup value's key. + * + * @var string + */ protected $lookupValueKey; - /** @var string */ + /** + * The lookup bundle's key. + * + * @var string + */ protected $lookupBundleKey; - /** @var string */ + /** + * The lookup bundle. + * + * @var string + */ protected $lookupBundle; - /** @var string */ + /** + * The lookup entity type. + * + * @var string + */ protected $lookupEntityType; - /** @var string */ + /** + * The destination property or field. + * + * @var string + */ protected $destinationProperty; /** @@ -117,9 +153,18 @@ public static function create(ContainerInterface $container, array $configuratio * {@inheritdoc} */ public function transform($value, MigrateExecutableInterface $migrateExecutable, Row $row, $destinationProperty) { + // If the source data is an empty array, return the same. + if (gettype($value) === 'array' && count($value) === 0) { + return []; + } + + // In case of subfields ('field_reference/target_id'), extract the field + // name only. + $parts = explode('/', $destinationProperty); + $destinationProperty = reset($parts); $this->determineLookupProperties($destinationProperty); - $this->destinationProperty = $this->configuration['destination_field']; + $this->destinationProperty = isset($this->configuration['destination_field']) ? $this->configuration['destination_field'] : NULL; return $this->query($value); } @@ -146,31 +191,41 @@ protected function determineLookupProperties($destinationProperty) { } if (empty($this->lookupValueKey) || empty($this->lookupBundleKey) || empty($this->lookupBundle) || empty($this->lookupEntityType)) { - // See if we can introspect the lookup properties from the destination field. + // 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); - if ($fieldConfig->getType() != 'entity_reference') { - throw new MigrateException('The entity_lookup plugin found no entity reference field.'); - } - - if (empty($this->lookupBundle)) { - $handlerSettings = $fieldConfig->getSetting('handler_settings'); - $bundles = array_filter((array) $handlerSettings['target_bundles']); - if (count($bundles) == 1) { - $this->lookupBundle = reset($bundles); - } - // This was added in 8.1.x is not supported in 8.0.x. - elseif (!empty($handlerSettings['auto_create']) && !empty($handlerSettings['auto_create_bundle'])) { - $this->lookupBundle = reset($handlerSettings['auto_create_bundle']); - } + switch ($fieldConfig->getType()) { + case 'entity_reference': + if (empty($this->lookupBundle)) { + $handlerSettings = $fieldConfig->getSetting('handler_settings'); + $bundles = array_filter((array) $handlerSettings['target_bundles']); + if (count($bundles) == 1) { + $this->lookupBundle = reset($bundles); + } + // This was added in 8.1.x is not supported in 8.0.x. + elseif (!empty($handlerSettings['auto_create']) && !empty($handlerSettings['auto_create_bundle'])) { + $this->lookupBundle = reset($handlerSettings['auto_create_bundle']); + } + } + + // 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'); + break; + + case 'file': + case 'image': + $this->lookupEntityType = 'file'; + $this->lookupValueKey = $this->lookupValueKey ?: 'uri'; + break; + + default: + throw new MigrateException('Destination field type ' . + $fieldConfig->getType() . 'is not a recognized reference type.'); } - - // 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'); } } @@ -189,8 +244,8 @@ protected function determineLookupProperties($destinationProperty) { /** * Checks for the existence of some value. * - * @param $value - * The value to query. + * @param mixed $value + * The value to query. * * @return mixed|null * Entity id if the queried entity exists. Otherwise NULL. @@ -217,25 +272,25 @@ protected function query($value) { return NULL; } - if ($multiple && !empty($this->destinationProperty)) { - array_walk($results, function (&$value) { - $value = [$this->destinationProperty => $value]; - }); - - return array_values($results); - } - // By default do a case-sensitive comparison. if (!$ignoreCase) { // Returns the entity's identifier. - foreach ($results as $identifier) { - if ($value === $this->entityManager->getStorage($this->lookupEntityType)->load($identifier)->{$this->lookupValueKey}->value) { - return $identifier; + foreach ($results as $k => $identifier) { + $entity = $this->entityManager->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]); } } } - return reset($results); + if ($multiple && !empty($this->destinationProperty)) { + array_walk($results, function (&$value) { + $value = [$this->destinationProperty => $value]; + }); + } + + return $multiple ? array_values($results) : reset($results); } } diff --git a/web/modules/migrate_plus/src/Plugin/migrate/process/FileBlob.php b/web/modules/migrate_plus/src/Plugin/migrate/process/FileBlob.php new file mode 100644 index 0000000000..22341b6bc6 --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/FileBlob.php @@ -0,0 +1,155 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate\process; + +use Drupal\Core\File\FileSystemInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\MigrateSkipProcessException; +use Drupal\migrate\ProcessPluginBase; +use Drupal\migrate\Row; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Copy a file from a blob into a file. + * + * @MigrateProcessPlugin( + * id = "file_blob" + * ) + */ +class FileBlob extends ProcessPluginBase implements ContainerFactoryPluginInterface { + + /** + * The file system service. + * + * @var \Drupal\Core\File\FileSystemInterface + */ + protected $fileSystem; + + /** + * Constructs a file_blob process plugin. + * + * @param array $configuration + * The plugin configuration. + * @param string $plugin_id + * The plugin ID. + * @param mixed $plugin_definition + * The plugin definition. + * @param \Drupal\Core\File\FileSystemInterface $file_system + * The file system service. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, FileSystemInterface $file_system) { + $configuration += [ + 'reuse' => FALSE, + ]; + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->fileSystem = $file_system; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('file_system') + ); + } + + /** + * {@inheritdoc} + */ + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + // If we're stubbing a file entity, return a URI of NULL so it will get + // stubbed by the general process. + if ($row->isStub()) { + return NULL; + } + list($destination, $blob) = $value; + + // Determine if we going to overwrite existing files or not touch them. + $replace = $this->getOverwriteMode(); + + // Attempt to save the file to avoid calling file_prepare_directory() any + // more than absolutely necessary. + if ($this->putFile($destination, $blob, $replace)) { + return $destination; + } + $dir = $this->getDirectory($destination); + if (!file_prepare_directory($dir, FILE_CREATE_DIRECTORY)) { + throw new MigrateSkipProcessException("Could not create directory '$dir'"); + } + if ($this->putFile($destination, $blob, $replace)) { + return $destination; + } + throw new MigrateSkipProcessException("Blob data could not be copied to $destination."); + } + + /** + * Try to save the file. + * + * @param string $destination + * The destination path or URI. + * @param string $blob + * The base64 encoded file contents. + * @param int $replace + * (optional) FILE_EXISTS_REPLACE (default) or FILE_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)) { + if (file_put_contents($path, $blob)) { + return $path; + } + else { + return FALSE; + } + } + + // File was already copied. + return $destination; + } + + /** + * Determines how to handle file conflicts. + * + * @return int + * Either FILE_EXISTS_REPLACE (default) or FILE_EXISTS_ERROR, depending on + * the configuration. + */ + protected function getOverwriteMode() { + if (!empty($this->configuration['reuse'])) { + return FILE_EXISTS_ERROR; + } + + return FILE_EXISTS_REPLACE; + } + + /** + * Returns the directory component of a URI or path. + * + * For URIs like public://foo.txt, the full physical path of public:// + * will be returned, since a scheme by itself will trip up certain file + * API functions (such as file_prepare_directory()). + * + * @param string $uri + * The URI or path. + * + * @return string|false + * The directory component of the path or URI, or FALSE if it could not + * be determined. + */ + protected function getDirectory($uri) { + $dir = $this->fileSystem->dirname($uri); + if (substr($dir, -3) == '://') { + return $this->fileSystem->realpath($dir); + } + return $dir; + } + +} diff --git a/web/modules/migrate_plus/src/Plugin/migrate/process/Merge.php b/web/modules/migrate_plus/src/Plugin/migrate/process/Merge.php new file mode 100644 index 0000000000..463595e9d7 --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/Merge.php @@ -0,0 +1,68 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate\process; + +use Drupal\migrate\ProcessPluginBase; +use Drupal\migrate\MigrateException; +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\Row; + +/** + * This plugin merges arrays together. + * + * @MigrateProcessPlugin( + * id = "merge" + * ) + * + * Use to merge several fields into one. In the following example, imagine a D7 + * node with a field_collections field and an image field that migrations were + * written for to make paragraph entities in D8. We would like to add those + * paragraph entities to the 'paragraphs_field'. Consider the following: + * + * source: + * plugin: d7_node + * process: + * temp_body: + * plugin: iterator + * source: field_section + * process: + * target_id: + * plugin: migration_lookup + * migration: field_collection_field_section_to_paragraph + * source: value + * temp_images: + * plugin: iterator + * source: field_image + * process + * target_id: + * plugin: migration_lookup + * migration: image_entities_to_paragraph + * source: fid + * paragraphs_field: + * plugin: merge + * source: + * - '@temp_body' + * - '@temp_images' + * destination: + * plugin: 'entity:node' + */ +class Merge extends ProcessPluginBase { + + /** + * {@inheritdoc} + */ + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + if (!is_array($value)) { + 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) { + 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); + } + return $new_value; + } + +} diff --git a/web/modules/migrate_plus/src/Plugin/migrate/process/MultipleValues.php b/web/modules/migrate_plus/src/Plugin/migrate/process/MultipleValues.php new file mode 100644 index 0000000000..be6779a124 --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/MultipleValues.php @@ -0,0 +1,58 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate\process; + +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\ProcessPluginBase; +use Drupal\migrate\Row; + +/** + * Treat an array of values as a separate / individual values. + * + * @code + * process: + * field_authors: + * - + * plugin: explode + * delimiter: ', ' + * source: authors + * - + * plugin: single_value + * - + * plugin: callback + * callable: custom_sort_authors + * - + * plugin: multiple_values + * @endcode + * + * Assume the "authors" field contains comma separated author names. + * + * We split the names into multiple values and then use the "single_value" + * plugin to treat them as a single array of author names. After that, we + * pass the values through a custom sort. Callback multiple setting is false. To + * convert from a single value to multiple, use the "multiple_values" plugin. It + * will make the next plugin treat the values individually instead of an array + * of values. + * + * @MigrateProcessPlugin( + * id = "multiple_values", + * handle_multiples = TRUE + * ) + */ +class MultipleValues extends ProcessPluginBase { + + /** + * {@inheritdoc} + */ + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + return $value; + } + + /** + * {@inheritdoc} + */ + public function multiple() { + return TRUE; + } + +} diff --git a/web/modules/migrate_plus/src/Plugin/migrate/process/SingleValue.php b/web/modules/migrate_plus/src/Plugin/migrate/process/SingleValue.php new file mode 100644 index 0000000000..2583a9026c --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/SingleValue.php @@ -0,0 +1,45 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate\process; + +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\ProcessPluginBase; +use Drupal\migrate\Row; + +/** + * Treat an array of values as a single value. + * + * @code + * process: + * field_authors: + * - + * plugin: explode + * delimiter: ', ' + * source: authors + * - + * plugin: single_value + * @endcode + * + * Assume the "authors" field contains comma separated author names. + * + * After the explode, we end up with each author name as an individual value. + * But if we want to perform a sort on all values using a callback, we will + * need to send all the values to a callable together as an array of author + * names. Calling the "single_value" plugin in such a case will combine all the + * values into a single array for the next plugin. + * + * @MigrateProcessPlugin( + * id = "single_value", + * handle_multiples = TRUE + * ) + */ +class SingleValue extends ProcessPluginBase { + + /** + * {@inheritdoc} + */ + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + return $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 new file mode 100644 index 0000000000..1a0c8f545c --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/SkipOnValue.php @@ -0,0 +1,135 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate\process; + +use Drupal\migrate\MigrateException; +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\MigrateSkipProcessException; +use Drupal\migrate\MigrateSkipRowException; +use Drupal\migrate\ProcessPluginBase; +use Drupal\migrate\Row; + +/** + * If the source evaluates to a configured value, skip processing or whole row. + * + * @MigrateProcessPlugin( + * id = "skip_on_value" + * ) + * + * Available configuration keys: + * - 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. + * + * Examples: + * + * Example usage with minimal configuration: + * @code + * type: + * plugin: skip_on_value + * source: content_type + * method: row + * value: blog + * @endcode + * + * The above example will skip processing the input property if the content_type + * source field equals "blog". + * + * Example usage with full configuration: + * @code + * type: + * plugin: skip_on_value + * not_equals: true + * source: content_type + * method: row + * value: + * - 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". + */ +class SkipOnValue extends ProcessPluginBase { + + /** + * {@inheritdoc} + */ + public function row($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + if (empty($this->configuration['value']) && !array_key_exists('value', $this->configuration)) { + throw new MigrateException('Skip on value plugin is missing value configuration.'); + } + + if (is_array($this->configuration['value'])) { + $value_in_array = FALSE; + $not_equals = isset($this->configuration['not_equals']); + + foreach ($this->configuration['value'] as $skipValue) { + $value_in_array |= $this->compareValue($value, $skipValue); + } + + if (($not_equals && !$value_in_array) || (!$not_equals && $value_in_array)) { + throw new MigrateSkipRowException(); + } + } + elseif ($this->compareValue($value, $this->configuration['value'], !isset($this->configuration['not_equals']))) { + throw new MigrateSkipRowException(); + } + + return $value; + } + + /** + * {@inheritdoc} + */ + public function process($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + if (empty($this->configuration['value']) && !array_key_exists('value', $this->configuration)) { + throw new MigrateException('Skip on value plugin is missing value configuration.'); + } + + if (is_array($this->configuration['value'])) { + $value_in_array = FALSE; + $not_equals = isset($this->configuration['not_equals']); + + foreach ($this->configuration['value'] as $skipValue) { + $value_in_array |= $this->compareValue($value, $skipValue); + } + + if (($not_equals && !$value_in_array) || (!$not_equals && $value_in_array)) { + throw new MigrateSkipProcessException(); + } + } + elseif ($this->compareValue($value, $this->configuration['value'], !isset($this->configuration['not_equals']))) { + throw new MigrateSkipProcessException(); + } + + return $value; + } + + /** + * Compare values to see if they are equal. + * + * @param mixed $value + * Actual value. + * @param mixed $skipValue + * Value to compare against. + * @param bool $equal + * Compare as equal or not equal. + * + * @return bool + * True if the compare successfully, FALSE otherwise. + */ + protected function compareValue($value, $skipValue, $equal = TRUE) { + if ($equal) { + return (string) $value == (string) $skipValue; + } + + return (string) $value != (string) $skipValue; + + } + +} diff --git a/web/modules/migrate_plus/src/Plugin/migrate/process/StrReplace.php b/web/modules/migrate_plus/src/Plugin/migrate/process/StrReplace.php new file mode 100644 index 0000000000..2b033bebc3 --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/StrReplace.php @@ -0,0 +1,104 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate\process; + +use Drupal\migrate\MigrateException; +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\ProcessPluginBase; +use Drupal\migrate\Row; + +/** + * Uses the str_replace() method on a source string. + * + * @MigrateProcessPlugin( + * id = "str_replace" + * ) + * + * To do a simple hardcoded string replace use the following: + * + * @code + * field_text: + * plugin: str_replace + * source: text + * 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". + * + * Case insensitive searches can be achieved using the following: + * @code + * field_text: + * plugin: str_replace + * case_insensitive: true + * source: text + * 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". + * + * Also regular expressions can be matched using: + * @code + * field_text: + * plugin: str_replace + * regex: true + * source: text + * 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". + * + * 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. + */ +class StrReplace extends ProcessPluginBase { + + /** + * Flag indicating whether there are multiple values. + * + * @var bool + */ + protected $multiple; + + /** + * {@inheritdoc} + */ + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + if (!isset($this->configuration['search'])) { + throw new MigrateException('"search" must be configured.'); + } + if (!isset($this->configuration['replace'])) { + throw new MigrateException('"replace" must be configured.'); + } + $this->multiple = is_array($value); + $this->configuration += [ + 'case_insensitive' => FALSE, + 'regex' => FALSE, + ]; + $function = "str_replace"; + if ($this->configuration['case_insensitive']) { + $function = 'str_ireplace'; + } + if ($this->configuration['regex']) { + $function = 'preg_replace'; + } + return $function($this->configuration['search'], $this->configuration['replace'], $value); + } + + /** + * {@inheritdoc} + */ + public function multiple() { + return $this->multiple; + } + +} diff --git a/web/modules/migrate_plus/src/Plugin/migrate/process/Transliteration.php b/web/modules/migrate_plus/src/Plugin/migrate/process/Transliteration.php new file mode 100644 index 0000000000..217869e205 --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate/process/Transliteration.php @@ -0,0 +1,83 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate\process; + +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\Row; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Transliterates text from Unicode to US-ASCII. + * + * The transliteration process plugin takes the source value and runs it through + * the transliteration service. Letters will have language decorations and + * accents removed. + * + * Example: + * + * @code + * process: + * bar: + * plugin: transliteration + * source: foo + * @endcode + * + * If the value of foo in the source is 'áéÃ!' then the destination value of + * bar will be 'aei!'. + * + * @see \Drupal\migrate\Plugin\MigrateProcessInterface + * + * @MigrateProcessPlugin( + * id = "transliteration" + * ) + */ +class Transliteration extends ProcessPluginBase implements ContainerFactoryPluginInterface { + + /** + * The transliteration service. + * + * @var \Drupal\Component\Transliteration\TransliterationInterface + */ + protected $transliteration; + + /** + * Constructs a Transliteration plugin. + * + * @param array $configuration + * The plugin configuration. + * @param string $plugin_id + * The plugin ID. + * @param mixed $plugin_definition + * The plugin definition. + * @param \Drupal\Component\Transliteration\TransliterationInterface $transliteration + * The transliteration service. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, TransliterationInterface $transliteration) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->transliteration = $transliteration; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('transliteration') + ); + } + + /** + * {@inheritdoc} + */ + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + return $this->transliteration->transliterate($value, LanguageInterface::LANGCODE_DEFAULT, '_'); + } + +} diff --git a/web/modules/migrate_plus/src/Plugin/migrate/source/Url.php b/web/modules/migrate_plus/src/Plugin/migrate/source/Url.php index 92f6d449db..242223b618 100644 --- a/web/modules/migrate_plus/src/Plugin/migrate/source/Url.php +++ b/web/modules/migrate_plus/src/Plugin/migrate/source/Url.php @@ -3,7 +3,6 @@ namespace Drupal\migrate_plus\Plugin\migrate\source; use Drupal\migrate\Plugin\MigrationInterface; -use Drupal\migrate_plus\DataParserPluginInterface; /** * Source plugin for retrieving data via URLs. @@ -38,15 +37,6 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition parent::__construct($configuration, $plugin_id, $plugin_definition, $migration); $this->sourceUrls = $configuration['urls']; - - // Set a default Accept header. -/* $this->headers = array_merge(['Accept' => 'application/json'], - $configuration['headers'] ?: []);*/ - - // See if this is a paged response with next links. If so, add to the source_urls array. -/* foreach ( (array) $configuration['urls'] as $url) { - $this->sourceUrls += $this->getNextLinks($url); - }*/ } /** @@ -85,55 +75,4 @@ protected function initializeIterator() { return $this->getDataParserPlugin(); } - /** - * Collect an array of next links from a paged response. - */ -/* protected function getNextLinks($url) { - $urls = array(); - $more = TRUE; - while ($more == TRUE) { - $response = $this->dataParserPlugin->getDataFetcher()->getResponse($url); - if ($url = $this->getNextFromHeaders($response)) { - $urls[] = $url; - } - elseif ($url = $this->getNextFromLinks($response)) { - $urls[] = $url; - } - else { - $more = FALSE; - } - } - return $urls; - } -*/ - /** - * See if the next link is in a 'links' group in the response. - * - * @param \Psr\Http\Message\ResponseInterface $response - */ -/* protected function getNextFromLinks(ResponseInterface $response) { - $body = json_decode($response->getBody(), TRUE); - if (!empty($body['links']) && array_key_exists('next', $body['links'])) { - return $body['links']['next']; - } - return FALSE; - } -*/ - /** - * See if the next link is in the header. - * - * @param \Psr\Http\Message\ResponseInterface $response - */ -/* protected function getNextFromHeaders(ResponseInterface $response) { - $headers = $response->getHeader('Link'); - foreach ($headers as $header) { - $matches = array(); - preg_match('/^<(.*)>; rel="next"$/', $header, $matches); - if (!empty($matches) && !empty($matches[1])) { - return $matches[1]; - } - } - return FALSE; - } -*/ } diff --git a/web/modules/migrate_plus/src/Plugin/migrate_plus/authentication/Basic.php b/web/modules/migrate_plus/src/Plugin/migrate_plus/authentication/Basic.php new file mode 100644 index 0000000000..f1ca69ab6c --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate_plus/authentication/Basic.php @@ -0,0 +1,30 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate_plus\authentication; + +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\migrate_plus\AuthenticationPluginBase; + +/** + * Provides basic authentication for the HTTP resource. + * + * @Authentication( + * id = "basic", + * title = @Translation("Basic") + * ) + */ +class Basic extends AuthenticationPluginBase implements ContainerFactoryPluginInterface { + + /** + * {@inheritdoc} + */ + public function getAuthenticationOptions() { + return [ + 'auth' => [ + $this->configuration['username'], + $this->configuration['password'], + ], + ]; + } + +} diff --git a/web/modules/migrate_plus/src/Plugin/migrate_plus/authentication/Digest.php b/web/modules/migrate_plus/src/Plugin/migrate_plus/authentication/Digest.php new file mode 100644 index 0000000000..2466534306 --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate_plus/authentication/Digest.php @@ -0,0 +1,31 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate_plus\authentication; + +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\migrate_plus\AuthenticationPluginBase; + +/** + * Provides digest authentication for the HTTP resource. + * + * @Authentication( + * id = "digest", + * title = @Translation("Digest") + * ) + */ +class Digest extends AuthenticationPluginBase implements ContainerFactoryPluginInterface { + + /** + * {@inheritdoc} + */ + public function getAuthenticationOptions() { + return [ + 'auth' => [ + $this->configuration['username'], + $this->configuration['password'], + 'digest', + ], + ]; + } + +} 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 new file mode 100644 index 0000000000..116314d6bc --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate_plus/authentication/OAuth2.php @@ -0,0 +1,69 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate_plus\authentication; + +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\migrate\MigrateException; +use Drupal\migrate_plus\AuthenticationPluginBase; +use GuzzleHttp\Client; +use GuzzleHttp\HandlerStack; +use Sainsburys\Guzzle\Oauth2\GrantType\AuthorizationCode; +use Sainsburys\Guzzle\Oauth2\GrantType\ClientCredentials; +use Sainsburys\Guzzle\Oauth2\GrantType\JwtBearer; +use Sainsburys\Guzzle\Oauth2\GrantType\PasswordCredentials; +use Sainsburys\Guzzle\Oauth2\GrantType\RefreshToken; +use Sainsburys\Guzzle\Oauth2\Middleware\OAuthMiddleware; + +/** + * Provides OAuth2 authentication for the HTTP resource. + * + * @link https://packagist.org/packages/sainsburys/guzzle-oauth2-plugin + * + * @Authentication( + * id = "oauth2", + * title = @Translation("OAuth2") + * ) + */ +class OAuth2 extends AuthenticationPluginBase implements ContainerFactoryPluginInterface { + + /** + * {@inheritdoc} + */ + public function getAuthenticationOptions() { + $handlerStack = HandlerStack::create(); + $client = new Client([ + 'handler' => $handlerStack, + 'base_uri' => $this->configuration['base_uri'], + 'auth' => 'oauth2', + ]); + + switch ($this->configuration['grant_type']) { + 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); + + return [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $middleware->getAccessToken()->getToken(), + ], + ]; + } + +} 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 new file mode 100644 index 0000000000..dfa02d3bc3 --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate_plus/data_fetcher/File.php @@ -0,0 +1,52 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate_plus\data_fetcher; + +use Drupal\migrate\MigrateException; +use Drupal\migrate_plus\DataFetcherPluginBase; + +/** + * Retrieve data from a local path or general URL for migration. + * + * @DataFetcher( + * id = "file", + * title = @Translation("File") + * ) + */ +class File extends DataFetcherPluginBase { + + /** + * {@inheritdoc} + */ + public function setRequestHeaders(array $headers) { + // Does nothing. + } + + /** + * {@inheritdoc} + */ + public function getRequestHeaders() { + // Does nothing. + return []; + } + + /** + * {@inheritdoc} + */ + public function getResponse($url) { + $response = file_get_contents($url); + if ($response === FALSE) { + throw new MigrateException('file parser plugin: could not retrieve data from ' . $url); + } + return $response; + } + + /** + * {@inheritdoc} + */ + public function getResponseContent($url) { + $response = $this->getResponse($url); + return $response; + } + +} diff --git a/web/modules/migrate_plus/src/Plugin/migrate_plus/data_fetcher/Http.php b/web/modules/migrate_plus/src/Plugin/migrate_plus/data_fetcher/Http.php index a9b72d235b..db4c423a69 100644 --- a/web/modules/migrate_plus/src/Plugin/migrate_plus/data_fetcher/Http.php +++ b/web/modules/migrate_plus/src/Plugin/migrate_plus/data_fetcher/Http.php @@ -10,6 +10,19 @@ /** * Retrieve data over an HTTP connection for migration. * + * Example: + * + * @code + * source: + * plugin: url + * data_fetcher_plugin: http + * headers: + * Accept: application/json + * User-Agent: Internet Explorer 6 + * Authorization-Key: secret + * Arbitrary-Header: foobarbaz + * @endcode + * * @DataFetcher( * id = "http", * title = @Translation("HTTP") @@ -18,7 +31,7 @@ class Http extends DataFetcherPluginBase implements ContainerFactoryPluginInterface { /** - * The HTTP Client + * The HTTP client. * * @var \GuzzleHttp\Client */ @@ -31,12 +44,36 @@ class Http extends DataFetcherPluginBase implements ContainerFactoryPluginInterf */ protected $headers = []; + /** + * The data retrieval client. + * + * @var \Drupal\migrate_plus\AuthenticationPluginInterface + */ + protected $authenticationPlugin; + /** * {@inheritdoc} */ public function __construct(array $configuration, $plugin_id, $plugin_definition) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->httpClient = \Drupal::httpClient(); + + // Ensure there is a 'headers' key in the configuration. + $configuration += ['headers' => []]; + $this->setRequestHeaders($configuration['headers']); + } + + /** + * Returns the initialized authentication plugin. + * + * @return \Drupal\migrate_plus\AuthenticationPluginInterface + * The authentication plugin. + */ + public function getAuthenticationPlugin() { + if (!isset($this->authenticationPlugin)) { + $this->authenticationPlugin = \Drupal::service('plugin.manager.migrate_plus.authentication')->createInstance($this->configuration['authentication']['plugin'], $this->configuration['authentication']); + } + return $this->authenticationPlugin; } /** @@ -50,7 +87,7 @@ public function setRequestHeaders(array $headers) { * {@inheritdoc} */ public function getRequestHeaders() { - return !empty($this->headers) ? $this->headers : array(); + return !empty($this->headers) ? $this->headers : []; } /** @@ -58,17 +95,17 @@ public function getRequestHeaders() { */ public function getResponse($url) { try { - $response = $this->httpClient->get($url, array( - 'headers' => $this->getRequestHeaders(), - // Uncomment the following to debug the request. - //'debug' => true, - )); + $options = ['headers' => $this->getRequestHeaders()]; + if (!empty($this->configuration['authentication'])) { + $options = array_merge($options, $this->getAuthenticationPlugin()->getAuthenticationOptions()); + } + $response = $this->httpClient->get($url, $options); if (empty($response)) { throw new MigrateException('No response at ' . $url . '.'); } } catch (RequestException $e) { - throw new MigrateException('Error message: ' . $e->getMessage() . ' at ' . $url .'.'); + throw new MigrateException('Error message: ' . $e->getMessage() . ' at ' . $url . '.'); } return $response; } 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 694bc99de8..c1220bc5f4 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 @@ -3,9 +3,7 @@ namespace Drupal\migrate_plus\Plugin\migrate_plus\data_parser; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\migrate\MigrateException; use Drupal\migrate_plus\DataParserPluginBase; -use GuzzleHttp\Exception\RequestException; /** * Obtain JSON data for migration. @@ -17,13 +15,6 @@ */ class Json extends DataParserPluginBase implements ContainerFactoryPluginInterface { - /** - * The request headers passed to the data fetcher. - * - * @var array - */ - protected $headers = []; - /** * Iterator over the JSON data. * @@ -32,22 +23,64 @@ class Json extends DataParserPluginBase implements ContainerFactoryPluginInterfa protected $iterator; /** - * {@inheritdoc} + * Retrieves the JSON data and returns it as an array. + * + * @param string $url + * URL of a JSON feed. + * + * @return array + * The selected data to be iterated. + * + * @throws \GuzzleHttp\Exception\RequestException */ - public function __construct(array $configuration, $plugin_id, $plugin_definition) { - parent::__construct($configuration, $plugin_id, $plugin_definition); + protected function getSourceData($url) { + $response = $this->getDataFetcherPlugin()->getResponseContent($url); + + // Convert objects to associative arrays. + $source_data = json_decode($response, TRUE); + + // If json_decode() has returned NULL, it might be that the data isn't + // valid utf8 - see http://php.net/manual/en/function.json-decode.php#86997. + if (is_null($source_data)) { + $utf8response = utf8_encode($response); + $source_data = json_decode($utf8response, TRUE); + } + + // Backwards-compatibility for depth selection. + if (is_int($this->itemSelector)) { + return $this->selectByDepth($source_data); + } + + // Otherwise, we're using xpath-like selectors. + $selectors = explode('/', trim($this->itemSelector, '/')); + foreach ($selectors as $selector) { + if (!empty($selector)) { + $source_data = $source_data[$selector]; + } + } + return $source_data; } /** - * {@inheritdoc} + * Get the source data for reading. + * + * @param array $raw_data + * Raw data from the JSON feed. + * + * @return array + * Selected items at the requested depth of the JSON feed. */ - protected function getSourceData($url) { - $iterator = $this->getSourceIterator($url); - - // Recurse through the result array. When there is an array of items at the - // expected depth, pull that array out as a distinct item. - $identifierDepth = $this->itemSelector; + protected function selectByDepth(array $raw_data) { + // Return the results in a recursive iterator that can traverse + // multidimensional arrays. + $iterator = new \RecursiveIteratorIterator( + new \RecursiveArrayIterator($raw_data), + \RecursiveIteratorIterator::SELF_FIRST); $items = []; + // Backwards-compatibility - an integer item_selector is interpreted as a + // depth. When there is an array of items at the expected depth, pull that + // array out as a distinct item. + $identifierDepth = $this->itemSelector; $iterator->rewind(); while ($iterator->valid()) { $item = $iterator->current(); @@ -59,33 +92,6 @@ protected function getSourceData($url) { return $items; } - /** - * Get the source data for reading. - * - * @param string $url - * The URL to read the source data from. - * - * @return \RecursiveIteratorIterator|resource - * - * @throws \Drupal\migrate\MigrateException - */ - protected function getSourceIterator($url) { - try { - $response = $this->getDataFetcherPlugin()->getResponseContent($url); - // The TRUE setting means decode the response into an associative array. - $array = json_decode($response, TRUE); - - // Return the results in a recursive iterator that - // can traverse multidimensional arrays. - return new \RecursiveIteratorIterator( - new \RecursiveArrayIterator($array), - \RecursiveIteratorIterator::SELF_FIRST); - } - catch (RequestException $e) { - throw new MigrateException($e->getMessage(), $e->getCode(), $e); - } - } - /** * {@inheritdoc} */ @@ -103,7 +109,20 @@ protected function fetchNextRow() { $current = $this->iterator->current(); if ($current) { foreach ($this->fieldSelectors() as $field_name => $selector) { - $this->currentItem[$field_name] = $current[$selector]; + $field_data = $current; + $field_selectors = explode('/', trim($selector, '/')); + 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 { + $field_data = ''; + } + } + $this->currentItem[$field_name] = $field_data; + } + if (!empty($this->configuration['include_raw_data'])) { + $this->currentItem['raw'] = $current; } $this->iterator->next(); } 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 new file mode 100644 index 0000000000..8a58d8b075 --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate_plus/data_parser/SimpleXml.php @@ -0,0 +1,87 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate_plus\data_parser; + +use Drupal\migrate\MigrateException; +use Drupal\migrate_plus\DataParserPluginBase; + +/** + * Obtain XML data for migration using the SimpleXML API. + * + * @DataParser( + * id = "simple_xml", + * title = @Translation("Simple XML") + * ) + */ +class SimpleXml extends DataParserPluginBase { + + use XmlTrait; + + /** + * Array of matches from item_selector. + * + * @var array + */ + protected $matches = []; + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + + // Suppress errors during parsing, so we can pick them up after. + libxml_use_internal_errors(TRUE); + } + + /** + * {@inheritdoc} + */ + protected function openSourceUrl($url) { + // Clear XML error buffer. Other Drupal code that executed during the + // migration may have polluted the error buffer and could create false + // positives in our error check below. We are only concerned with errors + // that occur from attempting to load the XML string into an object here. + 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); + foreach (libxml_get_errors() as $error) { + $error_string = self::parseLibXmlError($error); + throw new MigrateException($error_string); + } + return TRUE; + } + + /** + * {@inheritdoc} + */ + protected function fetchNextRow() { + $target_element = array_shift($this->matches); + + // If we've found the desired element, populate the currentItem and + // currentId with its data. + if ($target_element !== FALSE && !is_null($target_element)) { + foreach ($this->fieldSelectors() as $field_name => $xpath) { + foreach ($target_element->xpath($xpath) as $value) { + if ($value->children() && !trim((string) $value)) { + $this->currentItem[$field_name] = $value; + } + else { + $this->currentItem[$field_name][] = (string) $value; + } + } + } + // Reduce single-value results to scalars. + foreach ($this->currentItem as $field_name => $values) { + if (is_array($values) && count($values) == 1) { + $this->currentItem[$field_name] = reset($values); + } + } + } + } + +} 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 60db902046..7f8c694e2f 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 @@ -3,6 +3,7 @@ namespace Drupal\migrate_plus\Plugin\migrate_plus\data_parser; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\migrate\Exception\RequirementsException; use Drupal\migrate\MigrateException; use Drupal\migrate_plus\DataParserPluginBase; @@ -46,8 +47,14 @@ class Soap extends DataParserPluginBase implements ContainerFactoryPluginInterfa /** * {@inheritdoc} + * + * @throws \Drupal\migrate\Exception\RequirementsException + * If PHP SOAP extension is not installed. */ public function __construct(array $configuration, $plugin_id, $plugin_definition) { + if (!class_exists('\SoapClient')) { + throw new RequirementsException('The PHP SOAP extension is not installed'); + } parent::__construct($configuration, $plugin_id, $plugin_definition); $this->function = $configuration['function']; $this->parameters = $configuration['parameters']; @@ -63,7 +70,7 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition * If we can't resolve the SOAP function or its response property. */ protected function openSourceUrl($url) { - // Will throw SoapFault if there's + // Will throw SoapFault if there's an error in a SOAP call. $client = new \SoapClient($url); // Determine the response property name. $function_found = FALSE; 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 c27922a9c3..6c08069276 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 @@ -2,19 +2,20 @@ namespace Drupal\migrate_plus\Plugin\migrate_plus\data_parser; -use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\migrate\MigrateException; use Drupal\migrate_plus\DataParserPluginBase; /** - * Obtain XML data for migration. + * Obtain XML data for migration using the XMLReader pull parser. * * @DataParser( * id = "xml", * title = @Translation("XML") * ) */ -class Xml extends DataParserPluginBase implements ContainerFactoryPluginInterface { +class Xml extends DataParserPluginBase { + + use XmlTrait; /** * The XMLReader we are encapsulating. @@ -173,6 +174,13 @@ public function rewind() { protected function openSourceUrl($url) { // (Re)open the provided URL. $this->reader->close(); + + // Clear XML error buffer. Other Drupal code that executed during the + // migration may have polluted the error buffer and could create false + // positives in our error check below. We are only concerned with errors + // that occur from attempting to load the XML string into an object here. + libxml_clear_errors(); + return $this->reader->open($url, NULL, \LIBXML_NOWARNING); } @@ -188,13 +196,13 @@ protected function fetchNextRow() { if ($this->reader->nodeType == \XMLReader::ELEMENT) { if ($this->prefixedName) { $this->currentPath[$this->reader->depth] = $this->reader->name; - if (array_key_exists($this->reader->name, $this->parentElementsOfInterest)) { + if (in_array($this->reader->name, $this->parentElementsOfInterest)) { $this->parentXpathCache[$this->reader->depth][$this->reader->name][] = $this->getSimpleXml(); } } else { $this->currentPath[$this->reader->depth] = $this->reader->localName; - if (array_key_exists($this->reader->localName, $this->parentElementsOfInterest)) { + if (in_array($this->reader->localName, $this->parentElementsOfInterest)) { $this->parentXpathCache[$this->reader->depth][$this->reader->name][] = $this->getSimpleXml(); } } @@ -228,8 +236,25 @@ protected function fetchNextRow() { // currentId with its data. if ($target_element !== FALSE && !is_null($target_element)) { foreach ($this->fieldSelectors() as $field_name => $xpath) { - foreach ($target_element->xpath($xpath) as $value) { - $this->currentItem[$field_name][] = (string) $value; + $prefix = substr($xpath, 0, 3); + if (in_array($prefix, ['../', '..\\'])) { + $name = str_replace($prefix, '', $xpath); + $up = substr_count($xpath, $prefix); + $values = $this->getAncestorElements($up, $name); + } + else { + $values = $target_element->xpath($xpath); + } + foreach ($values as $value) { + // If the SimpleXMLElement doesn't render to a string of any sort, + // and has children then return the whole object for the process + // plugin or other row manipulation. + if ($value->children() && !trim((string) $value)) { + $this->currentItem[$field_name] = $value; + } + else { + $this->currentItem[$field_name][] = (string) $value; + } } } // Reduce single-value results to scalars. @@ -293,59 +318,4 @@ public function getAncestorElements($levels_up, $name) { } } - /** - * Registers the iterator's namespaces to a SimpleXMLElement. - * - * @param \SimpleXMLElement $xml - * The element to apply namespace registrations to. - */ - protected function registerNamespaces(\SimpleXMLElement $xml) { - if (is_array($this->configuration['namespaces'])) { - foreach ($this->configuration['namespaces'] as $prefix => $ns) { - $xml->registerXPathNamespace($prefix, $ns); - } - } - } - - /** - * Parses a LibXMLError to a error message string. - * - * @param \LibXMLError $error - * Error thrown by the XML. - * - * @return string - * Error message - */ - public static function parseLibXmlError(\LibXMLError $error) { - $error_code_name = 'Unknown Error'; - switch ($error->level) { - case LIBXML_ERR_WARNING: - $error_code_name = t('Warning'); - break; - - case LIBXML_ERR_ERROR: - $error_code_name = t('Error'); - break; - - case LIBXML_ERR_FATAL: - $error_code_name = t('Fatal Error'); - break; - } - - return t( - "@libxmlerrorcodename @libxmlerrorcode: @libxmlerrormessage\n" . - "Line: @libxmlerrorline\n" . - "Column: @libxmlerrorcolumn\n" . - "File: @libxmlerrorfile", - [ - '@libxmlerrorcodename' => $error_code_name, - '@libxmlerrorcode' => $error->code, - '@libxmlerrormessage' => trim($error->message), - '@libxmlerrorline' => $error->line, - '@libxmlerrorcolumn' => $error->column, - '@libxmlerrorfile' => (($error->file)) ? $error->file : NULL, - ] - ); - } - } diff --git a/web/modules/migrate_plus/src/Plugin/migrate_plus/data_parser/XmlTrait.php b/web/modules/migrate_plus/src/Plugin/migrate_plus/data_parser/XmlTrait.php new file mode 100644 index 0000000000..28519ba516 --- /dev/null +++ b/web/modules/migrate_plus/src/Plugin/migrate_plus/data_parser/XmlTrait.php @@ -0,0 +1,62 @@ +<?php + +namespace Drupal\migrate_plus\Plugin\migrate_plus\data_parser; + +/** + * Common functionality for XML data parsers. + */ +trait XmlTrait { + + /** + * Registers the iterator's namespaces to a SimpleXMLElement. + * + * @param \SimpleXMLElement $xml + * The element to apply namespace registrations to. + */ + protected function registerNamespaces(\SimpleXMLElement $xml) { + if (isset($this->configuration['namespaces']) && is_array($this->configuration['namespaces'])) { + foreach ($this->configuration['namespaces'] as $prefix => $ns) { + $xml->registerXPathNamespace($prefix, $ns); + } + } + } + + /** + * Parses a LibXMLError to a error message string. + * + * @param \LibXMLError $error + * Error thrown by the XML. + * + * @return string + * Error message + */ + public static function parseLibXmlError(\LibXMLError $error) { + $error_code_name = 'Unknown Error'; + switch ($error->level) { + case LIBXML_ERR_WARNING: + $error_code_name = t('Warning'); + break; + + case LIBXML_ERR_ERROR: + $error_code_name = t('Error'); + break; + + case LIBXML_ERR_FATAL: + $error_code_name = t('Fatal Error'); + break; + } + + return t( + "@libxmlerrorcodename @libxmlerrorcode: @libxmlerrormessage\nLine: @libxmlerrorline\nColumn: @libxmlerrorcolumn\nFile: @libxmlerrorfile", + [ + '@libxmlerrorcodename' => $error_code_name, + '@libxmlerrorcode' => $error->code, + '@libxmlerrormessage' => trim($error->message), + '@libxmlerrorline' => $error->line, + '@libxmlerrorcolumn' => $error->column, + '@libxmlerrorfile' => (($error->file)) ? $error->file : NULL, + ] + ); + } + +} diff --git a/web/modules/migrate_plus/tests/data/missing_properties.json b/web/modules/migrate_plus/tests/data/missing_properties.json new file mode 100644 index 0000000000..59b7d6a6ef --- /dev/null +++ b/web/modules/migrate_plus/tests/data/missing_properties.json @@ -0,0 +1,21 @@ +[ + { + "id": "1", + "title": "Title", + "video": { + "title": "Video title", + "url": "https://localhost/" + } + }, + { + "id": "2", + "video": { + "title": "Video title", + "url": "https://localhost/" + } + }, + { + "id": "3", + "title": "Title 3" + } +] diff --git a/web/modules/migrate_plus/tests/data/simple_xml_reduce_single_value.xml b/web/modules/migrate_plus/tests/data/simple_xml_reduce_single_value.xml new file mode 100644 index 0000000000..0cffd28908 --- /dev/null +++ b/web/modules/migrate_plus/tests/data/simple_xml_reduce_single_value.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/src/Functional/LoadTest.php b/web/modules/migrate_plus/tests/src/Functional/LoadTest.php new file mode 100644 index 0000000000..c9aceda720 --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Functional/LoadTest.php @@ -0,0 +1,50 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Functional; + +use Drupal\Core\Url; +use Drupal\Tests\BrowserTestBase; + +/** + * Simple test to ensure that main page loads with module enabled. + * + * @group migrate_plus + */ +class LoadTest extends BrowserTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = [ + 'migrate_plus', + 'migrate_example', + 'migrate_example_advanced', + ]; + + /** + * A user with permission to administer site configuration. + * + * @var \Drupal\user\UserInterface + */ + protected $user; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + $this->user = $this->drupalCreateUser(['administer site configuration']); + $this->drupalLogin($this->user); + } + + /** + * Tests that the home page loads with a 200 response. + */ + public function testLoad() { + $this->drupalGet(Url::fromRoute('<front>')); + $this->assertSession()->statusCodeEquals(200); + } + +} diff --git a/web/modules/migrate_plus/tests/src/Kernel/MigrateTableTest.php b/web/modules/migrate_plus/tests/src/Kernel/MigrateTableTest.php new file mode 100644 index 0000000000..dc471770d7 --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Kernel/MigrateTableTest.php @@ -0,0 +1,163 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Kernel; + +use Drupal\Core\Database\Database; +use Drupal\migrate\MigrateExecutable; +use Drupal\Tests\migrate\Kernel\MigrateTestBase; + +/** + * Tests migration destination table. + * + * @group migrate + */ +class MigrateTableTest extends MigrateTestBase { + + const TABLE_NAME = 'migrate_test_destination_table'; + + /** + * The database connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $connection; + + public static $modules = ['migrate_plus']; + + /** + * {@inheritdoc} + */ + protected function setUp() { + 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, + ], + ], + 'primary key' => ['data'], + ]); + } + + /** + * {@inheritdoc} + */ + protected function tearDown() { + $this->connection->schema()->dropTable(static::TABLE_NAME); + parent::tearDown(); + } + + /** + * Create a minimally valid migration with some source data. + * + * @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', + ], + [ + 'data' => 'dummy value2', + 'data2' => 'dummy2 value2', + 'data3' => 'dummy3 value2', + ], + [ + 'data' => 'dummy value3', + 'data2' => 'dummy2 value3', + 'data3' => 'dummy3 value3', + ], + ], + 'ids' => [ + 'data' => ['type' => 'string'], + ], + ], + 'destination' => [ + 'plugin' => 'table', + 'table_name' => static::TABLE_NAME, + 'id_fields' => ['data' => ['type' => 'string']], + ], + 'process' => [ + 'data' => 'data', + 'data2' => 'data2', + 'data3' => 'data3', + ], + ]; + return $definition; + } + + /** + * Tests table destination. + */ + public function testTableDestination() { + $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('dummy2 value', $values['dummy value']->data2); + $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) + ->execute() + ->fetchAllAssoc('data'); + + $this->assertEquals(0, count($values)); + } + +} diff --git a/web/modules/migrate_plus/src/Tests/MigrationConfigEntityTest.php b/web/modules/migrate_plus/tests/src/Kernel/MigrationConfigEntityTest.php similarity index 50% rename from web/modules/migrate_plus/src/Tests/MigrationConfigEntityTest.php rename to web/modules/migrate_plus/tests/src/Kernel/MigrationConfigEntityTest.php index 97573f3962..354da7628a 100644 --- a/web/modules/migrate_plus/src/Tests/MigrationConfigEntityTest.php +++ b/web/modules/migrate_plus/tests/src/Kernel/MigrationConfigEntityTest.php @@ -1,11 +1,9 @@ <?php -namespace Drupal\migrate_plus\Tests; +namespace Drupal\Tests\migrate_plus\Kernel; -use Drupal\Component\Plugin\Exception\PluginNotFoundException; use Drupal\KernelTests\KernelTestBase; use Drupal\migrate_plus\Entity\Migration; -use Drupal\migrate_plus\Plugin\MigrationConfigEntityPluginManager; /** * Test migration config entity discovery. @@ -17,15 +15,23 @@ class MigrationConfigEntityTest extends KernelTestBase { public static $modules = ['migrate', 'migrate_plus']; /** - * @var MigrationConfigEntityPluginManager + * The plugin manager. + * + * @var \Drupal\migrate\Plugin\MigrationPluginManager */ - protected $pluginMananger; + protected $pluginManager; + /** + * {@inheritdoc} + */ protected function setUp() { parent::setUp(); - $this->pluginMananger = \Drupal::service('plugin.manager.config_entity_migration'); + $this->pluginManager = \Drupal::service('plugin.manager.migration'); } + /** + * Tests cache invalidation. + */ public function testCacheInvalidation() { $config = Migration::create([ 'id' => 'test', @@ -37,19 +43,18 @@ public function testCacheInvalidation() { ]); $config->save(); - $this->assertTrue($this->pluginMananger->getDefinition('test')); - $this->assertSame('Label A', $this->pluginMananger->getDefinition('test')['label']); + $this->assertTrue($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 // persistent cache. - $this->pluginMananger->useCaches(FALSE); - $this->pluginMananger->useCaches(TRUE); + $this->pluginManager->useCaches(FALSE); + $this->pluginManager->useCaches(TRUE); $config->set('label', 'Label B'); $config->save(); - $this->assertSame('Label B', $this->pluginMananger->getDefinition('test')['label']); - $this->assertSame('Label B', \Drupal::service('plugin.manager.migration')->getDefinition('test')['label']); + $this->assertSame('Label B', $this->pluginManager->getDefinition('test')['label']); } } diff --git a/web/modules/migrate_plus/src/Tests/MigrationGroupTest.php b/web/modules/migrate_plus/tests/src/Kernel/MigrationGroupTest.php similarity index 65% rename from web/modules/migrate_plus/src/Tests/MigrationGroupTest.php rename to web/modules/migrate_plus/tests/src/Kernel/MigrationGroupTest.php index c84855bc78..53d3ad467d 100644 --- a/web/modules/migrate_plus/src/Tests/MigrationGroupTest.php +++ b/web/modules/migrate_plus/tests/src/Kernel/MigrationGroupTest.php @@ -1,11 +1,10 @@ <?php -namespace Drupal\migrate_plus\Tests; +namespace Drupal\Tests\migrate_plus\Kernel; use Drupal\KernelTests\KernelTestBase; use Drupal\migrate_plus\Entity\Migration; use Drupal\migrate_plus\Entity\MigrationGroup; -use Drupal\migrate_plus\Entity\MigrationGroupInterface; /** * Test migration groups. @@ -22,18 +21,22 @@ class MigrationGroupTest extends KernelTestBase { public function testConfigurationMerge() { $group_id = 'test_group'; - /** @var MigrationGroupInterface $migration_group */ + /** @var \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group */ $group_configuration = [ 'id' => $group_id, 'shared_configuration' => [ - 'migration_tags' => ['Drupal 6'], // In migration, so will be overridden. + // In migration, so will be overridden. + 'migration_tags' => ['Drupal 6'], 'source' => [ 'constants' => [ - 'type' => 'image', // Not in migration, so will be added. - 'cardinality' => '1', // In migration, so will be overridden. + // Not in migration, so will be added. + 'type' => 'image', + // In migration, so will be overridden. + 'cardinality' => '1', ], ], - 'destination' => ['plugin' => 'field_storage_config'], // Not in migration, so will be added. + // Not in migration, so will be added. + 'destination' => ['plugin' => 'field_storage_config'], ], ]; $this->container->get('entity_type.manager')->getStorage('migration_group') @@ -42,20 +45,25 @@ public function testConfigurationMerge() { /** @var \Drupal\migrate_plus\Entity\MigrationInterface $migration */ $migration = $this->container->get('entity_type.manager') ->getStorage('migration')->create([ - 'id' => 'specific_migration', - 'load' => [], - 'migration_group' => $group_id, - 'label' => 'Unaffected by the group', - 'migration_tags' => ['Drupal 7'], // Overrides group. - 'destination' => [], - 'source' => [], - 'migration_dependencies' => [], - ]); + 'id' => 'specific_migration', + 'load' => [], + 'migration_group' => $group_id, + 'label' => 'Unaffected by the group', + // Overrides group. + 'migration_tags' => ['Drupal 7'], + 'destination' => [], + 'source' => [], + 'process' => [], + 'migration_dependencies' => [], + ]); $migration->set('source', [ - 'plugin' => 'empty', // Not in group, persists. + // Not in group, persists. + 'plugin' => 'empty', 'constants' => [ - 'entity_type' => 'user', // Not in group, persists. - 'cardinality' => '3', // Overrides group. + // Not in group, persists. + 'entity_type' => 'user', + // Overrides group. + 'cardinality' => '3', ], ]); $migration->save(); @@ -75,7 +83,7 @@ public function testConfigurationMerge() { 'destination' => ['plugin' => 'field_storage_config'], ]; /** @var \Drupal\migrate\Plugin\MigrationInterface $loaded_migration */ - $loaded_migration = $this->container->get('plugin.manager.config_entity_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); @@ -87,7 +95,7 @@ public function testConfigurationMerge() { * Test that deleting a group deletes its migrations. */ public function testDelete() { - /** @var MigrationGroupInterface $migration_group */ + /** @var \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group */ $group_configuration = [ 'id' => 'test_group', ]; @@ -98,14 +106,14 @@ public function testDelete() { /** @var \Drupal\migrate_plus\Entity\MigrationInterface $migration */ $migration = $this->container->get('entity_type.manager') ->getStorage('migration')->create([ - 'id' => 'specific_migration', - 'migration_group' => 'test_group', - 'migration_tags' => [], - 'load' => [], - 'destination' => [], - 'source' => [], - 'migration_dependencies' => [], - ]); + 'id' => 'specific_migration', + 'migration_group' => 'test_group', + 'migration_tags' => [], + 'load' => [], + 'destination' => [], + 'source' => [], + 'migration_dependencies' => [], + ]); $migration->save(); /** @var \Drupal\migrate_plus\Entity\MigrationGroupInterface $loaded_migration_group */ 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 new file mode 100644 index 0000000000..3d46496de4 --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate/process/EntityGenerateTest.php @@ -0,0 +1,788 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Kernel\Plugin\migrate\process; + +use Drupal\Core\Config\Entity\ConfigEntityInterface; +use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\Core\Language\LanguageInterface; +use Drupal\field\Tests\EntityReference\EntityReferenceTestTrait; +use Drupal\KernelTests\KernelTestBase; +use Drupal\migrate\MigrateExecutable; +use Drupal\migrate\MigrateMessageInterface; +use Drupal\node\Entity\NodeType; +use Drupal\taxonomy\Entity\Vocabulary; + +/** + * Tests the migration plugin. + * + * @coversDefaultClass \Drupal\migrate_plus\Plugin\migrate\process\EntityGenerate + * @group migrate_plus + */ +class EntityGenerateTest extends KernelTestBase implements MigrateMessageInterface { + + use EntityReferenceTestTrait; + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'migrate_plus', + 'migrate', + 'user', + 'system', + 'node', + 'taxonomy', + 'field', + 'text', + 'filter', + ]; + + /** + * The bundle used in this test. + * + * @var string + */ + protected $bundle = 'page'; + + /** + * The name of the field used in this test. + * + * @var string + */ + protected $fieldName = 'field_entity_reference'; + + /** + * The vocabulary id. + * + * @var string + */ + protected $vocabulary = 'fruit'; + + /** + * The migration plugin manager. + * + * @var \Drupal\migrate\Plugin\MigrationPluginManager + */ + protected $migrationPluginManager; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + // Create article content type. + $values = [ + 'type' => $this->bundle, + 'name' => 'Page', + ]; + $node_type = NodeType::create($values); + $node_type->save(); + + $this->installEntitySchema('node'); + $this->installEntitySchema('taxonomy_term'); + $this->installEntitySchema('taxonomy_vocabulary'); + $this->installEntitySchema('user'); + $this->installSchema('system', ['sequences']); + $this->installSchema('user', 'users_data'); + $this->installConfig($this->modules); + + // Create a vocabulary. + $vocabulary = Vocabulary::create([ + 'name' => $this->vocabulary, + 'description' => $this->vocabulary, + 'vid' => $this->vocabulary, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ]); + $vocabulary->save(); + + // Create a field. + $this->createEntityReferenceField( + 'node', + $this->bundle, + $this->fieldName, + 'Term reference', + 'taxonomy_term', + 'default', + ['target_bundles' => [$this->vocabulary]], + FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED + ); + + $this->migrationPluginManager = \Drupal::service('plugin.manager.migration'); + } + + /** + * Tests generating an entity. + * + * @dataProvider transformDataProvider + * + * @covers ::transform + */ + public function testTransform(array $definition, array $expected, array $preSeed = []) { + // Pre seed some test data. + foreach ($preSeed as $storageName => $values) { + // If the first element of $values is a non-empty array, create multiple + // entities. Otherwise, create just one entity. + if (isset($values[0])) { + foreach ($values as $itemValues) { + $this->createTestData($storageName, $itemValues); + } + } + else { + $this->createTestData($storageName, $values); + } + } + + /** @var \Drupal\migrate\Plugin\Migration $migration */ + $migration = $this->migrationPluginManager->createStubMigration($definition); + /** @var EntityStorageBase $storage */ + $storage = $this->readAttribute($migration->getDestinationPlugin(), 'storage'); + $migrationExecutable = (new MigrateExecutable($migration, $this)); + $migrationExecutable->import(); + + foreach ($expected as $row) { + $entity = $storage->load($row['id']); + $properties = array_diff_key($row, array_flip(['id'])); + foreach ($properties as $property => $value) { + if (is_array($value)) { + if (empty($value)) { + $this->assertEmpty($entity->{$property}->getValue(), "Expected value is 'unset' but field $property is set."); + } + else { + // Check if we're testing multiple values in one field. If so, loop + // through them one-by-one and check that they're present in the + // $entity. + if (isset($value[0])) { + foreach ($value as $valueID => $valueToCheck) { + 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."); + } + else { + $this->assertTrue($entity->{$property}->isEmpty(), "FOOBAR Expected value is empty but field $property is not empty."); + } + } + elseif ($entity->{$property}->getValue()) { + $this->assertEquals($expectedValue, $entity->{$property}[$valueID]->entity->$key->value); + } + else { + $this->fail("Expected value: $expectedValue does not exist in $property."); + } + } + } + } + // If we get to this point, we're only checking a + // single field value. + else { + 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."); + } + else { + $this->assertTrue($entity->{$property}->isEmpty(), "BINBAZ Expected value is empty but field $property is not empty."); + } + } + elseif ($entity->{$property}->getValue()) { + $referenced_entity = $entity->{$property}[0]->entity; + $result_value = $referenced_entity instanceof ConfigEntityInterface ? $referenced_entity->get($key) : $referenced_entity->get($key)->value; + $this->assertEquals($expectedValue, $result_value); + } + else { + $this->fail("Expected value: $expectedValue does not exist in $property."); + } + } + } + } + } + else { + $this->assertNotEmpty($entity, 'Entity with label ' . $row[$property] . ' is empty'); + $this->assertEquals($row[$property], $entity->label()); + } + } + } + } + + /** + * Provides multiple migration definitions for "transform" test. + */ + public function transformDataProvider() { + return [ + 'no arguments' => [ + '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'], + ], + ], + 'process' => [ + 'id' => 'id', + 'type' => [ + 'plugin' => 'default_value', + 'default_value' => $this->bundle, + ], + 'title' => 'title', + $this->fieldName => [ + 'plugin' => 'entity_generate', + 'source' => 'term', + ], + ], + 'destination' => [ + 'plugin' => 'entity:node', + ], + ], + 'expected' => [ + 'row 1' => [ + 'id' => 1, + 'title' => 'content item 1', + $this->fieldName => [ + 'tid' => 2, + 'name' => 'Apples', + ], + ], + 'row 2' => [ + 'id' => 2, + 'title' => 'content item 2', + $this->fieldName => [ + 'tid' => 3, + 'name' => 'Bananas', + ], + ], + 'row 3' => [ + 'id' => 3, + 'title' => 'content item 3', + $this->fieldName => [ + 'tid' => 1, + 'name' => 'Grapes', + ], + ], + ], + 'pre seed' => [ + 'taxonomy_term' => [ + 'name' => 'Grapes', + 'vid' => $this->vocabulary, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + ], + ], + 'no arguments_lookup_only' => [ + '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'], + ], + ], + 'process' => [ + 'id' => 'id', + 'type' => [ + 'plugin' => 'default_value', + 'default_value' => $this->bundle, + ], + 'title' => 'title', + $this->fieldName => [ + 'plugin' => 'entity_lookup', + 'source' => 'term', + ], + ], + 'destination' => [ + 'plugin' => 'entity:node', + ], + ], + 'expected' => [ + 'row 1' => [ + 'id' => 1, + 'title' => 'content item 1', + $this->fieldName => [ + 'tid' => NULL, + 'name' => NULL, + ], + ], + 'row 2' => [ + 'id' => 2, + 'title' => 'content item 2', + $this->fieldName => [ + 'tid' => NULL, + 'name' => NULL, + ], + ], + 'row 3' => [ + 'id' => 3, + 'title' => 'content item 3', + $this->fieldName => [ + 'tid' => 1, + 'name' => 'Grapes', + ], + ], + ], + 'pre seed' => [ + 'taxonomy_term' => [ + 'name' => 'Grapes', + 'vid' => $this->vocabulary, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + ], + ], + 'provide 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'], + ], + ], + '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' => [ + 'description' => '@term_upper', + ], + ], + ], + 'destination' => [ + 'plugin' => 'entity:node', + ], + ], + 'expected' => [ + 'row 1' => [ + 'id' => 1, + 'title' => 'content item 1', + $this->fieldName => [ + 'tid' => 2, + 'name' => 'Apples', + 'description' => 'APPLES', + ], + ], + 'row 2' => [ + 'id' => 2, + 'title' => 'content item 2', + $this->fieldName => [ + 'tid' => 3, + 'name' => 'Bananas', + 'description' => 'BANANAS', + ], + ], + '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' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'title' => 'content item 1', + 'term' => 'Grapes', + ], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + ], + ], + 'process' => [ + 'id' => 'id', + 'type' => [ + 'plugin' => 'default_value', + 'default_value' => $this->bundle, + ], + 'title' => 'title', + $this->fieldName => [ + 'plugin' => 'entity_lookup', + 'source' => 'term', + ], + ], + 'destination' => [ + 'plugin' => 'entity:node', + ], + ], + 'expected' => [ + 'row 1' => [ + 'id' => 1, + 'title' => 'content item 1', + $this->fieldName => [ + 'tid' => 1, + 'name' => 'Grapes', + ], + ], + ], + 'pre seed' => [ + 'taxonomy_term' => [ + 'name' => 'Grapes', + 'vid' => $this->vocabulary, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + ], + ], + 'lookup single missing term returns null value' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'title' => 'content item 1', + 'term' => 'Apple', + ], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + ], + ], + 'process' => [ + 'id' => 'id', + 'type' => [ + 'plugin' => 'default_value', + 'default_value' => $this->bundle, + ], + 'title' => 'title', + $this->fieldName => [ + 'plugin' => 'entity_lookup', + 'source' => 'term', + ], + ], + 'destination' => [ + 'plugin' => 'entity:node', + ], + ], + 'expected' => [ + 'row 1' => [ + 'id' => 1, + 'title' => 'content item 1', + $this->fieldName => [], + ], + ], + 'pre seed' => [ + 'taxonomy_term' => [ + 'name' => 'Grapes', + 'vid' => $this->vocabulary, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + ], + ], + 'lookup multiple existing terms returns correct terms' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'title' => 'content item 1', + 'term' => [ + 'Grapes', + 'Apples', + ], + ], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + ], + ], + 'process' => [ + 'id' => 'id', + 'title' => 'title', + 'type' => [ + 'plugin' => 'default_value', + 'default_value' => $this->bundle, + ], + $this->fieldName => [ + 'plugin' => 'entity_lookup', + 'source' => 'term', + ], + ], + 'destination' => [ + 'plugin' => 'entity:node', + ], + ], + 'expected' => [ + 'row 1' => [ + 'id' => 1, + 'title' => 'content item 1', + $this->fieldName => [ + [ + 'tid' => 1, + 'name' => 'Grapes', + ], + [ + 'tid' => 2, + 'name' => 'Apples', + ], + ], + ], + ], + 'pre seed' => [ + 'taxonomy_term' => [ + [ + 'name' => 'Grapes', + 'vid' => $this->vocabulary, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + [ + 'name' => 'Apples', + 'vid' => $this->vocabulary, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + ], + ], + ], + 'lookup multiple mixed terms returns correct terms' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'title' => 'content item 1', + 'term' => [ + 'Grapes', + 'Pears', + ], + ], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + ], + ], + 'process' => [ + 'id' => 'id', + 'title' => 'title', + 'type' => [ + 'plugin' => 'default_value', + 'default_value' => $this->bundle, + ], + $this->fieldName => [ + 'plugin' => 'entity_lookup', + 'source' => 'term', + ], + ], + 'destination' => [ + 'plugin' => 'entity:node', + ], + ], + 'expected' => [ + 'row 1' => [ + 'id' => '1', + 'title' => 'content item 1', + $this->fieldName => [ + [ + 'tid' => 1, + 'name' => 'Grapes', + ], + ], + ], + ], + 'pre seed' => [ + 'taxonomy_term' => [ + [ + 'name' => 'Grapes', + 'vid' => $this->vocabulary, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + [ + 'name' => 'Apples', + 'vid' => $this->vocabulary, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + ], + ], + ], + 'lookup with empty term value returns no terms' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'title' => 'content item 1', + 'term' => [], + ], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + ], + ], + 'process' => [ + 'id' => 'id', + 'title' => 'title', + 'type' => [ + 'plugin' => 'default_value', + 'default_value' => $this->bundle, + ], + $this->fieldName => [ + 'plugin' => 'entity_lookup', + 'source' => 'term', + ], + ], + 'destination' => [ + 'plugin' => 'entity:node', + ], + ], + 'expected' => [ + 'row 1' => [ + 'id' => 1, + 'title' => 'content item 1', + $this->fieldName => [], + ], + ], + 'pre seed' => [ + 'taxonomy_term' => [ + 'name' => 'Grapes', + 'vid' => $this->vocabulary, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + ], + ], + 'lookup config entity' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'name' => 'user 1', + 'mail' => 'user1@user1.com', + 'roles' => ['role_1'], + ], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + ], + ], + 'process' => [ + 'id' => 'id', + 'name' => 'name', + 'roles' => [ + 'plugin' => 'entity_lookup', + 'entity_type' => 'user_role', + 'value_key' => 'id', + 'source' => 'roles', + ], + ], + 'destination' => [ + 'plugin' => 'entity:user', + ], + ], + 'expected' => [ + 'row 1' => [ + 'id' => 1, + 'name' => 'user 1', + 'roles' => [ + 'id' => 'role_1', + 'label' => 'Role 1', + ], + ], + ], + 'pre seed' => [ + 'user_role' => [ + 'id' => 'role_1', + 'label' => 'Role 1', + ], + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function display($message, $type = 'status') { + $this->assertTrue($type == 'status', $message); + } + + /** + * Create pre-seed test data. + * + * @param string $storageName + * The storage manager to create. + * @param array $values + * The values to use when creating the entity. + */ + private function createTestData($storageName, array $values) { + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $storage = $this->container + ->get('entity_type.manager') + ->getStorage($storageName); + $entity = $storage->create($values); + $entity->save(); + } + +} 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 new file mode 100644 index 0000000000..76a5c18474 --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_fetcher/HttpTest.php @@ -0,0 +1,59 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Kernel\Plugin\migrate_plus\data_fetcher; + +use Drupal\KernelTests\KernelTestBase; +use Drupal\migrate_plus\Plugin\migrate_plus\data_fetcher\Http; + +/** + * Class HttpTest. + * + * @group migrate_plus + * @package Drupal\Tests\migrate_plus\Unit\migrate_plus\data_fetcher + */ +class HttpTest extends KernelTestBase { + + /** + * Test http headers option. + * + * @dataProvider headerDataProvider + */ + public function testHttpHeaders(array $definition, array $expected, array $preSeed = []) { + $http = new Http($definition, 'http', []); + $this->assertEquals($expected, $http->getRequestHeaders()); + } + + /** + * Provides multiple test cases for the testHttpHeaders method. + * + * @return array + * The test cases + */ + public function headerDataProvider() { + return [ + 'dummy headers specified' => [ + 'definition' => [ + 'headers' => [ + 'Accept' => 'application/json', + 'User-Agent' => 'Internet Explorer 6', + 'Authorization-Key' => 'secret', + 'Arbitrary-Header' => 'foobarbaz', + ], + ], + 'expected' => [ + 'Accept' => 'application/json', + 'User-Agent' => 'Internet Explorer 6', + 'Authorization-Key' => 'secret', + 'Arbitrary-Header' => 'foobarbaz', + ], + ], + 'no headers specified' => [ + 'definition' => [ + 'no_headers_here' => 'foo', + ], + 'expected' => [], + ], + ]; + } + +} 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 new file mode 100644 index 0000000000..2fb4f2458d --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_parser/JsonTest.php @@ -0,0 +1,112 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Kernel\Plugin\migrate_plus\data_parser; + +use Drupal\KernelTests\KernelTestBase; + +/** + * Test of the data_parser Json migrate_plus plugin. + * + * @group migrate_plus + */ +class JsonTest extends KernelTestBase { + + public static $modules = ['migrate', 'migrate_plus']; + + /** + * Tests missing properties in json file. + * + * @param string $file + * File name in tests/data/ directory of this module. + * @param array $ids + * Array of ids to pass to the plugin. + * @param array $fields + * Array of fields to pass to the plugin. + * @param array $expected + * Expected array from json decoded file. + * + * @dataProvider jsonBaseDataProvider + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + * @throws \Exception + */ + public function testMissingProperties($file, array $ids, array $fields, array $expected) { + $path = $this->container + ->get('module_handler') + ->getModule('migrate_plus') + ->getPath(); + $url = $path . '/tests/data/' . $file; + + /** @var \Drupal\migrate_plus\DataParserPluginManager $plugin_manager */ + $plugin_manager = $this->container + ->get('plugin.manager.migrate_plus.data_parser'); + $conf = [ + 'plugin' => 'url', + 'data_fetcher_plugin' => 'file', + 'data_parser_plugin' => 'json', + 'destination' => 'node', + 'urls' => [$url], + 'ids' => $ids, + 'fields' => $fields, + 'item_selector' => NULL, + ]; + $json_parser = $plugin_manager->createInstance('json', $conf); + + $data = []; + foreach ($json_parser as $item) { + $data[] = $item; + } + + $this->assertEquals($expected, $data); + } + + /** + * Provides multiple test cases for the testMissingProperty method. + * + * @return array + * The test cases. + */ + public function jsonBaseDataProvider() { + return [ + 'missing properties' => [ + 'file' => 'missing_properties.json', + 'ids' => ['id' => ['type' => 'integer']], + 'fields' => [ + [ + 'name' => 'id', + 'label' => 'Id', + 'selector' => '/id', + ], + [ + 'name' => 'title', + 'label' => 'Title', + 'selector' => '/title', + ], + [ + 'name' => 'video_url', + 'label' => 'Video url', + 'selector' => '/video/url', + ], + ], + 'expected' => [ + [ + 'id' => '1', + 'title' => 'Title', + 'video_url' => 'https://localhost/', + ], + [ + 'id' => '2', + 'title' => '', + 'video_url' => 'https://localhost/', + ], + [ + 'id' => '3', + 'title' => 'Title 3', + 'video_url' => '', + ], + ], + ], + ]; + } + +} 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 new file mode 100644 index 0000000000..fe6b345e1d --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Kernel/Plugin/migrate_plus/data_parser/SimpleXmlTest.php @@ -0,0 +1,77 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Kernel\Plugin\migrate_plus\data_parser; + +use Drupal\KernelTests\KernelTestBase; + +/** + * Test of the data_parser SimpleXml migrate_plus plugin. + * + * @group migrate_plus + */ +class SimpleXmlTest extends KernelTestBase { + + public static $modules = ['migrate', 'migrate_plus']; + + /** + * Tests reducing single values. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + * @throws \Exception + */ + 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 + ->get('plugin.manager.migrate_plus.data_parser'); + $conf = [ + 'plugin' => 'url', + 'data_fetcher_plugin' => 'file', + 'data_parser_plugin' => 'simple_xml', + 'destination' => 'node', + 'urls' => [$url], + 'ids' => ['id' => ['type' => 'integer']], + 'fields' => [ + [ + 'name' => 'id', + 'label' => 'Id', + 'selector' => '@id', + ], + [ + 'name' => 'values', + 'label' => 'Values', + 'selector' => 'values', + ], + ], + '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 = [ + [ + 'Value 1', + 'Value 2', + ], + [ + 'Value 1 (single)', + ], + ]; + + $this->assertEquals($expected, $data); + } + +} diff --git a/web/modules/migrate_plus/tests/src/Unit/process/ArrayPopTest.php b/web/modules/migrate_plus/tests/src/Unit/process/ArrayPopTest.php new file mode 100644 index 0000000000..84316d799a --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Unit/process/ArrayPopTest.php @@ -0,0 +1,71 @@ +<?php + +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; + +/** + * Tests the array pop process plugin. + * + * @group migrate + * @coversDefaultClass \Drupal\migrate_plus\Plugin\migrate\process\ArrayPop + */ +class ArrayPopTest extends MigrateProcessTestCase { + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->plugin = new ArrayPop([], 'array_pop', []); + parent::setUp(); + } + + /** + * Data provider for testArrayPop(). + * + * @return array + * An array containing input values and expected output values. + */ + public function arrayPopDataProvider() { + return [ + 'indexed array' => [ + 'input' => ['v1', 'v2', 'v3'], + 'expected_output' => 'v3', + ], + 'associative array' => [ + 'input' => ['i1' => 'v1', 'i2' => 'v2', 'i3' => 'v3'], + 'expected_output' => 'v3', + ], + 'empty array' => [ + 'input' => [], + 'expected_output' => NULL, + ], + ]; + } + + /** + * Test array pop plugin. + * + * @param array $input + * The input values. + * @param mixed $expected_output + * The expected output. + * + * @dataProvider arrayPopDataProvider + */ + public function testArrayPop(array $input, $expected_output) { + $output = $this->plugin->transform($input, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertSame($output, $expected_output); + } + + /** + * Test invalid input. + */ + public function testArrayPopFromString() { + $this->setExpectedException(MigrateException::class, '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 new file mode 100644 index 0000000000..6c6950fee1 --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Unit/process/ArrayShiftTest.php @@ -0,0 +1,71 @@ +<?php + +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; + +/** + * Tests the array shift process plugin. + * + * @group migrate + * @coversDefaultClass \Drupal\migrate_plus\Plugin\migrate\process\ArrayShift + */ +class ArrayShiftTest extends MigrateProcessTestCase { + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->plugin = new ArrayShift([], 'array_shift', []); + parent::setUp(); + } + + /** + * Data provider for testArrayShift(). + * + * @return array + * An array containing input values and expected output values. + */ + public function arrayShiftDataProvider() { + return [ + 'indexed array' => [ + 'input' => ['v1', 'v2', 'v3'], + 'expected_output' => 'v1', + ], + 'associative array' => [ + 'input' => ['i1' => 'v1', 'i2' => 'v2', 'i3' => 'v3'], + 'expected_output' => 'v1', + ], + 'empty array' => [ + 'input' => [], + 'expected_output' => NULL, + ], + ]; + } + + /** + * Test array shift plugin. + * + * @param array $input + * The input values. + * @param mixed $expected_output + * The expected output. + * + * @dataProvider arrayShiftDataProvider + */ + public function testArrayShift(array $input, $expected_output) { + $output = $this->plugin->transform($input, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertSame($output, $expected_output); + } + + /** + * Test invalid input. + */ + public function testArrayShiftFromString() { + $this->setExpectedException(MigrateException::class, '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/MultipleValuesTest.php b/web/modules/migrate_plus/tests/src/Unit/process/MultipleValuesTest.php new file mode 100644 index 0000000000..60a520c0df --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Unit/process/MultipleValuesTest.php @@ -0,0 +1,32 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Unit\process; + +use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase; +use Drupal\migrate_plus\Plugin\migrate\process\MultipleValues; + +/** + * @coversDefaultClass \Drupal\migrate_plus\Plugin\migrate\process\MultipleValues + * @group migrate + */ +class MultipleValuesTest extends MigrateProcessTestCase { + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->plugin = new MultipleValues([], 'multiple_values', []); + parent::setUp(); + } + + /** + * Test input treated as multiple value output. + */ + public function testTreatAsMultiple() { + $value = ['v1', 'v2', 'v3']; + $output = $this->plugin->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertSame($output, $value); + $this->assertTrue($this->plugin->multiple()); + } + +} diff --git a/web/modules/migrate_plus/tests/src/Unit/process/SingleValueTest.php b/web/modules/migrate_plus/tests/src/Unit/process/SingleValueTest.php new file mode 100644 index 0000000000..203b1c5cb3 --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Unit/process/SingleValueTest.php @@ -0,0 +1,32 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Unit\process; + +use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase; +use Drupal\migrate_plus\Plugin\migrate\process\SingleValue; + +/** + * @coversDefaultClass \Drupal\migrate_plus\Plugin\migrate\process\SingleValue + * @group migrate + */ +class SingleValueTest extends MigrateProcessTestCase { + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->plugin = new SingleValue([], 'single_value', []); + parent::setUp(); + } + + /** + * Test input treated as single value output. + */ + public function testTreatAsSingle() { + $value = ['v1', 'v2', 'v3']; + $output = $this->plugin->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertSame($output, $value); + $this->assertFalse($this->plugin->multiple()); + } + +} diff --git a/web/modules/migrate_plus/tests/src/Unit/process/SkipOnValueTest.php b/web/modules/migrate_plus/tests/src/Unit/process/SkipOnValueTest.php new file mode 100644 index 0000000000..968e6f0fbc --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Unit/process/SkipOnValueTest.php @@ -0,0 +1,145 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Unit\process; + +use Drupal\migrate\MigrateException; +use Drupal\migrate\MigrateSkipProcessException; +use Drupal\migrate\MigrateSkipRowException; +use Drupal\migrate_plus\Plugin\migrate\process\SkipOnValue; +use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase; + +/** + * Tests the skip on value process plugin. + * + * @group migrate + * @coversDefaultClass \Drupal\migrate_plus\Plugin\migrate\process\SkipOnValue + */ +class SkipOnValueTest extends MigrateProcessTestCase { + + /** + * @covers ::process + */ + public function testProcessSkipsOnValue() { + $configuration['method'] = 'process'; + $configuration['value'] = 86; + $this->setExpectedException(MigrateSkipProcessException::class); + (new SkipOnValue($configuration, 'skip_on_value', [])) + ->transform('86', $this->migrateExecutable, $this->row, 'destinationproperty'); + } + + /** + * @covers ::process + */ + public function testProcessSkipsOnMultipleValue() { + $configuration['method'] = 'process'; + $configuration['value'] = [1, 1, 2, 3, 5, 8]; + $this->setExpectedException(MigrateSkipProcessException::class); + (new SkipOnValue($configuration, 'skip_on_value', [])) + ->transform('5', $this->migrateExecutable, $this->row, 'destinationproperty'); + } + + /** + * @covers ::process + */ + public function testProcessBypassesOnNonValue() { + $configuration['method'] = 'process'; + $configuration['value'] = 'sourcevalue'; + $configuration['not_equals'] = TRUE; + $value = (new SkipOnValue($configuration, 'skip_on_value', [])) + ->transform('sourcevalue', $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertEquals($value, 'sourcevalue'); + $configuration['value'] = 86; + $value = (new SkipOnValue($configuration, 'skip_on_value', [])) + ->transform('86', $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertEquals($value, '86'); + } + + /** + * @covers ::process + */ + public function testProcessSkipsOnMultipleNonValue() { + $configuration['method'] = 'process'; + $configuration['value'] = [1, 1, 2, 3, 5, 8]; + $value = (new SkipOnValue($configuration, 'skip_on_value', [])) + ->transform(4, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertEquals($value, '4'); + } + + /** + * @covers ::process + */ + public function testProcessBypassesOnMultipleNonValue() { + $configuration['method'] = 'process'; + $configuration['value'] = [1, 1, 2, 3, 5, 8]; + $configuration['not_equals'] = TRUE; + $value = (new SkipOnValue($configuration, 'skip_on_value', [])) + ->transform(5, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertEquals($value, '5'); + $value = (new SkipOnValue($configuration, 'skip_on_value', [])) + ->transform(1, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertEquals($value, '1'); + } + + /** + * @covers ::row + */ + public function testRowBypassesOnMultipleNonValue() { + $configuration['method'] = 'row'; + $configuration['value'] = [1, 1, 2, 3, 5, 8]; + $configuration['not_equals'] = TRUE; + $value = (new SkipOnValue($configuration, 'skip_on_value', [])) + ->transform(5, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertEquals($value, '5'); + $value = (new SkipOnValue($configuration, 'skip_on_value', [])) + ->transform(1, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertEquals($value, '1'); + } + + /** + * @covers ::row + */ + public function testRowSkipsOnValue() { + $configuration['method'] = 'row'; + $configuration['value'] = 86; + $this->setExpectedException(MigrateSkipRowException::class); + (new SkipOnValue($configuration, 'skip_on_value', [])) + ->transform('86', $this->migrateExecutable, $this->row, 'destinationproperty'); + } + + /** + * @covers ::row + */ + public function testRowBypassesOnNonValue() { + $configuration['method'] = 'row'; + $configuration['value'] = 'sourcevalue'; + $configuration['not_equals'] = TRUE; + $value = (new SkipOnValue($configuration, 'skip_on_value', [])) + ->transform('sourcevalue', $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertEquals($value, 'sourcevalue'); + $configuration['value'] = 86; + $value = (new SkipOnValue($configuration, 'skip_on_value', [])) + ->transform('86', $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertEquals($value, 86); + } + + /** + * @covers ::row + */ + public function testRequiredRowConfiguration() { + $configuration['method'] = 'row'; + $this->setExpectedException(MigrateException::class); + (new SkipOnValue($configuration, 'skip_on_value', [])) + ->transform('sourcevalue', $this->migrateExecutable, $this->row, 'destinationproperty'); + } + + /** + * @covers ::process + */ + public function testRequiredProcessConfiguration() { + $configuration['method'] = 'process'; + $this->setExpectedException(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 new file mode 100644 index 0000000000..7194c711be --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Unit/process/StrReplaceTest.php @@ -0,0 +1,100 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Unit\process; + +use Drupal\migrate_plus\Plugin\migrate\process\StrReplace; +use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase; + +/** + * Tests the str replace process plugin. + * + * @group migrate + * @coversDefaultClass \Drupal\migrate_plus\Plugin\migrate\process\StrReplace + */ +class StrReplaceTest extends MigrateProcessTestCase { + + /** + * Test for a simple str_replace string. + */ + public function testStrReplace() { + $value = 'vero eos et accusam et justo vero'; + $configuration['search'] = 'et'; + $configuration['replace'] = 'that'; + $plugin = new StrReplace($configuration, 'str_replace', []); + $actual = $plugin->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertSame('vero eos that accusam that justo vero', $actual); + + } + + /** + * Test for case insensitive searches. + */ + public function testStrIreplace() { + $value = 'VERO eos et accusam et justo vero'; + $configuration['search'] = 'vero'; + $configuration['replace'] = 'that'; + $configuration['case_insensitive'] = TRUE; + $plugin = new StrReplace($configuration, 'str_replace', []); + $actual = $plugin->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertSame('that eos et accusam et justo that', $actual); + + } + + /** + * Test for regular expressions. + */ + public function testPregReplace() { + $value = 'vero eos et 123 accusam et justo 123 duo'; + $configuration['search'] = '/[0-9]{3}/'; + $configuration['replace'] = 'the'; + $configuration['regex'] = TRUE; + $plugin = new StrReplace($configuration, 'str_replace', []); + $actual = $plugin->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertSame('vero eos et the accusam et justo the duo', $actual); + } + + /** + * Test for MigrateException for "search" configuration. + */ + public function testSearchMigrateException() { + $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.'); + $plugin->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + } + + /** + * Test for MigrateException for "replace" configuration. + */ + public function testReplaceMigrateException() { + $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.'); + $plugin->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + } + + /** + * Test for multiple. + */ + public function testIsMultiple() { + $value = [ + 'vero eos et accusam et justo vero', + 'et eos vero accusam vero justo et', + ]; + + $expected = [ + 'vero eos that accusam that justo vero', + 'that eos vero accusam vero justo that', + ]; + $configuration['search'] = 'et'; + $configuration['replace'] = 'that'; + $plugin = new StrReplace($configuration, 'str_replace', []); + $actual = $plugin->transform($value, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertArrayEquals($expected, $actual); + + $this->assertTrue($plugin->multiple()); + } + +} diff --git a/web/modules/migrate_plus/tests/src/Unit/process/TransliterationTest.php b/web/modules/migrate_plus/tests/src/Unit/process/TransliterationTest.php new file mode 100644 index 0000000000..dbfe288570 --- /dev/null +++ b/web/modules/migrate_plus/tests/src/Unit/process/TransliterationTest.php @@ -0,0 +1,51 @@ +<?php + +namespace Drupal\Tests\migrate_plus\Unit\process; + +use Drupal\Component\Transliteration\PhpTransliteration; +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\Row; +use Drupal\migrate_plus\Plugin\migrate\process\Transliteration; +use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase; + +/** + * Tests the transliteration process plugin. + * + * @group migrate_plus + */ +class TransliterationTest extends MigrateProcessTestCase { + + /** + * A transliteration instance. + * + * @var \Drupal\Component\Transliteration\TransliterationInterface + */ + protected $transliteration; + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->transliteration = new PhpTransliteration(); + $this->row = $this->getMockBuilder(Row::class) + ->disableOriginalConstructor() + ->getMock(); + $this->migrateExecutable = $this->getMockBuilder(MigrateExecutableInterface::class) + ->disableOriginalConstructor() + ->getMock(); + parent::setUp(); + } + + /** + * Tests transliteration transformation of non-alphanumeric characters. + */ + public function testTransform() { + $actual = '9000004351_53494854_Spøgelsesjægerneáéö'; + $expected_result = '9000004351_53494854_Spogelsesjaegerneaeo'; + + $plugin = new Transliteration([], 'transliteration', [], $this->transliteration); + $value = $plugin->transform($actual, $this->migrateExecutable, $this->row, 'destinationproperty'); + $this->assertEquals($expected_result, $value); + } + +} diff --git a/web/modules/migrate_tools/LICENSE.txt b/web/modules/migrate_tools/LICENSE.txt new file mode 100644 index 0000000000..d159169d10 --- /dev/null +++ b/web/modules/migrate_tools/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/web/modules/migrate_tools/README.txt b/web/modules/migrate_tools/README.txt new file mode 100644 index 0000000000..5f46ba08e4 --- /dev/null +++ b/web/modules/migrate_tools/README.txt @@ -0,0 +1,15 @@ +The Migrate Tools module provides tools for running and managing Drupal 8 +migrations. + +Drush commands supported include: + +* migrate-status - Lists migrations and their status. +* migrate-import - Performs import operations. +* migrate-rollback - Performs rollback operations. +* migrate-stop - Cleanly stops a running operation. +* migrate-reset-status - Sets a migration status to Idle if it's gotten stuck. +* migrate-messages - Lists any messages associated with a migration import. + +The UI at this point provides a front-end equivalent to the migrate-status and +migrate-messages commands. It will be enhanced to allow running the other +operations, as well as provide the ability to create and alter migrations. diff --git a/web/modules/migrate_tools/composer.json b/web/modules/migrate_tools/composer.json new file mode 100644 index 0000000000..d5c8fca8fd --- /dev/null +++ b/web/modules/migrate_tools/composer.json @@ -0,0 +1,27 @@ +{ + "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" + } + } + } +} diff --git a/web/modules/migrate_tools/drush.services.yml b/web/modules/migrate_tools/drush.services.yml new file mode 100644 index 0000000000..ed0a3917b1 --- /dev/null +++ b/web/modules/migrate_tools/drush.services.yml @@ -0,0 +1,6 @@ +services: + migrate_tools.commands: + class: \Drupal\migrate_tools\Commands\MigrateToolsCommands + arguments: ['@plugin.manager.migration', '@date.formatter', '@entity_type.manager', '@keyvalue'] + tags: + - { name: drush.command } diff --git a/web/modules/migrate_tools/migrate_tools.drush.inc b/web/modules/migrate_tools/migrate_tools.drush.inc new file mode 100644 index 0000000000..73ddec5307 --- /dev/null +++ b/web/modules/migrate_tools/migrate_tools.drush.inc @@ -0,0 +1,561 @@ +<?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 new file mode 100644 index 0000000000..e3cb18da29 --- /dev/null +++ b/web/modules/migrate_tools/migrate_tools.info.yml @@ -0,0 +1,16 @@ +type: module +name: Migrate Tools +description: 'Tools to assist in developing and running migrations.' +package: Migration +# core: 8.x +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' +project: 'migrate_tools' +datestamp: 1535380087 diff --git a/web/modules/migrate_tools/migrate_tools.links.action.yml b/web/modules/migrate_tools/migrate_tools.links.action.yml new file mode 100644 index 0000000000..869297cf7c --- /dev/null +++ b/web/modules/migrate_tools/migrate_tools.links.action.yml @@ -0,0 +1,11 @@ +migrate_tools.add_group_action: + route_name: entity.migration_group.add_form + title: 'Add migration group' + appears_on: + - entity.migration_group.list + +#migrate_tools.add_migration_action: +# route_name: entity.migration.add_form +# title: 'Add migration' +# appears_on: +# - entity.migration.list diff --git a/web/modules/migrate_tools/migrate_tools.links.menu.yml b/web/modules/migrate_tools/migrate_tools.links.menu.yml new file mode 100644 index 0000000000..d55ba56c13 --- /dev/null +++ b/web/modules/migrate_tools/migrate_tools.links.menu.yml @@ -0,0 +1,5 @@ +migrate_tools.menu: + title: Migrations + parent: system.admin_structure + description: Manage migration processes. + route_name: entity.migration_group.list diff --git a/web/modules/migrate_tools/migrate_tools.links.task.yml b/web/modules/migrate_tools/migrate_tools.links.task.yml new file mode 100644 index 0000000000..da508a0ff0 --- /dev/null +++ b/web/modules/migrate_tools/migrate_tools.links.task.yml @@ -0,0 +1,53 @@ +# Component routing definition +entity.migration.overview: + route_name: entity.migration.overview + base_route: entity.migration.overview + title: View + +entity.migration.overview_general: + title: Overview + route_name: entity.migration.overview + parent_id: entity.migration.overview +entity.migration.overview_source: + title: Source + route_name: entity.migration.source + parent_id: entity.migration.overview + weight: 1 +entity.migration.overview_process: + title: Process + route_name: entity.migration.process + parent_id: entity.migration.overview + weight: 2 +entity.migration.overview_destination: + title: Destination + route_name: entity.migration.destination + parent_id: entity.migration.overview + weight: 3 + +entity.migration.edit_form: + route_name: entity.migration.edit_form + base_route: entity.migration.overview + title: Edit + +migrate_tools.messages: + route_name: migrate_tools.messages + base_route: entity.migration.overview + title: Messages + +entity.migration.delete_form: + route_name: entity.migration.delete_form + base_route: entity.migration.overview + 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 new file mode 100644 index 0000000000..b8113fe3d8 --- /dev/null +++ b/web/modules/migrate_tools/migrate_tools.module @@ -0,0 +1,58 @@ +<?php + +/** + * @file + * Provides tools for implementing and managing migrations. + */ + +/** + * Implements hook_entity_type_build(). + */ +function migrate_tools_entity_type_build(array &$entity_types) { + // Inject our UI into the general migration and migration group config + // entities. + /** @var \Drupal\Core\Config\Entity\ConfigEntityType[] $entity_types */ + $entity_types['migration'] + ->set('admin_permission', 'administer migrations') + ->setHandlerClass('list_builder', 'Drupal\migrate_tools\Controller\MigrationListBuilder') + ->setFormClass('edit', 'Drupal\migrate_tools\Form\MigrationEditForm') + ->setFormClass('delete', 'Drupal\migrate_tools\Form\MigrationDeleteForm') + ->setLinkTemplate('list-form', '/admin/structure/migrate/manage/{migration_group}/migrations'); + + $entity_types['migration_group'] + ->set('admin_permission', 'administer migrations') + ->setHandlerClass('list_builder', 'Drupal\migrate_tools\Controller\MigrationGroupListBuilder') + ->setFormClass('add', 'Drupal\migrate_tools\Form\MigrationGroupAddForm') + ->setFormClass('edit', 'Drupal\migrate_tools\Form\MigrationGroupEditForm') + ->setFormClass('delete', 'Drupal\migrate_tools\Form\MigrationGroupDeleteForm') + ->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.permissions.yml b/web/modules/migrate_tools/migrate_tools.permissions.yml new file mode 100644 index 0000000000..b5301e5c1f --- /dev/null +++ b/web/modules/migrate_tools/migrate_tools.permissions.yml @@ -0,0 +1,3 @@ +'administer migrations': + title: 'Administer migrations' + description: Create, edit, and manage migration processed. diff --git a/web/modules/migrate_tools/migrate_tools.routing.yml b/web/modules/migrate_tools/migrate_tools.routing.yml new file mode 100644 index 0000000000..08f43e5b58 --- /dev/null +++ b/web/modules/migrate_tools/migrate_tools.routing.yml @@ -0,0 +1,197 @@ +# This is the router item for listing all migration group entities. +entity.migration_group.list: + path: '/admin/structure/migrate' + defaults: + _entity_list: 'migration_group' + _title: 'Migrations' + requirements: + _permission: 'administer migrations' + +# This is the router item for adding our migration group entity. +entity.migration_group.add_form: + path: '/admin/structure/migrate/add' + defaults: + _title: 'Add migration group' + _entity_form: migration_group.add + requirements: + _entity_create_access: migration_group + +# This is the router item for editing our migration group entity. +entity.migration_group.edit_form: + path: '/admin/structure/migrate/manage/{migration_group}' + defaults: + _title: 'Edit migration group' + _entity_form: migration_group.edit + requirements: + _entity_access: migration_group.update + +# This is the router item for deleting an instance of our migration group entity. +entity.migration_group.delete_form: + path: '/admin/structure/migrate/manage/{migration_group}/delete' + defaults: + _title: 'Delete migration group' + _entity_form: migration_group.delete + requirements: + _entity_access: migration_group.delete + +# This is the router item for listing all migration entities. +entity.migration.list: + path: '/admin/structure/migrate/manage/{migration_group}/migrations' + defaults: + _entity_list: 'migration' + _title: 'Migrations' + requirements: + _permission: 'administer migrations' + +# This is the router item for adding our migration entity. +#entity.migration.add_form: +# path: '/admin/structure/migrate/manage/{migration_group}/migrations/add' +# defaults: +# _title: 'Add migration' +# _entity_form: migration.add +# requirements: +# _entity_create_access: migration + +# This is the router item for viewing our migration entity. +entity.migration.overview: + path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}' + defaults: + _controller: '\Drupal\migrate_tools\Controller\MigrationController::overview' + _title: 'Migration overview' + _migrate_group: true + requirements: + _permission: 'administer migrations' + options: + parameters: + migration: + type: entity:migration + migration_group: + type: entity:migration_group +entity.migration.source: + path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/source' + defaults: + _controller: '\Drupal\migrate_tools\Controller\MigrationController::source' + _title: 'Source' + _migrate_group: true + requirements: + _permission: 'administer migrations' + options: + parameters: + migration: + type: entity:migration + migration_group: + type: entity:migration_group +entity.migration.process: + path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/process' + defaults: + _controller: '\Drupal\migrate_tools\Controller\MigrationController::process' + _title: 'Process' + _migrate_group: true + requirements: + _permission: 'administer migrations' + options: + parameters: + migration: + type: entity:migration + migration_group: + type: entity:migration_group +entity.migration.process.run: + path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/process/run' + defaults: + _controller: '\Drupal\migrate_tools\Controller\MigrationController::run' + _title: 'Run' + _migrate_group: true + requirements: + _permission: 'administer migrations' + options: + parameters: + migration: + type: entity:migration + migration_group: + type: entity:migration_group +entity.migration.destination: + path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/destination' + defaults: + _controller: '\Drupal\migrate_tools\Controller\MigrationController::destination' + _title: 'Destination' + _migrate_group: true + requirements: + _permission: 'administer migrations' + options: + parameters: + migration: + type: entity:migration + migration_group: + type: entity:migration_group +# This is the router item for editing our migration entity. +entity.migration.edit_form: + path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/edit' + defaults: + _title: 'Edit migration' + _entity_form: migration.edit + _migrate_group: true + requirements: + _entity_access: migration.update + options: + parameters: + migration: + type: entity:migration + migration_group: + type: entity:migration_group +# This is the router item for deleting an instance of our migration entity. +entity.migration.delete_form: + path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/delete' + defaults: + _title: 'Delete migration' + _entity_form: migration.delete + _migrate_group: true + requirements: + _entity_access: migration.delete + options: + parameters: + migration: + type: entity:migration + migration_group: + type: entity:migration_group +migrate_tools.messages: + path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/messages' + defaults: + _controller: '\Drupal\migrate_tools\Controller\MessageController::overview' + _title: 'Messages' + _migrate_group: true + requirements: + _permission: 'administer migrations' + options: + parameters: + migration: + type: entity:migration + migration_group: + type: entity:migration_group +migrate_tools.execute: + path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/execute' + defaults: + _form: '\Drupal\migrate_tools\Form\MigrationExecuteForm' + _title: 'Execute migration' + _migrate_group: true + requirements: + _permission: 'administer migrations' + options: + parameters: + migration: + 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 new file mode 100644 index 0000000000..fa61e007f5 --- /dev/null +++ b/web/modules/migrate_tools/migrate_tools.services.yml @@ -0,0 +1,9 @@ +services: + logger.channel.migrate_tools: + class: Drupal\Core\Logger\LoggerChannel + factory: logger.factory:get + arguments: ['migrate_tools'] + route_processor.migrate_group: + class: Drupal\migrate_tools\Routing\RouteProcessor + tags: + - { name: route_processor_outbound } diff --git a/web/modules/migrate_tools/phpcs.xml b/web/modules/migrate_tools/phpcs.xml new file mode 100644 index 0000000000..a18054efbc --- /dev/null +++ b/web/modules/migrate_tools/phpcs.xml @@ -0,0 +1,207 @@ +<?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 new file mode 100644 index 0000000000..976b3c684d --- /dev/null +++ b/web/modules/migrate_tools/src/Commands/MigrateToolsCommands.php @@ -0,0 +1,719 @@ +<?php + +namespace Drupal\migrate_tools\Commands; + +use Consolidation\OutputFormatters\StructuredData\RowsOfFields; +use Drupal\Component\Utility\Unicode; +use Drupal\Core\Datetime\DateFormatter; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; +use Drupal\migrate\Exception\RequirementsException; +use Drupal\migrate\Plugin\MigrationInterface; +use Drupal\migrate\Plugin\MigrationPluginManager; +use Drupal\migrate\Plugin\RequirementsInterface; +use Drupal\migrate_tools\Drush9LogMigrateMessage; +use Drupal\migrate_tools\MigrateExecutable; +use Drush\Commands\DrushCommands; + +/** + * Migrate Tools drush commands. + */ +class MigrateToolsCommands extends DrushCommands { + + /** + * Migration plugin manager service. + * + * @var \Drupal\migrate\Plugin\MigrationPluginManager + */ + protected $migrationPluginManager; + + /** + * Date formatter service. + * + * @var \Drupal\Core\Datetime\DateFormatter + */ + protected $dateFormatter; + + /** + * Entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * Key-value store service. + * + * @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface + */ + protected $keyValue; + + /** + * Migrate message logger. + * + * @var \Drupal\migrate_tools\Drush9LogMigrateMessage + */ + protected $migrateMessage; + + /** + * MigrateToolsCommands constructor. + * + * @param \Drupal\migrate\Plugin\MigrationPluginManager $migrationPluginManager + * Migration Plugin Manager service. + * @param \Drupal\Core\Datetime\DateFormatter $dateFormatter + * Date formatter service. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager + * Entity type manager service. + * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $keyValue + * Key-value store service. + */ + public function __construct(MigrationPluginManager $migrationPluginManager, DateFormatter $dateFormatter, EntityTypeManagerInterface $entityTypeManager, KeyValueFactoryInterface $keyValue) { + parent::__construct(); + $this->migrationPluginManager = $migrationPluginManager; + $this->dateFormatter = $dateFormatter; + $this->entityTypeManager = $entityTypeManager; + $this->keyValue = $keyValue; + } + + /** + * List all migrations with current status. + * + * @param string $migration_names + * Restrict to a comma-separated list of migrations (Optional). + * @param array $options + * Additional options for the command. + * + * @command migrate:status + * + * @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) + * + * @usage migrate:status + * Retrieve status for all migrations + * @usage migrate:status --group=beer + * Retrieve status for all migrations in a given group + * @usage migrate:status --tag=user + * Retrieve status for all migrations with a given tag + * @usage migrate:status --group=beer --tag=user + * Retrieve status for all migrations in the beer group + * and with the user tag. + * @usage migrate:status beer_term,beer_node + * Retrieve status for specific migrations + * + * @validate-module-enabled migrate_tools + * + * @aliases ms, migrate-status + * + * @field-labels + * group: Group + * id: Migration ID + * status: Status + * total: Total + * imported: Imported + * unprocessed: Unprocessed + * last_imported: Last Imported + * @default-fields group,id,status,total,imported,unprocessed,last_imported + * + * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields + * Migrations status formatted as table. + */ + public function status($migration_names = '', array $options = ['group' => NULL, 'tag' => NULL, 'names-only' => NULL]) { + $names_only = $options['names-only']; + + $migrations = $this->migrationsList($migration_names, $options); + + $table = []; + // 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 */ + $group = $this->entityTypeManager->getStorage('migration_group')->load($group_id); + $group_name = !empty($group) ? "{$group->label()} ({$group->id()})" : $group_id; + + foreach ($migration_list as $migration_id => $migration) { + if ($names_only) { + $table[] = [ + 'group' => dt('Group: @name', ['@name' => $group_name]), + 'id' => $migration_id, + ]; + } + else { + try { + $map = $migration->getIdMap(); + $imported = $map->importedCount(); + $source_plugin = $migration->getSourcePlugin(); + } + catch (\Exception $e) { + $this->logger()->error( + dt( + 'Failure retrieving information on @migration: @message', + ['@migration' => $migration_id, '@message' => $e->getMessage()] + ) + ); + continue; + } + + 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) { + $this->logger()->error( + 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 = $this->keyValue->get( + 'migrate_last_imported' + ); + $last_imported = $migrate_last_imported_store->get( + $migration->id(), + FALSE + ); + if ($last_imported) { + $last_imported = $this->dateFormatter->format( + $last_imported / 1000, + 'custom', + 'Y-m-d H:i:s' + ); + } + else { + $last_imported = ''; + } + $table[] = [ + 'group' => $group_name, + 'id' => $migration_id, + 'status' => $status, + 'total' => $source_rows, + 'imported' => $imported, + 'unprocessed' => $unprocessed, + 'last_imported' => $last_imported, + ]; + } + } + + // Add empty row to separate groups, for readability. + end($migrations); + if ($group_id !== key($migrations)) { + $table[] = []; + } + } + + return new RowsOfFields($table); + } + + /** + * Perform one or more migration processes. + * + * @param string $migration_names + * ID of migration(s) to import. Delimit multiple using commas. + * @param array $options + * Additional options for the command. + * + * @command migrate:import + * + * @option all Process all migrations. + * @option group A comma-separated list of migration groups to import + * @option tag Name of the migration tag to import + * @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 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 execute-dependencies Execute all dependent migrations first. + * + * @usage migrate:import --all + * Perform all migrations + * @usage migrate:import --group=beer + * Import all migrations in the beer group + * @usage migrate:import --tag=user + * Import all migrations with the user tag + * @usage migrate:import --group=beer --tag=user + * Import all migrations in the beer group and with the user tag + * @usage migrate:import beer_term,beer_node + * Import new terms and nodes + * @usage migrate:import beer_user --limit=2 + * Import no more than 2 users + * @usage migrate:import beer_user --idlist=5 + * Import the user record with source ID 5 + * + * @validate-module-enabled migrate_tools + * + * @aliases mim, migrate-import + * + * @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]) { + $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.')); + } + + // Take it one group at a time, importing the migrations within each group. + foreach ($migrations as $group_id => $migration_list) { + array_walk( + $migration_list, + [$this, 'executeMigration'], + $additional_options + ); + } + } + + /** + * Rollback one or more migrations. + * + * @param string $migration_names + * Name of migration(s) to rollback. Delimit multiple using commas. + * @param array $options + * Additional options for the command. + * + * @command migrate:rollback + * + * @option all Process all migrations. + * @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 + * + * @usage migrate:rollback --all + * Perform all migrations + * @usage migrate:rollback --group=beer + * Rollback all migrations in the beer group + * @usage migrate:rollback --tag=user + * Rollback all migrations with the user tag + * @usage migrate:rollback --group=beer --tag=user + * Rollback all migrations in the beer group and with the user tag + * @usage migrate:rollback beer_term,beer_node + * Rollback imported terms and nodes + * @validate-module-enabled migrate_tools + * + * @aliases mr, migrate-rollback + * + * @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]) { + $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.')); + } + + // 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, + $this->getMigrateMessage(), + $additional_options + ); + // drush_op() provides --simulate support. + drush_op([$executable, 'rollback']); + } + } + } + + /** + * Stop an active migration operation. + * + * @param string $migration_id + * ID of migration to stop. + * + * @command migrate:stop + * + * @validate-module-enabled migrate_tools + * @aliases mst, migrate-stop + */ + public function stop($migration_id = '') { + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = $this->migrationPluginManager->createInstance( + $migration_id + ); + if ($migration) { + $status = $migration->getStatus(); + switch ($status) { + case MigrationInterface::STATUS_IDLE: + $this->logger()->warning( + dt('Migration @id is idle', ['@id' => $migration_id]) + ); + break; + + case MigrationInterface::STATUS_DISABLED: + $this->logger()->warning( + dt('Migration @id is disabled', ['@id' => $migration_id]) + ); + break; + + case MigrationInterface::STATUS_STOPPING: + $this->logger()->warning( + dt('Migration @id is already stopping', ['@id' => $migration_id]) + ); + break; + + default: + $migration->interruptMigration(MigrationInterface::RESULT_STOPPED); + $this->logger()->notice( + dt('Migration @id requested to stop', ['@id' => $migration_id]) + ); + break; + } + } + else { + $this->logger()->error( + dt('Migration @id does not exist', ['@id' => $migration_id]) + ); + } + } + + /** + * Reset a active migration's status to idle. + * + * @param string $migration_id + * ID of migration to reset. + * + * @command migrate:reset-status + * + * @validate-module-enabled migrate_tools + * @aliases mrs, migrate-reset-status + */ + public function resetStatus($migration_id = '') { + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = $this->migrationPluginManager->createInstance( + $migration_id + ); + if ($migration) { + $status = $migration->getStatus(); + if ($status == MigrationInterface::STATUS_IDLE) { + $this->logger()->warning( + dt('Migration @id is already Idle', ['@id' => $migration_id]) + ); + } + else { + $migration->setStatus(MigrationInterface::STATUS_IDLE); + $this->logger()->notice( + dt('Migration @id reset to Idle', ['@id' => $migration_id]) + ); + } + } + else { + $this->logger()->error( + dt('Migration @id does not exist', ['@id' => $migration_id]) + ); + } + } + + /** + * View any messages associated with a migration. + * + * @param string $migration_id + * ID of the migration. + * @param array $options + * Additional options for the command. + * + * @command migrate:messages + * + * @option csv Export messages as a CSV + * + * @usage migrate:messages MyNode + * Show all messages for the MyNode migration + * + * @validate-module-enabled migrate_tools + * + * @aliases mmsg,migrate-messages + * + * @field-labels + * source_ids_hash: Source IDs Hash + * level: Level + * message: Message + * @default-fields source_ids_hash,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]) { + /** @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; + } + + $map = $migration->getIdMap(); + $table = []; + foreach ($map->getMessageIterator() as $row) { + unset($row->msgid); + $table[] = (array) $row; + } + if (empty($table)) { + $this->logger()->notice(dt('No messages for this migration')); + return NULL; + } + + if ($options['csv']) { + fputcsv(STDOUT, array_keys($table[0])); + foreach ($table as $row) { + fputcsv(STDOUT, $row); + } + return NULL; + } + return new RowsOfFields($table); + } + + /** + * List the fields available for mapping in a source. + * + * @param string $migration_id + * ID of the migration. + * + * @command migrate:fields-source + * + * @usage migrate:fields-source my_node + * List fields for the source in the my_node migration + * + * @validate-module-enabled migrate_tools + * + * @aliases mfs, migrate-fields-source + * + * @field-labels + * machine_name: Machine Name + * description: Description + * @default-fields machine_name,description + * + * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields + * Source fields of the given migration formatted as a table. + */ + public function fieldsSource($migration_id) { + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = $this->migrationPluginManager->createInstance( + $migration_id + ); + if ($migration) { + $source = $migration->getSourcePlugin(); + $table = []; + foreach ($source->fields() as $machine_name => $description) { + $table[] = [ + 'machine_name' => $machine_name, + 'description' => strip_tags($description), + ]; + } + return new RowsOfFields($table); + } + else { + $this->logger()->error( + dt('Migration @id does not exist', ['@id' => $migration_id]) + ); + } + } + + /** + * Retrieve a list of active migrations. + * + * @param string $migration_ids + * Comma-separated list of migrations - + * if present, return only these migrations. + * @param array $options + * Command 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'] + ) : []; + + $manager = $this->migrationPluginManager; + $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 : []; + } + + /** + * 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 of the command. + * + * @throws \Exception + * If some migrations failed during execution. + */ + protected function executeMigration(MigrationInterface $migration, $migration_id, array $options = []) { + // Keep track of all migrations run during this command so the same + // migration is not run multiple times. + static $executed_migrations = []; + + if (isset($options['execute-dependencies'])) { + $required_migrations = $migration->get('requirements'); + $required_migrations = array_filter($required_migrations, function ($value) use ($executed_migrations) { + return !isset($executed_migrations[$value]); + }); + + if (!empty($required_migrations)) { + $manager = $this->migrationPluginManager; + $required_migrations = $manager->createInstances($required_migrations); + $dependency_options = array_merge($options, ['is_dependency' => TRUE]); + array_walk($required_migrations, [$this, __FUNCTION__], $dependency_options); + $executed_migrations += $required_migrations; + } + } + if (!empty($options['force'])) { + $migration->set('requirements', []); + } + if (!empty($options['update'])) { + $migration->getIdMap()->prepareUpdate(); + } + $executable = new MigrateExecutable($migration, $this->getMigrateMessage(), $options); + // drush_op() provides --simulate support. + 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] + ) + ); + } + } + + /** + * Gets the migrate message logger. + * + * @return \Drupal\migrate\MigrateMessageInterface + * The migrate message service. + */ + protected function getMigrateMessage() { + if (!isset($this->migrateMessage)) { + $this->migrateMessage = new Drush9LogMigrateMessage($this->logger()); + } + return $this->migrateMessage; + } + +} diff --git a/web/modules/migrate_tools/src/Controller/MessageController.php b/web/modules/migrate_tools/src/Controller/MessageController.php new file mode 100644 index 0000000000..fbd347c96d --- /dev/null +++ b/web/modules/migrate_tools/src/Controller/MessageController.php @@ -0,0 +1,144 @@ +<?php + +namespace Drupal\migrate_tools\Controller; + +use Drupal\Component\Utility\Html; +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Database\Connection; +use Drupal\migrate\Plugin\MigrationInterface; +use Drupal\migrate\Plugin\MigrationPluginManagerInterface; +use Drupal\migrate_plus\Entity\MigrationGroupInterface; +use Drupal\migrate_plus\Entity\MigrationInterface as MigratePlusMigrationInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Returns responses for migrate_tools message routes. + */ +class MessageController extends ControllerBase { + + /** + * The database service. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + + /** + * Plugin manager for migration plugins. + * + * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface + */ + protected $migrationPluginManager; + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('database'), + $container->get('plugin.manager.migration') + ); + } + + /** + * Constructs a MessageController object. + * + * @param \Drupal\Core\Database\Connection $database + * A database connection. + * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager + * The migration plugin manager. + */ + public function __construct(Connection $database, MigrationPluginManagerInterface $migration_plugin_manager) { + $this->database = $database; + $this->migrationPluginManager = $migration_plugin_manager; + } + + /** + * Gets an array of log level classes. + * + * @return array + * An array of log level classes. + */ + public static function getLogLevelClassMap() { + return [ + MigrationInterface::MESSAGE_INFORMATIONAL => 'migrate-message-4', + MigrationInterface::MESSAGE_NOTICE => 'migrate-message-3', + MigrationInterface::MESSAGE_WARNING => 'migrate-message-2', + MigrationInterface::MESSAGE_ERROR => 'migrate-message-1', + ]; + } + + /** + * Displays a listing of migration messages. + * + * Messages are truncated at 56 chars. + * + * @param \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group + * The migration group. + * @param \Drupal\migrate_plus\Entity\MigrationInterface $migration + * The $migration. + * + * @return array + * A render array as expected by drupal_render(). + */ + public function overview(MigrationGroupInterface $migration_group, MigratePlusMigrationInterface $migration) { + $rows = []; + $classes = static::getLogLevelClassMap(); + $migration_plugin = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); + $source_id_field_names = array_keys($migration_plugin->getSourcePlugin()->getIds()); + $column_number = 1; + foreach ($source_id_field_names as $source_id_field_name) { + $header[] = [ + 'data' => $source_id_field_name, + 'field' => 'sourceid' . $column_number++, + 'class' => [RESPONSIVE_PRIORITY_MEDIUM], + ]; + } + $header[] = [ + 'data' => $this->t('Severity level'), + 'field' => 'level', + 'class' => [RESPONSIVE_PRIORITY_LOW], + ]; + $header[] = [ + 'data' => $this->t('Message'), + 'field' => 'message', + ]; + + $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(); + + foreach ($result as $message_row) { + $column_number = 1; + foreach ($source_id_field_names as $source_id_field_name) { + $column_name = 'sourceid' . $column_number++; + $row[$column_name] = $message_row->$column_name; + } + $row['level'] = $message_row->level; + $row['message'] = $message_row->message; + $row['class'] = [Html::getClass('migrate-message-' . $message_row->level), $classes[$message_row->level]]; + $rows[] = $row; + } + + $build['message_table'] = [ + '#type' => 'table', + '#header' => $header, + '#rows' => $rows, + '#attributes' => ['id' => $message_table, 'class' => [$message_table]], + '#empty' => $this->t('No messages for this migration.'), + ]; + $build['message_pager'] = ['#type' => 'pager']; + + return $build; + } + +} diff --git a/web/modules/migrate_tools/src/Controller/MigrationController.php b/web/modules/migrate_tools/src/Controller/MigrationController.php new file mode 100644 index 0000000000..15c90795ca --- /dev/null +++ b/web/modules/migrate_tools/src/Controller/MigrationController.php @@ -0,0 +1,300 @@ +<?php + +namespace Drupal\migrate_tools\Controller; + +use Drupal\Core\Controller\ControllerBase; +use Drupal\Component\Utility\Xss; +use Drupal\Component\Utility\Html; +use Drupal\Core\Routing\CurrentRouteMatch; +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. + */ +class MigrationController extends ControllerBase implements ContainerInjectionInterface { + + /** + * Plugin manager for migration plugins. + * + * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface + */ + protected $migrationPluginManager; + + /** + * The current route match. + * + * @var \Drupal\Core\Routing\CurrentRouteMatch + */ + protected $currentRouteMatch; + + /** + * Constructs a new MigrationController object. + * + * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager + * The plugin manager for config entity-based migrations. + * @param \Drupal\Core\Routing\CurrentRouteMatch $currentRouteMatch + * The current route match. + */ + public function __construct(MigrationPluginManagerInterface $migration_plugin_manager, CurrentRouteMatch $currentRouteMatch) { + $this->migrationPluginManager = $migration_plugin_manager; + $this->currentRouteMatch = $currentRouteMatch; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.migration'), + $container->get('current_route_match') + ); + } + + /** + * Displays an overview of a migration entity. + * + * @param \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group + * The migration group. + * @param \Drupal\migrate_plus\Entity\MigrationInterface $migration + * The $migration. + * + * @return array + * A render array as expected by drupal_render(). + */ + public function overview(MigrationGroupInterface $migration_group, MigrationInterface $migration) { + $build['overview'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Overview'), + ]; + + $build['overview']['group'] = [ + '#title' => $this->t('Group:'), + '#markup' => Xss::filterAdmin($migration_group->label()), + '#type' => 'item', + ]; + + $build['overview']['description'] = [ + '#title' => $this->t('Description:'), + '#markup' => Xss::filterAdmin($migration->label()), + '#type' => 'item', + ]; + $migration_plugin = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); + $migration_dependencies = $migration_plugin->getMigrationDependencies(); + if (!empty($migration_dependencies['required'])) { + $build['overview']['dependencies'] = [ + '#title' => $this->t('Migration Dependencies') , + '#markup' => Xss::filterAdmin(implode(', ', $migration_dependencies['required'])), + '#type' => 'item', + ]; + } + if (!empty($migration_dependencies['optional'])) { + $build['overview']['soft_dependencies'] = [ + '#title' => $this->t('Soft Migration Dependencies'), + '#markup' => Xss::filterAdmin(implode(', ', $migration_dependencies['optional'])), + '#type' => 'item', + ]; + } + + return $build; + } + + /** + * Display source information of a migration entity. + * + * @param \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group + * The migration group. + * @param \Drupal\migrate_plus\Entity\MigrationInterface $migration + * The $migration. + * + * @return array + * A render array as expected by drupal_render(). + */ + public function source(MigrationGroupInterface $migration_group, MigrationInterface $migration) { + // Source field information. + $build['source'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Source'), + '#group' => 'detail', + '#description' => $this->t('<p>These are the fields available from the source of this migration task. The machine names listed here may be used as sources in the process pipeline.</p>'), + '#attributes' => [ + 'id' => 'migration-detail-source', + ], + ]; + $migration_plugin = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); + $source = $migration_plugin->getSourcePlugin(); + $build['source']['query'] = [ + '#type' => 'item', + '#title' => $this->t('Query'), + '#markup' => '<pre>' . Xss::filterAdmin($source) . '</pre>', + ]; + $header = [$this->t('Machine name'), $this->t('Description')]; + $rows = []; + foreach ($source->fields($migration_plugin) as $machine_name => $description) { + $rows[] = [ + ['data' => Html::escape($machine_name)], + ['data' => Xss::filterAdmin($description)], + ]; + } + + $build['source']['fields'] = [ + '#theme' => 'table', + '#header' => $header, + '#rows' => $rows, + '#empty' => $this->t('No fields'), + ]; + + return $build; + } + + /** + * Run a migration. + * + * @param \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group + * The migration group. + * @param \Drupal\migrate_plus\Entity\MigrationInterface $migration + * The $migration. + * + * @return array + * A render array as expected by drupal_render(). + */ + public function run(MigrationGroupInterface $migration_group, MigrationInterface $migration) { + $migrateMessage = new MigrateMessage(); + $options = []; + + $migration_plugin = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); + $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(), + ]; + return batch_process(Url::fromRoute('entity.migration.process', $route_parameters)); + } + + /** + * Display process information of a migration entity. + * + * @param \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group + * The migration group. + * @param \Drupal\migrate_plus\Entity\MigrationInterface $migration + * The $migration. + * + * @return array + * A render array as expected by drupal_render(). + */ + public function process(MigrationGroupInterface $migration_group, MigrationInterface $migration) { + $migration_plugin = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); + + // Process information. + $build['process'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Process'), + ]; + + $header = [ + $this->t('Destination'), + $this->t('Source'), + $this->t('Process plugin'), + $this->t('Default'), + ]; + $rows = []; + foreach ($migration_plugin->getProcess() as $destination_id => $process_line) { + $row = []; + $row[] = ['data' => Html::escape($destination_id)]; + if (isset($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'])]; + } + else { + $row[] = ''; + } + if (isset($process_line[0]['default_value'])) { + $row[] = ['data' => Xss::filterAdmin($process_line[0]['default_value'])]; + } + else { + $row[] = ''; + } + $rows[] = $row; + } + + $build['process']['fields'] = [ + '#theme' => 'table', + '#header' => $header, + '#rows' => $rows, + '#empty' => $this->t('No process defined.'), + ]; + + $build['process']['run'] = [ + '#type' => 'link', + '#title' => $this->t('Run'), + '#url' => Url::fromRoute('entity.migration.process.run', ['migration_group' => $migration_group->id(), 'migration' => $migration->id()]), + ]; + + return $build; + } + + /** + * Displays destination information of a migration entity. + * + * @param \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group + * The migration group. + * @param \Drupal\migrate_plus\Entity\MigrationInterface $migration + * The $migration. + * + * @return array + * A render array as expected by drupal_render(). + */ + public function destination(MigrationGroupInterface $migration_group, MigrationInterface $migration) { + $migration_plugin = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); + + // Destination field information. + $build['destination'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Destination'), + '#group' => 'detail', + '#description' => $this->t('<p>These are the fields available in the destination plugin of this migration task. The machine names are those available to be used as the keys in the process pipeline.</p>'), + '#attributes' => [ + 'id' => 'migration-detail-destination', + ], + ]; + $destination = $migration_plugin->getDestinationPlugin(); + $build['destination']['type'] = [ + '#type' => 'item', + '#title' => $this->t('Type'), + '#markup' => Xss::filterAdmin($destination->getPluginId()), + ]; + $header = [$this->t('Machine name'), $this->t('Description')]; + $rows = []; + $destination_fields = $destination->fields() ?: []; + foreach ($destination_fields as $machine_name => $description) { + $rows[] = [ + ['data' => Html::escape($machine_name)], + ['data' => Xss::filterAdmin($description)], + ]; + } + + $build['destination']['fields'] = [ + '#theme' => 'table', + '#header' => $header, + '#rows' => $rows, + '#empty' => $this->t('No fields'), + ]; + + return $build; + } + +} diff --git a/web/modules/migrate_tools/src/Controller/MigrationGroupListBuilder.php b/web/modules/migrate_tools/src/Controller/MigrationGroupListBuilder.php new file mode 100644 index 0000000000..8ab2b87fb5 --- /dev/null +++ b/web/modules/migrate_tools/src/Controller/MigrationGroupListBuilder.php @@ -0,0 +1,68 @@ +<?php + +namespace Drupal\migrate_tools\Controller; + +use Drupal\Core\Config\Entity\ConfigEntityListBuilder; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Url; + +/** + * Provides a listing of migration group entities. + * + * @package Drupal\migrate_tools\Controller + * + * @ingroup migrate_tools + */ +class MigrationGroupListBuilder extends ConfigEntityListBuilder { + + /** + * Builds the header row for the entity listing. + * + * @return array + * A render array structure of header strings. + * + * @see Drupal\Core\Entity\EntityListController::render() + */ + public function buildHeader() { + $header['label'] = $this->t('Migration Group'); + $header['machine_name'] = $this->t('Machine Name'); + $header['description'] = $this->t('Description'); + $header['source_type'] = $this->t('Source Type'); + return $header + parent::buildHeader(); + } + + /** + * Builds a row for an entity in the entity listing. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity for which to build the row. + * + * @return array + * A render array of the table row for displaying the entity. + * + * @see \Drupal\Core\Entity\EntityListController::render() + */ + public function buildRow(EntityInterface $entity) { + $row['label'] = $entity->label(); + $row['machine_name'] = $entity->id(); + $row['description'] = $entity->get('description'); + $row['source_type'] = $entity->get('source_type'); + + return $row + parent::buildRow($entity); + } + + /** + * {@inheritdoc} + */ + public function getDefaultOperations(EntityInterface $entity) { + $operations = parent::getDefaultOperations($entity); + $operations['list'] = [ + 'title' => $this->t('List migrations'), + 'weight' => 0, + 'url' => Url::fromRoute('entity.migration.list', ['migration_group' => $entity->id()]), + ]; + + return $operations; + } + +} diff --git a/web/modules/migrate_tools/src/Controller/MigrationListBuilder.php b/web/modules/migrate_tools/src/Controller/MigrationListBuilder.php new file mode 100644 index 0000000000..d078764cea --- /dev/null +++ b/web/modules/migrate_tools/src/Controller/MigrationListBuilder.php @@ -0,0 +1,244 @@ +<?php + +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\migrate\Plugin\MigrationPluginManagerInterface; +use Drupal\migrate_plus\Entity\MigrationGroup; +use Drupal\Core\Url; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Provides a listing of migration entities in a given group. + * + * @package Drupal\migrate_tools\Controller + * + * @ingroup migrate_tools + */ +class MigrationListBuilder extends ConfigEntityListBuilder implements EntityHandlerInterface { + + /** + * Default object for current_route_match service. + * + * @var \Drupal\Core\Routing\CurrentRouteMatch + */ + protected $currentRouteMatch; + + /** + * Plugin manager for migration plugins. + * + * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface + */ + protected $migrationPluginManager; + + /** + * The logger service. + * + * @var \Drupal\Core\Logger\LoggerChannelInterface + */ + protected $logger; + + /** + * Constructs a new EntityListBuilder object. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type definition. + * @param \Drupal\Core\Entity\EntityStorageInterface $storage + * The entity storage class. + * @param \Drupal\Core\Routing\CurrentRouteMatch $current_route_match + * 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 + * The logger service. + */ + public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, CurrentRouteMatch $current_route_match, MigrationPluginManagerInterface $migration_plugin_manager, LoggerChannelInterface $logger) { + parent::__construct($entity_type, $storage); + $this->currentRouteMatch = $current_route_match; + $this->migrationPluginManager = $migration_plugin_manager; + $this->logger = $logger; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('entity.manager')->getStorage($entity_type->id()), + $container->get('current_route_match'), + $container->get('plugin.manager.migration'), + $container->get('logger.channel.migrate_tools') + ); + } + + /** + * Retrieve the migrations belonging to the appropriate group. + * + * @return array + * An array of entity IDs. + */ + protected function getEntityIds() { + $migration_group = $this->currentRouteMatch->getParameter('migration_group'); + + $query = $this->getStorage()->getQuery() + ->sort($this->entityType->getKey('id')); + + $migration_groups = MigrationGroup::loadMultiple(); + + if (array_key_exists($migration_group, $migration_groups)) { + $query->condition('migration_group', $migration_group); + } + else { + $query->notExists('migration_group'); + } + // Only add the pager if a limit is specified. + if ($this->limit) { + $query->pager($this->limit); + } + return $query->execute(); + } + + /** + * Builds the header row for the entity listing. + * + * @return array + * A render array structure of header strings. + * + * @see \Drupal\Core\Entity\EntityListController::render() + */ + public function buildHeader() { + $header['label'] = $this->t('Migration'); + $header['machine_name'] = $this->t('Machine Name'); + $header['status'] = $this->t('Status'); + $header['total'] = $this->t('Total'); + $header['imported'] = $this->t('Imported'); + $header['unprocessed'] = $this->t('Unprocessed'); + $header['messages'] = $this->t('Messages'); + $header['last_imported'] = $this->t('Last Imported'); + $header['operations'] = $this->t('Operations'); + return $header; + } + + /** + * Builds a row for a migration plugin. + * + * @param \Drupal\Core\Entity\EntityInterface $migration_entity + * The migration plugin for which to build the row. + * + * @return array|null + * A render array of the table row for displaying the plugin information. + * + * @see \Drupal\Core\Entity\EntityListController::render() + */ + 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'); + if (!$migration_group) { + $migration_group = 'default'; + } + $route_parameters = [ + 'migration_group' => $migration_group, + 'migration' => $migration->id(), + ]; + $row['label'] = [ + 'data' => [ + '#type' => 'link', + '#title' => $migration->label(), + '#url' => Url::fromRoute("entity.migration.overview", $route_parameters), + ], + ]; + $row['machine_name'] = $migration->id(); + $row['status'] = $migration->getStatusLabel(); + } + catch (PluginException $e) { + $this->logger->warning('Migration entity id %id is malformed: %orig', ['%id' => $migration_entity->id(), '%orig' => $e->getMessage()]); + return NULL; + } + + try { + // Derive the stats. + $source_plugin = $migration->getSourcePlugin(); + $row['total'] = $source_plugin->count(); + $map = $migration->getIdMap(); + $row['imported'] = $map->importedCount(); + // -1 indicates uncountable sources. + if ($row['total'] == -1) { + $row['total'] = $this->t('N/A'); + $row['unprocessed'] = $this->t('N/A'); + } + else { + $row['unprocessed'] = $row['total'] - $map->processedCount(); + } + $row['messages'] = [ + 'data' => [ + '#type' => 'link', + '#title' => $map->messageCount(), + '#url' => Url::fromRoute("migrate_tools.messages", $route_parameters), + ], + ]; + $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'); + $row['last_imported'] = $date_formatter->format($last_imported / 1000, + 'custom', 'Y-m-d H:i:s'); + } + else { + $row['last_imported'] = ''; + } + + $row['operations']['data'] = [ + '#type' => 'dropbutton', + '#links' => [ + 'simple_form' => [ + 'title' => $this->t('Execute'), + 'url' => Url::fromRoute('migrate_tools.execute', [ + 'migration_group' => $migration_group, + 'migration' => $migration->id(), + ]), + ], + ], + ]; + } + 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'); + } + + return $row; + } + + /** + * Add group route parameter. + * + * @param \Drupal\Core\Url $url + * The URL associated with an operation. + * @param string $migration_group + * The migration's parent group. + */ + protected function addGroupParameter(Url $url, $migration_group) { + if (!$migration_group) { + $migration_group = 'default'; + } + $route_parameters = $url->getRouteParameters() + ['migration_group' => $migration_group]; + $url->setRouteParameters($route_parameters); + } + +} diff --git a/web/modules/migrate_tools/src/Drush9LogMigrateMessage.php b/web/modules/migrate_tools/src/Drush9LogMigrateMessage.php new file mode 100644 index 0000000000..0217a58e18 --- /dev/null +++ b/web/modules/migrate_tools/src/Drush9LogMigrateMessage.php @@ -0,0 +1,48 @@ +<?php + +namespace Drupal\migrate_tools; + +use Drupal\migrate\MigrateMessageInterface; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\LoggerInterface; + +/** + * Print message in drush from migrate message. Drush 9 version. + * + * @package Drupal\migrate_tools + */ +class Drush9LogMigrateMessage implements MigrateMessageInterface, LoggerAwareInterface { + + use LoggerAwareTrait; + + /** + * The map between migrate status and drush log levels. + * + * @var array + */ + protected $map = [ + 'status' => 'notice', + ]; + + /** + * DrushLogMigrateMessage constructor. + */ + public function __construct(LoggerInterface $logger) { + $this->setLogger($logger); + } + + /** + * Output a message from the migration. + * + * @param string $message + * The message to display. + * @param string $type + * The type of message to display. + */ + public function display($message, $type = 'status') { + $type = isset($this->map[$type]) ? $this->map[$type] : $type; + $this->logger->log($type, $message); + } + +} diff --git a/web/modules/migrate_tools/src/DrushLogMigrateMessage.php b/web/modules/migrate_tools/src/DrushLogMigrateMessage.php new file mode 100644 index 0000000000..4f7e92494c --- /dev/null +++ b/web/modules/migrate_tools/src/DrushLogMigrateMessage.php @@ -0,0 +1,28 @@ +<?php + +namespace Drupal\migrate_tools; + +use Drupal\migrate\MigrateMessageInterface; + +/** + * Class DrushLogMigrateMessage. + * + * @package Drupal\migrate_tools + */ +class DrushLogMigrateMessage implements MigrateMessageInterface { + + /** + * Output a message from the migration. + * + * @param string $message + * The message to display. + * @param string $type + * The type of message to display. + * + * @see drush_log() + */ + public function display($message, $type = 'status') { + drush_log($message, $type); + } + +} diff --git a/web/modules/migrate_tools/src/Form/MigrationAddForm.php b/web/modules/migrate_tools/src/Form/MigrationAddForm.php new file mode 100644 index 0000000000..63c1fc5c31 --- /dev/null +++ b/web/modules/migrate_tools/src/Form/MigrationAddForm.php @@ -0,0 +1,37 @@ +<?php + +namespace Drupal\migrate_tools\Form; + +use Drupal\Core\Form\FormStateInterface; + +/** + * Class MigrationAddForm. + * + * Provides the add form for our migration entity. + * + * @package Drupal\migrate_tools\Form + * + * @ingroup migrate_tools + */ +class MigrationAddForm extends MigrationFormBase { + + /** + * Returns the actions provided by this form. + * + * For our add form, we only need to change the text of the submit button. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * An associative array containing the current state of the form. + * + * @return array + * An array of supported actions for the current entity form. + */ + protected function actions(array $form, FormStateInterface $form_state) { + $actions = parent::actions($form, $form_state); + unset($actions['submit']); + return $actions; + } + +} diff --git a/web/modules/migrate_tools/src/Form/MigrationDeleteForm.php b/web/modules/migrate_tools/src/Form/MigrationDeleteForm.php new file mode 100644 index 0000000000..593046dbae --- /dev/null +++ b/web/modules/migrate_tools/src/Form/MigrationDeleteForm.php @@ -0,0 +1,71 @@ +<?php + +namespace Drupal\migrate_tools\Form; + +use Drupal\Core\Entity\EntityConfirmFormBase; +use Drupal\Core\Url; +use Drupal\Core\Form\FormStateInterface; + +/** + * Provides the delete form for our Migration entity. + * + * @package Drupal\migrate_tools\Form + * + * @ingroup migrate_tools + */ +class MigrationDeleteForm extends EntityConfirmFormBase { + + /** + * Gathers a confirmation question. + * + * @return string + * Translated string. + */ + public function getQuestion() { + return $this->t('Are you sure you want to delete migration %label?', [ + '%label' => $this->entity->label(), + ]); + } + + /** + * Gather the confirmation text. + * + * @return string + * Translated string. + */ + public function getConfirmText() { + return $this->t('Delete Migration'); + } + + /** + * Gets the cancel URL. + * + * @return \Drupal\Core\Url + * The URL to go to if the user cancels the deletion. + */ + public function getCancelUrl() { + return new Url('entity.migration_group.list'); + } + + /** + * The submit handler for the confirm form. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * An associative array containing the current state of the form. + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Delete the entity. + $this->entity->delete(); + + // Set a message that the entity was deleted. + drupal_set_message(t('Migration %label was deleted.', [ + '%label' => $this->entity->label(), + ])); + + // Redirect the user to the list controller when complete. + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/web/modules/migrate_tools/src/Form/MigrationEditForm.php b/web/modules/migrate_tools/src/Form/MigrationEditForm.php new file mode 100644 index 0000000000..214d0ae35d --- /dev/null +++ b/web/modules/migrate_tools/src/Form/MigrationEditForm.php @@ -0,0 +1,50 @@ +<?php + +namespace Drupal\migrate_tools\Form; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; + +/** + * Provides the edit form for our Migration entity. + * + * @package Drupal\migrate_tools\Form + * + * @ingroup migrate_tools + */ +class MigrationEditForm extends MigrationFormBase { + + /** + * Returns the actions provided by this form. + * + * For the edit form, we only need to change the text of the submit button. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * An associative array containing the current state of the form. + * + * @return array + * An array of supported actions for the current entity form. + */ + public function actions(array $form, FormStateInterface $form_state) { + $actions = parent::actions($form, $form_state); + $actions['submit']['#value'] = t('Update Migration'); + + return $actions; + } + + /** + * Add group route parameter. + * + * @param \Drupal\Core\Url $url + * The URL associated with an operation. + * @param string $migration_group + * The migration's parent group. + */ + protected function addGroupParameter(Url $url, $migration_group) { + $route_parameters = $url->getRouteParameters() + ['migration_group' => $migration_group]; + $url->setRouteParameters($route_parameters); + } + +} diff --git a/web/modules/migrate_tools/src/Form/MigrationExecuteForm.php b/web/modules/migrate_tools/src/Form/MigrationExecuteForm.php new file mode 100644 index 0000000000..d05b736ebb --- /dev/null +++ b/web/modules/migrate_tools/src/Form/MigrationExecuteForm.php @@ -0,0 +1,212 @@ +<?php + +namespace Drupal\migrate_tools\Form; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Form\FormBase; +use Drupal\migrate\MigrateMessage; +use Drupal\migrate\Plugin\MigrationInterface; +use Drupal\migrate\Plugin\MigrationPluginManagerInterface; +use Drupal\migrate_tools\MigrateBatchExecutable; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * This form is specifically for configuring process pipelines. + */ +class MigrationExecuteForm extends FormBase { + + /** + * Plugin manager for migration plugins. + * + * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface + */ + protected $migrationPluginManager; + + /** + * Constructs a new MigrationExecuteForm object. + * + * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager + * The plugin manager for config entity-based migrations. + */ + public function __construct(MigrationPluginManagerInterface $migration_plugin_manager) { + $this->migrationPluginManager = $migration_plugin_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.migration') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'migration_execute_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + + $form = []; + + $form['operations'] = $this->migrateMigrateOperations(); + + return $form; + } + + /** + * Get Operations. + */ + private function migrateMigrateOperations() { + // Build the 'Update options' form. + $form = [ + '#type' => 'fieldset', + '#title' => t('Operations'), + ]; + $options = [ + 'import' => t('Import'), + 'rollback' => t('Rollback'), + 'stop' => t('Stop'), + 'reset' => t('Reset'), + ]; + $form['operation'] = [ + '#type' => 'select', + '#title' => t('Choose an operation to run'), + '#options' => $options, + '#default_value' => 'import', + '#required' => TRUE, + ]; + $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, + ]; + + $form['options'] = [ + '#type' => 'fieldset', + '#title' => t('Options'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ]; + $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'), + ]; + $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.'), + ]; + // @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; + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + + $operation = $form_state->getValue('operation'); + + if ($form_state->getValue('limit')) { + $limit = $form_state->getValue('limit'); + } + else { + $limit = 0; + } + + if ($form_state->getValue('update')) { + $update = $form_state->getValue('update'); + } + else { + $update = 0; + } + if ($form_state->getValue('force')) { + $force = $form_state->getValue('force'); + } + else { + $force = 0; + } + + $migration = \Drupal::routeMatch()->getParameter('migration'); + if ($migration) { + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration_plugin */ + $migration_plugin = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); + $migrateMessage = new MigrateMessage(); + + switch ($operation) { + case 'import': + + $options = [ + 'limit' => $limit, + 'update' => $update, + 'force' => $force, + ]; + + $executable = new MigrateBatchExecutable($migration_plugin, $migrateMessage, $options); + $executable->batchImport(); + + break; + + case 'rollback': + + $options = [ + 'limit' => $limit, + 'update' => $update, + 'force' => $force, + ]; + + $executable = new MigrateBatchExecutable($migration_plugin, $migrateMessage, $options); + $executable->rollback(); + + break; + + case 'stop': + + $migration_plugin->interruptMigration(MigrationInterface::RESULT_STOPPED); + + break; + + case 'reset': + + $migration_plugin->setStatus(MigrationInterface::STATUS_IDLE); + + break; + + } + } + } + +} diff --git a/web/modules/migrate_tools/src/Form/MigrationFormBase.php b/web/modules/migrate_tools/src/Form/MigrationFormBase.php new file mode 100644 index 0000000000..3ba6a0f55f --- /dev/null +++ b/web/modules/migrate_tools/src/Form/MigrationFormBase.php @@ -0,0 +1,184 @@ +<?php + +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. + * + * @package Drupal\migrate_tools\Form + * + * @ingroup migrate_tools + */ +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(). + * + * Builds the entity add/edit form. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * An associative array containing the current state of the form. + * + * @return array + * An associative array containing the migration add/edit form. + */ + public function buildForm(array $form, FormStateInterface $form_state) { + // Get anything we need from the base class. + $form = parent::buildForm($form, $form_state); + + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = $this->entity; + + $form['warning'] = [ + '#markup' => $this->t('Creating migrations is not yet supported. See <a href=":url">:url</a>', [ + ':url' => 'https://www.drupal.org/node/2573241', + ]), + ]; + + // Build the form. + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => $migration->label(), + '#required' => TRUE, + ]; + $form['id'] = [ + '#type' => 'machine_name', + '#title' => $this->t('Machine name'), + '#default_value' => $migration->id(), + '#machine_name' => [ + 'exists' => [$this, 'exists'], + 'replace_pattern' => '([^a-z0-9_]+)|(^custom$)', + 'error' => 'The machine-readable name must be unique, and can only contain lowercase letters, numbers, and underscores. Additionally, it can not be the reserved word "custom".', + ], + '#disabled' => !$migration->isNew(), + ]; + + $groups = MigrationGroup::loadMultiple(); + $group_options = []; + foreach ($groups as $group) { + $group_options[$group->id()] = $group->label(); + } + if (!$migration->get('migration_group') && isset($group_options['default'])) { + $migration->set('migration_group', 'default'); + } + + $form['migration_group'] = [ + '#type' => 'select', + '#title' => $this->t('Migration Group'), + '#empty_value' => '', + '#default_value' => $migration->get('migration_group'), + '#options' => $group_options, + '#description' => $this->t('Assign this migration to an existing group.'), + ]; + + return $form; + } + + /** + * Checks for an existing migration group. + * + * @param string|int $entity_id + * The entity ID. + * @param array $element + * The form element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return bool + * 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 the entity ID to see if its in use. + $result = $query->condition('id', $element['#field_prefix'] . $entity_id) + ->execute(); + + // We don't need to return the ID, only if it exists or not. + return (bool) $result; + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::actions(). + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * An associative array containing the current state of the form. + * + * @return array + * 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. + $actions = parent::actions($form, $form_state); + + // Change the submit button text. + $actions['submit']['#value'] = $this->t('Save'); + + // Return the result. + return $actions; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $migration = $this->getEntity(); + $status = $migration->save(); + + if ($status == SAVED_UPDATED) { + // If we edited an existing entity... + drupal_set_message($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()])); + } + + // Redirect the user back to the listing route after the save operation. + $form_state->setRedirect('entity.migration.list', + ['migration_group' => $migration->get('migration_group')]); + } + +} diff --git a/web/modules/migrate_tools/src/Form/MigrationGroupAddForm.php b/web/modules/migrate_tools/src/Form/MigrationGroupAddForm.php new file mode 100644 index 0000000000..3efbf8c884 --- /dev/null +++ b/web/modules/migrate_tools/src/Form/MigrationGroupAddForm.php @@ -0,0 +1,37 @@ +<?php + +namespace Drupal\migrate_tools\Form; + +use Drupal\Core\Form\FormStateInterface; + +/** + * Class MigrationGroupAddForm. + * + * Provides the add form for our migration_group entity. + * + * @package Drupal\migrate_tools\Form + * + * @ingroup migrate_tools + */ +class MigrationGroupAddForm extends MigrationGroupFormBase { + + /** + * Returns the actions provided by this form. + * + * For our add form, we only need to change the text of the submit button. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * An associative array containing the current state of the form. + * + * @return array + * An array of supported actions for the current entity form. + */ + protected function actions(array $form, FormStateInterface $form_state) { + $actions = parent::actions($form, $form_state); + $actions['submit']['#value'] = $this->t('Create Migration Group'); + return $actions; + } + +} diff --git a/web/modules/migrate_tools/src/Form/MigrationGroupDeleteForm.php b/web/modules/migrate_tools/src/Form/MigrationGroupDeleteForm.php new file mode 100644 index 0000000000..98d1baf700 --- /dev/null +++ b/web/modules/migrate_tools/src/Form/MigrationGroupDeleteForm.php @@ -0,0 +1,71 @@ +<?php + +namespace Drupal\migrate_tools\Form; + +use Drupal\Core\Entity\EntityConfirmFormBase; +use Drupal\Core\Url; +use Drupal\Core\Form\FormStateInterface; + +/** + * Provides the delete form for our Migration Group entity. + * + * @package Drupal\migrate_tools\Form + * + * @ingroup migrate_tools + */ +class MigrationGroupDeleteForm extends EntityConfirmFormBase { + + /** + * Gathers a confirmation question. + * + * @return string + * Translated string. + */ + public function getQuestion() { + return $this->t('Are you sure you want to delete migration group %label?', [ + '%label' => $this->entity->label(), + ]); + } + + /** + * Gather the confirmation text. + * + * @return string + * Translated string. + */ + public function getConfirmText() { + return $this->t('Delete Migration Group'); + } + + /** + * Gets the cancel URL. + * + * @return \Drupal\Core\Url + * The URL to go to if the user cancels the deletion. + */ + public function getCancelUrl() { + return new Url('entity.migration_group.list'); + } + + /** + * The submit handler for the confirm form. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * An associative array containing the current state of the form. + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Delete the entity. + $this->entity->delete(); + + // Set a message that the entity was deleted. + drupal_set_message(t('Migration group %label was deleted.', [ + '%label' => $this->entity->label(), + ])); + + // Redirect the user to the list controller when complete. + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/web/modules/migrate_tools/src/Form/MigrationGroupEditForm.php b/web/modules/migrate_tools/src/Form/MigrationGroupEditForm.php new file mode 100644 index 0000000000..1eedc2d261 --- /dev/null +++ b/web/modules/migrate_tools/src/Form/MigrationGroupEditForm.php @@ -0,0 +1,35 @@ +<?php + +namespace Drupal\migrate_tools\Form; + +use Drupal\Core\Form\FormStateInterface; + +/** + * Provides the edit form for our Migration Group entity. + * + * @package Drupal\migrate_tools\Form + * + * @ingroup migrate_tools + */ +class MigrationGroupEditForm extends MigrationGroupFormBase { + + /** + * Returns the actions provided by this form. + * + * For the edit form, we only need to change the text of the submit button. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * An associative array containing the current state of the form. + * + * @return array + * An array of supported actions for the current entity form. + */ + public function actions(array $form, FormStateInterface $form_state) { + $actions = parent::actions($form, $form_state); + $actions['submit']['#value'] = 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 new file mode 100644 index 0000000000..d60944a759 --- /dev/null +++ b/web/modules/migrate_tools/src/Form/MigrationGroupFormBase.php @@ -0,0 +1,172 @@ +<?php + +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. + * + * @package Drupal\migrate_tools\Form + * + * @ingroup migrate_tools + */ +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(). + * + * Builds the entity add/edit form. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * An associative array containing the current state of the form. + * + * @return array + * An associative array containing the migration group add/edit form. + */ + public function buildForm(array $form, FormStateInterface $form_state) { + // Get anything we need from the base class. + $form = parent::buildForm($form, $form_state); + + /** @var \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group */ + $migration_group = $this->entity; + + // Build the form. + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => $migration_group->label(), + '#required' => TRUE, + ]; + $form['id'] = [ + '#type' => 'machine_name', + '#title' => $this->t('Machine name'), + '#default_value' => $migration_group->id(), + '#machine_name' => [ + 'exists' => [$this, 'exists'], + 'replace_pattern' => '([^a-z0-9_]+)|(^custom$)', + 'error' => 'The machine-readable name must be unique, and can only contain lowercase letters, numbers, and underscores. Additionally, it can not be the reserved word "custom".', + ], + '#disabled' => !$migration_group->isNew(), + ]; + $form['description'] = [ + '#type' => 'textfield', + '#title' => $this->t('Description'), + '#maxlength' => 255, + '#default_value' => $migration_group->get('description'), + ]; + $form['source_type'] = [ + '#type' => 'textfield', + '#title' => $this->t('Source type'), + '#description' => $this->t('Type of source system the group is migrating from, for example "Drupal 6" or "WordPress 4".'), + '#maxlength' => 255, + '#default_value' => $migration_group->get('source_type'), + ]; + + // Return the form. + return $form; + } + + /** + * Checks for an existing migration group. + * + * @param string|int $entity_id + * The entity ID. + * @param array $element + * The form element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return bool + * 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 the entity ID to see if its in use. + $result = $query->condition('id', $element['#field_prefix'] . $entity_id) + ->execute(); + + // We don't need to return the ID, only if it exists or not. + return (bool) $result; + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::actions(). + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * An associative array containing the current state of the form. + * + * @return array + * 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. + $actions = parent::actions($form, $form_state); + + // Change the submit button text. + $actions['submit']['#value'] = $this->t('Save'); + + // Return the result. + return $actions; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $migration_group = $this->getEntity(); + $status = $migration_group->save(); + + 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()])); + } + else { + // If we created a new entity... + drupal_set_message($this->t('Migration group %label has been added.', ['%label' => $migration_group->label()])); + } + + // Redirect the user back to the listing route after the save operation. + $form_state->setRedirect('entity.migration_group.list'); + } + +} diff --git a/web/modules/migrate_tools/src/Form/SourceCsvForm.php b/web/modules/migrate_tools/src/Form/SourceCsvForm.php new file mode 100644 index 0000000000..40b5dc3bac --- /dev/null +++ b/web/modules/migrate_tools/src/Form/SourceCsvForm.php @@ -0,0 +1,453 @@ +<?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/MigrateBatchExecutable.php b/web/modules/migrate_tools/src/MigrateBatchExecutable.php new file mode 100644 index 0000000000..ee24f64d77 --- /dev/null +++ b/web/modules/migrate_tools/src/MigrateBatchExecutable.php @@ -0,0 +1,295 @@ +<?php + +namespace Drupal\migrate_tools; + +use Drupal\migrate\MigrateMessage; +use Drupal\migrate\MigrateMessageInterface; +use Drupal\migrate\Plugin\Migration; +use Drupal\migrate\Plugin\MigrationInterface; + +/** + * Defines a migrate executable class for batch migrations through UI. + */ +class MigrateBatchExecutable extends MigrateExecutable { + + /** + * Representing a batch import operation. + */ + const BATCH_IMPORT = 1; + + /** + * Indicates if we need to update existing rows or skip them. + * + * @var int + */ + protected $updateExistingRows = 0; + + /** + * Indicates if we need import dependent migrations also. + * + * @var int + */ + protected $checkDependencies = 0; + + /** + * The current batch context. + * + * @var array + */ + protected $batchContext = []; + + /** + * Plugin manager for migration plugins. + * + * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface + */ + protected $migrationPluginManager; + + /** + * {@inheritdoc} + */ + public function __construct(MigrationInterface $migration, MigrateMessageInterface $message, array $options = []) { + + if (isset($options['update'])) { + $this->updateExistingRows = $options['update']; + } + + if (isset($options['force'])) { + $this->checkDependencies = $options['force']; + } + + parent::__construct($migration, $message, $options); + $this->migrationPluginManager = \Drupal::getContainer()->get('plugin.manager.migration'); + } + + /** + * Sets the current batch content so listeners can update the messages. + * + * @param array $context + * The batch context. + */ + public function setBatchContext(array &$context) { + $this->batchContext = &$context; + } + + /** + * Gets a reference to the current batch context. + * + * @return array + * The batch context. + */ + public function &getBatchContext() { + return $this->batchContext; + } + + /** + * Setup batch operations for running the migration. + */ + public function batchImport() { + // Create the batch operations for each migration that needs to be executed. + // This includes the migration for this executable, but also the dependent + // migrations. + $operations = $this->batchOperations([$this->migration], 'import', [ + 'limit' => $this->itemLimit, + 'update' => $this->updateExistingRows, + 'force' => $this->checkDependencies, + ]); + + 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()]), + 'finished' => '\Drupal\migrate_tools\MigrateBatchExecutable::batchFinishedImport', + ]; + + batch_set($batch); + } + } + + /** + * Helper to generate the batch operations for importing migrations. + * + * @param \Drupal\migrate\Plugin\MigrationInterface[] $migrations + * The migrations. + * @param string $operation + * The batch operation to perform. + * @param array $options + * The migration options. + * + * @return array + * The batch operations to perform. + */ + protected function batchOperations(array $migrations, $operation, array $options = []) { + $operations = []; + foreach ($migrations as $id => $migration) { + + if (!empty($options['update'])) { + $migration->getIdMap()->prepareUpdate(); + } + + if (!empty($options['force'])) { + $migration->set('requirements', []); + } + else { + $dependencies = $migration->getMigrationDependencies(); + 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, [ + 'limit' => 0, + 'update' => $options['update'], + 'force' => $options['force'], + ]); + } + } + + $operations[] = [ + '\Drupal\migrate_tools\MigrateBatchExecutable::batchProcessImport', + [$migration->id(), $options], + ]; + } + + return $operations; + } + + /** + * Batch 'operation' callback. + * + * @param string $migration_id + * The migration id. + * @param array $options + * The batch executable options. + * @param array $context + * The sandbox context. + */ + public static function batchProcessImport($migration_id, array $options, array &$context) { + if (empty($context['sandbox'])) { + $context['finished'] = 0; + $context['sandbox'] = []; + $context['sandbox']['total'] = 0; + $context['sandbox']['counter'] = 0; + $context['sandbox']['batch_limit'] = 0; + $context['sandbox']['operation'] = MigrateBatchExecutable::BATCH_IMPORT; + } + + // Prepare the migration executable. + $message = new MigrateMessage(); + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = \Drupal::getContainer()->get('plugin.manager.migration')->createInstance($migration_id); + $executable = new MigrateBatchExecutable($migration, $message, $options); + + if (empty($context['sandbox']['total'])) { + $context['sandbox']['total'] = $executable->getSource()->count(); + $context['sandbox']['batch_limit'] = $executable->calculateBatchLimit($context); + $context['results'][$migration->id()] = [ + '@numitems' => 0, + '@created' => 0, + '@updated' => 0, + '@failures' => 0, + '@ignored' => 0, + '@name' => $migration->id(), + ]; + } + + // Every iteration, we reset out batch counter. + $context['sandbox']['batch_counter'] = 0; + + // Make sure we know our batch context. + $executable->setBatchContext($context); + + // Do the import. + $result = $executable->import(); + + // Store the result; will need to combine the results of all our iterations. + $context['results'][$migration->id()] = [ + '@numitems' => $context['results'][$migration->id()]['@numitems'] + $executable->getProcessedCount(), + '@created' => $context['results'][$migration->id()]['@created'] + $executable->getCreatedCount(), + '@updated' => $context['results'][$migration->id()]['@updated'] + $executable->getUpdatedCount(), + '@failures' => $context['results'][$migration->id()]['@failures'] + $executable->getFailedCount(), + '@ignored' => $context['results'][$migration->id()]['@ignored'] + $executable->getIgnoredCount(), + '@name' => $migration->id(), + ]; + + // Do some housekeeping. + if ( + $result != MigrationInterface::RESULT_INCOMPLETE + ) { + $context['finished'] = 1; + } + else { + $context['sandbox']['counter'] = $context['results'][$migration->id()]['@numitems']; + if ($context['sandbox']['counter'] <= $context['sandbox']['total']) { + $context['finished'] = ((float) $context['sandbox']['counter'] / (float) $context['sandbox']['total']); + $context['message'] = t('Importing %migration (@percent%).', [ + '%migration' => $migration->label(), + '@percent' => (int) ($context['finished'] * 100), + ]); + } + } + + } + + /** + * Finished callback for import batches. + * + * @param bool $success + * A boolean indicating whether the batch has completed successfully. + * @param array $results + * The value set in $context['results'] by callback_batch_operation(). + * @param array $operations + * If $success is FALSE, contains the operations that remained unprocessed. + */ + public static function batchFinishedImport($success, array $results, array $operations) { + if ($success) { + 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'], + $singular_message, + $plural_message, + $result)); + } + } + } + + /** + * {@inheritdoc} + */ + public function checkStatus() { + $status = parent::checkStatus(); + + if ($status == MigrationInterface::RESULT_COMPLETED) { + // Do some batch housekeeping. + $context = $this->getBatchContext(); + + if (!empty($context['sandbox']) && $context['sandbox']['operation'] == MigrateBatchExecutable::BATCH_IMPORT) { + $context['sandbox']['batch_counter']++; + if ($context['sandbox']['batch_counter'] >= $context['sandbox']['batch_limit']) { + $status = MigrationInterface::RESULT_INCOMPLETE; + } + } + } + + return $status; + } + + /** + * Calculates how much a single batch iteration will handle. + * + * @param array $context + * The sandbox context. + * + * @return float + * The batch limit. + */ + public function calculateBatchLimit(array $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 new file mode 100644 index 0000000000..2282ab8cbc --- /dev/null +++ b/web/modules/migrate_tools/src/MigrateExecutable.php @@ -0,0 +1,395 @@ +<?php + +namespace Drupal\migrate_tools; + +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_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; + +/** + * Defines a migrate executable class for drush. + */ +class MigrateExecutable extends MigrateExecutableBase { + + /** + * Counters of map statuses. + * + * @var array + * Set of counters, keyed by MigrateIdMapInterface::STATUS_* constant. + */ + protected $saveCounters = [ + MigrateIdMapInterface::STATUS_FAILED => 0, + MigrateIdMapInterface::STATUS_IGNORED => 0, + MigrateIdMapInterface::STATUS_IMPORTED => 0, + MigrateIdMapInterface::STATUS_NEEDS_UPDATE => 0, + ]; + + /** + * Counter of map saves, used to detect the item limit threshold. + * + * @var int + */ + protected $itemLimitCounter = 0; + + /** + * Counter of map deletions. + * + * @var int + */ + protected $deleteCounter = 0; + + /** + * Maximum number of items to process in this migration. + * + * 0 indicates no limit is to be applied. + * + * @var int + */ + protected $itemLimit = 0; + + /** + * Frequency (in items) at which progress messages should be emitted. + * + * @var int + */ + protected $feedback = 0; + + /** + * List of specific source IDs to import. + * + * @var array + */ + protected $idlist = []; + + /** + * Count of number of items processed so far in this migration. + * + * @var int + */ + protected $counter = 0; + + /** + * Whether the destination item exists before saving. + * + * @var bool + */ + protected $preExistingItem = FALSE; + + /** + * List of event listeners we have registered. + * + * @var array + */ + protected $listeners = []; + + /** + * {@inheritdoc} + */ + public function __construct(MigrationInterface $migration, MigrateMessageInterface $message, array $options = []) { + parent::__construct($migration, $message); + if (isset($options['limit'])) { + $this->itemLimit = $options['limit']; + } + 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->listeners[MigrateEvents::MAP_SAVE] = [$this, 'onMapSave']; + $this->listeners[MigrateEvents::MAP_DELETE] = [$this, 'onMapDelete']; + $this->listeners[MigrateEvents::POST_IMPORT] = [$this, 'onPostImport']; + $this->listeners[MigrateEvents::POST_ROLLBACK] = [$this, 'onPostRollback']; + $this->listeners[MigrateEvents::PRE_ROW_SAVE] = [$this, 'onPreRowSave']; + $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); + } + } + + /** + * Count up any map save events. + * + * @param \Drupal\migrate\Event\MigrateMapSaveEvent $event + * The map event. + */ + public function onMapSave(MigrateMapSaveEvent $event) { + // Only count saves for this migration. + if ($event->getMap()->getQualifiedMapTableName() == $this->migration->getIdMap()->getQualifiedMapTableName()) { + $fields = $event->getFields(); + $this->itemLimitCounter++; + // Distinguish between creation and update. + if ($fields['source_row_status'] == MigrateIdMapInterface::STATUS_IMPORTED && + $this->preExistingItem + ) { + $this->saveCounters[MigrateIdMapInterface::STATUS_NEEDS_UPDATE]++; + } + else { + $this->saveCounters[$fields['source_row_status']]++; + } + } + } + + /** + * Count up any rollback events. + * + * @param \Drupal\migrate\Event\MigrateMapDeleteEvent $event + * The map event. + */ + public function onMapDelete(MigrateMapDeleteEvent $event) { + $this->deleteCounter++; + } + + /** + * Return the number of items created. + * + * @return int + * The number of items created. + */ + public function getCreatedCount() { + return $this->saveCounters[MigrateIdMapInterface::STATUS_IMPORTED]; + } + + /** + * Return the number of items updated. + * + * @return int + * The updated count. + */ + public function getUpdatedCount() { + return $this->saveCounters[MigrateIdMapInterface::STATUS_NEEDS_UPDATE]; + } + + /** + * Return the number of items ignored. + * + * @return int + * The ignored count. + */ + public function getIgnoredCount() { + return $this->saveCounters[MigrateIdMapInterface::STATUS_IGNORED]; + } + + /** + * Return the number of items that failed. + * + * @return int + * The failed count. + */ + public function getFailedCount() { + return $this->saveCounters[MigrateIdMapInterface::STATUS_FAILED]; + } + + /** + * Return the total number of items processed. + * + * Note that STATUS_NEEDS_UPDATE is not counted, since this is typically set + * on stubs created as side effects, not on the primary item being imported. + * + * @return int + * The processed count. + */ + public function getProcessedCount() { + return $this->saveCounters[MigrateIdMapInterface::STATUS_IMPORTED] + + $this->saveCounters[MigrateIdMapInterface::STATUS_NEEDS_UPDATE] + + $this->saveCounters[MigrateIdMapInterface::STATUS_IGNORED] + + $this->saveCounters[MigrateIdMapInterface::STATUS_FAILED]; + } + + /** + * Return the number of items rolled back. + * + * @return int + * The rollback count. + */ + public function getRollbackCount() { + return $this->deleteCounter; + } + + /** + * Reset all the per-status counters to 0. + */ + protected function resetCounters() { + foreach ($this->saveCounters as $status => $count) { + $this->saveCounters[$status] = 0; + } + $this->deleteCounter = 0; + } + + /** + * React to migration completion. + * + * @param \Drupal\migrate\Event\MigrateImportEvent $event + * The map event. + */ + 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)); + $this->progressMessage(); + $this->removeListeners(); + } + + /** + * Clean up all our event listeners. + */ + protected function removeListeners() { + foreach ($this->listeners as $event => $listener) { + \Drupal::service('event_dispatcher')->removeListener($event, $listener); + } + } + + /** + * Emit information on what we've done. + * + * Either since the last feedback or the beginning of this migration. + * + * @param bool $done + * TRUE if this is the last items to process. Otherwise FALSE. + */ + protected function progressMessage($done = TRUE) { + $processed = $this->getProcessedCount(); + if ($done) { + $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'"; + } + else { + $singular_message = "Processed 1 item (@created created, @updated updated, @failures failed, @ignored ignored) - continuing with '@name'"; + $plural_message = "Processed @numitems items (@created created, @updated updated, @failures failed, @ignored ignored) - continuing with '@name'"; + } + $this->message->display(\Drupal::translation()->formatPlural($processed, + $singular_message, $plural_message, + [ + '@numitems' => $processed, + '@created' => $this->getCreatedCount(), + '@updated' => $this->getUpdatedCount(), + '@failures' => $this->getFailedCount(), + '@ignored' => $this->getIgnoredCount(), + '@name' => $this->migration->id(), + ] + )); + } + + /** + * React to rollback completion. + * + * @param \Drupal\migrate\Event\MigrateRollbackEvent $event + * The map event. + */ + public function onPostRollback(MigrateRollbackEvent $event) { + $this->rollbackMessage(); + $this->removeListeners(); + } + + /** + * Emit information on what we've done. + * + * Either since the last feedback or the beginning of this migration. + * + * @param bool $done + * TRUE if this is the last items to rollback. Otherwise FALSE. + */ + protected function rollbackMessage($done = TRUE) { + $rolled_back = $this->getRollbackCount(); + if ($done) { + $singular_message = "Rolled back 1 item - done with '@name'"; + $plural_message = "Rolled back @numitems items - done with '@name'"; + } + else { + $singular_message = "Rolled back 1 item - continuing with '@name'"; + $plural_message = "Rolled back @numitems items - continuing with '@name'"; + } + $this->message->display(\Drupal::translation()->formatPlural($rolled_back, + $singular_message, $plural_message, + [ + '@numitems' => $rolled_back, + '@name' => $this->migration->id(), + ] + )); + } + + /** + * React to an item about to be imported. + * + * @param \Drupal\migrate\Event\MigratePreRowSaveEvent $event + * The pre-save event. + */ + public function onPreRowSave(MigratePreRowSaveEvent $event) { + $id_map = $event->getRow()->getIdMap(); + if (!empty($id_map['destid1'])) { + $this->preExistingItem = TRUE; + } + else { + $this->preExistingItem = FALSE; + } + } + + /** + * React to item rollback. + * + * @param \Drupal\migrate\Event\MigrateRowDeleteEvent $event + * The post-save event. + */ + public function onPostRowDelete(MigrateRowDeleteEvent $event) { + if ($this->feedback && ($this->deleteCounter) && $this->deleteCounter % $this->feedback == 0) { + $this->rollbackMessage(FALSE); + $this->resetCounters(); + } + } + + /** + * React to a new row. + * + * @param \Drupal\migrate_plus\Event\MigratePrepareRowEvent $event + * The prepare-row 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) { + $this->progressMessage(FALSE); + $this->resetCounters(); + } + $this->counter++; + if ($this->itemLimit && ($this->itemLimitCounter + 1) >= $this->itemLimit) { + $event->getMigration()->interruptMigration(MigrationInterface::RESULT_COMPLETED); + } + + } + +} diff --git a/web/modules/migrate_tools/src/Routing/RouteProcessor.php b/web/modules/migrate_tools/src/Routing/RouteProcessor.php new file mode 100644 index 0000000000..fe0c5e623f --- /dev/null +++ b/web/modules/migrate_tools/src/Routing/RouteProcessor.php @@ -0,0 +1,27 @@ +<?php + +namespace Drupal\migrate_tools\Routing; + +use Drupal\Core\Render\BubbleableMetadata; +use Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface; +use Symfony\Component\Routing\Route; + +/** + * Route processor to expand migrate_group. + */ +class RouteProcessor implements OutboundRouteProcessorInterface { + + /** + * {@inheritdoc} + */ + public function processOutbound($route_name, Route $route, array &$parameters, BubbleableMetadata $bubbleable_metadata = NULL) { + if ($route->hasDefault('_migrate_group')) { + if ($migration = \Drupal::entityTypeManager()->getStorage('migration')->load($parameters['migration'])) { + if ($group = $migration->get('migration_group')) { + $parameters['migration_group'] = $group; + } + } + } + } + +} 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 new file mode 100644 index 0000000000..0d600a9ffc --- /dev/null +++ b/web/modules/migrate_tools/tests/modules/csv_source_test/config/install/migrate_plus.migration.csv_source_test.yml @@ -0,0 +1,33 @@ +langcode: en +status: true +dependencies: { } +id: csv_source_test +label: Test edit of column aliases for CSV source plugin +class: null +field_plugin_method: null +cck_plugin_method: null +migration_tags: { } +migration_group: csv_test +source: + plugin: csv + path: 'public://test.csv' + header_row_count: 1 + enclosure: '"' + keys: + - vid + column_names: + 0: + vid: 'Vocabulary Id' + 1: + name: 'Name' + 2: + description: 'Description' +process: + vid: vid + name: name + description: description +destination: + plugin: entity:taxonomy_vocabulary +migration_dependencies: + required: { } + optional: { } 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 new file mode 100644 index 0000000000..1c2b05931e --- /dev/null +++ b/web/modules/migrate_tools/tests/modules/csv_source_test/config/install/migrate_plus.migration_group.csv_test.yml @@ -0,0 +1,4 @@ +id: csv_test +label: CSV source plugin edit test +description: Test editting of source plugin via the UI +source_type: CSV 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 new file mode 100644 index 0000000000..f16e6bbd79 --- /dev/null +++ b/web/modules/migrate_tools/tests/modules/csv_source_test/csv_source_test.info.yml @@ -0,0 +1,15 @@ +type: module +name: CSV Source edit test +description: 'Test editing of source plugin via the UI.' +package: Testing +# core: 8.x +dependencies: + - drupal:migrate (>=8.3) + - 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' +project: 'migrate_tools' +datestamp: 1535380087 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 new file mode 100644 index 0000000000..7f606d127f --- /dev/null +++ b/web/modules/migrate_tools/tests/modules/migrate_tools_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_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 new file mode 100644 index 0000000000..0f35a21895 --- /dev/null +++ b/web/modules/migrate_tools/tests/modules/migrate_tools_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_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 new file mode 100644 index 0000000000..bbeb698678 --- /dev/null +++ b/web/modules/migrate_tools/tests/modules/migrate_tools_test/migrate_tools_test.info.yml @@ -0,0 +1,14 @@ +type: module +name: Migrate Tools Test +description: 'Test module to test Migrate Tools.' +package: Testing +# core: 8.x +dependencies: + - drupal:migrate (>=8.3) + - migrate_plus:migrate_plus + +# Information added by Drupal.org packaging script on 2018-08-27 +version: '8.x-4.0' +core: '8.x' +project: 'migrate_tools' +datestamp: 1535380087 diff --git a/web/modules/migrate_tools/tests/src/Functional/MigrateExecutionFormTest.php b/web/modules/migrate_tools/tests/src/Functional/MigrateExecutionFormTest.php new file mode 100644 index 0000000000..2f29bf5ba2 --- /dev/null +++ b/web/modules/migrate_tools/tests/src/Functional/MigrateExecutionFormTest.php @@ -0,0 +1,132 @@ +<?php + +namespace Drupal\Tests\migrate_tools\Functional; + +use Drupal\taxonomy\Entity\Vocabulary; +use Drupal\Tests\BrowserTestBase; + +/** + * Execution form test. + * + * @group migrate_tools + */ +class MigrateExecutionFormTest extends BrowserTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'user', + 'filter', + 'field', + 'node', + 'text', + 'taxonomy', + 'migrate', + 'migrate_plus', + 'migrate_tools', + 'migrate_tools_test', + ]; + + /** + * {@inheritdoc} + */ + protected $profile = 'testing'; + + /** + * The vocabulary. + * + * @var \Drupal\taxonomy\VocabularyInterface + */ + protected $vocabulary; + + /** + * The vocabulary query. + * + * @var \Drupal\Core\Entity\Query\QueryInterface + */ + protected $vocabularyQuery; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + $this->vocabulary = $this->createVocabulary(['vid' => 'fruit', 'name' => 'Fruit']); + $this->vocabularyQuery = $this->container->get('entity_type.manager') + ->getStorage('taxonomy_term') + ->getQuery(); + // Log in as user 1. Migrations in the UI can only be performed as user 1. + $this->drupalLogin($this->rootUser); + } + + /** + * Tests execution of import and rollback of a migration. + * + * @throws \Behat\Mink\Exception\ExpectationException + */ + public function testExecution() { + $group = 'default'; + $migration = 'fruit_terms'; + $urlPath = "/admin/structure/migrate/manage/{$group}/migrations/{$migration}/execute"; + $real_count = $this->vocabularyQuery->count()->execute(); + $expected_count = 0; + $this->assertEquals($expected_count, $real_count); + $this->drupalGet($urlPath); + $this->assertSession()->responseContains('Choose an operation to run'); + $edit = [ + 'operation' => 'import', + ]; + $this->drupalPostForm($urlPath, $edit, 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')); + $real_count = $this->vocabularyQuery->count()->execute(); + $expected_count = 0; + $this->assertEquals($expected_count, $real_count); + $edit = [ + 'operation' => 'import', + ]; + $this->drupalPostForm($urlPath, $edit, t('Execute')); + $real_count = $this->vocabularyQuery->count()->execute(); + $expected_count = 3; + $this->assertEquals($expected_count, $real_count); + } + + /** + * Creates a custom vocabulary based on default settings. + * + * @param array $values + * An array of settings to change from the defaults. + * Example: 'vid' => 'foo'. + * + * @return \Drupal\taxonomy\VocabularyInterface + * Created vocabulary. + */ + protected function createVocabulary(array $values = []) { + // Find a non-existent random vocabulary name. + if (!isset($values['vid'])) { + do { + $id = strtolower($this->randomMachineName(8)); + } while (Vocabulary::load($id)); + } + else { + $id = $values['vid']; + } + $values += [ + 'id' => $id, + 'name' => $id, + ]; + $vocabulary = Vocabulary::create($values); + $status = $vocabulary->save(); + + $this->assertSame($status, SAVED_NEW); + + return $vocabulary; + } + +} diff --git a/web/modules/migrate_tools/tests/src/Functional/SourceCsvFormTest.php b/web/modules/migrate_tools/tests/src/Functional/SourceCsvFormTest.php new file mode 100644 index 0000000000..767a9bfe08 --- /dev/null +++ b/web/modules/migrate_tools/tests/src/Functional/SourceCsvFormTest.php @@ -0,0 +1,241 @@ +<?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()); + } + +} -- GitLab