eca-1.0.x-dev/modules/development/src/Drush/Commands/DocsCommands.php

modules/development/src/Drush/Commands/DocsCommands.php
<?php

namespace Drupal\eca_development\Drush\Commands;

use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Action\ActionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Form\FormState;
use Drupal\eca\Entity\Eca;
use Drupal\eca\Plugin\ECA\Condition\ConditionInterface;
use Drupal\eca\Plugin\ECA\Event\EventInterface;
use Drupal\eca\Service\Actions;
use Drupal\eca\Service\Conditions;
use Drupal\eca\Service\ExportRecipe;
use Drupal\eca\Service\Modellers;
use Drush\Attributes as CLI;
use Drush\Commands\DrushCommands;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Twig\Environment as TwigEnvironment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
use Twig\Loader\ArrayLoader as TwigLoader;

/**
 * ECA documentation Drush command file.
 */
final class DocsCommands extends DrushCommands {

  /**
   * Table of contents.
   *
   * @var array
   */
  protected array $toc = [];

  /**
   * List of all processed modules.
   *
   * @var array
   */
  protected array $modules = [];

  /**
   * Entity type manager service.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected EntityTypeManagerInterface $entityTypeManager;

  /**
   * ECA Action service.
   *
   * @var \Drupal\eca\Service\Actions
   */
  protected Actions $actionServices;

  /**
   * ECA Condition service.
   *
   * @var \Drupal\eca\Service\Conditions
   */
  protected Conditions $conditionServices;

  /**
   * ECA Modeller service.
   *
   * @var \Drupal\eca\Service\Modellers
   */
  protected Modellers $modellerServices;

  /**
   * File system service.
   *
   * @var \Drupal\Core\File\FileSystemInterface
   */
  protected FileSystemInterface $fileSystem;

  /**
   * Module handler service.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected ModuleHandlerInterface $moduleHandler;

  /**
   * Module Extension List.
   *
   * @var \Drupal\Core\Extension\ModuleExtensionList
   */
  protected ModuleExtensionList $moduleExtensionList;

  /**
   * List of extensions.
   *
   * @var \Drupal\Core\Extension\Extension[]
   */
  protected ?array $moduleExtensions;

  /**
   * Twig array loader.
   *
   * @var \Twig\Loader\ArrayLoader
   */
  protected TwigLoader $twigLoader;

  /**
   * Twig environment service.
   *
   * @var \Twig\Environment
   */
  protected TwigEnvironment $twigEnvironment;

  /**
   * The export as recipe service.
   *
   * @var \Drupal\eca\Service\ExportRecipe
   */
  protected ExportRecipe $exportRecipe;

  /**
   * DocsCommands constructor.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Drupal\eca\Service\Actions $actionServices
   *   The ECA action services.
   * @param \Drupal\eca\Service\Conditions $conditionServices
   *   The ECA condition services.
   * @param \Drupal\eca\Service\Modellers $modellerServices
   *   The ECA modeller services.
   * @param \Drupal\Core\File\FileSystemInterface $fileSystem
   *   The file system service.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
   *   The module handler service.
   * @param \Drupal\Core\Extension\ModuleExtensionList $moduleExtensionList
   *   The module extension list service.
   * @param \Drupal\eca\Service\ExportRecipe $exportRecipe
   *   The export as recipe service.
   */
  public function __construct(EntityTypeManagerInterface $entityTypeManager, Actions $actionServices, Conditions $conditionServices, Modellers $modellerServices, FileSystemInterface $fileSystem, ModuleHandlerInterface $moduleHandler, ModuleExtensionList $moduleExtensionList, ExportRecipe $exportRecipe) {
    parent::__construct();
    $this->entityTypeManager = $entityTypeManager;
    $this->actionServices = $actionServices;
    $this->conditionServices = $conditionServices;
    $this->modellerServices = $modellerServices;
    $this->fileSystem = $fileSystem;
    $this->moduleHandler = $moduleHandler;
    $this->twigLoader = new TwigLoader();
    $this->twigEnvironment = new TwigEnvironment($this->twigLoader);
    $this->moduleExtensionList = $moduleExtensionList;
    $this->moduleExtensions = $moduleExtensionList->getList();
    $this->exportRecipe = $exportRecipe;
  }

