commercetools-8.x-1.2-alpha1/src/Form/SubscriptionSettingsForm.php
src/Form/SubscriptionSettingsForm.php
<?php
namespace Drupal\commercetools\Form;
use Drupal\commercetools\SubscriptionDestinationTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Settings form for configuring commercetools subscriptions.
*/
class SubscriptionSettingsForm extends CommercetoolsSettingsFormBase implements TrustedCallbackInterface {
const CONFIGURATION_NAME = 'commercetools.subscriptions_settings';
/**
* The Commercetools Subscriptions API service.
*
* @var \Drupal\commercetools\CommercetoolsSubscriptionsApi
*/
protected $ctSubscriptionsApi;
/**
* The Commercetools Subscriptions Destination Plugin Manager.
*
* @var \Drupal\commercetools\SubscriptionsDestinationPluginManager
*/
protected $destinationManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$instance = parent::create($container);
$instance->ctSubscriptionsApi = $container->get('commercetools.subscriptions');
$instance->destinationManager = $container->get('plugin.manager.commercetools_subscriptions_destination');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getEditableConfigNames() {
$config_names = [static::CONFIGURATION_NAME];
foreach ($this->destinationManager->getDefinitions() as $def) {
$config_names[] = $def['config_name'];
}
return $config_names;
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config(static::CONFIGURATION_NAME);
$destination = $form_state->getValue('destination') ?? $config->get('destination');
$subscription_key = $config->get('subscription_key');
$form['description'] = [
'#markup' => $this->t('Configuring commercetools subscriptions allow you to invalidate the cached data instantly when it is changed on the commercetools platform, without waiting for the cron run. Without configured subscriptions the module invalidates caches during the Drupal cron run, and if you configure subscriptions - they replaces the cron job. <a target="_blank" href="https://docs.commercetools.com/api/projects/subscriptions">More details about commercetools subscriptions</a>.'),
];
if ($subscription_key) {
$form['destination_description'] = [
'#type' => 'item',
'#title' => $this->t('To install a new destination, you need to delete the current one.'),
];
}
elseif ($config->get('destination')) {
$form['destination_description'] = [
'#type' => 'item',
'#title' => $this->t('The subscription was not connected successfully.'),
];
}
$options = [];
foreach ($this->destinationManager->getDefinitions() as $def) {
$options[$def['id']] = $def['label'];
}
$form['service'] = [
'#type' => 'fieldset',
'#title' => 'Cloud messaging service',
'#description' => $this->t('Subscriptions require configuring a cloud messaging service, more technical details <a target="_blank" href="https://docs.commercetools.com/api/projects/subscriptions#destination-1">here</a>.'),
'#description_display' => 'before',
];
$form['service']['destination'] = $this->getFormElement('destination', [
'#type' => 'select',
'#description' => $this->t('Choose the cloud messaging service type, configured on the commercetools platform.'),
'#options' => $options,
'#empty_option' => $this->t('- Select -'),
'#ajax' => [
'callback' => '::ajaxToggleServiceSettings',
'wrapper' => 'service-settings-wrapper',
'event' => 'change',
],
'#disabled' => (bool) $config->get('destination'),
], 'commercetools.subscriptions_settings');
$form['service']['subscription_key'] = $this->getFormElement('subscription_key', [
'#required' => TRUE,
'#description' => $this->t('Unique identifier of the Subscription. 2–256 characters. Use only A–Z, a–z, 0–9, _ or -. No spaces or other symbols.'),
'#states' => [
'visible' => [
':input[name="destination"]' => [
'!value' => '',
],
],
],
'#disabled' => (bool) $subscription_key,
], 'commercetools.subscriptions_settings');
$form['service']['service_settings'] = [
'#type' => 'container',
'#attributes' => ['id' => 'service-settings-wrapper'],
];
if ($destination) {
$destinationPlugin = $this->getDestinationPlugin($destination);
foreach ($destinationPlugin->getSettingsFormFields() as $fieldName => $field) {
$form['service']['service_settings'][$fieldName] = $this->getFormElement($fieldName, $field, $destinationPlugin->getConfigName());
}
}
$form['service']['changes'] = $this->getFormElement('changes', [
'#type' => 'textarea',
'#description' => $this->t('List here a comma-separated list of resource type to subscribe. Usually it is <code>product,category,cart</code>. Full list of resource types <a target="_blank" href="https://docs.commercetools.com/api/projects/subscriptions#changesubscriptionresourcetypeid">here</a>.'),
'#required' => TRUE,
'#states' => [
'visible' => [
':input[name="destination"]' => [
'!value' => '',
],
],
],
], 'commercetools.subscriptions_settings');
$form['service']['lambda_description'] = [
'#markup' => $this->t('On the cloud messaging service side you should configure a lambda function that will trigger webhook on receiving the events.'),
];
$form['service']['lambda_example'] = [
'#type' => 'fieldset',
'#title' => $this->t('Lambda function template'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
];
$form['service']['lambda_example']['function'] = [
'#type' => 'textarea',
'#title' => $this->t('Add this lambda function to the cloud messaging service configuration:'),
'#value' => <<<EOT
export const handler = async (event) => {
const resp = await fetch(process.env.DRUPAL_WEBHOOK_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
'Authorization': 'Bearer ' + process.env.DRUPAL_WEBHOOK_ACCESS_TOKEN,
},
body: JSON.stringify({ message: event.Records }),
});
if (JSON.parse(result.value.body).status !== "success") {
throw new Error("Webhook error");
}
}
EOT,
'#disabled' => TRUE,
];
$form['service']['lambda_example']['note'] = [
'#markup' => $this->t('In addition to this lambda function, you have to configure two environment variables with values from the "Webhook configuration" section below:
<ul>
<li><code>DRUPAL_WEBHOOK_URL</code></li>
<li><code>DRUPAL_WEBHOOK_ACCESS_TOKEN</code>.</li>
</ul>'),
];
$form['webhook'] = [
'#type' => 'fieldset',
'#title' => $this->t('Webhook configuration'),
'#description' => $this->t('Configuration of the webhook endpoint on the Drupal side, which will receive notifications.'),
'#description_display' => 'before',
];
$form['webhook']['url'] = [
'#type' => 'textfield',
'#title' => $this->t('Webhook URL'),
'#value' => Url::fromRoute('commercetools.subscriptions_webhook', [], ['absolute' => TRUE])->toString(),
'#description' => $this->t('Use this url as a webhook endpoint in the cloud service lambda function configuration. The domain must be publicly reachable from the internet—no localhost, private IP ranges, or dev-only domains—and should be served over HTTPS so the cloud can deliver events to it.'),
'#disabled' => TRUE,
'#size' => 120,
];
$form['webhook']['webhook_token'] = $this->getFormElement('webhook_token', [
'#required' => TRUE,
'#maxlength' => 255,
'#attributes' => [
'autocomplete' => 'off',
'spellcheck' => 'false',
],
'#description' => $this->t('Set any string as an access token. It is used to protect the webhook endpoint from unauthorized access.'),
], 'commercetools.subscriptions_settings');
$form['webhook']['logging'] = $this->getFormElement('logging', [
'#type' => 'checkbox',
'#description' => $this->t('Enables logging of each webhook call, useful for debugging. Do not use this on production to improve the performance.'),
], 'commercetools.subscriptions_settings');
$form = parent::buildForm($form, $form_state);
$form['actions'] = [
'#type' => 'actions',
'submit' => [
'#type' => 'submit',
'#value' => $this->t('Save configuration'),
'#button_type' => 'primary',
'#access' => !$subscription_key,
],
'update' => [
'#type' => 'submit',
'#value' => $this->t('Save updated configuration'),
'#button_type' => 'primary',
'#submit' => ['::update'],
'#access' => (bool) $subscription_key,
],
'clear' => [
'#type' => 'submit',
'#value' => $this->t('Clear, delete destination'),
'#submit' => ['::clear'],
'#access' => (bool) $destination,
],
];
return $form;
}
/**
* AJAX callback: re-renders the destination-specific settings section.
*/
public function ajaxToggleServiceSettings(array &$form, FormStateInterface $form_state) {
return $form['service']['service_settings'];
}
/**
* Removes the subscription and clears stored config.
*/
public function clear(array &$form, FormStateInterface $form_state): void {
$values = $form_state->cleanValues()->getValues();
if ($values['subscription_key']) {
$subscription = $this->ctSubscriptionsApi->getSubscriptionByKey($values['subscription_key']);
if (!empty($subscription['key']) && !empty($subscription['version'])) {
$this->ctSubscriptionsApi->deleteSubscription($subscription['key'], $subscription['version']);
}
}
foreach ($this->getEditableConfigNames() as $configName) {
$config = $this->configFactory()->getEditable($configName);
foreach ($values as $key => $value) {
$config->clear($key);
}
$config->save();
}
$this->messenger()->addStatus($this->t('Subscription removed.'));
}
/**
* Updates the subscription.
*/
public function update(array &$form, FormStateInterface $form_state): void {
$values = $form_state->cleanValues()->getValues();
$config = $this->config(static::CONFIGURATION_NAME);
if ($values['subscription_key']) {
$subscription = $this->ctSubscriptionsApi->getSubscriptionByKey($values['subscription_key']);
if (!empty($subscription['key']) && !empty($subscription['version'])) {
$destinationPlugin = $this->getDestinationPlugin($values['destination']);
$subscription = $this->ctSubscriptionsApi->updateSubscription(
$subscription['key'],
$subscription['version'],
[
'changeDestination' => [
'destination' => [
$destinationPlugin->getDestinationQueryName() => $destinationPlugin->getDestinationQueryPart($values),
],
],
]
);
if ($values['changes'] !== $config->get('changes')) {
$changes = [];
foreach (explode(',', trim($values['changes'])) as $change) {
$changes[] = ['resourceTypeId' => $change];
}
$subscription = $this->ctSubscriptionsApi->updateSubscription(
$subscription['key'],
$subscription['version'],
[
'setChanges' => [
'changes' => $changes,
],
]
);
}
if (!empty($subscription['key'])) {
$this->messenger()->addStatus($this->t('Subscription successfully updated.'));
}
}
}
parent::submitForm($form, $form_state);
}
/**
* {@inheritdoc}
*
* Saves config and creates a commercetools subscription.
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$values = $form_state->cleanValues()->getValues();
$destinationPlugin = $this->getDestinationPlugin($values['destination']);
$subscription = $this->ctSubscriptionsApi->createSubscription(
key: $values['subscription_key'],
destination: $destinationPlugin->getDestinationQueryName(),
paramsDestination: $destinationPlugin->getDestinationQueryPart($values),
changes: explode(',', trim($values['changes'])),
);
if (!empty($subscription['key'])) {
$this->messenger()->addStatus($this->t('Subscription successfully added.'));
}
parent::submitForm($form, $form_state);
}
/**
* Instantiates a destination plugin by its ID.
*
* @param string $destination
* The plugin ID (e.g., 'sqs').
*
* @return \Drupal\commercetools\SubscriptionDestinationTypeInterface
* The destination plugin instance.
*/
protected function getDestinationPlugin(string $destination): SubscriptionDestinationTypeInterface {
return $this->destinationManager->createInstance($destination);
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks(): array {
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(array $element): array {
Element::setAttributes($element, ['value']);
return $element;
}
}
