Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -0,0 +1,66 @@
<?php
namespace Drupal\devel_generate\Annotation;
use Drupal\Component\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
/**
* Defines a DevelGenerate annotation object.
*
* DevelGenerate handle the bulk creation of entites.
*
* Additional annotation keys for DevelGenerate can be defined in
* hook_devel_generate_info_alter().
*
* @Annotation
*
* @see \Drupal\devel_generate\DevelGeneratePluginManager
* @see \Drupal\devel_generate\DevelGenerateBaseInterface
*/
class DevelGenerate extends Plugin {
/**
* The human-readable name of the DevelGenerate type.
*
* @ingroup plugin_translatable
*/
public Translation $label;
/**
* A short description of the DevelGenerate type.
*
* @ingroup plugin_translatable
*/
public Translation $description;
/**
* A url to access the plugin settings form.
*/
public string $url;
/**
* The permission required to access the plugin settings form.
*/
public string $permission;
/**
* The name of the DevelGenerate class.
*
* This is not provided manually, it will be added by the discovery mechanism.
*/
public string $class;
/**
* An array of settings passed to the DevelGenerate settingsForm.
*
* The keys are the names of the settings and the values are the default
* values for those settings.
*/
public array $settings = [];
/**
* Modules that should be enabled in order to make the plugin discoverable.
*/
public array $dependencies = [];
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Drupal\devel_generate\Attributes;
use Consolidation\AnnotatedCommand\Parser\CommandInfo;
/**
* Devel generate plugin details.
*/
#[\Attribute(\Attribute::TARGET_METHOD)]
class Generator {
public function __construct(
public string $id,
) {}
public static function handle(\ReflectionAttribute $attribute, CommandInfo $commandInfo): void {
$args = $attribute->getArguments();
$commandInfo->addAnnotation('pluginId', $args['id']);
}
}

View File

@ -0,0 +1,343 @@
<?php
namespace Drupal\devel_generate;
use Drupal\Component\Utility\Random;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Plugin\PluginBase;
use JetBrains\PhpStorm\Deprecated;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a base DevelGenerate plugin implementation.
*/
abstract class DevelGenerateBase extends PluginBase implements DevelGenerateBaseInterface {
/**
* The entity type manager service.
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* The language manager.
*/
protected LanguageManagerInterface $languageManager;
/**
* The module handler.
*/
protected ModuleHandlerInterface $moduleHandler;
/**
* The entity field manager.
*/
protected EntityFieldManagerInterface $entityFieldManager;
/**
* The plugin settings.
*/
protected array $settings = [];
/**
* The random data generator.
*/
protected ?Random $random = NULL;
/**
* Instantiates a new instance of this class.
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
$instance = new static($configuration, $plugin_id, $plugin_definition);
$instance->entityTypeManager = $container->get('entity_type.manager');
$instance->languageManager = $container->get('language_manager');
$instance->moduleHandler = $container->get('module_handler');
$instance->stringTranslation = $container->get('string_translation');
$instance->entityFieldManager = $container->get('entity_field.manager');
$instance->setMessenger($container->get('messenger'));
return $instance;
}
/**
* {@inheritdoc}
*/
public function getSetting(string $key) {
// Merge defaults if we have no value for the key.
if (!array_key_exists($key, $this->settings)) {
$this->settings = $this->getDefaultSettings();
}
return $this->settings[$key] ?? NULL;
}
/**
* {@inheritdoc}
*/
public function getDefaultSettings(): array {
$definition = $this->getPluginDefinition();
return $definition['settings'];
}
/**
* {@inheritdoc}
*/
public function getSettings(): array {
return $this->settings;
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state): array {
return [];
}
/**
* {@inheritdoc}
*/
public function settingsFormValidate(array $form, FormStateInterface $form_state): void {
// Validation is optional.
}
/**
* {@inheritdoc}
*/
public function generate(array $values): void {
$this->generateElements($values);
$this->messenger()->addMessage('Generate process complete.');
}
/**
* Business logic relating with each DevelGenerate plugin.
*
* @param array $values
* The input values from the settings form.
*/
protected function generateElements(array $values): void {
}
/**
* Populate the fields on a given entity with sample values.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to be enriched with sample field values.
* @param array $skip
* A list of field names to avoid when populating.
* @param array $base
* A list of base field names to populate.
*/
public function populateFields(EntityInterface $entity, array $skip = [], array $base = []): void {
if (!$entity->getEntityType()->entityClassImplements(FieldableEntityInterface::class)) {
// Nothing to do.
return;
}
$instances = $this->entityFieldManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle());
$instances = array_diff_key($instances, array_flip($skip));
foreach ($instances as $instance) {
$field_storage = $instance->getFieldStorageDefinition();
$field_name = $field_storage->getName();
if ($field_storage->isBaseField() && !in_array($field_name, $base)) {
// Skip base field unless specifically requested.
continue;
}
$max = $field_storage->getCardinality();
$cardinality = $max;
if ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
// Just an arbitrary number for 'unlimited'.
$max = random_int(1, 3);
}
$entity->$field_name->generateSampleItems($max);
}
}
/**
* {@inheritdoc}
*/
public function handleDrushParams($args) {
}
/**
* Set a message for either drush or the web interface.
*
* @param string|\Drupal\Component\Render\MarkupInterface $msg
* The message to display.
* @param string $type
* (optional) The message type, as defined in MessengerInterface. Defaults
* to MessengerInterface::TYPE_STATUS.
*/
#[Deprecated(reason: 'Use the messenger trait directly.')]
protected function setMessage($msg, string $type = MessengerInterface::TYPE_STATUS): void {
$this->messenger()->addMessage($msg, $type);
}
/**
* Check if a given param is a number.
*
* @param mixed $number
* The parameter to check.
*
* @return bool
* TRUE if the parameter is a number, FALSE otherwise.
*/
public static function isNumber(mixed $number): bool {
if ($number === NULL) {
return FALSE;
}
return is_numeric($number);
}
/**
* Returns the random data generator.
*
* @return \Drupal\Component\Utility\Random
* The random data generator.
*/
protected function getRandom(): Random {
if (!$this->random instanceof Random) {
$this->random = new Random();
}
return $this->random;
}
/**
* Generates a random sentence of specific length.
*
* Words are randomly selected with length from 2 up to the optional parameter
* $max_word_length. The first word is capitalised. No ending period is added.
*
* @param int $sentence_length
* The total length of the sentence, including the word-separating spaces.
* @param int $max_word_length
* (optional) Maximum length of each word. Defaults to 8.
*
* @return string
* A sentence of the required length.
*/
protected function randomSentenceOfLength(int $sentence_length, int $max_word_length = 8): string {
// Maximum word length cannot be longer than the sentence length.
$max_word_length = min($sentence_length, $max_word_length);
$words = [];
$remainder = $sentence_length;
do {
// If near enough to the end then generate the exact length word to fit.
// Otherwise, the remaining space cannot be filled with one word, so
// choose a random length, short enough for a following word of at least
// minimum length.
$next_word = $remainder <= $max_word_length ? $remainder : mt_rand(2, min($max_word_length, $remainder - 3));
$words[] = $this->getRandom()->word($next_word);
$remainder = $remainder - $next_word - 1;
} while ($remainder > 0);
return ucfirst(implode(' ', $words));
}
/**
* Creates the language and translation section of the form.
*
* This is used by both Content and Term generation.
*
* @param string $items
* The name of the things that are being generated - 'nodes' or 'terms'.
*
* @return array
* The language details section of the form.
*/
protected function getLanguageForm(string $items): array {
// We always need a language, even if the language module is not installed.
$options = [];
$languages = $this->languageManager->getLanguages(LanguageInterface::STATE_CONFIGURABLE);
foreach ($languages as $langcode => $language) {
$options[$langcode] = $language->getName();
}
$language_module_exists = $this->moduleHandler->moduleExists('language');
$translation_module_exists = $this->moduleHandler->moduleExists('content_translation');
$form['language'] = [
'#type' => 'details',
'#title' => $this->t('Language'),
'#open' => $language_module_exists,
];
$form['language']['add_language'] = [
'#type' => 'select',
'#title' => $this->t('Select the primary language(s) for @items', ['@items' => $items]),
'#multiple' => TRUE,
'#description' => $language_module_exists ? '' : $this->t('Disabled - requires Language module'),
'#options' => $options,
'#default_value' => [
$this->languageManager->getDefaultLanguage()->getId(),
],
'#disabled' => !$language_module_exists,
];
$form['language']['translate_language'] = [
'#type' => 'select',
'#title' => $this->t('Select the language(s) for translated @items', ['@items' => $items]),
'#multiple' => TRUE,
'#description' => $translation_module_exists ? $this->t('Translated @items will be created for each language selected.', ['@items' => $items]) : $this->t('Disabled - requires Content Translation module.'),
'#options' => $options,
'#disabled' => !$translation_module_exists,
];
return $form;
}
/**
* Return a language code.
*
* @param array $add_language
* Optional array of language codes from which to select one at random.
* If empty then return the site's default language.
*
* @return string
* The language code to use.
*/
protected function getLangcode(array $add_language): string {
if ($add_language === []) {
return $this->languageManager->getDefaultLanguage()->getId();
}
return $add_language[array_rand($add_language)];
}
/**
* Convert a csv string into an array of items.
*
* Borrowed from Drush.
*
* @param string|array|null $args
* A simple csv string; e.g. 'a,b,c'
* or a simple list of items; e.g. array('a','b','c')
* or some combination; e.g. array('a,b','c') or array('a,','b,','c,').
*/
public static function csvToArray($args): array {
if ($args === NULL) {
return [];
}
// 1: implode(',',$args) converts from array('a,','b,','c,') to 'a,,b,,c,'
// 2: explode(',', ...) converts to array('a','','b','','c','')
// 3: array_filter(...) removes the empty items
// 4: array_map(...) trims extra whitespace from each item
// (handles csv strings with extra whitespace, e.g. 'a, b, c')
//
$args = is_array($args) ? implode(',', array_map('strval', $args)) : (string) $args;
return array_map('trim', array_filter(explode(',', $args)));
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace Drupal\devel_generate;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Base interface definition for "DevelGenerate" plugins.
*
* This interface details base wrapping methods that most DevelGenerate
* implementations will want to directly inherit from
* Drupal\devel_generate\DevelGenerateBase.
*
* DevelGenerate implementation plugins should have their own settingsForm() and
* generateElements() to achieve their own behaviour.
*/
interface DevelGenerateBaseInterface extends PluginInspectionInterface {
public function __construct(array $configuration, $plugin_id, $plugin_definition);
/**
* Returns the array of settings, including defaults for missing settings.
*
* @param string $key
* The setting name.
*
* @return array|int|string|bool|null
* The setting.
*/
public function getSetting(string $key);
/**
* Returns the default settings for the plugin.
*
* @return array
* The array of default setting values, keyed by setting names.
*/
public function getDefaultSettings(): array;
/**
* Returns the current settings for the plugin.
*
* @return array
* The array of current setting values, keyed by setting names.
*/
public function getSettings(): array;
/**
* Returns the form for the plugin.
*
* @return array
* The array of default setting values, keyed by setting names.
*/
public function settingsForm(array $form, FormStateInterface $form_state): array;
/**
* Form validation handler.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public function settingsFormValidate(array $form, FormStateInterface $form_state): void;
/**
* Execute the instructions in common for all DevelGenerate plugin.
*
* @param array $values
* The input values from the settings form.
*/
public function generate(array $values): void;
/**
* Responsible for validating Drush params.
*
* @param array $args
* The command arguments.
* @param array $options
* The commend options.
*
* @return array
* An array of values ready to be used for generateElements().
*/
public function validateDrushParams(array $args, array $options = []): array;
}

View File

@ -0,0 +1,10 @@
<?php
namespace Drupal\devel_generate;
/**
* DevelGenerateException extending Generic Plugin exception class.
*/
class DevelGenerateException extends \Exception {
}

View File

@ -0,0 +1,53 @@
<?php
namespace Drupal\devel_generate;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides dynamic permissions of the filter module.
*/
class DevelGeneratePermissions implements ContainerInjectionInterface {
use StringTranslationTrait;
/**
* The plugin manager.
*/
protected DevelGeneratePluginManager $develGeneratePluginManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
$instance = new self();
$instance->develGeneratePluginManager = $container->get('plugin.manager.develgenerate');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* A permissions' callback.
*
* @see devel_generate.permissions.yml
*
* @return array
* An array of permissions for all plugins.
*/
public function permissions(): array {
$permissions = [];
$devel_generate_plugins = $this->develGeneratePluginManager->getDefinitions();
foreach ($devel_generate_plugins as $plugin) {
$permission = $plugin['permission'];
$permissions[$permission] = [
'title' => $this->t('@permission', ['@permission' => $permission]),
];
}
return $permissions;
}
}

View File

@ -0,0 +1,105 @@
<?php
namespace Drupal\devel_generate;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\devel_generate\Annotation\DevelGenerate;
/**
* Plugin type manager for DevelGenerate plugins.
*/
class DevelGeneratePluginManager extends DefaultPluginManager {
/**
* The entity type manager.
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* The messenger service.
*/
protected MessengerInterface $messenger;
/**
* The language manager.
*/
protected LanguageManagerInterface $languageManager;
/**
* The translation manager.
*/
protected TranslationInterface $stringTranslation;
/**
* Constructs a DevelGeneratePluginManager object.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The translation manager.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager
* The entity field manager.
*/
public function __construct(
\Traversable $namespaces,
CacheBackendInterface $cache_backend,
ModuleHandlerInterface $module_handler,
EntityTypeManagerInterface $entity_type_manager,
MessengerInterface $messenger,
LanguageManagerInterface $language_manager,
TranslationInterface $string_translation,
protected EntityFieldManagerInterface $entityFieldManager,
) {
parent::__construct('Plugin/DevelGenerate', $namespaces, $module_handler, NULL, DevelGenerate::class);
$this->entityTypeManager = $entity_type_manager;
$this->messenger = $messenger;
$this->languageManager = $language_manager;
$this->moduleHandler = $module_handler;
$this->stringTranslation = $string_translation;
$this->alterInfo('devel_generate_info');
$this->setCacheBackend($cache_backend, 'devel_generate_plugins');
}
/**
* {@inheritdoc}
*/
protected function findDefinitions(): array {
$definitions = [];
foreach (parent::findDefinitions() as $plugin_id => $plugin_definition) {
$plugin_available = TRUE;
foreach ($plugin_definition['dependencies'] as $module_name) {
// If a plugin defines module dependencies and at least one module is
// not installed don't make this plugin available.
if (!$this->moduleHandler->moduleExists($module_name)) {
$plugin_available = FALSE;
break;
}
}
if ($plugin_available) {
$definitions[$plugin_id] = $plugin_definition;
}
}
return $definitions;
}
}

View File

@ -0,0 +1,263 @@
<?php
namespace Drupal\devel_generate\Drush\Commands;
use Consolidation\AnnotatedCommand\CommandData;
use Consolidation\AnnotatedCommand\Hooks\HookManager;
use Drupal\devel_generate\Attributes\Generator;
use Drupal\devel_generate\DevelGenerateBaseInterface;
use Drupal\devel_generate\DevelGeneratePluginManager;
use Drush\Attributes as CLI;
use Drush\Commands\AutowireTrait;
use Drush\Commands\DrushCommands;
/**
* Provide Drush commands for all the core Devel Generate plugins.
*
* For commands that are parts of modules, Drush expects to find commandfiles in
* __MODULE__/src/Drush/Commands, and the namespace is Drupal/__MODULE__/Drush/Commands.
*/
final class DevelGenerateCommands extends DrushCommands {
use AutowireTrait;
const USERS = 'devel-generate:users';
const TERMS = 'devel-generate:terms';
const VOCABS = 'devel-generate:vocabs';
const MENUS = 'devel-generate:menus';
const CONTENT = 'devel-generate:content';
const BLOCK_CONTENT = 'devel-generate:block-content';
const MEDIA = 'devel-generate:media';
/**
* The plugin instance.
*/
private DevelGenerateBaseInterface $pluginInstance;
/**
* The Generate plugin parameters.
*/
private array $parameters;
/**
* DevelGenerateCommands constructor.
*
* @param \Drupal\devel_generate\DevelGeneratePluginManager $manager
* The DevelGenerate plugin manager.
*/
public function __construct(protected DevelGeneratePluginManager $manager) {
parent::__construct();
$this->setManager($manager);
}
/**
* Get the DevelGenerate plugin manager.
*
* @return \Drupal\devel_generate\DevelGeneratePluginManager
* The DevelGenerate plugin manager.
*/
public function getManager(): DevelGeneratePluginManager {
return $this->manager;
}
/**
* Set the DevelGenerate plugin manager.
*
* @param \Drupal\devel_generate\DevelGeneratePluginManager $manager
* The DevelGenerate plugin manager.
*/
public function setManager(DevelGeneratePluginManager $manager): void {
$this->manager = $manager;
}
/**
* Get the DevelGenerate plugin instance.
*
* @return \Drupal\devel_generate\DevelGenerateBaseInterface
* The DevelGenerate plugin instance.
*/
public function getPluginInstance(): DevelGenerateBaseInterface {
return $this->pluginInstance;
}
/**
* Set the DevelGenerate plugin instance.
*
* @param mixed $pluginInstance
* The DevelGenerate plugin instance.
*/
public function setPluginInstance(mixed $pluginInstance): void {
$this->pluginInstance = $pluginInstance;
}
/**
* Get the DevelGenerate plugin parameters.
*
* @return array
* The plugin parameters.
*/
public function getParameters(): array {
return $this->parameters;
}
/**
* Set the DevelGenerate plugin parameters.
*
* @param array $parameters
* The plugin parameters.
*/
public function setParameters(array $parameters): void {
$this->parameters = $parameters;
}
/**
* Create users.
*/
#[CLI\Command(name: self::USERS, aliases: ['genu', 'devel-generate-users'])]
#[CLI\Argument(name: 'num', description: 'Number of users to generate.')]
#[CLI\Option(name: 'kill', description: 'Delete all users before generating new ones.')]
#[CLI\Option(name: 'roles', description: 'A comma delimited list of role IDs for new users. Don\'t specify <info>authenticated</info>.')]
#[CLI\Option(name: 'pass', description: 'Specify a password to be set for all generated users.')]
#[Generator(id: 'user')]
public function users(string|int $num = 50, array $options = ['kill' => FALSE, 'roles' => self::REQ]): void {
// @todo pass $options to the plugins.
$this->generate();
}
/**
* Create terms in specified vocabulary.
*/
#[CLI\Command(name: self::TERMS, aliases: ['gent', 'devel-generate-terms'])]
#[CLI\Argument(name: 'num', description: 'Number of terms to generate.')]
#[CLI\Option(name: 'kill', description: 'Delete all terms in these vocabularies before generating new ones.')]
#[CLI\Option(name: 'bundles', description: 'A comma-delimited list of machine names for the vocabularies where terms will be created.')]
#[CLI\Option(name: 'feedback', description: 'An integer representing interval for insertion rate logging.')]
#[CLI\Option(name: 'languages', description: 'A comma-separated list of language codes')]
#[CLI\Option(name: 'translations', description: 'A comma-separated list of language codes for translations.')]
#[CLI\Option(name: 'min-depth', description: 'The minimum depth of hierarchy for the new terms.')]
#[CLI\Option(name: 'max-depth', description: 'The maximum depth of hierarchy for the new terms.')]
#[Generator(id: 'term')]
public function terms(?string $num = '50', array $options = ['kill' => FALSE, 'bundles' => self::REQ, 'feedback' => '1000', 'languages' => self::REQ, 'translations' => self::REQ, 'min-depth' => '1', 'max-depth' => '4']): void {
$this->generate();
}
/**
* Create vocabularies.
*/
#[CLI\Command(name: self::VOCABS, aliases: ['genv', 'devel-generate-vocabs'])]
#[CLI\Argument(name: 'num', description: 'Number of vocabularies to generate.')]
#[Generator(id: 'vocabulary')]
#[CLI\ValidateModulesEnabled(modules: ['taxonomy'])]
#[CLI\Option(name: 'kill', description: 'Delete all vocabs before generating new ones.')]
public function vocabs(?string $num = '1', array $options = ['kill' => FALSE]): void {
$this->generate();
}
/**
* Create menus.
*/
#[CLI\Command(name: self::MENUS, aliases: ['genm', 'devel-generate-menus'])]
#[CLI\Argument(name: 'number_menus', description: 'Number of menus to generate.')]
#[CLI\Argument(name: 'number_links', description: 'Number of links to generate.')]
#[CLI\Argument(name: 'max_depth', description: 'Max link depth.')]
#[CLI\Argument(name: 'max_width', description: 'Max width of first level of links.')]
#[CLI\Option(name: 'kill', description: 'Delete any menus and menu links previously created by devel_generate before generating new ones.')]
#[Generator(id: 'menu')]
public function menus(?string $number_menus = '2', ?string $number_links = '50', ?string $max_depth = '3', string $max_width = '8', array $options = ['kill' => FALSE]): void {
$this->generate();
}
/**
* Create content.
*/
#[CLI\Command(name: self::CONTENT, aliases: ['genc', 'devel-generate-content'])]
#[CLI\ValidateModulesEnabled(modules: ['node'])]
#[CLI\Argument(name: 'num', description: 'Number of nodes to generate.')]
#[CLI\Argument(name: 'max_comments', description: 'Maximum number of comments to generate.')]
#[CLI\Option(name: 'kill', description: 'Delete all content before generating new content.')]
#[CLI\Option(name: 'bundles', description: 'A comma-delimited list of content types to create.')]
#[CLI\Option(name: 'authors', description: 'A comma delimited list of authors ids. Defaults to all users.')]
#[CLI\Option(name: 'roles', description: 'A comma delimited list of role machine names to filter the random selection of users. Defaults to all roles.')]
#[CLI\Option(name: 'feedback', description: 'An integer representing interval for insertion rate logging.')]
#[CLI\Option(name: 'skip-fields', description: 'A comma delimited list of fields to omit when generating random values')]
#[CLI\Option(name: 'base-fields', description: 'A comma delimited list of base field names to populate')]
#[CLI\Option(name: 'languages', description: 'A comma-separated list of language codes')]
#[CLI\Option(name: 'translations', description: 'A comma-separated list of language codes for translations.')]
#[CLI\Option(name: 'add-type-label', description: 'Add the content type label to the front of the node title')]
#[Generator(id: 'content')]
public function content(string $num = '50', ?string $max_comments = '0', array $options = ['kill' => FALSE, 'bundles' => 'page,article', 'authors' => self::REQ, 'roles' => self::REQ, 'feedback' => 1000, 'skip-fields' => self::REQ, 'base-fields' => self::REQ, 'languages' => self::REQ, 'translations' => self::REQ, 'add-type-label' => FALSE]): void {
$this->generate();
}
/**
* Create Block content blocks.
*/
#[CLI\Command(name: self::BLOCK_CONTENT, aliases: ['genbc', 'devel-generate-block-content'])]
#[CLI\ValidateModulesEnabled(modules: ['block_content'])]
#[CLI\Argument(name: 'num', description: 'Number of blocks to generate.')]
#[CLI\Option(name: 'kill', description: 'Delete all block content before generating new.')]
#[CLI\Option(name: 'block_types', description: 'A comma-delimited list of block content types to create.')]
#[CLI\Option(name: 'authors', description: 'A comma delimited list of authors ids. Defaults to all users.')]
#[CLI\Option(name: 'feedback', description: 'An integer representing interval for insertion rate logging.')]
#[CLI\Option(name: 'skip-fields', description: 'A comma delimited list of fields to omit when generating random values')]
#[CLI\Option(name: 'base-fields', description: 'A comma delimited list of base field names to populate')]
#[CLI\Option(name: 'languages', description: 'A comma-separated list of language codes')]
#[CLI\Option(name: 'translations', description: 'A comma-separated list of language codes for translations.')]
#[CLI\Option(name: 'add-type-label', description: 'Add the block type label to the front of the node title')]
#[CLI\Option(name: 'reusable', description: 'Create re-usable blocks. Disable for inline Layout Builder blocks, for example.')]
#[Generator(id: 'block_content')]
public function blockContent(?string $num = '50', array $options = ['kill' => FALSE, 'block_types' => 'basic', 'feedback' => 1000, 'skip-fields' => self::REQ, 'base-fields' => self::REQ, 'languages' => self::REQ, 'translations' => self::REQ, 'add-type-label' => FALSE, 'reusable' => TRUE]): void {
$this->generate();
}
/**
* Create media items.
*/
#[CLI\Command(name: self::MEDIA, aliases: ['genmd', 'devel-generate-media'])]
#[CLI\Argument(name: 'num', description: 'Number of media to generate.')]
#[CLI\Option(name: 'kill', description: 'Delete all media items before generating new.')]
#[CLI\Option(name: 'media_types', description: 'A comma-delimited list of media types to create.')]
#[CLI\Option(name: 'feedback', description: 'An integer representing interval for insertion rate logging.')]
#[CLI\Option(name: 'skip-fields', description: 'A comma delimited list of fields to omit when generating random values')]
#[CLI\Option(name: 'base-fields', description: 'A comma delimited list of base field names to populate')]
#[CLI\Option(name: 'languages', description: 'A comma-separated list of language codes')]
#[CLI\ValidateModulesEnabled(modules: ['media'])]
#[Generator(id: 'media')]
public function media(?string $num = '50', array $options = ['kill' => FALSE, 'media-types' => self::REQ, 'feedback' => 1000, 'skip-fields' => self::REQ, 'languages' => self::REQ, 'base-fields' => self::REQ]): void {
$this->generate();
}
/**
* The standard drush validate hook.
*
* @param \Consolidation\AnnotatedCommand\CommandData $commandData
* The data sent from the drush command.
*/
#[CLI\Hook(HookManager::ARGUMENT_VALIDATOR)]
public function validate(CommandData $commandData): void {
$manager = $this->manager;
$args = $commandData->input()->getArguments();
// The command name is the first argument but we do not need this.
array_shift($args);
/** @var \Drupal\devel_generate\DevelGenerateBaseInterface $instance */
$instance = $manager->createInstance($commandData->annotationData()->get('pluginId'), []);
$this->setPluginInstance($instance);
$parameters = $instance->validateDrushParams($args, $commandData->input()->getOptions());
$this->setParameters($parameters);
}
/**
* Wrapper for calling the plugin instance generate function.
*/
public function generate(): void {
$instance = $this->pluginInstance;
$instance->generate($this->parameters);
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace Drupal\devel_generate\Form;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\devel_generate\DevelGenerateBaseInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a form that allows privileged users to generate entities.
*/
class DevelGenerateForm extends FormBase {
/**
* The manager to be used for instantiating plugins.
*/
protected PluginManagerInterface $develGenerateManager;
/**
* Logger service.
*/
protected LoggerInterface $logger;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->develGenerateManager = $container->get('plugin.manager.develgenerate');
$instance->messenger = $container->get('messenger');
$instance->logger = $container->get('logger.channel.devel_generate');
$instance->requestStack = $container->get('request_stack');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'devel_generate_form_' . $this->getPluginIdFromRequest();
}
/**
* Returns the value of the param _plugin_id for the current request.
*
* @see \Drupal\devel_generate\Routing\DevelGenerateRouteSubscriber
*/
protected function getPluginIdFromRequest() {
$request = $this->requestStack->getCurrentRequest();
return $request->get('_plugin_id');
}
/**
* Returns a DevelGenerate plugin instance for a given plugin id.
*
* @param string $plugin_id
* The plugin_id for the plugin instance.
*
* @return \Drupal\devel_generate\DevelGenerateBaseInterface
* A DevelGenerate plugin instance.
*/
public function getPluginInstance(string $plugin_id): DevelGenerateBaseInterface {
return $this->develGenerateManager->createInstance($plugin_id, []);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state): array {
$plugin_id = $this->getPluginIdFromRequest();
$instance = $this->getPluginInstance($plugin_id);
$form = $instance->settingsForm($form, $form_state);
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Generate'),
'#button_type' => 'primary',
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state): void {
$plugin_id = $this->getPluginIdFromRequest();
$instance = $this->getPluginInstance($plugin_id);
$instance->settingsFormValidate($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
try {
$plugin_id = $this->getPluginIdFromRequest();
$instance = $this->getPluginInstance($plugin_id);
$instance->generate($form_state->getValues());
}
catch (\Exception $e) {
$this->logger->error($this->t('Failed to generate elements due to "%error".', ['%error' => $e->getMessage()]));
$this->messenger->addMessage($this->t('Failed to generate elements due to "%error".', ['%error' => $e->getMessage()]));
}
}
}

View File

@ -0,0 +1,493 @@
<?php
namespace Drupal\devel_generate\Plugin\DevelGenerate;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Extension\ExtensionPathResolver;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\block_content\BlockContentInterface;
use Drupal\content_translation\ContentTranslationManagerInterface;
use Drupal\devel_generate\DevelGenerateBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a BlockContentDevelGenerate plugin.
*
* @DevelGenerate(
* id = "block_content",
* label = @Translation("Block Content"),
* description = @Translation("Generate a given number of Block content blocks. Optionally delete current blocks."),
* url = "block-content",
* permission = "administer devel_generate",
* settings = {
* "num" = 50,
* "kill" = FALSE,
* "title_length" = 4,
* "add_type_label" = FALSE,
* "reusable" = TRUE
* },
* )
*/
class BlockContentDevelGenerate extends DevelGenerateBase implements ContainerFactoryPluginInterface {
/**
* The block content storage.
*/
protected EntityStorageInterface $blockContentStorage;
/**
* The block content type storage.
*/
protected EntityStorageInterface $blockContentTypeStorage;
/**
* The extension path resolver service.
*/
protected ExtensionPathResolver $extensionPathResolver;
/**
* The entity type bundle info service.
*/
protected EntityTypeBundleInfoInterface $entityTypeBundleInfo;
/**
* The content translation manager.
*/
protected ?ContentTranslationManagerInterface $contentTranslationManager;
/**
* The Drush batch flag.
*/
protected bool $drushBatch = FALSE;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
$entity_type_manager = $container->get('entity_type.manager');
// @phpstan-ignore ternary.alwaysTrue (False positive)
$content_translation_manager = $container->has('content_translation.manager') ? $container->get('content_translation.manager') : NULL;
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$instance->blockContentStorage = $entity_type_manager->getStorage('block_content');
$instance->blockContentTypeStorage = $entity_type_manager->getStorage('block_content_type');
$instance->extensionPathResolver = $container->get('extension.path.resolver');
$instance->entityTypeBundleInfo = $container->get('entity_type.bundle.info');
$instance->contentTranslationManager = $content_translation_manager;
return $instance;
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state): array {
/** @var \Drupal\block_content\BlockContentTypeInterface[] $blockTypes */
$blockTypes = $this->blockContentTypeStorage->loadMultiple();
$options = [];
foreach ($blockTypes as $type) {
$options[$type->id()] = [
'type' => [
'label' => $type->label(),
'description' => $type->getDescription(),
],
];
}
$header = [
'type' => $this->t('Block Content type'),
'description' => $this->t('Description'),
];
$form['block_types'] = [
'#type' => 'tableselect',
'#header' => $header,
'#options' => $options,
];
$form['kill'] = [
'#type' => 'checkbox',
'#title' => $this->t('<strong>Delete all content</strong> in these block types before generating new content.'),
'#default_value' => $this->getSetting('kill'),
];
$form['num'] = [
'#type' => 'number',
'#title' => $this->t('How many blocks would you like to generate?'),
'#default_value' => $this->getSetting('num'),
'#required' => TRUE,
'#min' => 0,
];
$form['title_length'] = [
'#type' => 'number',
'#title' => $this->t('Maximum number of words in block descriptions'),
'#default_value' => $this->getSetting('title_length'),
'#required' => TRUE,
'#min' => 1,
'#max' => 255,
];
$form['skip_fields'] = [
'#type' => 'textfield',
'#title' => $this->t('Fields to leave empty'),
'#description' => $this->t('Enter the field names as a comma-separated list. These will be skipped and have a default value in the generated content.'),
'#default_value' => NULL,
];
$form['base_fields'] = [
'#type' => 'textfield',
'#title' => $this->t('Base fields to populate'),
'#description' => $this->t('Enter the field names as a comma-separated list. These will be populated.'),
'#default_value' => NULL,
];
$form['reusable'] = [
'#type' => 'checkbox',
'#title' => $this->t('Reusable blocks'),
'#description' => $this->t('This will mark the blocks to be created as reusable.'),
'#default_value' => $this->getSetting('reusable'),
];
$form['add_type_label'] = [
'#type' => 'checkbox',
'#title' => $this->t('Prefix the title with the block type label.'),
'#description' => $this->t('This will not count against the maximum number of title words specified above.'),
'#default_value' => $this->getSetting('add_type_label'),
];
// Add the language and translation options.
$form += $this->getLanguageForm('blocks');
return $form;
}
/**
* {@inheritdoc}
*/
public function settingsFormValidate(array $form, FormStateInterface $form_state): void {
if (array_filter($form_state->getValue('block_types')) === []) {
$form_state->setErrorByName('block_types', $this->t('Please select at least one block type'));
}
$skip_fields = is_null($form_state->getValue('skip_fields')) ? [] : self::csvToArray($form_state->getValue('skip_fields'));
$base_fields = is_null($form_state->getValue('base_fields')) ? [] : self::csvToArray($form_state->getValue('base_fields'));
$form_state->setValue('skip_fields', $skip_fields);
$form_state->setValue('base_fields', $base_fields);
}
/**
* {@inheritdoc}
*/
public function validateDrushParams(array $args, array $options = []): array {
$add_language = self::csvToArray($options['languages']);
// Intersect with the enabled languages to make sure the language args
// passed are actually enabled.
$valid_languages = array_keys($this->languageManager->getLanguages(LanguageInterface::STATE_ALL));
$values['add_language'] = array_intersect($add_language, $valid_languages);
$translate_language = self::csvToArray($options['translations']);
$values['translate_language'] = array_intersect($translate_language, $valid_languages);
$values['add_type_label'] = $options['add-type-label'];
$values['kill'] = $options['kill'];
$values['feedback'] = $options['feedback'];
$values['skip_fields'] = is_null($options['skip-fields']) ? [] : self::csvToArray($options['skip-fields']);
$values['base_fields'] = is_null($options['base-fields']) ? [] : self::csvToArray($options['base-fields']);
$values['title_length'] = 6;
$values['num'] = array_shift($args);
$values['max_comments'] = array_shift($args);
$all_types = array_keys($this->blockContentGetBundles());
$selected_types = self::csvToArray($options['block_types']);
if ($selected_types === []) {
throw new \Exception(dt('No Block content types available'));
}
$values['block_types'] = array_combine($selected_types, $selected_types);
$block_types = array_filter($values['block_types']);
if (!empty($values['kill']) && $block_types === []) {
throw new \Exception(dt('To delete content, please provide the Block content types (--bundles)'));
}
// Checks for any missing block content types before generating blocks.
if (array_diff($block_types, $all_types) !== []) {
throw new \Exception(dt('One or more block content types have been entered that don\'t exist on this site'));
}
if ($this->isBatch($values['num'])) {
$this->drushBatch = TRUE;
}
return $values;
}
/**
* {@inheritdoc}
*/
protected function generateElements(array $values): void {
if ($this->isBatch($values['num'])) {
$this->generateBatchContent($values);
}
else {
$this->generateContent($values);
}
}
/**
* Generate content in batch mode.
*
* This method is used when the number of elements is 50 or more.
*/
private function generateBatchContent(array $values): void {
$operations = [];
// Remove unselected block content types.
$values['block_types'] = array_filter($values['block_types']);
// If it is drushBatch then this operation is already run in the
// self::validateDrushParams().
// Add the kill operation.
if ($values['kill']) {
$operations[] = [
'devel_generate_operation',
[$this, 'batchContentKill', $values],
];
}
// Add the operations to create the blocks.
for ($num = 0; $num < $values['num']; ++$num) {
$operations[] = [
'devel_generate_operation',
[$this, 'batchContentAddBlock', $values],
];
}
// Set the batch.
$batch = [
'title' => $this->t('Generating Content'),
'operations' => $operations,
'finished' => 'devel_generate_batch_finished',
'file' => $this->extensionPathResolver->getPath('module', 'devel_generate') . '/devel_generate.batch.inc',
];
batch_set($batch);
if ($this->drushBatch) {
drush_backend_batch_process();
}
}
/**
* Batch wrapper for calling ContentAddBlock.
*/
public function batchContentAddBlock(array $vars, array &$context): void {
if (!isset($context['results']['num'])) {
$context['results']['num'] = 0;
}
if ($this->drushBatch) {
++$context['results']['num'];
$this->develGenerateContentAddBlock($vars);
}
else {
$context['results'] = $vars;
$this->develGenerateContentAddBlock($context['results']);
}
if (!empty($vars['num_translations'])) {
$context['results']['num_translations'] += $vars['num_translations'];
}
}
/**
* Batch wrapper for calling ContentKill.
*/
public function batchContentKill(array $vars, array &$context): void {
if ($this->drushBatch) {
$this->contentKill($vars);
}
else {
$context['results'] = $vars;
$this->contentKill($context['results']);
}
}
/**
* Generate content when not in batch mode.
*
* This method is used when the number of elements is under 50.
*/
private function generateContent(array $values): void {
$values['block_types'] = array_filter($values['block_types']);
if (!empty($values['kill']) && $values['block_types']) {
$this->contentKill($values);
}
if (isset($values['block_types']) && $values['block_types'] !== []) {
$start = time();
$values['num_translations'] = 0;
for ($i = 1; $i <= $values['num']; ++$i) {
$this->develGenerateContentAddBlock($values);
if (isset($values['feedback']) && $i % $values['feedback'] == 0) {
$now = time();
$options = [
'@feedback' => $values['feedback'],
'@rate' => ($values['feedback'] * 60) / ($now - $start),
];
$this->messenger->addStatus(dt('Completed @feedback blocks (@rate blocks/min)', $options));
$start = $now;
}
}
}
$this->setMessage($this->formatPlural($values['num'], 'Created 1 block', 'Created @count blocks'));
if ($values['num_translations'] > 0) {
$this->setMessage($this->formatPlural($values['num_translations'], 'Created 1 block translation', 'Created @count block translations'));
}
}
/**
* Create one block. Used by both batch and non-batch code branches.
*
* @param array $results
* Results information.
*/
protected function develGenerateContentAddBlock(array &$results): void {
if (!isset($results['time_range'])) {
$results['time_range'] = 0;
}
$block_type = array_rand($results['block_types']);
// Add the block type label if required.
$title_prefix = $results['add_type_label'] ? $this->blockContentTypeStorage->load($block_type)->label() . ' - ' : '';
$values = [
'info' => $title_prefix . $this->getRandom()->sentences(mt_rand(1, $results['title_length']), TRUE),
'type' => $block_type,
// A flag to let hook_block_content_insert() implementations know that this is a generated block.
'devel_generate' => $results,
];
if (isset($results['add_language'])) {
$values['langcode'] = $this->getLangcode($results['add_language']);
}
if (isset($results['reusable'])) {
$values['reusable'] = (int) $results['reusable'];
}
/** @var \Drupal\block_content\BlockContentInterface $block */
$block = $this->blockContentStorage->create($values);
// Populate non-skipped fields with sample values.
$this->populateFields($block, $results['skip_fields'], $results['base_fields']);
// Remove the fields which are intended to have no value.
foreach ($results['skip_fields'] as $field) {
unset($block->$field);
}
$block->save();
// Add translations.
$this->develGenerateContentAddBlockTranslation($results, $block);
}
/**
* Create translation for the given block.
*
* @param array $results
* Results array.
* @param \Drupal\block_content\BlockContentInterface $block
* Block to add translations to.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
protected function develGenerateContentAddBlockTranslation(array &$results, BlockContentInterface $block): void {
if (empty($results['translate_language'])) {
return;
}
if (is_null($this->contentTranslationManager)) {
return;
}
if (!$this->contentTranslationManager->isEnabled('block_content', $block->bundle())) {
return;
}
if ($block->get('langcode')->getLangcode() === LanguageInterface::LANGCODE_NOT_SPECIFIED
|| $block->get('langcode')->getLangcode() === LanguageInterface::LANGCODE_NOT_APPLICABLE) {
return;
}
if (!isset($results['num_translations'])) {
$results['num_translations'] = 0;
}
// Translate the block to each target language.
$skip_languages = [
LanguageInterface::LANGCODE_NOT_SPECIFIED,
LanguageInterface::LANGCODE_NOT_APPLICABLE,
$block->get('langcode')->getLangcode(),
];
foreach ($results['translate_language'] as $langcode) {
if (in_array($langcode, $skip_languages)) {
continue;
}
$translation_block = $block->addTranslation($langcode);
$translation_block->setInfo($block->label() . ' (' . $langcode . ')');
$this->populateFields($translation_block);
$translation_block->save();
++$results['num_translations'];
}
}
/**
* Deletes all blocks of given block content types.
*
* @param array $values
* The input values from the settings form.
*/
protected function contentKill(array $values): void {
$bids = $this->blockContentStorage->getQuery()
->condition('type', $values['block_types'], 'IN')
->accessCheck(FALSE)
->execute();
if (!empty($bids)) {
$blocks = $this->blockContentStorage->loadMultiple($bids);
$this->blockContentStorage->delete($blocks);
$this->setMessage($this->t('Deleted %count blocks.', ['%count' => count($bids)]));
}
}
/**
* Determines if the content should be generated in batch mode.
*/
protected function isBatch($content_count): bool {
return $content_count >= 50;
}
/**
* Returns a list of available block content type names.
*
* This list can include types that are queued for addition or deletion.
*
* @return string[]
* An array of block content type labels,
* keyed by the block content type name.
*/
public function blockContentGetBundles(): array {
return array_map(static fn($bundle_info) => $bundle_info['label'], $this->entityTypeBundleInfo->getBundleInfo('block_content'));
}
}

View File

@ -0,0 +1,888 @@
<?php
namespace Drupal\devel_generate\Plugin\DevelGenerate;
use Drupal\comment\CommentManagerInterface;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Utility\Random;
use Drupal\content_translation\ContentTranslationManagerInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Extension\ExtensionPathResolver;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\devel_generate\DevelGenerateBase;
use Drupal\field\Entity\FieldConfig;
use Drupal\node\NodeInterface;
use Drupal\node\NodeStorageInterface;
use Drupal\path_alias\PathAliasStorage;
use Drupal\user\RoleStorageInterface;
use Drupal\user\UserStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a ContentDevelGenerate plugin.
*
* @DevelGenerate(
* id = "content",
* label = @Translation("content"),
* description = @Translation("Generate a given number of content. Optionally delete current content."),
* url = "content",
* permission = "administer devel_generate",
* settings = {
* "num" = 50,
* "kill" = FALSE,
* "max_comments" = 0,
* "title_length" = 4,
* "add_type_label" = FALSE
* },
* dependencies = {
* "node",
* },
* )
*/
class ContentDevelGenerate extends DevelGenerateBase implements ContainerFactoryPluginInterface {
/**
* The node storage.
*/
protected NodeStorageInterface $nodeStorage;
/**
* The node type storage.
*/
protected EntityStorageInterface $nodeTypeStorage;
/**
* The user storage.
*/
protected UserStorageInterface $userStorage;
/**
* The url generator service.
*/
protected UrlGeneratorInterface $urlGenerator;
/**
* The alias storage.
*/
protected PathAliasStorage $aliasStorage;
/**
* The date formatter service.
*/
protected DateFormatterInterface $dateFormatter;
/**
* Provides system time.
*/
protected TimeInterface $time;
/**
* Database connection.
*/
protected Connection $database;
/**
* The extension path resolver service.
*/
protected ExtensionPathResolver $extensionPathResolver;
/**
* The role storage.
*/
protected RoleStorageInterface $roleStorage;
/**
* The comment manager service.
*/
protected ?CommentManagerInterface $commentManager;
/**
* The content translation manager.
*/
protected ?ContentTranslationManagerInterface $contentTranslationManager;
/**
* The Drush batch flag.
*/
protected bool $drushBatch = FALSE;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
$entity_type_manager = $container->get('entity_type.manager');
// @phpstan-ignore ternary.alwaysTrue (False positive)
$comment_manager = $container->has('comment.manager') ? $container->get('comment.manager') : NULL;
// @phpstan-ignore ternary.alwaysTrue (False positive)
$content_translation_manager = $container->has('content_translation.manager') ? $container->get('content_translation.manager') : NULL;
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$instance->nodeTypeStorage = $entity_type_manager->getStorage('node_type');
$instance->nodeStorage = $entity_type_manager->getStorage('node');
$instance->userStorage = $entity_type_manager->getStorage('user');
$instance->urlGenerator = $container->get('url_generator');
$instance->aliasStorage = $entity_type_manager->getStorage('path_alias');
$instance->dateFormatter = $container->get('date.formatter');
$instance->time = $container->get('datetime.time');
$instance->database = $container->get('database');
$instance->extensionPathResolver = $container->get('extension.path.resolver');
$instance->roleStorage = $entity_type_manager->getStorage('user_role');
$instance->commentManager = $comment_manager;
$instance->contentTranslationManager = $content_translation_manager;
return $instance;
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state): array {
$types = $this->nodeTypeStorage->loadMultiple();
if (empty($types)) {
$create_url = $this->urlGenerator->generateFromRoute('node.type_add');
$this->messenger()->addMessage($this->t('You do not have any content types that can be generated. <a href=":create-type">Go create a new content type</a>', [':create-type' => $create_url]), 'error');
return [];
}
$options = [];
foreach ($types as $type) {
$options[$type->id()] = [
'type' => ['#markup' => $type->label()],
];
if ($this->commentManager instanceof CommentManagerInterface) {
$comment_fields = $this->commentManager->getFields('node');
$map = [$this->t('Hidden'), $this->t('Closed'), $this->t('Open')];
$fields = [];
foreach ($comment_fields as $field_name => $info) {
// Find all comment fields for the bundle.
if (in_array($type->id(), $info['bundles'])) {
$instance = FieldConfig::loadByName('node', $type->id(), $field_name);
$default_value = $instance->getDefaultValueLiteral();
$default_mode = reset($default_value);
$fields[] = new FormattableMarkup('@field: @state', [
'@field' => $instance->label(),
'@state' => $map[$default_mode['status']],
]);
}
}
// @todo Refactor display of comment fields.
if ($fields !== []) {
$options[$type->id()]['comments'] = [
'data' => [
'#theme' => 'item_list',
'#items' => $fields,
],
];
}
else {
$options[$type->id()]['comments'] = $this->t('No comment fields');
}
}
}
$header = [
'type' => $this->t('Content type'),
];
if ($this->commentManager instanceof CommentManagerInterface) {
$header['comments'] = [
'data' => $this->t('Comments'),
'class' => [RESPONSIVE_PRIORITY_MEDIUM],
];
}
$form['node_types'] = [
'#type' => 'tableselect',
'#header' => $header,
'#options' => $options,
];
$form['kill'] = [
'#type' => 'checkbox',
'#title' => $this->t('<strong>Delete all content</strong> in these content types before generating new content.'),
'#default_value' => $this->getSetting('kill'),
];
$form['num'] = [
'#type' => 'number',
'#title' => $this->t('How many nodes would you like to generate?'),
'#default_value' => $this->getSetting('num'),
'#required' => TRUE,
'#min' => 0,
];
$options = [1 => $this->t('Now')];
foreach ([3600, 86400, 604800, 2592000, 31536000] as $interval) {
$options[$interval] = $this->dateFormatter->formatInterval($interval, 1) . ' ' . $this->t('ago');
}
$form['time_range'] = [
'#type' => 'select',
'#title' => $this->t('How far back in time should the nodes be dated?'),
'#description' => $this->t('Node creation dates will be distributed randomly from the current time, back to the selected time.'),
'#options' => $options,
'#default_value' => 604800,
];
$form['max_comments'] = [
'#type' => $this->moduleHandler->moduleExists('comment') ? 'number' : 'value',
'#title' => $this->t('Maximum number of comments per node.'),
'#description' => $this->t('You must also enable comments for the content types you are generating. Note that some nodes will randomly receive zero comments. Some will receive the max.'),
'#default_value' => $this->getSetting('max_comments'),
'#min' => 0,
'#access' => $this->moduleHandler->moduleExists('comment'),
];
$form['title_length'] = [
'#type' => 'number',
'#title' => $this->t('Maximum number of words in titles'),
'#default_value' => $this->getSetting('title_length'),
'#required' => TRUE,
'#min' => 1,
'#max' => 255,
];
$form['skip_fields'] = [
'#type' => 'textfield',
'#title' => $this->t('Fields to leave empty'),
'#description' => $this->t('Enter the field names as a comma-separated list. These will be skipped and have a default value in the generated content.'),
'#default_value' => NULL,
];
$form['base_fields'] = [
'#type' => 'textfield',
'#title' => $this->t('Base fields to populate'),
'#description' => $this->t('Enter the field names as a comma-separated list. These will be populated.'),
'#default_value' => NULL,
];
$form['add_type_label'] = [
'#type' => 'checkbox',
'#title' => $this->t('Prefix the title with the content type label.'),
'#description' => $this->t('This will not count against the maximum number of title words specified above.'),
'#default_value' => $this->getSetting('add_type_label'),
];
$form['add_alias'] = [
'#type' => 'checkbox',
'#disabled' => !$this->moduleHandler->moduleExists('path'),
'#description' => $this->t('Requires path.module'),
'#title' => $this->t('Add an url alias for each node.'),
'#default_value' => FALSE,
];
$form['add_statistics'] = [
'#type' => 'checkbox',
'#title' => $this->t('Add statistics for each node (node_counter table).'),
'#default_value' => TRUE,
'#access' => $this->moduleHandler->moduleExists('statistics'),
];
// Add the language and translation options.
$form += $this->getLanguageForm('nodes');
// Add the user selection checkboxes.
$author_header = [
'id' => $this->t('User ID'),
'user' => $this->t('Name'),
'role' => $this->t('Role(s)'),
];
$num_users = $this->database->select('users')
->countQuery()
->execute()
->fetchField();
$author_form_limit = 50;
$query = $this->database->select('users', 'u')
->fields('u', ['uid'])
->range(0, $author_form_limit)
->orderBy('uid');
$uids = $query->execute()->fetchCol();
$author_rows = [];
foreach ($uids as $uid) {
/** @var \Drupal\user\UserInterface $user */
$user = $this->userStorage->load($uid);
$author_rows[$user->id()] = [
'id' => ['#markup' => $user->id()],
'user' => ['#markup' => $user->getAccountName()],
'role' => ['#markup' => implode(", ", $user->getRoles())],
];
}
$form['authors-wrap'] = [
'#type' => 'details',
'#title' => $this->t('Users'),
'#open' => FALSE,
'#description' => $this->t('Select users for randomly assigning as authors of the generated content.')
. ($num_users > $author_form_limit ? ' ' . $this->t('The site has @num_users users, only the first @$author_form_limit are shown and selectable here.', ['@num_users' => $num_users, '@$author_form_limit' => $author_form_limit]) : ''),
];
$form['authors-wrap']['authors'] = [
'#type' => 'tableselect',
'#header' => $author_header,
'#options' => $author_rows,
];
$role_rows = [];
$roles = array_map(static fn($role): string => $role->label(), $this->roleStorage->loadMultiple());
foreach ($roles as $role_id => $role_name) {
$role_rows[$role_id] = [
'id' => ['#markup' => $role_id],
'role' => ['#markup' => $role_name],
];
}
$form['authors-wrap']['roles'] = [
'#type' => 'tableselect',
'#header' => [
'id' => $this->t('Role ID'),
'role' => $this->t('Role Description'),
],
'#options' => $role_rows,
'#prefix' => $this->t('Specify the roles that randomly selected authors must have.'),
'#suffix' => $this->t('You can select users and roles. Authors will be randomly selected that match at least one of the criteria. Leave <em>both</em> selections unchecked to use a random selection of @$author_form_limit users, including Anonymous.', ['@$author_form_limit' => $author_form_limit]),
];
$form['#redirect'] = FALSE;
return $form;
}
/**
* {@inheritdoc}
*/
public function settingsFormValidate(array $form, FormStateInterface $form_state): void {
if (array_filter($form_state->getValue('node_types')) === []) {
$form_state->setErrorByName('node_types', $this->t('Please select at least one content type'));
}
$skip_fields = is_null($form_state->getValue('skip_fields')) ? [] : self::csvToArray($form_state->getValue('skip_fields'));
$base_fields = is_null($form_state->getValue('base_fields')) ? [] : self::csvToArray($form_state->getValue('base_fields'));
$form_state->setValue('skip_fields', $skip_fields);
$form_state->setValue('base_fields', $base_fields);
}
/**
* {@inheritdoc}
*/
protected function generateElements(array $values): void {
if ($this->isBatch($values['num'], $values['max_comments'])) {
$this->generateBatchContent($values);
}
else {
$this->generateContent($values);
}
}
/**
* Generate content when not in batch mode.
*
* This method is used when the number of elements is under 50.
*/
private function generateContent(array $values): void {
$values['node_types'] = array_filter($values['node_types']);
if (!empty($values['kill']) && $values['node_types']) {
$this->contentKill($values);
}
if ($values['node_types'] !== []) {
// Generate nodes.
$this->develGenerateContentPreNode($values);
$start = time();
$values['num_translations'] = 0;
for ($i = 1; $i <= $values['num']; ++$i) {
$this->develGenerateContentAddNode($values);
if (isset($values['feedback']) && $i % $values['feedback'] == 0) {
$now = time();
$options = [
'@feedback' => $values['feedback'],
'@rate' => ($values['feedback'] * 60) / ($now - $start),
];
$this->messenger->addStatus(dt('Completed @feedback nodes (@rate nodes/min)', $options));
$start = $now;
}
}
}
$this->messenger()->addMessage($this->formatPlural($values['num'], 'Created 1 node', 'Created @count nodes'));
if ($values['num_translations'] > 0) {
$this->messenger()->addMessage($this->formatPlural($values['num_translations'], 'Created 1 node translation', 'Created @count node translations'));
}
}
/**
* Generate content in batch mode.
*
* This method is used when the number of elements is 50 or more.
*/
private function generateBatchContent(array $values): void {
$operations = [];
// Remove unselected node types.
$values['node_types'] = array_filter($values['node_types']);
// If it is drushBatch then this operation is already run in the
// self::validateDrushParams().
if (!$this->drushBatch) {
// Setup the batch operations and save the variables.
$operations[] = [
'devel_generate_operation',
[$this, 'batchContentPreNode', $values],
];
}
// Add the kill operation.
if ($values['kill']) {
$operations[] = [
'devel_generate_operation',
[$this, 'batchContentKill', $values],
];
}
// Add the operations to create the nodes.
for ($num = 0; $num < $values['num']; ++$num) {
$operations[] = [
'devel_generate_operation',
[$this, 'batchContentAddNode', $values],
];
}
// Set the batch.
$batch = [
'title' => $this->t('Generating Content'),
'operations' => $operations,
'finished' => 'devel_generate_batch_finished',
'file' => $this->extensionPathResolver->getPath('module', 'devel_generate') . '/devel_generate.batch.inc',
];
batch_set($batch);
if ($this->drushBatch) {
drush_backend_batch_process();
}
}
/**
* Batch wrapper for calling ContentPreNode.
*/
public function batchContentPreNode($vars, array &$context): void {
$context['results'] = $vars;
$context['results']['num'] = 0;
$context['results']['num_translations'] = 0;
$this->develGenerateContentPreNode($context['results']);
}
/**
* Batch wrapper for calling ContentAddNode.
*/
public function batchContentAddNode(array $vars, array &$context): void {
if ($this->drushBatch) {
$this->develGenerateContentAddNode($vars);
}
else {
$this->develGenerateContentAddNode($context['results']);
}
if (!isset($context['results']['num'])) {
$context['results']['num'] = 0;
}
++$context['results']['num'];
if (!empty($vars['num_translations'])) {
$context['results']['num_translations'] += $vars['num_translations'];
}
}
/**
* Batch wrapper for calling ContentKill.
*/
public function batchContentKill(array $vars, array &$context): void {
if ($this->drushBatch) {
$this->contentKill($vars);
}
else {
$this->contentKill($context['results']);
}
}
/**
* {@inheritdoc}
*/
public function validateDrushParams(array $args, array $options = []): array {
$add_language = self::csvToArray($options['languages']);
// Intersect with the enabled languages to make sure the language args
// passed are actually enabled.
$valid_languages = array_keys($this->languageManager->getLanguages(LanguageInterface::STATE_ALL));
$values['add_language'] = array_intersect($add_language, $valid_languages);
$translate_language = self::csvToArray($options['translations']);
$values['translate_language'] = array_intersect($translate_language, $valid_languages);
$values['add_type_label'] = $options['add-type-label'];
$values['kill'] = $options['kill'];
$values['feedback'] = $options['feedback'];
$values['skip_fields'] = is_null($options['skip-fields']) ? [] : self::csvToArray($options['skip-fields']);
$values['base_fields'] = is_null($options['base-fields']) ? [] : self::csvToArray($options['base-fields']);
$values['title_length'] = 6;
$values['num'] = (int) trim(array_shift($args));
$values['max_comments'] = (int) array_shift($args);
// Do not use csvToArray for 'authors' because it removes '0' values.
$values['authors'] = is_null($options['authors']) ? [] : explode(',', $options['authors']);
$values['roles'] = self::csvToArray($options['roles']);
$all_types = array_keys(node_type_get_names());
$default_types = array_intersect(['page', 'article'], $all_types);
$selected_types = self::csvToArray($options['bundles'] ?: $default_types);
if ($selected_types === []) {
throw new \Exception(dt('No content types available'));
}
$values['node_types'] = array_combine($selected_types, $selected_types);
$node_types = array_filter($values['node_types']);
if (!empty($values['kill']) && $node_types === []) {
throw new \Exception(dt('To delete content, please provide the content types (--bundles)'));
}
// Checks for any missing content types before generating nodes.
if (array_diff($node_types, $all_types) !== []) {
throw new \Exception(dt('One or more content types have been entered that don\'t exist on this site'));
}
if ($this->isBatch((int) $values['num'], (int) $values['max_comments'])) {
$this->drushBatch = TRUE;
$this->develGenerateContentPreNode($values);
}
return $values;
}
/**
* Determines if the content should be generated in batch mode.
*/
protected function isBatch(int $content_count, int $comment_count): bool {
return $content_count >= 50 || $comment_count >= 10;
}
/**
* Deletes all nodes of given node types.
*
* @param array $values
* The input values from the settings form.
*/
protected function contentKill(array $values): void {
$nids = $this->nodeStorage->getQuery()
->condition('type', $values['node_types'], 'IN')
->accessCheck(FALSE)
->execute();
if (!empty($nids)) {
$nodes = $this->nodeStorage->loadMultiple($nids);
$this->nodeStorage->delete($nodes);
$this->messenger()->addMessage($this->t('Deleted @count nodes.', ['@count' => count($nids)]));
}
}
/**
* Preprocesses $results before adding content.
*
* @param array $results
* Results information.
*/
protected function develGenerateContentPreNode(array &$results): void {
$authors = $results['authors'];
// Remove non-selected users. !== 0 will leave the Anonymous user in if it
// was selected on the form or entered in the drush parameters.
$authors = array_filter($authors, static fn($k): bool => $k !== 0);
// Likewise remove non-selected roles.
$roles = $results['roles'];
$roles = array_filter($roles, static fn($k): bool => $k !== 0);
// If specific roles have been selected then also add up to 50 users who
// have one of these roles. There is no direct way randomise the selection
// using entity queries, so we use a database query instead.
if ($roles !== [] && !in_array('authenticated', $roles)) {
$query = $this->database->select('user__roles', 'ur')
->fields('ur', ['entity_id', 'roles_target_id'])
->condition('roles_target_id', $roles, 'in')
->range(0, 50)
->orderRandom();
$uids = array_unique($query->execute()->fetchCol());
// If the 'anonymous' role is selected, then add '0' to the user ids. Also
// do this if no users were specified and none were found with the role(s)
// requested. This makes it clear that no users were found. It would be
// worse to fall through and select completely random users who do not
// have any of the roles requested.
if (in_array('anonymous', $roles) || ($authors === [] && $uids === [])) {
$uids[] = '0';
}
$authors = array_unique(array_merge($authors, $uids));
}
// If still no authors have been collected, or the 'authenticated' role was
// requested then add a random set of users up to a maximum of 50.
if ($authors === [] || in_array('authenticated', $roles)) {
$query = $this->database->select('users', 'u')
->fields('u', ['uid'])
->range(0, 50)
->orderRandom();
$uids = $query->execute()->fetchCol();
$authors = array_unique(array_merge($authors, $uids));
}
$results['users'] = $authors;
}
/**
* Create one node. Used by both batch and non-batch code branches.
*
* @param array $results
* Results information.
*/
protected function develGenerateContentAddNode(array &$results): void {
if (!isset($results['time_range'])) {
$results['time_range'] = 0;
}
$users = $results['users'];
$node_type = array_rand($results['node_types']);
$uid = $users[array_rand($users)];
// Add the content type label if required.
$title_prefix = $results['add_type_label'] ? $this->nodeTypeStorage->load($node_type)->label() . ' - ' : '';
$values = [
'nid' => NULL,
'type' => $node_type,
'title' => $title_prefix . $this->getRandom()->sentences(mt_rand(1, $results['title_length']), TRUE),
'uid' => $uid,
'revision' => mt_rand(0, 1),
'moderation_state' => 'published',
'status' => TRUE,
'promote' => mt_rand(0, 1),
'created' => $this->time->getRequestTime() - mt_rand(0, $results['time_range']),
// A flag to let hook_node_insert() implementations know that this is a
// generated node.
'devel_generate' => $results,
];
if (isset($results['add_language'])) {
$values['langcode'] = $this->getLangcode($results['add_language']);
}
/** @var \Drupal\node\NodeInterface $node */
$node = $this->nodeStorage->create($values);
// Populate non-skipped fields with sample values.
$this->populateFields($node, $results['skip_fields'], $results['base_fields']);
// Remove the fields which are intended to have no value.
foreach ($results['skip_fields'] as $field) {
unset($node->$field);
}
$node->save();
$this->insertNodeData($node);
// Add url alias if required.
if (!empty($results['add_alias'])) {
$path_alias = $this->aliasStorage->create([
'path' => '/node/' . $node->id(),
'alias' => '/node-' . $node->id() . '-' . $node->bundle(),
'langcode' => $values['langcode'] ?? LanguageInterface::LANGCODE_NOT_SPECIFIED,
]);
$path_alias->save();
}
// Add translations.
$this->develGenerateContentAddNodeTranslation($results, $node);
}
/**
* Create translation for the given node.
*
* @param array $results
* Results array.
* @param \Drupal\node\NodeInterface $node
* Node to add translations to.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
protected function develGenerateContentAddNodeTranslation(array &$results, NodeInterface $node): void {
if (empty($results['translate_language'])) {
return;
}
if (is_null($this->contentTranslationManager)) {
return;
}
if (!$this->contentTranslationManager->isEnabled('node', $node->getType())) {
return;
}
if ($node->get('langcode')->getLangcode() === LanguageInterface::LANGCODE_NOT_SPECIFIED
|| $node->get('langcode')->getLangcode() === LanguageInterface::LANGCODE_NOT_APPLICABLE) {
return;
}
if (!isset($results['num_translations'])) {
$results['num_translations'] = 0;
}
// Translate node to each target language.
$skip_languages = [
LanguageInterface::LANGCODE_NOT_SPECIFIED,
LanguageInterface::LANGCODE_NOT_APPLICABLE,
$node->get('langcode')->getLangcode(),
];
foreach ($results['translate_language'] as $langcode) {
if (in_array($langcode, $skip_languages)) {
continue;
}
$translation_node = $node->addTranslation($langcode);
$translation_node->setTitle($node->getTitle() . ' (' . $langcode . ')');
$this->populateFields($translation_node);
$translation_node->save();
if ($translation_node->id() > 0 && !empty($results['add_alias'])) {
$path_alias = $this->aliasStorage->create([
'path' => '/node/' . $translation_node->id(),
'alias' => '/node-' . $translation_node->id() . '-' . $translation_node->bundle() . '-' . $langcode,
'langcode' => $langcode,
]);
$path_alias->save();
}
++$results['num_translations'];
}
}
private function insertNodeData(NodeInterface $node): void {
if (!isset($node->devel_generate)) {
return;
}
$results = $node->devel_generate;
if (!empty($results['max_comments'])) {
foreach ($node->getFieldDefinitions() as $field_name => $field_definition) {
if ($field_definition->getType() !== 'comment') {
continue;
}
if ($node->get($field_name)->getValue()[0]['status'] !== CommentItemInterface::OPEN) {
continue;
}
// Add comments for each comment field on entity.
$this->addNodeComments($node, $field_definition, $results['users'], $results['max_comments'], $results['title_length']);
}
}
if (!empty($results['add_statistics'])) {
$this->addNodeStatistics($node);
}
}
/**
* Create comments and add them to a node.
*
* @param \Drupal\node\NodeInterface $node
* Node to add comments to.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field storage definition.
* @param array $users
* Array of users to assign comment authors.
* @param int $max_comments
* Max number of comments to generate per node.
* @param int $title_length
* Max length of the title of the comments.
*/
private function addNodeComments(NodeInterface $node, FieldDefinitionInterface $field_definition, array $users, int $max_comments, int $title_length = 8): void {
$parents = [];
$commentStorage = $this->entityTypeManager->getStorage('comment');
$field_name = $field_definition->getName();
$num_comments = mt_rand(0, $max_comments);
for ($i = 1; $i <= $num_comments; ++$i) {
$query = $commentStorage->getQuery();
switch ($i % 3) {
case 0:
// No parent.
case 1:
// Top level parent.
$parents = $query
->condition('pid', 0)
->condition('entity_id', $node->id())
->condition('entity_type', 'node')
->condition('field_name', $field_name)
->range(0, 1)
->accessCheck(FALSE)
->execute();
break;
case 2:
// Non top level parent.
$parents = $query
->condition('pid', 0, '>')
->condition('entity_id', $node->id())
->condition('entity_type', 'node')
->condition('field_name', $field_name)
->range(0, 1)
->accessCheck(FALSE)
->execute();
break;
}
$random = new Random();
$stub = [
'entity_type' => $node->getEntityTypeId(),
'entity_id' => $node->id(),
'field_name' => $field_name,
'name' => 'devel generate',
'mail' => 'devel_generate@example.com',
'timestamp' => mt_rand($node->getCreatedTime(), $this->time->getRequestTime()),
'subject' => substr($random->sentences(mt_rand(1, $title_length), TRUE), 0, 63),
'uid' => $users[array_rand($users)],
'langcode' => $node->language()->getId(),
];
if ($parents) {
$stub['pid'] = current($parents);
}
$comment = $commentStorage->create($stub);
// Populate all core fields.
$this->populateFields($comment);
$comment->save();
}
}
/**
* Generate statistics information for a node.
*
* @param \Drupal\node\NodeInterface $node
* A node object.
*/
private function addNodeStatistics(NodeInterface $node): void {
if (!$this->moduleHandler->moduleExists('statistics')) {
return;
}
$statistic = [
'nid' => $node->id(),
'totalcount' => mt_rand(0, 500),
'timestamp' => $this->time->getRequestTime() - mt_rand(0, $node->getCreatedTime()),
];
$statistic['daycount'] = mt_rand(0, $statistic['totalcount']);
$this->database->insert('node_counter')->fields($statistic)->execute();
}
}

View File

@ -0,0 +1,534 @@
<?php
namespace Drupal\devel_generate\Plugin\DevelGenerate;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\ContentEntityStorageInterface;
use Drupal\Core\Extension\ExtensionPathResolver;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\devel_generate\DevelGenerateBase;
use Drupal\user\UserStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a plugin that generates media entities.
*
* @DevelGenerate(
* id = "media",
* label = @Translation("media"),
* description = @Translation("Generate a given number of media entities."),
* url = "media",
* permission = "administer devel_generate",
* settings = {
* "num" = 50,
* "kill" = FALSE,
* "name_length" = 4,
* },
* dependencies = {
* "media",
* },
* )
*/
class MediaDevelGenerate extends DevelGenerateBase implements ContainerFactoryPluginInterface {
/**
* The media entity storage.
*/
protected ContentEntityStorageInterface $mediaStorage;
/**
* The media type entity storage.
*/
protected ConfigEntityStorageInterface $mediaTypeStorage;
/**
* The user entity storage.
*/
protected UserStorageInterface $userStorage;
/**
* The url generator service.
*/
protected UrlGeneratorInterface $urlGenerator;
/**
* The date formatter service.
*/
protected DateFormatterInterface $dateFormatter;
/**
* The system time service.
*/
protected TimeInterface $time;
/**
* The extension path resolver service.
*/
protected ExtensionPathResolver $extensionPathResolver;
/**
* The Drush batch flag.
*/
protected bool $drushBatch = FALSE;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
$entity_type_manager = $container->get('entity_type.manager');
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$instance->mediaStorage = $entity_type_manager->getStorage('media');
$instance->mediaTypeStorage = $entity_type_manager->getStorage('media_type');
$instance->userStorage = $entity_type_manager->getStorage('user');
$instance->urlGenerator = $container->get('url_generator');
$instance->dateFormatter = $container->get('date.formatter');
$instance->time = $container->get('datetime.time');
$instance->extensionPathResolver = $container->get('extension.path.resolver');
return $instance;
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state): array {
$types = $this->mediaTypeStorage->loadMultiple();
if (empty($types)) {
$create_url = $this->urlGenerator->generateFromRoute('entity.media_type.add_form');
$this->setMessage($this->t('You do not have any media types that can be generated. <a href=":url">Go create a new media type</a>', [
':url' => $create_url,
]), MessengerInterface::TYPE_ERROR);
return [];
}
$options = [];
foreach ($types as $type) {
$options[$type->id()] = ['type' => ['#markup' => $type->label()]];
}
$form['media_types'] = [
'#type' => 'tableselect',
'#header' => ['type' => $this->t('Media type')],
'#options' => $options,
];
$form['kill'] = [
'#type' => 'checkbox',
'#title' => $this->t('<strong>Delete all media</strong> in these types before generating new media.'),
'#default_value' => $this->getSetting('kill'),
];
$form['num'] = [
'#type' => 'number',
'#title' => $this->t('How many media items would you like to generate?'),
'#default_value' => $this->getSetting('num'),
'#required' => TRUE,
'#min' => 0,
];
$options = [1 => $this->t('Now')];
foreach ([3600, 86400, 604800, 2592000, 31536000] as $interval) {
$options[$interval] = $this->dateFormatter->formatInterval($interval, 1) . ' ' . $this->t('ago');
}
$form['time_range'] = [
'#type' => 'select',
'#title' => $this->t('How far back in time should the media be dated?'),
'#description' => $this->t('Media creation dates will be distributed randomly from the current time, back to the selected time.'),
'#options' => $options,
'#default_value' => 604800,
];
$form['name_length'] = [
'#type' => 'number',
'#title' => $this->t('Maximum number of words in names'),
'#default_value' => $this->getSetting('name_length'),
'#required' => TRUE,
'#min' => 1,
'#max' => 255,
];
$form['skip_fields'] = [
'#type' => 'textfield',
'#title' => $this->t('Fields to leave empty'),
'#description' => $this->t('Enter the field names as a comma-separated list. These will be skipped and have a default value in the generated content.'),
'#default_value' => NULL,
];
$form['base_fields'] = [
'#type' => 'textfield',
'#title' => $this->t('Base fields to populate'),
'#description' => $this->t('Enter the field names as a comma-separated list. These will be populated.'),
'#default_value' => NULL,
];
$options = [];
// We always need a language.
$languages = $this->languageManager->getLanguages(LanguageInterface::STATE_ALL);
foreach ($languages as $langcode => $language) {
$options[$langcode] = $language->getName();
}
$form['add_language'] = [
'#type' => 'select',
'#title' => $this->t('Set language on media'),
'#multiple' => TRUE,
'#description' => $this->t('Requires locale.module'),
'#options' => $options,
'#default_value' => [
$this->languageManager->getDefaultLanguage()->getId(),
],
];
$form['#redirect'] = FALSE;
return $form;
}
/**
* {@inheritdoc}
*/
public function settingsFormValidate(array $form, FormStateInterface $form_state): void {
// Remove the media types not selected.
$media_types = array_filter($form_state->getValue('media_types'));
if ($media_types === []) {
$form_state->setErrorByName('media_types', $this->t('Please select at least one media type'));
}
// Store the normalized value back, in form state.
$form_state->setValue('media_types', array_combine($media_types, $media_types));
$skip_fields = is_null($form_state->getValue('skip_fields')) ? [] : self::csvToArray($form_state->getValue('skip_fields'));
$base_fields = is_null($form_state->getValue('base_fields')) ? [] : self::csvToArray($form_state->getValue('base_fields'));
$form_state->setValue('skip_fields', $skip_fields);
$form_state->setValue('base_fields', $base_fields);
}
/**
* {@inheritdoc}
*/
protected function generateElements(array $values): void {
if ($this->isBatch((int) $values['num'])) {
$this->generateBatchMedia($values);
}
else {
$this->generateMedia($values);
}
}
/**
* Method for creating media when number of elements is less than 50.
*
* @param array $values
* Array of values submitted through a form.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* Thrown if the storage handler couldn't be loaded.
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* Thrown if the entity type doesn't exist.
* @throws \Drupal\Core\Entity\EntityStorageException
* Thrown if the bundle does not exist or was needed but not specified.
*/
protected function generateMedia(array $values): void {
if (!empty($values['kill']) && $values['media_types']) {
$this->mediaKill($values);
}
if (!empty($values['media_types'])) {
// Generate media items.
$this->preGenerate($values);
$start = time();
for ($i = 1; $i <= $values['num']; ++$i) {
$this->createMediaItem($values);
if (isset($values['feedback']) && $i % $values['feedback'] == 0) {
$now = time();
$this->messenger->addStatus(dt('Completed !feedback media items (!rate media/min)', [
'!feedback' => $values['feedback'],
'!rate' => ($values['feedback'] * 60) / ($now - $start),
]));
$start = $now;
}
}
}
$this->setMessage($this->formatPlural($values['num'], '1 media item created.', 'Finished creating @count media items.'));
}
/**
* Method for creating media when number of elements is greater than 50.
*
* @param array $values
* The input values from the settings form.
*/
protected function generateBatchMedia(array $values): void {
$operations = [];
// Setup the batch operations and save the variables.
$operations[] = [
'devel_generate_operation',
[$this, 'batchPreGenerate', $values],
];
// Add the kill operation.
if ($values['kill']) {
$operations[] = [
'devel_generate_operation',
[$this, 'batchMediaKill', $values],
];
}
// Add the operations to create the media.
for ($num = 0; $num < $values['num']; ++$num) {
$operations[] = [
'devel_generate_operation',
[$this, 'batchCreateMediaItem', $values],
];
}
// Start the batch.
$batch = [
'title' => $this->t('Generating media items'),
'operations' => $operations,
'finished' => 'devel_generate_batch_finished',
'file' => $this->extensionPathResolver->getPath('module', 'devel_generate') . '/devel_generate.batch.inc',
];
batch_set($batch);
if ($this->drushBatch) {
drush_backend_batch_process();
}
}
/**
* Provides a batch version of preGenerate().
*
* @param array $vars
* The input values from the settings form.
* @param iterable $context
* Batch job context.
*
* @see self::preGenerate()
*/
public function batchPreGenerate(array $vars, iterable &$context): void {
$context['results'] = $vars;
$context['results']['num'] = 0;
$this->preGenerate($context['results']);
}
/**
* Provides a batch version of createMediaItem().
*
* @param array $vars
* The input values from the settings form.
* @param iterable $context
* Batch job context.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* Thrown if the storage handler couldn't be loaded.
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* Thrown if the entity type doesn't exist.
* @throws \Drupal\Core\Entity\EntityStorageException
* Thrown if the bundle does not exist or was needed but not specified.
*
* @see self::createMediaItem()
*/
public function batchCreateMediaItem(array $vars, iterable &$context): void {
if ($this->drushBatch) {
$this->createMediaItem($vars);
}
else {
$this->createMediaItem($context['results']);
}
if (!isset($context['results']['num'])) {
$context['results']['num'] = 0;
}
++$context['results']['num'];
}
/**
* Provides a batch version of mediaKill().
*
* @param array $vars
* The input values from the settings form.
* @param iterable $context
* Batch job context.
*
* @see self::mediaKill()
*/
public function batchMediaKill(array $vars, iterable &$context): void {
if ($this->drushBatch) {
$this->mediaKill($vars);
}
else {
$this->mediaKill($context['results']);
}
}
/**
* {@inheritdoc}
*/
public function validateDrushParams(array $args, array $options = []): array {
$add_language = $options['languages'];
if (!empty($add_language)) {
$add_language = explode(',', str_replace(' ', '', $add_language));
// Intersect with the enabled languages to make sure the language args
// passed are actually enabled.
$values['values']['add_language'] = array_intersect($add_language, array_keys($this->languageManager->getLanguages(LanguageInterface::STATE_ALL)));
}
$values['kill'] = $options['kill'];
$values['feedback'] = $options['feedback'];
$values['name_length'] = 6;
$values['num'] = (int) array_shift($args);
$values['skip_fields'] = is_null($options['skip-fields']) ? [] : self::csvToArray($options['skip-fields']);
$values['base_fields'] = is_null($options['base-fields']) ? [] : self::csvToArray($options['base-fields']);
$all_media_types = array_values($this->mediaTypeStorage->getQuery()->accessCheck(FALSE)->execute());
$requested_media_types = self::csvToArray($options['media-types'] ?: $all_media_types);
if ($requested_media_types === []) {
throw new \Exception(dt('No media types available'));
}
// Check for any missing media type.
if (($invalid_media_types = array_diff($requested_media_types, $all_media_types)) !== []) {
throw new \Exception("Requested media types don't exists: " . implode(', ', $invalid_media_types));
}
$values['media_types'] = array_combine($requested_media_types, $requested_media_types);
if ($this->isBatch($values['num'])) {
$this->drushBatch = TRUE;
$this->preGenerate($values);
}
return $values;
}
/**
* Deletes all media of given media media types.
*
* @param array $values
* The input values from the settings form.
*
* @throws \Drupal\Core\Entity\EntityStorageException
* Thrown if the media type does not exist.
*/
protected function mediaKill(array $values): void {
$mids = $this->mediaStorage->getQuery()
->condition('bundle', $values['media_types'], 'IN')
->accessCheck(FALSE)
->execute();
if (!empty($mids)) {
$media = $this->mediaStorage->loadMultiple($mids);
$this->mediaStorage->delete($media);
$this->setMessage($this->t('Deleted %count media items.', ['%count' => count($mids)]));
}
}
/**
* Code to be run before generating items.
*
* Returns the same array passed in as parameter, but with an array of uids
* for the key 'users'.
*
* @param array $results
* The input values from the settings form.
*/
protected function preGenerate(array &$results): void {
// Get user id.
$users = array_values($this->userStorage->getQuery()
->range(0, 50)
->accessCheck(FALSE)
->execute());
$users = array_merge($users, ['0']);
$results['users'] = $users;
}
/**
* Create one media item. Used by both batch and non-batch code branches.
*
* @param array $results
* The input values from the settings form.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* Thrown if the storage handler couldn't be loaded.
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* Thrown if the entity type doesn't exist.
* @throws \Drupal\Core\Entity\EntityStorageException
* Thrown if the bundle does not exist or was needed but not specified.
*/
protected function createMediaItem(array &$results): void {
if (!isset($results['time_range'])) {
$results['time_range'] = 0;
}
$media_type = array_rand($results['media_types']);
$uid = $results['users'][array_rand($results['users'])];
$media = $this->mediaStorage->create([
'bundle' => $media_type,
'name' => $this->getRandom()->sentences(mt_rand(1, $results['name_length']), TRUE),
'uid' => $uid,
'revision' => mt_rand(0, 1),
'status' => TRUE,
'moderation_state' => 'published',
'created' => $this->time->getRequestTime() - mt_rand(0, $results['time_range']),
'langcode' => $this->getLangcode($results),
// A flag to let hook implementations know that this is a generated item.
'devel_generate' => $results,
]);
// Populate all non-skipped fields with sample values.
$this->populateFields($media, $results['skip_fields'], $results['base_fields']);
// Remove the fields which are intended to have no value.
foreach ($results['skip_fields'] as $field) {
unset($media->$field);
}
$media->save();
}
/**
* Determine language based on $results.
*
* @param array $results
* The input values from the settings form.
*
* @return string
* The language code.
*/
protected function getLangcode(array $results): string {
if (isset($results['add_language'])) {
$langcodes = $results['add_language'];
return $langcodes[array_rand($langcodes)];
}
return $this->languageManager->getDefaultLanguage()->getId();
}
/**
* Finds out if the media item generation will run in batch process.
*
* @param int $media_items_count
* Number of media items to be generated.
*
* @return bool
* If the process should be a batch process.
*/
protected function isBatch(int $media_items_count): bool {
return $media_items_count >= 50;
}
}

