commercetools-8.x-1.2-alpha1/src/CommercetoolsService.php

src/CommercetoolsService.php
<?php

namespace Drupal\commercetools;

use Drupal\commercetools\Cache\CacheableCommercetoolsGraphQlResponse;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Logger\LoggerChannelTrait;
use GraphQL\Actions\Query;
use GraphQL\Entities\Node;
use Punic\Number;

/**
 * Commercetools main service.
 */
class CommercetoolsService {

  use LoggerChannelTrait;

  const CONFIGURATION_NAME = CommercetoolsConfiguration::CONFIGURATION_SETTINGS;

  const PRODUCT_TYPE_CACHE_KEY = 'commercetools_product';
  const CATEGORY_CACHE_KEY = 'commercetools_product_category';

  const CONFIG_PRICE_CUSTOMER_GROUP = 'price_customer_group';
  const CONFIG_ITEMS_PER_PAGE = 'items_per_page';
  const CONFIG_CARD_IMAGE_STYLE = 'card_image_style';
  const CONFIG_UNAVAILABLE_DATA_TEXT = 'unavailable_data_text';

  const CONFIG_DISPLAY_CONNECTION_ERRORS = 'display_connection_errors';
  const CONFIG_LOG_CT_REQUESTS = 'log_commercetools_requests';

  const CONFIG_CHECKOUT_MODE = 'checkout_mode';
  const CONFIG_CHECKOUT_CT_APP_KEY = 'checkout_commercetools_app_key';
  const CONFIG_CHECKOUT_CT_INLINE = 'checkout_commercetools_inline';
  const CONFIG_PAGE_ATTRIBUTES_ENABLED = 'customize_page_attributes_enabled';
  const CONFIG_PAGE_ATTRIBUTES = 'customize_page_attributes';

  const FILTER_TYPE_VALUE = 'value';
  const FILTER_TYPE_TREE = 'tree';
  const FILTER_TYPE_RANGE = 'range';
  const CT_VARIANTS_PRICE_AMOUNT = 'variants.price.centAmount';

  const CATEGORY_LIST_LIMIT = 500;

  /**
   * The product types.
   *
   * @var array
   */
  protected array $productsTypesLocalized;

  /**
   * CommercetoolsGraphQLService constructor.
   *
   * @param \Drupal\commercetools\CommercetoolsApiServiceInterface $ctApi
   *   The Commercetools API service.
   * @param \Drupal\commercetools\CommercetoolsConfiguration $ctConfig
   *   The commercetools configuration service.
   * @param \Drupal\commercetools\CommercetoolsLocalization $ctLocalization
   *   The commercetools localization service.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cacheBackend
   *   The cache backend.
   */
  public function __construct(
    protected CommercetoolsApiServiceInterface $ctApi,
    protected CommercetoolsConfiguration $ctConfig,
    protected CommercetoolsLocalization $ctLocalization,
    protected CacheBackendInterface $cacheBackend,
  ) {}

  /**
   * Get product types.
   *
   * @param bool $reset
   *   If the list of values should be reset.
   *
   * @return array
   *   Array of product types.
   */
  public function getProductsTypes(bool $reset = FALSE): array {
    if (isset($this->productsTypesLocalized)) {
      return $this->productsTypesLocalized;
    }
    if (!$reset && $cacheData = $this->cacheBackend->get(self::PRODUCT_TYPE_CACHE_KEY)) {
      $this->productsTypesLocalized = $this->localizeProductTypes($cacheData->data);
      return $this->productsTypesLocalized;
    }

    $query = new Query('productTypes');
    $resultsQuery = $query->results([]);
    $resultsQuery->use('key', 'name');
    $attrDefinition = $resultsQuery->attributeDefinitions([]);
    $attrResults = $attrDefinition->results([]);
    $attrResults->use('name', 'isSearchable');
    $attrResults->labelAllLocales([])->use('locale', 'value');
    $attrResults->type([])->use('name');
    $response = $this->ctApi->executeGraphQlOperation($query);
    $result = $response->getData();

    $productTypes = [];
    foreach ($result['productTypes']['results'] as $productType) {
      $attributeDefinitions = [];
      foreach ($productType['attributeDefinitions']['results'] as $attribute) {
        // @todo Implement supporting set attribute type.
        if ($attribute['type']['name'] === 'set') {
          continue;
        }
        $attribute['type'] = $attribute['type']['name'];
        $attributeDefinitions[$attribute['name']] = $attribute;
      }
      $productTypes[$productType['key']] = ['attributeDefinitions' => $attributeDefinitions] + $productType;
    }

    $this->cacheBackend->set(self::PRODUCT_TYPE_CACHE_KEY, $productTypes, tags: [
      'config:' . self::CONFIGURATION_NAME,
      'config:' . CommercetoolsApiServiceInterface::CONFIGURATION_NAME,
    ]);
    $this->productsTypesLocalized = $this->localizeProductTypes($productTypes);
    return $this->productsTypesLocalized;
  }

