acquia_commercemanager-8.x-1.122/modules/acm_sku/src/ProductManager.php

modules/acm_sku/src/ProductManager.php
<?php

namespace Drupal\acm_sku;

use Drupal\acm\I18nHelper;
use Drupal\acm_sku\Entity\SKU;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\node\NodeInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Drupal\acm_sku\Event\AcmSkuValidateEvent;

/**
 * Class ProductManager.
 *
 * @ingroup acm_sku
 */
class ProductManager implements ProductManagerInterface {

  /**
   * Drupal Entity Type Manager Instance.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  private $entityManager;

  /**
   * Drupal Config Factory Instance.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  private $configFactory;

  /**
   * Logger instance.
   *
   * @var \Psr\Log\LoggerInterface
   */
  private $logger;

  /**
   * Category Repository.
   *
   * @var \Drupal\acm_sku\CategoryRepositoryInterface
   */
  private $categoryRepo;

  /**
   * Product Options Manager service instance.
   *
   * @var \Drupal\acm_sku\ProductOptionsManager
   */
  private $productOptionsManager;

  /**
   * Instance of I18nHelper service.
   *
   * @var \Drupal\acm\I18nHelper
   */
  private $i18nHelper;

  /**
   * SKU Fields Manager.
   *
   * @var \Drupal\acm_sku\SKUFieldsManager
   */
  private $skuFieldsManager;

  /**
   * Module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  private $moduleHandler;

  /**
   * Event Dispatcher.
   *
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
   */
  private $eventDispatcher;

  /**
   * True if you want extra logging for debugging.
   *
   * @var bool
   */
  private $debug;

  private $failed;
  private $ignored;
  private $deleted;
  private $created;
  private $updated;
  private $failedSkus;
  private $createdSkus;
  private $ignoredSkus;
  private $deletedSkus;
  private $updatedSkus;
  private $debugDir;

  /**
   * Construct.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
   *   The logger channel factory.
   * @param \Drupal\acm_sku\CategoryRepositoryInterface $cat_repo
   *   Category Repository instance.
   * @param \Drupal\acm_sku\ProductOptionsManager $product_options_manager
   *   Product Options Manager service instance.
   * @param \Drupal\acm\I18nHelper $i18nHelper
   *   Instance of I18nHelper service.
   * @param \Drupal\acm_sku\SKUFieldsManager $sku_fields_manager
   *   SKU Fields Manager.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
   *   Module handler.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
   *   Event dispatcher object.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager,
                              ConfigFactoryInterface $config_factory,
                              LoggerChannelFactoryInterface $logger_factory,
                              CategoryRepositoryInterface $cat_repo,
                              ProductOptionsManager $product_options_manager,
                              I18nHelper $i18nHelper,
                              SKUFieldsManager $sku_fields_manager,
                              ModuleHandlerInterface $moduleHandler,
                              EventDispatcherInterface $event_dispatcher) {
    $this->entityManager = $entity_type_manager;
    $this->configFactory = $config_factory;
    $this->logger = $logger_factory->get('acm');
    $this->categoryRepo = $cat_repo;
    $this->productOptionsManager = $product_options_manager;
    $this->i18nHelper = $i18nHelper;
    $this->skuFieldsManager = $sku_fields_manager;
    $this->moduleHandler = $moduleHandler;
    $this->eventDispatcher = $event_dispatcher;

    $this->debug = $this->configFactory->get('acm.connector')->get('debug');
    $this->debugDir = $this->configFactory->get('acm.connector')->get('debug_dir');
  }

  /**
   * Write to the log only if the debug flag is set true.
   *
   * @param string $message
   *   The message to write to the log.
   * @param array $context
   *   Optional array to write to the log, nominally to convey the context.
   */
  protected function debugLogger(string $message, array $context = []) {
    if ($this->debug) {
      $this->logger->debug($message, $context);
    }

  }

