commercetools-8.x-1.2-alpha1/modules/commercetools_demo/src/DemoConfigurationDeployer.php

modules/commercetools_demo/src/DemoConfigurationDeployer.php
<?php

namespace Drupal\commercetools_demo;

use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DefaultContent\Existing;
use Drupal\Core\DefaultContent\Finder;
use Drupal\Core\DefaultContent\Importer;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\File\FileExists;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Routing\RouteBuilderInterface;
use Drupal\Core\State\StateInterface;
use Drupal\commercetools\CommercetoolsConfiguration;
use Drupal\commercetools\CommercetoolsLocalization;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\NodeInterface;

/**
 * Deploys demo Configuration presets.
 */
class DemoConfigurationDeployer {
  private const CONFIG_DIRECTORY_PATH = 'config' . DIRECTORY_SEPARATOR . 'demo';

  private const CREDENTIALS_DIRECTORY_PATH = 'fixtures' . DIRECTORY_SEPARATOR . 'demo_accounts';
  private const THEMES_CONFIG_DIRECTORY_PATH = 'fixtures' . DIRECTORY_SEPARATOR . 'themes_config';
  private const UI_COMPONENTS_DIRECTORY_PATH = 'fixtures' . DIRECTORY_SEPARATOR . 'ui_components';
  private const DEMO_COMPONENTS_DIRECTORY_PATH = 'fixtures' . DIRECTORY_SEPARATOR . 'demo_components';

  private const DEMO_PRODUCTS_DIRECTORY_PATH = [
    'b2c_lifestyle' => self::CREDENTIALS_DIRECTORY_PATH . DIRECTORY_SEPARATOR . 'b2c_lifestyle',
    'b2b_machinery' => self::CREDENTIALS_DIRECTORY_PATH . DIRECTORY_SEPARATOR . 'b2b_machinery',
  ];

  private const STATE_KEY_UI_DEPLOYMENT_PREFIX = 'commercetools_demo.ui_deployment.';

  private const NODE_SUBDIR = 'node_templates';
  private const BLOCK_SUBDIR = 'block_content';

  private const LAYOUT_BUILDER_FIELD_NAME = 'layout_builder__layout';

  private const NODE_DEMO_TEMPLATE = 'node.template_demo_products';

  /**
   * CommercetoolsCarts constructor.
   *
   * @param \Drupal\Core\Routing\RouteBuilderInterface $routerBuilder
   *   The router builder.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Drupal\Core\State\StateInterface $state
   *   The Drupal state storage service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The config factory.
   * @param \Drupal\commercetools\CommercetoolsConfiguration $ctConfig
   *   The commercetools configuration service.
   * @param \Drupal\commercetools\CommercetoolsLocalization $ctLocalization
   *   The commercetools localization service.
   * @param \Drupal\Core\DefaultContent\Importer $importer
   *   Core content Importer.
   * @param \Drupal\Core\File\FileSystemInterface $fileSystem
   *   File system wrapper.
   * @param \Drupal\Component\Uuid\UuidInterface $uuid
   *   Uuid generator.
   */
  public function __construct(
    protected readonly RouteBuilderInterface $routerBuilder,
    protected readonly EntityTypeManagerInterface $entityTypeManager,
    protected readonly StateInterface $state,
    protected readonly ConfigFactoryInterface $configFactory,
    protected readonly CommercetoolsConfiguration $ctConfig,
    protected readonly CommercetoolsLocalization $ctLocalization,
    protected readonly Importer $importer,
    protected readonly FileSystemInterface $fileSystem,
    protected readonly UuidInterface $uuid,
  ) {
  }

  /**
   * Provides a default theme machine name.
   *
   * @return string
   *   A default theme machine name.
   */
  public function getDefaultTheme(): string {
    return $this->configFactory
      ->get('system.theme')
      ->get('default');
  }

