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

src/CommercetoolsProducts.php
<?php

namespace Drupal\commercetools;

use Drupal\commercetools\Exception\CommercetoolsResourceNotFoundException;
use Drupal\Core\Config\ConfigFactoryInterface;
use GraphQL\Actions\Query;
use GraphQL\Entities\Node;
use GraphQL\Entities\Variable;

/**
 * The commercetools products service.
 */
class CommercetoolsProducts {

  /**
   * Translatable product fields.
   */
  const FIELDS_TRANSLATABLE_PRODUCT = [
    'name',
    'slug',
    'description',
    'metaTitle',
    'metaDescription',
    'metaKeywords',
  ];

  const MISSING_SLUG_VALUE = 'broken-path';

  /**
   * CommercetoolsGraphQLService constructor.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The config factory service.
   * @param \Drupal\commercetools\CommercetoolsService $ct
   *   The Commercetools service.
   * @param \Drupal\commercetools\CommercetoolsApiServiceInterface $ctApi
   *   The Commercetools API service.
   */
  public function __construct(
    protected ConfigFactoryInterface $configFactory,
    protected CommercetoolsService $ct,
    protected CommercetoolsApiServiceInterface $ctApi,
  ) {
  }

  /**
   * Retrieves products based on the provided parameters.
   *
   * @param array $args
   *   An array with the query arguments:
   *    - sorts: An associative array of sort options to apply to the query.
   *    - queryFilters: An associative array of filters to apply to the query.
   *    - facets: An associative array of facets to add to the query.
   *    - offset: The number of products to skip before starting to collect the
   *    result set. Defaults to 0.
   *    - limit: The maximum number of products to return. Defaults to 10.
   * @param bool|null $includeDetails
   *   Whether to include detailed product information. Defaults to NULL.
   *
   * @return \Drupal\commercetools\CacheableCommercetoolsResponse
   *   The response containing the product data.
   */
  public function getProducts(array $args = [], ?bool $includeDetails = NULL) {
    $query = $this->getProductsSearchQuery();
    $this->addProductFields(
      // @todo It overwrites the 'results' Node, rework to get the current one.
      $query->results([]),
      includeDetails: $includeDetails,
    );
    if (isset($args['facets'])) {
      $this->addFacets($query);
    }

    $configLocale = $this->configFactory
      ->get(CommercetoolsLocalization::CONFIGURATION_NAME);
    $localeLanguage = $configLocale->get(CommercetoolsLocalization::CONFIG_LANGUAGE);
    $localeStore = $configLocale->get(CommercetoolsLocalization::CONFIG_STORE);

    $variables = array_filter([
      'filters' => $args['filters'] ?? [],
      'queryFilters' => $args['queryFilters'] ?? [],
      'text' => $args['text'] ?? NULL,
      'locale' => empty($args['text']) ? NULL : ($args['locale'] ?? $localeLanguage),
      'facets' => isset($args['facets']) ? array_map(function ($facet) {
        return $facet['graphql'];
      }, $args['facets']) : NULL,
      'where' => isset($args['where']) ? $this->getProductsWhere($args['where']) : NULL,
      'limit' => $args['limit'] ?? 10,
      'offset' => $args['offset'] ?? 0,
      'sort' => $args['sort'] ?? NULL,
      'sorts' => $args['sorts'] ?? NULL,
      'storeProjection' => $localeStore ?: NULL,
    ]);

    $response = $this->ctApi->executeGraphQlOperation($query, $variables);
    $result = $response->getData();
    $productDataList = $result['productProjectionSearch'] ?? ['results' => []];
    foreach ($productDataList['results'] as &$productData) {
      $productData = $this->productDataToProduct($productData);
    }
    if (isset($productDataList['facets'])) {
      foreach ($productDataList['facets'] as &$facet) {
        $configKey = array_search($facet['facet'], array_column($args['facets'], 'path'));
        $facet['label'] = $args['facets'][$configKey]['label'];
      }
    }

    return $this->ctApi->prepareResponse($productDataList, $response->getCacheableMetadata());
  }

  /**
   * Add facets to the query.
   */
  public function addFacets(Query $query) {
    $termsNode = new Node('terms');
    $termsNode->use('term', 'productCount');

    // Add range facets (for price ranges).
    $rangesNode = new Node('ranges');
    $rangesNodeType = $rangesNode->use('type');
    $rangesNodeType->on('RangeCountDouble')->use('min', 'max');

    $facetsQuery = $query->facets([]);
    $facetsQuery->use('facet');
    $facetsQueryValue = $facetsQuery->value([]);

    $facetsQueryValue->on('TermsFacetResult')->use($termsNode);
    $facetsQueryValue->on('RangeFacetResult')->use($rangesNode);
  }

