eca-1.0.x-dev/modules/modeller_bpmn/src/ModellerBpmnBase.php

modules/modeller_bpmn/src/ModellerBpmnBase.php
<?php

namespace Drupal\eca_modeller_bpmn;

use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Component\Utility\Random;
use Drupal\Core\Action\ActionInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Form\FormState;
use Drupal\eca\Entity\Eca;
use Drupal\eca\Entity\Model;
use Drupal\eca\Plugin\ECA\Condition\ConditionInterface;
use Drupal\eca\Plugin\ECA\EcaPluginBase;
use Drupal\eca\Plugin\ECA\Event\EventInterface;
use Drupal\eca\Plugin\ECA\Modeller\ModellerBase;
use Drupal\eca\Plugin\ECA\Modeller\ModellerInterface;
use Drupal\eca\Service\Modellers;
use Drupal\eca_ui\Service\TokenBrowserService;
use Mtownsend\XmlToArray\XmlToArray;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Abstract class for BPMN modellers.
 *
 * Providing generic functionality which is similar to all such modellers.
 */
abstract class ModellerBpmnBase extends ModellerBase {

  /**
   * The model data as an XML string.
   *
   * @var string
   */
  protected string $modeldata;

  /**
   * The unserialized model data as an XML object.
   *
   * @var array
   */
  protected array $xmlModel;

  /**
   * The filename of the BPMN model, if saved in the file system.
   *
   * @var string
   */
  protected string $filename;

  /**
   * The DOM of the XML data for detailed processing.
   *
   * @var \DOMDocument
   */
  protected \DOMDocument $doc;

  /**
   * The DOM Xpath object for DOM queries.
   *
   * @var \DOMXPath
   */
  protected \DOMXPath $xpath;