  /**
   * Provides a list of YAML encoded presets in a directory.
   *
   * @param string $directory
   *   The absolute path to a directory to seek in.
   *
   * @return array
   *   A list of suggested themes keyed by name.
   */
  protected function getPresets(string $directory): array {
    $presets = [];
    $files = scandir($directory);
    foreach ($files as $file) {
      if (!preg_match('#^(.+)\.yml#', $file, $matches)) {
        continue;
      }
      $name = $matches[1];
      $presets[$name] = (array) Yaml::decode(file_get_contents($directory . DIRECTORY_SEPARATOR . $file));
    }

    return $presets;
  }

  /**
   * Provides a list of suggested themes.
   *
   * @return array
   *   A list of suggested themes keyed by machine name.
   */
  public function getSuggestedThemes(): array {
    $themesDirectory = dirname(__DIR__) . DIRECTORY_SEPARATOR . self::THEMES_CONFIG_DIRECTORY_PATH;
    return $this->getPresets($themesDirectory);
  }

  /**
   * Provides the list of demo account credentials.
   *
   * @return array
   *   An array with available demo configurations.
   */
  public function getDemoAccounts(): array {
    static $accounts = [];
    if (empty($accounts)) {
      $accountsDirectory = dirname(__DIR__) . DIRECTORY_SEPARATOR . self::CREDENTIALS_DIRECTORY_PATH;
      $accounts = $this->getPresets($accountsDirectory);
    }
    return $accounts;
  }

  /**
   * Deploys demo account configuration.
   *
   * @param string $account
   *   The demo account ID.
   */
  public function deployDemoAccount(string $account): void {
    $this->unsetConnectionConfig();
    $demoAccount = $this->getDemoAccounts()[$account];
    $demoConfig = $demoAccount['config'];
    foreach ($demoConfig as $configName => $values) {
      $config = $this->configFactory->getEditable($configName);
      if ($config) {
        foreach ($values as $key => $value) {
          $config->set($key, $value);
        }
      }
      $config->save();
    }
  }

  /**
   * Cleans up connection configuration.
   */
  public function unsetConnectionConfig(): void {
    $config = $this->configFactory->getEditable(CommercetoolsConfiguration::CONFIGURATION_API);
    $keys = $this->ctConfig->listCredentialKeys();
    // API scope is also related to credentials.
    $keys[] = 'scope';
    foreach ($keys as $key) {
      $config->set($key, '');
    }
    $config->save();
  }

  /**
   * Provides the demo configuration options for the module.
   *
   * @param string $module
   *   The module name.
   *
   * @return array
   *   The config values.
   */
  public function getDemoConfig(string $module): array {
    $fileName = "{$module}.settings.yml";
    $filePath = dirname(__DIR__) . DIRECTORY_SEPARATOR . self::CONFIG_DIRECTORY_PATH . DIRECTORY_SEPARATOR . $fileName;
    return (array) Yaml::decode(file_get_contents($filePath));
  }

  /**
   * Overrides configuration with demo values for the module.
   *
   * @param string $module
   *   The module name.
   */
  public function setDemoConfig(string $module): void {
    $demoConfig = $this->getDemoConfig($module);
    $configName = "{$module}.settings";
    $config = $this->configFactory->getEditable($configName);
    foreach ($demoConfig as $key => $value) {
      $config->set($key, $value);
    }
    $config->save();
    // We need to rebuild the router to apply the catalog path changes.
    $this->routerBuilder->rebuild();
  }

  /**
   * Provides the list of demo UI components for a module.
   *
   * @param string $module
   *   The UI module name.
   *
   * @return array
   *   The list of UI components.
   */
  public function getDemoComponents(string $module): array {
    $directory = match ($module) {
      'commercetools_demo' => self::DEMO_COMPONENTS_DIRECTORY_PATH,
      default => self::UI_COMPONENTS_DIRECTORY_PATH,
    };
    $componentsDirectory = dirname(__DIR__) . DIRECTORY_SEPARATOR . $directory;
    $components = $this->getPresets($componentsDirectory);
    foreach ($components as &$component) {
      // Check a component affiliation.
      if (!empty($component['_commercetools_metadata'])) {
        $metadata = $component['_commercetools_metadata'];
        // Allow-list.
        if (!empty($metadata['target_modules']) && !in_array($module, $metadata['target_modules'])) {
          continue;
        }
        // Deny-list.
        if (!empty($metadata['ignore_modules']) && in_array($module, $metadata['ignore_modules'])) {
          continue;
        }
        unset($component['_commercetools_metadata']);
      }
    }
    return $components;
  }