  /**
   * Return an instance of these Drush commands.
   *
   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
   *   The container.
   *
   * @return \Drupal\eca_development\Drush\Commands\DocsCommands
   *   The instance of Drush commands.
   */
  public static function create(ContainerInterface $container): DocsCommands {
    return new DocsCommands(
      $container->get('entity_type.manager'),
      $container->get('eca.service.action'),
      $container->get('eca.service.condition'),
      $container->get('eca.service.modeller'),
      $container->get('file_system'),
      $container->get('module_handler'),
      $container->get('extension.list.module'),
      $container->get('eca.export.recipe'),
    );
  }

  /**
   * Export documentation for all plugins.
   */
  #[CLI\Command(name: 'eca:doc:plugins', aliases: [])]
  #[CLI\Usage(name: 'eca:doc:plugins', description: 'Export documentation for all plugins.')]
  public function plugins(): void {
    @$this->fileSystem->mkdir('../mkdocs/include/modules', NULL, TRUE);
    @$this->fileSystem->mkdir('../mkdocs/include/plugins', NULL, TRUE);
    $this->toc['0-ECA']['0-placeholder'] = 'plugins/eca/index.md';

    foreach ($this->modellerServices->events() as $event) {
      $this->pluginDoc($event);
    }
    foreach ($this->conditionServices->conditions() as $condition) {
      $this->pluginDoc($condition);
    }
    foreach ($this->actionServices->actions() as $action) {
      $this->pluginDoc($action);
    }
    $this->updateToc('plugins');
  }

  /**
   * Export documentation for all models.
   *
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   */
  #[CLI\Command(name: 'eca:doc:models', aliases: [])]
  #[CLI\Usage(name: 'eca:doc:models', description: 'Export documentation for all models.')]
  public function models(): void {
    /** @var \Drupal\eca\Entity\Eca $eca */
    foreach ($this->entityTypeManager
      ->getStorage('eca')
      ->loadMultiple() as $eca) {
      $this->modelDoc($eca);
      $this->exportRecipe->doExport($eca, $this->exportRecipe->defaultName($eca), ExportRecipe::DEFAULT_NAMESPACE, '../recipes/' . $eca->id());
    }
    $this->updateToc('library');
  }

  /**
   * Update the TOC file identified by $key.
   *
   * @param string $key
   *   The key for the TOC to update.
   */
  private function updateToc(string $key): void {
    $filename = '../mkdocs/toc/' . $key . '.yml';
    // @todo Merge with potentially existing TOC,
    $this->sortNestedArrayAssoc($this->toc);
    $content = Yaml::encode($this->toc);
    $content = '- ' . $key . '/index.md' . PHP_EOL . str_replace(
      ['0-ECA:', '  0-placeholder: ', '  1-', '  2-', '  3-'],
      ['ECA:', '  ', '  ', '  ', '  '],
      $content);
    $content = preg_replace_callback('/\n\s*/', static function (array $matches) {
      return $matches[0] . '- ';
    }, $content);
    file_put_contents($filename, substr($content, 0, -2));
  }

  /**
   * Sort array by key recursively.
   *
   * @param mixed $a
   *   The array to sort by key.
   */
  private function sortNestedArrayAssoc(mixed &$a): void {
    if (!is_array($a)) {
      return;
    }
    ksort($a);
    foreach ($a as $k => $v) {
      $this->sortNestedArrayAssoc($a[$k]);
    }
  }

