eca-1.0.x-dev/src/Service/ExportRecipe.php

src/Service/ExportRecipe.php
<?php

namespace Drupal\eca\Service;

use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Config\ManagedStorage;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\File\FileExists;
use Drupal\Core\File\FileSystem;
use Drupal\Core\Messenger\Messenger;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\eca\Entity\Eca;

/**
 * Service provides export to recipe functionality for ECA models.
 */
class ExportRecipe {

  use StringTranslationTrait;

  public const DEFAULT_NAMESPACE = 'drupal-eca-recipe';
  public const DEFAULT_DESTINATION = 'temporary://recipe';

  /**
   * Constructs the recipe export service.
   *
   * @param \Drupal\Core\Config\ManagedStorage $configStorage
   *   The config storage.
   * @param \Drupal\Core\File\FileSystem $fileSystem
   *   The file system.
   * @param \Drupal\Core\Extension\ModuleExtensionList $moduleExtensionList
   *   The module extension list.
   * @param \Drupal\eca\Service\Modellers $modellerService
   *   The ECA modeller service.
   * @param \Drupal\Core\Messenger\Messenger $messenger
   *   The messenger.
   */
  public function __construct(
    protected readonly ManagedStorage $configStorage,
    protected readonly FileSystem $fileSystem,
    protected readonly ModuleExtensionList $moduleExtensionList,
    protected readonly Modellers $modellerService,
    protected readonly Messenger $messenger,
  ) {}

  /**
   * Exports the given ECA model to a recipe.
   *
   * @param \Drupal\eca\Entity\Eca $eca
   *   The ECA model.
   * @param string|null $name
   *   The name of the model.
   * @param string $namespace
   *   The namespace to use for composer.
   * @param string $destination
   *   The directory, where to store the recipe.
   */
  public function doExport(Eca $eca, ?string $name = NULL, string $namespace = self::DEFAULT_NAMESPACE, string $destination = self::DEFAULT_DESTINATION): void {
    $destination = rtrim($destination, '/');
    $configDestination = $destination . '/config';
    $composerJson = $destination . '/composer.json';
    $recipeYml = $destination . '/recipe.yml';
    $readmeMd = $destination . '/README.md';
    if (file_exists($configDestination) && !$this->fileSystem->deleteRecursive($configDestination)) {
      $this->messenger->addError($this->t('A config directory already exists in the given destination and can not be removed.'));
      return;
    }
    if (file_exists($composerJson) && !$this->fileSystem->unlink($composerJson)) {
      $this->messenger->addError($this->t('A composer.json already exists in the given destination and can not be removed.'));
      return;
    }
    if (file_exists($recipeYml) && !$this->fileSystem->unlink($recipeYml)) {
      $this->messenger->addError($this->t('A recipe.yml already exists in the given destination and can not be removed.'));
      return;
    }
    if (file_exists($readmeMd) && !$this->fileSystem->unlink($readmeMd)) {
      $this->messenger->addError($this->t('A README.md already exists in the given destination and can not be removed.'));
      return;
    }
    if (!$this->fileSystem->mkdir($configDestination, FileSystem::CHMOD_DIRECTORY, TRUE)) {
      $this->messenger->addError($this->t('The destination does not exist or is not writable.'));
      return;
    }
    if (!is_writable($configDestination)) {
      $this->messenger->addError($this->t('The destination is not writable.'));
      return;
    }
    $this->fileSystem->prepareDirectory($destination);
    $this->fileSystem->prepareDirectory($configDestination);

    if ($name === NULL) {
      $name = $this->defaultName($eca);
    }
    $description = $eca->getModel()->getDocumentation();
    $dependencies = [
      'config' => [
        'eca.eca.' . $eca->id(),
      ],
      'module' => [],
    ];
    $comesWithEcaModel = $this->configStorage->read('eca.model.' . $eca->id());
    if ($comesWithEcaModel) {
      $dependencies['config'][] = 'eca.model.' . $eca->id();
    }
    $this->modellerService->getNestedDependencies($dependencies, $eca->getDependencies());

    $actions = [];
    $imports = [];
    foreach ($dependencies['config'] as $configName) {
      $config = $this->configStorage->read($configName);
      if (!$config) {
        continue;
      }
      unset($config['uuid'], $config['_core']);
      if (str_starts_with($configName, 'user.role.')) {
        $actions[$configName] = [
          'ensure_exists' => [
            'label' => $config['label'],
          ],
          'grantPermissions' => $config['permissions'],
        ];
      }
      else {
        $canBeImported = FALSE;
        foreach ($config['dependencies']['module'] ?? [] as $module) {
          if ($this->isProvidedByModule($module, $configName)) {
            $imports[$module][] = $configName;
            $canBeImported = TRUE;
            break;
          }
        }
        if (!$canBeImported) {
          $this->fileSystem->saveData(Yaml::encode($config), $configDestination . '/' . $configName . '.yml', FileExists::Replace);
        }
      }
    }

    $this->fileSystem->saveData(json_encode($this->getComposer($eca->id(), $namespace, $name, $dependencies['module']), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) . PHP_EOL, $composerJson, FileExists::Replace);
    $this->fileSystem->saveData(Yaml::encode($this->getRecipe($name, $description, $dependencies['module'], $actions, $imports)), $recipeYml, FileExists::Replace);
    $this->fileSystem->saveData($this->getReadme($eca->id(), $name, $description, $namespace), $readmeMd, FileExists::Replace);
  }

