Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • project/migrate_plus
  • issue/migrate_plus-2640516
  • issue/migrate_plus-2678194
  • issue/migrate_plus-3196547
  • issue/migrate_plus-3082078
  • issue/migrate_plus-2947711
  • issue/migrate_plus-3218356
  • issue/migrate_plus-3121204
  • issue/migrate_plus-3223182
  • issue/migrate_plus-3227120
  • issue/migrate_plus-3227245
  • issue/migrate_plus-3227250
  • issue/migrate_plus-3229479
  • issue/migrate_plus-3230549
  • issue/migrate_plus-3232208
  • issue/migrate_plus-3232211
  • issue/migrate_plus-3280809
  • issue/migrate_plus-3279861
  • issue/migrate_plus-3007709
  • issue/migrate_plus-3241509
  • issue/migrate_plus-3243514
  • issue/migrate_plus-3050274
  • issue/migrate_plus-3113394
  • issue/migrate_plus-3251921
  • issue/migrate_plus-3253347
  • issue/migrate_plus-3232488
  • issue/migrate_plus-3028162
  • issue/migrate_plus-3232214
  • issue/migrate_plus-3255945
  • issue/migrate_plus-3256021
  • issue/migrate_plus-3258044
  • issue/migrate_plus-3258552
  • issue/migrate_plus-2980132
  • issue/migrate_plus-3259516
  • issue/migrate_plus-3259530
  • issue/migrate_plus-3259540
  • issue/migrate_plus-3259471
  • issue/migrate_plus-3256823
  • issue/migrate_plus-3015199
  • issue/migrate_plus-3261273
  • issue/migrate_plus-3261274
  • issue/migrate_plus-3261276
  • issue/migrate_plus-3261275
  • issue/migrate_plus-3254969
  • issue/migrate_plus-3261288
  • issue/migrate_plus-3261294
  • issue/migrate_plus-2974206
  • issue/migrate_plus-2938112
  • issue/migrate_plus-3263877
  • issue/migrate_plus-3263893
  • issue/migrate_plus-3263911
  • issue/migrate_plus-3264767
  • issue/migrate_plus-3265262
  • issue/migrate_plus-3225569
  • issue/migrate_plus-3265410
  • issue/migrate_plus-3265411
  • issue/migrate_plus-3267505
  • issue/migrate_plus-3269494
  • issue/migrate_plus-2787219
  • issue/migrate_plus-2822737
  • issue/migrate_plus-3273003
  • issue/migrate_plus-3225457
  • issue/migrate_plus-3096393
  • issue/migrate_plus-3276619
  • issue/migrate_plus-3277622
  • issue/migrate_plus-3050058
  • issue/migrate_plus-3068584
  • issue/migrate_plus-3284318
  • issue/migrate_plus-3294980
  • issue/migrate_plus-2820649
  • issue/migrate_plus-3083838
  • issue/migrate_plus-3330911
  • issue/migrate_plus-3334436
  • issue/migrate_plus-3276799
  • issue/migrate_plus-3320260
  • issue/migrate_plus-3355814
  • issue/migrate_plus-3352503
  • issue/migrate_plus-3400226
  • issue/migrate_plus-3403545
  • issue/migrate_plus-3375685
  • issue/migrate_plus-3380054
  • issue/migrate_plus-3396027
  • issue/migrate_plus-3396583
  • issue/migrate_plus-3396696
  • issue/migrate_plus-3413533
  • issue/migrate_plus-3087614
  • issue/migrate_plus-3427482
  • issue/migrate_plus-3379669
  • issue/migrate_plus-3427939
  • issue/migrate_plus-3443550
  • issue/migrate_plus-2921374
  • issue/migrate_plus-3040427
  • issue/migrate_plus-3232212
  • issue/migrate_plus-3440904
  • issue/migrate_plus-2933531
  • issue/migrate_plus-3458322
  • issue/migrate_plus-3459031
  • issue/migrate_plus-3462520
  • issue/migrate_plus-2830058
  • issue/migrate_plus-3123534
  • issue/migrate_plus-3465782
  • issue/migrate_plus-3466604
  • issue/migrate_plus-3466499
  • issue/migrate_plus-3469900
  • issue/migrate_plus-3476474
  • issue/migrate_plus-3019187
  • issue/migrate_plus-2837684
  • issue/migrate_plus-3478009
  • issue/migrate_plus-3357844
  • issue/migrate_plus-2891964
  • issue/migrate_plus-3481311
  • issue/migrate_plus-3486188
  • issue/migrate_plus-3064562
  • issue/migrate_plus-3488691
  • issue/migrate_plus-3488331
  • issue/migrate_plus-3498416
  • issue/migrate_plus-3498423
  • issue/migrate_plus-3502423
  • issue/migrate_plus-3497174
  • issue/migrate_plus-2944627
  • issue/migrate_plus-3508679