  /**
   * CreateDisplayNode.
   *
   * Create a product display node for a set of SKU entities.
   *
   * @param array $product
   *   Product data.
   * @param string $langcode
   *   Language code.
   *
   * @return \Drupal\Core\Entity\EntityInterface
   *   Node object.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   */
  public function createDisplayNode(array $product, $langcode = '') {

    $description = (isset($product['attributes']['description'])) ? $product['attributes']['description'] : '';

    $categories = (isset($product['categories'])) ? $product['categories'] : [];

    $config = $this->configFactory->get('acm.connector');
    $product_node_type = $config->get('product_node_type') ?: 'acm_product';
    $sku_field_name = $config->get('sku_field_name') ?: 'field_skus';
    $category_field_name = $config->get('category_field_name');
    $text_format = $config->get('text_format') ?: 'rich_text';

    $node_title = html_entity_decode($product['name']);
    if ($config->get('product_title_use_sku')) {
      $node_title = html_entity_decode($product['sku']);
    }

    $node_values = [
      'type' => $product_node_type,
      'title' => $node_title,
      'body' => [
        'value' => $description,
        'format' => $text_format,
      ],
      $sku_field_name => [$product['sku']],
    ];

    if ($langcode) {
      $node_values['langcode'] = $langcode;
    }

    // Add categories if they're configured to be synced.
    if ($category_field_name) {
      $categories = $this->formatCategories($categories);
      $node_values[$category_field_name] = $categories;
    }

    $node = $this->entityManager->getStorage('node')->create($node_values);

    if ($config->get('product_publish')) {
      $node->setPublished();
    }
    else {
      $node->setUnpublished();
    }

    // Invoke the alter hook to allow all modules to update the node.
    $this->moduleHandler->alter('acm_sku_product_node', $node, $product);

    return $node;
  }

  /**
   * Update node translation.
   *
   * @param \Drupal\node\NodeInterface $node
   *   Object of node we want to update with precreated translation.
   * @param array $product
   *   Array with product values.
   * @param string $langcode
   *   Langcode string.
   */
  public function updateNodeTranslation(NodeInterface &$node, array $product, string $langcode) {
    $description = (isset($product['attributes']['description'])) ? $product['attributes']['description'] : '';

    $categories = (isset($product['categories'])) ? $product['categories'] : [];

    $config = $this->configFactory->get('acm.connector');
    $sku_field_name = $config->get('sku_field_name') ?: 'field_skus';
    $category_field_name = $config->get('category_field_name');
    $text_format = $config->get('text_format') ?: 'rich_text';

    $node_title = html_entity_decode($product['name']);
    if ($config->get('product_title_use_sku')) {
      $node_title = html_entity_decode($product['sku']);
    }
    $node->setTitle($node_title);
    $body = [
      'value' => $description,
      'format' => $text_format,
    ];
    $node->set('body', $body);

    // Add categories if they're configured to be synced.
    if ($category_field_name) {
      $categories = $this->formatCategories($categories);
      $node->{$category_field_name} = $categories;
    }
    $node->{$sku_field_name} = [$product['sku']];

    if ($config->get('product_publish')) {
      $node->setPublished();
    }
    else {
      $node->setUnpublished();
    }

    // Invoke the alter hook to allow all modules to update the node.
    $this->moduleHandler->alter('acm_sku_product_node_translation_update', $node, $product, $langcode);
  }

