EmicoEcommerce/Magento2TweakwiseExport

View on GitHub
Model/Write/Products.php

Summary

Maintainability
B
5 hrs
Test Coverage
<?php

/**
 * Tweakwise (https://www.tweakwise.com/) - All Rights Reserved
 *
 * @copyright Copyright (c) 2017-2022 Tweakwise.com B.V. (https://www.tweakwise.com)
 * @license   http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
 */

namespace Tweakwise\Magento2TweakwiseExport\Model\Write;

use Tweakwise\Magento2TweakwiseExport\Model\Config;
use Tweakwise\Magento2TweakwiseExport\Model\Helper;
use Tweakwise\Magento2TweakwiseExport\Model\Logger;
use Tweakwise\Magento2TweakwiseExport\Model\Write\Products\Iterator;
use Magento\Catalog\Model\Product;
use Magento\Eav\Model\Config as EavConfig;
use Magento\Eav\Model\Entity\Attribute\AbstractAttribute;
use Magento\Eav\Model\Entity\Attribute\Source\SourceInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Profiler;
use Magento\Store\Api\Data\StoreInterface;
use Magento\Store\Model\Store;
use Magento\Store\Model\StoreManager;

class Products implements WriterInterface
{
    /**
     * @var Config
     */
    protected $config;

    /**
     * @var Iterator
     */
    protected $iterator;

    /**
     * @var StoreManager
     */
    protected $storeManager;

    /**
     * @var Helper
     */
    protected $helper;

    /**
     * @var Logger
     */
    protected $log;

    /**
     * @var EavConfig
     */
    protected $eavConfig;

    /**
     * @var array
     */
    protected $attributeOptionMap = [];

    /**
     * Products constructor.
     *
     * @param Config $config
     * @param Iterator $iterator
     * @param StoreManager $storeManager
     * @param Helper $helper
     * @param Logger $log
     * @param EavConfig $eavConfig
     */
    public function __construct(
        Config $config,
        Iterator $iterator,
        StoreManager $storeManager,
        Helper $helper,
        Logger $log,
        EavConfig $eavConfig
    ) {
        $this->config = $config;
        $this->iterator = $iterator;
        $this->storeManager = $storeManager;
        $this->helper = $helper;
        $this->log = $log;
        $this->eavConfig = $eavConfig;
    }

    /**
     * @param Writer $writer
     * @param XMLWriter $xml
     * @param StoreInterface|null $store
     */
    public function write(Writer $writer, XMLWriter $xml, StoreInterface  $store = null): void
    {
        $xml->startElement('items');

        $stores = [];
        if ($store) {
            $stores[] = $store;
        } else {
            $stores = $this->storeManager->getStores();
        }

        /** @var Store $store */
        foreach ($stores as $store) {
            if ($this->config->isEnabled($store)) {
                $profileKey = 'products::' . $store->getCode();
                try {
                    Profiler::start($profileKey);
                    $this->exportStore($writer, $xml, $store);
                } finally {
                    Profiler::stop($profileKey);
                }

                $this->log->debug(sprintf('Export products for store %s', $store->getName()));
            } else {
                $this->log->debug(sprintf('Skip products for store %s (disabled)', $store->getName()));
            }
        }

        $xml->endElement(); // items
        $writer->flush();
    }

    /**
     * @param Writer $writer
     * @param XMLWriter $xml
     * @param Store $store
     * @param int[] $entityIds
     */
    public function exportStore(Writer $writer, XMLWriter $xml, Store $store, array $entityIds = []): void
    {
        $this->iterator->setStore($store);
        // Purge iterator entity ids for each store
        $this->iterator->setEntityIds($entityIds);

        foreach ($this->iterator as $index => $data) {
            $this->writeProduct($xml, $store->getId(), $data);
            // Flush every so often
            if ($index % 100 === 0) {
                $writer->flush();
            }
        }

        // Flush any remaining products
        $writer->flush();
    }

    /**
     * @param XMLWriter $xml
     * @param int $storeId
     * @param array $data
     */
    protected function writeProduct(XMLWriter $xml, $storeId, array $data): void
    {
        $xml->startElement('item');

        // Write product base data
        $tweakwiseId = $this->helper->getTweakwiseId($storeId, $data['entity_id']);
        $xml->writeElement('id', $tweakwiseId);
        $xml->writeElement('name', $this->scalarValue($data['name']));
        $xml->writeElement('price', $this->scalarValue($data['price']));
        $xml->writeElement('stock', $this->scalarValue($data['stock']));

        // Write product categories
        $xml->startElement('categories');
        foreach ($data['categories'] as $categoryId) {
            $categoryTweakwiseId = $this->helper->getTweakwiseId($storeId, $categoryId);
            if ($xml->hasCategoryExport($categoryTweakwiseId)) {
                $xml->writeElement('categoryid', $categoryTweakwiseId);
            } else {
                $this->log->debug(
                    sprintf('Skip product (%s) category (%s) relation', $tweakwiseId, $categoryTweakwiseId)
                );
            }
        }

        $xml->endElement(); // categories

        // Write product attributes
        $xml->startElement('attributes');
        foreach ($data['attributes'] as $attributeKeyValue) {
            $this->writeAttribute($xml, $storeId, $attributeKeyValue['attribute'], $attributeKeyValue['value']);
        }

        $xml->endElement(); // attributes

        $xml->endElement(); // </item>

        $this->log->debug(sprintf('Export product [%s] %s', $tweakwiseId, $data['name']));
    }

