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