  /**
   * Prepare documentation for given plugin.
   *
   * @param \Drupal\Component\Plugin\PluginInspectionInterface $plugin
   *   The ECA plugin for which documentation should be created.
   */
  private function pluginDoc(PluginInspectionInterface $plugin): void {
    if (!empty($plugin->getPluginDefinition()['nodocs'])) {
      return;
    }
    $values = $this->getPluginValues($plugin);
    if ($values === NULL) {
      return;
    }
    $id = str_replace(':', '_', $plugin->getPluginId());
    $values['id_fs'] = $id;
    $this->modules[$values['provider']] = $values;

    $provider = $values['provider'];
    $values['extension_info'] = [
      'standalone' => TRUE,
      'module' => $provider,
    ];
    if (isset($this->moduleExtensions[$provider])) {
      // @phpstan-ignore-next-line
      if ($this->moduleExtensions[$provider]->origin === 'core') {
        $values['extension_info']['standalone'] = FALSE;
        $values['extension_info']['module'] = 'core';
      }
      else {
        // @phpstan-ignore-next-line
        $subpath = $this->moduleExtensions[$provider]->subpath;
        // @phpstan-ignore-next-line
        foreach ($this->moduleExtensions[$provider]->requires as $require) {
          // @phpstan-ignore-next-line
          if (isset($this->moduleExtensions[$require->getName()]) && str_contains($subpath, $this->moduleExtensions[$require->getName()]->subpath . '/')) {
            $values['extension_info']['standalone'] = FALSE;
            $values['extension_info']['module'] = $require->getName();
            break;
          }
        }
      }
    }

    $path = $values['path'];
    $filename = $path . '/' . $id . '.md';
    @$this->fileSystem->mkdir('../mkdocs/docs/' . $path, NULL, TRUE);
    file_put_contents('../mkdocs/docs/' . $filename, $this->render(__DIR__ . '/../../../templates/docs/plugin.md.twig', $values));

    $path = '../mkdocs/include/plugins/' . $values['provider'] . '/' . $values['type'] . '/';
    @$this->fileSystem->mkdir($path, NULL, TRUE);
    if (!file_exists($path . $id . '.md')) {
      file_put_contents($path . $id . '.md', '');
    }
    $path .= $id . '/';
    @$this->fileSystem->mkdir($path, NULL, TRUE);
    foreach ($values['fields'] as $field) {
      if (!file_exists($path . $field['name'] . '.md')) {
        file_put_contents($path . $field['name'] . '.md', '');
      }
    }

    if (!isset($values['toc'][$values['provider_name']])) {
      // Initialize TOC for a new provider.
      $values['toc'][$values['provider_name']]['0-placeholder'] = $values['provider_path'] . '/index.md';

      file_put_contents('../mkdocs/docs/' . $values['provider_path'] . '/index.md', $this->render(__DIR__ . '/../../../templates/docs/provider.md.twig', $values));
      if (!file_exists('../mkdocs/include/modules/' . $provider . '.md')) {
        file_put_contents('../mkdocs/include/modules/' . $provider . '.md', '');
      }
    }
    $values['toc'][$values['provider_name']][$values['weight'] . '-' . ucfirst($values['type']) . 's'][(string) $values['label']] = $filename;
  }

  /**
   * Extracts all required values from the given plugin.
   *
   * @param \Drupal\Component\Plugin\PluginInspectionInterface $plugin
   *   The ECA plugin for which values should be extracted.
   *
   * @return array|null
   *   The extracted values.
   */
  private function getPluginValues(PluginInspectionInterface $plugin): ?array {
    $values = $plugin->getPluginDefinition();
    if ($values['provider'] === 'core') {
      $values['provider_name'] = 'Drupal core';
    }
    else {
      $values['provider_name'] = $this->moduleExtensionList->getName($values['provider']);
    }
    if (str_starts_with($values['provider'], 'eca_')) {
      $basePath = str_replace('eca_', 'eca/', $values['provider']);
      $values['toc'] = &$this->toc['0-ECA'];
    }
    else {
      $basePath = $values['provider'];
      $values['toc'] = &$this->toc;
    }
    if (!isset($values['eca_version_introduced'])) {
      $values['eca_version_introduced'] = 'unknown';
    }
    $form_state = new FormState();
    if ($plugin instanceof EventInterface) {
      $weight = 1;
      $type = 'event';
      $form = $plugin->buildConfigurationForm([], $form_state);
      $values['tokens'] = $plugin->getTokens();
    }
    elseif ($plugin instanceof ConditionInterface) {
      $weight = 2;
      $type = 'condition';
      $form = $plugin->buildConfigurationForm([], $form_state);
    }
    elseif ($plugin instanceof ActionInterface) {
      $weight = 3;
      $type = 'action';
      $form = $this->actionServices->getConfigurationForm($plugin, $form_state);
      if ($form === NULL) {
        return NULL;
      }
    }
    else {
      $weight = 4;
      $type = 'error';
      $form = [];
    }
    $values['path'] = sprintf('plugins/%s/%ss',
      $basePath,
      $type
    );
    $values['provider_path'] = sprintf('plugins/%s',
      $basePath,
    );
    $fields = [];
    $extraDescriptions = [];
    foreach ($form as $key => $def) {
      if (empty($def)) {
        continue;
      }
      switch ($def['#type'] ?? 'markup') {
        case 'hidden':
        case 'actions':
          continue 2;

        case 'item':
        case 'markup':
          if (isset($def['#markup']) && !str_starts_with($key, 'eca_token_')) {
            $extraDescriptions[] = (string) $def['#markup'];
          }
          continue 2;

        default:
          $fields[] = [
            'name' => $key,
            'label' => $def['#title'] ?? $key,
            'description' => $def['#description'] ?? '',
          ];
      }
    }
    $values['weight'] = $weight;
    $values['type'] = $type;
    $values['fields'] = $fields;
    $values['extraDescriptions'] = $extraDescriptions;
    return $values;
  }

