g2-8.x-1.x-dev/src/Form/SettingsForm.php

src/Form/SettingsForm.php
<?php

declare(strict_types=1);

namespace Drupal\g2\Form;

use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteBuilderInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\g2\Alphabar;
use Drupal\g2\G2;
use Drupal\g2\WOTD;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * Class SettingsForm contains the G2 configuration form.
 *
 * @todo Refactor like \Drupal\config_inspector\Form\ConfigInspectorItemForm.
 * @todo Relate service.alphabar.contents configuration with routes like
 *   g2.initial.
 *
 * @phpstan-consistent-constructor
 */
class SettingsForm extends ConfigFormBase {

  use StringTranslationTrait;

  /**
   * The ID of the Alphabar contents in the form. Used for the Ajax callback.
   */
  const ALPHABAR_WRAPPER_ID = 'services-alphabar-contents-wrapper';

  /**
   * The g2.alphabar service.
   *
   * @var \Drupal\g2\Alphabar
   */
  protected Alphabar $alphabar;

  /**
   * The entity_type.manager service.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected EntityTypeManagerInterface $etm;

  /**
   * The router.builder service.
   *
   * @var \Drupal\Core\Routing\RouteBuilderInterface
   */
  protected RouteBuilderInterface $routerBuilder;

  /**
   * The g2.wotd service.
   *
   * @var \Drupal\g2\WOTD
   */
  protected WOTD $wotd;

  /**
   * Constructs a \Drupal\system\ConfigFormBase object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $etm
   *   The entity_type.manager service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The factory for configuration objects.
   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
   *   The config.typed service.
   * @param \Drupal\Core\Routing\RouteBuilderInterface $router_builder
   *   The router.builder service.
   * @param \Drupal\g2\Alphabar $alphabar
   *   The g2.alphabar service.
   * @param \Drupal\g2\WOTD $wotd
   *   The g2.wotd service.
   */
  public function __construct(
    EntityTypeManagerInterface $etm,
    ConfigFactoryInterface $configFactory,
    TypedConfigManagerInterface $typedConfigManager,
    RouteBuilderInterface $router_builder,
    Alphabar $alphabar,
    WOTD $wotd,
  ) {
    parent::__construct($configFactory, $typedConfigManager);
    $this->alphabar = $alphabar;
    $this->etm = $etm;
    $this->routerBuilder = $router_builder;
    $this->wotd = $wotd;
  }

  /**
   * Extract the last part of a dotted path as used in config and state.
   *
   * @param string $name
   *   The path to extract from.
   *
   * @return string
   *   The last component.
   */
  protected function component(string $name): string {
    $ar = explode('.', $name);
    return end($ar);
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): static {
    $alphabar = $container->get(G2::SVC_ALPHABAR);

    /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $etm */
    $etm = $container->get(G2::SVC_ETM);

    /** @var \Drupal\Core\Config\ConfigFactoryInterface $config_factory */
    $config_factory = $container->get(G2::SVC_CONF);

    /** @var \Drupal\Core\Routing\RouteBuilderInterface $router_builder */
    $router_builder = $container->get('router.builder');

    /** @var \Drupal\Core\Config\TypedConfigManagerInterface $typed_config */
    $typed_config = $container->get('config.typed');

    /** @var \Drupal\g2\WOTD $wotd */
    $wotd = $container->get(G2::SVC_WOTD);

    return new static($etm, $config_factory, $typed_config, $router_builder, $alphabar, $wotd);
  }

  /**
   * Helper for form builders : prepare top-level details.
   *
   * @param mixed[] $form
   *   The base form array.
   * @param array<string,mixed> $schema
   *   The configuration schema from which to take the list of details.
   * @param string $section
   *   The configuration section in which to build the list of details.
   *
   * @return mixed[]
   *   The extended array.
   */
  protected static function prepareTopLevelDetails(array $form, array $schema, string $section): array {
    /**
     * @var string $top
     * @var mixed[] $top_schema
     */
    foreach ($schema as $top => $top_schema) {
      if (!isset($form[$section])) {
        $form[$section] = [];
      }
      $form[$section][$top] = [
        '#type' => 'details',
        '#title' => $top_schema['label'],
      ];
    }
    return $form;
  }

