added migration modules

This commit is contained in:
Bachir Soussi Chiadmi
2017-11-14 14:56:04 +01:00
parent 5a43b028b7
commit c546d42e72
183 changed files with 12883 additions and 0 deletions
@@ -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.
@@ -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.
@@ -0,0 +1,513 @@
<?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_tools\MigrateExecutable;
use Drupal\migrate_tools\DrushLogMigrateMessage;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\migrate_plus\Entity\MigrationGroup;
/**
* 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'],
];
$items['migrate-rollback'] = array(
'description' => 'Rollback one or more migrations.',
'options' => array(
'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' => array(
'migration' => 'Name of migration(s) to rollback. Delimit multiple using commas.',
),
'examples' => array(
'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' => array('migrate_tools'),
'aliases' => array('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;
}
/**
* @param string $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', array('@name' => $group_name))
];
}
else {
$table[] = [
dt('Group: @name', array('@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 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);
}
/**
* @param string $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');
$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.config_entity_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);
// drush_op() provides --simulate support
drush_op(array($executable, 'import'));
}
/**
* @param string $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(array($executable, 'rollback'));
}
}
}
/**
* @param string $migration_id
*/
function drush_migrate_tools_migrate_stop($migration_id = '') {
/** @var 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');
}
}
/**
* @param string $migration_id
*/
function drush_migrate_tools_migrate_reset_status($migration_id = '') {
/** @var 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');
}
}
/**
* @param string $migration_id
*/
function drush_migrate_tools_migrate_messages($migration_id) {
/** @var 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');
}
}
/**
* @param string $migration_id
*/
function drush_migrate_tools_migrate_fields_source($migration_id) {
/** @var 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 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 : [];
}
@@ -0,0 +1,14 @@
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
# Information added by Drupal.org packaging script on 2017-05-10
version: '8.x-4.0-beta1'
core: '8.x'
project: 'migrate_tools'
datestamp: 1494450194
@@ -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
@@ -0,0 +1,5 @@
migrate_tools.menu:
title: Migrations
parent: system.admin_structure
description: Manage migration processes.
route_name: entity.migration_group.list
@@ -0,0 +1,41 @@
# 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
@@ -0,0 +1,33 @@
<?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('add', 'Drupal\migrate_tools\Form\MigrationAddForm')
->setFormClass('edit', 'Drupal\migrate_tools\Form\MigrationEditForm')
->setFormClass('delete', 'Drupal\migrate_tools\Form\MigrationDeleteForm')
// ->setLinkTemplate('edit-form', '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}')
->setLinkTemplate('list-form', '/admin/structure/migrate/manage/{migration_group}/migrations')
/* ->setLinkTemplate('delete-form', '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/delete')*/;
$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('list-form', '/admin/structure/migrate/manage/{migration_group}/migrations')*/
->setLinkTemplate('delete-form', '/admin/structure/migrate/manage/{migration_group}/delete');
}
@@ -0,0 +1,3 @@
'administer migrations':
title: 'Administer migrations'
description: Create, edit, and manage migration processed.
@@ -0,0 +1,109 @@
# 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'
requirements:
_permission: 'administer migrations'
entity.migration.source:
path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/source'
defaults:
_controller: '\Drupal\migrate_tools\Controller\MigrationController::source'
_title: 'Source'
requirements:
_permission: 'administer migrations'
entity.migration.process:
path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/process'
defaults:
_controller: '\Drupal\migrate_tools\Controller\MigrationController::process'
_title: 'Process'
requirements:
_permission: 'administer migrations'
entity.migration.destination:
path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/destination'
defaults:
_controller: '\Drupal\migrate_tools\Controller\MigrationController::destination'
_title: 'Destination'
requirements:
_permission: 'administer migrations'
# 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
requirements:
_entity_access: migration.update
# 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
requirements:
_entity_access: migration.delete
migrate_tools.messages:
path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/messages'
defaults:
_controller: '\Drupal\migrate_tools\Controller\MessageController::overview'
_title: 'Messages'
requirements:
_permission: 'administer migrations'
@@ -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_plus\Plugin\MigrationConfigEntityPluginManager;
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_plus\Plugin\MigrationConfigEntityPluginManager
*/
protected $migrationConfigEntityPluginManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('database'),
$container->get('plugin.manager.config_entity_migration')
);
}
/**
* Constructs a MessageController object.
*
* @param \Drupal\Core\Database\Connection $database
* A database connection.
* @param \Drupal\migrate_plus\Plugin\MigrationConfigEntityPluginManager $migration_config_entity_plugin_manager
* The plugin manager for config entity-based migrations.
*/
public function __construct(Connection $database, MigrationConfigEntityPluginManager $migration_config_entity_plugin_manager) {
$this->database = $database;
$this->migrationConfigEntityPluginManager = $migration_config_entity_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 string $migration_group
* Machine name of the migration's group.
*
* @param string $migration
* Machine name of the migration.
*
* @return array
* A render array as expected by drupal_render().
*/
public function overview($migration_group, $migration) {
$rows = [];
$classes = static::getLogLevelClassMap();
/** @var MigrationInterface $migration */
$migration = $this->migrationConfigEntityPluginManager->createInstance($migration);
$source_id_field_names = array_keys($migration->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->getIdMap()->messageTableName();
$map_table = $migration->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;
}
}
@@ -0,0 +1,262 @@
<?php
namespace Drupal\migrate_tools\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\Component\Utility\Xss;
use Drupal\Component\Utility\Html;
use Drupal\migrate_plus\Plugin\MigrationConfigEntityPluginManager;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Returns responses for migrate_tools migration view routes.
*/
class MigrationController extends ControllerBase implements ContainerInjectionInterface {
/**
* Plugin manager for migration plugins.
*
* @var \Drupal\migrate_plus\Plugin\MigrationConfigEntityPluginManager
*/
protected $migrationConfigEntityPluginManager;
/**
* Constructs a new MigrationController object.
*
* @param \Drupal\migrate_plus\Plugin\MigrationConfigEntityPluginManager $migration_config_entity_plugin_manager
* The plugin manager for config entity-based migrations.
*/
public function __construct(MigrationConfigEntityPluginManager $migration_config_entity_plugin_manager) {
$this->migrationConfigEntityPluginManager = $migration_config_entity_plugin_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.config_entity_migration')
);
}
/**
* Displays an overview of a migration entity.
*
* @param string $migration_group
* Machine name of the migration's group.
* @param string $migration
* Machine name of the migration.
*
* @return array
* A render array as expected by drupal_render().
*/
public function overview($migration_group, $migration) {
/** @var MigrationInterface $migration */
$migration = $this->migrationConfigEntityPluginManager->createInstance($migration);
$build['overview'] = [
'#type' => 'fieldset',
'#title' => $this->t('Overview'),
];
$build['overview']['group'] = [
'#title' => $this->t('Group:'),
'#markup' => Xss::filterAdmin($migration_group),
'#type' => 'item',
];
$build['overview']['description'] = [
'#title' => $this->t('Description:'),
'#markup' => Xss::filterAdmin($migration->label()),
'#type' => 'item',
];
$migration_dependencies = $migration->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 string $migration_group
* Machine name of the migration's group.
* @param string $migration
* Machine name of the migration.
*
* @return array
* A render array as expected by drupal_render().
*/
public function source($migration_group, $migration) {
/** @var MigrationInterface $migration */
$migration = $this->migrationConfigEntityPluginManager->createInstance($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',
],
];
$source = $migration->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) 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;
}
/**
* Display process information of a migration entity.
*
* @param string $migration_group
* Machine name of the migration's group.
* @param string $migration
* Machine name of the migration.
*
* @return array
* A render array as expected by drupal_render().
*/
public function process($migration_group, $migration) {
/** @var MigrationInterface $migration */
$migration = $this->migrationConfigEntityPluginManager->createInstance($migration);
// 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->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.'),
];
return $build;
}
/**
* Displays destination information of a migration entity.
*
* @param string $migration_group
* Machine name of the migration's group.
* @param string $migration
* Machine name of the migration.
*
* @return array
* A render array as expected by drupal_render().
*/
public function destination($migration_group, $migration) {
/** @var MigrationInterface $migration */
$migration = $this->migrationConfigEntityPluginManager->createInstance($migration);
// 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->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;
}
}
@@ -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 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;
}
}
@@ -0,0 +1,211 @@
<?php
namespace Drupal\migrate_tools\Controller;
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\Routing\CurrentRouteMatch;
use Drupal\migrate_plus\Entity\MigrationGroup;
use Drupal\migrate_plus\Plugin\MigrationConfigEntityPluginManager;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Datetime\DateFormatter;
/**
* 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_plus\Plugin\MigrationConfigEntityPluginManager
*/
protected $migrationConfigEntityPluginManager;
/**
* 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_plus\Plugin\MigrationConfigEntityPluginManager $migration_config_entity_plugin_manager
* The plugin manager for config entity-based migrations.
*/
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, CurrentRouteMatch $current_route_match, MigrationConfigEntityPluginManager $migration_config_entity_plugin_manager) {
parent::__construct($entity_type, $storage);
$this->currentRouteMatch = $current_route_match;
$this->migrationConfigEntityPluginManager = $migration_config_entity_plugin_manager;
}
/**
* {@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.config_entity_migration')
);
}
/**
* 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');
return $header; // + parent::buildHeader();
}
/**
* 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
* A render array of the table row for displaying the plugin information.
*
* @see Drupal\Core\Entity\EntityListController::render()
*/
public function buildRow(EntityInterface $migration_entity) {
$migration = $this->migrationConfigEntityPluginManager->createInstance($migration_entity->id());
$migration_group = $migration->get('migration_group');
if (!$migration_group) {
$migration_group = 'default';
}
$route_parameters = array(
'migration_group' => $migration_group,
'migration' => $migration->id(),
);
$row['label'] = array(
'data' => array(
'#type' => 'link',
'#title' => $migration->label(),
'#url' => Url::fromRoute("entity.migration.overview", $route_parameters),
),
);
$row['machine_name'] = $migration->id();
$row['status'] = $migration->getStatusLabel();
// 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'] = array(
'data' => array(
'#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 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'] = '';
}
return $row; // + parent::buildRow($migration_entity);
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
$operations = parent::getDefaultOperations($entity);
$migration_group = $entity->get('migration_group');
if (!$migration_group) {
$migration_group = 'default';
}
// $this->addGroupParameter($operations['edit']['url'], $migration_group);
// $this->addGroupParameter($operations['delete']['url'], $migration_group);
return $operations;
}
/**
* @param \Drupal\Core\Url $url
* The URL associated with an operation.
*
* @param $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);
}
}
@@ -0,0 +1,23 @@
<?php
namespace Drupal\migrate_tools;
use Drupal\migrate\MigrateMessageInterface;
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);
}
}
@@ -0,0 +1,38 @@
<?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);
// $actions['submit']['#value'] = $this->t('Create Migration');
unset($actions['submit']);
return $actions;
}
}
@@ -0,0 +1,71 @@
<?php
namespace Drupal\migrate_tools\Form;
use Drupal\Core\Entity\EntityConfirmFormBase;
use Drupal\Core\Url;
use Drupal\Core\Form\FormStateInterface;
/**
* Class MigrationDeleteForm.
*
* @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?', array(
'%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.', array(
'%label' => $this->entity->label(),
)));
// Redirect the user to the list controller when complete.
$form_state->setRedirectUrl($this->getCancelUrl());
}
}
@@ -0,0 +1,51 @@
<?php
namespace Drupal\migrate_tools\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Class MigrationEditForm
*
* 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;
}
/**
* @param \Drupal\Core\Url $url
* The URL associated with an operation.
*
* @param $migration_group
* The migration's parent group.
*/
protected function addGroupParameter(Url $url, $migration_group) {
$route_parameters = $url->getRouteParameters() + array('migration_group' => $migration_group);
$url->setRouteParameters($route_parameters);
}
}
@@ -0,0 +1,195 @@
<?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;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Class MigrationFormBase.
*
* @package Drupal\migrate_tools\Form
*
* @ingroup migrate_tools
*/
class MigrationFormBase extends EntityForm {
/**
* @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;
}
/**
* Factory method for MigrationFormBase.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* A container interface service.
*
* @return \Drupal\migrate_tools\Form\MigrationGroupFormBase
*/
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 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'] = array(
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#maxlength' => 255,
'#default_value' => $migration->label(),
'#required' => TRUE,
);
$form['id'] = array(
'#type' => 'machine_name',
'#title' => $this->t('Machine name'),
'#default_value' => $migration->id(),
'#machine_name' => array(
'exists' => array($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'] = array(
'#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 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;
}
/**
* Overrides Drupal\Core\Entity\EntityFormController::save().
*
* @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 $this
*/
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.', array('%label' => $migration->label())));
}
else {
// If we created a new entity...
drupal_set_message($this->t('Migration %label has been added.', array('%label' => $migration->label())));
}
// Redirect the user back to the listing route after the save operation.
$form_state->setRedirect('entity.migration.list',
array('migration_group' => $migration->get('migration_group')));
}
}
@@ -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;
}
}
@@ -0,0 +1,71 @@
<?php
namespace Drupal\migrate_tools\Form;
use Drupal\Core\Entity\EntityConfirmFormBase;
use Drupal\Core\Url;
use Drupal\Core\Form\FormStateInterface;
/**
* Class MigrationGroupDeleteForm.
*
* @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?', array(
'%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.', array(
'%label' => $this->entity->label(),
)));
// Redirect the user to the list controller when complete.
$form_state->setRedirectUrl($this->getCancelUrl());
}
}
@@ -0,0 +1,37 @@
<?php
namespace Drupal\migrate_tools\Form;
use Drupal\Core\Form\FormStateInterface;
/**
* Class MigrationGroupEditForm
*
* 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;
}
}
@@ -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 Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\migrate_plus\Entity\MigrationGroupInterface;
/**
* Class MigrationGroupFormBase.
*
* @package Drupal\migrate_tools\Form
*
* @ingroup migrate_tools
*/
class MigrationGroupFormBase extends EntityForm {
/**
* @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;
}
/**
* Factory method for MigrationGroupFormBase.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* A container interface service.
*
* @return \Drupal\migrate_tools\Form\MigrationFormBase
*
*/
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 MigrationGroupInterface $migration_group */
$migration_group = $this->entity;
// Build the form.
$form['label'] = array(
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#maxlength' => 255,
'#default_value' => $migration_group->label(),
'#required' => TRUE,
);
$form['id'] = array(
'#type' => 'machine_name',
'#title' => $this->t('Machine name'),
'#default_value' => $migration_group->id(),
'#machine_name' => array(
'exists' => array($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'] = array(
'#type' => 'textfield',
'#title' => $this->t('Description'),
'#maxlength' => 255,
'#default_value' => $migration_group->get('description'),
);
$form['source_type'] = array(
'#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 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;
}
/**
* Overrides Drupal\Core\Entity\EntityFormController::save().
*
* @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 $this
*/
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.', array('%label' => $migration_group->label())));
}
else {
// If we created a new entity...
drupal_set_message($this->t('Migration group %label has been added.', array('%label' => $migration_group->label())));
}
// Redirect the user back to the listing route after the save operation.
$form_state->setRedirect('entity.migration_group.list');
}
}
@@ -0,0 +1,365 @@
<?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;
class MigrateExecutable extends MigrateExecutableBase {
/**
* Counters of map statuses.
*
* @var array
* Set of counters, keyed by MigrateIdMapInterface::STATUS_* constant.
*/
protected $saveCounters = array(
MigrateIdMapInterface::STATUS_FAILED => 0,
MigrateIdMapInterface::STATUS_IGNORED => 0,
MigrateIdMapInterface::STATUS_IMPORTED => 0,
MigrateIdMapInterface::STATUS_NEEDS_UPDATE => 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'])) {
$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();
// 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
*/
public function getCreatedCount() {
return $this->saveCounters[MigrateIdMapInterface::STATUS_IMPORTED];
}
/**
* Return the number of items updated.
*
* @return int
*/
public function getUpdatedCount() {
return $this->saveCounters[MigrateIdMapInterface::STATUS_NEEDS_UPDATE];
}
/**
* Return the number of items ignored.
*
* @return int
*/
public function getIgnoredCount() {
return $this->saveCounters[MigrateIdMapInterface::STATUS_IGNORED];
}
/**
* Return the number of items that failed.
*
* @return int
*/
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
*/
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
*/
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 since the last feedback (or the
* beginning of this migration).
*
* @param bool $done
*/
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,
array('@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 since the last feedback (or the
* beginning of this migration).
*
* @param bool $done
*/
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,
array('@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->getProcessedCount() + 1) >= $this->itemLimit) {
$event->getMigration()->interruptMigration(MigrationInterface::RESULT_COMPLETED);
}
}
}