View File

@ -0,0 +1,383 @@
<?php
namespace Drupal\devel_generate\Plugin\DevelGenerate;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Menu\MenuLinkTreeInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\devel_generate\DevelGenerateBase;
use Drupal\menu_link_content\MenuLinkContentStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a MenuDevelGenerate plugin.
*
* @DevelGenerate(
* id = "menu",
* label = @Translation("menus"),
* description = @Translation("Generate a given number of menus and menu
* links. Optionally delete current menus."), url = "menu", permission =
* "administer devel_generate", settings = {
* "num_menus" = 2,
* "num_links" = 50,
* "title_length" = 12,
* "max_width" = 6,
* "kill" = FALSE,
* }
* )
*/
class MenuDevelGenerate extends DevelGenerateBase implements ContainerFactoryPluginInterface {
/**
* The menu tree service.
*/
protected MenuLinkTreeInterface $menuLinkTree;
/**
* The menu storage.
*/
protected EntityStorageInterface $menuStorage;
/**
* The menu link storage.
*/
protected MenuLinkContentStorageInterface $menuLinkContentStorage;
/**
* Database connection.
*/
protected Connection $database;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
$entity_type_manager = $container->get('entity_type.manager');
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$instance->menuLinkTree = $container->get('menu.link_tree');
$instance->menuStorage = $entity_type_manager->getStorage('menu');
$instance->menuLinkContentStorage = $entity_type_manager->getStorage('menu_link_content');
$instance->database = $container->get('database');
return $instance;
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state): array {
$menus = array_map(static fn($menu) => $menu->label(), $this->menuStorage->loadMultiple());
asort($menus);
$menus = ['__new-menu__' => $this->t('Create new menu(s)')] + $menus;
$form['existing_menus'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Generate links for these menus'),
'#options' => $menus,
'#default_value' => ['__new-menu__'],
'#required' => TRUE,
];
$form['num_menus'] = [
'#type' => 'number',
'#title' => $this->t('Number of new menus to create'),
'#default_value' => $this->getSetting('num_menus'),
'#min' => 0,
'#states' => [
'visible' => [
':input[name="existing_menus[__new-menu__]"]' => ['checked' => TRUE],
],
],
];
$form['num_links'] = [
'#type' => 'number',
'#title' => $this->t('Number of links to generate'),
'#default_value' => $this->getSetting('num_links'),
'#required' => TRUE,
'#min' => 0,
];
$form['title_length'] = [
'#type' => 'number',
'#title' => $this->t('Maximum length for menu titles and menu links'),
'#description' => $this->t('Text will be generated at random lengths up to this value. Enter a number between 2 and 128.'),
'#default_value' => $this->getSetting('title_length'),
'#required' => TRUE,
'#min' => 2,
'#max' => 128,
];
$form['link_types'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Types of links to generate'),
'#options' => [
'node' => $this->t('Nodes'),
'front' => $this->t('Front page'),
'external' => $this->t('External'),
],
'#default_value' => ['node', 'front', 'external'],
'#required' => TRUE,
];
$form['max_depth'] = [
'#type' => 'select',
'#title' => $this->t('Maximum link depth'),
'#options' => range(0, $this->menuLinkTree->maxDepth()),
'#default_value' => floor($this->menuLinkTree->maxDepth() / 2),
'#required' => TRUE,
];
unset($form['max_depth']['#options'][0]);
$form['max_width'] = [
'#type' => 'number',
'#title' => $this->t('Maximum menu width'),
'#default_value' => $this->getSetting('max_width'),
'#description' => $this->t("Limit the width of the generated menu's first level of links to a certain number of items."),
'#required' => TRUE,
'#min' => 0,
];
$form['kill'] = [
'#type' => 'checkbox',
'#title' => $this->t('Delete existing custom generated menus and menu links before generating new ones.'),
'#default_value' => $this->getSetting('kill'),
];
return $form;
}
/**
* {@inheritdoc}
*/
protected function generateElements(array $values): void {
// If the create new menus checkbox is off, set the number of menus to 0.
if (!isset($values['existing_menus']['__new-menu__']) || !$values['existing_menus']['__new-menu__']) {
$values['num_menus'] = 0;
}
else {
// Unset the aux menu to avoid attach menu new items.
unset($values['existing_menus']['__new-menu__']);
}
// Delete custom menus.
if ($values['kill']) {
[$menus_deleted, $links_deleted] = $this->deleteMenus();
$this->setMessage($this->t('Deleted @menus_deleted menu(s) and @links_deleted other link(s).',
[
'@menus_deleted' => $menus_deleted,
'@links_deleted' => $links_deleted,
]));
}
// Generate new menus.
$new_menus = $this->generateMenus($values['num_menus'], $values['title_length']);
if ($new_menus !== []) {
$this->setMessage($this->formatPlural(count($new_menus), 'Created the following 1 new menu: @menus', 'Created the following @count new menus: @menus',
['@menus' => implode(', ', $new_menus)]));
}
// Generate new menu links.
$menus = $new_menus;
if (isset($values['existing_menus'])) {
$menus += $values['existing_menus'];
}
$new_links = $this->generateLinks($values['num_links'], $menus, $values['title_length'], $values['link_types'], $values['max_depth'], $values['max_width']);
$this->setMessage($this->formatPlural(count($new_links), 'Created 1 new menu link.', 'Created @count new menu links.'));
}
/**
* {@inheritdoc}
*/
public function validateDrushParams(array $args, array $options = []): array {
$link_types = ['node', 'front', 'external'];
$values = [
'num_menus' => array_shift($args),
'num_links' => array_shift($args),
'kill' => $options['kill'],
'link_types' => array_combine($link_types, $link_types),
];
$max_depth = array_shift($args);
$max_width = array_shift($args);
$values['max_depth'] = $max_depth ?: 3;
$values['max_width'] = $max_width ?: 8;
$values['title_length'] = $this->getSetting('title_length');
$values['existing_menus']['__new-menu__'] = TRUE;
if ($this->isNumber($values['num_menus']) == FALSE) {
throw new \Exception(dt('Invalid number of menus'));
}
if ($this->isNumber($values['num_links']) == FALSE) {
throw new \Exception(dt('Invalid number of links'));
}
if ($this->isNumber($values['max_depth']) == FALSE || $values['max_depth'] > 9 || $values['max_depth'] < 1) {
throw new \Exception(dt('Invalid maximum link depth. Use a value between 1 and 9'));
}
if ($this->isNumber($values['max_width']) == FALSE || $values['max_width'] < 1) {
throw new \Exception(dt('Invalid maximum menu width. Use a positive numeric value.'));
}
return $values;
}
/**
* Deletes custom generated menus.
*/
protected function deleteMenus(): array {
$menu_ids = [];
if ($this->moduleHandler->moduleExists('menu_ui')) {
$all = $this->menuStorage->loadMultiple();
foreach ($all as $menu) {
if (str_starts_with($menu->id(), 'devel-')) {
$menu_ids[] = $menu->id();
}
}
if ($menu_ids !== []) {
$menus = $this->menuStorage->loadMultiple($menu_ids);
$this->menuStorage->delete($menus);
}
}
// Delete menu links in other menus, but generated by devel.
$link_ids = $this->menuLinkContentStorage->getQuery()
->condition('menu_name', 'devel', '<>')
->condition('link__options', '%' . $this->database->escapeLike('s:5:"devel";b:1') . '%', 'LIKE')
->accessCheck(FALSE)
->execute();
if ($link_ids) {
$links = $this->menuLinkContentStorage->loadMultiple($link_ids);
$this->menuLinkContentStorage->delete($links);
}
return [count($menu_ids), count($link_ids)];
}
/**
* Generates new menus.
*
* @param int $num_menus
* Number of menus to create.
* @param int $title_length
* (optional) Maximum length of menu name.
*
* @return array
* Array containing the generated menus.
*/
protected function generateMenus(int $num_menus, int $title_length = 12): array {
$menus = [];
for ($i = 1; $i <= $num_menus; ++$i) {
$name = $this->randomSentenceOfLength(mt_rand(2, $title_length));
// Create a random string of random length for the menu id. The maximum
// machine-name length is 32, so allowing for prefix 'devel-' we can have
// up to 26 here. For safety avoid accidentally reusing the same id.
do {
$id = 'devel-' . $this->getRandom()->word(mt_rand(2, 26));
} while (array_key_exists($id, $menus));
$menu = $this->menuStorage->create([
'label' => $name,
'id' => $id,
'description' => $this->t('Description of @name', ['@name' => $name]),
]);
$menu->save();
$menus[$menu->id()] = $menu->label();
}
return $menus;
}
/**
* Generates menu links in a tree structure.
*
* @return array<int|string, string>
* Array containing the titles of the generated menu links.
*/
protected function generateLinks(int $num_links, array $menus, int $title_length, array $link_types, int $max_depth, int $max_width): array {
$links = [];
$menus = array_keys(array_filter($menus));
$link_types = array_keys(array_filter($link_types));
$nids = [];
for ($i = 1; $i <= $num_links; ++$i) {
// Pick a random menu.
$menu_name = $menus[array_rand($menus)];
// Build up our link.
$link_title = $this->getRandom()->word(mt_rand(2, max(2, $title_length)));
/** @var \Drupal\menu_link_content\MenuLinkContentInterface $menuLinkContent */
$menuLinkContent = $this->menuLinkContentStorage->create([
'menu_name' => $menu_name,
'weight' => mt_rand(-50, 50),
'title' => $link_title,
'bundle' => 'menu_link_content',
'description' => $this->t('Description of @title.', ['@title' => $link_title]),
]);
$link = $menuLinkContent->get('link');
$options['devel'] = TRUE;
$link->setValue(['options' => $options]);
// For the first $max_width items, make first level links, otherwise, get
// a random parent menu depth.
$max_link_depth = $i <= $max_width ? 0 : mt_rand(1, max(1, $max_depth - 1));
// Get a random parent link from the proper depth.
for ($depth = $max_link_depth; $depth >= 0; --$depth) {
$parameters = new MenuTreeParameters();
$parameters->setMinDepth($depth);
$parameters->setMaxDepth($depth);
$tree = $this->menuLinkTree->load($menu_name, $parameters);
if ($tree === []) {
continue;
}
$menuLinkContent->set('parent', array_rand($tree));
break;
}
$link_type = array_rand($link_types);
switch ($link_types[$link_type]) {
case 'node':
// Grab a random node ID.
$select = $this->database->select('node_field_data', 'n')
->fields('n', ['nid', 'title'])
->condition('n.status', 1)
->range(0, 1)
->orderRandom();
// Don't put a node into the menu twice.
if (isset($nids[$menu_name])) {
$select->condition('n.nid', $nids[$menu_name], 'NOT IN');
}
$node = $select->execute()->fetchAssoc();
if (isset($node['nid'])) {
$nids[$menu_name][] = $node['nid'];
$link->setValue(['uri' => 'entity:node/' . $node['nid']]);
$menuLinkContent->set('title', $node['title']);
break;
}
case 'external':
$link->setValue(['uri' => 'https://www.example.com/']);
break;
case 'front':
$link->setValue(['uri' => 'internal:/<front>']);
break;
default:
break;
}
$menuLinkContent->save();
$links[$menuLinkContent->id()] = $menuLinkContent->getTitle();
}
return $links;
}
}