  /**
   * Constructs a GraphQL query for retrieving products.
   *
   * @return \GraphQL\Actions\Query
   *   The constructed GraphQL query.
   */
  public function getProductsQuery() {
    $arguments = [
      'limit' => new Variable('limit', 'Int'),
      'offset' => new Variable('offset', 'Int'),
      'where' => new Variable('where', 'String'),
      'sort' => new Variable('sort', '[String!]'),
      'skus' => new Variable('skus', '[String!]'),
    ];
    $query = new Query('products', $arguments);
    $query->use('total');
    $query->results([])->use('id');

    return $query;
  }

  /**
   * Constructs a GraphQL query for the search products.
   *
   * @return \GraphQL\Actions\Query
   *   The constructed GraphQL query.
   */
  public function getProductsSearchQuery() {
    $arguments = [
      'limit' => new Variable('limit', 'Int'),
      'offset' => new Variable('offset', 'Int'),
      'sorts' => new Variable('sorts', '[String!]'),
      'facets' => new Variable('facets', '[SearchFacetInput!]'),
      'filters' => new Variable('filters', '[SearchFilterInput!]'),
      'queryFilters' => new Variable('queryFilters', '[SearchFilterInput!]'),
      'text' => new Variable('text', 'String'),
      'locale' => new Variable('locale', 'Locale'),
      'storeProjection' => new Variable('storeProjection', 'String'),
    ];
    $query = new Query('productProjectionSearch', $arguments);
    $query->use('total');
    $query->results([])->use('id');
    return $query;
  }

  /**
   * Retrieves a product by its slug.
   *
   * @param string $slug
   *   The slug of the product.
   *
   * @return \Drupal\commercetools\CacheableCommercetoolsResponse
   *   The response containing the product data.
   */
  public function getProductBySlug(
    string $slug,
  ) {
    $filter = [
      'slug' => $slug,
    ];
    $query = $this->getProductsQuery();
    $variables = [
      'limit' => 1,
      'where' => $this->getProductsWhere($filter),
    ];

    $this->addProductFields(
      node: $query->results([]),
      includeDetails: TRUE,
      includeVariants: TRUE,
    );
    $resultCacheable = $this->ctApi->executeGraphQlOperation($query, $variables);
    $result = $resultCacheable->getData();
    if (count($result['products']['results']) === 0) {
      throw new CommercetoolsResourceNotFoundException($slug);
    }
    $productData = $result['products']['results'][0];
    $productDetails = $this->productDataToProduct($productData);

    return $this->ctApi->prepareResponse($productDetails, $resultCacheable->getCacheableMetadata());
  }

  /**
   * Converts the raw product data response to a simplified product array.
   *
   * @param array $productData
   *   The product data array.
   *
   * @return array
   *   The converted product array.
   */
  protected function productDataToProduct(array $productData): array {
    $product = $productData['masterData']['current'] ?? $productData;
    $product['id'] = $productData['id'];
    $product['type'] = $productData['productType'];
    $product['masterVariant'] = $this->variantDataToVariant($product['masterVariant'], $product['type']['key']);
    $product += $product['masterVariant'];
    if (isset($product['variants'])) {
      foreach ($product['variants'] as &$productDataVariant) {
        $productDataVariant = $this->variantDataToVariant($productDataVariant, $product['type']['key']);
      }
    }
    return $product;
  }

  /**
   * Converts the raw variant data response to a simplified variant array.
   *
   * @param array $variant
   *   The variant data variant array.
   * @param string $productType
   *   The product type.
   *
   * @return array
   *   The converted variant array.
   */
  public function variantDataToVariant(array $variant, string $productType): array {
    if ($variant['price']) {
      $price = $this->ct->formatPrice($variant['price']['value']);
      if (isset($variant['price']['discounted'])) {
        $price['discounted'] = $this->ct->formatPrice($variant['price']['discounted']['value']);
      }
      $variant['price'] = $price;
    }
    if (isset($variant['availability'])) {
      $variant['availability'] = $variant['availability']['noChannel']['availableQuantity'] ?? 0;
    }
    $variant['attributes'] = $this->attributesDataToAttributes($variant['attributesRaw'], $productType);
    unset($variant['attributesRaw']);

    return $variant;
  }

  /**
   * Converts the attributes data response to a simplified attribute array.
   *
   * @param array $attrs
   *   The attributes data attributes array.
   * @param string $productType
   *   The product type.
   *
   * @return array
   *   The converted attributes array.
   */
  protected function attributesDataToAttributes(array $attrs, string $productType): array {
    $productTypesDefinition = $this->ct->getProductsTypes();
    $productTypeDefinition = $productTypesDefinition[$productType] ?? [];

    $variantAttrs = [];
    foreach ($attrs as $attr) {
      if (empty($productTypeDefinition['attributeDefinitions'][$attr['name']])) {
        continue;
      }

      $attrDefinition = $productTypeDefinition['attributeDefinitions'][$attr['name']];

      switch ($attrDefinition['type']) {
        case 'enum':
          $attr['labelValue'] = $attr['value']['label'];
          $attr['value'] = $attr['value']['key'];
          break;

        case 'lenum':
          $attr['labelValue'] = $this->ct->getTranslationValue($attr['value']['label']);
          $attr['value'] = $attr['value']['key'];
          break;

        case 'ltext':
          $attr['value'] = $attr['labelValue'] = $this->ct->getTranslationValue($attr['value']);
          break;

        default:
          $attr['labelValue'] = $attr['value'];
          break;
      }
      $attr['label'] = $attrDefinition['label'];

      $variantAttrs[$attr['name']] = $attr;
    }

    return $variantAttrs;
  }