    /**
     * @param XMLWriter $xml
     * @param int $storeId
     * @param string $name
     * @param string|string[]|int|int[]|float|float[] $attributeValue
     */
    public function writeAttribute(
        XMLWriter $xml,
        $storeId,
        $name,
        $attributeValue
    ): void {
        $values = $this->normalizeAttributeValue($storeId, $name, $attributeValue);
        $values = array_unique($values);

        foreach ($values as $value) {
            if (empty($value) && $value !== "0") {
                continue;
            }

            $xml->startElement('attribute');
            $xml->writeAttribute('datatype', is_numeric($value) ? 'numeric' : 'text');
            $xml->writeElement('name', $name);
            $xml->writeElement('value', $value);
            $xml->endElement(); // </attribute>
        }
    }

    /**
     * @param int $storeId
     * @param AbstractAttribute $attribute
     * @return string[]
     */
    protected function getAttributeOptionMap($storeId, AbstractAttribute $attribute): array
    {
        $attributeKey = $storeId . '-' . $attribute->getId();
        if (!isset($this->attributeOptionMap[$attributeKey])) {
            $map = [];

            // Set store id to trick in fetching correct options
            $attribute->setData('store_id', $storeId);

            foreach ($attribute->getSource()->getAllOptions() as $option) {
                $map[$option['value']] = (string)$option['label'];
            }

            $this->attributeOptionMap[$attributeKey] = $map;
        }

        return $this->attributeOptionMap[$attributeKey];
    }

    /**
     * Get scalar value from object, array or scalar value
     *
     * @param mixed $value
     *
     * @return string|array
     * phpcs:disable Magento2.Functions.DiscouragedFunction.Discouraged
     */
    protected function scalarValue($value)
    {
        if (is_array($value)) {
            $data = [];
            foreach ($value as $key => $childValue) {
                $data[$key] = $this->scalarValue($childValue);
            }

            return $data;
        }

        if (is_object($value)) {
            if (method_exists($value, 'toString')) {
                $value = $value->toString();
            } elseif (method_exists($value, '__toString')) {
                $value = (string)$value;
            } else {
                $value = spl_object_hash($value);
            }
        }

        if (is_numeric($value)) {
            $value = $this->normalizeExponent($value);
        }

        if ($value !== null) {
            return html_entity_decode($value, ENT_NOQUOTES | ENT_HTML5);
        }

        return '';
    }

    /**
     * @param float|int $value
     * @return float|string
     */
    protected function normalizeExponent($value)
    {
        if (stripos($value, 'E+') !== false) {
            // Assume integer value
            $decimals = 0;
            if (is_float($value)) {
                // Update decimals if not int
                $decimals = 5;
            }

            return number_format($value, $decimals, '.', '');
        }

        return $value;
    }

    /**
     * @param mixed $data
     * @return array
     */
    protected function ensureArray($data): array
    {
        return is_array($data) ? $data : [$data];
    }

    /**
     * @param string[] $data
     * @param string $delimiter
     * @return string[]
     */
    protected function explodeValues(array $data, string $delimiter = ','): array
    {
        $result = [];
        foreach ($data as $value) {
            $result[] = explode($delimiter, $value) ?: [];
        }

        return !empty($result) ? array_merge([], ...$result) : [];
    }

    /**
     * Convert attribute value to array of scalar values.
     *
     * @param int $storeId
     * @param string $attributeCode
     * @param mixed $value
     * @return array
     */
    protected function normalizeAttributeValue(int $storeId, string $attributeCode, $value): array
    {
        $values = $this->ensureArray($value);
        $values = array_map(
            function ($value) {
                return $this->scalarValue($value);
            },
            $values
        );

        try {
            $attribute = $this->eavConfig->getAttribute(Product::ENTITY, $attributeCode);
        } catch (LocalizedException $e) {
            $this->log->error($e->getMessage());
            return $values;
        }

        // Attribute does not exists so just return value
        if (!$attribute || !$attribute->getId()) {
            return $values;
        }

        // Apparently Magento adds a default source model to the attribute even if it does not use a source
        if (!$attribute->usesSource()) {
            return $values;
        }

        // Explode values if source is used (multi select)
        $values = $this->explodeValues($values);
        try {
            $attributeSource = $attribute->getSource();
        } catch (LocalizedException $e) {
            $this->log->error($e->getMessage());
            return $values;
        }

        if (!$attributeSource instanceof SourceInterface) {
            return $values;
        }

        $result = [];
        /** @var string $attributeValue */
        foreach ($values as $attributeValue) {
            $map = $this->getAttributeOptionMap($storeId, $attribute);
            $result[] = $map[$attributeValue] ?? null;
        }

        return $result;
    }
}