121 results
Show changes
Commits on Source (32)
Showing
with 522 additions and 159 deletions
################
# DrupalCI GitLabCI template
#
# Gitlab-ci.yml to replicate DrupalCI testing for Contrib
#
# With thanks to:
# * The GitLab Acceleration Initiative participants
# * DrupalSpoons
################
################
# Guidelines
#
# This template is designed to give any Contrib maintainer everything they need to test, without requiring modification. It is also designed to keep up to date with Core Development automatically through the use of include files that can be centrally maintained.
#
# However, you can modify this template if you have additional needs for your project.
################
################
# Includes
#
# Additional configuration can be provided through includes.
# One advantage of include files is that if they are updated upstream, the changes affect all pipelines using that include.
#
# Includes can be overridden by re-declaring anything provided in an include, here in gitlab-ci.yml
# https://docs.gitlab.com/ee/ci/yaml/includes.html#override-included-configuration-values
################
include:
- project: 'drupalspoons/composer-plugin'
# Best practice is to pin to a tag or a SHA1. https://docs.gitlab.com/ee/ci/yaml/#includefile
ref: "2.1.0"
# The template below may be inspected at https://gitlab.com/drupalspoons/composer-plugin/-/blob/master/templates/.gitlab-ci.yml
file: 'templates/.gitlab-ci.yml'
# Run tests on Drupal 9.1 by default, including a phpspec/prophecy-phpunit requirement.
composer_node:
variables:
# https://getcomposer.org/doc/articles/versions.md#next-significant-release-operators
DRUPAL_CORE_CONSTRAINT: ~9.1.0
after_script:
# See https://www.drupal.org/project/drupal/issues/3182653
# This will fail on PHPUnit 8-, that is OK as its not needed there.
- vendor/bin/spoon require --no-progress phpspec/prophecy-phpunit:^2 || true
################
# DrupalCI includes:
# As long as you include this, any future includes added by the Drupal Association will be accessible to your pipelines automatically.
# View these include files at https://git.drupalcode.org/project/gitlab_templates/
################
- project: $_GITLAB_TEMPLATES_REPO
ref: $_GITLAB_TEMPLATES_REF
file:
- '/includes/include.drupalci.main.yml'
# EXPERIMENTAL: For Drupal 7, remove the above line and uncomment the below.
# - '/includes/include.drupalci.main-d7.yml'
- '/includes/include.drupalci.variables.yml'
- '/includes/include.drupalci.workflows.yml'
################
# Pipeline configuration variables
#
# These are the variables provided to the Run Pipeline form that a user may want to override.
#
# Docs at https://git.drupalcode.org/project/gitlab_templates/-/blob/1.0.x/includes/include.drupalci.variables.yml
################
variables:
_SHOW_ENVIRONMENT_VARIABLES: '1'
phpcs:
allow_failure: false
###################################################################################
#
# *
# /(
# ((((,
# /(((((((
# ((((((((((*
# ,(((((((((((((((
# ,(((((((((((((((((((
# ((((((((((((((((((((((((*
# *(((((((((((((((((((((((((((((
# ((((((((((((((((((((((((((((((((((*
# *(((((((((((((((((( .((((((((((((((((((
# ((((((((((((((((((. /(((((((((((((((((*
# /((((((((((((((((( .(((((((((((((((((,
# ,(((((((((((((((((( ((((((((((((((((((
# .(((((((((((((((((((( .(((((((((((((((((
# ((((((((((((((((((((((( ((((((((((((((((/
# (((((((((((((((((((((((((((/ ,(((((((((((((((*
# .((((((((((((((/ /(((((((((((((. ,(((((((((((((((
# *(((((((((((((( ,(((((((((((((/ *((((((((((((((.
# ((((((((((((((, /(((((((((((((. ((((((((((((((,
# (((((((((((((/ ,(((((((((((((* ,(((((((((((((,
# *((((((((((((( .((((((((((((((( ,(((((((((((((
# ((((((((((((/ /((((((((((((((((((. ,((((((((((((/
# ((((((((((((( *(((((((((((((((((((((((* *((((((((((((
# ((((((((((((( ,(((((((((((((..((((((((((((( *((((((((((((
# ((((((((((((, /((((((((((((* /((((((((((((/ ((((((((((((
# ((((((((((((( /((((((((((((/ (((((((((((((* ((((((((((((
# (((((((((((((/ /(((((((((((( ,((((((((((((, *((((((((((((
# (((((((((((((( *(((((((((((/ *((((((((((((. ((((((((((((/
# *((((((((((((((((((((((((((, /(((((((((((((((((((((((((
# ((((((((((((((((((((((((( ((((((((((((((((((((((((,
# .(((((((((((((((((((((((/ ,(((((((((((((((((((((((
# ((((((((((((((((((((((/ ,(((((((((((((((((((((/
# *((((((((((((((((((((( (((((((((((((((((((((,
# ,(((((((((((((((((((((, ((((((((((((((((((((/
# ,(((((((((((((((((((((* /((((((((((((((((((((
# ((((((((((((((((((((((, ,/((((((((((((((((((((,
# ,(((((((((((((((((((((((((((((((((((((((((((((((((((
# .(((((((((((((((((((((((((((((((((((((((((((((
# .((((((((((((((((((((((((((((((((((((,.
# .,(((((((((((((((((((((((((.
#
###################################################################################
# Migrate Plus
The [Migrate Plus](https://www.drupal.org/project/migrate_plus) project provides extensions to core migration framework functionality, as well as examples.
## Table of contents
- Requirements
- Installation
- Configuration entities
- API extensions
- Process plugins
- Destination plugins
- Source plugins
- Examples
- Related modules
## Requirements
This module requires no modules outside of Drupal core.
## Installation
Install as you would normally install a contributed Drupal module. For further
information, see
[Installing Drupal Modules](https://www.drupal.org/docs/extending-drupal/installing-drupal-modules).
## Configuration entities
- Migration plugins can be implemented as configuration entities, allowing them to flexibly be loaded, modified, and saved.
- MigrationGroup configuration entities allow migrations to be grouped in [UI and command-line tools](https://www.drupal.org/project/migrate_tools), and also allow configuration to be shared among multiple migrations.
## API extensions
- A PREPARE_ROW event is provided to allow object-oriented responses to the core prepare_row hook (modifying source data before processing begins).
## Process plugins
- `entity_lookup` - Allows you to match source data to existing Drupal 8 entities and return their IDs (primarily for populating entity reference fields).
- `entity_generate` - Extends entity_lookup to actually generate an entity from the source data where one does not already exist.
- `file_blob` - Allows you to create a file (and corresponding file entity) from blob data.
- `merge` - Allows you to merge multiple source arrays into one array.
- `skip_on_value` - Like core's skip_on_empty, but allows you to skip either the row or process upon matching (or not) a specific value.
- `str_replace` - Wrapper around str_replace, str_ireplace and preg_replace.
- `transliteration` - process strings through the transliteration service to remove language decorations and accents. Especially helpful with file names.
- Plus many, many more.
## Destination plugins
- Table - allows migrating data directly into a SQL table.
## Source plugins
- SourcePluginExtension - an abstract source plugin class providing a standard mechanism for specifying a source's IDs and fields via configuration.
- Url - a source plugin supporting file- or stream-based content (where a URL, including potentially a local filepath, points to a file containing data to be migrated). The source plugin itself simply manages the (potentially multiple) source URLs, and works with fetcher plugins to retrieve the content and parser plugins to parse it (see below).
## Additional plugin types
### `data_fetcher`
Data fetcher plugins are embedded in the Url source plugin to manage retrieval of data via a given protocol.
- File - A general-purpose fetcher for accessing any local file or stream wrapper supported by PHP's file_get_contents.
- Http - An HTTP-specific fetcher permitting usage of HTTP-specific features (such as specifying request headers).
### `data_parser`
Data parser plugins are embedded in the Url source plugin to parse the content retrieved by a fetcher.
- XML - Parses XML content using the progressive XMLReader PHP extension. Use this when XML content may be too large to be completely parsed in one go in memory.
- SimpleXML - Parses XML content using the SimpleXML PHP extension. Use this when you need full xpath support to access data elements, and the XML files are not too large.
- JSON - Parses JSON content. See [this Lullabot article](https://www.lullabot.com/articles/pull-content-from-a-remote-drupal-8-site-using-migrate-and-json-api) for an example.
- Soap - Parses SOAP feeds.
### `authentication`
Provides authentication services to the HTTP fetcher.
- Basic - supports HTTP Basic authentication.
- Digest - supports HTTP Digest authentication.
- OAuth2 - supports OAuth2 authentication over HTTP.
## Examples
Two submodules provide examples of implementing migrations.
- `migrate_example` - A carefully documented implementation of a custom migration scenario, designed to walk you through the basic concepts of the Drupal 8 migration framework.
- `migrate_example_advanced` (still in progress) - Examples of more advanced techniques for Drupal 8 migration.
## Related modules
Tools for running/managing migrations
- `migrate_tools` - General-purpose drush commands and basic UI for managing migrations.
- `migrate_upgrade` - Drush commands for running upgrades from Drupal 6 or 7 to Drupal 8+.
- `migrate_source_csv` - Source plugin for importing CSV data.
The migrate_plus module extends the core migration system with API enhancements
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 MigrationGroup configuration entity is provided, which enables migrations to
be organized in groups, and to maintain shared configuration in one place.
* A MigrateEvents::PREPARE_ROW event is provided to dispatch
hook_migrate_prepare_row() invocations as events.
* A SourcePluginExtension class is provided, enabling one to define fields and
IDs for a source plugin via configuration rather than requiring PHP code.
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
=======
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
========
* The migrate_example submodule provides a fully functional and runnable
example migration scenario demonstrating the basic concepts and most common
techniques for SQL-based migrations.
* The migrate_example_advanced submodule provides examples of migration from
different kinds of sources, as well as less common techniques.
# Learn to make one for your own drupal.org project:
# https://www.drupal.org/drupalorg/docs/drupal-ci/customizing-drupalci-testing
build:
assessment:
validate_codebase:
phplint:
container_composer:
phpcs:
# phpcs will use core's specified version of Coder.
sniff-all-files: true
halt-on-fail: false
testing:
# run_tests task is executed several times in order of performance speeds.
# halt-on-fail can be set on the run_tests tasks in order to fail fast.
# suppress-deprecations is false in order to be alerted to usages of
# deprecated code.
run_tests.standard:
types: 'Simpletest,PHPUnit-Unit,PHPUnit-Kernel,PHPUnit-Functional'
testgroups: '--all'
suppress-deprecations: false
......@@ -38,7 +38,6 @@ final class MigrateExampleTest extends MigrateDrupalTestBase {
'migrate_example',
]);
$this->installSchema('system', ['sequences']);
$this->installSchema('comment', ['comment_entity_statistics']);
$this->installSchema('node', ['node_access']);
$this->installSchema('user', ['users_data']);
......@@ -47,9 +46,11 @@ final class MigrateExampleTest extends MigrateDrupalTestBase {
\Drupal::service('module_installer')->install(['migrate_example_setup']);
$this->installConfig(['migrate_example_setup']);
$this->startCollectingMessages();
// Execute "beer" migrations from 'migrate_example' module.
$this->executeMigration('beer_user');
$this->executeMigrations([
'beer_user',
'beer_term',
'beer_node',
'beer_comment',
......
......@@ -27,6 +27,8 @@ source:
- 'public://migrate_json_example/products.json'
# An xpath-like selector corresponding to the items to be imported.
item_selector: product
# If no item_selector is present in the JSON file, replace 'product' with '0'.
# item_selector: 0
# 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,
......
......@@ -24,4 +24,11 @@ abstract class DataFetcherPluginBase extends PluginBase implements DataFetcherPl
return new static($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public function getNextUrls(string $url): array {
return [];
}
}
......@@ -51,4 +51,17 @@ interface DataFetcherPluginInterface {
*/
public function getResponse(string $url): ResponseInterface;
/**
* Collect next urls from the metadata of a paged response.
*
* Examples of this include HTTP headers and file naming conventions.
*
* @param string $url
* URL of the resource to check for pager metadata.
*
* @return array
* Array of URIs.
*/
public function getNextUrls(string $url): array;
}
......@@ -82,8 +82,7 @@ abstract class DataParserPluginBase extends PluginBase implements DataParserPlug
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function rewind() {
public function rewind(): void {
$this->activeUrl = NULL;
$this->next();
}
......@@ -91,8 +90,7 @@ abstract class DataParserPluginBase extends PluginBase implements DataParserPlug
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function next() {
public function next(): void {
$this->currentItem = $this->currentId = NULL;
if (is_null($this->activeUrl)) {
if (!$this->nextSource()) {
......@@ -153,6 +151,9 @@ abstract class DataParserPluginBase extends PluginBase implements DataParserPlug
}
if ($this->openSourceUrl($this->urls[$this->activeUrl])) {
if (!empty($this->configuration['pager'])) {
$this->addNextUrls($this->activeUrl);
}
// We have a valid source.
return TRUE;
}
......@@ -161,11 +162,40 @@ abstract class DataParserPluginBase extends PluginBase implements DataParserPlug
return FALSE;
}
/**
* Add next page of source data following the active URL.
*
* @param int $activeUrl
* The index within the source URL array to insert the next URL resource.
* This is parameterized to enable custom plugins to control the ordering of
* next URLs injected into the source URL backlog.
*/
protected function addNextUrls(int $activeUrl = 0): void {
$next_urls = $this->getNextUrls($this->urls[$this->activeUrl]);
if (!empty($next_urls)) {
array_splice($this->urls, $activeUrl + 1, 0, $next_urls);
$this->urls = array_values(array_unique($this->urls));
}
}
/**
* Collected the next urls from a paged response.
*
* @param string $url
* URL of the currently active source.
*
* @return array
* Array of URLs representing next paged resources.
*/
protected function getNextUrls(string $url): array {
return $this->getDataFetcherPlugin()->getNextUrls($url);
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function current() {
public function current(): mixed {
return $this->currentItem;
}
......@@ -181,24 +211,21 @@ abstract class DataParserPluginBase extends PluginBase implements DataParserPlug
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function key() {
public function key(): ?array {
return $this->currentId;
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function valid() {
public function valid(): bool {
return !empty($this->currentItem);
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function count() {
public function count(): int {
return iterator_count($this);
}
......
......@@ -56,6 +56,9 @@ class Migration extends ConfigEntityBase implements MigrationInterface {
*/
protected function invalidateTagsOnSave($update): void {
parent::invalidateTagsOnSave($update);
\Drupal::service('plugin.manager.migration')->clearCachedDefinitions();
// TODO: remove after 10.1 and earlier support sunsets.
Cache::invalidateTags(['migration_plugins']);
}
......@@ -64,6 +67,9 @@ class Migration extends ConfigEntityBase implements MigrationInterface {
*/
protected static function invalidateTagsOnDelete(EntityTypeInterface $entity_type, array $entities): void {
parent::invalidateTagsOnDelete($entity_type, $entities);
\Drupal::service('plugin.manager.migration')->clearCachedDefinitions();
// TODO: remove after 10.1 and earlier support sunsets.
Cache::invalidateTags(['migration_plugins']);
}
......
......@@ -183,11 +183,8 @@ class Table extends DestinationBase implements ContainerFactoryPluginInterface,
elseif ($batch_inserts && $fieldInfo['use_auto_increment']) {
if (count($this->rowsToInsert) === 0) {
// Get the highest existing ID, so we will create IDs above it.
$this->lastId = $this->dbConnection->query("SELECT MAX($field) AS MaxId FROM {{$this->tableName}}")
$this->lastId = (int) $this->dbConnection->query("SELECT MAX($field) AS MaxId FROM {{$this->tableName}}")
->fetchField();
if (!$this->lastId) {
$this->lastId = 0;
}
}
$id = ++$this->lastId;
$ids[$field] = $id;
......
<?php
declare(strict_types=1);
namespace Drupal\migrate_plus\Plugin\migrate\process;
use Drupal\Component\Utility\NestedArray;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Builds an array based on configuration, source, destination, and pipeline.
*
* Usage:
*
* @code
* process:
* bar:
* plugin: array_template
* source: foo
* template:
* key: literal string
* properties:
* - source:field_body/0/value
* - dest:field_body/0/value
* - pipeline:some/nested/key
* @endcode
*
* The result is an array with the same structure (string and numeric keys,
* nesting) as the template. Any string value starting with 'source:' or 'dest:'
* is replaced by the corresponding source or destination property. Do not
* prefix destination properties with '@'. The string value 'pipeline:' is
* replaced with the source, or the previous value from the process pipeline.
* You can also extract keys using the '/' separator.
*
* For example, to convert an indexed array to a keyed array,
*
* @code
* process:
* field_paragraph:
* - plugin: migration_lookup
* # ...
* - plugin: array_template
* template:
* target_id: pipeline:0
* target_revision_id: pipeline:1
* @endcode
*
* If you want a literal string like 'source:foo' in the result, then a
* work-around is to define a constant in the source configuration:
*
* @code
* source:
* # ...
* constants:
* do_not_process_me: source:foo
* process:
* some_field:
* - plugin: array_template
* template:
* - source:constants/do_not_process_me
* @endcode
*
* @MigrateProcessPlugin(id = "array_template")
*/
final class ArrayTemplate extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition) {
if (!is_array($configuration['template'] ?? NULL)) {
throw new \InvalidArgumentException('The "template" must be set to an array.');
}
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property): array {
$template = $this->configuration['template'] ?? NULL;
$args = ['row' => $row, 'pipeline' => $value];
array_walk_recursive($template, [$this, 'process'], $args);
return $template;
}
/**
* Replaces source, destination, or pipeline with the correct value.
*
* @param mixed $value
* The array value as provided by arraywalk_recursive(): any type other than
* array. Passed by reference.
* @param string $key
* The array key as provided by arraywalk_recursive(): ignored.
* @param array $args
* An array with the keys
* - row: the current Row object;
* - pipeline: the pipeline or source value for the process.
*/
protected function process(&$value, string $key, array $args): void {
if (!is_string($value)) {
return;
}
[$type, $key] = explode(':', "$value:", 2);
if ($key === '') {
return;
}
// Strip the added ':'.
$key = substr($key, 0, -1);
['row' => $row, 'pipeline' => $pipeline] = $args;
$value = match($type) {
'source' => $row->getSourceProperty($key),
'dest' => $row->getDestinationProperty($key),
'pipeline' => $key === '' ? $pipeline : NestedArray::getValue($pipeline, explode('/', $key)),
default => $value,
};
}
}
......@@ -126,12 +126,21 @@ class DomApplyStyles extends DomProcessBase implements ContainerFactoryPluginInt
$message = 'The "format" option must be a non-empty string.';
throw new InvalidPluginDefinitionException($this->getPluginId(), $message);
}
$editor_styles = $this->configFactory
->get("editor.editor.$format")
->get('settings.plugins.stylescombo.styles');
foreach (explode("\r\n", $editor_styles) as $rule) {
if (preg_match('/(.*)\|(.*)/', $rule, $matches)) {
$this->styles[$matches[2]] = $matches[1];
$editor_config = $this->configFactory->get("editor.editor.$format");
if ($editor_config->get('editor') === 'ckeditor') {
$editor_styles = $editor_config->get('settings.plugins.stylescombo.styles') ?? '';
foreach (explode("\r\n", $editor_styles) as $rule) {
if (preg_match('/(.*)\|(.*)/', $rule, $matches)) {
$this->styles[$matches[2]] = $matches[1];
}
}
}
else if ($editor_config->get('editor') === 'ckeditor5') {
$editor_styles = $editor_config->get('settings.plugins.ckeditor5_style.styles') ?? [];
foreach ($editor_styles as $editor_style) {
if (preg_match('/<(.*) class="(.*)">/', $editor_style['element'], $matches)) {
$this->styles[$editor_style['label']] = $matches[1] . '.' . $matches[2];
}
}
}
}
......
......@@ -61,6 +61,15 @@ use Drupal\migrate\Row;
* search: 'b'
* replace: 'strong'
* -
* plugin: dom_str_replace
* mode: attribute
* xpath: //a
* attribute_options:
* name: href
* regex: true
* search: '/foo-(\d+)/'
* replace: 'bar-$1'
* -
* plugin: dom
* method: export
* @endcode
......@@ -93,6 +102,10 @@ class DomStrReplace extends DomProcessBase {
];
foreach ($options_validation as $option_name => $possible_values) {
if (empty($this->configuration[$option_name])) {
if ($option_name === 'replace' && isset($this->configuration[$option_name])) {
// Allow empty string for replace.
continue;
}
throw new InvalidPluginDefinitionException(
$this->getPluginId(),
"Configuration option '$option_name' is required."
......
......@@ -4,10 +4,11 @@ declare(strict_types = 1);
namespace Drupal\migrate_plus\Plugin\migrate\process;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\Core\Field\FieldItemList;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
......@@ -174,7 +175,7 @@ class EntityLookup extends ProcessPluginBase implements ContainerFactoryPluginIn
case 'entity_reference':
if (empty($this->lookupBundle)) {
$handlerSettings = $fieldConfig->getSetting('handler_settings');
$bundles = array_filter((array) $handlerSettings['target_bundles']);
$bundles = array_filter((array) ($handlerSettings['target_bundles'] ?? []));
if (count($bundles) == 1) {
$this->lookupBundle = reset($bundles);
}
......@@ -227,11 +228,11 @@ class EntityLookup extends ProcessPluginBase implements ContainerFactoryPluginIn
* Entity id if the queried entity exists. Otherwise NULL.
*/
protected function query($value) {
// Entity queries typically are case-insensitive. Therefore, we need to
// handle case-sensitive filtering as a post-query step. By default, it
// filters case-insensitive. Change to true if that is not the desired
// outcome.
$ignoreCase = !empty($this->configuration['ignore_case']) ?: FALSE;
$query = $this->doGetQuery($value);
return $this->processResults($query->execute(), $value);
}
private function doGetQuery($value): QueryInterface {
$operator = !empty($this->configuration['operator']) ? $this->configuration['operator'] : '=';
$multiple = is_array($value);
......@@ -253,19 +254,37 @@ class EntityLookup extends ProcessPluginBase implements ContainerFactoryPluginIn
if ($this->lookupBundleKey) {
$query->condition($this->lookupBundleKey, (array) $this->lookupBundle, 'IN');
}
$results = $query->execute();
return $query;
}
private function processResults($results, $original_value) {
if (empty($results)) {
return NULL;
}
// Entity queries typically are case-insensitive. Therefore, we need to
// handle case-sensitive filtering as a post-query step. By default, it
// filters case-insensitive. Change to true if that is not the desired
// outcome.
$ignoreCase = !empty($this->configuration['ignore_case']) ?: FALSE;
$operator = !empty($this->configuration['operator']) ? $this->configuration['operator'] : '=';
$multiple = is_array($original_value);
// Do a case-sensitive comparison only for strict operators.
if (!$ignoreCase && in_array($operator, ['=', 'IN'], TRUE)) {
// Returns the entity's identifier.
foreach ($results as $k => $identifier) {
$entity = $this->entityTypeManager->getStorage($this->lookupEntityType)->load($identifier);
$result_value = $entity instanceof ConfigEntityInterface ? $entity->get($this->lookupValueKey) : $entity->get($this->lookupValueKey)->value;
if (($multiple && !in_array($result_value, $value, TRUE)) || (!$multiple && $result_value !== $value)) {
$result_value = $entity->get($this->lookupValueKey);
// If the value is a non-empty field, extract its first value's main
// property (most of the time "value" but sometimes "target_id" or
// anything declared by the field item).
if ($result_value instanceof FieldItemList && !$result_value->isEmpty()) {
$property = $result_value->first()->mainPropertyName();
$result_value = $result_value->{$property};
}
if (($multiple && !in_array($result_value, $original_value, TRUE)) || (!$multiple && $result_value !== $original_value)) {
unset($results[$k]);
}
}
......
......@@ -123,21 +123,19 @@ class FileBlob extends ProcessPluginBase implements ContainerFactoryPluginInterf
// Determine if we're 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;
}
// Create the directory or modify permissions if necessary
$dir = $this->getDirectory($destination);
$success = $this->fileSystem->prepareDirectory($dir, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
if (!$success) {
throw new MigrateSkipProcessException("Could not create directory '$dir'");
}
if ($this->putFile($destination, $blob, $replace)) {
return $destination;
// Attempt to save the file
if (!$this->putFile($destination, $blob, $replace)) {
throw new MigrateSkipProcessException("Blob data could not be copied to $destination.");
}
throw new MigrateSkipProcessException("Blob data could not be copied to $destination.");
return $destination;
}
/**
......
......@@ -44,6 +44,19 @@ use Drupal\migrate\Row;
* The above example will skip further processing of the input property if
* the content_type source field equals "blog".
*
* Example usage with a FieldAPI value:
* @code
* field_fruit:
* plugin: skip_on_value
* source: field_fruit/0/value
* method: row
* value: apple
* @endcode
* The above example will skip the entire row if the "fruit" field is set to
* "apple". When attempting to access values from a simple Field API-based value
* the "0/value" suffix must be used, otherwise it will fail with an "Array to
* string conversion" error.
*
* Example usage with full configuration:
* @code
* type:
......
......@@ -125,6 +125,14 @@ class StrReplace extends ProcessPluginBase {
if ($this->configuration['regex']) {
$function = 'preg_replace';
}
if($this->multiple) {
foreach($value as $key => $item) {
$item = (string) $item;
$value[$key] = $function($this->configuration['search'], $this->configuration['replace'], $item);
}
return $value;
}
$value = (string) $value;
return $function($this->configuration['search'], $this->configuration['replace'], $value);
}
......
......@@ -38,7 +38,10 @@ class File extends DataFetcherPluginBase {
* {@inheritdoc}
*/
public function getResponse($url): ResponseInterface {
$response = @file_get_contents($url);
$response = FALSE;
if (!empty($url)) {
$response = @file_get_contents($url);
}
if ($response === FALSE) {
throw new MigrateException('file parser plugin: could not retrieve data from ' . $url);
}
......
......@@ -4,6 +4,7 @@ declare(strict_types = 1);
namespace Drupal\migrate_plus\Plugin\migrate_plus\data_fetcher;
use Drupal\Component\Utility\NestedArray;
use GuzzleHttp\Client;
use Drupal\migrate_plus\AuthenticationPluginInterface;
use Psr\Http\Message\ResponseInterface;
......@@ -26,6 +27,11 @@ use GuzzleHttp\Exception\RequestException;
* User-Agent: Internet Explorer 6
* Authorization-Key: secret
* Arbitrary-Header: foobarbaz
* # Guzzle request options can be added.
* # See https://docs.guzzlephp.org/en/stable/request-options.html
* request_options:
* timeout: 300
* allow_redirects: false
* @endcode
*
* @DataFetcher(
......@@ -60,6 +66,9 @@ class Http extends DataFetcherPluginBase implements ContainerFactoryPluginInterf
// Ensure there is a 'headers' key in the configuration.
$configuration += ['headers' => []];
$this->setRequestHeaders($configuration['headers']);
// Set GET request-method by default.
$configuration += ['method' => 'GET'];
$this->configuration['method'] = $configuration['method'];
}
/**
......@@ -95,12 +104,13 @@ class Http extends DataFetcherPluginBase implements ContainerFactoryPluginInterf
try {
$options = ['headers' => $this->getRequestHeaders()];
if (!empty($this->configuration['authentication'])) {
$options = array_merge($options, $this->getAuthenticationPlugin()->getAuthenticationOptions());
$options = NestedArray::mergeDeep($options, $this->getAuthenticationPlugin()->getAuthenticationOptions());
}
if (!empty($this->configuration['request_options'])) {
$options = array_merge($options, $this->configuration['request_options']);
$options = NestedArray::mergeDeep($options, $this->configuration['request_options']);
}
$response = $this->httpClient->get($url, $options);
$method = $this->configuration['method'] ?? 'GET';
$response = $this->httpClient->request($method, $url, $options);
if (empty($response)) {
throw new MigrateException('No response at ' . $url . '.');
}
......@@ -118,4 +128,25 @@ class Http extends DataFetcherPluginBase implements ContainerFactoryPluginInterf
return (string) $this->getResponse($url)->getBody();
}
/**
* {@inheritdoc}
*/
public function getNextUrls(string $url): array {
$next_urls = [];
$headers = $this->getResponse($url)->getHeader('Link');
if (!empty($headers)) {
$headers = explode(',', $headers[0]);
foreach ($headers as $header) {
$matches = [];
preg_match('/^<(.*)>; rel="next"$/', trim($header), $matches);
if (!empty($matches) && !empty($matches[1])) {
$next_urls[] = $matches[1];
}
}
}
return array_merge(parent::getNextUrls($url), $next_urls);
}
}