View File

@ -0,0 +1,454 @@
<?php
namespace Drupal\devel_generate\Plugin\DevelGenerate;
use Drupal\Core\Database\Connection;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\content_translation\ContentTranslationManagerInterface;
use Drupal\devel_generate\DevelGenerateBase;
use Drupal\taxonomy\TermInterface;
use Drupal\taxonomy\TermStorageInterface;
use Drupal\taxonomy\VocabularyStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a TermDevelGenerate plugin.
*
* @DevelGenerate(
* id = "term",
* label = @Translation("terms"),
* description = @Translation("Generate a given number of terms. Optionally delete current terms."),
* url = "term",
* permission = "administer devel_generate",
* settings = {
* "num" = 10,
* "title_length" = 12,
* "minimum_depth" = 1,
* "maximum_depth" = 4,
* "kill" = FALSE,
* },
* dependencies = {
* "taxonomy",
* },
* )
*/
class TermDevelGenerate extends DevelGenerateBase implements ContainerFactoryPluginInterface {
/**
* The vocabulary storage.
*/
protected VocabularyStorageInterface $vocabularyStorage;
/**
* The term storage.
*/
protected TermStorageInterface $termStorage;
/**
* Database connection.
*/
protected Connection $database;
/**
* The module handler.
*/
protected ModuleHandlerInterface $moduleHandler;
/**
* The language manager.
*/
protected LanguageManagerInterface $languageManager;
/**
* The content translation manager.
*/
protected ?ContentTranslationManagerInterface $contentTranslationManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
$entity_type_manager = $container->get('entity_type.manager');
// @phpstan-ignore ternary.alwaysTrue (False positive)
$content_translation_manager = $container->has('content_translation.manager') ? $container->get('content_translation.manager') : NULL;
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$instance->vocabularyStorage = $entity_type_manager->getStorage('taxonomy_vocabulary');
$instance->termStorage = $entity_type_manager->getStorage('taxonomy_term');
$instance->database = $container->get('database');
$instance->contentTranslationManager = $content_translation_manager;
return $instance;
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state): array {
$options = [];
foreach ($this->vocabularyStorage->loadMultiple() as $vocabulary) {
$options[$vocabulary->id()] = $vocabulary->label();
}
// Sort by vocabulary label.
asort($options);
// Set default to 'tags' only if it exists as a vocabulary.
$default_vids = array_key_exists('tags', $options) ? 'tags' : '';
$form['vids'] = [
'#type' => 'select',
'#multiple' => TRUE,
'#title' => $this->t('Vocabularies'),
'#required' => TRUE,
'#default_value' => $default_vids,
'#options' => $options,
'#description' => $this->t('Restrict terms to these vocabularies.'),
];
$form['num'] = [
'#type' => 'number',
'#title' => $this->t('Number of terms'),
'#default_value' => $this->getSetting('num'),
'#required' => TRUE,
'#min' => 0,
];
$form['title_length'] = [
'#type' => 'number',
'#title' => $this->t('Maximum number of characters in term names'),
'#default_value' => $this->getSetting('title_length'),
'#required' => TRUE,
'#min' => 2,
'#max' => 255,
];
$form['minimum_depth'] = [
'#type' => 'number',
'#title' => $this->t('Minimum depth for new terms in the vocabulary hierarchy'),
'#description' => $this->t('Enter a value from 1 to 20.'),
'#default_value' => $this->getSetting('minimum_depth'),
'#min' => 1,
'#max' => 20,
];
$form['maximum_depth'] = [
'#type' => 'number',
'#title' => $this->t('Maximum depth for new terms in the vocabulary hierarchy'),
'#description' => $this->t('Enter a value from 1 to 20.'),
'#default_value' => $this->getSetting('maximum_depth'),
'#min' => 1,
'#max' => 20,
];
$form['kill'] = [
'#type' => 'checkbox',
'#title' => $this->t('Delete existing terms in specified vocabularies before generating new terms.'),
'#default_value' => $this->getSetting('kill'),
];
// Add the language and translation options.
$form += $this->getLanguageForm('terms');
return $form;
}
/**
* {@inheritdoc}
*/
protected function generateElements(array $values): void {
$new_terms = $this->generateTerms($values);
if (!empty($new_terms['terms'])) {
$this->setMessage($this->formatPlural($new_terms['terms'], 'Created 1 new term', 'Created @count new terms'));
// Helper function to format the number of terms and the list of terms.
$format_terms_func = function (array $data, $level) {
if ($data['total'] > 10) {
$data['terms'][] = '...';
}
return $this->formatPlural($data['total'],
'1 new term at level @level (@terms)',
'@count new terms at level @level (@terms)',
['@level' => $level, '@terms' => implode(',', $data['terms'])]);
};
foreach ($new_terms['vocabs'] as $vid => $vlabel) {
if (array_key_exists($vid, $new_terms)) {
ksort($new_terms[$vid]);
$termlist = implode(', ', array_map($format_terms_func, $new_terms[$vid], array_keys($new_terms[$vid])));
$this->setMessage($this->t('In vocabulary @vlabel: @termlist', ['@vlabel' => $vlabel, '@termlist' => $termlist]));
}
else {
$this->setMessage($this->t('In vocabulary @vlabel: No terms created', ['@vlabel' => $vlabel]));
}
}
}
if ($new_terms['terms_translations'] > 0) {
$this->setMessage($this->formatPlural($new_terms['terms_translations'], 'Created 1 term translation', 'Created @count term translations'));
}
}
/**
* Deletes all terms of given vocabularies.
*
* @param array $vids
* Array of vocabulary ids.
*
* @return int
* The number of terms deleted.
*/
protected function deleteVocabularyTerms(array $vids): int {
$tids = $this->vocabularyStorage->getToplevelTids($vids);
$terms = $this->termStorage->loadMultiple($tids);
$total_deleted = 0;
foreach ($vids as $vid) {
$total_deleted += count($this->termStorage->loadTree($vid));
}
$this->termStorage->delete($terms);
return $total_deleted;
}
/**
* Generates taxonomy terms for a list of given vocabularies.
*
* @param array $parameters
* The input parameters from the settings form or drush command.
*
* @return array
* Information about the created terms.
*/
protected function generateTerms(array $parameters): array {
$info = [
'terms' => 0,
'terms_translations' => 0,
];
$min_depth = $parameters['minimum_depth'];
$max_depth = $parameters['maximum_depth'];
// $parameters['vids'] from the UI has keys of the vocab ids. From drush
// the array is keyed 0,1,2. Therefore create $vocabs which has keys of the
// vocab ids, so it can be used with array_rand().
$vocabs = array_combine($parameters['vids'], $parameters['vids']);
// Delete terms from the vocabularies we are creating new terms in.
if ($parameters['kill']) {
$deleted = $this->deleteVocabularyTerms($vocabs);
$this->setMessage($this->formatPlural($deleted, 'Deleted 1 existing term', 'Deleted @count existing terms'));
if ($min_depth != 1) {
$this->setMessage($this->t('Minimum depth changed from @min_depth to 1 because all terms were deleted', ['@min_depth' => $min_depth]));
$min_depth = 1;
}
}
// Build an array of potential parents for the new terms. These will be
// terms in the vocabularies we are creating in, which have a depth of one
// less than the minimum for new terms up to one less than the maximum.
$all_parents = [];
foreach ($parameters['vids'] as $vid) {
$info['vocabs'][$vid] = $this->vocabularyStorage->load($vid)->label();
// Initialise the nested array for this vocabulary.
$all_parents[$vid] = ['top_level' => [], 'lower_levels' => []];
$ids = [];
for ($depth = 1; $depth < $max_depth; ++$depth) {
$query = $this->termStorage->getQuery()->accessCheck(FALSE)->condition('vid', $vid);
if ($depth == 1) {
// For the top level the parent id must be zero.
$query->condition('parent', 0);
}
else {
// For lower levels use the $ids array obtained in the previous loop.
$query->condition('parent', $ids, 'IN');
}
$ids = $query->execute();
if (empty($ids)) {
// Reached the end, no more parents to be found.
break;
}
// Store these terms as parents if they are within the depth range for
// new terms.
if ($depth == $min_depth - 1) {
$all_parents[$vid]['top_level'] = array_fill_keys($ids, $depth);
}
elseif ($depth >= $min_depth) {
$all_parents[$vid]['lower_levels'] += array_fill_keys($ids, $depth);
}
}
// No top-level parents will have been found above when the minimum depth
// is 1 so add a record for that data here.
if ($min_depth == 1) {
$all_parents[$vid]['top_level'] = [0 => 0];
}
elseif (empty($all_parents[$vid]['top_level'])) {
// No parents for required minimum level so cannot use this vocabulary.
unset($vocabs[$vid]);
}
}
if ($vocabs === []) {
// There are no available parents at the required depth in any vocabulary,
// so we cannot create any new terms.
throw new \Exception(sprintf('Invalid minimum depth %s because there are no terms in any vocabulary at depth %s', $min_depth, $min_depth - 1));
}
// Insert new data:
for ($i = 1; $i <= $parameters['num']; ++$i) {
// Select a vocabulary at random.
$vid = array_rand($vocabs);
// Set the group to use to select a random parent from. Using < 50 means
// on average half of the new terms will be top_level. Also if no terms
// exist yet in 'lower_levels' then we have to use 'top_level'.
$group = (mt_rand(0, 100) < 50 || empty($all_parents[$vid]['lower_levels'])) ? 'top_level' : 'lower_levels';
$parent = array_rand($all_parents[$vid][$group]);
$depth = $all_parents[$vid][$group][$parent] + 1;
$name = $this->getRandom()->word(mt_rand(2, $parameters['title_length']));
$values = [
'name' => $name,
'description' => 'Description of ' . $name . ' (depth ' . $depth . ')',
'format' => filter_fallback_format(),
'weight' => mt_rand(0, 10),
'vid' => $vid,
'parent' => [$parent],
// Give hook implementations access to the parameters used for generation.
'devel_generate' => $parameters,
];
if (isset($parameters['add_language'])) {
$values['langcode'] = $this->getLangcode($parameters['add_language']);
}
/** @var \Drupal\taxonomy\TermInterface $term */
$term = $this->termStorage->create($values);
// Populate all fields with sample values.
$this->populateFields($term);
$term->save();
// Add translations.
if (isset($parameters['translate_language']) && !empty($parameters['translate_language'])) {
$info['terms_translations'] += $this->generateTermTranslation($parameters['translate_language'], $term);
}
// If the depth of the new term is less than the maximum depth then it can
// also be saved as a potential parent for the subsequent new terms.
if ($depth < $max_depth) {
$all_parents[$vid]['lower_levels'] += [$term->id() => $depth];
}
// Store data about the newly generated term.
++$info['terms'];
@$info[$vid][$depth]['total']++;
// List only the first 10 new terms at each vocab/level.
if (!isset($info[$vid][$depth]['terms']) || count($info[$vid][$depth]['terms']) < 10) {
$info[$vid][$depth]['terms'][] = $term->label();
}
unset($term);
}
return $info;
}
/**
* Create translation for the given term.
*
* @param array $translate_language
* Potential translate languages array.
* @param \Drupal\taxonomy\TermInterface $term
* Term to add translations to.
*
* @return int
* Number of translations added.
*/
protected function generateTermTranslation(array $translate_language, TermInterface $term): int {
if (is_null($this->contentTranslationManager)) {
return 0;
}
if (!$this->contentTranslationManager->isEnabled('taxonomy_term', $term->bundle())) {
return 0;
}
if ($term->get('langcode')->getLangcode() === LanguageInterface::LANGCODE_NOT_SPECIFIED
|| $term->get('langcode')->getLangcode() === LanguageInterface::LANGCODE_NOT_APPLICABLE) {
return 0;
}
$num_translations = 0;
// Translate term to each target language.
$skip_languages = [
LanguageInterface::LANGCODE_NOT_SPECIFIED,
LanguageInterface::LANGCODE_NOT_APPLICABLE,
$term->get('langcode')->getLangcode(),
];
foreach ($translate_language as $langcode) {
if (in_array($langcode, $skip_languages)) {
continue;
}
$translation_term = $term->addTranslation($langcode);
$translation_term->setName($term->getName() . ' (' . $langcode . ')');
$this->populateFields($translation_term);
$translation_term->save();
++$num_translations;
}
return $num_translations;
}
/**
* {@inheritdoc}
*/
public function validateDrushParams(array $args, array $options = []): array {
// Get default settings from the annotated command definition.
$defaultSettings = $this->getDefaultSettings();
$bundles = self::csvToarray($options['bundles']);
if (count($bundles) < 1) {
throw new \Exception(dt('Please provide a vocabulary machine name (--bundles).'));
}
foreach ($bundles as $bundle) {
// Verify that each bundle is a valid vocabulary id.
if (!$this->vocabularyStorage->load($bundle)) {
throw new \Exception(dt('Invalid vocabulary machine name: @name', ['@name' => $bundle]));
}
}
$number = array_shift($args) ?: $defaultSettings['num'];
if (!$this->isNumber($number)) {
throw new \Exception(dt('Invalid number of terms: @num', ['@num' => $number]));
}
$minimum_depth = $options['min-depth'] ?? $defaultSettings['minimum_depth'];
$maximum_depth = $options['max-depth'] ?? $defaultSettings['maximum_depth'];
if ($minimum_depth < 1 || $minimum_depth > 20 || $maximum_depth < 1 || $maximum_depth > 20 || $minimum_depth > $maximum_depth) {
throw new \Exception(dt('The depth values must be in the range 1 to 20 and min-depth cannot be larger than max-depth (values given: min-depth @min, max-depth @max)', ['@min' => $minimum_depth, '@max' => $maximum_depth]));
}
$values = [
'num' => $number,
'kill' => $options['kill'],
'title_length' => 12,
'vids' => $bundles,
'minimum_depth' => $minimum_depth,
'maximum_depth' => $maximum_depth,
];
$add_language = self::csvToArray($options['languages']);
// Intersect with the enabled languages to make sure the language args
// passed are actually enabled.
$valid_languages = array_keys($this->languageManager->getLanguages(LanguageInterface::STATE_ALL));
$values['add_language'] = array_intersect($add_language, $valid_languages);
$translate_language = self::csvToArray($options['translations']);
$values['translate_language'] = array_intersect($translate_language, $valid_languages);
return $values;
}
}

