acquia_commercemanager-8.x-1.122/modules/acm_sku/src/Commands/AcmSkuDrushCommands.php
modules/acm_sku/src/Commands/AcmSkuDrushCommands.php
<?php namespace Drupal\acm_sku\Commands; use Drupal\acm\Connector\APIWrapperInterface; use Drupal\acm\Connector\IngestAPIWrapper; use Drupal\acm\I18nHelper; use Drupal\acm_sku\CategoryManager; use Drupal\acm_sku\Entity\SKU; use Drupal\acm_sku\Plugin\rest\resource\ProductSyncResource; use Drupal\acm_sku\ProductOptionsManager; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\CacheTagsInvalidatorInterface; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Database\Connection; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\Query\QueryFactory; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Logger\LoggerChannelFactoryInterface; use Drupal\taxonomy\TermInterface; use Drush\Commands\DrushCommands; use Drush\Exceptions\UserAbortException; /** * Class AcmSkuDrushCommands. * * @package Drupal\acm_sku\Commands */ class AcmSkuDrushCommands extends DrushCommands { const DELETE_BATCH_COUNT = 200; /** * Api Wrapper. * * @var \Drupal\acm\Connector\APIWrapperInterface */ private $apiWrapper; /** * I18n Helper. * * @var \Drupal\acm\I18nHelper */ private $i18nhelper; /** * Ingest Api Wrapper. * * @var \Drupal\acm\Connector\IngestAPIWrapper */ private $ingestApiWrapper; /** * ACM Category manager service. * * @var \Drupal\acm_sku\CategoryManager */ private $acmCategoryManager; /** * Product options manager. * * @var \Drupal\acm_sku\ProductOptionsManager */ private $productOptionsManager; /** * Database Connection. * * @var \Drupal\Core\Database\Connection */ private $connection; /** * Entity Type Manager. * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ private $entityTypeManager; /** * Query Factory. * * @var \Drupal\Core\Entity\Query\QueryFactory */ private $queryFactory; /** * Entity Manager. * * @var \Drupal\Core\Entity\EntityManagerInterface */ private $entityManager; /** * Language Manager. * * @var \Drupal\Core\Language\LanguageManagerInterface */ private $languageManager; /** * Module Handler. * * @var \Drupal\Core\Extension\ModuleHandlerInterface */ private $moduleHandler; /** * Linked SKU cache service. * * @var \Drupal\Core\Cache\CacheBackendInterface */ private $linkedSkuCache; /** * Cache Tags Invalidator. * * @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface */ private $cacheTagsInvalidator; /** * Product Node Type - default: acm_product. * * @var string */ private $productNodeType; /** * SKU Field Name - default: field_skus. * * @var string */ private $skuFieldName; /** * Category Vocabulary name - default: acm_product_category. * * @var string */ private $categoryVid; /** * AcmSkuDrushCommands constructor. * * @param \Drupal\acm\Connector\APIWrapperInterface $api_wrapper * API Wrapper. * @param \Drupal\acm\I18nHelper $i18n_helper * I18n Helper. * @param \Drupal\acm\Connector\IngestAPIWrapper $ingest_api_wrapper * Ingest API Wrapper. * @param \Drupal\acm_sku\CategoryManager $acm_category_manager * ACM Category Manager. * @param \Drupal\acm_sku\ProductOptionsManager $product_options_manager * Product Options Manager. * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory * Logger Factory. * @param \Drupal\Core\Database\Connection $connection * Database Connection. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * Entity Type Manager. * @param \Drupal\Core\Entity\Query\QueryFactory $query_factory * Query Factory. * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager * Entity Manager. * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager * Language Manager. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * Module Handler. * @param \Drupal\Core\Cache\CacheBackendInterface $linked_sku_cache * Linked SKU cache service. * @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tags_invalidator * Cache Tags Invalidator. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * Config Factory. */ public function __construct(APIWrapperInterface $api_wrapper, I18nHelper $i18n_helper, IngestAPIWrapper $ingest_api_wrapper, CategoryManager $acm_category_manager, ProductOptionsManager $product_options_manager, LoggerChannelFactoryInterface $logger_factory, Connection $connection, EntityTypeManagerInterface $entity_type_manager, QueryFactory $query_factory, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager, ModuleHandlerInterface $module_handler, CacheBackendInterface $linked_sku_cache, CacheTagsInvalidatorInterface $cache_tags_invalidator, ConfigFactoryInterface $config_factory) { parent::__construct(); $this->apiWrapper = $api_wrapper; $this->i18nhelper = $i18n_helper; $this->ingestApiWrapper = $ingest_api_wrapper; $this->acmCategoryManager = $acm_category_manager; $this->productOptionsManager = $product_options_manager; $this->logger = $logger_factory->get('AcmSkuDrushCommands'); $this->connection = $connection; $this->entityTypeManager = $entity_type_manager; $this->queryFactory = $query_factory; $this->entityManager = $entity_manager; $this->languageManager = $language_manager; $this->moduleHandler = $module_handler; $this->linkedSkuCache = $linked_sku_cache; $this->cacheTagsInvalidator = $cache_tags_invalidator; $config = $config_factory->get('acm.connector'); $this->productNodeType = $config->get('product_node_type'); $this->skuFieldName = $config->get('sku_field_name'); $this->categoryVid = $config->get('category_vid'); } /** * Run a full synchronization of all commerce product records. * * @param string $langcode * Sync products available in this langcode. * @param string $page_size * Number of items to be synced in one batch. * @param array $options * Command options. * * @command acm_sku:sync-products * * @option skus SKUs to import (like query). * @option category_id Magento category id to sync the products for. * * @validate-module-enabled acm_sku * * @aliases acsp,sync-commerce-products * * @usage drush acsp en 50 * Run a full product synchronization of all available products in store * linked to en and page size 50. * @usage drush acsp en 50 --skus=\'M-H3495 130 2 FW\',\'M-H3496 130 004FW\',\'M-H3496 130 005FW\'' * Synchronize sku data for the skus M-H3495 130 2 FW, M-H3496 130 004FW * & M-H3496 130 005FW only in store linked to en and page size 50. * @usage drush acsp en 50 --category_id=1234 * Synchronize sku data for the skus in category with id 1234 only in store * linked to en and page size 50. * * @throws \Drush\Exceptions\UserAbortException */ public function syncProducts($langcode, $page_size, array $options = ['skus' => NULL, 'category_id' => NULL]) { $langcode = strtolower($langcode); $store_id = $this->i18nhelper->getStoreIdFromLangcode($langcode); if (empty($store_id)) { $this->output->writeln(dt("Store id not found for provided language code.")); return; } $page_size = (int) $page_size; if ($page_size <= 0) { $this->output->writeln(dt("Page size must be a positive integer.")); return; } $skus = $options['skus']; $category_id = $options['category_id']; // Apply only one filer at a time. if ($category_id) { $skus = ''; } // Ask for confirmation from user if attempt is to run full sync. if (empty($skus) && empty($category_id)) { $confirmation_text = dt('I CONFIRM'); $input = $this->io()->ask(dt('Are you sure you want to import all products for @language language? If yes, type: "@confirmation"', [ '@language' => $langcode, '@confirmation' => $confirmation_text, ])); if ($input != $confirmation_text) { throw new UserAbortException(dt('Please be more attentive in using this command and prove you are not sleep working...')); } } $this->output->writeln(dt('Requesting all commerce products for selected language code...')); $this->ingestApiWrapper->productFullSync($store_id, $langcode, $skus, $category_id, $page_size); $this->output->writeln(dt('Done.')); } /** * Run a full synchronization of all commerce product category records. * * @command acm_sku:sync-categories * * @validate-module-enabled acm_sku * * @aliases acsc,sync-commerce-cats * * @usage drush acsc * Run a full category synchronization of all available categories. */ public function syncCategories() { $this->output->writeln(dt('Synchronizing all commerce categories, please wait...')); $response = $this->acmCategoryManager->synchronizeTree($this->categoryVid); // We trigger delete only if there is any term update/create. // So if API does not return anything, we don't delete all the categories. if (!empty($response['created']) || !empty($response['updated'])) { // Get all category terms with commerce id. $orphan_categories = $this->acmCategoryManager->getOrphanCategories($response); // If there are categories to delete. if (!empty($orphan_categories)) { // Show `tid + cat name + commerce id` for review. $this->io()->table([ dt('Category Id'), dt('Category Name'), dt('Category Commerce Id'), ], $orphan_categories); // Confirmation to delete old categories. if ($this->io()->confirm(dt('Are you sure you want to clean these old categories'), FALSE)) { // Allow other modules to skipping the deleting of terms. $this->moduleHandler->alter('acm_sku_sync_categories_delete', $orphan_categories); foreach ($orphan_categories as $tid => $rs) { $term = $this->entityTypeManager->getStorage('taxonomy_term')->load($tid); if ($term instanceof TermInterface) { // Delete the term. $term->delete(); } } } } } else { $this->logger->notice(dt('Not cleaning(deleting) old terms as there is no term update/create.')); } $this->output->writeln(dt('Done.')); } /** * Run a full synchronization of all commerce product options. * * @command acm_sku:sync-product-options * * @validate-module-enabled acm_sku * * @aliases acspo,sync-commerce-product-options */ public function syncProductOptions() { $this->logger->notice(dt('Synchronizing all commerce product options, please wait...')); $this->productOptionsManager->synchronizeProductOptions(); $this->logger->notice(dt('Product attribute sync completed.')); } /** * Run a partial synchronization of commerce product records synchronously. * * This is used for testing / dev environments. * * @param int $count * Number of product records to sync. * * @command acm_sku:sync-products-test * * @validate-module-enabled acm_sku * * @aliases acdsp,sync-commerce-products-test * * @usage drush acdsp * Run a partial synchronization of commerce product records synchronously * for testing / dev. */ public function syncProductsTest($count) { $this->output->writeln(dt('Synchronizing @count commerce products for testing / dev...', ['@count' => $count])); $container = \Drupal::getContainer(); foreach ($this->i18nhelper->getStoreLanguageMapping() as $langcode => $store_id) { $this->apiWrapper->updateStoreContext($store_id); $products = $this->apiWrapper->getProducts($count); $product_sync_resource = ProductSyncResource::create($container, [], NULL, NULL); $product_sync_resource->post($products); } } /** * Remove all duplicate categories available in system. * * @command acm_sku:remove-category-duplicates * * @validate-module-enabled acm_sku * * @aliases acccrd,commerce-cats-remove-duplicates * * @usage drush acccrd * Remove all duplicate categories available in system. */ public function removeCategoryDuplicates() { $this->output->writeln(dt('Cleaning all commerce categories, please wait...')); $db = $this->connection; /** @var \Drupal\taxonomy\TermStorageInterface $termStorage */ $termStorage = \Drupal::entityTypeManager()->getStorage('taxonomy_term'); /** @var \Drupal\node\NodeStorageInterface $nodeStorage */ $nodeStorage = \Drupal::entityTypeManager()->getStorage('node'); $query = $db->select('taxonomy_term__field_commerce_id', 'ttfci'); $query->addField('ttfci', 'field_commerce_id_value', 'commerce_id'); $query->groupBy('commerce_id'); $query->having('count(*) > 1'); $result = $query->execute()->fetchAllKeyed(0, 0); if (empty($result)) { $this->output->writeln(dt('No duplicate categories found.')); return; } foreach ($result as $commerce_id) { $this->output->writeln(dt('Duplicate categories found for commerce id: @commerce_id.', [ '@commerce_id' => $commerce_id, ])); $query = $db->select('taxonomy_term__field_commerce_id', 'ttfci'); $query->addField('ttfci', 'entity_id', 'tid'); $query->condition('ttfci.field_commerce_id_value', $commerce_id); $query->orderBy('tid', 'DESC'); $tids = $query->execute()->fetchAllKeyed(0, 0); foreach ($tids as $tid) { $query = $nodeStorage->getQuery(); $query->condition('field_category', $tid); $nodes = $query->execute(); if (empty($nodes)) { $this->output->writeln(dt('No nodes found for tid: @tid for commerce id: @commerce_id. Deleting', [ '@commerce_id' => $commerce_id, '@tid' => $tid, ])); $term = $termStorage->load($tid); $term->delete(); unset($tids[$tid]); // Break the loop if only one left now, we might not have any products // added yet and categories are synced which means there will be no // nodes for any term. if (count($tids) == 1) { break; } } else { $this->output->writeln(dt('@count nodes found for tid: @tid for commerce id: @commerce_id. Not Deleting', [ '@commerce_id' => $commerce_id, '@tid' => $tid, '@count' => count($nodes), ])); } } } $this->output->writeln(dt('Done.')); } /** * Remove all duplicate products available in system. * * @command acm_sku:remove-product-duplicates * * @validate-module-enabled acm_sku * * @aliases accprd,commerce-products-remove-duplicates * * @usage drush accprd * Remove all duplicate products available in system. */ public function removeProductDuplicates() { $this->output->writeln(dt('Removing duplicates in commerce products, please wait...')); $skus_to_sync = []; /** @var \Drupal\node\NodeStorageInterface $nodeStorage */ $nodeStorage = $this->entityTypeManager->getStorage('node'); $query = $this->connection->select('acm_sku_field_data', 't1'); $query->addField('t1', 'id', 'id'); $query->addField('t1', 'sku', 'sku'); $query->leftJoin('acm_sku_field_data', 't2', 't1.sku = t2.sku'); $query->where('t1.id != t2.id'); $result = $query->execute()->fetchAllKeyed(0, 1); if (empty($result)) { $this->output->writeln(dt('No duplicate skus found.')); } else { $skus = []; foreach ($result as $id => $sku) { $skus[$sku][$id] = $id; $skus_to_sync[$sku] = $sku; } foreach ($skus as $sku => $ids) { $this->output->writeln(dt('Duplicate skus found for sku: @sku with ids: @ids.', [ '@sku' => $sku, '@ids' => implode(', ', $ids), ])); // Always delete the one with higher id, first one will have more // translations. sort($ids); // Remove the first id which we don't want to delete. array_shift($ids); foreach ($ids as $id) { $this->output->writeln(dt('Deleting sku with id @id for sku @sku.', [ '@sku' => $sku, '@id' => $id, ])); $sku_entity = SKU::load($id); $sku_entity->delete(); } } } $query = $this->connection->select('node__' . $this->skuFieldName, 't1'); $query->addField('t1', 'entity_id', 'id'); $query->addField('t1', $this->skuFieldName . '_value', 'sku'); $query->leftJoin('node__' . $this->skuFieldName, 't2', 't1.' . $this->skuFieldName . '_value = t2.' . $this->skuFieldName . '_value'); $query->where('t1.entity_id != t2.entity_id'); $result = $query->execute()->fetchAllKeyed(0, 1); if (empty($result)) { $this->output->writeln(dt('No duplicate product nodes found.')); } else { $nids_to_delete = []; $skus = []; foreach ($result as $id => $sku) { $skus[$sku][$id] = $id; $skus_to_sync[$sku] = $sku; } foreach ($skus as $sku => $ids) { $this->output->writeln(dt('Duplicate nodes found for sku: @sku with ids: @ids.', [ '@sku' => $sku, '@ids' => implode(', ', $ids), ])); // Always delete the one with higher nid, first one will have proper // url alias. sort($ids); // Remove the first id which we don't want to delete. array_shift($ids); foreach ($ids as $id) { $this->output->writeln(dt('Deleting node with id @id for sku @sku.', [ '@sku' => $sku, '@id' => $id, ])); $nids_to_delete[$id] = $id; } } if ($nids_to_delete) { $nodeStorage->delete($nodeStorage->loadMultiple($nids_to_delete)); } } if ($skus_to_sync) { $sku_texts = implode(',', $skus_to_sync); $this->output->writeln(dt('Requesting resync for skus @skus.', [ '@skus' => $sku_texts, ])); foreach ($this->i18nhelper->getStoreLanguageMapping() as $langcode => $store_id) { // Using very small page size to avoid any issues for skus which already // had corrupt data. $this->ingestApiWrapper->productFullSync($store_id, $langcode, $sku_texts, NULL, 5); } } $this->output->writeln(dt('Done.')); } /** * Flush all commerce data from the site. * * Handles Products, SKUs, Product Categories and Product Options and allows * more data to be added for cleanup via alter hook. * * @command acm_sku:flush-synced-data * * @validate-module-enabled acm_sku * * @aliases accd,clean-synced-data * * @usage drush accd * Flush all commerce data from the site (Products, SKUs, Product Categories * and Product Options). * * @throws \Drush\Exceptions\UserAbortException */ public function flushSyncedData() { if (!$this->io()->confirm(dt("Are you sure you want to clean commerce data?"))) { throw new UserAbortException(); } $this->output->writeln(dt('Cleaning synced commerce data, please wait...')); // Set batch operation. $batch = [ 'title' => t('Clean synced data'), 'init_message' => t('Cleaning synced commerce data starting...'), 'operations' => [ ['\Drupal\acm_sku\Commands\AcmSkuDrushCommands::skuCleanProcess', []], ], 'progress_message' => t('Processed @current out of @total.'), 'error_message' => t('Synced data could not be cleaned because an error occurred.'), 'finished' => '_acm_sku_clean_finished', ]; batch_set($batch); drush_backend_batch_process(); $this->output->writeln(dt('Synced commerce data cleaned.')); } /** * Function to process entity delete operation. * * @param mixed|array $context * The batch current context. */ public static function skuCleanProcess(&$context) { // Use the $context['sandbox'] at your convenience to store the // information needed to track progression between successive calls. if (empty($context['sandbox'])) { $config = \Drupal::config('acm.connector'); // Get all the entities that need to be deleted. $context['sandbox']['results'] = []; // Get all product entities. $query = \Drupal::entityQuery('node'); $query->condition('type', $config->get('product_node_type')); $product_entities = $query->execute(); foreach ($product_entities as $entity_id) { $context['sandbox']['results'][] = [ 'type' => 'node', 'entity_id' => $entity_id, ]; } // Get all acm_sku entities. $query = \Drupal::entityQuery('acm_sku'); $sku_entities = $query->execute(); foreach ($sku_entities as $entity_id) { $context['sandbox']['results'][] = [ 'type' => 'acm_sku', 'entity_id' => $entity_id, ]; } // Get all taxonomy_term entities. $categories = [$config->get('category_vid'), 'sku_product_option']; $query = \Drupal::entityQuery('taxonomy_term'); $query->condition('vid', $categories, 'IN'); $cat_entities = $query->execute(); foreach ($cat_entities as $entity_id) { $context['sandbox']['results'][] = [ 'type' => 'taxonomy_term', 'entity_id' => $entity_id, ]; } // Allow other modules to add data to be deleted when cleaning up. \Drupal::moduleHandler()->alter('acm_sku_clean_synced_data', $context); $context['sandbox']['progress'] = 0; $context['sandbox']['current_id'] = 0; $context['sandbox']['max'] = count($context['sandbox']['results']); } $results = []; if (isset($context['sandbox']['results']) && !empty($context['sandbox']['results'])) { $results = $context['sandbox']['results']; } $results = array_slice($results, isset($context['sandbox']['current']) ? $context['sandbox']['current'] : 0, self::DELETE_BATCH_COUNT); $delete = []; foreach ($results as $key => $result) { $context['results'][] = $results['type'] . ' : ' . $result['entity_id']; $context['sandbox']['progress']++; $context['sandbox']['current_id'] = $result['entity_id']; $delete[$result['type']][] = $result['entity_id']; // Update our progress information. $context['sandbox']['current']++; } foreach ($delete as $type => $entity_ids) { try { $storage = \Drupal::entityTypeManager()->getStorage($type); $entities = $storage->loadMultiple($entity_ids); $storage->delete($entities); } catch (\Exception $e) { \Drupal::logger('AcmSkuDrushCommands')->error($e->getMessage()); } } $context['message'] = 'Processed ' . $context['sandbox']['progress'] . ' out of ' . $context['sandbox']['max'] . '.'; if ($context['sandbox']['progress'] !== $context['sandbox']['max']) { $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; } } /** * Clear linked SKUs cache. * * @param array $options * Command Options. * * @command acm_sku:clear-linked-skus-cache * * @option sku SKU to clean linked skus cache of. * * @validate-module-enabled acm_sku * * @aliases acclsc,clear-linked-skus-cache * * @usage drush acclsc * Clear linked SKUs cache for all SKUs. * @usage drush acclsc --skus=SKU * Clear linked SKUs cache for particular SKU. * * @throws \Drush\Exceptions\UserAbortException */ public function flushLinkedSkuCache(array $options = ['sku' => NULL]) { // Check if we are asked to clear cache of specific SKU. if (!empty($options['sku'])) { if ($sku_entity = SKU::loadFromSku($options['sku'])) { $this->cacheTagsInvalidator->invalidateTags([ 'acm_sku:linked_skus:' . $sku_entity->id(), 'acm_sku:' . $sku_entity->id(), ]); $this->output->writeln(dt('Invalidated linked SKUs cache for @sku.', [ '@sku' => $options['sku'], ])); } return; } if (!$this->io()->confirm(dt('Are you sure you want to clear linked SKUs cache for all SKUs?'))) { throw new UserAbortException(); } $this->linkedSkuCache->deleteAll(); $this->output->writeln(dt('Cleared all linked SKUs cache.')); } }