  /**
   * Localize product types.
   *
   * @param array $productTypes
   *   Array of product types.
   *
   * @return array
   *   Array of localized product types.
   */
  protected function localizeProductTypes(array $productTypes): array {
    foreach ($productTypes as $typeName => $productType) {
      foreach ($productType['attributeDefinitions'] as $attrName => $attr) {
        $productTypes[$typeName]['attributeDefinitions'][$attrName]['label'] = $this->getTranslationValue($attr['labelAllLocales']);
      }
    }
    return $productTypes;
  }

  /**
   * Return product attribute path.
   */
  public function getAttributePath(string $productTypeKey, string $attributeKey): string|null {
    $productTypes = $this->getProductsTypes();
    if (!($attribute = $productTypes[$productTypeKey]['attributeDefinitions'][$attributeKey])) {
      return NULL;
    }

    switch ($attribute['type']) {
      case 'enum':
        $path = "variants.attributes.{$attribute['name']}.label";
        break;

      case 'ltext':
        $path = "variants.attributes.{$attribute['name']}.[current_locale]";
        break;

      case 'lenum':
        $path = "variants.attributes.{$attribute['name']}.label.[current_locale]";
        break;

      default:
        $path = "variants.attributes.{$attribute['name']}";
    }

    return $path;
  }

  /**
   * Replace the special locale token with the current locale.
   */
  public function localizePath($path) {
    return str_replace('[current_locale]', $this->ctLocalization->getOption(CommercetoolsLocalization::CONFIG_LANGUAGE), $path);
  }

  /**
   * Build facet GraphQL structure to get facet by request.
   */
  public function buildFacetGraphql($path, $countProducts = FALSE): array {
    return [
      'model' => [
        'terms' => [
          'path' => $path,
          'countProducts' => $countProducts,
        ],
      ],
    ];
  }

  /**
   * Build facet GraphQL structure to get facet by request.
   */
  public function buildRangeFacetGraphql($path): array {
    return [
      'model' => [
        'range' => [
          'countProducts' => FALSE,
          'path' => $path,
          'ranges' => [
            'from' => '0',
            'to' => '*',
          ],
        ],
      ],
    ];
  }

  /**
   * Get attributes which should be used on the product page.
   *
   * @return array
   *   A list of product attributes.
   */
  public function getEnabledAttributes(): array {
    $attributes = [];
    $customization = $this->ctConfig->settings->get(self::CONFIG_PAGE_ATTRIBUTES_ENABLED) ?: NULL;
    foreach ($this->getProductsTypes() as $productTypeKey => $productType) {
      foreach ($productType['attributeDefinitions'] as $attributeKey => $attribute) {
        if (isset($customization) && empty($customization[$productTypeKey . '_' . $attributeKey])) {
          continue;
        }
        $attributes[$productTypeKey][] = $attribute;
      }
    }
    return $attributes;
  }