  /**
   * {@inheritdoc}
   */
  public function synchronizeProducts(array $products = [], $storeId = '') {
    /** @var \Drupal\Core\Lock\PersistentDatabaseLockBackend $lock */
    $lock = \Drupal::service('lock.persistent');

    $this->created = 0;
    $this->updated = 0;
    $this->failed = 0;
    $this->ignored = 0;
    $this->deleted = 0;
    $processLaterList = [];

    // Get langcode for v2 connector.
    $langcode = '';
    if (!empty($storeId)) {
      $langcode = $this->i18nHelper->getLangcodeFromStoreId($storeId);
    }

    $this->debugLogger('Number of products: @count', ['@count' => count($products)]);
    foreach ($products as $product) {
      try {
        // Allow other modules to subscribe to pre-validation of the SKU being
        // imported.
        $event = new AcmSkuValidateEvent($product);
        $this->eventDispatcher->dispatch(AcmSkuValidateEvent::ACM_SKU_VALIDATE, $event);
        $product = $event->getProduct();

        // If skip attribute is set via any event subscriber, skip importing the
        // product.
        if (!empty($product['skip'])) {
          // We mark the status to disabled so product is deleted if available.
          $product['status'] = 0;

          $this->logger->warning('Updated status of sku @sku to 0 as it is marked as skipped.', [
            '@sku' => $product['sku'],
          ]);
        }

        $lock_key = 'synchronizeProduct' . $product['sku'];
        // Acquire lock to ensure parallel processes are executed one by one.
        do {
          $lock_acquired = $lock->acquire($lock_key);
          // Sleep for half a second before trying again.
          if (!$lock_acquired) {
            usleep(500000);
          }
        } while (!$lock_acquired);

        // For v1 connector we are going to get store_id from each product,
        // because we are not sending X-ACM-UUID header.
        // Noting product sync hits the standard Magento API and
        // so $product['store_id'] is not set on product sync.
        // However, it is set on product async because
        // that hits ACM Magento module.
        if (empty($langcode)) {
          $langcode = $this->i18nHelper->getLangcodeFromStoreId($product['store_id']);
        }

        // If langcode is still empty at this point, we probably don't support
        // this store. This is because we are sending all data for all stores.
        if (empty($langcode)) {
          $this->debugLogger("Lang code is empty. Otherwise would have synchronize product SKU @sku for store_id @store in language @langcode", [
            '@sku' => $product['sku'],
            '@store' => $product['store_id'],
            '@langcode' => $langcode,
          ]);
          $this->ignoredSkus[] = $product['sku'] . '(Langcode is empty for store_id:' . $product['store_id'] . '.)';
          $this->ignored++;
          // Release the lock on this sku.
          $lock->release($lock_key);
          $lock_key = NULL;
          continue;
        }

        $this->debugLogger("Synchronize product SKU @sku for store_id @store in language @langcode", [
          '@sku' => $product['sku'],
          '@store' => $product['store_id'],
          '@langcode' => $langcode,
        ]);
        $this->debugLogger('Product data: @data', ['@data' => print_r($product, TRUE)]);
        $display = NULL;

        if ($this->debug && !empty($this->debugDir)) {
          // Export product data into file.
          if (!isset($fps) || !isset($fps[$langcode])) {
            $filename = $this->debugDir . '/products_' . $langcode . '.data';
            $fps[$langcode] = fopen($filename, 'a');
          }
          fwrite($fps[$langcode], var_export($product, 1));
          fwrite($fps[$langcode], '\n');
        }

        if (!isset($product['type'])) {
          $message = "Product type must be defined. " . $product['sku'] . " was not synchronized.";
          $this->failedSkus[] = $product['sku'] . '(Missing Product Type)';
          $this->failed++;
          // Release the lock on this sku.
          $lock->release($lock_key);
          $lock_key = NULL;
          continue;
        }

        $query = $this->entityManager->getStorage('acm_sku_type')->getQuery()
          ->condition('id', $product['type'])
          ->count();

        $has_bundle = $query->execute();

        if (!$has_bundle) {
          $message = "Product type " . $product['type'] . " is not supported yet. " . $product['sku'] . " was not synchronized.";
          $this->ignoredSkus[] = $product['sku'] . '(Product type not supported yet.' . $product['type'] . ')';
          $this->ignored++;
          // Release the lock on this sku.
          $lock->release($lock_key);
          $lock_key = NULL;
          continue;
        }

        if (!isset($product['sku']) || !strlen($product['sku'])) {
          $this->ignoredSkus[] = $product['sku'] . '(Invalid or empty product SKU.)';
          $this->ignored++;
          // Release the lock on this sku.
          $lock->release($lock_key);
          $lock_key = NULL;
          continue;
        }

        // Don't import configurable SKU if it has no configurable options.
        // @TODO(mirom): Call validation function by $product['type'].
        if ($product['type'] == 'configurable' && empty($product['extension']['configurable_product_options'])) {
          $productToString = print_r($product, TRUE);
          $this->debugLogger('Empty configurable options for SKU: @sku, Details: @deets', [
            '@sku' => $product['sku'],
            '@deets' => $productToString,
          ]);
          $this->ignoredSkus[] = $product['sku'] . '(Empty configurable options for SKU.)';
          $this->ignored++;
          // Release the lock on this sku.
          $lock->release($lock_key);
          $lock_key = NULL;
          continue;
        }

        $query = $this->entityManager->getStorage('acm_sku')->getQuery()
          ->condition('sku', $product['sku']);
        $sku_ids = $query->execute();

        if (count($sku_ids) > 1) {
          $this->failedSkus[] = $product['sku'] . '(Duplicate product SKU found.)';
          $this->failed++;
          // Release the lock on this sku.
          $lock->release($lock_key);
          $lock_key = NULL;
          continue;
        }

        $sku = $this->processSku($product, $langcode);

        if (is_null($sku)) {
          continue;
        }

        /** @var \Drupal\acm_sku\AcquiaCommerce\SKUPluginBase $plugin */
        $plugin = $sku->getPluginInstance();
        $plugin->processImport($sku, $product);

        if ($product['status'] == 1 && $product['visibility'] == 1) {
          $node = $plugin->getDisplayNode($sku, FALSE, TRUE);
          if (empty($node)) {
            $node = $this->createDisplayNode($product, $langcode);
            $this->createdSkus[] = $product['sku'];
            $this->created++;
          }
          elseif ($node->hasTranslationChanges()) {
            $this->updateNodeTranslation($node, $product, $langcode);
          }

          // We doing this because when the translation of node is created by
          // addTranslation(), pathauto alias is not created for the translated
          // version.
          // @see https://www.drupal.org/project/pathauto/issues/2995829.
          if ($this->moduleHandler->moduleExists('pathauto')) {
            $node->path->pathauto = 1;
          }

          // Invoke the alter hook to allow all modules to update the node.
          $this->moduleHandler->alter('acm_sku_product_node', $node, $product, $langcode);
          $node->save();
        }
        else {
          try {
            // Un-publish if node available.
            if ($node = $plugin->getDisplayNode($sku, FALSE, FALSE)) {
              $node->setUnpublished();
              $node->save();
            }
          }
          catch (\Exception $e) {
            // Do nothing, we may not have the node available in system.
          }
        }

        $plugin_manager = \Drupal::service('plugin.manager.sku');
        $plugin_definition = $plugin_manager->pluginFromSKU($sku);
        if (!empty($plugin_definition)) {
          $plugin = $plugin_manager->createInstance($plugin_definition['id']);
          $processedImport = $plugin->processImport($sku, $product);
          if (!$processedImport) {
            $this->debugLogger("@sku will be processed later", ['@sku' => $product['sku']]);
            $processLaterList[] = [
              'plugin' => $plugin,
              'sku' => $sku,
              'product' => $product,
            ];
          }
        }
      }
      catch (\Exception $e) {
        // We consider this as failure as it failed for an unknown reason.
        // (not taken care of above).
        $this->failedSkus[] = $product['sku'] . '(' . $e->getMessage() . ')';
        $this->failed++;
      }
      catch (\Throwable $e) {
        // We consider this as failure as it failed for an unknown reason.
        // (not taken care of above).
        $this->failedSkus[] = $product['sku'] . '(' . $e->getMessage() . ')';
        $this->failed++;
      }
      finally {
        // Release the lock if acquired.
        if (!empty($lock_key) && !empty($lock_acquired)) {
          $lock->release($lock_key);

          // We will come here again for next loop item and we might face
          // exception before we reach the code that sets $lock_key.
          // To ensure we don't keep releasing the lock again and again
          // we set it to NULL here.
          $lock_key = NULL;
        }
      }
    }

    if (isset($fps)) {
      foreach ($fps as $fp) {
        fclose($fp);
      }
    }

    // @TODO(mirom): Review usage of processImport(), it's called 5 times here.
    // ProcessImport again if necessary (eg configured products).
    foreach ($processLaterList as $item) {
      $this->debugLogger("@sku is being processed later", ['@sku' => $item['product']['sku']]);

      $lock_key = 'synchronizeProduct' . $item['product']['sku'];
      // Acquire lock to ensure parallel processes are executed one by one.
      do {
        $lock_acquired = $lock->acquire($lock_key);
      } while (!$lock_acquired);

      $processedImport = $item['plugin']->processImport($item['sku'], $item['product']);
      if (!$processedImport) {
        $this->logger->error("Product @name(@sku) failed to process completely. Please check it before use. This normally happens when a configured product does not have access to one or more of its underlying simple products.",
          [
            '@name' => $item['product']['name'],
            '@sku' => $item['product']['sku'],
          ]);
      }
      // Release the lock on this sku.
      $lock->release($lock_key);
      $lock_key = NULL;
    }

    // Log product import summary.
    if (!empty($this->createdSkus)) {
      $this->logger->info('SKU import, created: @created_skus', ['@created_skus' => implode(',', $this->createdSkus)]);
    }

    if (!empty($this->deletedSkus)) {
      $this->logger->info('SKU import, deleted: @deleted_skus', ['@deleted_skus' => implode(',', $this->deletedSkus)]);
    }

    if (!empty($this->updatedSkus)) {
      $this->logger->info('SKU import, updated: @updated_skus', ['@updated_skus' => implode(',', $this->updatedSkus)]);
    }

    if (!empty($this->failedSkus)) {
      $this->logger->error('SKU import, failed: @failed_skus', ['@failed_skus' => implode(',', $this->failedSkus)]);
    }

    if (!empty($this->ignoredSkus)) {
      $this->logger->error('SKU import, ignored: @ignored_skus', ['@ignored_skus' => implode(',', $this->ignoredSkus)]);
    }

    // Return success true always, we reached here which means we successfully
    // processed the sync request.
    return [
      'success' => TRUE,
      'created' => $this->created,
      'updated' => $this->updated,
      'failed' => $this->failed,
      'ignored' => $this->ignored,
      'deleted' => $this->deleted,
    ];
  }