View File

@ -0,0 +1,187 @@
<?php
namespace Drupal\devel_generate\Plugin\DevelGenerate;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\devel_generate\DevelGenerateBase;
use Drupal\user\RoleStorageInterface;
use Drupal\user\UserStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a UserDevelGenerate plugin.
*
* @DevelGenerate(
* id = "user",
* label = @Translation("users"),
* description = @Translation("Generate a given number of users. Optionally delete current users."),
* url = "user",
* permission = "administer devel_generate",
* settings = {
* "num" = 50,
* "kill" = FALSE,
* "pass" = ""
* }
* )
*/
class UserDevelGenerate extends DevelGenerateBase implements ContainerFactoryPluginInterface {
/**
* The user storage.
*/
protected UserStorageInterface $userStorage;
/**
* The date formatter service.
*/
protected DateFormatterInterface $dateFormatter;
/**
* Provides system time.
*/
protected TimeInterface $time;
/**
* The role storage.
*/
protected RoleStorageInterface $roleStorage;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
$entity_type_manager = $container->get('entity_type.manager');
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$instance->userStorage = $entity_type_manager->getStorage('user');
$instance->dateFormatter = $container->get('date.formatter');
$instance->time = $container->get('datetime.time');
$instance->roleStorage = $entity_type_manager->getStorage('user_role');
return $instance;
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state): array {
$form['num'] = [
'#type' => 'number',
'#title' => $this->t('How many users would you like to generate?'),
'#default_value' => $this->getSetting('num'),
'#required' => TRUE,
'#min' => 0,
];
$form['kill'] = [
'#type' => 'checkbox',
'#title' => $this->t('Delete all users (except user id 1) before generating new users.'),
'#default_value' => $this->getSetting('kill'),
];
$roles = array_map(static fn($role): string => $role->label(), $this->roleStorage->loadMultiple());
unset($roles[AccountInterface::AUTHENTICATED_ROLE], $roles[AccountInterface::ANONYMOUS_ROLE]);
$form['roles'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Which roles should the users receive?'),
'#description' => $this->t('Users always receive the <em>authenticated user</em> role.'),
'#options' => $roles,
];
$form['pass'] = [
'#type' => 'textfield',
'#title' => $this->t('Password to be set'),
'#default_value' => $this->getSetting('pass'),
'#size' => 32,
'#description' => $this->t('Leave this field empty if you do not need to set a password'),
];
$options = [1 => $this->t('Now')];
foreach ([3600, 86400, 604800, 2592000, 31536000] as $interval) {
$options[$interval] = $this->dateFormatter->formatInterval($interval, 1) . ' ' . $this->t('ago');
}
$form['time_range'] = [
'#type' => 'select',
'#title' => $this->t('How old should user accounts be?'),
'#description' => $this->t('User ages will be distributed randomly from the current time, back to the selected time.'),
'#options' => $options,
'#default_value' => 604800,
];
return $form;
}
/**
* {@inheritdoc}
*/
protected function generateElements(array $values): void {
$num = $values['num'];
$kill = $values['kill'];
$pass = $values['pass'];
$age = $values['time_range'];
$roles = array_filter($values['roles']);
if ($kill) {
$uids = $this->userStorage->getQuery()
->condition('uid', 1, '>')
->accessCheck(FALSE)
->execute();
$users = $this->userStorage->loadMultiple($uids);
$this->userStorage->delete($users);
$this->setMessage($this->formatPlural(count($uids), '1 user deleted', '@count users deleted.'));
}
if ($num > 0) {
$names = [];
while (count($names) < $num) {
$name = $this->getRandom()->word(mt_rand(6, 12));
$names[$name] = '';
}
if ($roles === []) {
$roles = [AccountInterface::AUTHENTICATED_ROLE];
}
foreach (array_keys($names) as $name) {
/** @var \Drupal\user\UserInterface $account */
$account = $this->userStorage->create([
'uid' => NULL,
'name' => $name,
'pass' => $pass,
'mail' => $name . '@example.com',
'status' => 1,
'created' => $this->time->getRequestTime() - mt_rand(0, $age),
'roles' => array_values($roles),
// A flag to let hook_user_* know that this is a generated user.
'devel_generate' => TRUE,
]);
// Populate all fields with sample values.
$this->populateFields($account);
$account->save();
}
}
$this->setMessage($this->t('@num_users created.',
['@num_users' => $this->formatPlural($num, '1 user', '@count users')]));
}
/**
* {@inheritdoc}
*/
public function validateDrushParams(array $args, array $options = []): array {
return [
'num' => array_shift($args),
'time_range' => 0,
'roles' => self::csvToArray($options['roles']),
'kill' => $options['kill'],
'pass' => $options['pass'],
];
}
}