  /**
   * Checks if deployment is done for the module.
   *
   * @param string $module
   *   The UI module name.
   *
   * @return bool
   *   A deployment status.
   */
  public function isDemoComponentsDeployedFor(string $module): bool {
    $deployed = $this->state->get(self::STATE_KEY_UI_DEPLOYMENT_PREFIX . $module);
    if (!$deployed) {
      return FALSE;
    }
    foreach ($deployed as $id => $uuid) {
      preg_match('/(?<type>.+)\.(?<name>.+)/', $id, $matches);
      try {
        $storage = $this->entityTypeManager->getStorage($matches['type']);
        /** @var \Drupal\Core\Entity\EntityInterface $entity */
        $entity = $storage->loadByProperties(['uuid' => $uuid]);
        if (!$entity) {
          return FALSE;
        }
      }
      catch (PluginNotFoundException) {
        return FALSE;
      }
    }
    return TRUE;
  }

  /**
   * Gets Demo page for module.
   *
   * @param string $module
   *   The UI module name.
   *
   * @return null|NodeInterface
   *   Node or NULL.
   */
  public function getDemoPage(string $module): ?NodeInterface {
    $deployed = $this->state->get(self::STATE_KEY_UI_DEPLOYMENT_PREFIX . $module);
    if (!$deployed || empty($deployed[self::NODE_DEMO_TEMPLATE])) {
      return NULL;
    }
    $demoProductsPage = $this->entityTypeManager->getStorage('node')
      ->loadByProperties(['uuid' => $deployed[self::NODE_DEMO_TEMPLATE]]);

    if (!empty($demoProductsPage)) {
      return reset($demoProductsPage);
    }

    return NULL;
  }

  /**
   * Creates a new entity by type and data given.
   *
   * @param string $type
   *   The entity type ID.
   * @param array $values
   *   The entity properties as associative array.
   *
   * @return \Drupal\Core\Entity\EntityInterface|null
   *   A new entity or NULL if skipped.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function deployEntity(string $type, array $values): ?EntityInterface {
    switch ($type) {
      case 'configurable_language':
        // Entity already exist - skip deployment.
        if (!$entity = ConfigurableLanguage::load($values['id'])) {
          $entity = ConfigurableLanguage::createFromLangcode($values['id']);
        }
        foreach ($values as $key => $value) {
          $entity->set($key, $value);
        }
        break;

      default:
        $storage = $this->entityTypeManager->getStorage($type);
        $entity = $storage->create($values);
    }
    try {
      if (!empty($entity)) {
        $entity->save();
      }
    }
    catch (EntityStorageException) {
      if ($storage instanceof EntityStorageInterface) {
        $entityDuplicate = $storage->load($entity->id());
        $entityDuplicate->delete();
        $entity->save();
      }
    }
    return $entity;
  }

  /**
   * Collects all values for replacing in variables.
   *
   * @param string $module
   *   The UI module name.
   * @param array $variables
   *   The variables to replace in values as [KEY]. Optional.
   *
   * @return array
   *   The list of replacements keyed by variable name.
   */
  protected function prepareVariables(string $module, array $variables = []): array {
    $moduleSettings = $this->configFactory->get("{$module}.settings");
    $variables += [
      'module' => $module,
      'module_label' => match ($module) {
        'commercetools_demo' => 'commercetools Demo',
        'commercetools_content' => 'commercetools Content',
        'commercetools_decoupled' => 'commercetools Decoupled',
        default => $module,
      },
      'catalog_path' => $moduleSettings->get('catalog_path'),
      ...$this->getThemeVariables(),
    ];

    return $variables;
  }

