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;
}
}