  /**
   * FormatCategories.
   *
   * @return array
   *   Array of terms.
   */
  private function formatCategories(array $categories) {

    $terms = [];

    foreach ($categories as $cid) {
      $term = $this->categoryRepo->loadCategoryTerm($cid);
      if ($term) {
        $terms[] = $term->id();
      }
    }

    return ($terms);
  }

  /**
   * FormatProductAttributes.
   *
   * Format the product attributes data as an array for saving in a
   * key value field.
   *
   * @param array $attributes
   *   Array of product attributes.
   *
   * @return array
   *   Array of formatted product attributes.
   */
  private function formatProductAttributes(array $attributes) {

    $formatted = [];

    foreach ($attributes as $name => $value) {
      if (is_string($value)) {
        $valueAsString = $value;
      }
      else {
        // Does the key=>value store module expect serialize?
        // (I would prefer JSON)
        // >> Yes. (But are we using the key=>value store for this?)
        // Can we pass it the object and get 'auto serialize/unserialize'?
        // >> Maybe.
        if (is_array($value) || is_object($value)) {
          $valueAsString = serialize($value);
        }
        else {
          // 'Trust' PHP's type-casting for now.
          $valueAsString = (string) $value;
        }
      }

      $formatted[] = [
        'key' => $name,
        'value' => $valueAsString,
      ];
    }

    return $formatted;
  }