  /**
   * Retrieves product categories to be used for displaying products.
   *
   * @return array
   *   A list of available categories.
   */
  public function getProductCategories(): array {
    $list = [];

    // Check for data in cache.
    if ($cached = $this->cacheBackend->get(self::CATEGORY_CACHE_KEY)) {
      $list = $cached->data;
    }
    // Request data from API.
    else {
      $query = new Query('categories', [
        'limit' => self::CATEGORY_LIST_LIMIT,
        'sort' => 'orderHint ASC',
      ]);
      $query->results([])->use(
        'id',
        'key',
        'orderHint',
        'nameAllLocales{locale, value}',
        'parent{id}',
      );
      $result = $this->ctApi->executeGraphQlOperation($query)->getData();
      $list = $result['categories']['results'];
      $this->cacheBackend->set(self::CATEGORY_CACHE_KEY, $list, tags: [
        CacheableCommercetoolsGraphQlResponse::CACHE_TAG_CATEGORY_LIST,
      ]);
    }

    // Format categories list.
    $categories = [];
    foreach ($list as $item) {
      $categories[$item['id']] = [
        'id' => $item['id'],
        'key' => $item['key'],
        'name' => $this->getTranslationValue($item['nameAllLocales']),
        'orderHint' => $item['orderHint'],
        'parent' => $item['parent']['id'] ?? NULL,
      ];
    }

    return $categories;
  }

  /**
   * Builds product categories tree.
   *
   * @param array|null $list
   *   The list of categories. Optional.
   *
   * @return array
   *   A tree of categories.
   */
  public function getProductCategoriesTree(?array $list = NULL): array {
    $categories = $list ?? $this->getProductCategories();
    $fillChildren = function ($parentId = NULL) use ($categories, &$fillChildren) {
      $children = [];
      foreach ($categories as $category) {
        if (($category['parent'] ?? NULL) == $parentId) {
          $catChildren = $fillChildren($category['id']);
          $category['children'] = $catChildren;
          $children[$category['id']] = $category;
        }
      }
      return $children;
    };
    return array_filter($fillChildren(), fn($cat) => empty($cat['parent']));
  }

  /**
   * Get a localized price.
   *
   * @param array $price
   *   The array with all price data.
   * @param string|null $locale
   *   Locale in which the number would be formatted.
   *
   * @return false|string
   *   The localized price.
   */
  public function localizePrice(array $price, ?string $locale = NULL): string|false {
    $locale ??= $this->ctLocalization->getOption(CommercetoolsLocalization::CONFIG_LANGUAGE);
    $currencyValue = $price['centAmount'] / pow(10, $price['fractionDigits']);
    return $this->formatCurrency($locale, $currencyValue, $price['currencyCode']);
  }

  /**
   * Formats a monetary amount for a given locale and currency.
   */
  protected function formatCurrency(string $locale, float $currencyValue, string $currencyCode): string {
    if (class_exists(\NumberFormatter::class) && function_exists('numfmt_create') && function_exists('numfmt_format_currency')) {
      $fmt = numfmt_create($locale, \NumberFormatter::CURRENCY);
      $result = numfmt_format_currency($fmt, $currencyValue, $currencyCode);
      if ($result !== FALSE) {
        return $result;
      }
    }
    return Number::formatCurrency($currencyValue, $currencyCode, 'standard', NULL, '', $locale);
  }

  /**
   * Format a price array from the given array.
   *
   * @param array $price
   *   The price array, with the "currencyCode", "centAmount"
   *   and "fractionDigits" keys.
   *
   * @return array
   *   Array of all necessary price values.
   */
  public function formatPrice(array $price): array {
    return [
      'centAmount' => $price['centAmount'],
      'currencyCode' => $price['currencyCode'],
      'currencyValue' => $price['centAmount'] / pow(10, $price['fractionDigits']),
      'fractionDigits' => $price['fractionDigits'],
      'localizedPrice' => $this->localizePrice($price),
    ];
  }

  /**
   * Add default Commercetools price node to graphql node.
   *
   * @param \GraphQL\Entities\Node $node
   *   The node to which the price node will be added.
   * @param bool $includeFilters
   *   If true, price filters will be added.
   * @param array $filters
   *   The array with extra price filters.
   */
  public function addPriceNode(Node $node, bool $includeFilters = FALSE, array $filters = []): void {
    $filter = $includeFilters ? $filters + array_filter([
      'currency' => $this->ctLocalization->getOption(CommercetoolsLocalization::CONFIG_CURRENCY),
      'country' => $this->ctLocalization->getOption(CommercetoolsLocalization::CONFIG_COUNTRY),
      'customerGroupId' => $this->ctConfig->settings->get(self::CONFIG_PRICE_CUSTOMER_GROUP) ?? NULL,
      'channelId' => $this->ctLocalization->getOption(CommercetoolsLocalization::CONFIG_CHANNEL),
    ]) : [];

    $price = $node->price($filter);
    self::addPriceNodeFields($price->value([]));
    self::addPriceNodeFields($price->discounted->value([]));
  }