  /**
   * Collects all values for replacing in variables.
   *
   * @param array $component
   *   The component properties as associative array.
   * @param string $module
   *   The UI module name.
   * @param array $variables
   *   The variables to replace in values as [KEY]. Optional.
   *
   * @return array
   *   The prepared component.
   */
  protected function prepareComponentConfig(array $component, string $module, array $variables = []): array {
    $component = $this->replaceValuesVariables(
      $component,
      $this->prepareVariables($module, $variables),
    );
    $component += [
      'dependencies' => [
        'enforced' => [
          'module' => [
            'commercetools_demo',
            $module,
          ],
        ],
      ],
    ];
    return $component;
  }

  /**
   * Deploys demo components.
   *
   * @param string $module
   *   The UI module name.
   * @param array $variables
   *   The variables to replace in values as [KEY]. Optional.
   */
  public function deployDemoComponents(string $module, array $variables = []): void {
    $components = $this->getDemoComponents($module);
    $deployed = $this->state->get(self::STATE_KEY_UI_DEPLOYMENT_PREFIX . $module, []);
    foreach ($components as $id => $config) {
      preg_match('/(?<type>.+)\.(?<name>.+)/', $id, $matches);
      $config = $this->prepareComponentConfig($config, $module, $variables);
      $entity = $this->deployEntity($matches['type'], $config);
      if (!empty($entity)) {
        $deployed[$id] = $entity->uuid();
      }
    }
    $this->state->set(self::STATE_KEY_UI_DEPLOYMENT_PREFIX . $module, $deployed);
  }

  /**
   * Deploys demo Product page.
   *
   * @param string $module
   *   The UI module name.
   */
  public function deployDemoPages(string $module): void {
    $paths = $this->getPresetPaths();
    // Prevents page deployment without demo config.
    if (empty($paths)) {
      return;
    }
    $tempDir = $this->prepareTempDirectory(self::NODE_SUBDIR);
    $blockDir = $this->prepareTempDirectory(self::BLOCK_SUBDIR);

    $nodePresets = $this->getPresets($paths['nodes']);
    $blockPresets = $this->getPresets($paths['blocks']);

    // Create block contents.
    $blockUuids = $this->createBlockContent($blockPresets, $module, $blockDir);
    if (!empty($blockUuids)) {
      $content = new Finder($blockDir);
      $this->importer->importContent($content, Existing::Skip);
    }

    foreach ($nodePresets as $nodeId => &$nodePreset) {
      $nodePreset['_meta']['uuid'] = $this->uuid->generate();
      $nodePreset['default'] = $this->prepareComponentConfig($nodePreset['default'], $module);
      unset($nodePreset['default']['dependencies']);

      foreach ($nodePreset['default'][self::LAYOUT_BUILDER_FIELD_NAME] as $sectionId => $section) {
        // Build path per section.
        $componentsPath = [self::LAYOUT_BUILDER_FIELD_NAME, $sectionId, 'section', 'components'];
        $components = NestedArray::getValue($nodePreset['default'], $componentsPath);
        $components = $this->rebuildComponentKeys($components);
        $components = $this->processBlockReferences($components, $blockUuids, $module);
        NestedArray::setValue($nodePreset['default'], $componentsPath, $components);
      }

      $this->writeYamlToTempDir($nodePreset, $tempDir, $nodeId . '.yml');
    }

    $content = new Finder($tempDir);
    $this->importer->importContent($content, Existing::Skip);

    $contentDeployed = array_flip(
      array_map(fn ($f) => pathinfo($f['_meta']['path'], PATHINFO_FILENAME), $content->data)
    );
    $deployed = $this->state->get(self::STATE_KEY_UI_DEPLOYMENT_PREFIX . $module);
    $this->state->set(self::STATE_KEY_UI_DEPLOYMENT_PREFIX . $module, array_merge($contentDeployed, $deployed));

  }