  /**
   * Process SKU creation.
   *
   * @param array $product
   *   Array with product data fetched from eComm.
   * @param string $langcode
   *   String representation of langcode.
   *
   * @return \Drupal\acm_sku\Entity\SKUInterface|null
   *   SKU object.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Core\TypedData\Exception\ReadOnlyException
   *   If the data is read-only.
   * @throws \Drupal\Core\TypedData\Exception\MissingDataException
   *   If the complex data structure is unset and no item can be set.
   * @throws \Exception
   *   Just in case.
   */
  public function processSku(array $product, $langcode) {
    $em = $this->entityManager->getStorage('acm_sku');
    if ($sku = SKU::loadFromSku($product['sku'], $langcode, FALSE, TRUE)) {
      if ($product['status'] != 1 && $this->configFactory->get('acm.connector')->get('delete_disabled_skus')) {
        $this->logger->info('Removing disabled SKU from system: @sku.', ['@sku' => $product['sku']]);
        try {
          /** @var \Drupal\acm_sku\AcquiaCommerce\SKUPluginBase $plugin */
          $plugin = $sku->getPluginInstance();
          if ($node = $plugin->getDisplayNode($sku, FALSE, FALSE)) {
            // Delete the node if it is linked to this SKU only.
            $node->delete();
            $this->logger->info('Deleted node for SKU @sku for @langcode.', [
              '@sku' => $sku->getSku(),
              '@langcode' => $langcode,
            ]);
          }
          else {
            $this->logger->info('Node for SKU @sku for @langcode not found for deletion.', [
              '@sku' => $sku->getSku(),
              '@langcode' => $langcode,
            ]);
          }
        }
        catch (\Exception $e) {
          // Not doing anything, we might not have node for the sku.
          $this->logger->info('Error while deleting node for the SKU @sku for @langcode. Message:@message', [
            '@sku' => $sku->getSku(),
            '@langcode' => $langcode,
            '@message' => $e->getMessage(),
          ]);
        }

        // Delete the SKU.
        $sku->delete();
        $this->deletedSkus[] = $product['sku'];
        $this->deleted++;
        return NULL;
      }

      $this->logger->info('Updating product SKU @sku.', ['@sku' => $product['sku']]);
      $this->updatedSkus[] = $product['sku'];
      $this->updated++;
    }
    else {
      if ($product['status'] != 1 && $this->configFactory->get('acm.connector')->get('delete_disabled_skus')) {
        $this->ignoredSkus[] = $product['sku'] . '(Disabled SKU).';
        $this->ignored++;
        return NULL;
      }
      /** @var \Drupal\acm_sku\Entity\SKU $sku */
      $sku = $em->create([
        'type' => $product['type'],
        'sku' => $product['sku'],
        'langcode' => $langcode,
      ]);

      $this->createdSkus[] = $product['sku'];
      $this->created++;
    }

    $sku->name->value = html_entity_decode($product['name']);
    $sku->price->value = $product['price'];
    $sku->special_price->value = $product['special_price'];
    $sku->final_price->value = $product['final_price'];
    $sku->attributes = $this->formatProductAttributes($product['attributes']);

    $hasSerializableMedia = (
      array_key_exists('extension', $product) &&
      array_key_exists('media', $product['extension']) &&
      $product['extension']['media']
    );
    if ($hasSerializableMedia) {
      $sku->media = serialize($product['extension']['media']);
    }
    $sku->attribute_set = $product['attribute_set_label'];
    $sku->product_id = $product['product_id'];

    // Update the fields based on the values from attributes.
    $this->updateFields('attributes', $sku, $product['attributes']);

    // Update the fields based on the values from extension.
    $this->updateFields('extension', $sku, $product['extension']);

    // Update upsell linked SKUs.
    $this->updateLinkedSkus('upsell', $sku, $product['linked']);

    // Update crosssell linked SKUs.
    $this->updateLinkedSkus('crosssell', $sku, $product['linked']);

    // Update related linked SKUs.
    $this->updateLinkedSkus('related', $sku, $product['linked']);

    /** @var \Drupal\acm_sku\AcquiaCommerce\SKUPluginBase $plugin */
    $plugin = $sku->getPluginInstance();
    $plugin->processImport($sku, $product);

    // Invoke the alter hook to allow all modules to update the node.
    $this->moduleHandler->alter('acm_sku_product_sku', $sku, $product);

    $sku->save();

    // Update product media to set proper position.
    $sku->media = $this->getProcessedMedia($product, $sku->media->value);
    $sku->getMedia();

    if (empty($sku->get('image')->target_id)) {
      $thumbnail = $sku->getThumbnail();
      if (!empty($thumbnail)) {
        $sku->set('image', $thumbnail['fid']);
        $sku->save();
      }
    }
    return $sku;
  }

