devel_wizard-2.x-dev/src/Plugin/DevelWizard/Spell/ConfigEntityAdminViewSpellBase.php
src/Plugin/DevelWizard/Spell/ConfigEntityAdminViewSpellBase.php
<?php
declare(strict_types=1);
namespace Drupal\devel_wizard\Plugin\DevelWizard\Spell;
use Drupal\Component\Uuid\UuidInterface;
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\Serialization\Yaml;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Template\TwigEnvironment;
use Drupal\devel_wizard\ShellProcessFactoryInterface;
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;
/**
* Creates a page with views for administrators to list content instances for a specific bundle.
*/
abstract class ConfigEntityAdminViewSpellBase extends ConfigEntitySpellBase {
use SpellTraitPackageManager;
protected SpellManagerInterface $spellManager;
protected UuidInterface $uuid;
protected TwigEnvironment $twig;
protected Filesystem $fs;
/**
* @abstract
*/
protected string $primaryTabPath = '';
/**
* @abstract
*/
protected string $bundleIdInConfigTemplate = '';
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('devel_wizard.shell_process_factory'),
$container->get('module_installer'),
$container->get('extension.list.module'),
$container->get('uuid'),
$container->get('twig'),
new Filesystem(),
);
}
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
MessengerInterface $messenger,
LoggerInterface $logger,
TranslationInterface $stringTranslation,
Utils $utils,
ConfigFactoryInterface $configFactory,
EntityTypeManagerInterface $entityTypeManager,
SpellManagerInterface $spellManager,
ShellProcessFactoryInterface $shellProcessFactory,
ModuleInstallerInterface $moduleInstaller,
ModuleExtensionList $moduleList,
UuidInterface $uuid,
TwigEnvironment $twig,
?Filesystem $fs = NULL,
) {
$this->entityTypeManager = $entityTypeManager;
$this->spellManager = $spellManager;
$this->shellProcessFactory = $shellProcessFactory;
$this->moduleInstaller = $moduleInstaller;
$this->moduleList = $moduleList;
$this->uuid = $uuid;
$this->twig = $twig;
$this->fs = $fs ?: new Filesystem();
parent::__construct(
$configuration,
$plugin_id,
$plugin_definition,
$messenger,
$logger,
$stringTranslation,
$utils,
$configFactory,
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'machine_name' => '',
'module' => [
'machine_name' => 'app_core',
],
];
}
protected function populateCalculatedConfigurationValues(): static {
parent::populateCalculatedConfigurationValues();
$conf =& $this->configuration;
if (empty($conf['machine_name'])) {
throw new \InvalidArgumentException('bundle.machine_name is required');
}
if (empty($conf['module']['machine_name'])) {
throw new \InvalidArgumentException('module.machine_name is required');
}
$conf['idPrefix'] = (new UnicodeString($conf['module']['machine_name']))
->split('_')[0]
->toString();
$conf['module']['upperCamel'] = (new UnicodeString("a_{$conf['module']['machine_name']}"))
->camel()
->trimPrefix('a')
->toString();
$conf['configEntityTypeIdUpperCamel'] = (new UnicodeString("a_{$this->configEntityTypeId}"))
->camel()
->trimPrefix('a')
->toString();
$conf['contentEntityTypeIdUpperCamel'] = (new UnicodeString("a_{$this->contentEntityTypeId}"))
->camel()
->trimPrefix('a')
->toString();
$conf['linksTaskBase']['class'] = "LinksTaskEntityOverview";
$conf['linksTaskBase']['classNamespace'] = "Drupal\\{$conf['module']['machine_name']}\Plugin\Derivative";
$conf['linksTaskBase']['classFqn'] = "{$conf['linksTaskBase']['classNamespace']}\\{$conf['linksTaskBase']['class']}";
$conf['linksTaskDeriver']['class'] = "{$conf['linksTaskBase']['class']}{$conf['contentEntityTypeIdUpperCamel']}";
$conf['linksTaskDeriver']['classNamespace'] = $conf['linksTaskBase']['classNamespace'];
$conf['linksTaskDeriver']['classFqn'] = "{$conf['linksTaskDeriver']['classNamespace']}\\{$conf['linksTaskDeriver']['class']}";
$conf['linksActionBase']['class'] = "LinksActionEntityAdd";
$conf['linksActionBase']['classNamespace'] = "Drupal\\{$conf['module']['machine_name']}\Plugin\Derivative";
$conf['linksActionBase']['classFqn'] = "{$conf['linksActionBase']['classNamespace']}\\{$conf['linksActionBase']['class']}";
$conf['linksActionDeriver']['class'] = "{$conf['linksActionBase']['class']}{$conf['contentEntityTypeIdUpperCamel']}";
$conf['linksActionDeriver']['classNamespace'] = $conf['linksActionBase']['classNamespace'];
$conf['linksActionDeriver']['classFqn'] = "{$conf['linksActionDeriver']['classNamespace']}\\{$conf['linksActionDeriver']['class']}";
return $this;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$configuration = $this->getConfiguration();
$configEntityType = $this->getConfigEntityType();
// @todo Move $machineNamePattern into a common place.
$machineNamePattern = '[a-z][a-z0-9_]*';
$form['machine_name'] = [
'#type' => 'textfield',
'#required' => TRUE,
'#title' => $configEntityType?->getLabel() ?: $this->t('Bundle'),
'#description' => $this->t(
'The @entity_type.label you want to create the admin view for.',
[
'@entity_type.label' => $configEntityType?->getLabel() ?: $this->configEntityTypeId,
],
),
'#default_value' => $configuration['machine_name'],
'#autocomplete_route_name' => 'devel_wizard.autocomplete.config_entity_instance',
'#autocomplete_route_parameters' => [
'entityTypeId' => $this->configEntityTypeId,
],
];
$form['module'] = [
'#tree' => TRUE,
'#type' => 'container',
'machine_name' => [
'#type' => 'textfield',
'#required' => TRUE,
'#title' => $this->t('Module machine-name'),
'#description' => $this->t('Machine-name of the module to put the new code files into. If not exists then it will be created in the modules/custom directory'),
'#default_value' => $configuration['module']['machine_name'],
'#pattern' => $machineNamePattern,
'#autocomplete_route_name' => 'devel_wizard.autocomplete.module',
],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
// @todo Implement validateConfigurationForm() method.
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$values = $form_state->getValue($form['#parents'], []);
$this->setConfiguration($values);
}
/**
* @throws \Drupal\Component\Plugin\Exception\PluginException
* @throws \Drupal\Core\Entity\EntityMalformedException
* @throws \Drupal\Core\Entity\EntityStorageException
* @throws \Twig\Error\Error
*/
protected function doIt(): static {
if (!isset($this->batchContext['sandbox']['current_step'])) {
$this->batchContext['sandbox']['current_step'] = 'composer_packages_require';
}
$configuration = $this->getConfiguration();
switch ($this->batchContext['sandbox']['current_step']) {
case 'composer_packages_require':
$this->doItComposerRequire();
drupal_flush_all_caches();
$this->batchContext['message'] = $this->t('Create primary and secondary tab related modules');
$this->batchContext['sandbox']['current_step'] = 'module_create';
$this->batchContext['sandbox']['finished'] = 0.2;
break;
case 'module_create':
if (!$this->moduleList->exists($configuration['module']['machine_name'])) {
$moduleSpellConfiguration = $configuration['module'];
$moduleSpellConfiguration['type'] = 'custom';
$moduleSpellContext = [];
/* @noinspection PhpUnhandledExceptionInspection */
$moduleSpell = $this->spellManager->createInstance(
'devel_wizard_module',
$moduleSpellConfiguration,
);
$moduleSpell->prepare();
$moduleSpell->abracadabra($moduleSpellContext);
drupal_flush_all_caches();
}
$this->batchContext['message'] = $this->t('Install required modules');
$this->batchContext['sandbox']['current_step'] = 'module_install';
$this->batchContext['sandbox']['finished'] = 0.4;
break;
case 'module_install':
$this->doItModuleInstall();
drupal_flush_all_caches();
$this->batchContext['message'] = $this->t('Create views and menu items');
$this->batchContext['sandbox']['current_step'] = 'tabs_create';
$this->batchContext['sandbox']['finished'] = 0.6;
break;
case 'tabs_create':
$this->doItTabsPermissions();
$this->doItTabsCreate();
drupal_flush_all_caches();
$this->batchContext['message'] = $this->t('Create primary and secondary tabs');
$this->batchContext['sandbox']['current_step'] = 'tasks_and_actions';
$this->batchContext['sandbox']['finished'] = 0.8;
break;
case 'tasks_and_actions':
$this->doItLinksTasks();
$this->doItLinksActions();
drupal_flush_all_caches();
$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 doItComposerRequire(): static {
$this->installComposerPackages('prod', $this->getRequiredPackagesProd());
$this->installComposerPackages('dev', $this->getRequiredPackagesDev());
return $this;
}
protected function doItModuleInstall(): static {
foreach ($this->getModuleNamesToInstall() as $moduleName) {
$this->ensureModuleInstalled($moduleName);
}
return $this;
}
/**
* Creates custom permission entries.
*/
protected function doItTabsPermissions(): static {
$configuration = $this->getConfiguration();
$moduleMachineName = $configuration['module']['machine_name'];
$configEntityType = $this->getConfigEntityType();
$configEntityTypeLabel = $configEntityType ? $configEntityType->getLabel() : $this->configEntityTypeId;
$modulePath = $this->getModulePath($moduleMachineName);
$ymlFileName = Path::join(
$modulePath,
"{$moduleMachineName}.permissions.yml",
);
$entries = [
"{$moduleMachineName}.{$this->configEntityTypeId}.overview" => [
'title' => "Access to {$configEntityTypeLabel} overview page",
'description' => "Allows users to access to the {$configEntityTypeLabel} overview page.",
],
];
$this->utils->ymlFileReplace($ymlFileName, $entries);
return $this;
}
/**
* @throws \Drupal\Core\Entity\EntityMalformedException
* @throws \Drupal\Core\Entity\EntityStorageException
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function doItTabsCreate(): static {
/* @noinspection PhpUnhandledExceptionInspection */
$viewStorage = $this->entityTypeManager->getStorage('view');
// @todo Config views.view.content" is required.
// @todo Check that if the links.task entry for the main route is exists or not.
$viewRaw = $this->getMainTabView();
if ($viewRaw) {
/** @var \Drupal\views\ViewEntityInterface $view */
$view = $viewStorage->load($viewRaw['id']);
if ($view) {
$this->messageConfigEntityExists($view);
}
else {
/** @var \Drupal\views\ViewEntityInterface $view */
$view = $viewStorage->create($viewRaw);
$view->save();
$this->messageConfigEntityCreate($view);
}
}
$viewRaw = $this->getSecondaryTaskView();
if ($viewRaw) {
/** @var \Drupal\views\ViewEntityInterface $view */
$view = $viewStorage->load($viewRaw['id']);
if ($view) {
$this->messageConfigEntityExists($view);
}
else {
/** @var \Drupal\views\ViewEntityInterface $view */
$view = $viewStorage->create($viewRaw);
$view->save();
$this->messageConfigEntityCreate($view);
}
}
return $this;
}
/**
* @throws \Twig\Error\Error
*/
protected function doItLinksTasks(): static {
$conf =& $this->configuration;
$modulePath = $this->getModulePath($conf['module']['machine_name']);
$ymlFilePath = Path::join(
$modulePath,
"{$conf['module']['machine_name']}.links.task.yml",
);
$ymlEntries = [
"{$conf['module']['machine_name']}_{$this->contentEntityTypeId}_overview" => [
'class' => 'Drupal\Core\Menu\LocalTaskDefault',
'deriver' => "{$conf['linksTaskDeriver']['classFqn']}",
],
];
$this->utils->ymlFileReplace($ymlFilePath, $ymlEntries);
$baseTemplate = '@devel_wizard/derivative/links-task/devel_wizard.entity-overview.class.php.twig';
$baseFilePath = Path::join(
$modulePath,
'src',
'Plugin',
'Derivative',
"{$conf['linksTaskBase']['class']}.php",
);
$baseFileContent = $this->twig->render($baseTemplate, $conf);
$this->fs->mkdir(Path::getDirectory($baseFilePath));
$this->fs->dumpFile($baseFilePath, $baseFileContent);
$this->messageFilesystemEntryCreate($baseFilePath);
$derivativeTemplate = "@devel_wizard/derivative/links-task/devel_wizard.entity-overview-{$this->contentEntityTypeId}.class.php.twig";
$derivativeFilePath = Path::join(
$modulePath,
'src',
'Plugin',
'Derivative',
"{$conf['linksTaskDeriver']['class']}.php",
);
$derivativeFileContent = $this->twig->render($derivativeTemplate, $conf);
$this->fs->mkdir(Path::getDirectory($derivativeFilePath));
$this->fs->dumpFile($derivativeFilePath, $derivativeFileContent);
$this->messageFilesystemEntryCreate($derivativeFilePath);
return $this;
}
/**
* @throws \Twig\Error\Error
*/
protected function doItLinksActions(): static {
$conf =& $this->configuration;
$modulePath = $this->getModulePath($conf['module']['machine_name']);
$ymlFilePath = Path::join(
$modulePath,
"{$conf['module']['machine_name']}.links.action.yml",
);
$ymlEntries = [
"{$conf['module']['machine_name']}_{$this->contentEntityTypeId}_add" => [
'class' => 'Drupal\Core\Menu\LocalActionDefault',
'deriver' => "{$conf['linksActionDeriver']['classFqn']}",
],
];
$this->utils->ymlFileReplace($ymlFilePath, $ymlEntries);
$baseTemplate = '@devel_wizard/derivative/links-action/devel_wizard.entity-add.class.php.twig';
$baseFilePath = Path::join(
$modulePath,
'src',
'Plugin',
'Derivative',
"{$conf['linksActionBase']['class']}.php",
);
$baseFileContent = $this->twig->render($baseTemplate, $conf);
$this->fs->mkdir(Path::getDirectory($baseFilePath));
$this->fs->dumpFile($baseFilePath, $baseFileContent);
$this->messageFilesystemEntryCreate($baseFilePath);
$derivativeTemplate = "@devel_wizard/derivative/links-action/devel_wizard.entity-add-{$this->contentEntityTypeId}.class.php.twig";
$derivativeFilePath = Path::join(
$modulePath,
'src',
'Plugin',
'Derivative',
"{$conf['linksActionDeriver']['class']}.php",
);
$derivativeFileContent = $this->twig->render($derivativeTemplate, $conf);
$this->fs->mkdir(Path::getDirectory($derivativeFilePath));
$this->fs->dumpFile($derivativeFilePath, $derivativeFileContent);
$this->messageFilesystemEntryCreate($derivativeFilePath);
return $this;
}
public function getRequiredPackagesProd(): array {
return [];
}
public function getRequiredPackagesDev(): array {
return [];
}
protected function getModuleNamesToInstall(): array {
$configuration = $this->getConfiguration();
return array_filter([
'views',
$this->provider,
$configuration['module']['machine_name'],
]);
}
protected function getMainTabView(): ?array {
return NULL;
}
protected function getSecondaryTaskViewId(): string {
$conf =& $this->configuration;
return "{$conf['idPrefix']}_{$this->contentEntityTypeId}_{$conf['machine_name']}_admin";
}
protected function getSecondaryTaskView(): ?array {
$conf =& $this->configuration;
$contentEntityType = $this->getContentEntityType();
$configEntityType = $this->getConfigEntityType();
$configPrefix = $configEntityType->getConfigPrefix();
$bundleKey = $contentEntityType->getKey('bundle');
$filePath = $this->getConfigTemplateFilePath("views.view.{$this->contentEntityTypeId}.bundle_admin.yml");
$data = Yaml::decode(file_get_contents($filePath));
$data['uuid'] = $this->uuid->generate();
$data['id'] = $this->getSecondaryTaskViewId();
$data['label'] = "{$this->contentEntityTypeId} - {$conf['machine_name']} - admin";
$key = array_search("{$this->provider}.{$configPrefix}.{$this->bundleIdInConfigTemplate}", $data['dependencies']);
if ($key !== FALSE) {
unset($data['dependencies'][$key]);
}
$data['dependencies'][] = "{$this->provider}.$configPrefix.{$conf['machine_name']}";
// @todo Get the bundle label.
$data['display']['default']['display_options']['title'] = $conf['machine_name'];
$data['display']['overview']['display_options']['menu']['title'] = $conf['machine_name'];
$data['display']['default']['display_options']['filters'][$bundleKey]['value'] = [
$conf['machine_name'] => $conf['machine_name'],
];
return $data;
}
protected function getConfigTemplateFilePath(string $fileName): string {
return Path::join(
$this->moduleList->getPath('devel_wizard'),
'config',
'template',
$fileName,
);
}
/**
* Creates custom routing entries.
*/
protected function createDerivativeRouting(): static {
$configuration = $this->getConfiguration();
$moduleMachineName = $configuration['module']['machine_name'];
$configEntityType = $this->getConfigEntityType();
$dashedContentEntityTypeId = str_replace('_', '-', $this->contentEntityTypeId);
$modulePath = $this->getModulePath($moduleMachineName);
$ymlFileName = Path::join(
$modulePath,
"{$moduleMachineName}.routing.yml",
);
$entries = [
"{$moduleMachineName}.{$this->contentEntityTypeId}.collection" => [
'path' => "/admin/content/{$dashedContentEntityTypeId}",
'defaults' => [
'_title' => $configEntityType ? $configEntityType->getLabel()->render() : $this->configEntityTypeId,
'_entity_list' => $this->contentEntityTypeId,
],
'requirements' => [
// @todo Permission.
'_permission' => 'access content',
],
],
];
$this->utils->ymlFileReplace($ymlFileName, $entries);
return $this;
}
}