  /**
   * Creates block contents templates.
   *
   * @param array $blockPresets
   *   Block presets.
   * @param string $module
   *   The UI module name.
   * @param string $blockDir
   *   Block directory.
   *
   * @return array
   *   Block uuids.
   */
  private function createBlockContent(array $blockPresets, string $module, string $blockDir): array {
    $blockUuids = [];
    foreach ($blockPresets as $name => &$blockPreset) {
      $blockUuids[$name] = $this->uuid->generate();
      $block = $this->prepareComponentConfig(
        $blockPreset,
        $module,
        ['uuid' => $blockUuids[$name]]
      );
      $this->writeYamlToTempDir($block, $blockDir, $name . '.yml');
    }
    return $blockUuids;
  }

  /**
   * Gets path of content templates.
   *
   * @return string[]
   *   Type of content with path.
   */
  private function getPresetPaths(): array {
    $demoAccounts = $this->getDemoAccounts();
    // Define active credentials set.
    foreach ($demoAccounts as $demoAccountKey => $demoAccountData) {
      $connectionConfig = $demoAccountData['config'][CommercetoolsConfiguration::CONFIGURATION_API];
      if ($this->ctConfig->isConnectionEqual($connectionConfig)) {
        $connector = self::CREDENTIALS_DIRECTORY_PATH . DIRECTORY_SEPARATOR . $demoAccountKey;
      }
    }
    // No connection.
    if (empty($connector)) {
      return [];
    }
    $base = dirname(__DIR__) . DIRECTORY_SEPARATOR . $connector;
    return [
      'nodes' => $base . DIRECTORY_SEPARATOR . self::NODE_SUBDIR,
      'blocks' => $base . DIRECTORY_SEPARATOR . self::BLOCK_SUBDIR,
    ];
  }

  /**
   * Prepares the temporary directory.
   *
   * @param string $subdir
   *   Subdir to use.
   *
   * @return string
   *   Temp directory.
   */
  private function prepareTempDirectory(string $subdir): string {
    // Clean tmp dir.
    $this->fileSystem->deleteRecursive($this->fileSystem->getTempDirectory() . DIRECTORY_SEPARATOR . $subdir);
    $tempDir = $this->fileSystem->getTempDirectory() . DIRECTORY_SEPARATOR . $subdir;
    $this->fileSystem->prepareDirectory(
      $tempDir,
      FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS
    );
    return $tempDir;
  }

  /**
   * Rebuilds Layout builder component keys with generated UUID.
   *
   * @param array $components
   *   Components.
   *
   * @return array
   *   Rebuild components.
   */
  private function rebuildComponentKeys(array $components): array {
    $rebuilt = [];
    foreach ($components as $component) {
      $uuid = $this->uuid->generate();
      $component['uuid'] = $uuid;
      $rebuilt[$uuid] = $component;
    }
    return $rebuilt;
  }

  /**
   * Prepare content blocks.
   *
   * @param array $components
   *   Layout builder components.
   * @param array $blockUuids
   *   Processed blocks.
   * @param string $module
   *   The UI module name.
   *
   * @return array
   *   Processed components.
   */
  private function processBlockReferences(array $components, array $blockUuids, string $module): array {
    $blockIds = $this->getInlineBlockId($blockUuids);
    foreach ($components as $uuid => $component) {
      $blockId = $component['configuration']['block_id'] ?? NULL;
      if (!$blockId) {
        continue;
      }
      $normalizedBlockId = trim($blockId, '[]');
      // Follow the naming pattern.
      $blockKey = 'block.demo_products_info_' . $normalizedBlockId;
      if (!isset($blockIds[$blockKey])) {
        continue;
      }

      // Prepare token mappings for configuration updates.
      $tokenReplacements = [
        $normalizedBlockId => $blockIds[$blockKey],
      ];
      $components[$uuid]['configuration'] = $this->prepareComponentConfig($component['configuration'], $module, $tokenReplacements);
    }
    return $components;
  }

