commercetools-8.x-1.2-alpha1/src/Form/GeneralSettingsForm.php
src/Form/GeneralSettingsForm.php
<?php
namespace Drupal\commercetools\Form;
use Drupal\commercetools\Cache\CacheableCommercetoolsGraphQlResponse;
use Drupal\commercetools\CommercetoolsApiServiceInterface;
use Drupal\commercetools\CommercetoolsConfiguration;
use Drupal\commercetools\CommercetoolsService;
use Drupal\commercetools\Event\CommercetoolsConfigurationEvent;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\Password;
use Drupal\Core\Render\Markup;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configure a Commercetools settings form for this site.
*/
class GeneralSettingsForm extends CommercetoolsSettingsFormBase implements TrustedCallbackInterface {
const STEP_CONFIG = 'config';
const STEP_CONFIRM = 'confirm';
const CHECKOUT_MODE_LOCAL = 'local';
const CHECKOUT_MODE_COMMERCETOOLS = 'commercetools';
const CONFIGURATION_NAME = CommercetoolsService::CONFIGURATION_NAME;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The Commercetools service.
*
* @var \Drupal\commercetools\CommercetoolsService
*/
protected CommercetoolsService $ct;
/**
* The event dispatcher service.
*
* @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
*/
protected $dispatcher;
/**
* The commercetools configuration service.
*
* @var \Drupal\commercetools\CommercetoolsConfiguration
*/
protected CommercetoolsConfiguration $ctConfig;
/**
* Cache tags invalidator service.
*
* @var \Drupal\Core\Cache\CacheTagsInvalidator
*/
protected CacheTagsInvalidatorInterface $cacheInvalidator;
/**
* The current form step.
*
* @var string
*/
protected $formStep = self::STEP_CONFIG;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$instance = parent::create($container);
$instance->moduleHandler = $container->get('module_handler');
$instance->ct = $container->get('commercetools');
$instance->dispatcher = $container->get('event_dispatcher');
$instance->ctConfig = $container->get('commercetools.config');
$instance->cacheInvalidator = $container->get('cache_tags.invalidator');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getEditableConfigNames() {
return [
CommercetoolsService::CONFIGURATION_NAME,
CommercetoolsApiServiceInterface::CONFIGURATION_NAME,
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
switch ($this->formStep) {
case self::STEP_CONFIRM:
$form = $this->getConfirmFormElements();
$form = parent::buildForm($form, $form_state);
unset($form['actions']['submit']);
break;
default:
$form = $this->getConfigFormElements();
$form = parent::buildForm($form, $form_state);
}
$form['#attributes']['class'][] = 'commercetools-general-settings-form';
return $form;
}
/**
* Provides configuration form elements.
*
* @return array
* Configuration form elements.
*/
private function getConfigFormElements(): array {
$elements = [];
$elements['connection_settings'] = [
'#type' => 'fieldset',
'#title' => $this->t('Connection Settings'),
'#description' => $this->t('Essential settings required to connect to the commercetools account.'),
];
if (!$this->ctConfig->isConnectionConfigured()) {
$elements['connection_settings']['description'] = [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => $this->t('The module requires a commercetools account to display products, enter here access credentials to your commercetools account. You can register a free trial account <a href="https://commercetools.com/free-trial">here</a>.'),
];
if ($this->moduleHandler->moduleExists('commercetools_demo')) {
$elements['connection_settings']['demo_description'] = [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => $this->t('To test the functionality you can use preconfigured demo accounts on the <a href="@url">demo configuration form page</a>.', [
'@url' => Url::fromRoute('commercetools_demo.settings')->toString(),
]),
];
}
else {
$elements['connection_settings']['demo_description'] = [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => $this->t('Also, to test the functionality you can use preconfigured demo accounts, provided by the <a href="@url">installing the "commercetools Demo" module</a>.', [
'@url' => Url::fromRoute('system.modules_list', options: ['fragment' => 'module-commercetools-demo'])->toString(),
]),
];
}
}
else {
// Display links to the catalog pages if UI modules are installed.
$uiModules = [
'commercetools_content' => 'commercetools Content',
'commercetools_decoupled' => 'commercetools Decoupled',
];
foreach ($uiModules as $module => $title) {
if ($this->moduleHandler->moduleExists($module)) {
$routeProviderClass = "Drupal\\$module\\Routing\\RouteProvider";
$moduleCatalogLinks[] = Link::createFromRoute($title, $routeProviderClass::ROUTE_PREFIX . $routeProviderClass::PAGE_CATALOG_ROUTE)
->toString();
}
}
if (isset($moduleCatalogLinks)) {
$elements['connection_settings']['catalog_links'] = [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => $this->t('Product catalog main page: @links.', [
'@links' => Markup::create(implode(', ', $moduleCatalogLinks)),
]),
];
}
else {
$messageParts = [];
// Display a warning if no UI modules are installed.
$messageParts[] = $this->t('No UI modules installed. Install the <a href="@commercetools_content_link">commercetools Content</a> or <a href="@commercetools_decoupled_link">commercetools Decoupled</a> module to display commercetools Products.', [
'@commercetools_content_link' => Url::fromRoute('system.modules_list', options: ['fragment' => 'module-commercetools-content'])
->toString(),
'@commercetools_decoupled_link' => Url::fromRoute('system.modules_list', options: ['fragment' => 'module-commercetools-decoupled'])
->toString(),
]);
if ($this->moduleHandler->moduleExists('commercetools_demo')) {
$messageParts[] = $this->t('Also, you can deploy a pre-configured demo setup on the <a href="@url">commercetools Demo configuration page</a>.', [
'@url' => Url::fromRoute('commercetools_demo.settings')->toString(),
]);
}
else {
$messageParts[] = $this->t('Also, you can <a href="@url">install a commercetools Demo module</a> to deploy a pre-configured setup.', [
'@url' => Url::fromRoute('system.modules_list', options: [
'fragment' => 'module-commercetools-demo',
])->toString(),
]);
}
$this->messenger()
->addWarning(Markup::create(implode(' ', $messageParts)));
}
if ($this->moduleHandler->moduleExists('commercetools_demo')) {
$elements['connection_settings']['demo_description'] = [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => $this->t('Also, you can deploy demo credentials on the <a href="@url">commercetools Demo configuration page</a>.', [
'@url' => Url::fromRoute('commercetools_demo.settings')->toString(),
]),
];
}
}
$elements['connection_settings'][CommercetoolsApiServiceInterface::CONFIG_CLIENT_ID] = $this->getFormElement(CommercetoolsApiServiceInterface::CONFIG_CLIENT_ID, [
'#description' => $this->t(
'The Client ID associated with your commercetools API Client. This ID is used to identify your application when making requests to the commercetools API. You can retrieve this ID from your commercetools Dashboard. Example: <code>@example</code>. Learn more: <a href="@docs">Create API Client</a>.',
[
'@example' => 'aY3r5ltWCokRUJ4lgxQu_gvr',
'@docs' => 'https://docs.commercetools.com/getting-started/create-api-client',
]
),
'#required' => TRUE,
], CommercetoolsApiServiceInterface::CONFIGURATION_NAME);
$elements['connection_settings'][CommercetoolsApiServiceInterface::CONFIG_CLIENT_SECRET] = $this->getFormElement(CommercetoolsApiServiceInterface::CONFIG_CLIENT_SECRET, [
'#type' => 'password',
'#pre_render' => [
[Password::class, 'preRenderPassword'],
// A trick to prefill the stored secret value.
[self::class, 'fillStoredPasswordCallback'],
],
'#description' => $this->t(
'The Client Secret associated with your commercetools API Client. This is a confidential key used in combination with the Client ID to authenticate API requests. You can retrieve this key from your commercetools Dashboard. Ensure it is kept secure and not shared publicly. Example: <code>@example</code>.',
[
'@example' => 'dwYpX1yAgI1OZsJDM5e7rgZdARpYry92B',
]
),
'#required' => TRUE,
], CommercetoolsApiServiceInterface::CONFIGURATION_NAME);
$elements['connection_settings'][CommercetoolsApiServiceInterface::CONFIG_PROJECT_KEY] = $this->getFormElement(CommercetoolsApiServiceInterface::CONFIG_PROJECT_KEY, [
'#description' => $this->t('The Project Key associated with your commercetools account. This key identifies your specific project within commercetools and is required for making API calls. You can find the Project Key in your commercetools Dashboard. Example: <code>@example</code>.', [
'@example' => 'my-shop',
]),
'#required' => TRUE,
], CommercetoolsApiServiceInterface::CONFIGURATION_NAME);
$elements['connection_settings'][CommercetoolsApiServiceInterface::CONFIG_HOSTED_REGION] = $this->getFormElement(CommercetoolsApiServiceInterface::CONFIG_HOSTED_REGION, [
'#type' => 'select',
'#options' => $this->getHostedRegionOptions(),
'#description' => $this->t(
'Select the HTTP API region of your commercetools account. For more details, refer to the <a href="@docs" target="_blank">commercetools API Documentation</a>.',
['@docs' => 'https://docs.commercetools.com/api/general-concepts#hosts']
),
'#required' => TRUE,
], CommercetoolsApiServiceInterface::CONFIGURATION_NAME);
$elements['connection_settings']['actions']['test_credentials'] = [
'#type' => 'submit',
'#value' => $this->t('Test Credentials'),
'#submit' => ['::testCredentials'],
'#button_type' => 'secondary',
];
$elements['checkout_config'] = [
'#type' => 'fieldset',
'#title' => $this->t('Checkout configuration'),
'#tree' => FALSE,
CommercetoolsService::CONFIG_CHECKOUT_MODE => $this->getFormElement(CommercetoolsService::CONFIG_CHECKOUT_MODE, [
'#type' => 'radios',
'#options' => [
self::CHECKOUT_MODE_LOCAL => $this->t('Local checkout on the Drupal side'),
self::CHECKOUT_MODE_COMMERCETOOLS => $this->t('commercetools Checkout'),
],
]),
CommercetoolsService::CONFIG_CHECKOUT_CT_APP_KEY => $this->getFormElement(CommercetoolsService::CONFIG_CHECKOUT_CT_APP_KEY, [
'#description' => $this->t('Application key of the Complete Checkout Application type. The key is required to enable <a href="@docs">commercetools checkout solution</a>. You can find a list of applications in the Merchant Center under the <strong>Checkout > Overview</strong> tab.', [
'@docs' => 'https://docs.commercetools.com/checkout/overview',
]),
'#states' => [
'visible' => [
':input[name="' . CommercetoolsService::CONFIG_CHECKOUT_MODE . '"]' => ['value' => 'commercetools'],
],
],
]),
CommercetoolsService::CONFIG_CHECKOUT_CT_INLINE => $this->getFormElement(CommercetoolsService::CONFIG_CHECKOUT_CT_INLINE, [
'#type' => 'checkbox',
'#description' => $this->t('The commercetools checkout interface will be displayed as an inline frame inside your website design, instead of the full-page view.'),
'#states' => [
'visible' => [
':input[name="' . CommercetoolsService::CONFIG_CHECKOUT_MODE . '"]' => ['value' => 'commercetools'],
],
],
]),
];
// Advanced settings fieldset.
$elements['advanced_settings'] = [
'#type' => 'details',
'#title' => $this->t('Advanced Settings'),
'#description' => $this->t('Additional configuration options. Modify only if necessary.'),
'#open' => FALSE,
];
$elements['advanced_settings'][CommercetoolsService::CONFIG_DISPLAY_CONNECTION_ERRORS] = $this->getFormElement(CommercetoolsService::CONFIG_DISPLAY_CONNECTION_ERRORS, [
'#type' => 'checkbox',
'#description' => $this->t('Displays connection problems in the UI using Drupal Messenger. Disable to suppress visual error messages and use exceptions instead.'),
]);
$elements['advanced_settings'][CommercetoolsService::CONFIG_LOG_CT_REQUESTS] = $this->getFormElement(CommercetoolsService::CONFIG_LOG_CT_REQUESTS, [
'#type' => 'checkbox',
'#description' => $this->t('Enable logging of each request to commercetools.'),
]);
$elements['advanced_settings'][CommercetoolsApiServiceInterface::CONFIG_CACHE_RESPONSES_TTL] = $this->getFormElement(CommercetoolsApiServiceInterface::CONFIG_CACHE_RESPONSES_TTL, [
'#type' => 'number',
'#description' => $this->t('The module caches most commercetools data locally to ensure fast page rendering and invalidates caches via cron using commercetools messages. However, due to <a href="https://github.com/commercetools/commercetools-sdk-php-v2/issues/302">issue #302</a>, not all changes trigger messages, potentially causing stale caches. To address this, set a time-to-live (TTL) value in seconds for cached responses. Special values: "-1" means no auto-expiration, and "0" disables caching entirely.'),
'#min' => -1,
], CommercetoolsApiServiceInterface::CONFIGURATION_NAME);
$elements['advanced_settings']['cache_clear'] = [
'#type' => 'submit',
'#value' => $this->t('Clear cache'),
'#submit' => ['::clearCachesSubmit'],
];
$elements['advanced_settings'][CommercetoolsApiServiceInterface::CONFIG_SCOPE] = $this->getFormElement(CommercetoolsApiServiceInterface::CONFIG_SCOPE, [
'#description' => $this->t('The scopes required for accessing the commercetools API. Scopes specify the permissions granted to the API client for operations such as read, write, and manage. Ensure the scope aligns with your project\'s configuration in commercetools. Enter multiple values separated by spaces. For more details, refer to the <a href="@docs" target="_blank">commercetools API Documentation</a>. Example: <code>@example</code>.', [
'@example' => 'manage_project',
'@docs' => 'https://docs.commercetools.com/api/scopes',
]),
], CommercetoolsApiServiceInterface::CONFIGURATION_NAME);
return $elements;
}
/**
* Cache clear submit.
*/
public function clearCachesSubmit(): void {
$this->cacheInvalidator->invalidateTags([CacheableCommercetoolsGraphQlResponse::CACHE_TAG_GENERAL]);
$this->logger('commercetools')->debug('commercetools caches has been cleared.');
$this->messenger()->addMessage($this->t('commercetools caches has been cleared.'));
}
/**
* Provides confirmation form elements.
*
* @return array
* Configuration form elements.
*/
private function getConfirmFormElements(): array {
$elements = [];
$elements['confirm'] = [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => $this->t('Changing the Client ID or Project ID requires migration of all project-related custom settings (product attributes, currencies, stores, etc) and cleaning up all cached user and anonymous carts, orders, and other custom configurations. Please confirm this action.'),
];
$elements['elements'] = [
'#type' => 'container',
'#attributes' => [
'class' => [
'hidden',
'visually-hidden',
'invisible',
],
],
] + $this->getConfigFormElements();
$elements['actions'] = [
'#type' => 'actions',
'confirm' => [
'#type' => 'submit',
'#value' => $this->t('Save updated configuration'),
'#button_type' => 'primary button--danger',
],
'cancel' => [
'#type' => 'link',
'#title' => $this->t('Leave previous configuration'),
'#url' => Url::fromRoute('commercetools.settings'),
'#attributes' => [
'class' => [
'button',
'button--secondary',
],
],
],
];
return $elements;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
if ($this->formStep == self::STEP_CONFIG) {
$values = $form_state->getValues();
if ($this->ctConfig->isProjectChanging($values)) {
$form_state->setRebuild();
$this->formStep = self::STEP_CONFIRM;
return;
}
}
if ($this->formStep == self::STEP_CONFIRM) {
$values = $form_state->getValues();
$config = $this->ctConfig->getConnectionConfig();
$updated = array_intersect_key($values, $config);
$event = new CommercetoolsConfigurationEvent($updated);
$this->dispatcher->dispatch($event);
}
parent::submitForm($form, $form_state);
}
/**
* Get the hosted region options.
*
* @return array
* An array of hosted region options.
*/
private function getHostedRegionOptions() {
return [
'us-central1.gcp' => 'https://api.us-central1.gcp.commercetools.com - ' . $this->t('North America (Google Cloud, Iowa)'),
'us-east-2.aws' => 'https://api.us-east-2.aws.commercetools.com - ' . $this->t('North America (AWS, Ohio)'),
'europe-west1.gcp' => 'https://api.europe-west1.gcp.commercetools.com - ' . $this->t('Europe (Google Cloud, Belgium)'),
'eu-central-1.aws' => 'https://api.eu-central-1.aws.commercetools.com - ' . $this->t('Europe (AWS, Frankfurt)'),
'australia-southeast1.gcp' => 'https://api.australia-southeast1.gcp.commercetools.com - ' . $this->t('Australia (Google Cloud, Sydney)'),
];
}
/**
* Test commercetools credentials.
*
* @param array $form
* Form elements.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public function testCredentials(array &$form, FormStateInterface $form_state) {
$form_state->setRebuild();
$override = $this->getSubmittedCredentials($form_state);
try {
$this->ctApi->setOverriddenConfig($override);
$projectInfo = $this->ctApi->getProjectInfo();
// Display success message with the project name.
$this->messenger()->addMessage($this->t('Credentials are valid. Project name: @project_name', [
'@project_name' => $projectInfo['name'],
]));
}
catch (\Exception $e) {
// Handle errors and display a message to the user.
$this->messenger()->addError($this->t('Test failed: @message', ['@message' => $e->getMessage()]));
}
finally {
$this->ctApi->restoreOriginalConfig();
}
}
/**
* Collects submitted API credentials.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public function getSubmittedCredentials(FormStateInterface $form_state) {
$credentials = [];
$keys = $this->ctConfig->listCredentialKeys();
// "API Scope" parameter is also required.
$keys[] = CommercetoolsApiServiceInterface::CONFIG_SCOPE;
foreach ($keys as $key) {
$credentials[$key] = $form_state->getValue($key);
}
return $credentials;
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['fillStoredPasswordCallback'];
}
/**
* Pre-render callback for the password element to fill the stored password.
*
* @param array $element
* The element to pre-render.
*
* @return array
* The pre-rendered element.
*/
public static function fillStoredPasswordCallback($element) {
Element::setAttributes($element, ['value']);
return $element;
}
}
