Commit b95a4815 authored by webchick's avatar webchick

Issue #2530030 by phenaproxima, mikeryan, benjy: Create the migrate builder plugin type

parent bd1dbf55
......@@ -20,4 +20,4 @@ destination:
plugin: book
migration_dependencies:
required:
- d6_node
- d6_node:*
......@@ -2,6 +2,9 @@ id: d6_field
label: Drupal 6 field configuration
migration_tags:
- Drupal 6
builder:
plugin: d6_cck_migration
cck_plugin_method: processField
source:
plugin: d6_field
constants:
......
......@@ -2,6 +2,9 @@ id: d6_field_formatter_settings
label: Drupal 6 field formatter configuration
migration_tags:
- Drupal 6
builder:
plugin: d6_cck_migration
cck_plugin_method: processFieldFormatter
source:
plugin: d6_field_instance_per_view_mode
constants:
......
......@@ -2,6 +2,9 @@ id: d6_field_instance
label: Drupal 6 field instance configuration
migration_tags:
- Drupal 6
builder:
plugin: d6_cck_migration
cck_plugin_method: processFieldInstance
source:
plugin: d6_field_instance
constants:
......
......@@ -2,6 +2,9 @@ id: d6_field_instance_widget_settings
label: Drupal 6 field instance widget configuration
migration_tags:
- Drupal 6
builder:
plugin: d6_cck_migration
cck_plugin_method: processFieldWidget
source:
plugin: d6_field_instance_per_form_display
constants:
......
......@@ -24,28 +24,12 @@ class FieldInstance extends DrupalSqlBase {
* {@inheritdoc}
*/
public function query() {
$query = $this->select('content_node_field_instance', 'cnfi')
->fields('cnfi', array(
'field_name',
'type_name',
'weight',
'label',
'widget_type',
'widget_settings',
'display_settings',
'description',
'widget_module',
'widget_active',
'description',
))
->fields('cnf', array(
'required',
'active',
'global_settings',
));
$query = $this->select('content_node_field_instance', 'cnfi')->fields('cnfi');
if (isset($this->configuration['node_type'])) {
$query->condition('cnfi.type_name', $this->configuration['node_type']);
}
$query->join('content_node_field', 'cnf', 'cnf.field_name = cnfi.field_name');
$query->orderBy('weight');
$query->fields('cnf');
return $query;
}
......
......@@ -28,6 +28,9 @@ migrate.migration.*:
destination:
type: migrate.destination.[plugin]
label: 'Destination'
template:
type: string
label: 'Template'
migration_dependencies:
type: mapping
label: 'Dependencies'
......
......@@ -8,6 +8,9 @@ services:
migrate.template_storage:
class: Drupal\migrate\MigrateTemplateStorage
arguments: ['@module_handler']
migrate.migration_builder:
class: Drupal\migrate\MigrationBuilder
arguments: ['@plugin.manager.migrate.builder']
plugin.manager.migrate.source:
class: Drupal\migrate\Plugin\MigratePluginManager
arguments: [source, '@container.namespaces', '@cache.discovery', '@module_handler', 'Drupal\migrate\Annotation\MigrateSource']
......@@ -20,3 +23,6 @@ services:
plugin.manager.migrate.id_map:
class: Drupal\migrate\Plugin\MigratePluginManager
arguments: [id_map, '@container.namespaces', '@cache.discovery', '@module_handler']
plugin.manager.migrate.builder:
class: Drupal\migrate\Plugin\MigratePluginManager
arguments: [builder, '@container.namespaces', '@cache.discovery', '@module_handler']
......@@ -232,6 +232,13 @@ class Migration extends ConfigEntityBase implements MigrationInterface, Requirem
*/
protected $dependencies = [];
/**
* The ID of the template from which this migration was derived, if any.
*
* @var string|NULL
*/
protected $template;
/**
* The entity manager.
*
......
<?php
/**
* @file
* Contains \Drupal\migrate\MigrationBuilder.
*/
namespace Drupal\migrate;
use Drupal\migrate\Entity\Migration;
use Drupal\migrate\Plugin\MigratePluginManager;
/**
* Builds migration entities from migration templates.
*/
class MigrationBuilder {
/**
* The builder plugin manager.
*
* @var \Drupal\migrate\Plugin\MigratePluginManager
*/
protected $builderManager;
/**
* Constructs a MigrationBuilder.
*
* @param \Drupal\migrate\Plugin\MigratePluginManager $builder_manager
* The builder plugin manager.
*/
public function __construct(MigratePluginManager $builder_manager) {
$this->builderManager = $builder_manager;
}
/**
* Builds migration entities from templates.
*
* @param array $templates
* The parsed templates (each of which is an array parsed from YAML), keyed
* by ID.
*
* @return \Drupal\migrate\Entity\MigrationInterface[]
* The migration entities derived from the templates.
*/
public function createMigrations(array $templates) {
/** @var \Drupal\migrate\Entity\MigrationInterface[] $migrations */
$migrations = [];
foreach ($templates as $template_id => $template) {
if (isset($template['builder'])) {
$variants = $this->builderManager
->createInstance($template['builder']['plugin'], $template['builder'])
->buildMigrations($template);
}
else {
$variants = array(Migration::create($template));
}
/** @var \Drupal\migrate\Entity\MigrationInterface[] $variants */
foreach ($variants as $variant) {
$variant->set('template', $template_id);
}
$migrations = array_merge($migrations, $variants);
}
return $migrations;
}
}
......@@ -8,13 +8,110 @@
namespace Drupal\migrate;
use Drupal\Component\Graph\Graph;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\Entity\ConfigEntityStorage;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Query\QueryFactoryInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Storage for migration entities.
*/
class MigrationStorage extends ConfigEntityStorage implements MigrateBuildDependencyInterface {
/**
* The entity query factory service.
*
* @var \Drupal\Core\Entity\Query\QueryFactoryInterface
*/
protected $queryFactory;
/**
* Constructs a MigrationStorage object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* An entity type definition.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\Component\Uuid\UuidInterface $uuid_service
* The UUID service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Entity\Query\QueryFactoryInterface $query_factory
* The entity query factory service.
*/
public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, UuidInterface $uuid_service, LanguageManagerInterface $language_manager, QueryFactoryInterface $query_factory) {
parent::__construct($entity_type, $config_factory, $uuid_service, $language_manager);
$this->queryFactory = $query_factory;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('config.factory'),
$container->get('uuid'),
$container->get('language_manager'),
$container->get('entity.query.config')
);
}
/**
* {@inheritdoc}
*/
public function loadMultiple(array $ids = NULL) {
/** @var \Drupal\migrate\Entity\MigrationInterface[] $migrations */
$migrations = parent::loadMultiple($ids);
foreach ($migrations as $migration) {
$migration->set('migration_dependencies', $this->expandDependencies($migration->getMigrationDependencies()));
}
return $migrations;
}
/**
* Expands template dependencies.
*
* Migration dependencies which match the template_id:* pattern are a signal
* that the migration depends on every variant of template_id. This method
* queries for those variant IDs and splices them into the list of
* dependencies.
*
* @param array $dependencies
* The original migration dependencies (with template IDs), organized by
* group (required, optional, etc.)
*
* @return array
* The expanded list of dependencies, organized by group.
*/
protected function expandDependencies(array $dependencies) {
$expanded_dependencies = [];
foreach (array_keys($dependencies) as $group) {
$expanded_dependencies[$group] = [];
foreach ($dependencies[$group] as $dependency_id) {
if (substr($dependency_id, -2) == ':*') {
$template_id = substr($dependency_id, 0, -2);
$variants = $this->queryFactory->get($this->entityType, 'OR')
->condition('id', $template_id)
->condition('template', $template_id)
->execute();
$expanded_dependencies[$group] = array_merge($expanded_dependencies[$group], $variants);
}
else {
$expanded_dependencies[$group][] = $dependency_id;
}
}
}
return $expanded_dependencies;
}
/**
* {@inheritdoc}
*/
......
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\MigrateBuilderInterface.
*/
namespace Drupal\migrate\Plugin;
/**
* Defines the builder plugin type.
*
* Builder plugins implement custom logic to generate migration entities from
* migration templates. For example, a migration may need to be customized
* based on data that's present in the source database; such customization is
* implemented by builders.
*/
interface MigrateBuilderInterface {
/**
* Builds migration entities based on a template.
*
* @param array $template
* The parsed template.
*
* @return \Drupal\migrate\Entity\MigrationInterface[]
* The unsaved migrations generated from the template.
*/
public function buildMigrations(array $template);
}
<?php
/**
* @file
* Contains \Drupal\migrate\Plugin\migrate\builder\BuilderBase.
*/
namespace Drupal\migrate\Plugin\migrate\builder;
use Drupal\Core\Plugin\PluginBase;
use Drupal\migrate\Entity\Migration;
use Drupal\migrate\Plugin\MigrateBuilderInterface;
/**
* Base class for builder plugins.
*/
abstract class BuilderBase extends PluginBase implements MigrateBuilderInterface {
/**
* Returns a fully initialized instance of a source plugin.
*
* @param string $plugin_id
* The plugin ID.
* @param array $configuration
* (optional) Additional configuration for the plugin.
*
* @return \Drupal\migrate\Plugin\MigrateSourceInterface
* The fully initialized source plugin.
*/
protected function getSourcePlugin($plugin_id, array $configuration = []) {
$configuration['plugin'] = $plugin_id;
// By default, SqlBase subclasses will try to join on a map table. But in
// this case we're trying to use the source plugin as a detached iterator
// over the source data, so we don't want to join on (or create) the map
// table.
// @see SqlBase::initializeIterator()
$configuration['ignore_map'] = TRUE;
// Source plugins are tightly coupled to migration entities, so we need
// to create a fake migration in order to properly initialize the plugin.
$values = [
'id' => uniqid(),
'source' => $configuration,
// Since this isn't a real migration, we don't want a real destination --
// the 'null' destination is perfect for this.
'destination' => [
'plugin' => 'null',
],
];
return Migration::create($values)->getSourcePlugin();
}
}
......@@ -133,7 +133,7 @@ protected function initializeIterator() {
// OR above high water).
$conditions = $this->query->orConditionGroup();
$condition_added = FALSE;
if ($this->mapJoinable()) {
if (empty($this->configuration['ignore_map']) && $this->mapJoinable()) {
// Build the join to the map table. Because the source key could have
// multiple fields, we need to build things up.
$count = 1;
......
......@@ -28,29 +28,41 @@ protected function getMigration() {
$this->migrationConfiguration += ['migrationClass' => 'Drupal\migrate\Entity\Migration'];
$this->idMap = $this->getMock('Drupal\migrate\Plugin\MigrateIdMapInterface');
$this->idMap->expects($this->any())
$this->idMap
->method('getQualifiedMapTableName')
->will($this->returnValue('test_map'));
->willReturn('test_map');
$migration = $this->getMockBuilder($this->migrationConfiguration['migrationClass'])
->disableOriginalConstructor()
->getMock();
$migration->expects($this->any())
->method('checkRequirements')
->will($this->returnValue(TRUE));
$migration->expects($this->any())
->method('getIdMap')
->will($this->returnValue($this->idMap));
$migration->method('checkRequirements')
->willReturn(TRUE);
$migration->method('getIdMap')
->willReturn($this->idMap);
$migration->method('getMigrationDependencies')
->willReturn([
'required' => [],
'optional' => [],
]);
$configuration = &$this->migrationConfiguration;
$migration->expects($this->any())->method('get')->will($this->returnCallback(function ($argument) use (&$configuration) {
return isset($configuration[$argument]) ? $configuration[$argument] : '';
}));
$migration->expects($this->any())->method('set')->will($this->returnCallback(function ($argument, $value) use (&$configuration) {
$migration->method('get')
->willReturnCallback(function ($argument) use (&$configuration) {
return isset($configuration[$argument]) ? $configuration[$argument] : '';
});
$migration->method('set')
->willReturnCallback(function ($argument, $value) use (&$configuration) {
$configuration[$argument] = $value;
}));
$migration->expects($this->any())
->method('id')
->will($this->returnValue($configuration['id']));
});
$migration->method('id')
->willReturn($configuration['id']);
return $migration;
}
......
<?php
/**
* @file
* Contains \Drupal\Tests\migrate\Unit\MigrationStorageTest.
*/
namespace Drupal\Tests\migrate\Unit;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Query\QueryFactoryInterface;
use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\migrate\MigrationStorage;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\migrate\MigrationStorage
* @group migrate
*/
class MigrationStorageTest extends UnitTestCase {
/**
* @var \Drupal\Tests\migrate\Unit\TestMigrationStorage
*/
protected $storage;
/**
* @var \Drupal\Core\Entity\Query\QueryInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $query;
/**
* {@inheritdoc}
*/
public function setUp() {
$this->query = $this->getMock(QueryInterface::class);
$this->query->method('condition')
->willReturnSelf();
$query_factory = $this->getMock(QueryFactoryInterface::class);
$query_factory->method('get')
->willReturn($this->query);
$this->storage = new TestMigrationStorage(
$this->getMock(EntityTypeInterface::class),
$this->getMock(ConfigFactoryInterface::class),
$this->getMock(UuidInterface::class),
$this->getMock(LanguageManagerInterface::class),
$query_factory
);
}
/**
* Tests expandDependencies() when variants exist.
*
* @covers ::expandDependencies
*/
public function testExpandDependenciesWithVariants() {
$this->query->method('execute')
->willReturn(['d6_node__page', 'd6_node__article']);
$dependencies = [
'required' => [
'd6_node:*',
'd6_user',
],
];
$dependencies = $this->storage->expandDependencies($dependencies);
$this->assertSame(['d6_node__page', 'd6_node__article', 'd6_user'], $dependencies['required']);
}
/**
* Tests expandDependencies() when no variants exist.
*
* @covers ::expandDependencies
*/
public function testExpandDependenciesNoVariants() {
$this->query->method('execute')
->willReturn([]);
$dependencies = [
'required' => [
'd6_node:*',
'd6_user',
],
];
$dependencies = $this->storage->expandDependencies($dependencies);
$this->assertSame(['d6_user'], $dependencies['required']);
}
/**
* Tests expandDependencies() when no variants exist and there are no static
* (non-variant) dependencies.
*
* @covers ::expandDependencies
*/
public function testExpandDependenciesNoVariantsOrStaticDependencies() {
$this->query->method('execute')
->willReturn([]);
$dependencies = [
'required' => [
'd6_node:*',
'd6_node_revision:*',
],
];
$dependencies = $this->storage->expandDependencies($dependencies);
$this->assertSame([], $dependencies['required']);
}
}
/**
* Test version of \Drupal\migrate\MigrationStorage.
*
* Exposes protected methods for testing.
*/
class TestMigrationStorage extends MigrationStorage {
/**
* {@inheritdoc}
*/
public function expandDependencies(array $dependencies) {
return parent::expandDependencies($dependencies);
}
}
......@@ -14,4 +14,4 @@ destination:
migration_dependencies:
required:
- d6_cck_field_values
- d6_node_revision
- d6_node_revision:*
......@@ -16,6 +16,6 @@ destination:
plugin: entity:node
migration_dependencies:
required:
- d6_node
- d6_node:*
- d6_field_formatter_settings
- d6_field_instance_widget_settings
......@@ -24,5 +24,5 @@ destination:
migration_dependencies:
required:
- d6_file
- d6_node
- d6_node:*
- d6_upload_field_instance
......@@ -12,6 +12,7 @@
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Query\QueryFactoryInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
use Drupal\migrate_drupal\Plugin\CckFieldMigrateSourceInterface;
......@@ -47,11 +48,13 @@ class MigrationStorage extends BaseMigrationStorage {
* The UUID service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Entity\Query\QueryFactoryInterface $query_factory
* The entity query factory.
* @param \Drupal\migrate_drupal\Plugin\MigratePluginManager
* The cckfield plugin manager.
*/
public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, UuidInterface $uuid_service, LanguageManagerInterface $language_manager, MigratePluginManager $cck_plugin_manager) {
parent::__construct($entity_type, $config_factory, $uuid_service, $language_manager);
public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, UuidInterface $uuid_service, LanguageManagerInterface $language_manager, QueryFactoryInterface $query_factory, MigratePluginManager $cck_plugin_manager) {
parent::__construct($entity_type, $config_factory, $uuid_service, $language_manager, $query_factory);
$this->cckPluginManager = $cck_plugin_manager;
}
......@@ -64,6 +67,7 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
$container->get('config.factory'),
$container->get('uuid'),
$container->get('language_manager'),
$container->get('entity.query.config'),
$container->get('plugin.manager.migrate.cckfield')
);
}
......
<?php
/**
* @file
* Contains \Drupal\migrate_drupal\Plugin\migrate\builder\d6\CckBuilder.
*/
namespace Drupal\migrate_drupal\Plugin\migrate\builder\d6;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Plugin\migrate\builder\BuilderBase;
use Drupal\migrate\Plugin\MigratePluginManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base class for builders which leverage cckfield plugins.
*/
abstract class CckBuilder extends BuilderBase implements ContainerFactoryPluginInterface {
/**
* The cckfield plugin manager.
*
* @var \Drupal\migrate\Plugin\MigratePluginManager
*/
protected $cckPluginManager;
/**
* Constructs a CckBuilder.
*
* @param array $configuration
* Plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param mixed $plugin_definition
* The plugin definition.
* @param \Drupal\migrate\Plugin\MigratePluginManager $cck_manager
* The cckfield plugin manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigratePluginManager $cck_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->cckPluginManager = $cck_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('plugin.manager.migrate.cckfield')
);
}
}