  /**
   * Creates documentation for the given ECA model.
   *
   * @param \Drupal\eca\Entity\Eca $eca
   *   The ECA config entity for which documentation should be created.
   */
  private function modelDoc(Eca $eca): void {
    $model = $eca->getModel();
    $modeller = $eca->getModeller();
    if ($modeller === NULL) {
      return;
    }
    $tags = $model->getTags();
    if (empty($tags) || (count($tags) === 1 && ($tags[0] === 'untagged' || $tags[0] === ''))) {
      // Do not export models without at least one tag.
      return;
    }

    $values = [
      'rawid' => $eca->id(),
      'id' => str_replace([':', ' '], '_', mb_strtolower($eca->label())),
      'label' => $eca->label(),
      'version' => $eca->get('version'),
      'changelog' => $modeller->getChangelog(),
      'main_tag' => $tags[0],
      'tags' => $tags,
      'documentation' => $model->getDocumentation(),
      'events' => [],
      'conditions' => [],
      'actions' => [],
      'model_filename' => $modeller->getPluginId() . '-' . $eca->id(),
      'library_path' => 'library/' . $tags[0],
      'namespace' => ExportRecipe::DEFAULT_NAMESPACE,
    ];
    foreach ($eca->getUsedEvents() as $event) {
      $label = $eca->getEventInfo($event);
      $plugin = $event->getPlugin();
      if (!empty($plugin->getPluginDefinition()['nodocs'])) {
        continue;
      }
      $info = $this->getPluginValues($plugin);
      $id = str_replace(':', '_', $plugin->getPluginId());
      $values['events'][] = '[' . $label . '](/' . $info['path'] . '/' . $id . '.md)';
    }
    $values['events'] = array_unique($values['events']);
    foreach ($eca->getConditions() as $condition) {
      if ($plugin = $this->conditionServices->createInstance($condition['plugin'])) {
        if (!empty($plugin->getPluginDefinition()['nodocs'])) {
          continue;
        }
        $label = $condition['label'] ?? $plugin->getPluginDefinition()['label'];
        $info = $this->getPluginValues($plugin);
        $id = str_replace(':', '_', $plugin->getPluginId());
        $values['conditions'][] = '[' . $label . '](/' . $info['path'] . '/' . $id . '.md)';
      }
    }
    $values['conditions'] = array_unique($values['conditions']);
    foreach ($eca->getActions() as $action) {
      if ($plugin = $this->actionServices->createInstance($action['plugin'])) {
        if (!empty($plugin->getPluginDefinition()['nodocs'])) {
          continue;
        }
        $label = $action['label'] ?? $plugin->getPluginDefinition()['label'];
        $info = $this->getPluginValues($plugin);
        $id = str_replace(':', '_', $plugin->getPluginId());
        $values['actions'][] = '[' . $label . '](/' . $info['path'] . '/' . $id . '.md)';
      }
    }
    $values['actions'] = array_unique($values['actions']);

    @$this->fileSystem->mkdir('../mkdocs/docs/' . $values['library_path'] . '/' . $values['id'], NULL, TRUE);

    $archiveFileName = '../mkdocs/docs/' . $values['library_path'] . '/' . $values['id'] . '/' . $values['model_filename'] . '.tar.gz';
    $values['dependencies'] = $this->modellerServices->exportArchive($eca, $archiveFileName);

    file_put_contents('../mkdocs/docs/' . $values['library_path'] . '/' . $values['id'] . '.md', $this->render(__DIR__ . '/../../../templates/docs/library.md.twig', $values));
    file_put_contents('../mkdocs/docs/' . $values['library_path'] . '/' . $values['id'] . '/' . $values['model_filename'] . '.xml', $model->getModeldata());

    $this->toc[$values['main_tag']][$values['label']] = $values['library_path'] . '/' . $values['id'] . '.md';
  }

  /**
   * Renders a twig template in filename with given values.
   *
   * @param string $filename
   *   The filename of a twig template.
   * @param array $values
   *   The values for rendering.
   *
   * @return string
   *   The rendered result of the twig template.
   */
  private function render(string $filename, array $values): string {
    $this->twigLoader->setTemplate($filename, file_get_contents($filename));
    try {
      return $this->twigEnvironment->render($filename, $values);
    }
    catch (LoaderError | RuntimeError | SyntaxError) {
      // @todo Log these exceptions.
    }
    return '';
  }

}

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

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