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

src/CommercetoolsApiService.php
<?php

namespace Drupal\commercetools;

use Commercetools\Api\Client\ClientCredentialsConfig;
use Commercetools\Api\Client\Config;
use Commercetools\Api\Models\GraphQl\GraphQLRequestBuilder;
use Commercetools\Api\Models\GraphQl\GraphQLVariablesMapModel;
use Commercetools\Client\ApiRequestBuilder;
use Commercetools\Client\ClientCredentials;
use Commercetools\Client\MiddlewareFactory;
use Commercetools\Client\OAuth2Handler;
use Commercetools\Client\OAuthHandlerFactory;
use Commercetools\Exception\InvalidArgumentException;
use Commercetools\Exception\NotFoundException;
use Drupal\commercetools\Cache\CacheableCommercetoolsGraphQlResponse;
use Drupal\commercetools\Event\CommercetoolsGraphQlOperationEvent;
use Drupal\commercetools\Event\CommercetoolsGraphQlOperationResultEvent;
use Drupal\commercetools\Exception\CommercetoolsGraphqlAccessException;
use Drupal\commercetools\Exception\CommercetoolsGraphqlErrorException;
use Drupal\commercetools\Exception\CommercetoolsOperationFailedException;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Cache\UseCacheBackendTrait;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Http\ClientFactory;
use Drupal\Core\Logger\LoggerChannelTrait;
use GraphQL\Entities\Node;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\HandlerStack;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface as PsrSimpleCacheInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

/**
 * Commercetools API service.
 */
class CommercetoolsApiService implements CommercetoolsApiServiceInterface {

  use LoggerChannelTrait;
  use UseCacheBackendTrait;

  const COMMERCETOOLS_API_HOST = 'commercetools.com';

  /**
   * Guzzle Http client with Commercetools middlewares.
   *
   * @var \GuzzleHttp\ClientInterface|null
   */
  protected ?ClientInterface $client;

  /**
   * Auth Config.
   *
   * @var \Commercetools\Api\Client\ClientCredentialsConfig
   */
  protected ClientCredentialsConfig $authConfig;

  /**
   * Auth Handler.
   *
   * @var \Commercetools\Client\OAuth2Handler
   */
  protected OAuth2Handler $authHandler;

  /**
   * The current connection credentials config.
   *
   * @var array
   */
  protected array $connectionConfig;

  /**
   * CommercetoolsApiService constructor.
   *
   * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $eventDispatcher
   *   An event dispatcher instance.
   * @param \Drupal\commercetools\CommercetoolsConfiguration $ctConfig
   *   The commercetools configuration service.
   * @param \Psr\Log\LoggerInterface $logger
   *   The logger service.
   * @param \Drupal\Core\Http\ClientFactory $httpClientFactory
   *   The HTTP client factory.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cacheBackend
   *   A cache backend.
   * @param \Psr\SimpleCache\CacheInterface $psrCache
   *   The cache instance for the Commercetools Auth.
   * @param \Drupal\Component\Datetime\TimeInterface $time
   *   The time service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The configuration factory.
   */
  public function __construct(
    protected readonly EventDispatcherInterface $eventDispatcher,
    protected readonly CommercetoolsConfiguration $ctConfig,
    protected readonly LoggerInterface $logger,
    protected readonly ClientFactory $httpClientFactory,
    CacheBackendInterface $cacheBackend,
    protected readonly PsrSimpleCacheInterface $psrCache,
    protected readonly TimeInterface $time,
    protected readonly ConfigFactoryInterface $configFactory,
  ) {
    $this->cacheBackend = $cacheBackend;
    $this->connectionConfig = $ctConfig->getConnectionConfig();
  }

  /**
   * {@inheritdoc}
   */
  public function setOverriddenConfig(array $customConfig): void {
    $this->connectionConfig = array_merge($this->connectionConfig, $customConfig);
    $this->resetConnection();
  }

  /**
   * {@inheritdoc}
   */
  public function restoreOriginalConfig(): void {
    $this->connectionConfig = $this->ctConfig->getConnectionConfig();
    $this->resetConnection();
  }

  /**
   * {@inheritdoc}
   */
  public function getConnectionConfig(): array {
    return $this->connectionConfig;
  }

