language_negotiation_matrix-1.0.0-beta2/src/Form/NegotiationUrlMatrixForm.php
src/Form/NegotiationUrlMatrixForm.php
<?php
namespace Drupal\language_negotiation_matrix\Form;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ChangedCommand;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Config\Config;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\language_negotiation_matrix\MatrixManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\language_negotiation_matrix\Plugin\LanguageNegotiation\LanguageNegotiationUrlMatrix;
/**
* Configure the URL language negotiation method for this site.
*
* @internal
*/
class NegotiationUrlMatrixForm extends ConfigFormBase {
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected LanguageManagerInterface $languageManager;
/**
* The Matrix manager.
*
* @var \Drupal\language_negotiation_matrix\MatrixManagerInterface
*/
protected MatrixManagerInterface $matrixManager;
/**
* Constructs a new NegotiationUrlForm object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\language_negotiation_matrix\MatrixManagerInterface $matrix_manager
* The matrix manager.
*/
public function __construct(ConfigFactoryInterface $config_factory, LanguageManagerInterface $language_manager, MatrixManagerInterface $matrix_manager) {
parent::__construct($config_factory);
$this->languageManager = $language_manager;
$this->matrixManager = $matrix_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('language_manager'),
$container->get('language_negotiation_matrix.matrix_manager'),
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'language_negotiation_configure_matrix_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['language.negotiation'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('language.negotiation');
$module_handler = \Drupal::moduleHandler();
// Get the form values and raw input (unvalidated values).
$values = $form_state->getValues();
// Define a wrapper id to populate new content into.
$ajax_wrapper = 'matrix-ajax-wrapper';
$form['language'] = [
'#type' => 'details',
'#tree' => TRUE,
'#title' => $this->t('Language Configuration'),
'#open' => TRUE,
'#attributes' => [
'id' => $ajax_wrapper,
]
];
if($module_handler->moduleExists('key')){
$options['key'] = $this->t('Key');
}
$options['json'] = $this->t('JSON');
$form['language']['language_negotiation_matrix'] = [
'#title' => $this->t('Matrix Definition'),
'#type' => 'select',
'#empty_value' => '',
'#empty_option' => '- Select -',
'#description' => $this->t('This language negotiation configuration can accept two ways of implementing the matrix: via a KEY using the <strong>key</strong> module [%enabled] or through a JSON object.',
['%enabled' => $module_handler->moduleExists('key') ? 'Enabled' : 'Not Installed']
),
'#options' => $options,
'#default_value' => $config->get('matrix.language.language_negotiation_matrix'),
];
$multi_language = $this->getMultiLanguageState($form_state, $config);
if ($module_handler->moduleExists('key')){
$form['language']['multi_language'] = [
'#type' => 'checkbox',
'#title' => $this->t('Use multiple language keys'),
'#description' => $this->t('If the site uses several languages, this option will permit associating different ENVIRONMENT variables with different KEYS. Do not forget to set them all up.'),
'#default_value' => $config->get('matrix.language.multi_language'),
'#ajax' => [
'callback' => [$this, 'ajaxSelectChange'],
'event' => 'change',
'wrapper' => $ajax_wrapper,
],
'#states' => [
'visible' => [
':input[name="language[language_negotiation_matrix]"]' => [
'value' => (string) 'key',
],
],
],
];
$form['language']['language_matrix'] = $this->buildKeySelectElement($config, $multi_language);
}
$form['language']['language_matrix_json'] = [
'#type' => 'codemirror',
'#title' => $this->t('Language Matrix JSON'),
'#description' => $this->t('Provide the matrix in JSON. To get the mappings to work, please SUBMIT the form and continue applying your settings.'),
'#mode' => 'json',
'#default_value' => $config->get('matrix.language.language_matrix_json'),
'#states' => [
'visible' => [
':input[name="language[language_negotiation_matrix]"]' => [
'value' => (string) 'json',
],
],
],
];
$form['language_negotiation_url_matrix_type'] = [
'#title' => $this->t('Part of the URL that determines language'),
'#type' => 'select',
'#empty_value' => '',
'#empty_option' => '- Select -',
'#options' => [
LanguageNegotiationUrlMatrix::CONFIG_PATH_PREFIX => $this->t('Path prefix'),
LanguageNegotiationUrlMatrix::CONFIG_DOMAIN => $this->t('Domain'),
],
'#default_value' => $config->get('matrix.source'),
];
if (empty($values)) {
$matrix_definition = $config->get('matrix.language.language_negotiation_matrix');
} else {
$matrix_definition = NestedArray::getValue($values, ['language','language_negotiation_matrix']);
}
$matrix = $this->loadMatrix($form_state, $config, $matrix_definition, $multi_language);
// Build the matrix mapping interface if we have a matrix
if ((!empty($values) && !empty($values['language']['language_negotiation_matrix'])) || !empty($matrix)) {
$this->buildMatrixMappingInterface($form, $form_state, $config, $matrix);
}
$form_state->setRedirect('language.negotiation');
return parent::buildForm($form, $form_state);
}
/**
* The callback function for when the `language_negotiation_matrix` element is changed.
*
* The return will replace the wrapper provided.
*/
public function ajaxSelectChange(array $form, FormStateInterface $form_state) {
// Return the element that will replace the wrapper (we return itself).
return $form['language'];
}
/**
* The callback function for when the `language_matrix` element is changed.
*
* The return will replace the wrapper provided.
*/
public function languageMatrixAjaxCallback(array &$form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$response->addCommand(new ReplaceCommand('#path-ajax-wrapper', $form['path_language']));
return $response;
}
/**
* Determines the current multi_language state from form state or config.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param \Drupal\Core\Config\Config $config
* The configuration object.
*
* @return bool
* TRUE if multi-language is enabled.
*/
protected function getMultiLanguageState(FormStateInterface $form_state, Config $config): bool {
$multi_language = NULL;
$user_inputs = $form_state->getUserInput();
if (isset($user_inputs['language'])) {
if (!isset($user_inputs['language']['multi_language'])) {
$multi_language = 0;
} else {
$multi_language = $user_inputs['language']['multi_language'];
}
}
// Fall back to config if not in form state
if ($multi_language === NULL) {
$multi_language = $config->get('matrix.language.multi_language');
}
return (bool) $multi_language;
}
/**
* Builds the key select element with appropriate multiple support.
*
* @param \Drupal\Core\Config\Config $config
* The configuration object.
* @param bool $multi_language
* Whether multi-language support is enabled.
*
* @return array
* The form element array.
*/
protected function buildKeySelectElement(Config $config, bool $multi_language): array {
$element = [
'#type' => 'key_select',
'#title' => $multi_language
? $this->t('Language Matrix ENV variables')
: $this->t('Language Matrix ENV variable'),
'#description' => $multi_language
? $this->t('Select multiple environment variables for different language matrices. Each key should contain a JSON object mapping language codes to paths.')
: $this->t('To take advantage of this language detection you will require an ENVIRONMENT variable describing the language to path relationship. This will be consumed by the API and converted into a matrix for proper language switching.'),
'#key_filters' => ['provider' => 'env', 'group' => 'generic'],
'#default_value' => $config->get('matrix.language.language_matrix'),
'#multiple' => $multi_language,
'#states' => [
'visible' => [
':input[name="language[language_negotiation_matrix]"]' => [
'value' => (string) 'key',
],
],
],
];
// For multiple selection, adjust the description and add size
if ($multi_language) {
$element['#size'] = 5;
$element['#description'] .= '<br><strong>' . $this->t('Hold Ctrl/Cmd to select multiple keys.') . '</strong>';
$element['#ajax'] = [
'callback' => [$this, 'languageMatrixAjaxCallback'],
'event' => 'change',
'wrapper' => 'path-ajax-wrapper', // must match container below
'progress' => ['type' => 'throbber', 'message' => NULL],
];
}
return $element;
}
/**
* Loads the matrix from various sources based on configuration.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param \Drupal\Core\Config\Config $config
* The configuration object.
* @param string $matrix_definition
* The matrix definition type ('key' or 'json').
* @param bool $multi_language
* Whether multi-language support is enabled.
*
* @return array|null
* The loaded matrix or NULL if none found.
*/
protected function loadMatrix(FormStateInterface $form_state, Config $config, string $matrix_definition, bool $multi_language): ?array {
$matrix = NULL;
if ($matrix_definition == 'key') {
$matrix = $this->loadMatrixFromKeys($form_state, $config, $multi_language);
}
elseif ($matrix_definition == 'json') {
$matrix = $this->loadMatrixFromJson($form_state, $config);
}
return $matrix;
}
/**
* Loads matrix data from key(s).
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param \Drupal\Core\Config\Config $config
* The configuration object.
* @param bool $multi_language
* Whether multi-language support is enabled.
*
* @return array|null
* The loaded matrix or NULL if none found.
*/
protected function loadMatrixFromKeys(FormStateInterface $form_state, Config $config, bool $multi_language): ?array {
$matrix = [];
$key_repository = \Drupal::service('key.repository');
// Get key IDs from form state or config
$key_ids = $form_state->getValue(['language','language_matrix']);
if (empty($key_ids)) {
$key_ids = $config->get('matrix.language.language_matrix');
}
if (empty($key_ids)) {
return NULL;
}
// Handle multiple keys
if ($multi_language && is_array($key_ids)) {
foreach ($key_ids as $key_id) {
if (!empty($key_id)) {
$key = $key_repository->getKey($key_id);
if ($key) {
$key_values = $key->getKeyValues();
$langcode = $key->getKeyType()->getConfiguration()['language'];
if ($langcode) {
$matrix[$langcode] = reset($key_values);
} else {
if (is_string($key_values)) {
// If key value is JSON string, decode it
$decoded_values = Json::decode($key_values);
if ($decoded_values) {
$matrix = array_merge($matrix, $decoded_values);
}
} else {
$matrix = array_merge($matrix, $key_values);
}
}
}
}
}
}
// Handle single key (backward compatibility)
else {
$key_id = is_array($key_ids) ? reset($key_ids) : $key_ids;
if (!empty($key_id)) {
$key = $key_repository->getKey($key_id);
if ($key) {
$key_values = $key->getKeyValues();
if (is_string($key_values)) {
$matrix = Json::decode($key_values);
} else {
$matrix = $key_values;
}
}
}
}
return $matrix ?: NULL;
}
/**
* Loads matrix data from JSON configuration.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param \Drupal\Core\Config\Config $config
* The configuration object.
*
* @return array|null
* The loaded matrix or NULL if none found.
*/
protected function loadMatrixFromJson(FormStateInterface $form_state, Config $config): ?array {
// Try to get from form state first
$json_value = $form_state->getValue(['language', 'language_matrix_json']);
if (empty($json_value)) {
$json_value = $config->get('matrix.language.language_matrix_json');
}
if (!empty($json_value)) {
if (is_string($json_value)) {
return Json::decode($json_value);
}
return $json_value;
}
return NULL;
}
/**
* Builds the matrix mapping interface.
*
* @param array &$form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param \Drupal\Core\Config\Config $config
* The configuration object.
* @param array $matrix
* The matrix data.
*/
protected function buildMatrixMappingInterface(array &$form, FormStateInterface $form_state, Config $config, array $matrix): void {
global $base_root;
$default_value = $config->get('matrix.prefixes.mapping');
$languages = $this->languageManager->getLanguages();
$prefixes = $config->get('matrix.prefixes');
$domains = $config->get('matrix.domains');
$requested_path = [];
$language_mapping = [];
$user_input = $form_state->getUserInput();
$source = $config->get('matrix.source');
foreach ($matrix as $langcode => $site_path) {
$site_path = '/' . ltrim($site_path, '/');
$requested_path[$langcode] = $site_path;
if (isset($languages[$langcode])) {
$language_mapping[$site_path] = $languages[$langcode]->getId();
} else {
$language_mapping[$site_path] = $langcode . ' (uninstalled)';
}
}
$form['path_language'] = [
'#type' => 'container',
'#attributes' => ['id' => 'path-ajax-wrapper'],
];
// Build prefix configuration
$form['path_language']['prefix'] = [
'#type' => 'details',
'#tree' => TRUE,
'#title' => $this->t('Path prefix configuration'),
'#open' => TRUE,
'#states' => [
'visible' => [
':input[name="language_negotiation_url_matrix_type"]' => [
'value' => (string) LanguageNegotiationUrlMatrix::CONFIG_PATH_PREFIX,
],
],
],
];
if (
(isset($user_input['language_negotiation_url_matrix_type']) && $user_input['language_negotiation_url_matrix_type'] !== LanguageNegotiationUrlMatrix::CONFIG_PATH_PREFIX) ||
(!isset($user_input['language_negotiation_url_matrix_type']) && $source !== LanguageNegotiationUrlMatrix::CONFIG_PATH_PREFIX)
) {
$form['path_language']['prefix']['#attributes'] = ['style' => 'display: none'];
}
$form['path_language']['prefix']['mapping'] = [
'#type' => 'mapping',
'#title' => $this->t('Language Matrix'),
'#description_display' => 'before',
'#source' => $requested_path,
'#source__title' => $this->t('Path Requested'),
'#destination' => $language_mapping,
'#destination__title' => $this->t('Language Mapping'),
'#destination__description' => NULL,
'#default_value' => $default_value,
'#filter' => FALSE,
'#description' => $this->t('The Language Matrix Negotiation will attempt to map the paths to <strong>active</strong> languages per the environment variable(s) provided. If you need to modify this list, please choose the desired mapping and save.'),
'#prefix' => '<div id="mappings-wrapper">',
'#suffix' => '</div>',
'#required' => TRUE,
];
// Build domain configuration
$form['path_language']['domain'] = [
'#type' => 'details',
'#tree' => TRUE,
'#title' => $this->t('Domain configuration'),
'#open' => TRUE,
'#description' => $this->t('The domain names to use for these languages. <strong>Modifying this value may break existing URLs. Use with caution in a production environment.</strong> Example: Specifying "de.example.com" as language domain for German will result in a URL like "http://de.example.com/contact".'),
'#states' => [
'visible' => [
':input[name="language_negotiation_url_matrix_type"]' => [
'value' => (string) LanguageNegotiationUrlMatrix::CONFIG_DOMAIN,
],
],
],
];
if (
(isset($user_input['language_negotiation_url_matrix_type']) && $user_input['language_negotiation_url_matrix_type'] !== LanguageNegotiationUrlMatrix::CONFIG_DOMAIN) ||
(!isset($user_input['language_negotiation_url_matrix_type']) && $source !== LanguageNegotiationUrlMatrix::CONFIG_DOMAIN)
) {
$form['path_language']['domain']['#attributes'] = ['style' => 'display: none'];
}
$form['path_language']['domain']['mapping'] = [
'#type' => 'mapping',
'#title' => $this->t('Language Matrix'),
'#description_display' => 'before',
'#source' => $requested_path,
'#source__title' => $this->t('Path Requested'),
'#destination' => $language_mapping,
'#destination__title' => $this->t('Language Mapping'),
'#destination__description' => NULL,
'#default_value' => $default_value,
'#filter' => FALSE,
'#description' => $this->t('The Language Matrix Negotiation will attempt to map the paths to <strong>active</strong> languages per the environment variable(s) provided. If you need to modify this list, please choose the desired mapping and save.'),
'#prefix' => '<div id="mappings-wrapper">',
'#suffix' => '</div>',
'#required' => TRUE,
];
$form['path_language']['prefix']['set'] = [
'#type' => 'markup',
'#markup' => $this->t('<h4>Active Languages for Prefixes</h4>'),
];
$form['path_language']['domain']['set'] = [
'#type' => 'markup',
'#markup' => $this->t('<h4>Active Languages for Domains</h4>'),
];
foreach ($languages as $langcode => $language) {
$t_args = ['%language' => $language->getName(), '%langcode' => $language->getId()];
$form['path_language']['prefix'][$langcode] = [
'#type' => 'textfield',
'#title' => $language->isDefault() ? $this->t('%language (%langcode) path prefix (Default language)', $t_args) : $this->t('%language (%langcode) path prefix', $t_args),
'#maxlength' => 64,
'#default_value' => $prefixes[$langcode] ?? '',
'#field_prefix' => $base_root . ($requested_path[$langcode] ?? '') . '/',
];
$form['path_language']['domain'][$langcode] = [
'#type' => 'textfield',
'#title' => $this->t('%language (%langcode) domain', ['%language' => $language->getName(), '%langcode' => $language->getId()]),
'#maxlength' => 128,
'#default_value' => $domains[$langcode] ?? '',
'#field_suffix' => ($requested_path[$langcode] ?? '') . '/',
];
}
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state): void {
$languages = $this->languageManager->getLanguages();
$prefix_type = $form_state->getValue('language_negotiation_url_matrix_type');
switch ($prefix_type) {
case LanguageNegotiationUrlMatrix::CONFIG_PATH_PREFIX:
// Get the form values for PREFIX
$form_values = $form_state->getValue('prefix');
// Set up the mappings for later
$mapping = $this->matrixManager->getSiteAliasMapping($form_values);
if (isset($form_values)) {
// Remove "mapping" array
unset($form_values['mapping']);
$count = array_count_values($form_values);
// This step is to walk through the language array and implement the
// necessary validation to ensure configuration won't fail.
foreach ($languages as $langcode => $language) {
$prefix_language = $form_state->getValue(['prefix', $langcode]);
// First, we'll check to see if the language prefixes event exist.
if ($prefix_language === '') {
// stub -- may be used for future purpose.
}
elseif (str_contains($prefix_language, '/')) {
// Throw a form error if the string contains a slash,
// which would not work.
$form_state->setErrorByName("prefix][$langcode", $this->t('The prefix may not contain a slash.'));
}
elseif (isset($count[$prefix_language]) && $count[$prefix_language] > 1) {
// Throw a form error if there are two languages with the same
// domain/prefix.
$form_state->setErrorByName("prefix][$langcode", $this->t('The prefix for %language, %value, is not unique.', ['%language' => $language->getName(), '%value' => $prefix_language]));
}
// Now that we've checked if we have any errors in the form values,
// we need to check and see if the paths exist on the system.
// Consider if using Apache or Nginx, some sort of symbolic links
// and folder combination will permit drupal to render its site at
// the proper URI. For this to work, the folder structure needs to
// exist. Let us check if the symlinks and directories are present,
// otherwise note the discrepancy.
// @TODO: We may want to let this slide so bad prefixes CAN exist.
if (!$this->matrixManager->siteAliasExists($mapping, $langcode)) {
// Throw a form error if the sites alias (folder structure) doesn't exist.
$form_state->setErrorByName("prefix][$langcode", $this->t('The folder %folder does not exist, therefore a proper matrix cannot yet be created. Please update your server configuration accordingly.', ['%folder' => $mapping[$langcode]]));
}
}
}
break;
case LanguageNegotiationUrlMatrix::CONFIG_DOMAIN:
// Get the form values for DOMAIN
$form_values = $form_state->getValue('domain');
if (isset($form_values)) {
// remove "mapping" array
unset($form_values['mapping']);
$count = array_count_values($form_values);
foreach ($languages as $langcode => $language) {
$domain_language = $form_state->getValue(['domain', $langcode]);
if ($domain_language === '') {
if ($form_state->getValue('language_negotiation_url_matrix_type') == LanguageNegotiationUrlMatrix::CONFIG_DOMAIN) {
// Throw a form error if the domain is blank for a non-default language,
// although it is required for selected negotiation type.
$form_state->setErrorByName("domain][$langcode", $this->t('The domain may not be left blank for %language.', ['%language' => $language->getName()]));
}
}
elseif (isset($count[$domain_language]) && $count[$domain_language] > 1) {
// Throw a form error if there are two languages with the same
// domain/domain.
$form_state->setErrorByName("domain][$langcode", $this->t('The domain for %language, %value, is not unique.', [
'%language' => $language->getName(),
'%value' => $domain_language
]));
}
}
// Domain names should not contain protocol and/or ports.
foreach ($languages as $langcode => $language) {
$domain_language = $form_state->getValue(['domain', $langcode]);
if (!empty($domain_language)) {
// Ensure we have exactly one protocol when checking the hostname.
$host = 'http://' . str_replace(['http://', 'https://'], '', $domain_language);
if (parse_url($host, PHP_URL_HOST) != $domain_language) {
$form_state->setErrorByName("domain][$langcode", $this->t('The domain for %language may only contain the domain name, not a trailing slash, protocol and/or port.', ['%language' => $language->getName()]));
}
}
}
}
break;
}
parent::validateForm($form, $form_state);
}
public function submitForm(array &$form, FormStateInterface $form_state) {
$config = $this->config('language.negotiation');
$prefix_type = $form_state->getValue('language_negotiation_url_matrix_type');
switch ($prefix_type) {
case LanguageNegotiationUrlMatrix::CONFIG_PATH_PREFIX:
$mappings = $form_state->getValue('prefix');
break;
case LanguageNegotiationUrlMatrix::CONFIG_DOMAIN:
$mappings = $form_state->getValue('domain');
break;
}
if (!isset($mappings)) {
$this->messenger()->addWarning($this->t('The %type values are not set properly. There is a good chance this Language Negotiation will not do what you want it to. Consider looking into the Matrix Definition and make sure the proper values are set or being passed in correctly.', [
'%type' => strtoupper($prefix_type),
]));
$config->set('matrix.has_path', FALSE);
} else {
if ($prefix_type == LanguageNegotiationUrlMatrix::CONFIG_PATH_PREFIX) {
$config->set('matrix.has_path', TRUE);
}
}
// Handle multi-language key array properly
$language_config = $form_state->getValue('language');
$language_matrix = $language_config['language_matrix'] ?? NULL;
// Ensure we store the language matrix properly (array for multi, string for single)
if (isset($language_config['multi_language']) && $language_config['multi_language'] && is_array($language_matrix)) {
// Keep as array for multiple keys
$language_config['language_matrix'] = array_filter($language_matrix);
}
$config
->set('matrix.language', $language_config)
->set('matrix.source', $prefix_type)
->set('matrix.prefixes', $form_state->getValue('prefix'))
->set('matrix.domains', $form_state->getValue('domain'))
->save();
parent::submitForm($form, $form_state);
}
}