  /**
   * ECA token browser service.
   *
   * @var \Drupal\eca_ui\Service\TokenBrowserService
   */
  protected TokenBrowserService $tokenBrowserService;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
    $instance->tokenBrowserService = $container->get('eca_ui.service.token_browser');
    return $instance;
  }

  /**
   * Prepares the data for further updates processes.
   *
   * @param string $data
   *   The serialized data of this model.
   */
  protected function prepareForUpdate(string $data): void {
    $this->modeldata = $data;
    $this->xmlModel = XmlToArray::convert($this->modeldata);
    $this->doc = new \DOMDocument();
    $this->doc->loadXML($this->modeldata);
    $this->xpath = new \DOMXPath($this->doc);
  }

  /**
   * {@inheritdoc}
   */
  protected function prepareForExport(): void {
    $this->prepareForUpdate($this->eca->getModel()->getModeldata());
  }

  /**
   * Return the XML namespace prefix used by the BPMN modeller.
   *
   * @return string
   *   The namespace prefix used by the current modeller.
   */
  protected function xmlNsPrefix(): string {
    return '';
  }

  /**
   * Prepares data for a new and empty BPMN model.
   *
   * @return string
   *   The model data.
   */
  public function prepareEmptyModelData(string &$id): string {
    $id = $this->generateId();
    $emptyBpmn = file_get_contents($this->extensionPathResolver->getPath('module', 'eca_modeller_bpmn') . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'empty.bpmn');
    return str_replace([
      'SIDPLACEHOLDER1',
      'SIDPLACEHOLDER2',
      'IDPLACEHOLDER',
    ], [
      'sid-' . $this->uuid->generate(),
      'sid-' . $this->uuid->generate(),
      $id,
    ], $emptyBpmn);
  }

  /**
   * {@inheritdoc}
   */
  public function generateId(): string {
    $random = new Random();
    return 'Process_' . $random->name(7);
  }

  /**
   * {@inheritdoc}
   */
  public function createNewModel(string $id, string $model_data, ?string $filename = NULL, bool $save = FALSE): Eca {
    $eca = Eca::create(['id' => mb_strtolower($id)]);
    $eca->getModel()->setModeldata($model_data);
    $this->setConfigEntity($eca);
    if ($save) {
      $this->save($model_data, $filename);
      $eca = $this->getEca();
    }
    return $eca;
  }

  /**
   * {@inheritdoc}
   */
  public function save(string $data, ?string $filename = NULL, ?bool $status = NULL): bool {
    $this->prepareForUpdate($data);
    $this->filename = $filename ?? '';
    if ($status !== NULL) {
      $this->xmlModel[$this->xmlNsPrefix() . 'process']['@attributes']['isExecutable'] = $status ? 'true' : 'false';
    }
    return $this->modellerServices->saveModel($this);
  }

  /**
   * {@inheritdoc}
   *
   * @throws \DOMException
   */
  public function updateModel(Model $model): bool {
    $this->prepareForUpdate($this->eca->getModel()->getModeldata());
    $changed = FALSE;
    $idxExtension = $this->xmlNsPrefix() . 'extensionElements';
    foreach ($this->getTemplates() as $template) {
      foreach ($template['appliesTo'] as $type) {
        switch ($type) {
          case 'bpmn:Event':
            $objects = $this->getStartEvents();
            break;

          case 'bpmn:SequenceFlow':
            $objects = $this->getSequenceFlows();
            break;

          case 'bpmn:Task':
            $objects = $this->getTasks();
            break;

          default:
            $objects = [];

        }
        foreach ($objects as $object) {
          if (isset($object['@attributes']['modelerTemplate']) && $template['id'] === $object['@attributes']['modelerTemplate']) {
            $fields = $this->findFields($object[$idxExtension]);
            $id = $object['@attributes']['id'];
            /**
             * @var \DOMElement|null $element
             */
            $element = $this->xpath->query("//*[@id='$id']")->item(0);
            if ($element) {
              /**
               * @var \DOMElement|null $extensions
               */
              $extensions = $this->xpath->query("//*[@id='$id']/$idxExtension")
                ->item(0);
              if (!$extensions) {
                $node = $this->doc->createElement($idxExtension);
                $extensions = $element->appendChild($node);
              }
              foreach ($template['properties'] as $property) {
                switch ($property['binding']['type']) {
                  case 'camunda:property':
                    if ($this->findProperty($object[$idxExtension], $property['binding']['name']) !== $property['value']) {
                      $element->setAttribute($property['binding']['name'], $property['value']);
                      $changed = TRUE;
                    }
                    break;

                  case 'camunda:field':
                    if (isset($fields[$property['binding']['name']])) {
                      // Field exists, remove it from the list.
                      unset($fields[$property['binding']['name']]);
                    }
                    else {
                      $fieldNode = $this->doc->createElement('camunda:field');
                      $fieldNode->setAttribute('name', $property['binding']['name']);
                      $valueNode = $this->doc->createElement('camunda:string');
                      $valueNode->textContent = $property['value'];
                      $fieldNode->appendChild($valueNode);
                      $extensions->appendChild($fieldNode);
                      $changed = TRUE;
                    }
                    break;
                }
              }
              // Remove remaining fields from the model.
              foreach ($fields as $name => $value) {
                /**
                 * @var \DOMElement $fieldElement
                 */
                if ($fieldElement = $this->xpath->query("//*[@id='$id']/$idxExtension/camunda:field[@name='$name']")
                  ->item(0)) {
                  $extensions->removeChild($fieldElement);
                  $changed = TRUE;
                }
              }
            }
          }
        }
      }
    }
    if ($changed) {
      $this->prepareForUpdate($this->doc->saveXML());
      $model->setModeldata($this->modeldata);
    }
    return $changed;
  }

  /**
   * {@inheritdoc}
   */
  public function enable(): ModellerInterface {
    $this->prepareForUpdate($this->eca->getModel()->getModeldata());
    /** @var \DOMElement|null $element */
    $element = $this->xpath->query("//*[@id='{$this->getId()}']")->item(0);
    if ($element) {
      $element->setAttribute('isExecutable', 'true');
    }
    try {
      $this->save($this->doc->saveXML());
    }
    catch (\LogicException | EntityStorageException $e) {
      $this->messenger->addError($e->getMessage());
    }
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function disable(): ModellerInterface {
    $this->prepareForUpdate($this->eca->getModel()->getModeldata());
    /** @var \DOMElement|null $element */
    $element = $this->xpath->query("//*[@id='{$this->getId()}']")->item(0);
    if ($element) {
      $element->setAttribute('isExecutable', 'false');
    }
    try {
      $this->save($this->doc->saveXML());
    }
    catch (\LogicException | EntityStorageException $e) {
      $this->messenger->addError($e->getMessage());
    }
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function clone(): ?Eca {
    $this->prepareForUpdate($this->eca->getModel()->getModeldata());
    $id = $this->generateId();
    /** @var \DOMElement|null $element */
    $element = $this->xpath->query("//*[@id='{$this->getId()}']")->item(0);
    if ($element) {
      $element->setAttribute('id', $id);
      $element->setAttribute('name', $this->getLabel() . ' (' . $this->t('clone') . ')');
    }
    try {
      return $this->createNewModel($id, $this->doc->saveXML(), NULL, TRUE);
    }
    catch (\LogicException | EntityStorageException $e) {
      $this->messenger->addError($e->getMessage());
    }
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getFilename(): string {
    return $this->filename;
  }

  /**
   * {@inheritdoc}
   */
  public function setModeldata(string $data): ModellerInterface {
    $this->prepareForUpdate($data);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getModeldata(): string {
    return $this->modeldata;
  }

  /**
   * {@inheritdoc}
   */
  public function getId(): string {
    return $this->xmlModel[$this->xmlNsPrefix() . 'process']['@attributes']['id'];
  }

  /**
   * {@inheritdoc}
   */
  public function getLabel(): string {
    return $this->xmlModel[$this->xmlNsPrefix() . 'process']['@attributes']['name'] ?? 'noname';
  }

  /**
   * {@inheritdoc}
   */
  public function getTags(): array {
    $process = $this->xmlNsPrefix() . 'process';
    $extensions = $this->xmlNsPrefix() . 'extensionElements';
    $tags = isset($this->xmlModel[$process][$extensions]) ?
      explode(',', $this->findProperty($this->xmlModel[$process][$extensions], 'Tags')) :
      [];
    array_walk($tags, static function (&$item) {
      $item = trim((string) $item);
    });
    return $tags;
  }

  /**
   * {@inheritdoc}
   */
  public function getChangelog(): array {
    $this->prepareForExport();
    $process = $this->xmlNsPrefix() . 'process';
    $extensions = $this->xmlNsPrefix() . 'extensionElements';
    $changelog = [];
    if (isset($this->xmlModel[$process][$extensions])) {
      $v = 1;
      while ($item = $this->findProperty($this->xmlModel[$process][$extensions], 'Changelog v' . $v)) {
        $changelog['v' . $v] = $item;
        $v++;
      }
    }
    return $changelog;
  }

  /**
   * {@inheritdoc}
   */
  public function getDocumentation(): string {
    return $this->xmlModel[$this->xmlNsPrefix() . 'process'][$this->xmlNsPrefix() . 'documentation'] ?? '';
  }

  /**
   * {@inheritdoc}
   */
  public function getStatus(): bool {
    return mb_strtolower($this->xmlModel[$this->xmlNsPrefix() . 'process']['@attributes']['isExecutable'] ?? 'true') === 'true';
  }

  /**
   * {@inheritdoc}
   */
  public function getVersion(): string {
    return $this->xmlModel[$this->xmlNsPrefix() . 'process']['@attributes']['versionTag'] ?? '';
  }

  /**
   * Returns all the startEvent (events) objects from the XML model.
   *
   * @return array
   *   The list of all start events in the model data.
   */
  private function getStartEvents(): array {
    $events = $this->xmlModel[$this->xmlNsPrefix() . 'process'][$this->xmlNsPrefix() . 'startEvent'] ?? [];
    if (isset($events['@attributes'])) {
      return [$events];
    }
    return $events;
  }

  /**
   * Returns all the task objects (actions) from the XML model.
   *
   * @return array
   *   The list of all tasks in the model data.
   */
  private function getTasks(): array {
    $actions = $this->xmlModel[$this->xmlNsPrefix() . 'process'][$this->xmlNsPrefix() . 'task'] ?? [];
    if (isset($actions['@attributes'])) {
      return [$actions];
    }
    return $actions;
  }

  /**
   * Returns all the sequenceFlow objects (condition) from the XML model.
   *
   * @return array
   *   The list of all sequence flows in the model data.
   */
  private function getSequenceFlows(): array {
    $conditions = $this->xmlModel[$this->xmlNsPrefix() . 'process'][$this->xmlNsPrefix() . 'sequenceFlow'] ?? [];
    if (isset($conditions['@attributes'])) {
      return [$conditions];
    }
    return $conditions;
  }

  /**
   * Returns all the gateway objects from the XML model.
   *
   * @return array
   *   The list of all gateways in the model data.
   */
  private function getGateways(): array {
    $types = [
      $this->conditionServices::GATEWAY_TYPE_EXCLUSIVE => 'exclusiveGateway',
      $this->conditionServices::GATEWAY_TYPE_PARALLEL => 'parallelGateway',
      $this->conditionServices::GATEWAY_TYPE_INCLUSIVE => 'inclusiveGateway',
      $this->conditionServices::GATEWAY_TYPE_COMPLEX => 'complexGateway',
      $this->conditionServices::GATEWAY_TYPE_EVENTBASED => 'eventBasedGateway',
    ];
    $gateways = [];
    foreach ($types as $key => $type) {
      $objects = $this->xmlModel[$this->xmlNsPrefix() . 'process'][$this->xmlNsPrefix() . $type] ?? [];
      if (isset($objects['@attributes'])) {
        $objects = [$objects];
      }
      foreach ($objects as $object) {
        $object['type'] = $key;
        $gateways[] = $object;
      }
    }
    return $gateways;
  }

  /**
   * {@inheritdoc}
   */
  public function readComponents(Eca $eca): ModellerInterface {
    $this->eca = $eca;
    $this->eca->resetComponents();
    $idxExtension = $this->xmlNsPrefix() . 'extensionElements';

    $this->hasError = FALSE;
    $flow = [];
    foreach ($this->getSequenceFlows() as $sequenceFlow) {
      if (isset($sequenceFlow[$idxExtension])) {
        $pluginId = $this->findProperty($sequenceFlow[$idxExtension], 'pluginid');
        $condition = $this->findAttribute($sequenceFlow, 'id');
        if (!empty($pluginId) && !empty($condition)) {
          if (!$eca->addCondition(
            $condition,
            $pluginId,
            $this->findFields($sequenceFlow[$idxExtension])
          )) {
            $this->hasError = TRUE;
          }
        }
        else {
          $condition = '';
        }
      }
      else {
        $condition = '';
      }
      $flow[$this->findAttribute($sequenceFlow, 'sourceRef')][] = [
        'id' => $this->findAttribute($sequenceFlow, 'targetRef'),
        'condition' => $condition,
      ];
    }

    foreach ($this->getGateways() as $gateway) {
      $gatewayId = $this->findAttribute($gateway, 'id');
      $eca->addGateway($gatewayId, $gateway['type'], $flow[$gatewayId] ?? []);
    }

    foreach ($this->getStartEvents() as $startEvent) {
      $extension = $startEvent[$idxExtension] ?? [];
      $pluginId = $this->findProperty($extension, 'pluginid');
      if (empty($pluginId)) {
        continue;
      }
      if (!$eca->addEvent(
        $this->findAttribute($startEvent, 'id'),
        $pluginId,
        $this->findAttribute($startEvent, 'name'),
        $this->findFields($extension),
        $flow[$this->findAttribute($startEvent, 'id')] ?? []
      )) {
        $this->hasError = TRUE;
      }
    }

    foreach ($this->getTasks() as $task) {
      $extension = $task[$idxExtension] ?? [];
      $pluginId = $this->findProperty($extension, 'pluginid');
      if (empty($pluginId)) {
        continue;
      }
      if (!$eca->addAction(
        $this->findAttribute($task, 'id'),
        $pluginId,
        $this->findAttribute($task, 'name'),
        $this->findFields($extension),
        $flow[$this->findAttribute($task, 'id')] ?? []
      )) {
        $this->hasError = TRUE;
      }
    }

    return $this;
  }

  /**
   * Prepares the plugin's configuration form and catches errors.
   *
   * @param \Drupal\eca\Plugin\ECA\Event\EventInterface|\Drupal\eca\Plugin\ECA\Condition\ConditionInterface|\Drupal\Core\Action\ActionInterface $plugin
   *   The plugin.
   *
   * @return array
   *   The configuration form.
   */
  protected function buildConfigurationForm(EventInterface|ConditionInterface|ActionInterface $plugin): array {
    $form_state = new FormState();
    try {
      if ($plugin instanceof ActionInterface) {
        $form = $this->actionServices->getConfigurationForm($plugin, $form_state) ?? [
          'error_message' => [
            '#type' => 'checkbox',
            '#title' => $this->t('Error in configuration form!!!'),
            '#description' => $this->t('Details can be found in the Drupal error log.'),
          ],
        ];
      }
      else {
        $form = $plugin->buildConfigurationForm([], $form_state);
      }
    }
    catch (\Throwable $ex) {
      // @todo Replace this with some markup when that's supported by bpmn_io.
      $form['error_message'] = [
        '#type' => 'checkbox',
        '#title' => $this->t('Error in configuration form!!!'),
        '#description' => $ex->getMessage(),
      ];
    }
    return $form;
  }

  /**
   * Returns all the templates for the modeller UI.
   *
   * This includes templates for events, conditions and actions.
   *
   * @return array
   *   The list of all templates.
   */
  protected function getTemplates(): array {
    $templates = [];
    foreach ($this->modellerServices->events() as $event) {
      $templates[] = $this->properties($event, 'event', 'bpmn:Event', $this->buildConfigurationForm($event));
    }
    foreach ($this->conditionServices->conditions() as $condition) {
      $templates[] = $this->properties($condition, 'condition', 'bpmn:SequenceFlow', $this->buildConfigurationForm($condition));
    }
    foreach ($this->actionServices->actions() as $action) {
      $templates[] = $this->properties($action, 'action', 'bpmn:Task', $this->buildConfigurationForm($action));
    }
    return $templates;
  }

  /**
   * {@inheritdoc}
   */
  public function exportTemplates(): ModellerInterface {
    // Nothing to do by default.
    return $this;
  }

  /**
   * Helper function to build a template for an event, condition or action.
   *
   * @param \Drupal\Component\Plugin\PluginInspectionInterface $plugin
   *   The event, condition or action plugin for which the template should
   *   be build.
   * @param string $plugin_type
   *   The string identifying the plugin type, which is one of event, condition
   *   or action.
   * @param string $applies_to
   *   The string to tell the modeller, to which object type the template will
   *   apply. Valid values are "bpmn:Event", "bpmn:sequenceFlow" or "bpmn:task".
   * @param array $form
   *   An array containing the configuration form of the plugin.
   *
   * @return array
   *   The completed template for BPMN modellers for the given plugin and its
   *   fields.
   */
  protected function properties(PluginInspectionInterface $plugin, string $plugin_type, string $applies_to, array $form): array {
    $properties = [
      [
        'label' => 'Plugin ID',
        'type' => 'Hidden',
        'value' => $plugin->getPluginId(),
        'binding' => [
          'type' => 'camunda:property',
          'name' => 'pluginid',
        ],
      ],
    ];
    $extraDescriptions = [];
    foreach ($this->prepareConfigFields($form, $extraDescriptions) as $field) {
      if (!isset($field['value'])) {
        $value = '';
      }
      elseif (is_scalar($field['value'])) {
        $value = (string) $field['value'];
      }
      elseif (is_object($field['value']) && method_exists($field['value'], '__toString')) {
        $value = $field['value']->__toString();
      }
      else {
        $this->logger->error('Found config field %field in %plugin with non-supported value.', [
          '%field' => $field['label'],
          '%plugin' => $plugin->getPluginId(),
        ]);
        $value = '';
      }
      $property = [
        'label' => $field['label'],
        'type' => $field['type'],
        'value' => $value,
        'editable' => $field['editable'] ?? TRUE,
        'binding' => [
          'type' => 'camunda:field',
          'name' => $field['name'],
        ],
      ];
      if (!empty($field['required'])) {
        $property['constraints']['notEmpty'] = TRUE;
      }
      if (isset($field['description'])) {
        $property['description'] = (string) $field['description'];
      }
      if (isset($field['extras'])) {
        /* @noinspection SlowArrayOperationsInLoopInspection */
        $property = array_merge_recursive($property, $field['extras']);
      }
      $properties[] = $property;
    }
    $extraDescriptions = array_unique($extraDescriptions);
    $pluginDefinition = $plugin->getPluginDefinition();
    $template = [
      'name' => (string) $pluginDefinition['label'],
      'id' => 'org.drupal.' . $plugin_type . '.' . $plugin->getPluginId(),
      'category' => [
        'id' => $pluginDefinition['provider'],
        'name' => EcaPluginBase::$modules[$pluginDefinition['provider']],
      ],
      'appliesTo' => [$applies_to],
      'properties' => $properties,
    ];
    if (isset($pluginDefinition['description']) || $extraDescriptions) {
      $template['description'] = (string) ($pluginDefinition['description'] ?? '') . ' ' . implode(' ', $extraDescriptions);
    }
    if ($doc_url = $this->pluginDocUrl($plugin, $plugin_type)) {
      $template['documentationRef'] = $doc_url;
    }
    return $template;
  }

  /**
   * Builds the URL to the offsite documentation for the given plugin.
   *
   * @param \Drupal\Component\Plugin\PluginInspectionInterface $plugin
   *   The plugin for which the documentation URL should be build.
   * @param string $plugin_type
   *   The string identifying the plugin type, which is one of event, condition
   *   or action.
   *
   * @return string|null
   *   The URL to the offsite documentation, or NULL if no URL was generated.
   */
  protected function pluginDocUrl(PluginInspectionInterface $plugin, string $plugin_type): ?string {
    if (!($domain = $this->documentationDomain)) {
      return NULL;
    }
    $provider = $plugin->getPluginDefinition()['provider'];
    $basePath = (mb_strpos($provider, 'eca_') === 0) ?
      str_replace('_', '/', $provider) :
      $provider;
    return sprintf('%s/plugins/%s/%ss/%s/', $domain, $basePath, $plugin_type, str_replace([':'], '_', $plugin->getPluginId()));
  }

  /**
   * Return a property of a given BPMN element.
   *
   * @param array $element
   *   The BPMN element from which the property should be returned.
   * @param string $property_name
   *   The name of the property in the BPMN element.
   *
   * @return string
   *   The property's value, default to an empty string.
   */
  protected function findProperty(array $element, string $property_name): string {
    if (isset($element['camunda:properties']['camunda:property'])) {
      $elements = isset($element['camunda:properties']['camunda:property']['@attributes']) ?
        [$element['camunda:properties']['camunda:property']] :
        $element['camunda:properties']['camunda:property'];
      foreach ($elements as $child) {
        if ($child['@attributes']['name'] === $property_name) {
          return $child['@attributes']['value'];
        }
      }
    }
    return '';
  }

  /**
   * Return an attribute of a given BPMN element.
   *
   * @param array $element
   *   The BPMN element from which the attribute should be returned.
   * @param string $attribute_name
   *   The name of the attribute in the BPMN element.
   *
   * @return string
   *   The attribute's value, default to an empty string.
   */
  protected function findAttribute(array $element, string $attribute_name): string {
    return $element['@attributes'][$attribute_name] ?? '';
  }

  /**
   * Return all the field values of a given BPMN element.
   *
   * @param array $element
   *   The BPMN element from which the field values should be returned.
   *
   * @return array
   *   An array containing all the field values, keyed by the field name.
   */
  protected function findFields(array $element): array {
    $fields = [];
    if (isset($element['camunda:field'])) {
      $elements = isset($element['camunda:field']['@attributes']) ? [$element['camunda:field']] : $element['camunda:field'];
      foreach ($elements as $child) {
        $fields[$child['@attributes']['name']] = isset($child['camunda:string']) && is_string($child['camunda:string']) ? $child['camunda:string'] : '';
      }
    }
    return $fields;
  }

  /**
   * Helper function preparing config fields for events, conditions and actions.
   *
   * @param array $form
   *   The array to which the fields should be added.
   * @param array $extraDescriptions
   *   An array receiving all markup "fields" which can be displayed separately
   *   in the UI.
   *
   * @return array
   *   The prepared config fields.
   */
  protected function prepareConfigFields(array $form, array &$extraDescriptions): array {
    // @todo Add support for nested form fields like e.g. in container/fieldset.
    $fields = [];
    foreach ($form as $key => $definition) {
      if (!is_array($definition)) {
        continue;
      }
      $label = $definition['#title'] ?? Modellers::convertKeyToLabel($key);
      $description = $definition['#description'] ?? NULL;
      $value = $definition['#default_value'] ?? '';
      $weight = $definition['#weight'] ?? 0;
      $type = 'String';
      $required = $definition['#required'] ?? FALSE;
      // @todo Map to more proper property types of bpmn-js.
      switch ($definition['#type'] ?? 'markup') {

        case 'hidden':
        case 'actions':
          // The modellers can't handle these types, so we ignore them for
          // the templates.
          continue 2;

        case 'item':
        case 'markup':
          if (isset($definition['#markup'])) {
            $extraDescriptions[] = (string) $definition['#markup'];
          }
          continue 2;

        case 'textarea':
          $type = 'Text';
          break;

        case 'checkbox':
          $fields[] = $this->checkbox($key, $label, $weight, $description, $value);
          continue 2;

        case 'checkboxes':
        case 'radios':
        case 'select':
          if (!is_array($value)) {
            $fields[] = $this->optionsField($key, $label, $weight, $description, $definition['#options'], (string) $value, $required);
            continue 2;
          }
          break;

      }
      if (is_bool($value)) {
        $fields[] = $this->checkbox($key, $label, $weight, $description, $value);
        continue;
      }
      if (is_array($value)) {
        $value = implode(',', $value);
      }
      $field = [
        'name' => $key,
        'label' => $label,
        'weight' => $weight,
        'type' => $type,
        'value' => $value,
        'required' => $required,
      ];
      if ($description !== NULL) {
        $field['description'] = $description;
      }
      $fields[] = $field;
    }

    // Sort fields by weight.
    usort($fields, static function ($f1, $f2) {
      $l1 = (int) $f1['weight'];
      $l2 = (int) $f2['weight'];
      if ($l1 < $l2) {
        return -1;
      }
      if ($l1 > $l2) {
        return 1;
      }
      return 0;
    });

    return $fields;
  }

  /**
   * Prepares a field with options as a drop-down.
   *
   * @param string $name
   *   The field name.
   * @param string $label
   *   The field label.
   * @param int $weight
   *   The field weight for sorting.
   * @param string|null $description
   *   The optional field description.
   * @param array $options
   *   Key/value list of available options.
   * @param string $value
   *   The default value for the field.
   * @param bool $required
   *   The setting, if this field is required to be filled by the user.
   *
   * @return array
   *   Prepared option field.
   */
  protected function optionsField(string $name, string $label, int $weight, ?string $description, array $options, string $value, bool $required = FALSE): array {
    $choices = [];
    foreach ($options as $optionValue => $optionName) {
      $choices[] = [
        'name' => (string) $optionName,
        'value' => (string) $optionValue,
      ];
      if ($required && $value === '') {
        $value = (string) $optionValue;
      }
    }
    $field = [
      'name' => $name,
      'label' => $label,
      'weight' => $weight,
      'type' => 'Dropdown',
      'value' => $value,
      'required' => $required,
      'extras' => [
        'choices' => $choices,
      ],
    ];
    if ($description !== NULL) {
      $field['description'] = $description;
    }
    return $field;
  }

  /**
   * Prepares a field as a checkbox.
   *
   * @param string $name
   *   The field name.
   * @param string $label
   *   The field label.
   * @param int $weight
   *   The field weight for sorting.
   * @param string|null $description
   *   The optional field description.
   * @param bool $value
   *   The default value for the field.
   *
   * @return array
   *   Prepared checkbox field.
   */
  protected function checkbox(string $name, string $label, int $weight, ?string $description, bool $value): array {
    $field = [
      'name' => $name,
      'label' => $label,
      'weight' => $weight,
      'type' => 'Dropdown',
      'value' => $value ? 'yes' : 'no',
      'extras' => [
        'choices' => [
          [
            'name' => 'no',
            'value' => 'no',
          ],
          [
            'name' => 'yes',
            'value' => 'yes',
          ],
        ],
      ],
    ];
    if ($description !== NULL) {
      $field['description'] = $description;
    }
    return $field;
  }

}

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

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