  /**
   * Update linked Skus.
   *
   * Prepare the field value for linked type (upsell, crosssell, etc.).
   * Get the position based on the position coming from API.
   *
   * @param string $type
   *   Type of link.
   * @param \Drupal\acm_sku\Entity\SKU $sku
   *   Root SKU.
   * @param array $linked
   *   Linked SKUs.
   *
   * @throws \Drupal\Core\TypedData\Exception\ReadOnlyException
   *   If the data is read-only.
   * @throws \Drupal\Core\TypedData\Exception\MissingDataException
   *   If the complex data structure is unset and no item can be set.
   */
  private function updateLinkedSkus($type, SKU &$sku, array $linked) {
    // Reset the upsell skus to null.
    $sku->{$type}->setValue([]);
    $fieldData = [];
    foreach ($linked as $link) {
      if ($link['type'] != $type) {
        continue;
      }
      $fieldData[$link['position']] = $link['linked_sku'];
    }
    // If there is no upsell skus to link, we simply return from here.
    if (empty($fieldData)) {
      return;
    }
    // Sort them based on position.
    ksort($fieldData);
    // Update the index to sequential values so we can set in field.
    $fieldData = array_values($fieldData);
    foreach ($fieldData as $delta => $value) {
      $sku->{$type}->set($delta, $value);
    }
  }