  /**
   * Gets the default name for the recipe.
   *
   * @param \Drupal\eca\Entity\Eca $eca
   *   The ECA model.
   *
   * @return string
   *   The default name for the recipe.
   */
  public function defaultName(Eca $eca): string {
    return (string) $eca->label();
  }

  /**
   * Helper function to determine if a config name is provided by given module.
   *
   * @param string $module
   *   The module.
   * @param string $configName
   *   The config name.
   *
   * @return bool
   *   TRUE, if that module provides that config, FALSE otherwise.
   */
  private function isProvidedByModule(string $module, string $configName): bool {
    $pathname = $this->fileSystem->dirName($this->moduleExtensionList->getPathname($module));
    foreach (['install', 'optional'] as $item) {
      if (file_exists($pathname . '/config/' . $item . '/' . $configName . '.yml')) {
        return TRUE;
      }
    }
    return FALSE;
  }

  /**
   * Builds the content of the composer.json file.
   *
   * @param string $id
   *   The recipe ID.
   * @param string $namespace
   *   The namespace.
   * @param string $description
   *   The recipe description.
   * @param array $modules
   *   The list of required module names.
   *
   * @return string[]
   *   The content of the composer.json file as an array.
   */
  protected function getComposer(string $id, string $namespace, string $description, array $modules = []): array {
    $composer = [
      'name' => $namespace . '/' . $id,
      'type' => 'drupal-recipe',
      'description' => $description,
      'license' => 'GPL-2.0-or-later',
    ];
    if ($modules) {
      $composer['require'] = [
        'drupal/core' => '>=10.3',
      ];
      $list = $this->moduleExtensionList->getList();
      foreach ($modules as $module) {
        $path = $this->moduleExtensionList->getPath($module);
        if (!str_starts_with($path, 'core/modules')) {
          foreach ($list[$module]->requires ?? [] as $key => $dependency) {
            if (str_starts_with($path, $this->moduleExtensionList->getPath($key) . '/')) {
              $module = $key;
              break;
            }
          }
          $composer['require']['drupal/' . $module] = '*';
        }
      }
    }
    return $composer;
  }

  /**
   * Builds the content of the recipe file.
   *
   * @param string $name
   *   The recipe name.
   * @param string $description
   *   The recipe description.
   * @param array $modules
   *   The list of required modules.
   * @param array $actions
   *   The list of config actions.
   * @param array $imports
   *   The list of config imports keyed by module name.
   *
   * @return string[]
   *   The content of the recipe file as an array.
   */
  protected function getRecipe(string $name, string $description, array $modules = [], array $actions = [], array $imports = []): array {
    $recipe = [
      'name' => $name,
      'description' => $description,
      'type' => 'ECA',
    ];
    if ($modules) {
      $recipe['install'] = $modules;
    }
    if ($actions) {
      $recipe['config']['actions'] = $actions;
    }
    if ($imports) {
      $recipe['config']['import'] = $imports;
    }
    return $recipe;
  }

  /**
   * Builds the content of the readme file.
   *
   * @param string $id
   *   The ID of the recipe.
   * @param string $name
   *   The recipe name.
   * @param string $description
   *   The recipe description.
   * @param string $namespace
   *   The namespace.
   *
   * @return string
   *   The content of the readme file.
   */
  protected function getReadme(string $id, string $name, string $description, string $namespace): string {
    $description = str_replace(['](/', '.md)'], [
      '](https://ecaguide.org/',
      ')',
    ], $description);
    return <<<end_of_readme
## ECA Recipe: $name

ID: $id

$description

### Installation

```shell
composer require $namespace/$id
cd web && php core/scripts/drupal recipe ../vendor/drupal-eca-recipe/$id
```
end_of_readme;
  }

}

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

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