content_sync-8.x-2.x-dev/src/Drush/Commands/ContentSyncDrushCommands.php
src/Drush/Commands/ContentSyncDrushCommands.php
<?php
namespace Drupal\content_sync\Drush\Commands;
use Drupal\content_sync\Content\ContentStorageComparer;
use Drupal\content_sync\ContentSyncManagerInterface;
use Drupal\content_sync\Exporter\ContentExporterInterface;
use Drupal\content_sync\Form\ContentExportTrait;
use Drupal\content_sync\Form\ContentImportTrait;
use Drupal\Core\Config\ConfigManagerInterface;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ModuleInstallerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drush\Attributes as CLI;
use Drush\Commands\DrushCommands;
use Drush\Exceptions\UserAbortException;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Provides drush commands for importing and exporting content.
*/
class ContentSyncDrushCommands extends DrushCommands {
use ContentExportTrait;
use ContentImportTrait;
use DependencySerializationTrait;
use StringTranslationTrait;
/**
* The configuration manager.
*
* @var \Drupal\Core\Config\ConfigManagerInterface
*/
protected $configManager;
/**
* The content storage.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $contentStorage;
/**
* The content sync storage.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $contentStorageSync;
/**
* The content sync manager.
*
* @var \Drupal\content_sync\ContentSyncManagerInterface
*/
protected $contentSyncManager;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The content exporter.
*
* @var \Drupal\content_sync\Exporter\ContentExporterInterface
*/
protected $contentExporter;
/**
* The lock information for this configuration.
*
* @var \Drupal\Core\TempStore\Lock|null
*/
protected $lock;
/**
* The typed config manager.
*
* @var \Drupal\Core\Config\TypedConfigManagerInterface
*/
protected $typedConfigManager;
/**
* The module installer.
*
* @var \Drupal\Core\Extension\ModuleInstallerInterface
*/
protected $moduleInstaller;
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* The string translation service.
*
* @var \Drupal\Core\StringTranslation\TranslationInterface
*/
protected $stringTranslation;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* Gets the configManager.
*
* @return \Drupal\Core\Config\ConfigManagerInterface
* The configManager.
*/
public function getConfigManager() {
return $this->configManager;
}
/**
* Gets the contentStorage.
*
* @return \Drupal\Core\Config\StorageInterface
* The contentStorage.
*/
public function getContentStorage() {
return $this->contentStorage;
}
/**
* Gets the contentStorageSync.
*
* @return \Drupal\Core\Config\StorageInterface
* The contentStorageSync.
*/
public function getContentStorageSync() {
return $this->contentStorageSync;
}
/**
* {@inheritdoc}
*/
protected function getEntityTypeManager() {
return $this->entityTypeManager;
}
/**
* {@inheritdoc}
*/
protected function getContentExporter() {
return $this->contentExporter;
}
/**
* {@inheritdoc}
*/
protected function getExportLogger() {
return $this->logger('content_sync');
}
/**
* ContentSyncCommands constructor.
*
* @param \Drupal\Core\Config\ConfigManagerInterface $configManager
* The configManager.
* @param \Drupal\Core\Config\StorageInterface $contentStorage
* The contentStorage.
* @param \Drupal\Core\Config\StorageInterface $contentStorageSync
* The contentStorageSync.
* @param \Drupal\content_sync\ContentSyncManagerInterface $contentSyncManager
* The contentSyncManager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entityTypeManager.
* @param \Drupal\content_sync\Exporter\ContentExporterInterface $content_exporter
* The contentExporter.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* The moduleHandler.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
* The eventDispatcher.
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock.
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
* The typedConfigManager.
* @param \Drupal\Core\Extension\ModuleInstallerInterface $moduleInstaller
* The moduleInstaller.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $themeHandler
* The themeHandler.
* @param \Drupal\Core\StringTranslation\TranslationInterface $stringTranslation
* The stringTranslation.
*/
public function __construct(ConfigManagerInterface $configManager, StorageInterface $contentStorage, StorageInterface $contentStorageSync, ContentSyncManagerInterface $contentSyncManager, EntityTypeManagerInterface $entity_type_manager, ContentExporterInterface $content_exporter, ModuleHandlerInterface $moduleHandler, EventDispatcherInterface $eventDispatcher, LockBackendInterface $lock, TypedConfigManagerInterface $typedConfigManager, ModuleInstallerInterface $moduleInstaller, ThemeHandlerInterface $themeHandler, TranslationInterface $stringTranslation) {
parent::__construct();
$this->configManager = $configManager;
$this->contentStorage = $contentStorage;
$this->contentStorageSync = $contentStorageSync;
$this->contentSyncManager = $contentSyncManager;
$this->entityTypeManager = $entity_type_manager;
$this->contentExporter = $content_exporter;
$this->moduleHandler = $moduleHandler;
$this->eventDispatcher = $eventDispatcher;
$this->lock = $lock;
$this->typedConfigManager = $typedConfigManager;
$this->moduleInstaller = $moduleInstaller;
$this->themeHandler = $themeHandler;
$this->stringTranslation = $stringTranslation;
}
/**
* Create a new instance of the class.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The container.
*
* @return static
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.manager'),
$container->get('content.storage'),
$container->get('content.storage.sync'),
$container->get('content_sync.manager'),
$container->get('entity_type.manager'),
$container->get('content_sync.exporter'),
$container->get('module_handler'),
$container->get('event_dispatcher'),
$container->get('lock'),
$container->get('config.typed'),
$container->get('module_installer'),
$container->get('theme_handler'),
$container->get('string_translation')
);
}
/**
* Import content from a content directory.
*/
#[CLI\Command(name: 'content-sync:import', aliases: [
'csi',
'content-sync-import',
])]
#[CLI\Argument(name: 'label', description: 'A content directory label (i.e. a key in $content_directories array in settings.php).')]
#[CLI\Option(name: 'entity-types', description: 'A list of entity type names separated by commas.')]
#[CLI\Option(name: 'uuids', description: 'A list of UUIDs separated by commas.')]
#[CLI\Option(name: 'actions', description: 'A list of Actions separated by commas.')]
#[CLI\Option(name: 'skiplist', description: 'Skip the change list before proceed with the import.')]
#[CLI\Usage(name: 'drush content-sync-import', description: 'Usage description')]
public function import($label = NULL, array $options = [
'entity-types' => '',
'uuids' => '',
'actions' => '',
'skiplist' => FALSE,
'compare-dates' => FALSE,
]) {
// Generate comparer with filters.
$storage_comparer = new ContentStorageComparer($this->contentStorageSync, $this->contentStorage);
$change_list = [];
$collections = $storage_comparer->getAllCollectionNames();
if (!empty($options['entity-types'])) {
$entity_types = explode(',', $options['entity-types']);
$match_collections = [];
foreach ($entity_types as $entity_type) {
$match_collections = $match_collections + preg_grep('/^' . $entity_type . '/', $collections);
}
$collections = $match_collections;
}
foreach ($collections as $collection) {
if (!empty($options['uuids'])) {
$storage_comparer->createChangelistbyCollectionAndNames($collection, $options['uuids']);
}
elseif ($options['compare-dates']) {
$storage_comparer->createChangelistbyCollection($collection, TRUE);
}
else {
$storage_comparer->createChangelistbyCollection($collection);
}
if (!empty($options['actions'])) {
$actions = explode(',', $options['actions']);
foreach ($actions as $op) {
if (in_array($op, ['create', 'update', 'delete'])) {
$change_list[$collection][$op] = $storage_comparer->getChangelist($op, $collection);
}
}
}
else {
$change_list[$collection] = $storage_comparer->getChangelist(NULL, $collection);
}
$change_list = array_map('array_filter', $change_list);
$change_list = array_filter($change_list);
}
unset($change_list['']);
if (empty($change_list)) {
$this->logger()->notice(dt('Nothing to import, the active content is identical to the content in files.'));
return;
}
// Display the change list.
if (empty($options['skiplist'])) {
// Show differences.
$this->output()
->writeln("Differences of the export directory to the active content:\n");
// Print a table with changes in color.
$table = self::contentChangesTable($change_list, $this->output());
$table->render();
// Ask to continue.
if (!$this->io()
->confirm(dt('Do you want to import?'))) {
throw new UserAbortException();
}
}
// Process import data.
$content_to_sync = [];
$content_to_delete = [];
foreach ($change_list as $collection => $actions) {
if (!empty($actions['create'])) {
$content_to_sync = array_merge($content_to_sync, $actions['create']);
}
if (!empty($actions['update'])) {
$content_to_sync = array_merge($content_to_sync, $actions['update']);
}
if (!empty($actions['delete'])) {
$content_to_delete = $actions['delete'];
}
}
// Set the Import Batch.
if (!empty($content_to_sync) || !empty($content_to_delete)) {
$batch = $this->generateImportBatch($content_to_sync,
$content_to_delete);
batch_set($batch);
drush_backend_batch_process();
}
}
/**
* Export Drupal content to a directory.
*/
#[CLI\Command(name: 'content-sync:export', aliases: [
'cse',
'content-sync-export',
])]
#[CLI\Argument(name: 'label', description: 'A content directory label (i.e. a key in $content_directories array in settings.php).')]
#[CLI\Option(name: 'entity-types', description: 'A list of entity type names separated by commas.')]
#[CLI\Option(name: 'uuids', description: 'A list of UUIDs separated by commas.')]
#[CLI\Option(name: 'actions', description: 'A list of Actions separated by commas.')]
#[CLI\Option(name: 'files', description: 'A value none/base64/folder - default folder.')]
#[CLI\Option(name: 'include-dependencies', description: 'Export content dependencies.')]
#[CLI\Option(name: 'skiplist', description: 'Skip the change list before proceed with the export.')]
#[CLI\Usage(name: 'drush content-sync-export', description: 'Usage description')]
public function export($label = NULL, array $options = [
'entity-types' => '',
'uuids' => '',
'actions' => '',
'files' => '',
'include-dependencies' => FALSE,
'skiplist' => FALSE,
'compare-dates' => FALSE,
'force' => FALSE,
]) {
// Don't rely on the snapshot/diffs
if (!empty($options['force'])){
$entities_allowed = [];
if (!empty($options['entity-types'])){
$entity_allowed = explode(',', $options['entity-types']);
foreach ($entity_allowed as $key => $entity){
list($type,$bundle) = explode('.', $entity);
$entities_allowed[$type][] = $bundle;
}
}
//Set batch operations by entity type/bundle
$entities_list = [];
$entity_type_definitions = $this->entityTypeManager->getDefinitions();
$entities_types_allowed = array_keys($entities_allowed);
foreach ($entity_type_definitions as $entity_type => $definition) {
$reflection = new \ReflectionClass($definition->getClass());
if ($reflection->implementsInterface(ContentEntityInterface::class)) {
if (empty($options['entity-types']) || in_array($entity_type, $entities_types_allowed)) {
$storage = $this->entityTypeManager->getStorage($entity_type);
$query = $storage->getQuery();
$query->accessCheck(FALSE);
if(!empty($options['entity-types'])){
$bundles_allowed = array_filter($entities_allowed[$entity_type]);
$query->condition('type', $bundles_allowed, 'in');
}
if (!empty($options['uuids'])){
$uuids = explode(',', $options['uuids']);
$query->condition('uuid', $uuids, 'in');
}
$entities = $query->execute();
foreach ($entities as $entity_id) {
$entities_list[] = [
'entity_type' => $entity_type,
'entity_id' => $entity_id,
];
}
}
}
}
}
else {
// Generate comparer with filters.
$storage_comparer = new ContentStorageComparer($this->contentStorage, $this->contentStorageSync);
$change_list = [];
$collections = $storage_comparer->getAllCollectionNames();
if (!empty($options['entity-types'])) {
$entity_types = explode(',', $options['entity-types']);
$match_collections = [];
foreach ($entity_types as $entity_type) {
$match_collections = $match_collections + preg_grep('/^' . $entity_type . '/', $collections);
}
$collections = $match_collections;
}
foreach ($collections as $collection) {
if (!empty($options['uuids'])) {
$storage_comparer->createChangelistbyCollectionAndNames($collection, $options['uuids']);
}
else {
$storage_comparer->createChangelistbyCollection($collection);
}
if (!empty($options['actions'])) {
$actions = explode(',', $options['actions']);
foreach ($actions as $op) {
if (in_array($op, ['create', 'update', 'delete'])) {
$change_list[$collection][$op] = $storage_comparer->getChangelist($op, $collection);
}
}
}
elseif ($options['compare-dates']) {
$storage_comparer->createChangelistbyCollection($collection, TRUE);
}
else {
$change_list[$collection] = $storage_comparer->getChangelist(NULL, $collection);
}
$change_list = array_map('array_filter', $change_list);
$change_list = array_filter($change_list);
}
unset($change_list['']);
if (empty($change_list)) {
$this->logger()->notice(dt('Nothing to export, the active content is identical to the content in files.'));
return;
}
// Display the change list.
if (empty($options['skiplist'])) {
// Show differences.
$this->output()
->writeln("Differences of the active content to the export directory:\n");
// Print a table with changes in color.
$table = self::contentChangesTable($change_list, $this->output());
$table->render();
// Ask to continue.
if (!$this->io()
->confirm(dt('Do you want to export?'))) {
throw new UserAbortException();
}
}
// Process the Export.
$entities_list = [];
foreach ($change_list as $collection => $changes) {
// $storage_comparer->getTargetStorage($collection)->deleteAll();
foreach ($changes as $change => $contents) {
switch ($change) {
case 'delete':
foreach ($contents as $content) {
$storage_comparer->getTargetStorage($collection)
->delete($content);
}
break;
case 'update':
case 'create':
foreach ($contents as $content) {
$entity = explode('.', $content);
$entities_list[] = [
'entity_type' => $entity[0],
'entity_uuid' => $entity[2],
];
}
break;
}
}
}
}
// Files options.
$include_files = self::processFilesOption($options);
// Set the Export Batch.
if (!empty($entities_list)) {
$batch = $this->generateExportBatch($entities_list,
[
'export_type' => 'folder',
'include_files' => $include_files,
'include_dependencies' => $options['include-dependencies'],
]);
batch_set($batch);
drush_backend_batch_process();
}
}
/**
* Builds a table of content changes.
*
* @param array $content_changes
* An array of changes keyed by collection.
* @param \Symfony\Component\Console\Output\OutputInterface $output
* The output.
* @param bool $use_color
* If it should use color.
*
* @return \Symfony\Component\Console\Helper\Table
* A Symfony table object.
*/
public static function contentChangesTable(array $content_changes, OutputInterface $output, $use_color = TRUE) {
$rows = [];
foreach ($content_changes as $collection => $changes) {
if (is_array($changes)) {
foreach ($changes as $change => $contents) {
switch ($change) {
case 'delete':
$colour = '<fg=white;bg=red>';
break;
case 'update':
$colour = '<fg=black;bg=yellow>';
break;
case 'create':
$colour = '<fg=white;bg=green>';
break;
default:
$colour = "<fg=black;bg=cyan>";
break;
}
if ($use_color) {
$prefix = $colour;
$suffix = '</>';
}
else {
$prefix = $suffix = '';
}
foreach ($contents as $content) {
$rows[] = [
$collection,
$content,
$prefix . ucfirst($change) . $suffix,
];
}
}
}
}
$table = new Table($output);
$table->setHeaders(['Collection', 'Content Name', 'Operation']);
$table->addRows($rows);
return $table;
}
/**
* Processes 'files' option.
*
* @param array $options
* The command options.
*
* @return string
* Processed 'files' option value.
*/
public static function processFilesOption(array $options) {
$include_files = !empty($options['files']) ? $options['files'] : 'folder';
if (!in_array($include_files, ['folder', 'base64'])) {
$include_files = 'none';
}
return $include_files;
}
}
