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);
}
}
