devel_wizard-2.x-dev/src/Plugin/DevelWizard/Spell/EntityTypeSpell.php

src/Plugin/DevelWizard/Spell/EntityTypeSpell.php
<?php

declare(strict_types=1);

namespace Drupal\devel_wizard\Plugin\DevelWizard\Spell;

use Drupal\Component\Plugin\ConfigurableInterface;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleInstallerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Template\TwigEnvironment;
use Drupal\devel_wizard\Attribute\DevelWizardSpell;
use Drupal\devel_wizard\ShellProcessFactory;
use Drupal\devel_wizard\Spell\SpellManagerInterface;
use Drupal\devel_wizard\Spell\SpellTraitPackageManager;
use Drupal\devel_wizard\Utils;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Path;
use Symfony\Component\String\UnicodeString;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\ConstraintViolationListInterface;

#[DevelWizardSpell(
  id: 'devel_wizard_entity_type',
  category: new TranslatableMarkup('Code'),
  label: new TranslatableMarkup('Entity type'),
  description: new TranslatableMarkup('Generates PHP and YML files for a new custom entity type.'),
  tags: [
    'code' => new TranslatableMarkup('Code'),
    'config_entity' => new TranslatableMarkup('Config entity'),
    'content_entity' => new TranslatableMarkup('Content entity'),
  ],
)]
class EntityTypeSpell extends SpellBase implements
  PluginFormInterface,
  ConfigurableInterface,
  ContainerFactoryPluginInterface {

  use SpellTraitPackageManager;

  protected SpellManagerInterface $spellManager;

  protected Filesystem $fs;

  protected TwigEnvironment $twig;

  public static function create(
    ContainerInterface $container,
    array $configuration,
    $plugin_id,
    $plugin_definition,
  ) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('messenger'),
      $container->get('logger.channel.devel_wizard_spell'),
      $container->get('string_translation'),
      $container->get('devel_wizard.utils'),
      $container->get('config.factory'),
      $container->get('plugin.manager.devel_wizard.spell'),
      $container->get('module_installer'),
      $container->get('extension.list.module'),
      $container->get('twig'),
      $container->get('devel_wizard.shell_process_factory'),
      new Filesystem(),
    );
  }

  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    MessengerInterface $messenger,
    LoggerInterface $logger,
    TranslationInterface $stringTranslation,
    Utils $utils,
    ConfigFactoryInterface $configFactory,
    SpellManagerInterface $spellManager,
    ModuleInstallerInterface $moduleInstaller,
    ModuleExtensionList $moduleList,
    TwigEnvironment $twig,
    ShellProcessFactory $shellProcessFactory,
    Filesystem $fs,
  ) {
    $this->spellManager = $spellManager;
    $this->moduleInstaller = $moduleInstaller;
    $this->moduleList = $moduleList;
    $this->twig = $twig;
    $this->shellProcessFactory = $shellProcessFactory;
    $this->fs = $fs;

    parent::__construct(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $messenger,
      $logger,
      $stringTranslation,
      $utils,
      $configFactory,
    );
  }

  /**
   * {@inheritdoc}
   *
   * @throws \Drupal\Component\Plugin\Exception\PluginException
   */
  public function defaultConfiguration() {
    /** @var \Drupal\devel_wizard\Plugin\DevelWizard\Spell\ModuleSpell $moduleSpell */
    $moduleSpell = $this
      ->spellManager
      ->createInstance('devel_wizard_module');

    return [
      'goal' => 'bundleable',
      'module' => $moduleSpell->defaultConfiguration(),
      'content' => [
        'id' => '',
      ],
      'config' => [
        'id' => '',
      ],
    ];
  }

  protected function populateCalculatedConfigurationValues(): static {
    parent::populateCalculatedConfigurationValues();
    $conf =& $this->configuration;

    if (empty($conf['module']['machine_name'])) {
      throw new \InvalidArgumentException('ID of the content entity type is required');
    }

    $conf += [
      'goal' => 'bundleable',
    ];

    if (!in_array($conf['goal'], ['bundleable', 'config', 'content'])) {
      throw new \InvalidArgumentException('ID of the content entity type is required');
    }

    $hasConfig = $conf['goal'] !== 'content';
    $hasContent = $conf['goal'] !== 'config';

    $conf['module'] += [
      'machineName' => $conf['module']['machine_name'],
      'machineNameDash' => str_replace('_', '-', $conf['module']['machine_name']),
      'namespace' => "Drupal\\{$conf['module']['machine_name']}",
      'info.yml' => [],
    ];

    $conf['module'] += $this->utils->stringVariants($conf['module']['machineName'], 'machineName');

    if ($hasConfig && $hasContent) {
      if (empty($conf['content']['id'])) {
        $conf['content']['id'] = !empty($conf['config']['id'])
          ? preg_replace('/_[^_]+$/', '', $conf['config']['id'])
          : $conf['module']['machine_name'];
      }

      if (empty($conf['config']['id'])) {
        $conf['config']['id'] = "{$conf['content']['id']}_type";
      }
    }
    elseif ($hasConfig && empty($conf['config']['id'])) {
      $conf['config']['id'] = $conf['module']['machineName'];
    }
    elseif ($hasContent && empty($conf['content']['id'])) {
      $conf['content']['id'] = $conf['module']['machineName'];
    }

    if ($hasConfig
      && $hasContent
      && $conf['config']['id'] === $conf['content']['id']
    ) {
      throw new \InvalidArgumentException('Config entity type ID and Content entity type ID has to be different');
    }

    if ($hasConfig) {
      $conf['module']['info.yml'] += [
        'configure' => "entity.{$conf['config']['id']}.collection",
      ];
    }

    if ($hasConfig) {
      $conf['config'] += $this->utils->stringVariants($conf['config']['id'], 'id');
      $conf['config'] += [
        'class' => $conf['config']['idUpperCamel'],
        'label' => (new UnicodeString($conf['config']['id']))
          ->replace('_', ' ')
          ->title(TRUE)
          ->toString(),
      ];
      $conf['config'] += [
        'label_plural' => preg_replace(
          '/ys$/',
          'ies',
          $conf['config']['label'] . 's',
        ),
        'label_singular' => $conf['config']['label'],
        'class_fqn' => "{$conf['module']['namespace']}\\Entity\\{$conf['config']['class']}",
        'interface' => "{$conf['config']['class']}Interface",
        'namespace' => "{$conf['module']['namespace']}\\{$conf['config']['class']}",
      ];

      $conf['config'] += [
        'interface_fqn' => "{$conf['module']['namespace']}\\{$conf['config']['interface']}",
        'label_collection' => $conf['config']['label_plural'],
      ];
    }

    if ($hasContent) {
      $conf['content'] += $this->utils->stringVariants($conf['content']['id'], 'id');
      $conf['content'] += [
        'class' => $conf['content']['idUpperCamel'],
        'label' => (new UnicodeString($conf['content']['id']))
          ->replace('_', ' ')
          ->title(TRUE)
          ->toString(),
      ];
      $conf['content'] += [
        'label_plural' => preg_replace(
          '/ys$/',
          'ies',
          $conf['content']['label'] . 's',
        ),
        'label_singular' => $conf['content']['label'],
        'class_fqn' => "{$conf['module']['namespace']}\\Entity\\{$conf['content']['class']}",
        'interface' => "{$conf['content']['class']}Interface",
        'namespace' => "{$conf['module']['namespace']}\\{$conf['content']['class']}",
      ];
      $conf['content'] += [
        'interface_fqn' => "{$conf['module']['namespace']}\\{$conf['content']['interface']}",
        'label_collection' => $conf['content']['label_plural'],
      ];
    }

    return $this;
  }

  public function validate(array $configuration): ConstraintViolationListInterface {
    $list = new ConstraintViolationList();

    // @todo Goal cannot be empty.
    // @todo Config entity ID and Content entity ID cannot be empty.
    // @todo Config entity ID and Content entity ID must be unique.
    // @todo Config entity ID and Content entity ID cannot be the same.
    return $list;
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $form['#attributes']['class'][] = 'devel-wizard-spell-subform-devel-wizard-entity-type';
    $form['#attached']['library'][] = 'devel_wizard/spell.entity_type';
    $parents = $form['#parents'];

    $form['description'] = [
      '#type' => 'html_tag',
      '#tag' => 'p',
      '#value' => $this->getPluginDefinition()->getDescription(),
    ];

    $conf = $this->getConfiguration();

    $goalParents = array_merge($parents, ['goal']);
    $goalName = $this->utils->inputName($parents, 'goal');
    $form['goal'] = [
      '#type' => 'fieldset',
      '#tree' => TRUE,
      '#title' => $this->t('Goal'),
      '#open' => TRUE,

      'bundleable' => [
        '#type' => 'radio',
        '#title' => $this->t('Bundleable content entity'),
        '#description' => $this->t(<<< 'TEXT'
          Bundleable, fieldable and revisionable content entity type.
          Also includes a config entity type, which represents the bundles.
          Similar to "Taxonomy Vocabulary (config)" and "Taxonomy Term (content)".
          TEXT
        ),
        '#return_value' => 'bundleable',
        '#default_value' => $conf['goal'] === 'bundleable' ? 'bundleable' : '',
        '#parents' => $goalParents,
        '#name' => $goalName,
      ],
      'config' => [
        '#type' => 'radio',
        '#title' => $this->t('Only config entity'),
        '#return_value' => 'config',
        '#default_value' => $conf['goal'] === 'config' ? 'config' : '',
        '#parents' => $goalParents,
        '#name' => $goalName,
      ],
      'content' => [
        '#type' => 'radio',
        '#title' => $this->t('Only content entity'),
        '#description' => $this->t('Fieldable and revisionable content entity type.'),
        '#return_value' => 'content',
        '#default_value' => $conf['goal'] === 'content' ? 'content' : '',
        '#parents' => $goalParents,
        '#name' => $goalName,
      ],
    ];

    /** @var \Drupal\devel_wizard\Plugin\DevelWizard\Spell\ModuleSpell $moduleSpell */
    /* @noinspection PhpUnhandledExceptionInspection */
    $moduleSpell = $this->spellManager->createInstance('devel_wizard_module');
    $moduleSpell->setConfiguration($conf['module']);
    $form['module'] = $moduleSpell->buildConfigurationForm(
      [
        '#parents' => array_merge($form['#parents'], ['module']),
        '#tree' => TRUE,
        '#type' => 'details',
        '#open' => TRUE,
        '#title' => $this->t('Module'),
        '#description' => $this->t('Details of the module to put the new code files into. If not exists then it will be created.'),
      ],
      $form_state,
    );

    $form['config'] = [
      '#tree' => TRUE,
      '#type' => 'details',
      '#open' => TRUE,
      '#title' => $this->t('Config entity'),
      '#states' => [
        'invisible' => [
          sprintf(':input[name="%s"]', $goalName) => ['value' => 'content'],
        ],
      ],
      'id' => [
        '#type' => 'machine_name',
        '#required' => FALSE,
        '#title' => $this->t('Config entity type machine-name'),
        '#description' => $this->t('Instances of this entity type will represent the bundles. Similar to Vocabularies'),
        '#default_value' => $conf['config']['id'],
        '#maxlength' => $this->identifierMaxLength,
        '#size' => $this->identifierInputSize,
        '#machine_name' => [
          'exists' => [Utils::class, 'alwaysFalse'],
          'standalone' => TRUE,
        ],
        '#autocomplete_route_name' => 'devel_wizard.autocomplete.entity_type_id',
      ],
    ];

    $form['content'] = [
      '#tree' => TRUE,
      '#type' => 'details',
      '#title' => $this->t('Content entity'),
      '#open' => TRUE,
      '#states' => [
        'invisible' => [
          sprintf(':input[name="%s"]', $goalName) => ['value' => 'config'],
        ],
      ],
      'id' => [
        '#type' => 'machine_name',
        '#required' => FALSE,
        '#title' => $this->t('Content entity type machine-name'),
        '#description' => $this->t('Similar to Taxonomy terms.'),
        '#default_value' => $conf['content']['id'],
        '#maxlength' => $this->identifierMaxLength,
        '#size' => $this->identifierInputSize,
        '#machine_name' => [
          'exists' => [Utils::class, 'alwaysFalse'],
          'standalone' => TRUE,
        ],
        '#autocomplete_route_name' => 'devel_wizard.autocomplete.entity_type_id',
      ],
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
    // @todo Implement validateConfigurationForm() method.
  }

  /**
   * {@inheritdoc}
   *
   * @throws \Drupal\Component\Plugin\Exception\PluginException
   */
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
    $values = $form_state->getValue($form['#parents'], []);

    /** @var \Drupal\devel_wizard\Plugin\DevelWizard\Spell\ModuleSpell $moduleSpell */
    $moduleSpell = $this
      ->spellManager
      ->createInstance('devel_wizard_module');
    $moduleSpell->submitConfigurationForm($form['module'], $form_state);
    $values['module'] = $moduleSpell->getConfiguration();

    $this->setConfiguration($values);
  }

  /**
   * @throws \Twig\Error\Error
   * @throws \Drupal\Component\Plugin\Exception\PluginException
   */
  protected function doIt(): static {
    $conf = $this->getConfiguration();

    if (!isset($this->batchContext['sandbox']['current_step'])) {
      $isModuleExists = $this->moduleList->exists($conf['module']['machine_name']);

      $this->batchContext['sandbox']['current_step'] = $isModuleExists ?
        'code_1'
        : 'module_create';

      $this->batchContext['sandbox']['sub_spells'] = [
        'module_create' => [
          'finished' => $isModuleExists ? 1.0 : 0,
        ],
        'content_settings' => [
          'finished' => $conf['goal'] === 'content' ? 0 : 1.0,
        ],
      ];

      if ($isModuleExists) {
        $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'] = $this
          ->moduleList
          ->getPath($conf['module']['machine_name']);
      }
    }

    switch ($this->batchContext['sandbox']['current_step']) {
      case 'module_create':
        $moduleSpell = $this->spellManager->createInstance(
          'devel_wizard_module',
          $conf['module'],
        );
        $moduleSpell->prepare();
        $moduleSpell->abracadabra($this->batchContext['sandbox']['sub_spells']['module_create']);
        $this->batchContext['sandbox']['module_create_spell_configuration'] = $moduleSpell->getConfiguration();

        if ($this->batchContext['sandbox']['sub_spells']['module_create']['finished'] < 1.0) {
          $this->batchContext['message'] = $this->t('Create module');
          $this->batchContext['sandbox']['finished'] = 0.2;

          break;
        }

        $this->batchContext['message'] = $this->t('Code generation in progress');
        $this->batchContext['sandbox']['current_step'] = 'code_1';
        $this->batchContext['sandbox']['finished'] = 0.4;
        drupal_flush_all_caches();
        break;

      case 'code_1':
        if ($this->configuration['goal'] === 'content') {
          $configObjectSpell = $this->spellManager->createInstance(
            'devel_wizard_config_object',
            [
              'module' => $this->configuration['module']['machine_name'],
              'object' => "{$this->configuration['content']['id']}.settings",
              'configSchema' => [
                'definition' => [
                  'type' => 'config_object',
                  'label' => "{$this->configuration['content']['label']} settings",
                  'mapping' => [
                    'help' => [
                      'type' => 'text',
                      'label' => 'Help',
                    ],
                  ],
                ],
              ],
              'configInstall' => [
                'definition' => [
                  'langcode' => 'en',
                  'help' => 'Useless help text.',
                ],
              ],
              'form' => [
                'id' => "{$this->configuration['module']['machine_name']}_{$this->configuration['content']['id']}_settings_form",
                'namespace' => $this->configuration['content']['namespace'],
                'class' => 'SettingsForm',
              ],
              'routing' => [
                'id' => sprintf(
                  'entity.%s.settings_form',
                  $this->configuration['content']['id'],
                ),
              ],
              'linksMenu' => [
                'definition' => [
                  'title' => $this->configuration['content']['label'],
                  'description' => "{$this->configuration['content']['label']} settings form",
                ],
              ],
              'parentPath' => '/admin/structure',
            ],
          );

          $configObjectSpell->prepare();
          $configObjectSpell->abracadabra($this->batchContext['sandbox']['sub_spells']['content_settings']);
          $this->batchContext['sandbox']['content_settings_spell_configuration'] = $configObjectSpell->getConfiguration();

          if ($this->batchContext['sandbox']['sub_spells']['content_settings']['finished'] < 1.0) {
            $this->batchContext['message'] = $this->t('Code generation in progress');
            $this->batchContext['sandbox']['finished'] = 0.4;

            break;
          }

          $this->doItContentConfigTranslation();
          $linksTask = [
            "entity.{$this->configuration['content']['id']}.settings_form" => [
              'route_name' => "entity.{$this->configuration['content']['id']}.settings_form",
              'title' => 'Settings',
              'base_route' => "entity.{$this->configuration['content']['id']}.settings_form",
            ],
          ];
          $dstDir = $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'];
          $filePath = Path::join(
            $dstDir,
            "{$this->configuration['module']['machine_name']}.links.task.yml",
          );

          $this->utils->ymlFileReplace($filePath, $linksTask);
        }

        $this->batchContext['message'] = $this->t('Code generation in progress');
        $this->batchContext['sandbox']['current_step'] = 'code_2';
        $this->batchContext['sandbox']['finished'] = 0.6;
        drupal_flush_all_caches();
        break;

      case 'code_2':
        switch ($conf['goal']) {
          case 'bundleable':
            $this->configEntityType();
            $this->contentEntityType();
            break;

          case 'config':
            $this->configEntityType();
            break;

          case 'content':
            $this->contentEntityType();
            break;
        }

        $this->batchContext['message'] = $this->t('Install required modules');
        $this->batchContext['sandbox']['current_step'] = 'module_install';
        $this->batchContext['sandbox']['finished'] = 0.8;
        drupal_flush_all_caches();
        break;

      case 'module_install':
        // @todo Re-enable.
        //$this->doItModuleInstall();

        $this->batchContext['message'] = $this->t('Finished');
        $this->batchContext['sandbox']['current_step'] = '_finished';
        $this->batchContext['sandbox']['finished'] = 1.0;
        drupal_flush_all_caches();
        break;

      case '_finished':
        $this->batchContext['sandbox']['finished'] = 1.0;
        break;
    }

    return $this;
  }

  protected function doItContentConfigTranslation(): static {
    $dstDir = $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'];

    $filePath = Path::join(
      $dstDir,
      "{$this->configuration['module']['machine_name']}.config_translation.yml",
    );
    $fileData = [
      "entity.{$this->configuration['content']['id']}.settings" => [
        'title' => "{$this->configuration['content']['id']} settings",
        'base_route_name' => "entity.{$this->configuration['content']['id']}.settings_form",
        'names' => [
          "{$this->configuration['content']['id']}.settings",
        ],
      ],
    ];
    $this->utils->ymlFileReplace($filePath, $fileData);

    return $this;
  }

  protected function doItModuleInstall(): static {
    foreach ($this->getModuleNamesToInstall() as $moduleName) {
      $this->ensureModuleInstalled($moduleName);
    }

    return $this;
  }

  protected function getModuleNamesToInstall(): array {
    $conf = $this->getConfiguration();

    $modules = [];
    if (in_array($conf['module']['type'], ['custom', 'sub_module'])) {
      $modules[] = $conf['module']['machine_name'];
    }

    // @todo Standalone module steps:
    //   - Add to composer.json#repositories
    //   - composer require
    //   This workflow is not supported yet.
    return $modules;
  }

  /**
   * @throws \Twig\Error\Error
   */
  protected function configEntityType(): static {
    $this->configEntityTypeClass();
    $this->configEntityTypeClassInterface();

    $this->configEntityTypeHandler('add_form');
    $this->configEntityTypeHandler('edit_form');
    $this->configEntityTypeHandler('delete_form');
    $this->configEntityTypeHandler('access_control_handler');
    $this->configEntityTypeHandler('list_builder');
    $this->configEntityTypeHandler('route_provider');
    $this->configEntityTypeHandler('storage');
    $this->configEntityTypeHandler('comparer');

    $this->configEntityTypeRouting();
    $this->configEntityTypeServices();
    $this->configEntityTypePermissions();
    $this->configEntityTypeLinksAction();
    $this->configEntityTypeLinksMenu();
    $this->configEntityTypeLinksTask();
    $this->configEntityTypeSchema();
    $this->configEntityTypeTests();

    return $this;
  }

  /**
   * @throws \Twig\Error\Error
   */
  protected function configEntityTypeClassInterface(): static {
    $filePath = Path::join(
      $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'],
      'src',
      "{$this->configuration['config']['class']}Interface.php",
    );

    $this->fs->mkdir(Path::getDirectory($filePath));
    $this->fs->dumpFile(
      $filePath,
      $this->twig->render(
        '@devel_wizard/spell/entity_type/config/interface.php.twig',
        $this->configuration,
      ),
    );
    $this->messageFilesystemEntryCreate($filePath);

    return $this;
  }

  /**
   * @throws \Twig\Error\Error
   */
  protected function configEntityTypeClass(): static {
    $filePath = Path::join(
      $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'],
      'src',
      'Entity',
      "{$this->configuration['config']['class']}.php",
    );

    $this->fs->mkdir(Path::getDirectory($filePath));
    $this->fs->dumpFile(
      $filePath,
      $this->twig->render(
        '@devel_wizard/spell/entity_type/config/class.php.twig',
        $this->configuration,
      ),
    );
    $this->messageFilesystemEntryCreate($filePath);

    return $this;
  }

  /**
   * @throws \Twig\Error\Error
   *
   * @todo DRY - This method is very similar to ::contentEntityTypeHandler.
   */
  protected function configEntityTypeHandler(string $handler): static {
    $type = 'config';
    $handlerUpperCamel = (new UnicodeString("a_$handler"))
      ->camel()
      ->trimPrefix('a')
      ->toString();

    $filePath = Path::join(
      $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'],
      'src',
      $this->utils->relativeNamespaceDir($this->configuration[$type]['namespace']),
      "$handlerUpperCamel.php",
    );

    $this->fs->mkdir(Path::getDirectory($filePath));
    $this->fs->dumpFile(
      $filePath,
      $this->twig->render(
        "@devel_wizard/spell/entity_type/$type/$handler.php.twig",
        $this->configuration,
      ),
    );
    $this->messageFilesystemEntryCreate($filePath);

    return $this;
  }

  protected function configEntityTypeRouting(): static {
    $filePath = Path::join(
      $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'],
      "{$this->configuration['module']['machine_name']}.routing.yml",
    );
    $this->utils->ymlFileReplace($filePath, $this->getConfigEntityTypeRoutes());

    return $this;
  }

  protected function configEntityTypeServices(): static {
    $filePath = Path::join(
      $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'],
      "{$this->configuration['module']['machine_name']}.services.yml",
    );
    $this->utils->ymlFileReplace(
      $filePath,
      $this->getConfigEntityTypeServices(),
      ['services'],
    );

    return $this;
  }

  /**
   * @throws \Twig\Error\Error
   */
  protected function configEntityTypePermissions(): static {
    $this->entityTypePermissions('config');

    return $this;
  }

  /**
   * @throws \Twig\Error\Error
   */
  protected function contentEntityTypePermissions(): static {
    $this->entityTypePermissions('content');

    return $this;
  }

  /**
   * @throws \Twig\Error\Error
   */
  protected function entityTypePermissions(string $type): static {
    $dstDir = $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'];
    $id = $this->configuration[$type]['id'];

    // Add entries to *.permissions.yml.
    $filePath = Path::join(
      $dstDir,
      "{$this->configuration['module']['machine_name']}.permissions.yml",
    );
    $fileContent = $this->fs->exists($filePath) ? file_get_contents($filePath) : '{}';
    $permissions = Yaml::decode($fileContent);
    $callable = "entity.$id.permission_provider:getPermissions";
    settype($permissions['permission_callbacks'], 'array');
    if (!in_array($callable, $permissions['permission_callbacks'])) {
      $permissions['permission_callbacks'][] = $callable;
      $this->fs->mkdir(Path::getDirectory($filePath));
      $this->fs->dumpFile($filePath, Yaml::encode($permissions));
    }

    // Add entries to *.services.yml.
    $filePath = Path::join(
      $dstDir,
      "{$this->configuration['module']['machine_name']}.services.yml",
    );

    $this->utils->ymlFileReplace(
      $filePath,
      [
        "entity.$id.permission_provider" => [
          'class' => "{$this->configuration['module']['namespace']}\\{$this->configuration[$type]['class']}\\PermissionProvider",
          'arguments' => [
            '@entity_type.manager',
          ],
          'calls' => [
            ['setEntityTypeId', [$id]],
          ],
        ],
      ],
      ['services'],
    );

    $handlers = [
      'permission_provider_interface',
      'permission_provider',
    ];
    foreach ($handlers as $handler) {
      switch ($type) {
        case 'config':
          $this->configEntityTypeHandler($handler);
          break;

        case 'content':
          $this->contentEntityTypeHandler($handler);
          break;

      }
    }

    return $this;
  }

  protected function configEntityTypeLinksAction(): static {
    $dstDir = $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'];

    $filePath = Path::join(
      $dstDir,
      "{$this->configuration['module']['machine_name']}.links.action.yml",
    );
    $this->utils->ymlFileReplace($filePath, $this->getConfigEntityTypeLinksAction());

    return $this;
  }

  protected function configEntityTypeLinksMenu(): static {
    $dstDir = $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'];
    $filePath = Path::join(
      $dstDir,
      "{$this->configuration['module']['machine_name']}.links.menu.yml",
    );
    $this->utils->ymlFileReplace($filePath, $this->getConfigEntityTypeLinksMenu());

    return $this;
  }

  protected function configEntityTypeLinksTask(): static {
    $dstDir = $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'];
    $filePath = Path::join(
      $dstDir,
      "{$this->configuration['module']['machine_name']}.links.task.yml",
    );
    $this->utils->ymlFileReplace($filePath, $this->getConfigEntityTypeLinksTask());

    return $this;
  }

  protected function configEntityTypeSchema(): static {
    $dstDir = $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'];
    $filePath = Path::join(
      $dstDir,
      'config',
      'schema',
      "{$this->configuration['module']['machine_name']}.schema.yml",
    );
    $this->utils->ymlFileReplace($filePath, $this->getConfigEntityTypeSchema());

    return $this;
  }

  /**
   * @throws \Twig\Error\Error
   */
  protected function configEntityTypeTests(): static {
    $dstDir = $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'];
    $tmpPrefix = '@devel_wizard/spell/entity_type/config';
    $filePaths = [
      'tests/src/FunctionalJavascript/TestBase.php' => "$tmpPrefix/test.functional-javascript.base.php.twig",
      'tests/src/FunctionalJavascript/ConfigEntityCrudTest.php' => "$tmpPrefix/test.functional-javascript.crud.php.twig",
    ];

    foreach ($filePaths as $dst => $tpl) {
      $filePath = Path::join($dstDir, $dst);
      $this->fs->mkdir(Path::getDirectory($filePath));
      $this->fs->dumpFile(
        $filePath,
        $this->twig->render($tpl, $this->configuration),
      );
      $this->messageFilesystemEntryCreate($filePath);
    }

    return $this;
  }

  protected function getConfigEntityTypeRoutes(): array {
    return [];
  }

  protected function getConfigEntityTypeServices(): array {
    return [];
  }

  protected function getConfigEntityTypeLinksAction(): array {
    $id = $this->configuration['config']['id'];

    return [
      "entity.$id.add_form" => [
        'route_name' => "entity.$id.add_form",
        'title' => "Add {$this->configuration['config']['label']}",
        'appears_on' => [
          "entity.$id.collection",
        ],
      ],
    ];
  }

  protected function getConfigEntityTypeLinksMenu(): array {
    $id = $this->configuration['config']['id'];

    return [
      "entity.$id.collection" => [
        'title' => $this->configuration['config']['label_plural'],
        'parent' => 'system.admin_structure',
        'description' => $this->configuration['goal'] === 'config'
          ? sprintf(
            'List of %s',
            $this->configuration['config']['label_plural'],
          )
          : sprintf(
            'Create and manage fields, forms, and display settings for your %s.',
            $this->configuration['content']['label_plural'],
          ),
        'route_name' => "entity.$id.collection",
      ],
    ];
  }

  protected function getConfigEntityTypeLinksTask(): array {
    $id = $this->configuration['config']['id'];

    return [
      "entity.$id.edit_form" => [
        'title' => 'Edit',
        'route_name' => "entity.$id.edit_form",
        'base_route' => "entity.$id.edit_form",
      ],
    ];
  }

  protected function getConfigEntityTypeSchema(): array {
    $module = $this->configuration['module']['machine_name'];
    $id = $this->configuration['config']['id'];

    return [
      "$module.$id.*" => [
        'type' => 'config_entity',
        'label' => $this->configuration['config']['label'],
        'mapping' => [
          'id' => [
            'type' => 'string',
            'label' => 'Machine-readable name',
          ],
          'label' => [
            'type' => 'label',
            'label' => 'Label',
          ],
          'description' => [
            'type' => 'text',
            'label' => 'Description',
          ],
          'help' => [
            'type' => 'text',
            'label' => 'Explanation or submission guidelines',
          ],
          'weight' => [
            'type' => 'integer',
            'label' => 'Weight',
          ],
        ],
      ],
    ];
  }

  /**
   * @throws \Twig\Error\Error
   */
  protected function contentEntityType(): static {
    $this->contentEntityTypeClass();
    $this->contentEntityTypeInterface();

    $this->contentEntityTypeHandler('access_control_handler');
    $this->contentEntityTypeHandler('add_form');
    $this->contentEntityTypeHandler('edit_form');
    $this->contentEntityTypeHandler('delete_form');
    $this->contentEntityTypeHandler('list_builder');
    $this->contentEntityTypeHandler('storage');
    $this->contentEntityTypeHandler('storage_interface');
    $this->contentEntityTypeHandler('storage_schema');
    $this->contentEntityTypeHandler('translation_handler');
    $this->contentEntityTypeHandler('view_builder');
    $this->contentEntityTypeHandler('view_controller');
    $this->contentEntityTypeHandler('controller');
    $this->contentEntityTypeHandler('route_provider');
    $this->contentEntityTypeHandler('revision_delete_form');
    $this->contentEntityTypeHandler('revision_revert_form');
    $this->contentEntityTypeHandler('revision_revert_translation_form');
    $this->contentEntityTypeHandler('views_data');

    $this->contentEntityTypePermissions();
    $this->contentEntityTypeLinksAction();
    $this->contentEntityTypeLinksContextual();
    $this->contentEntityTypeLinksMenu();
    $this->contentEntityTypeLinksTask();
    $this->contentEntityTypeRouting();

    if ($this->configuration['goal'] === 'content') {

    }

    $this->contentEntityTypeModule();
    $this->contentEntityTypeTemplate();

    return $this;
  }

  /**
   * @throws \Twig\Error\Error
   */
  protected function contentEntityTypeClass(): static {
    $dstDir = $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'];
    $filePath = Path::join(
      $dstDir,
      'src',
      'Entity',
      "{$this->configuration['content']['class']}.php",
    );
    $this->fs->mkdir(Path::getDirectory($filePath));
    $this->fs->dumpFile(
      $filePath,
      $this->twig->render(
        '@devel_wizard/spell/entity_type/content/class.php.twig',
        $this->configuration,
      ),
    );
    $this->messageFilesystemEntryCreate($filePath);

    return $this;
  }

  /**
   * @throws \Twig\Error\Error
   */
  protected function contentEntityTypeInterface(): static {
    $dstDir = $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'];
    $filePath = Path::join(
      $dstDir,
      'src',
      "{$this->configuration['content']['class']}Interface.php",
    );

    $this->fs->mkdir(Path::getDirectory($filePath));
    $this->fs->dumpFile(
      $filePath,
      $this->twig->render(
        '@devel_wizard/spell/entity_type/content/interface.php.twig',
        $this->configuration,
      ),
    );
    $this->messageFilesystemEntryCreate($filePath);

    return $this;
  }

  /**
   * @throws \Twig\Error\Error
   */
  protected function contentEntityTypeHandler(string $handler): static {
    $dstDir = $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'];
    $handlerUpperCamel = (new UnicodeString("a_$handler"))
      ->camel()
      ->trimPrefix('a')
      ->toString();

    $class = $handlerUpperCamel;
    $filePath = Path::join(
      $dstDir,
      'src',
      $this->utils->relativeNamespaceDir($this->configuration['content']['namespace']),
      "$class.php",
    );

    $this->fs->mkdir(Path::getDirectory($filePath));
    $this->fs->dumpFile(
      $filePath,
      $this->twig->render(
        "@devel_wizard/spell/entity_type/content/$handler.php.twig",
        $this->configuration,
      ),
    );
    $this->messageFilesystemEntryCreate($filePath);

    return $this;
  }

  protected function contentEntityTypeLinksAction(): static {
    $dstDir = $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'];
    $filePath = Path::join(
      $dstDir,
      "{$this->configuration['module']['machine_name']}.links.action.yml",
    );

    $this->utils->ymlFileReplace($filePath, $this->getContentEntityTypeLinksAction());

    return $this;
  }

  protected function getContentEntityTypeLinksAction(): array {
    $id = $this->configuration['content']['id'];

    return [
      "entity.$id.add_page" => [
        'route_name' => "entity.$id.add_page",
        'title' => "new {$this->configuration['content']['label']}",
        'appears_on' => [
          "entity.$id.collection",
        ],
      ],
    ];
  }

  protected function contentEntityTypeLinksContextual(): static {
    $dstDir = $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'];
    $filePath = Path::join(
      $dstDir,
      "{$this->configuration['module']['machine_name']}.links.contextual.yml",
    );

    $this->utils->ymlFileReplace($filePath, $this->getContentEntityTypeLinksContextual());

    return $this;
  }

  protected function getContentEntityTypeLinksContextual(): array {
    $id = $this->configuration['content']['id'];

    return [
      "entity.$id.edit_form" => [
        'route_name' => "entity.$id.edit_form",
        'group' => $id,
        'title' => 'Edit',
      ],
      "entity.$id.delete_form" => [
        'route_name' => "entity.$id.delete_form",
        'group' => $id,
        'title' => 'Delete',
        'weight' => 10,
      ],
    ];
  }

  protected function contentEntityTypeLinksMenu(): static {
    $dstDir = $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'];
    $filePath = Path::join(
      $dstDir,
      "{$this->configuration['module']['machine_name']}.links.menu.yml",
    );

    $this->utils->ymlFileReplace($filePath, $this->getContentEntityTypeLinksMenu());

    return $this;
  }

  protected function getContentEntityTypeLinksMenu(): array {
    $id = $this->configuration['content']['id'];

    return [
      "entity.$id.collection" => [
        'title' => $this->configuration['content']['label_plural'],
        'parent' => 'system.admin_content',
        'description' => "List of {$this->configuration['content']['label']} contents.",
        'route_name' => "entity.$id.collection",
      ],
    ];
  }

  protected function contentEntityTypeLinksTask(): static {
    $dstDir = $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'];
    $filePath = Path::join(
      $dstDir,
      "{$this->configuration['module']['machine_name']}.links.task.yml",
    );

    $this->utils->ymlFileReplace($filePath, $this->getContentEntityTypeLinksTask());

    return $this;
  }

  protected function getContentEntityTypeLinksTask(): array {
    $id = $this->configuration['content']['id'];

    return [
      "entity.$id.collection" => [
        'title' => $this->configuration['content']['label_plural'],
        'route_name' => "entity.$id.collection",
        'base_route' => 'system.admin_content',
      ],
      "entity.$id.canonical" => [
        'route_name' => "entity.$id.canonical",
        'base_route' => "entity.$id.canonical",
        'title' => 'View',
      ],
      "entity.$id.edit_form" => [
        'route_name' => "entity.$id.edit_form",
        'base_route' => "entity.$id.canonical",
        'title' => 'Edit',
      ],
      "entity.$id.revision_history" => [
        'route_name' => "entity.$id.revision_history",
        'base_route' => "entity.$id.canonical",
        'title' => 'Revisions',
        'weight' => 20,
      ],
      "entity.$id.delete_form" => [
        'route_name' => "entity.$id.delete_form",
        'base_route' => "entity.$id.canonical",
        'title' => 'Delete',
        'weight' => 10,
      ],
    ];
  }

  protected function contentEntityTypeRouting(): static {
    $dstDir = $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'];
    $fileName = Path::join(
      $dstDir,
      "{$this->configuration['module']['machine_name']}.routing.yml",
    );

    $this->utils->ymlFileReplace($fileName, $this->getContentEntityTypeRoutes());

    return $this;
  }

  protected function getContentEntityTypeRoutes(): array {
    return [];
  }

  /**
   * @throws \Twig\Error\Error
   */
  protected function contentEntityTypeModule(): static {
    $dstDir = $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'];
    $filePath = Path::join(
      $dstDir,
      "{$this->configuration['module']['machine_name']}.module",
    );

    $fileExists = $this->fs->exists($filePath);
    $fileContent = $fileExists ?
      file_get_contents($filePath)
      : implode("\n", [
        '<?php',
        '',
        '/**',
        ' * @file',
        ' * Common hooks.',
        ' */',
        '',
        'declare(strict_types=1);',
        '',
      ]);

    $hookTheme = $this->twig->render(
      '@devel_wizard/spell/entity_type/content/hook_theme.php.twig',
      $this->configuration,
    );

    $hookThemeName = "{$this->configuration['module']['machine_name']}_theme";
    if (!str_contains($fileContent, "function {$hookThemeName}(")) {
      $fileContent .= $hookTheme;
    }
    else {
      $this->messenger()->addWarning($this->t(
        'Function @name already exists. It has to be extended with the following code: <pre>@hook_theme</pre>',
        [
          '@name' => $hookThemeName,
          '@hook_theme' => $hookTheme,
        ],
      ));
    }

    if (!str_contains($fileContent, "function template_preprocess_{$this->configuration['content']['id']}(")) {
      $fileContent .= $this->twig->render(
        '@devel_wizard/spell/entity_type/content/module.php.twig',
        $this->configuration,
      );
    }

    // @todo Prevent duplication.
    $fileContent = $this->utils->addUseStatements(
      [
        'use Drupal\Core\Entity\EntityInterface;',
        'use Drupal\Core\Render\Element;',
      ],
      $fileContent,
    );
    $this->fs->mkdir(Path::getDirectory($filePath));
    $this->fs->dumpFile($filePath, $fileContent);

    return $this;
  }

  /**
   * @throws \Twig\Error\Error
   */
  protected function contentEntityTypeTemplate(): static {
    $dstDir = $this->batchContext['sandbox']['module_create_spell_configuration']['dst_dir'];

    $machineNameLowerDash = $this->configuration['module']['machineNameLowerDash'];
    $contentEntityIdDash = str_replace('_', '-', $this->configuration['content']['idLowerDash']);

    $filePath = Path::join(
      $dstDir,
      'templates',
      "$machineNameLowerDash.$contentEntityIdDash.html.twig",
    );

    $this->fs->mkdir(Path::getDirectory($filePath));
    $this->fs->dumpFile(
      $filePath,
      $this->twig->render(
        '@devel_wizard/spell/entity_type/content/template.twig.twig',
        $this->configuration,
      ),
    );
    $this->messageFilesystemEntryCreate($filePath);

    return $this;
  }

}

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

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