  /**
   * Adds product fields to a GraphQL node.
   *
   * @param \GraphQL\Entities\Node $node
   *   The GraphQL node.
   * @param bool|null $includeDetails
   *   Whether to include detailed product information. Defaults to NULL.
   * @param bool|null $includeVariants
   *   Whether to include product variants. Defaults to NULL.
   * @param string|null $variantSku
   *   The SKU of the variant to include. Defaults to NULL.
   * @param array|null $extraFields
   *   Additional fields to include. Defaults to NULL.
   *
   * @return \GraphQL\Entities\Node
   *   The modified GraphQL node.
   */
  protected function addProductFields(
    Node $node,
    ?bool $includeDetails = NULL,
    ?bool $includeVariants = NULL,
    ?string $variantSku = NULL,
    ?array $extraFields = NULL,
  ) {
    $product = $node->getRootNode()->getName() === 'products' ? $node->masterData([])->current([]) : $node;
    $node->use('id');
    $node->productType([])->use('id', 'key', 'name');
    $this->addProductCommonFields($product);
    if ($includeDetails) {
      $this->addProductDetailsFields($product);
    }

    if ($variantSku) {
      $productVariant = $product->variant(['sku' => $variantSku]);
      $this->addProductVariantFields($productVariant);
    }
    else {
      $productVariant = $product->masterVariant([]);
    }

    $this->addProductVariantFields($productVariant);

    if ($includeVariants) {
      $productVariants = $product->variants([]);
      $this->addProductVariantFields($productVariants);
    }

    return $node;
  }

  /**
   * Adds common fields to a product node.
   *
   * @param \GraphQL\Entities\Node $product
   *   The product node to which the fields will be added.
   *
   * @return \GraphQL\Entities\Node
   *   The product node with the added fields.
   */
  protected function addProductCommonFields(Node $product): Node {
    $this->ct->addFieldsToNode($product, [
      'slug',
      'name',
    ], self::FIELDS_TRANSLATABLE_PRODUCT);
    return $product;
  }

  /**
   * Adds detailed fields to a product data node.
   *
   * @param \GraphQL\Entities\Node $productData
   *   The product data node to which the fields will be added.
   *
   * @return \GraphQL\Entities\Node
   *   The product data node with the added fields.
   */
  protected function addProductDetailsFields(Node $productData): Node {
    $this->ct->addFieldsToNode($productData, [
      'description',
      'metaTitle',
      'metaDescription',
      'metaKeywords',
      'skus',
    ], self::FIELDS_TRANSLATABLE_PRODUCT);
    return $productData;
  }

  /**
   * Adds variant fields to a product variant node.
   *
   * @param \GraphQL\Entities\Node $productVariant
   *   The product variant node to which the fields will be added.
   *
   * @return \GraphQL\Entities\Node
   *   The product variant node with the added fields.
   */
  public function addProductVariantFields(Node $productVariant): Node {
    $productVariant->use(
      'id',
      'sku',
    );
    $productVariant->images([])->use('url', 'label');
    $this->addAttributes($productVariant);
    $this->ct->addPriceNode($productVariant, TRUE);
    $this->addAvailabilityField($productVariant);
    return $productVariant;
  }

  /**
   * Adds an availability field to a product variant node.
   *
   * @param \GraphQL\Entities\Node $productVariant
   *   The product variant node to which the price field will be added.
   */
  protected function addAvailabilityField(Node $productVariant): Node {
    $productVariant->availability([])->noChannel([])->use('availableQuantity');
    return $productVariant;
  }

  /**
   * Adds an attributesRaw field to a product variant node.
   *
   * @param \GraphQL\Entities\Node $productVariant
   *   The product variant node to which the price field will be added.
   */
  protected function addAttributes(Node $productVariant): Node {
    $includeAttributes = [];
    foreach ($this->ct->getEnabledAttributes() as $attributes) {
      foreach ($attributes as $attribute) {
        $includeAttributes[] = $attribute['name'];
      }
    }

    $productVariant->attributesRaw(['includeNames' => $includeAttributes])->use('name', 'value');
    return $productVariant;
  }

  /**
   * Constructs a where clause string for a product query.
   *
   * @param array $filter
   *   An associative array of filters to apply to the product query.
   *
   * @return string
   *   The constructed where clause string.
   */
  protected function getProductsWhere(array $filter): string {
    $where = [
      'masterData' => [
        'current' => [],
      ],
    ];
    foreach ($filter as $name => $value) {
      $where['masterData']['current'][$name] = $this->ct->getWhereValue($name, $value, self::FIELDS_TRANSLATABLE_PRODUCT);
    }
    return CommercetoolsService::whereToString($where);
  }

}

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

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