  /**
   * {@inheritdoc}
   */
  public function resetConnection(): void {
    $this->psrCache->clear();
    $this->client = NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getClient(): ClientInterface {
    // Return the client if it's already initialized and no rebuild required.
    if (isset($this->client)) {
      return $this->client;
    }

    // Set up the OAuth handler and middlewares.
    $this->authHandler = $this->getAuthHandler();
    $clientConfig = new Config([], $this->getApiUrl());
    $options = $clientConfig->getOptions();
    $options['handler'] = HandlerStack::create();

    // A copy-paste from
    // Commercetools\Client\ClientFactory::createGuzzleClientForHandler()
    // to use a customized Guzzle client in our case.
    $enableLogs = $this->configFactory->get(CommercetoolsService::CONFIGURATION_NAME)->get(CommercetoolsService::CONFIG_LOG_CT_REQUESTS);
    $middlewares = MiddlewareFactory::createDefaultMiddlewares(
      $this->authHandler,
      $enableLogs ? $this->logger : NULL,
      (int) ($options['maxRetries'] ?? 0)
    );
    // A copy-paste from
    // Commercetools\Client\ClientFactory::createGuzzleClientWithOptions()
    // to use a customized Guzzle client in our case.
    foreach ($middlewares as $key => $middleware) {
      if (!is_callable($middleware)) {
        throw new InvalidArgumentException('Middleware isn\'t callable');
      }
      $name = is_numeric($key) ? '' : $key;
      $options['handler']->push($middleware, $name);
    }

    $this->client = $this->httpClientFactory->fromOptions($options);
    return $this->client;
  }

  /**
   * Builds an OAuth2 handler for commercetools using client credentials.
   *
   * @param string|array|null $scope
   *   Optional scope override (e.g., "manage_project"). If omitted, the value
   *   from the connection configuration is used. The project key is appended
   *   automatically.
   *
   * @return \Commercetools\Client\OAuth2Handler
   *   A ready-to-use OAuth2 handler.
   */
  public function getAuthHandler(string|array|null $scope = NULL): OAuth2Handler {
    $config = $this->connectionConfig;
    $scope = $scope ?? ($config[self::CONFIG_SCOPE] ?? NULL);
    if (!empty($scope)) {
      $scope = is_array($scope)
        ? $scope
        : explode(' ', $scope);
    }
    // The commercetools API requires the project key included in the scope.
    $scopeWithProjectId = !empty($scope)
      ? implode(' ', array_map(static fn($s) => "{$s}:{$config[self::CONFIG_PROJECT_KEY]}", $scope))
      : NULL;

    $clientCredentials = new ClientCredentials(
      $config[self::CONFIG_CLIENT_ID],
      $config[self::CONFIG_CLIENT_SECRET],
      $scopeWithProjectId
    );

    // @todo Generate the HTTP client options from the factory using the method
    // stubAlterConfigOptions() to improve the test coverage.
    $httpClientOptions = [];
    $authConfig = new ClientCredentialsConfig($clientCredentials, $httpClientOptions, $this->getApiUrl('auth'));
    return OAuthHandlerFactory::ofAuthConfig($authConfig, $this->psrCache);
  }

  /**
   * {@inheritdoc}
   */
  public function isAccessConfigured(): bool {
    return $this->ctConfig->isConnectionConfigured();
  }

  /**
   * {@inheritdoc}
   */
  public function getBuilder(): ApiRequestBuilder {
    return new ApiRequestBuilder($this->connectionConfig[self::CONFIG_PROJECT_KEY], $this->getClient());
  }

  /**
   * {@inheritdoc}
   */
  public function getApiUrl(string $type = 'api'): string {
    $region = $this->connectionConfig[self::CONFIG_HOSTED_REGION];
    $host = $region . '.' . self::COMMERCETOOLS_API_HOST;
    return match ($type) {
      'region' => $region,
      'api' => "https://api.$host",
      'auth' => "https://auth.$host/oauth/token",
      'session' => "https://session.$host",
      default => throw new InvalidArgumentException('Invalid API endpoint type'),
    };
  }

  /**
   * Generates a cache key for the GraphQL request.
   *
   * @param string $query
   *   A GraphQL query.
   * @param array $variables
   *   An array of variables.
   * @param \Commercetools\Client\ApiRequestBuilder $builder
   *   The ApiRequestBuilder.
   *
   * @return string
   *   A cache key.
   */
  protected function getGraphQlRequestCacheKey(string $query, array $variables, ApiRequestBuilder $builder) {
    $hash = sha1(Json::encode([
      'request' => $query,
      'variables' => $variables,
      'builderArgs' => $builder->me()->getArgs(),
    ]));
    // @todo Make a constant with the prefix.
    return 'commercetools_graphql_' . $hash;
  }

  /**
   * Converts a max-age value to a real "expire" value for the Cache API.
   *
   * @param int $maxAge
   *   A max-age value.
   *
   * @return int
   *   A corresponding "expire" timestamp or permanent constant.
   *
   * @see \Drupal\Core\Cache\Cache::PERMANENT
   */
  protected function cacheMaxAgeToExpire(int $maxAge): int {
    if ($maxAge !== Cache::PERMANENT) {
      return $this->time->getRequestTime() + $maxAge;
    }
    return $maxAge;
  }

  /**
   * {@inheritdoc}
   */
  public function executeGraphQlOperation(
    string $query,
    ?array $variables = NULL,
    ?CacheableMetadata $cacheMetadata = NULL,
  ): CacheableCommercetoolsGraphQlResponse {
    $variables ??= [];
    $operationAllowed = TRUE;

    // Handle cache metadata and apply overrides if any.
    $cacheDefaultTtl = $this->connectionConfig[self::CONFIG_CACHE_RESPONSES_TTL];
    if ($cacheMetadata instanceof CacheableDependencyInterface) {
      $cacheableMetadata = CacheableMetadata::createFromObject($cacheMetadata);
      if (
        $cacheMetadata->getCacheMaxAge() == Cache::PERMANENT
        && $cacheDefaultTtl != Cache::PERMANENT
      ) {
        $cacheableMetadata->setCacheMaxAge($cacheDefaultTtl);
      }
    }
    else {
      $cacheableMetadata = new CacheableMetadata();
      $cacheableMetadata->setCacheMaxAge($cacheDefaultTtl);
    }

    // Allow changing variables and query cacheable metadata before execution.
    $event = new CommercetoolsGraphQlOperationEvent($variables, $operationAllowed, $cacheableMetadata, $query);
    $this->eventDispatcher->dispatch($event);

    if ($operationAllowed === FALSE) {
      throw new CommercetoolsGraphqlAccessException();
    }

    // Get the builder and use it to generate the cache ID and build request.
    $builder = $this->getBuilder();

    if ($cacheableMetadata->getCacheMaxAge() != 0) {
      $cid = $this->getGraphQlRequestCacheKey($query, $variables, $builder);
      if ($cachedResponse = $this->cacheGet($cid)) {
        return CacheableCommercetoolsGraphQlResponse::createFromCachedObject($cachedResponse);
      }
    }

    $graphQLRequest = GraphQLRequestBuilder::of()->withQuery($query);
    if ($variables) {
      $graphQLRequest->withVariables(GraphQLVariablesMapModel::of($variables));
    }
    $apiRequest = $builder->graphql()->post($graphQLRequest->build());
    try {
      $apiResponse = $apiRequest->execute();
      if ($errors = $apiResponse->getErrors()) {
        throw new CommercetoolsGraphqlErrorException($errors, $query, $variables);
      }
      $apiResponseData = $apiResponse->getData();
      if (empty($apiResponseData)) {
        throw new CommercetoolsOperationFailedException('The commercetools API returned an empty response.');
      }
    }
    catch (ClientException | ConnectException | NotFoundException | RequestException $e) {
      throw new CommercetoolsOperationFailedException(previous: $e);
    }

    // Converting nested object to nested associative array.
    $result = (array) Json::decode(Json::encode($apiResponseData));

    // Allow to change the query result after execution.
    $event = new CommercetoolsGraphQlOperationResultEvent($result, $cacheableMetadata, $query, $variables);
    $this->eventDispatcher->dispatch($event);

    $cacheMaxAge = $cacheableMetadata->getCacheMaxAge();
    if ($cacheMaxAge != 0) {
      $this->cacheSet(
        $cid ?? $this->getGraphQlRequestCacheKey($query, $variables, $builder),
        $result,
        $this->cacheMaxAgeToExpire($cacheMaxAge),
        $cacheableMetadata->getCacheTags()
      );
    }

    $response = new CacheableCommercetoolsGraphQlResponse($result);
    $response->addCacheableDependency($cacheableMetadata);

    return $response;
  }

  /**
   * {@inheritdoc}
   */
  public function getProjectInfo(): array {
    $query = new Node('');
    $query->project([])
      ->use('key', 'name', 'countries', 'currencies', 'languages');
    $query->customerGroups([])->results([])->use('name', 'id');
    $query->channels([])->results([])->use('id')->nameAllLocales([])->use('locale', 'value');
    $cacheMetadata = new CacheableMetadata();
    $cacheMetadata->setCacheMaxAge(0);
    // Try to retrieve stores list.
    try {
      $storeQuery = clone $query;
      $storeQuery->stores([])->results([])->use('key')->nameAllLocales([])->use('locale', 'value');
      $result = $this->executeGraphQlOperation($storeQuery->toString(), cacheMetadata: $cacheMetadata)
        ->getData();
    }
    catch (CommercetoolsGraphqlErrorException) {
      $result = $this->executeGraphQlOperation($query->toString(), cacheMetadata: $cacheMetadata)
        ->getData();
    }

    return $this->buildProjectInfo($result);
  }

  /**
   * Prepares a project info.
   *
   * @param array $data
   *   The project info from API response.
   *
   * @return array
   *   A prepared project info list.
   */
  private function buildProjectInfo(array $data): array {
    $projectInfo = [
      'stores' => [],
    ];
    $projectInfo += $data['project'] ?? [];
    foreach ($data['channels']['results'] ?? [] as $channel) {
      foreach ($channel['nameAllLocales'] as $locale) {
        $projectInfo['channels'][$channel['id']][$locale['locale']] = $locale['value'];
      }
    }
    foreach ($data['stores']['results'] ?? [] as $store) {
      foreach ($store['nameAllLocales'] as $locale) {
        $projectInfo['stores'][$store['key']][$locale['locale']] = $locale['value'];
      }
    }
    return $projectInfo;
  }

  /**
   * {@inheritdoc}
   */
  public function prepareResponse(array $data, ?RefinableCacheableDependencyInterface $cache = NULL): CacheableCommercetoolsResponse {
    $response = new CacheableCommercetoolsResponse($data);
    if ($cache) {
      $response->addCacheableDependency($cache);
    }
    return $response;
  }

}

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

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