View File

@ -0,0 +1,158 @@
<?php
namespace Drupal\devel_generate\Plugin\DevelGenerate;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\devel_generate\DevelGenerateBase;
use Drupal\taxonomy\VocabularyStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a VocabularyDevelGenerate plugin.
*
* @DevelGenerate(
* id = "vocabulary",
* label = @Translation("vocabularies"),
* description = @Translation("Generate a given number of vocabularies. Optionally delete current vocabularies."),
* url = "vocabs",
* permission = "administer devel_generate",
* settings = {
* "num" = 1,
* "title_length" = 12,
* "kill" = FALSE
* },
* dependencies = {
* "taxonomy",
* },
* )
*/
class VocabularyDevelGenerate extends DevelGenerateBase implements ContainerFactoryPluginInterface {
/**
* The vocabulary storage.
*/
protected VocabularyStorageInterface $vocabularyStorage;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
$entity_type_manager = $container->get('entity_type.manager');
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$instance->vocabularyStorage = $entity_type_manager->getStorage('taxonomy_vocabulary');
return $instance;
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state): array {
$form['num'] = [
'#type' => 'number',
'#title' => $this->t('Number of vocabularies?'),
'#default_value' => $this->getSetting('num'),
'#required' => TRUE,
'#min' => 0,
];
$form['title_length'] = [
'#type' => 'number',
'#title' => $this->t('Maximum number of characters in vocabulary names'),
'#default_value' => $this->getSetting('title_length'),
'#required' => TRUE,
'#min' => 2,
'#max' => 255,
];
$form['kill'] = [
'#type' => 'checkbox',
'#title' => $this->t('Delete existing vocabularies before generating new ones.'),
'#default_value' => $this->getSetting('kill'),
];
return $form;
}
/**
* {@inheritdoc}
*/
protected function generateElements(array $values): void {
if ($values['kill']) {
$this->deleteVocabularies();
$this->setMessage($this->t('Deleted existing vocabularies.'));
}
$new_vocs = $this->generateVocabularies($values['num'], $values['title_length']);
if ($new_vocs !== []) {
$this->setMessage($this->t('Created the following new vocabularies: @vocs', ['@vocs' => implode(', ', $new_vocs)]));
}
}
/**
* Deletes all vocabularies.
*/
protected function deleteVocabularies(): void {
$vocabularies = $this->vocabularyStorage->loadMultiple();
$this->vocabularyStorage->delete($vocabularies);
}
/**
* Generates vocabularies.
*
* @param int $records
* Number of vocabularies to create.
* @param int $maxlength
* (optional) Maximum length for vocabulary name.
*
* @return array
* Array containing the generated vocabularies id.
*/
protected function generateVocabularies(int $records, int $maxlength = 12): array {
$vocabularies = [];
// Insert new data:
for ($i = 1; $i <= $records; ++$i) {
$name = $this->getRandom()->word(mt_rand(2, $maxlength));
$vocabulary = $this->vocabularyStorage->create([
'name' => $name,
'vid' => mb_strtolower($name),
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
'description' => 'Description of ' . $name,
'hierarchy' => 1,
'weight' => mt_rand(0, 10),
'multiple' => 1,
'required' => 0,
'relations' => 1,
]);
// Populate all fields with sample values.
$this->populateFields($vocabulary);
$vocabulary->save();
$vocabularies[] = $vocabulary->id();
unset($vocabulary);
}
return $vocabularies;
}
/**
* {@inheritdoc}
*/
public function validateDrushParams(array $args, array $options = []): array {
$values = [
'num' => array_shift($args),
'kill' => $options['kill'],
'title_length' => 12,
];
if ($this->isNumber($values['num']) == FALSE) {
throw new \Exception(dt('Invalid number of vocabularies: @num.', ['@num' => $values['num']]));
}
return $values;
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace Drupal\devel_generate\Routing;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\devel_generate\Form\DevelGenerateForm;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Route;
/**
* Provides dynamic routes for devel_generate.
*/
class DevelGenerateRoutes implements ContainerInjectionInterface {
/**
* The manager to be used for instantiating plugins.
*/
protected PluginManagerInterface $develGenerateManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
$instance = new self();
$instance->develGenerateManager = $container->get('plugin.manager.develgenerate');
return $instance;
}
/**
* Define routes for all devel_generate plugins.
*/
public function routes(): array {
$devel_generate_plugins = $this->develGenerateManager->getDefinitions();
$routes = [];
foreach ($devel_generate_plugins as $id => $plugin) {
$label = $plugin['label'];
$type_url_str = str_replace('_', '-', $plugin['url']);
$routes['devel_generate.' . $id] = new Route(
'admin/config/development/generate/' . $type_url_str,
[
'_form' => DevelGenerateForm::class,
'_title' => 'Generate ' . $label,
'_plugin_id' => $id,
],
[
'_permission' => $plugin['permission'],
]
);
}
// Add the route for the 'Generate' admin group on the admin/config page.
// This also provides the page for all devel_generate links.
$routes['devel_generate.admin_config_generate'] = new Route(
'/admin/config/development/generate',
[
'_controller' => '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage',
'_title' => 'Generate',
],
[
'_permission' => 'administer devel_generate',
]
);
return $routes;
}
}