  /**
   * Update attribute fields.
   *
   * Update the fields based on the values from attributes.
   *
   * @param string $parent
   *   Fields to get from this parent will be processed.
   * @param \Drupal\acm_sku\Entity\SKU $sku
   *   The root SKU.
   * @param array $values
   *   The product attributes/extensions to get value from.
   *
   * @throws \Drupal\Core\TypedData\Exception\ReadOnlyException
   *   If the data is read-only.
   * @throws \Drupal\Core\TypedData\Exception\MissingDataException
   *   If the complex data structure is unset and no item can be set.
   */
  private function updateFields($parent, SKU $sku, array $values) {
    $additionalFields = $this->skuFieldsManager->getFieldAdditions();

    // Filter fields for the parent requested.
    $additionalFields = array_filter($additionalFields, function ($field) use ($parent) {
      return ($field['parent'] == $parent);
    });

    // Loop through all the fields we want to read from product data.
    foreach ($additionalFields as $key => $field) {
      $source = isset($field['source']) ? $field['source'] : $key;

      // Field key.
      $field_key = 'attr_' . $key;

      switch ($field['type']) {
        case 'attribute':
          // If attribute is not coming in response, then unset it.
          if (!isset($values[$source])) {
            $sku->{$field_key}->set(0, NULL);
          }
          else {
            $value = $values[$source];
            $value = $field['cardinality'] != 1 ? explode(',', $value) : [$value];
            foreach ($value as $index => $val) {
              if ($term = $this->productOptionsManager->loadProductOptionByOptionId($source, $val, $sku->language()->getId())) {
                $sku->{$field_key}->set($index, $term->getName());
              }
              else {
                $sku->{$field_key}->set($index, $val);
              }
            }
          }
          break;

        case 'string':
          // If attribute is not coming in response, then unset it.
          if (!isset($values[$source])) {
            $sku->{$field_key}->setValue(NULL);
          }
          else {
            $value = $values[$source];
            $value = $field['cardinality'] != 1 ? explode(',', $value) : $value;
            $sku->{$field_key}->setValue($value);
          }
          break;

        case 'text_long':
          // If attribute is not coming in response, then unset it.
          if (!isset($values[$source])) {
            $sku->{$field_key}->setValue(NULL);
          }
          else {
            $value = $values[$source];
            $value = !empty($field['serialize']) ? serialize($value) : $value;
            $sku->{$field_key}->setValue($value);
          }
          break;
      }
    }
  }