  /**
   * Load content blocks by uuids.
   *
   * @param array $blockUuids
   *   Blocks uuids.
   *
   * @return array
   *   Content block objects.
   */
  private function getInlineBlockId(array $blockUuids): array {
    $blocks = [];
    $storage = $this->entityTypeManager
      ->getStorage('block_content');
    $result = $storage->loadByProperties(['uuid' => $blockUuids]);
    // Ordered uuids by int key.
    $blockUuidsOrdered = array_keys(array_flip($blockUuids));
    // Sort accordingly with database order.
    usort($result, function ($a, $b) use ($blockUuidsOrdered) {
      $posA = array_search($a->uuid(), $blockUuidsOrdered);
      $posB = array_search($b->uuid(), $blockUuidsOrdered);
      return $posA - $posB;
    });
    // Relate block template name to it's id.
    $blockUuidsFlipped = array_flip($blockUuids);
    foreach ($result as $block) {
      $blocks[$blockUuidsFlipped[$block->uuid()]] = $block->id();
    }
    return $blocks;
  }

  /**
   * Write yaml to temp dir.
   *
   * @param array $data
   *   Data to write.
   * @param string $dir
   *   Directory to write.
   * @param string $filename
   *   Filename.
   *
   * @return string
   *   Path of created yaml.
   */
  private function writeYamlToTempDir(array $data, string $dir, string $filename): string {
    $tmpFile = $this->fileSystem->saveData(Yaml::encode($data), $dir, FileExists::Replace);
    $finalPath = $dir . DIRECTORY_SEPARATOR . $filename;
    $this->fileSystem->move($tmpFile, $finalPath, FileExists::Replace);
    return $finalPath;
  }

  /**
   * Removes demo UI components for a module.
   *
   * @param string $module
   *   The UI module name.
   */
  public function removeDemoComponents(string $module): void {
    $deployed = $this->state->get(self::STATE_KEY_UI_DEPLOYMENT_PREFIX . $module, []);
    foreach ($deployed as $id => $uuid) {
      preg_match('/(?<type>.+)\.(?<name>.+)/', $id, $matches);
      $storage = $this->entityTypeManager->getStorage($matches['type']);
      /** @var \Drupal\Core\Entity\EntityInterface $entity */
      $entity = current($storage->loadByProperties(['uuid' => $uuid]));
      if ($entity) {
        if (
          $entity->getEntityTypeId() == 'configurable_language'
          && $entity->id() == $this->ctLocalization->defaultLanguage->getId()
        ) {
          continue;
        }
        $entity->delete();
      }
    }
    $this->state->delete(self::STATE_KEY_UI_DEPLOYMENT_PREFIX . $module);
  }

  /**
   * Provides default variables for replacement.
   *
   * @return array
   *   The theme variables.
   */
  private function getThemeVariables(): array {
    $variables = [];
    $themeCurrent = $this->configFactory->get('system.theme')->get('default');
    $variables['theme'] = $themeCurrent;
    $themes = $this->getSuggestedThemes();
    $theme = $themes[$themeCurrent];
    foreach ($theme['regions'] as $key => $value) {
      $variables['region:' . $key] = $value;
    }
    return $variables;
  }

  /**
   * Replaces variables in values.
   *
   * @todo Consider token as alternative.
   *
   * @param array $values
   *   The values to check variables in.
   * @param array $variables
   *   The variables to replace in values as :[KEY].
   *
   * @return array
   *   The values with replacements.
   */
  private function replaceValuesVariables(array $values, array $variables): array {
    $string = Json::encode($values);
    foreach ($variables as $key => $variable) {
      if ($variable === NULL) {
        continue;
      }
      $search = "[$key]";
      $string = str_replace($search, $variable, $string);
    }
    return Json::decode($string);
  }

}

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

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