  /**
   * Add node price field into graphql node.
   *
   * @param \GraphQL\Entities\Node $node
   *   The node to which the price fields will be added.
   */
  public function addPriceNodeFields(Node $node): void {
    $node->use('centAmount', 'currencyCode', 'fractionDigits');
  }

  /**
   * Add node address field into graphql node.
   *
   * @param \GraphQL\Entities\Node $node
   *   The node to which the address fields will be added.
   */
  public function addAddressNodeFields(Node $node): void {
    $node->use(...CommercetoolsCustomers::ADDRESS_FIELDS);
  }

  /**
   * Adds fields to graphql node.
   *
   * @param \GraphQL\Entities\Node $node
   *   The node to which the fields will be added.
   * @param array $fields
   *   The list of fields to add.
   * @param array $translatableFields
   *   The list of translatable fields. Optional.
   *
   * @return \GraphQL\Entities\Node
   *   An updated graphql node.
   */
  public function addFieldsToNode(Node $node, array $fields, array $translatableFields = []): Node {
    foreach ($fields as $field) {
      if (in_array($field, $translatableFields)) {
        $node->$field($this->ctLocalization->getLanguageArgument());
      }
      else {
        $node->use($field);
      }
    }
    return $node;
  }

  /**
   * Matches translations list items to a current language.
   *
   * @param array $values
   *   The list of localized variants.
   *   Two types of data structures are acceptable:
   *   [
   *     'en' => 'Name',
   *   ]
   *   and
   *   [
   *     'locale' => 'en',
   *     'value' => 'Name',
   *   ].
   *
   * @return string
   *   A variant that matches current language.
   */
  public function getTranslationValue(array $values): string {
    $localized = [];
    foreach ($values as $key => $value) {
      if (isset($value['locale']) && isset($value['value'])) {
        $localized[$value['locale']] = $value['value'];
      }
      // Otherwise we assume that key is a locale.
      elseif (is_string($key) && is_string($value)) {
        $localized[$key] = $value;
      }
    }
    return $this->ctLocalization->fromLocalizedArray($localized);
  }

  /**
   * {@inheritdoc}
   */
  public function getWhereValue($name, $value, array $translatableFields = []) {
    if (in_array($name, $translatableFields)) {
      $localized = [];
      foreach ($this->ctLocalization->getLanguageFallbacks() as $fallback) {
        $localized[] = $fallback . '="' . addslashes($value) . '"';
      }
      $value = [implode(' or ', $localized)];
    }
    return $value;
  }

  /**
   * {@inheritdoc}
   */
  public static function whereToString(array $where): string {
    $string = '';
    // @todo Add support for different operators.
    // @todo Add support for AND and OR.
    foreach ($where as $key => $value) {
      if (is_array($value)) {
        $string .= $key . '(' . self::whereToString($value) . ')';
      }
      else {
        $string .= is_string($key) ? $key . '="' . addslashes($value) . '"' : $value;
      }
    }
    return $string;
  }

  /**
   * Builds a filter array for a given path and values.
   *
   * @param string $path
   *   The filter path.
   * @param string|array $values
   *   The filter values.
   * @param string $type
   *   The filter option. Optional. Default is 'value'.
   *
   * @return array
   *   The filter array.
   */
  public function buildFilter(string $path, string|array $values, string $type = self::FILTER_TYPE_VALUE): array {
    // @todo Add support for different options: range, missing, exists, string.
    switch ($type) {
      case self::FILTER_TYPE_TREE:
        return [
          'model' => [
            'tree' => [
              'path' => $path,
              'rootValues' => [],
              'subTreeValues' => array_values($values),
            ],
          ],
        ];

      case self::FILTER_TYPE_RANGE:
        return [
          'model' => [
            'range' => [
              'path' => $path,
              // Array ['from' => '', 'to' => ''].
              'ranges' => $values,
            ],
          ],
        ];

      default:
        return [
          'model' => [
            'value' => [
              'path' => $path,
              'values' => is_array($values) ? array_values($values) : $values,
            ],
          ],
        ];
    }
  }

}

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

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