  /**
   * Split a config label in two parts: title and description, if available.
   *
   * The split happens at the first "." or "?", if any.
   *
   * @param string $label
   *   The combined title and description, to be split.
   *
   * @return string[]
   *   A ['#title' => $title, '#description' => $description] array.
   */
  protected function getInfoFromLabel(string $label): array {
    // Merge a 2-element array to guarantee the destructuring assignment below.
    $split = preg_split('/([.?])/', $label, 2);
    assert(is_array($split));
    $exploded = array_merge($split, [NULL, NULL]);
    [$title, $description] = $exploded;
    $info = [];
    if (!empty($title)) {
      $info['#title'] = $title;
    }
    if (!empty($description)) {
      $info['#description'] = $description;
    }
    return $info;
  }

  /**
   * Return the list of config names which should not be immutable.
   *
   * @return string[]
   *   The names.
   */
  protected function getEditableConfigNames(): array {
    return [G2::CONFIG_NAME];
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId(): string {
    $match = $this->getRouteMatch();
    $section = $match->getParameter('section');
    $result = 'g2_settings-' . $section;
    return $result;
  }

  /**
   * Build the blocks configuration form.
   *
   * @param mixed[] $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $formState
   *   The form state.
   * @param array<string,mixed> $config
   *   The configuration for which to build a form.
   * @param array<string,mixed> $schema
   *   The schema of the configuration for which to build a form.
   *
   * @return mixed[]
   *   The form array.
   */
  public function buildBlockForm(array $form, FormStateInterface $formState, array $config, array $schema): array {
    $section = 'block';
    $form = $this->prepareTopLevelDetails($form, $schema, $section);
    $service_config = $this->config(G2::CONFIG_NAME)->get('services');

    $element = &$form[$section]['alphabar'];
    $element['row_length'] = [
      '#type' => 'number',
      '#title' => $schema['alphabar']['mapping']['row_length']['label'],
      '#default_value' => $config['alphabar']['row_length'],
    ];

    $element = &$form[$section]['latest'];
    $element['count'] = [
      '#type' => 'number',
      '#title' => $schema['latest']['mapping']['count']['label'],
      '#default_value' => $config['latest']['count'],
      '#max' => $service_config['latest']['max_count'],
      '#min' => 1,
    ];

    $element = &$form[$section]['random'];
    $element['max_age'] = [
      '#type' => 'number',
      '#min' => 1,
      '#default_value' => $config['random']['max_age'],
    ] + $this->getInfoFromLabel((string) ($schema['random']['mapping']['max_age']['label'] ?? ''));

    $path = explode('.', G2::VARTOPCOUNT);
    [$section, $group, $key] = $path;
    $element = &$form[$section][$group];
    $element[$key] = [
      '#type' => 'number',
      '#title' => $schema[$group]['mapping'][$key]['label'],
      '#default_value' => $config[$group][$key],
      '#max' => $service_config[$group]['max_count'],
      '#min' => 1,
    ];

    $element = &$form[$section]['wotd'];
    $element['wotd'] = [
      '#markup' => $this->t('<ul>
  <li>Place a G2 WOTD block in a region using the <a href=":block">block configuration page</a>,
    and configure its title, feed icon, and display conditions there.</li>
  <li>Use the <a href=":vm">G2 Block view display</a> on the %bundle to adjust the WOTD rendering.</li>
  <li>Configure the WOTD value and rotation on the <a href=":services">WOTD service configuration</a>.</li>
  </ul>',
        [
          ':block' => Url::fromRoute(G2::ROUTE_BLOCKS)->toString(),
          ':vm' => Url::fromRoute(G2::ROUTE_VM, [
            'node_type' => G2::BUNDLE,
            'view_mode_name' => G2::VM_BLOCK,
          ])->toString(),
          '%bundle' => G2::BUNDLE,
          ':services' => Url::fromRoute(G2::ROUTE_CONFIG_SERVICES)->toString(),
        ])
      . '</p>',
    ];

    return $form;
  }

  /**
   * Build the controllers configuration form.
   *
   * @param mixed[] $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $formState
   *   The form state.
   * @param array<string,mixed> $config
   *   The configuration for which to build a form.
   * @param array<string,mixed> $schema
   *   The schema of the configuration for which to build a form.
   *
   * @return mixed[]
   *   The form array.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   *
   * @todo provide an auto-complete for routes instead of using a plain string.
   * @todo provide an auto-complete for node ids instead of using a plain
   *   number.
   */
  public function buildControllerForm(array $form, FormStateInterface $formState, array $config, array $schema): array {
    $section = 'controller';
    $form = $this->prepareTopLevelDetails($form, $schema, $section);
    $this->messenger()
      ->addStatus($this->t('Be aware that saving configuration on this tab will mark the router as needing a rebuild.'));

    $element = &$form['controller']['main'];
    $element['nid'] = [
      '#type' => 'number',
      '#default_value' => $config['main']['nid'],
      '#element_validate' => [[$this, 'validateControllerMainNid']],
    ] + $this->getInfoFromLabel((string) ($schema['main']['mapping']['nid']['label'] ?? ''));

    foreach ([
      'main' => G2::ROUTE_AUTOCOMPLETE_ROUTE_0PARAM,
      'initial' => G2::ROUTE_AUTOCOMPLETE_ROUTE_1PARAM,
      'homonyms' => G2::ROUTE_AUTOCOMPLETE_ROUTE_1PARAM,
    ] as $name => $route) {
      $element = &$form['controller'][$name];
      $element['route'] = [
        '#type' => 'textfield',
        '#autocomplete_route_name' => $route,
        '#default_value' => $config[$name]['route'],
      ] + $this->getInfoFromLabel((string) ($schema[$name]['mapping']['route']['label'] ?? ''));
    }

    $element = &$form['controller']['homonyms'];
    $element['redirect_on_single_match'] = [
      '#type' => 'checkbox',
      '#default_value' => $config['homonyms']['redirect_on_single_match'],
    ] + $this->getInfoFromLabel((string) ($schema['homonyms']['mapping']['redirect_on_single_match']['label'] ?? ''));

    $redirects = [
      Response::HTTP_MOVED_PERMANENTLY,
      Response::HTTP_FOUND,
      Response::HTTP_TEMPORARY_REDIRECT,
      Response::HTTP_PERMANENTLY_REDIRECT,
    ];
    $options = [];
    foreach ($redirects as $redirect) {
      $options[$redirect] = $this->t(
        '@status: @text',
        [
          '@status' => $redirect,
          '@text' => Response::$statusTexts[$redirect],
        ]
      );
    }
    $element['redirect_status'] = [
      '#type' => 'radios',
      '#title' => $schema['homonyms']['mapping']['redirect_status']['label'],
      '#options' => $options,
      '#default_value' => $config['homonyms']['redirect_status'],
    ];

    $view_ids = $this->etm
      ->getStorage('view')
      ->getQuery()
      ->condition('tag', 'G2')
      ->execute();
    $views = $this->etm->getStorage('view')->loadMultiple($view_ids);
    $options = ['' => $this->t('-- Use plain node list --')];
    /** @var \Drupal\views\ViewEntityInterface $view */
    foreach ($views as $vid => $view) {
      $options[$vid] = $view->label();
    }
    $element['vid'] = [
      '#type' => 'select',
      '#options' => $options,
      '#default_value' => $config['homonyms']['vid'],
    ] + $this->getInfoFromLabel((string) ($schema['homonyms']['mapping']['vid']['label'] ?? ''));
    $element['nid'] = [
      '#type' => 'number',
      '#default_value' => $config['homonyms']['nid'],
      '#element_validate' => [[$this, 'validateControllerHomonymsNid']],
    ] + $this->getInfoFromLabel((string) ($schema['homonyms']['mapping']['nid']['label'] ?? ''));

    $path = explode('.', G2::VARLOGREFERERS);
    [$section, $group, $key] = $path;
    $element = &$form[$section][$group];
    $element[$key] = [
      '#type' => 'checkbox',
      '#default_value' => $config[$group][$key],
    ] + $this->getInfoFromLabel((string) ($schema[$group]['mapping'][$key]['label'] ?? ''));
    $element['wipe'] = [
      '#markup' => '<p>'
      . Link::fromTextAndUrl(
        $this->t('Wipe all stored referers'),
        Url::fromRoute(G2::ROUTE_WIPE_ALL),
      )->toString()
      . '</p>',
    ];

    $element = &$form['controller']['wotd'];
    $element['controller'] = [
      '#type' => 'markup',
      '#markup' => $this->t('Configure the WOTD feed by editing the <a href=":view"><code>g2_wotd</code> view</a> and access it on <a href=":url">:url</a>', [
        ':view' => Url::fromRoute('entity.view.edit_display_form', [
          'view' => G2::VIEW_WOTD,
          'display_id' => G2::VIEW_WOTD_DISPLAY,
        ])->toString(),
        ':url' => Url::fromRoute(G2::ROUTE_FEED_WOTD)->toString(),
      ]),
    ];
    return $form;
  }

  /**
   * Build the formatting configuration form.
   *
   * @param mixed[] $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $formState
   *   The form state.
   * @param array<string,mixed> $config
   *   The configuration for which to build a form.
   * @param array<string,mixed> $schema
   *   The schema of the configuration for which to build a form.
   *
   * @return mixed[]
   *   The form array.
   */
  public function buildFormattingForm(array $form, FormStateInterface $formState, array $config, array $schema): array {
    [, $element] = explode('.', G2::VARNOFREETAGGING);
    $form['formatting'][$element] = [
      '#type' => 'checkbox',
      '#title' => $schema[$element]['label'],
      '#default_value' => $config[$element],
    ];

    [, $element] = explode('.', G2::VARTOOLTIPS);
    $form['formatting'][$element] = [
      '#type' => 'select',
      '#options' => [
        G2::TOOLTIPS_NONE => $this->t('None'),
        G2::TOOLTIPS_TITLES => $this->t('Titles'),
        G2::TOOLTIPS_TEASERS => $this->t('Teasers'),
      ],
      '#default_value' => $config[$element],
      '#element_validate' => [[$this, 'validateFormattingTooltips']],
    ] + $this->getInfoFromLabel((string) ($schema[$element]['label'] ?? ''));

    [, $element] = explode('.', G2::VARPAGETITLE);
    $form['formatting'][$element] = [
      '#type' => 'textfield',
      '#default_value' => $config[$element],
    ] + $this->getInfoFromLabel((string) ($schema[$element]['label'] ?? ''));

    return $form;
  }

  /**
   * Build the API configuration form.
   *
   * @param mixed[] $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $formState
   *   The form state.
   * @param array<string,mixed> $config
   *   The configuration for which to build a form.
   * @param array<string,mixed> $schema
   *   The schema of the configuration for which to build a form.
   *
   * @return mixed[]
   *   The form array.
   */
  public function buildApiForm(array $form, FormStateInterface $formState, array $config, array $schema): array {
    $form['api']['help'] = [
      '#markup' => '<p>'
      . $this->t('Configure the G2 API client and server if needed.
The client enables your site to provide links using a remote G2 instance on
another site, while the server allows your site to provide entries to such
  client sites.')
      . '</p>',
    ];
    $form['api']['client'] = [
      '#type' => 'details',
      '#title' => $schema['client']['label'],
    ];
    $form['api']['client']['remote'] = [
      '#type' => 'textfield',
      '#default_value' => $config['client']['remote'],
      '#element_validate' => [[$this, 'validateApiRemote']],
    ] + $this->getInfoFromLabel((string) ($schema['client']['mapping']['remote']['label'] ?? ''));

    $form['api']['server'] = [
      '#type' => 'details',
      '#title' => $schema['server']['label'],
    ];
    $form['api']['server']['enabled'] = [
      '#type' => 'checkbox',
      '#title' => $schema['server']['mapping']['enabled']['label'],
      '#default_value' => $config['server']['enabled'],
    ];
    $form['api']['server']['throttle'] = [
      '#type' => 'range',
      '#default_value' => $config['server']['throttle'],
      '#max' => 1.0,
      '#min' => 0.0,
      '#step' => 0.1,
      '#element_validate' => [[$this, 'validateApiThrottle']],
    ] + $this->getInfoFromLabel((string) ($schema['server']['mapping']['throttle']['label'] ?? ''));

    $form['api']['version'] = [
      '#markup' => '<p>'
      . $this->t('This site is running G2 version %version. See the <a href=":page" title="G2 Glossary project page">G2 project page</a> on Drupal.org.', [
        '%version' => G2::VERSION,
        ':page' => 'https://www.drupal.org/project/g2',
      ])
      . '</p>',
    ];

    return $form;
  }

  /**
   * Build the services configuration form.
   *
   * @param mixed[] $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $formState
   *   The form state.
   * @param array<string,mixed> $config
   *   The configuration for which to build a form.
   * @param array<string,mixed> $schema
   *   The schema of the configuration for which to build a form.
   *
   * @return mixed[]
   *   The form array.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function buildServicesForm(array $form, FormStateInterface $formState, array $config, array $schema): array {
    $section = 'services';
    $form = $this->prepareTopLevelDetails($form, $schema, $section);

    $wrapperID = str_replace('.', '-', G2::VARALPHABARCONTENTS) . '-wrapper';
    $element = &$form[$section]['alphabar'];
    $element['contents'] = [
      '#type' => 'textfield',
      '#title' => $schema['alphabar']['mapping']['contents']['label'],
      '#default_value' => $config['alphabar']['contents'],
      '#prefix' => "<div id='$wrapperID'>",
      '#suffix' => '</div>',
    ];
    $element['generate'] = [
      '#type' => 'button',
      '#value' => $this->t('Rebuild from existing G2 entries'),
      '#name' => 'generate',
      '#ajax' => [
        'callback' => '::generateAlphabar',
        'disable-refocus' => TRUE,
        'wrapper' => $wrapperID,
        'progress' => [
          'type' => 'throbber',
          'message' => $this->t('Scanning G2 entries for their initials.'),
        ],
      ],
    ];
    // @todo Remove after #3369926 is fixed.
    $path = explode('.', G2::VARALPHABAR3369926);
    [$section, $group, $key] = $path;
    $element[$key] = [
      '#type' => 'checkbox',
      '#default_value' => $config[$group][$key] ?? FALSE,
    ] + $this->getInfoFromLabel((string) ($schema[$group]['mapping'][$key]['label'] ?? ''));

    $element = &$form[$section]['random'];
    $key = $this->component(G2::VARRANDOMSTORE);
    $element[$key] = [
      '#type' => 'checkbox',
      '#default_value' => $config['random'][$key],
    ] + $this->getInfoFromLabel((string) ($schema['random']['mapping'][$key]['label'] ?? ''));

    foreach (['latest', 'top'] as $service) {
      $element = &$form[$section][$service];
      $element['max_count'] = [
        '#type' => 'number',
        '#default_value' => $config[$service]['max_count'],
        '#min' => 1,
      ] + $this->getInfoFromLabel((string) ($schema[$service]['mapping']['max_count']['label'] ?? ''));

      $element = &$form[$section]['wotd'];
      $wotd = $this->wotd->get();
      $key = $this->component(G2::VARWOTDENTRY);
      $element[$key] = [
        '#type' => 'textfield',
        '#title' => $schema['wotd']['mapping'][$key]['label'],
        '#autocomplete_route_name' => G2::ROUTE_AUTOCOMPLETE_ENTRY,
        '#maxlength' => 60,
        '#required' => FALSE,
        '#default_value' => $this->wotd->numberedTitleInput($wotd),
        '#element_validate' => [[$this, 'validateServicesWordOfTheDay']],
      ];

      $key = $this->component(G2::VARWOTDAUTOCHANGE);
      $element[$key] = [
        '#type' => 'checkbox',
        '#default_value' => $config['wotd'][$key],
      ] + $this->getInfoFromLabel((string) ($schema['wotd']['mapping'][$key]['label'] ?? ''));
    }
    return $form;
  }

  /**
   * Build the settings form.
   *
   * @param mixed[] $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The configuration for which to build a form.
   * @param \Drupal\Core\Routing\RouteMatchInterface|null $route
   *   The current_route_match service.
   *
   * @return mixed[]
   *   The build form.
   */
  public function buildForm(array $form, FormStateInterface $form_state, RouteMatchInterface $route = NULL): array {
    if (empty($route)) {
      return $form;
    }
    $section = (string) ($route->getParameter('section') ?: '');
    $form['#tree'] = TRUE;
    $form['section'] = [
      '#type' => 'value',
      '#value' => $section,
    ];
    $form['#attached']['library'][] = G2::LIB_ADMIN;

    $builder = 'build' . ucfirst($section) . 'Form';
    if (method_exists($this, $builder)) {
      $config = $this->config(G2::CONFIG_NAME)->get($section);
      $configSchema = $this->typedConfigManager()->getDefinition(G2::CONFIG_NAME);
      $schema = $configSchema['mapping'][$section]['mapping'] ?? [];
      $form = $this->{$builder}($form, $form_state, $config, $schema);
    }

    return parent::buildForm($form, $form_state);
  }

  /**
   * AJAX callback for Services/Alphabar form "Generate from G2 entries" button.
   *
   * @param mixed[] $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $formState
   *   The form state.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse|null
   *   The response.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function generateAlphabar(array $form, FormStateInterface $formState, Request $request): ?AjaxResponse {
    $trigger = $formState->getTriggeringElement();
    if (($trigger['#name'] ?? '') !== 'generate') {
      return NULL;
    }
    $entries = $this->alphabar->fromEntries();
    $contents = implode(array_keys($entries));
    $element = $form['services']['alphabar']['contents'] ?? [];
    assert(is_array($element));
    $element['#value'] = $contents;
    $element['#description'] = $this->t('Alphabar regenerated. You can still update it before submitting the form.');
    $res = new AjaxResponse();
    $res->addCommand(new ReplaceCommand("", $element));
    return $res;
  }

  /**
   * Additional submit handler for the controller configuration form.
   *
   * @param mixed[] $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  public function submitControllerForm(array &$form, FormStateInterface $form_state): void {
    $this->routerBuilder->setRebuildNeeded();
    $this->messenger()->addStatus($this->t('The router has been marked for rebuilding.'));
  }

  /**
   * Additional submit handler for the services configuration form.
   *
   * @param mixed[] $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  public function submitServicesForm(array $form, FormStateInterface $form_state): void {
    G2::invalidateWotdView();
  }

  /**
   * The form submit handler.
   *
   * @param mixed[] $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  public function submitForm(array &$form, FormStateInterface $form_state): void {
    $values = $form_state->getValues();
    $section = $values['section'];
    $values = $values[$section];
    $this->configFactory()
      ->getEditable(G2::CONFIG_NAME)
      ->set($section, $values)
      ->save();

    $handler = 'submit' . ucfirst($section) . 'Form';
    if (method_exists($this, $handler)) {
      $this->{$handler}($form, $form_state);
    }

    $this->messenger()
      ->addStatus($this->t('The configuration options have been saved.'));
  }

  /**
   * Validate that a given nid is either 0 or matches an unpublished node.
   *
   * @param mixed[] $element
   *   The element.
   * @param \Drupal\Core\Form\FormStateInterface $formState
   *   The form state.
   * @param mixed[] $form
   *   The complete form.
   * @param string $path
   *   The formState path to the nid.
   * @param string $message
   *   The error message if validation fails.
   *
   * @return mixed[]
   *   The element, possibly updated.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  protected function validateNid(
    array &$element,
    FormStateInterface $formState,
    array $form,
    string $path,
    string $message,
  ): array {
    $arPath = explode('.', $path);
    [$section, $group, $key] = $arPath;
    $nid = $formState->getValue($arPath);
    assert(is_scalar($nid));
    if (empty($nid)) {
      $element['#value'] = 0;
      return $element;
    }

    /** @var \Drupal\node\NodeInterface|null $node */
    $node = $this->etm
      ->getStorage(G2::TYPE)
      ->load($nid);
    if (empty($node) || $node->isPublished()) {
      // $message is one of two constant errors strings, so the phpcs warning
      // is a false positive: this is not varying content.
      $translated = $this->t($message, ['@nid' => $nid]); // phpcs:ignore

      // See https://www.drupal.org/project/drupal/issues/3373098
      // @phpstan-ignore-next-line
      $formState->setError($form[$section][$group][$key], $translated);
    }
    return $element;
  }

  /**
   * Callback for #elementValidate on controller main nid.
   *
   * @param mixed[] $element
   *   The element.
   * @param \Drupal\Core\Form\FormStateInterface $formState
   *   The form state.
   * @param mixed[] $form
   *   The complete form.
   *
   * @return mixed[]
   *   The modified element.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function validateControllerMainNid(
    array &$element,
    FormStateInterface $formState,
    array $form,
  ): array {
    return $this->validateNid($element, $formState, $form, G2::VARMAINNID,
      'The node chosen for the main page must be a valid unpublished one, or 0: "@nid" does not satisfy these requirements.');
  }

  /**
   * Callback for #elementValidate on controller homonyms nid.
   *
   * @param mixed[] $element
   *   The element.
   * @param \Drupal\Core\Form\FormStateInterface $formState
   *   The form state.
   * @param mixed[] $form
   *   The complete form.
   *
   * @return mixed[]
   *   The modified element.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function validateControllerHomonymsNid(
    array &$element,
    FormStateInterface $formState,
    array $form,
  ): array {
    return $this->validateNid($element, $formState, $form, G2::VARHOMONYMSNID,
      'The node chosen for the homonyms disambiguation page must be a valid unpublished one, or 0: "@nid" does not satisfy these requirements.');
  }

  /**
   * The form validate handler.
   *
   * @param mixed[] $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  public function validateForm(array &$form, FormStateInterface $form_state): void {
    // Remove non-input elements.
    $form_state->unsetValue(['services', 'alphabar', 'generate']);
    parent::validateForm($form, $form_state);
  }

  /**
   * Callback for #elementValidate on formatting tooltips.
   *
   * @param mixed[] $element
   *   The element.
   * @param \Drupal\Core\Form\FormStateInterface $formState
   *   The form state.
   * @param mixed[] $form
   *   The complete form.
   *
   * @return mixed[]
   *   The modified element.
   */
  public function validateFormattingTooltips(
    array &$element,
    FormStateInterface $formState,
    array $form,
  ): array {
    $ttPath = explode('.', G2::VARTOOLTIPS);
    [$section, $key] = $ttPath;
    $level = $formState->getValue($ttPath);
    assert(is_scalar($level));
    if (empty($level)) {
      $element['#value'] = 0;
      return $element;
    }

    $remote = $this->configFactory->get(G2::CONFIG_NAME)->get(G2::VARREMOTEG2);
    if (!empty($remote)) {
      $formState->setError($form[$section][$key],
        // See https://www.drupal.org/project/drupal/issues/3373098
        // @phpstan-ignore-next-line
        $this->t('Tooltips are only available on local glossaries, but this G2 glossary is <a href=":admin">configured</a> to use the remote glossary at <a href=":remote">:remote</a>.', [
          ':admin' => Url::fromRoute(G2::ROUTE_CONFIG_API)->toString(),
          ':remote' => $remote,
        ]));
    }
    return $element;
  }

  /**
   * Callback for #elementValidate on API remote client URL.
   *
   * @param mixed[] $element
   *   The element.
   * @param \Drupal\Core\Form\FormStateInterface $formState
   *   The form state.
   * @param mixed[] $form
   *   The complete form.
   *
   * @return mixed[]
   *   The modified element.
   */
  public function validateApiRemote(
    array &$element,
    FormStateInterface $formState,
    array $form,
  ): array {
    $remPath = explode('.', G2::VARREMOTEG2);
    [$section, $group, $key] = $remPath;
    $url = $formState->getValue($remPath);
    if (empty($url)) {
      $element['#value'] = NULL;
      return $element;
    }

    $tt = $this->configFactory->get(G2::CONFIG_NAME)->get(G2::VARTOOLTIPS);
    if (!empty($tt)) {
      $formState->setError($form[$section][$group][$key],
        // See https://www.drupal.org/project/drupal/issues/3373098
        // @phpstan-ignore-next-line
        $this->t('This configuration attempts to enable the G2 remote client,
          although tooltips are <a href=":admin">configured</a>.', [
            ':admin' => Url::fromRoute(G2::ROUTE_CONFIG_FORMATTING)->toString(),
          ]
      ));
    }
    return $element;
  }

