devel_wizard-2.x-dev/src/Plugin/DevelWizard/Spell/EntityTypeGroupSpell.php
src/Plugin/DevelWizard/Spell/EntityTypeGroupSpell.php
<?php
declare(strict_types=1);
namespace Drupal\devel_wizard\Plugin\DevelWizard\Spell;
use Drupal\Component\Plugin\ConfigurableInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
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\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\String\UnicodeString;
#[DevelWizardSpell(
id: 'devel_wizard_entity_type_group',
category: new TranslatableMarkup('Code'),
label: new TranslatableMarkup('Group support for an entity type'),
description: new TranslatableMarkup('Generates PHP and YML files.'),
tags: [
'code' => new TranslatableMarkup('Code'),
'group' => new TranslatableMarkup('Group'),
'config_entity' => new TranslatableMarkup('Config entity'),
'content_entity' => new TranslatableMarkup('Content entity'),
],
)]
class EntityTypeGroupSpell extends SpellBase implements
PluginFormInterface,
ConfigurableInterface,
ContainerFactoryPluginInterface {
use SpellTraitPackageManager;
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('entity_type.manager'),
$container->get('plugin.manager.devel_wizard.spell'),
$container->get('module_installer'),
$container->get('extension.list.module'),
$container->get('twig'),
new Filesystem(),
);
}
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
MessengerInterface $messenger,
LoggerInterface $logger,
TranslationInterface $stringTranslation,
protected Utils $utils,
protected ConfigFactoryInterface $configFactory,
protected EntityTypeManagerInterface $entityTypeManager,
protected SpellManagerInterface $spellManager,
protected ModuleInstallerInterface $moduleInstaller,
protected ModuleExtensionList $moduleList,
protected TwigEnvironment $twig,
protected Filesystem $fs,
) {
parent::__construct(
$configuration,
$plugin_id,
$plugin_definition,
$messenger,
$logger,
$stringTranslation,
$utils,
$configFactory,
);
}
/**
* {@inheritdoc}
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
*/
public function defaultConfiguration() {
$moduleSpell = $this->spellManager->createInstance('devel_wizard_module');
$default = [
'contentEntityType' => [
'id' => '',
],
'module' => $moduleSpell->defaultConfiguration(),
];
$default['module']['type'] = 'sub_module';
return $default;
}
/**
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Component\Plugin\Exception\PluginException
*/
protected function populateCalculatedConfigurationValues(): static {
parent::populateCalculatedConfigurationValues();
if (empty($this->configuration['contentEntityType']['id'])) {
throw new \InvalidArgumentException('content entity type id is required');
}
$contentEntityType = $this
->entityTypeManager
->getDefinition($this->configuration['contentEntityType']['id']);
$this->configuration['contentEntityType'] += $this
->utils
->getEntityTypeInfo($contentEntityType);
if ($this->configuration['module']['type'] === 'sub_module') {
if (empty($this->configuration['module']['sub_module_of'])) {
$this->configuration['module']['sub_module_of'] = $contentEntityType->getProvider();
}
if (empty($this->configuration['module']['machine_name'])) {
$this->configuration['module']['machine_name'] = $this->configuration['module']['sub_module_of'] . '_group';
}
}
if (empty($this->configuration['module']['machine_name'])) {
$this->configuration['module']['machine_name'] = $contentEntityType->getProvider() . '_group';
}
$moduleSpell = $this->spellManager->createInstance(
'devel_wizard_module',
$this->configuration['module'],
);
$moduleSpell->prepare();
$this->configuration['module'] = $moduleSpell->getConfiguration();
// @todo Use the "project:module" format.
$this->configuration['module']['info.yml']['dependencies'][] = $contentEntityType->getProvider();
$this->configuration['module']['info.yml']['dependencies'][] = 'group:group';
$configEntityTypeId = $contentEntityType->getBundleEntityType();
if ($configEntityTypeId) {
$configEntityType = $this->entityTypeManager->getDefinition($configEntityTypeId);
$this->configuration['configEntityType']['id'] = $configEntityTypeId;
$this->configuration['configEntityType']['definition'] = $configEntityType;
$this->configuration['configEntityType'] += $this->utils->getEntityTypeInfo($configEntityType);
}
$this->configuration['groupRelationPlugin'] = [
'id' => $this->configuration['contentEntityType']['id'],
'deriverClass' => $this->configuration['contentEntityType']['idUpperCamel'] . 'Deriver',
'deriverClassFqn' => "Drupal\\{$this->configuration['module']['machineName']}\\Plugin\\Group\\Relation\\{$this->configuration['contentEntityType']['idUpperCamel']}Deriver",
];
$this->configuration['groupRelationPlugin']['idUpperCamel'] = (new UnicodeString('a_' . $this->configuration['groupRelationPlugin']['id']))
->camel()
->trimPrefix('a')
->toString();
$this->configuration['groupPermissionProvider'] = [
'class' => $this->configuration['groupRelationPlugin']['idUpperCamel'] . 'PermissionProvider',
'classNamespace' => "Drupal\\{$this->configuration['module']['machineName']}\\Plugin\\Group\\RelationHandler",
];
$this->configuration['groupPermissionProvider']['filePath'] = "src/Plugin/Group/RelationHandler/{$this->configuration['groupPermissionProvider']['class']}.php";
$this->configuration['groupPermissionProvider']['classFqn'] = "{$this->configuration['groupPermissionProvider']['classNamespace']}\\{$this->configuration['groupPermissionProvider']['class']}";
return $this;
}
/**
* {@inheritdoc}
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$conf = $this->getConfiguration();
$form['contentEntityType'] = [
'#tree' => TRUE,
'#type' => 'details',
'#open' => TRUE,
'#title' => $this->t('Content entity'),
'id' => [
'#type' => 'machine_name',
'#required' => FALSE,
'#title' => $this->t('Content entity type machine-name'),
'#description' => $this->t('Content entity type to add Group support for.'),
'#default_value' => $conf['contentEntityType']['id'],
'#maxlength' => $this->identifierMaxLength,
'#size' => $this->identifierInputSize,
'#machine_name' => [
'exists' => [Utils::class, 'alwaysFalse'],
'standalone' => TRUE,
],
'#autocomplete_route_name' => 'devel_wizard.autocomplete.entity_type_id',
'#autocomplete_route_parameters' => [
'types' => 'content',
],
],
];
$moduleSpell = $this->spellManager->createInstance('devel_wizard_module', $conf['module']);
if ($moduleSpell instanceof PluginFormInterface) {
$parents = $form['#parents'];
$parents[] = 'module';
$form['module'] = [
'#parents' => $parents,
'#type' => 'details',
'#tree' => TRUE,
'#open' => TRUE,
'#title' => $this->t('Module'),
'#description' => $this->t('The module to put the new codes into.'),
];
$form['module'] = $moduleSpell->buildConfigurationForm($form['module'], $form_state);
$form['module']['machine_name']['#required'] = FALSE;
}
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 \Drupal\Component\Plugin\Exception\PluginException
* @throws \Twig\Error\SyntaxError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\LoaderError
*/
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 ?
'module_install'
: 'module_create';
$this->batchContext['sandbox']['sub_spells'] = [
'module_create' => [
'finished' => $isModuleExists ? 1.0 : 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');
break;
}
$this->batchContext['message'] = $this->t('Install required modules');
$this->batchContext['sandbox']['current_step'] = 'module_install';
$this->batchContext['sandbox']['finished'] = 0.4;
drupal_flush_all_caches();
break;
case 'module_install':
$this->doItModuleInstall();
drupal_flush_all_caches();
$this->batchContext['message'] = $this->t('Code generation in progress');
$this->batchContext['sandbox']['current_step'] = 'code';
$this->batchContext['sandbox']['finished'] = 0.8;
break;
case 'code':
$this->doItGenerateCode();
$this->batchContext['message'] = $this->t('Finished');
$this->batchContext['sandbox']['current_step'] = '_finished';
$this->batchContext['sandbox']['finished'] = 1.0;
break;
case '_finished':
$this->batchContext['sandbox']['finished'] = 1.0;
break;
}
return $this;
}
protected function doItModuleInstall(): static {
foreach ($this->getModuleNamesToInstall() as $moduleName) {
$this->ensureModuleInstalled($moduleName);
}
return $this;
}
/**
* @throws \Twig\Error\SyntaxError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\LoaderError
*/
protected function doItGenerateCode(): static {
$conf = $this->getConfiguration();
$module = $conf['module']['machine_name'];
$moduleDir = $this->moduleList->getPath($module);
$fileNames = [
"$moduleDir/$module.module" => '@devel_wizard/spell/entity_type_group/module.php.twig',
"$moduleDir/src/Routing/RouteSubscriber.php" => '@devel_wizard/spell/entity_type_group/route-subscriber.php.twig',
"$moduleDir/src/Plugin/Group/Relation/{$conf['contentEntityType']['idUpperCamel']}.php" => '@devel_wizard/spell/entity_type_group/plugin-group-relation.php.twig',
"$moduleDir/{$conf['groupPermissionProvider']['filePath']}" => '@devel_wizard/spell/entity_type_group/plugin-group-relation-handler-permission-provider.php.twig',
];
foreach ($fileNames as $filePath => $tplName) {
$fileContent = $this->twig->render($tplName, $conf);
$this->fs->dumpFile($filePath, $fileContent);
$this->messageFilesystemEntryCreate($filePath);
}
$filePath = "$moduleDir/$module.services.yml";
$this->utils->ymlFileReplace(
$filePath,
[
"$module.route_subscriber" => [
'class' => "Drupal\\$module\\Routing\\RouteSubscriber",
'tags' => [
['name' => 'event_subscriber'],
],
],
"group.relation_handler.permission_provider.{$conf['groupRelationPlugin']['id']}" => [
'class' => $conf['groupPermissionProvider']['classFqn'],
'shared' => FALSE,
'arguments' => [
'@group.relation_handler.permission_provider',
],
],
],
['services'],
);
$this->messageFilesystemEntryUpdate($filePath);
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;
}
}
