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; } }