  /**
   * Callback for #elementValidate on API remote server throttle.
   *
   * @param mixed[] $element
   *   The element.
   * @param \Drupal\Core\Form\FormStateInterface $formState
   *   The form state.
   * @param mixed[] $form
   *   The complete form.
   *
   * @return mixed[]
   *   The modified element.
   */
  public function validateApiThrottle(
    array &$element,
    FormStateInterface $formState,
    array $form,
  ): array {
    // Sanitize API throttle.
    $tPath = explode('.', G2::VARAPITHROTTLE);
    $throttle = $formState->getValue($tPath);
    assert(is_scalar($throttle));
    $formState->setValueForElement($element, (float) $throttle);

    return $element;
  }

  /**
   * Callback for #elementValidate on WOTD entry.
   *
   * @param mixed[] $element
   *   The element.
   * @param \Drupal\Core\Form\FormStateInterface $formState
   *   The form state.
   * @param mixed[] $form
   *   The complete form.
   *
   * @return mixed[]
   *   The modified element.
   */
  public function validateServicesWordOfTheDay(
    array &$element,
    FormStateInterface $formState,
    array $form,
  ): array {
    $path = explode('.', G2::VARWOTDENTRY);
    $value = (string) $formState->getValue($path);
    if (empty($value)) {
      $element['#value'] = '';
      return $element;
    }
    $nodes = $this->wotd->matchesFromTitle($value);
    if (empty($nodes)) {
      return $element;
    }
    $node = current($nodes);
    $nid = (int) $node->id();
    $formState->setValueForElement($element, $nid);
    return $element;
  }

}

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

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