  /**
   * Extracts and orders the media gallery by position and sets the main image.
   *
   * Extracts the media gallery fields from product[extension][media][0].
   * Orders the media gallery by position and sets the base (main) image
   * to be positioned first.
   * Disabled images are left in place.
   *
   * @param array $product
   *   Array with product information.
   * @param string $current_value
   *   The current media data of the sku bein processed.
   *
   * @return string
   *   Serialized array with product's media information.
   */
  protected function getProcessedMedia(array $product, $current_value) {
    $media = [];

    // The Commerce Connector Service sends the media information
    // in $product['extension']['media'][0].
    // We note $product['extension']['media'][1] is deliberately always empty.
    // See Magento module method...
    // Model/ProductSyncManagement::processMediaGalleryExtension()
    // TODO What does Hybris send? The connector doesn't normalize.
    if (isset(
      $product['extension'],
      $product['extension']['media'],
      $product['extension']['media'][0])) {

      // Conveniently reset() returns the first element of the array.
      $media = reset($product['extension']['media']);

      if (isset($product['attributes'], $product['attributes']['image'])) {
        $image = $product['attributes']['image'];

        // If the base image is in the gallery then position it first.
        // But why? Is this an Acquia Commerce Manager requirement?
        // It is normal in Magento to set a gallery position for the base image
        // And to set images as 'hide in gallery' ie ['disabled'] = 1
        // So I suggest we honor the Magento data here.
        // TODO What does Hybris send? (what would the normalization be?)
        // Maybe we just bring in all images and let the Drupal user decide
        // what to display and in which position.
        // Later, will the Drupal theme honor ['disabled'] = 1 or do we
        // need to omit ['disabled'] = 1 images here.
        foreach ($media as &$data) {
          if (substr_compare($data['file'], $image, -strlen($image)) === 0) {
            $data['position'] = -1;
            break;
          }
        }
      }

      // Sort media data by position. Noting disabled images are included.
      usort($media, function ($a, $b) {
        $position1 = (int) $a['position'];
        $position2 = (int) $b['position'];

        if ($position1 == $position2) {
          return 0;
        }

        return ($position1 < $position2) ? -1 : 1;
      });
    }

    // Reassign old files to not have to redownload them.
    if (!empty($media)) {
      $current_media = unserialize($current_value);
      if (!empty($current_media) && is_array($current_media)) {
        $current_mapping = [];
        foreach ($current_media as $value) {
          if (!empty($value['fid'])) {
            $current_mapping[$value['value_id']]['fid'] = $value['fid'];
            $current_mapping[$value['value_id']]['file'] = $value['file'];
          }
        }

        foreach ($media as $key => $value) {
          if (isset($current_mapping[$value['value_id']])) {
            $media[$key]['fid'] = $current_mapping[$value['value_id']]['fid'];
            $media[$key]['file'] = $current_mapping[$value['value_id']]['file'];
          }
        }
      }
    }

    return serialize($media);
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc