Commit 6b14845b authored by catch's avatar catch
Browse files

Issue #3004456 by mikelutz, phenaproxima, alexpott, heddn, maxocub, quietone:...

Issue #3004456 by mikelutz, phenaproxima, alexpott, heddn, maxocub, quietone: Add a weighting option to MigrateField plugin's definition and lower weight of deprecated 'date' plugin so it isn't used
parent 971c9460
...@@ -32,7 +32,8 @@ public function getProcess() { ...@@ -32,7 +32,8 @@ public function getProcess() {
$field_type = $row->getSourceProperty('type'); $field_type = $row->getSourceProperty('type');
if ($this->fieldPluginManager->hasDefinition($field_type)) { if ($this->fieldPluginManager->hasDefinition($field_type)) {
if (!isset($this->fieldPluginCache[$field_type])) { if (!isset($this->fieldPluginCache[$field_type])) {
$this->fieldPluginCache[$field_type] = $this->fieldPluginManager->createInstance($field_type, [], $this); $plugin_id = $this->fieldPluginManager->getPluginIdFromFieldType($field_type, [], $this);
$this->fieldPluginCache[$field_type] = $this->fieldPluginManager->createInstance($plugin_id, [], $this);
} }
$info = $row->getSource(); $info = $row->getSource();
$this->fieldPluginCache[$field_type]->defineValueProcessPipeline($this, $field_name, $info); $this->fieldPluginCache[$field_type]->defineValueProcessPipeline($this, $field_name, $info);
......
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
namespace Drupal\datetime\Plugin\migrate\field\d6; namespace Drupal\datetime\Plugin\migrate\field\d6;
@trigger_error('DateField is deprecated in Drupal 8.4.x and will be removed before Drupal 9.0.x. Use \Drupal\datetime\Plugin\migrate\field\DateField instead.', E_USER_DEPRECATED);
use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateException; use Drupal\migrate\MigrateException;
use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase; use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase;
...@@ -18,7 +16,8 @@ ...@@ -18,7 +16,8 @@
* }, * },
* core = {6}, * core = {6},
* source_module = "date", * source_module = "date",
* destination_module = "datetime" * destination_module = "datetime",
* weight = 9999999,
* ) * )
* *
* @deprecated in Drupal 8.4.x, to be removed before Drupal 9.0.x. Use * @deprecated in Drupal 8.4.x, to be removed before Drupal 9.0.x. Use
...@@ -26,6 +25,14 @@ ...@@ -26,6 +25,14 @@
*/ */
class DateField extends FieldPluginBase { class DateField extends FieldPluginBase {
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition) {
@trigger_error('DateField is deprecated in Drupal 8.4.x and will be removed before Drupal 9.0.x. Use \Drupal\datetime\Plugin\migrate\field\DateField instead.', E_USER_DEPRECATED);
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
* id = "link", * id = "link",
* core = {6}, * core = {6},
* type_map = { * type_map = {
* "link_field" = "link" * "link" = "link",
* }, * },
* source_module = "link", * source_module = "link",
* destination_module = "link" * destination_module = "link"
......
<?php
/**
* @file
* Post update functions for migrate_drupal.
*/
/**
* Force MigrateField plugin definitions to be cleared.
*
* @see https://www.drupal.org/node/3006470
*/
function drupal_migrate_post_update_clear_migrate_field_plugin_cache() {
// Empty post-update hook.
}
...@@ -71,4 +71,15 @@ public function __construct($values) { ...@@ -71,4 +71,15 @@ public function __construct($values) {
*/ */
public $destination_module; public $destination_module;
/**
* The weight of this plugin relative to other plugins.
*
* The weight of this plugin relative to other plugins servicing the same
* field type and core version. The lowest weighted applicable plugin will be
* used for each field.
*
* @var int
*/
public $weight = 0;
} }
...@@ -28,7 +28,27 @@ class MigrateFieldPluginManager extends MigratePluginManager implements MigrateF ...@@ -28,7 +28,27 @@ class MigrateFieldPluginManager extends MigratePluginManager implements MigrateF
const DEFAULT_CORE_VERSION = 6; const DEFAULT_CORE_VERSION = 6;
/** /**
* {@inheritdoc} * Get the plugin ID from the field type.
*
* This method determines which field plugin should be used for a given field
* type and Drupal core version, returning the lowest weighted plugin
* supporting the provided core version, and which matches the field type
* either by plugin ID, or in the type_map annotation keys.
*
* @param string $field_type
* The field type being migrated.
* @param array $configuration
* (optional) An array of configuration relevant to the plugin instance.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* (optional) The current migration instance.
*
* @return string
* The ID of the plugin for the field type if available.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* If the plugin cannot be determined, such as if the field type is invalid.
*
* @see \Drupal\migrate_drupal\Annotation\MigrateField
*/ */
public function getPluginIdFromFieldType($field_type, array $configuration = [], MigrationInterface $migration = NULL) { public function getPluginIdFromFieldType($field_type, array $configuration = [], MigrationInterface $migration = NULL) {
$core = static::DEFAULT_CORE_VERSION; $core = static::DEFAULT_CORE_VERSION;
...@@ -67,4 +87,27 @@ public function processDefinition(&$definition, $plugin_id) { ...@@ -67,4 +87,27 @@ public function processDefinition(&$definition, $plugin_id) {
} }
} }
/**
* {@inheritdoc}
*/
protected function findDefinitions() {
$definitions = parent::findDefinitions();
$this->sortDefinitions($definitions);
return $definitions;
}
/**
* Sorts a definitions array.
*
* This sorts the definitions array first by the weight column, and then by
* the plugin ID, ensuring a stable, deterministic, and testable ordering of
* plugins.
*
* @param array $definitions
* The definitions array to sort.
*/
protected function sortDefinitions(array &$definitions) {
array_multisort(array_column($definitions, 'weight'), SORT_ASC, SORT_NUMERIC, array_keys($definitions), SORT_ASC, SORT_NATURAL, $definitions);
}
} }
...@@ -8,49 +8,132 @@ ...@@ -8,49 +8,132 @@
* Tests the field plugin manager. * Tests the field plugin manager.
* *
* @group migrate_drupal * @group migrate_drupal
* @coversDefaultClass \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManager
*/ */
class MigrateFieldPluginManagerTest extends MigrateDrupalTestBase { class MigrateFieldPluginManagerTest extends MigrateDrupalTestBase {
/**
* The field plugin manager.
*
* @var \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface
*/
protected $pluginManager;
/**
* {@inheritdoc}
*/
public static $modules = [
'datetime',
'system',
'user',
'field',
'migrate_drupal',
'options',
'file',
'image',
'text',
'link',
'migrate_field_plugin_manager_test',
];
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public static $modules = ['system', 'user', 'field', 'migrate_drupal', 'options', 'file', 'image', 'text', 'link', 'migrate_field_plugin_manager_test']; public function setUp() {
parent::setUp();
$this->pluginManager = $this->container->get('plugin.manager.migrate.field');
}
/** /**
* Tests that the correct MigrateField plugins are used. * Tests that the correct MigrateField plugins are used.
*
* @covers ::getPluginIdFromFieldType
*/ */
public function testPluginSelection() { public function testPluginSelection() {
$plugin_manager = $this->container->get('plugin.manager.migrate.field'); $this->assertSame('link', $this->pluginManager->getPluginIdFromFieldType('link', ['core' => 6]));
$this->assertSame('link_field', $this->pluginManager->getPluginIdFromFieldType('link_field', ['core' => 7]));
try { $this->assertSame('image', $this->pluginManager->getPluginIdFromFieldType('image', ['core' => 7]));
// If this test passes, getPluginIdFromFieldType will raise a $this->assertSame('file', $this->pluginManager->getPluginIdFromFieldType('file', ['core' => 7]));
// PluginNotFoundException and we'll never reach fail(). $this->assertSame('d6_file', $this->pluginManager->getPluginIdFromFieldType('file', ['core' => 6]));
$plugin_manager->getPluginIdFromFieldType('filefield', ['core' => 7]); $this->assertSame('d6_text', $this->pluginManager->getPluginIdFromFieldType('text', ['core' => 6]));
$this->fail('Expected Drupal\Component\Plugin\Exception\PluginNotFoundException.'); $this->assertSame('d7_text', $this->pluginManager->getPluginIdFromFieldType('text', ['core' => 7]));
}
catch (PluginNotFoundException $e) {
$this->assertIdentical($e->getMessage(), "Plugin ID 'filefield' was not found.");
}
$this->assertIdentical('link', $plugin_manager->getPluginIdFromFieldType('link', ['core' => 6])); // Test that the deprecated d6 'date' plugin is not returned.
$this->assertIdentical('link_field', $plugin_manager->getPluginIdFromFieldType('link_field', ['core' => 7])); $this->assertSame('datetime', $this->pluginManager->getPluginIdFromFieldType('date', ['core' => 6]));
$this->assertIdentical('image', $plugin_manager->getPluginIdFromFieldType('image', ['core' => 7]));
$this->assertIdentical('file', $plugin_manager->getPluginIdFromFieldType('file', ['core' => 7]));
$this->assertIdentical('d6_file', $plugin_manager->getPluginIdFromFieldType('file', ['core' => 6]));
$this->assertIdentical('d6_text', $plugin_manager->getPluginIdFromFieldType('text', ['core' => 6]));
$this->assertIdentical('d7_text', $plugin_manager->getPluginIdFromFieldType('text', ['core' => 7]));
// Test fallback when no core version is specified. // Test fallback when no core version is specified.
$this->assertIdentical('d6_no_core_version_specified', $plugin_manager->getPluginIdFromFieldType('d6_no_core_version_specified', ['core' => 6])); $this->assertSame('d6_no_core_version_specified', $this->pluginManager->getPluginIdFromFieldType('d6_no_core_version_specified', ['core' => 6]));
}
try { /**
// If this test passes, getPluginIdFromFieldType will raise a * Tests that a PluginNotFoundException is thrown when a plugin isn't found.
// PluginNotFoundException and we'll never reach fail(). *
$plugin_manager->getPluginIdFromFieldType('d6_no_core_version_specified', ['core' => 7]); * @covers ::getPluginIdFromFieldType
$this->fail('Expected Drupal\Component\Plugin\Exception\PluginNotFoundException.'); * @dataProvider nonExistentPluginExceptionsData
} */
catch (PluginNotFoundException $e) { public function testNonExistentPluginExceptions($core, $field_type) {
$this->assertIdentical($e->getMessage(), "Plugin ID 'd6_no_core_version_specified' was not found."); $this->setExpectedException(PluginNotFoundException::class, sprintf("Plugin ID '%s' was not found.", $field_type));
$this->pluginManager->getPluginIdFromFieldType($field_type, ['core' => $core]);
}
/**
* Provides data for testNonExistentPluginExceptions.
*
* @return array
* The data.
*/
public function nonExistentPluginExceptionsData() {
return [
'D7 Filefield' => [
'core' => 7,
'field_type' => 'filefield',
],
'D6 linkfield' => [
'core' => 6,
'field_type' => 'link_field',
],
'D7 link' => [
'core' => 7,
'field_type' => 'link',
],
'D7 no core version' => [
'core' => 7,
'field_type' => 'd6_no_core_version_specified',
],
];
}
/**
* Tests that deprecated plugins can still be directly created.
*
* Tests that a deprecated plugin triggers an error on instantiation. This
* test has an implicit assertion that the deprecation error will be triggered
* and does not need an explicit assertion to pass.
*
* @covers ::createInstance
* @group legacy
* @expectedDeprecation DateField is deprecated in Drupal 8.4.x and will be removed before Drupal 9.0.x. Use \Drupal\datetime\Plugin\migrate\field\DateField instead.
*/
public function testDeprecatedPluginDirectAccess() {
$this->pluginManager->createInstance('date');
}
/**
* Tests that plugins with no explicit weight are given a weight of 0.
*/
public function testDefaultWeight() {
$definitions = $this->pluginManager->getDefinitions();
$deprecated_plugins = [
'date',
];
foreach ($definitions as $id => $definition) {
$this->assertArrayHasKey('weight', $definition);
if (in_array($id, $deprecated_plugins, TRUE)) {
$this->assertSame(9999999, $definition['weight']);
}
else {
$this->assertSame(0, $definition['weight']);
}
} }
} }
......
<?php
namespace Drupal\Tests\migrate_drupal\Unit;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
use Drupal\migrate_drupal\Annotation\MigrateField;
use Drupal\migrate_drupal\Plugin\MigrateFieldPluginManager;
use Drupal\Tests\UnitTestCase;
/**
* Tests the MigrateFieldPluginManager class.
*
* @group migrate_drupal
* @coversDefaultClass \Drupal\migrate_drupal\Plugin\MigrateFieldPluginManager
*/
class MigrateFieldPluginManagerTest extends UnitTestCase {
/**
* Tests the plugin weighting system.
*
* @covers ::getPluginIdFromFieldType
* @covers ::sortDefinitions
* @covers ::findDefinitions
* @dataProvider weightsData
*/
public function testWeights($field_type, $core, $expected_plugin_id) {
/* @var \Drupal\Core\Cache\CacheBackendInterface $cache */
$cache = $this->prophesize(CacheBackendInterface::class)->reveal();
/* @var \Drupal\Core\Extension\ModuleHandlerInterfaceModuleHandlerInterface $module_handler */
$module_handler = $this->prophesize(ModuleHandlerInterface::class)->reveal();
$discovery = $this->prophesize(AnnotatedClassDiscovery::class);
$discovery->getDefinitions()->willReturn($this->pluginFixtureData());
$manager = new MigrateFieldPluginManagerTestClass('field', new \ArrayObject(), $cache, $module_handler, MigrateField::class, $discovery->reveal());
if (!$expected_plugin_id) {
$this->setExpectedException(PluginNotFoundException::class, sprintf("Plugin ID '%s' was not found.", $field_type));
}
$actual_plugin_id = $manager->getPluginIdFromFieldType($field_type, ['core' => $core]);
$this->assertSame($expected_plugin_id, $actual_plugin_id);
}
/**
* Provides data for testWeights().
*
* @return array
* The data.
*/
public function weightsData() {
return [
'Field 1, D6' => [
'field_type' => 'field_1',
'core' => 6,
'expected_plugin_id' => 'core_replacement_plugin',
],
'Field 2, D6' => [
'field_type' => 'field_2',
'core' => 6,
'expected_plugin_id' => 'field_1',
],
'Field 3, D6' => [
'field_type' => 'field_3',
'core' => 6,
'expected_plugin_id' => 'field_3',
],
'Field 4, D6' => [
'field_type' => 'field_4',
'core' => 6,
'expected_plugin_id' => 'field_4',
],
'Field 5, D6' => [
'field_type' => 'field_5',
'core' => 6,
'expected_plugin_id' => 'alphabetically_second',
],
'Field 1, D7' => [
'field_type' => 'field_1',
'core' => 7,
'expected_plugin_id' => 'core_replacement_plugin',
],
'Field 2, D7' => [
'field_type' => 'field_2',
'core' => 7,
'expected_plugin_id' => FALSE,
],
'Field 3, D7' => [
'field_type' => 'field_3',
'core' => 7,
'expected_plugin_id' => 'field_3',
],
'Field 4, D7' => [
'field_type' => 'field_4',
'core' => 7,
'expected_plugin_id' => 'contrib_override_plugin',
],
'Field 5, D7' => [
'field_type' => 'field_5',
'core' => 7,
'expected_plugin_id' => 'alphabetically_first',
],
];
}
/**
* Returns test plugin data for the test class to use.
*
* @return array
* The test plugin data.
*/
protected function pluginFixtureData() {
return [
// Represents a deprecated core field plugin that applied to field_1
// and field_2 for Drupal 6.
'field_1' => [
'weight' => 99999999,
'core' => [6],
'type_map' => [
'field_1' => 'field_1',
'field_2' => 'field_2',
],
'source_module' => 'system',
'destination_module' => 'system',
],
// Replacement for deprecated plugin for field_1 in Drupal 6 and 7.
// Does not provide replacement for field_2.
'core_replacement_plugin' => [
'weight' => 0,
'core' => [6, 7],
'type_map' => [
'field_1' => 'field_1',
],
'source_module' => 'system',
'destination_module' => 'system',
],
// Represents a core plugin with no type_map, applies to field_3 due to
// plugin id.
'field_3' => [
'core' => [6, 7],
'type_map' => [],
'weight' => 0,
'source_module' => 'system',
'destination_module' => 'system',
],
// Represents a core plugin with no type_map, applies to field_4 due to
// plugin id.
'field_4' => [
'core' => [6, 7],
'type_map' => [],
'weight' => 0,
'source_module' => 'system',
'destination_module' => 'system',
],
// Represents a contrib plugin overriding field_4 for Drupal 7 only.
'contrib_override_plugin' => [
'weight' => -100,
'core' => [7],
'type_map' => [
'field_4' => 'field_4',
],
'source_module' => 'system',
'destination_module' => 'system',
],
// field_5 is served by alphabetically_second in Drupal 6 and
// alphabetically_first and alphabetically_second in Drupal 7. It should
// be served by the alphabetically_first in Drupal 7 regardless of the
// order they appear here.
'alphabetically_second' => [
'weight' => 0,
'core' => [6, 7],
'type_map' => [
'field_5' => 'field_5',
],
'source_module' => 'system',
'destination_module' => 'system',
],
'alphabetically_first' => [
'weight' => 0,
'core' => [7],
'type_map' => [
'field_5' => 'field_5',
],
'source_module' => 'system',
'destination_module' => 'system',
],
];
}
}
/**
* Class to test MigrateFieldPluginManager.
*
* Overrides the constructor to inject a mock discovery class to provide a test
* list of plugins.
*/
class MigrateFieldPluginManagerTestClass extends MigrateFieldPluginManager {
/**
* Constructs a MigratePluginManagerTestClass object.
*
* @param string $type
* The type of the plugin: row, source, process, destination, entity_field,
* id_map.
* @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.
* @param string $annotation
* The annotation class name.
* @param \Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery $discovery
* A mock plugin discovery object for the test class to use.
*/
public function __construct($type, \Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, $annotation, AnnotatedClassDiscovery $discovery) {
parent::__construct($type, $namespaces, $cache_backend, $module_handler, $annotation);
$this->discovery = $discovery;
}
}
...@@ -33,7 +33,8 @@ public function getProcess() { ...@@ -33,7 +33,8 @@ public function getProcess() {
} }
if ($this->fieldPluginManager->hasDefinition($field_type)) { if ($this->fieldPluginManager->hasDefinition($field_type)) {
if (!isset($this->fieldPluginCache[$field_type])) { if (!isset($this->fieldPluginCache[$field_type])) {
$this->fieldPluginCache[$field_type] = $this->fieldPluginManager->createInstance($field_type, [], $this); $plugin_id = $this->fieldPluginManager->getPluginIdFromFieldType($field_type, [], $this);
$this->fieldPluginCache[$field_type] = $this->fieldPluginManager->createInstance($plugin_id, [], $this);
} }
$info = $row->getSource(); $info = $row->getSource();