first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
[[_TOC_]]
This module may be used to create entities that contain sample content. This is
useful when showing off your site to a client, for example. Even if the content
is not yet available, the site can show its look and feel and behavior.
The sample entities may be created via the Web or via the included Drush commands
like `drush genc`.
#### Recommended Modules
- [Devel Images Provider](http://drupal.org/project/devel_image_provider) allows to configure external providers for images.
#### Custom plugins
This module creates the _DevelGenerate_ plugin type.
All you need to do to provide a new instance for DevelGenerate plugin type
is to create your class extending `DevelGenerateBase` and following these steps:
1. Declare your plugin with annotations:
````
/**
* Provides a ExampleDevelGenerate plugin.
*
* @DevelGenerate(
* id = "example",
* label = @Translation("example"),
* description = @Translation("Generate a given number of example elements."),
* url = "example",
* permission = "administer example",
* settings = {
* "num" = 50,
* "kill" = FALSE,
* "another_property" = "default_value"
* }
* )
*/
````
1. Implement the `settingsForm` method to create a form using the properties
from the annotations.
1. Implement the `handleDrushParams` method. It should return an array of
values.
1. Implement the `generateElements` method. You can write here your business
logic using the array of values.
#### Notes
- You can alter existing properties for every plugin by implementing
`hook_devel_generate_info_alter`.
- DevelGenerateBaseInterface details base wrapping methods that most
DevelGenerate implementations will want to directly inherit from
`Drupal\devel_generate\DevelGenerateBase`.
- To give support for a new field type the field type base class should properly
implement `\Drupal\Core\Field\FieldItemInterface::generateSampleValue()`.
Devel Generate automatically uses the values returned by this method during the
generate process for generating placeholder field values. For more information
see: https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Field!FieldItemInterface.php/function/FieldItemInterface::generateSampleValue
- For Drupal 10, the webprofiler module has broken out to its own project at https://www.drupal.org/project/webprofiler

View File

@@ -0,0 +1,45 @@
{
"name": "drupal/devel_generate",
"description": "Generate test users, nodes, menus, taxonomy terms...",
"type": "drupal-module",
"homepage": "http://drupal.org/project/devel",
"authors": [
{
"name": "Moshe Weitzman",
"email": "weitzman@tejasa.com",
"homepage": "https://github.com/weitzman",
"role": "Maintainer"
},
{
"name": "Hans Salvisberg",
"email": "drupal@salvisberg.com",
"homepage": "https://www.drupal.org/u/salvis",
"role": "Maintainer"
},
{
"name": "Luca Lusso",
"homepage": "https://www.drupal.org/u/lussoluca",
"role": "Maintainer"
},
{
"name": "Marco (willzyx)",
"homepage": "https://www.drupal.org/u/willzyx",
"role": "Maintainer"
},
{
"name": "See contributors",
"homepage": "https://www.drupal.org/node/3236/committers"
}
],
"support": {
"issues": "http://drupal.org/project/devel",
"irc": "irc://irc.freenode.org/drupal-contribute",
"source": "http://cgit.drupalcode.org/devel"
},
"license": "GPL-2.0-or-later",
"minimum-stability": "dev",
"require": {},
"suggest": {
"drupal/realistic_dummy_content": "Generate realistic demo content with Devel's devel_generate module."
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* @file
* Provides common batch functions for every DevelGeneratePlugin.
*/
use Drupal\devel_generate\DevelGenerateBaseInterface;
/**
* Calls the correct method responsible for handling a given batch operation.
*/
function devel_generate_operation(DevelGenerateBaseInterface $class, $method, $vars, &$context) {
return $class->$method($vars, $context);
}
/**
* Standard finish batch function.
*/
function devel_generate_batch_finished($success, $results, $operations) {
if ($success) {
if (!empty($results['num_translations'])) {
$message = t('Finished @num elements and @num_translations translations created successfully.', ['@num' => $results['num'], '@num_translations' => $results['num_translations']]);
}
else {
$message = t('Finished @num elements created successfully.', ['@num' => $results['num']]);
}
}
else {
$message = t('Finished with an error.');
}
\Drupal::messenger()->addMessage($message);
}

View File

@@ -0,0 +1,12 @@
type: module
name: 'Devel Generate'
description: 'Generate test users, nodes, menus, taxonomy terms...'
package: Development
core_version_requirement: ">=10.0 <12.0.0-stable"
tags:
- developer
# Information added by Drupal.org packaging script on 2024-06-26
version: '5.2.1+50-dev'
project: 'devel'
datestamp: 1719414589

View File

@@ -0,0 +1,57 @@
<?php
/**
* @file
* Devel sub-module to for generating content, menus, taxonomy terms etc.
*
* See src/Plugin for specific details of each type that can be generated.
*/
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Implements hook_menu_links_discovered_alter().
*/
function devel_generate_menu_links_discovered_alter(array &$links): void {
$devel_generate_plugins = \Drupal::service('plugin.manager.develgenerate')->getDefinitions();
foreach ($devel_generate_plugins as $id => $plugin) {
$label = $plugin['label'];
$links['devel_generate.' . $id] = [
'title' => new TranslatableMarkup("Generate @label", ['@label' => $label]),
'parent' => 'devel_generate.admin_config_generate',
'description' => $plugin['description'],
'route_name' => 'devel_generate.' . $id,
'provider' => 'devel_generate',
];
}
// Store the basic link info for repeated use. Each of the three actual links
// require subtle variations on this.
$basics = [
'title' => new TranslatableMarkup('Generate'),
'description' => new TranslatableMarkup('Generate realistic items (content, users, menus, etc) to assist your site development and testing.'),
'route_name' => 'devel_generate.admin_config_generate',
'provider' => 'devel_generate',
];
// Define a separate group on admin/config page, so that 'Generate' has its
// own block with all the generate links.
$links['devel_generate.admin_config_generate'] = [
'parent' => 'system.admin_config',
// The main development group has weight -10 in system.links.menu.yml so use
// -9 here as this block should be near but just after it on the page.
'weight' => -9,
] + $basics;
// Add a link in the main development group, to allow direct access to the
// the Generate page and to make the back breadcrumb more useful.
$links['devel_generate.generate'] = [
'title' => new TranslatableMarkup('Devel generate'),
'parent' => 'system.admin_config_development',
] + $basics;
// Define a top-level link (with no parent) in the 'devel' menu. This also
// means that it will be available in the devel admin toolbar.
$links['devel_generate.generate2'] = ['menu_name' => 'devel'] + $basics;
}

View File

@@ -0,0 +1,5 @@
administer devel_generate:
title: 'Administer devel_generate'
permission_callbacks:
- \Drupal\devel_generate\DevelGeneratePermissions::permissions

View File

@@ -0,0 +1,2 @@
route_callbacks:
- '\Drupal\devel_generate\Routing\DevelGenerateRoutes::routes'

View File

@@ -0,0 +1,10 @@
services:
plugin.manager.develgenerate:
class: Drupal\devel_generate\DevelGeneratePluginManager
parent: default_plugin_manager
arguments: ['@entity_type.manager', '@messenger', '@language_manager', '@string_translation', '@entity_field.manager']
Drupal\devel_generate\DevelGeneratePluginManager: '@plugin.manager.develgenerate'
logger.channel.devel_generate:
parent: logger.channel_base
arguments: ['devel_generate']

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,347 @@
<?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 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->messenger = $container->get('messenger');
$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');
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->setMessage('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.
*/
protected function setMessage($msg, string $type = MessengerInterface::TYPE_STATUS): void {
if (function_exists('drush_log')) {
$msg = strip_tags($msg);
drush_log($msg);
}
else {
$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(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(int $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(int $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(int $number_menus = 2, int $number_links = 50, int $max_depth = 3, int $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(int $num = 50, int $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(int $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(int $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\block_content\BlockContentInterface;
use Drupal\content_translation\ContentTranslationManagerInterface;
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\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->setMessage($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->setMessage($this->formatPlural($values['num'], 'Created 1 node', 'Created @count nodes'));
if ($values['num_translations'] > 0) {
$this->setMessage($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'] = array_shift($args);
$values['max_comments'] = 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($values['num'], $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->setMessage($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 ($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($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'] = 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,384 @@
<?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'],
'pipe' => $options['pipe'],
'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\content_translation\ContentTranslationManagerInterface;
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\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;
}
}

View File

@@ -0,0 +1,12 @@
name: 'Devel Generate Example'
type: module
description: 'Create an example of a Devel Generate plugin type for testing purposes.'
package: Testing
configure: admin/config/development/generate
tags:
- developer
# Information added by Drupal.org packaging script on 2024-06-26
version: '5.2.1+50-dev'
project: 'devel'
datestamp: 1719414589

View File

@@ -0,0 +1,114 @@
<?php
namespace Drupal\devel_generate_example\Plugin\DevelGenerate;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\devel_generate\DevelGenerateBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a ExampleDevelGenerate plugin.
*
* @DevelGenerate(
* id = "devel_generate_example",
* label = "Example",
* description = "Generate a given number of examples.",
* url = "devel_generate_example",
* permission = "administer devel_generate",
* settings = {
* "num" = 50,
* "kill" = FALSE
* }
* )
*/
class ExampleDevelGenerate extends DevelGenerateBase implements ContainerFactoryPluginInterface {
/**
* Provides system time.
*/
protected TimeInterface $time;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$instance->time = $container->get('datetime.time');
return $instance;
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state): array {
$form['num'] = [
'#type' => 'textfield',
'#title' => $this->t('How many examples would you like to generate?'),
'#default_value' => $this->getSetting('num'),
'#size' => 10,
];
$form['kill'] = [
'#type' => 'checkbox',
'#title' => $this->t('Delete all examples before generating new examples.'),
'#default_value' => $this->getSetting('kill'),
];
return $form;
}
/**
* {@inheritdoc}
*/
protected function generateElements(array $values): void {
$num = $values['num'];
$kill = $values['kill'];
if ($kill) {
$this->setMessage($this->t('Old examples have been deleted.'));
}
// Creating user in order to demonstrate
// how to override default business login generation.
$edit = [
'uid' => NULL,
'name' => 'example_devel_generate',
'pass' => '',
'mail' => 'example_devel_generate@example.com',
'status' => 1,
'created' => $this->time->getRequestTime(),
'roles' => '',
// A flag to let hook_user_* know that this is a generated user.
'devel_generate' => TRUE,
];
$account = user_load_by_name('example_devel_generate');
if (!$account) {
$account = $this->entityTypeManager->getStorage('user')->create($edit);
}
// Populate all fields with sample values.
$this->populateFields($account);
$account->save();
$this->setMessage($this->t('@num_examples created.', [
'@num_examples' => $this->formatPlural($num, '1 example', '@count examples'),
]));
}
/**
* {@inheritdoc}
*/
public function validateDrushParams(array $args, array $options = []): array {
return [
'num' => $options['num'],
'kill' => $options['kill'],
];
}
}

View File

@@ -0,0 +1,11 @@
name: 'Devel Generate Fields'
type: module
description: 'Alter in a base field for testing purposes.'
package: Testing
tags:
- developer
# Information added by Drupal.org packaging script on 2024-06-26
version: '5.2.1+50-dev'
project: 'devel'
datestamp: 1719414589

View File

@@ -0,0 +1,21 @@
<?php
/**
* @file
* Test module for field population.
*/
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Implements hook_entity_base_field_info_alter().
*/
function devel_generate_fields_entity_base_field_info_alter(array &$fields, EntityTypeInterface $entity_type): void {
if (in_array($entity_type->id(), ['node', 'media'])) {
$fields['phish'] = BaseFieldDefinition::create('string')
->setName('phish')
->setLabel(new TranslatableMarkup('Phish music'));
}
}

View File

@@ -0,0 +1,439 @@
<?php
namespace Drupal\Tests\devel_generate\Functional;
use Drupal\media\Entity\Media;
use Drupal\node\Entity\Node;
use Drupal\taxonomy\Entity\Term;
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
/**
* Tests the logic to generate data.
*
* @group devel_generate
*/
class DevelGenerateBrowserTest extends DevelGenerateBrowserTestBase {
use MediaTypeCreationTrait;
/**
* Tests generating users.
*/
public function testDevelGenerateUsers(): void {
$this->drupalGet('admin/config/development/generate/user');
$edit = [
'num' => 4,
];
$this->submitForm($edit, 'Generate');
$this->assertSession()->pageTextContains('4 users created.');
$this->assertSession()->pageTextContains('Generate process complete.');
}
/**
* Tests that if no content types are selected an error message is shown.
*/
public function testDevelGenerateContent(): void {
$this->drupalGet('admin/config/development/generate/content');
$edit = [
'num' => 4,
'title_length' => 4,
];
$this->submitForm($edit, 'Generate');
$this->assertSession()->pageTextContains('Please select at least one content type');
// Create a node in order to test the Delete content checkbox.
$this->drupalCreateNode(['type' => 'article']);
// Generate articles with comments and aliases.
$this->drupalGet('admin/config/development/generate/content');
$edit = [
'num' => 4,
'kill' => TRUE,
'node_types[article]' => TRUE,
'time_range' => 604800,
'max_comments' => 3,
'title_length' => 4,
'add_alias' => 1,
];
$this->submitForm($edit, 'Generate');
$this->assertSession()->pageTextContains('Deleted 1 node');
$this->assertSession()->pageTextContains('Created 4 nodes');
$this->assertSession()->pageTextContains('Generate process complete.');
$this->assertSession()->pageTextNotContains('translations');
// Tests that nodes have been created in the generation process.
$nodes = Node::loadMultiple();
$this->assertEquals(4, count($nodes), 'Nodes generated successfully.');
// Tests url alias for the generated nodes.
foreach ($nodes as $node) {
$alias = 'node-' . $node->id() . '-' . $node->bundle();
$this->drupalGet($alias);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($node->getTitle());
}
// Generate articles with translations.
$this->drupalGet('admin/config/development/generate/content');
$edit = [
'num' => 3,
'kill' => TRUE,
'node_types[article]' => TRUE,
'add_language[]' => ['en'],
'translate_language[]' => ['de', 'ca'],
'add_alias' => TRUE,
];
$this->submitForm($edit, 'Generate');
$this->assertSession()->pageTextContains('Deleted 4 nodes');
$this->assertSession()->pageTextContains('Created 3 nodes');
// Two translations for each node makes six.
$this->assertSession()->pageTextContains('Created 6 node translations');
$articles = \Drupal::entityQuery('node')->accessCheck(FALSE)->execute();
$this->assertCount(3, $articles);
$node = Node::load(end($articles));
$this->assertTrue($node->hasTranslation('de'));
$this->assertTrue($node->hasTranslation('ca'));
$this->assertFalse($node->hasTranslation('fr'));
// Check url alias for each of the translations.
foreach (Node::loadMultiple($articles) as $node) {
foreach (['de', 'ca'] as $langcode) {
$translation_node = $node->getTranslation($langcode);
$alias = 'node-' . $translation_node->id() . '-' . $translation_node->bundle() . '-' . $langcode;
$this->drupalGet($langcode . '/' . $alias);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($translation_node->getTitle());
}
}
// Create article to make sure it is not deleted when only killing pages.
$article = $this->drupalCreateNode(['type' => 'article', 'title' => 'Alive']);
// The 'page' content type is not enabled for translation.
$edit = [
'num' => 2,
'kill' => TRUE,
'node_types[page]' => TRUE,
'add_language[]' => ['en'],
'translate_language[]' => ['fr'],
];
$this->drupalGet('admin/config/development/generate/content');
$this->submitForm($edit, 'Generate');
$this->assertSession()->pageTextNotContains('Deleted');
$this->assertSession()->pageTextContains('Created 2 nodes');
$this->assertSession()->pageTextNotContains('node translations');
// Check that 'kill' has not deleted the article.
$this->assertNotEmpty(Node::load($article->id()));
$pages = \Drupal::entityQuery('node')->condition('type', 'page')->accessCheck(FALSE)->execute();
$this->assertCount(2, $pages);
$node = Node::load(end($pages));
$this->assertFalse($node->hasTranslation('fr'));
// Create articles with add-type-label option.
$edit = [
'num' => 5,
'kill' => TRUE,
'node_types[article]' => TRUE,
'add_type_label' => TRUE,
];
$this->drupalGet('admin/config/development/generate/content');
$this->submitForm($edit, 'Generate');
$this->assertSession()->pageTextContains('Created 5 nodes');
$this->assertSession()->pageTextContains('Generate process complete');
// Count the articles created in the generation process.
$nodes = \Drupal::entityQuery('node')->accessCheck(FALSE)->condition('type', 'article')->execute();
$this->assertCount(5, $nodes);
// Load the final node and verify that the title starts with the label.
$node = Node::load(end($nodes));
$this->assertEquals('Article - ', substr($node->title->value, 0, 10));
// Test creating content with specified authors. First create 15 more users
// making 18 in total, to make the test much stronger.
for ($i = 0; $i < 15; ++$i) {
$this->drupalCreateUser();
}
$edit = [
'num' => 10,
'kill' => TRUE,
'node_types[article]' => TRUE,
'authors[3]' => TRUE,
'authors[4]' => TRUE,
];
$this->drupalGet('admin/config/development/generate/content');
$this->submitForm($edit, 'Generate');
// Display the full content list for information and debug only.
$this->drupalGet('admin/content');
// Count all the articles by user 3 and 4 and by others. We count the two
// users nodes separately to ensure that there are some by each user.
$nodes_by_user_3 = \Drupal::entityQuery('node')->accessCheck(FALSE)->condition('type', 'article')->condition('uid', ['3'], 'IN')->execute();
$nodes_by_user_4 = \Drupal::entityQuery('node')->accessCheck(FALSE)->condition('type', 'article')->condition('uid', ['4'], 'IN')->execute();
$nodes_by_others = \Drupal::entityQuery('node')->accessCheck(FALSE)->condition('type', 'article')->condition('uid', ['3', '4'], 'NOT IN')->execute();
// If the user option was not working correctly and users were assigned at
// random, then the chance that these assertions will correctly detect the
// error is 1 - (2/18 ** 10) = 99.99%.
$this->assertEquals(10, count($nodes_by_user_3) + count($nodes_by_user_4));
$this->assertCount(0, $nodes_by_others);
// If the user option is coded correctly the chance of either of these
// assertions giving a false failure is 1/2 ** 10 = 0.097%.
$this->assertGreaterThan(0, count($nodes_by_user_3));
$this->assertGreaterThan(0, count($nodes_by_user_4));
}
/**
* Tests generating terms.
*/
public function testDevelGenerateTerms(): void {
// Generate terms.
$edit = [
'vids[]' => $this->vocabulary->id(),
'num' => 5,
'title_length' => 12,
];
$this->drupalGet('admin/config/development/generate/term');
$this->submitForm($edit, 'Generate');
$this->assertSession()->pageTextContains('Created 5 new terms');
$this->assertSession()->pageTextContains('In vocabulary ' . $this->vocabulary->label());
$this->assertSession()->pageTextNotContains('translations');
$this->assertSession()->pageTextContains('Generate process complete.');
$this->assertCount(5, \Drupal::entityQuery('taxonomy_term')->accessCheck(FALSE)->execute());
// Generate terms with translations.
$edit = [
'vids[]' => $this->vocabulary->id(),
'num' => 3,
'add_language[]' => ['en'],
'translate_language[]' => ['ca'],
];
$this->drupalGet('admin/config/development/generate/term');
$this->submitForm($edit, 'Generate');
$this->assertSession()->pageTextNotContains('Deleted');
$this->assertSession()->pageTextContains('Created 3 new terms');
$this->assertSession()->pageTextContains('Created 3 term translations');
// Not using 'kill' so there should be 8 terms.
$terms = \Drupal::entityQuery('taxonomy_term')->accessCheck(FALSE)->execute();
$this->assertCount(8, $terms);
// Check the translations created (and not created).
$term = Term::load(end($terms));
$this->assertTrue($term->hasTranslation('ca'));
$this->assertFalse($term->hasTranslation('de'));
$this->assertFalse($term->hasTranslation('fr'));
// Generate terms in vocabulary 2 only.
$edit = [
'vids[]' => $this->vocabulary2->id(),
'num' => 4,
];
$this->drupalGet('admin/config/development/generate/term');
$this->submitForm($edit, 'Generate');
$this->assertSession()->pageTextContains('Created 4 new terms');
$this->assertSession()->pageTextNotContains('In vocabulary ' . $this->vocabulary->label());
$this->assertSession()->pageTextContains('In vocabulary ' . $this->vocabulary2->label());
// Check the term count in each vocabulary.
$terms1 = \Drupal::entityQuery('taxonomy_term')->accessCheck(FALSE)->condition('vid', $this->vocabulary->id())->execute();
$this->assertCount(8, $terms1);
$terms2 = \Drupal::entityQuery('taxonomy_term')->accessCheck(FALSE)->condition('vid', $this->vocabulary2->id())->execute();
$this->assertCount(4, $terms2);
// Generate in vocabulary 2 with 'kill' to remove the existing vocab2 terms.
$edit = [
'vids[]' => $this->vocabulary2->id(),
'num' => 6,
'kill' => TRUE,
];
$this->drupalGet('admin/config/development/generate/term');
$this->submitForm($edit, 'Generate');
$this->assertSession()->pageTextContains('Deleted 4 existing terms');
$this->assertSession()->pageTextContains('Created 6 new terms');
// Check the term count in vocabulary 1 has not changed.
$terms1 = \Drupal::entityQuery('taxonomy_term')->accessCheck(FALSE)->condition('vid', $this->vocabulary->id())->execute();
$this->assertCount(8, $terms1);
// Check the term count in vocabulary 2 is just from the second call.
$terms2 = \Drupal::entityQuery('taxonomy_term')->accessCheck(FALSE)->condition('vid', $this->vocabulary2->id())->execute();
$this->assertCount(6, $terms2);
// Generate in both vocabularies and specify minimum and maximum depth.
$edit = [
'vids[]' => [$this->vocabulary->id(), $this->vocabulary2->id()],
'num' => 9,
'minimum_depth' => 2,
'maximum_depth' => 6,
];
$this->drupalGet('admin/config/development/generate/term');
$this->submitForm($edit, 'Generate');
$this->assertSession()->pageTextContains('Created 9 new terms');
// Check the total term count is 8 + 6 + 9 = 23.
$terms1 = \Drupal::entityQuery('taxonomy_term')->accessCheck(FALSE)->condition('vid', $this->vocabulary->id())->execute();
$terms2 = \Drupal::entityQuery('taxonomy_term')->accessCheck(FALSE)->condition('vid', $this->vocabulary2->id())->execute();
$this->assertCount(23, $terms1 + $terms2);
}
/**
* Tests generating vocabularies.
*/
public function testDevelGenerateVocabs(): void {
$edit = [
'num' => 5,
'title_length' => 12,
'kill' => TRUE,
];
$this->drupalGet('admin/config/development/generate/vocabs');
$this->submitForm($edit, 'Generate');
$this->assertSession()->pageTextContains('Created the following new vocabularies: ');
$this->assertSession()->pageTextContains('Generate process complete.');
}
/**
* Tests generating menus.
*
* @todo Add test coverage to check:
* - title_length is not exceeded.
* - max_depth and max_width work as designed.
* - generating links in existing menus, and then deleting them with kill.
* - using specific link_types settings only create those links.
*/
public function testDevelGenerateMenus(): void {
$edit = [
'num_menus' => 5,
'num_links' => 7,
];
$this->drupalGet('admin/config/development/generate/menu');
$this->submitForm($edit, 'Generate');
$this->assertSession()->pageTextContains('Created the following 5 new menus: ');
$this->assertSession()->pageTextContains('Created 7 new menu links');
$this->assertSession()->pageTextContains('Generate process complete.');
// Use big numbers for menus and links, but short text, to test for clashes.
// Also verify the kill option.
$edit = [
'num_menus' => 160,
'num_links' => 380,
'title_length' => 3,
'kill' => 1,
];
$this->drupalGet('admin/config/development/generate/menu');
$this->submitForm($edit, 'Generate');
$this->assertSession()->pageTextContains('Deleted 5 menu(s) and 0 other link(s).');
$this->assertSession()->pageTextContains('Created the following 160 new menus: ');
$this->assertSession()->pageTextContains('Created 380 new menu links');
$this->assertSession()->pageTextContains('Generate process complete.');
}
/**
* Tests generating media.
*/
public function testDevelGenerateMedia(): void {
// As the 'media' plugin has a dependency on 'media' module, the plugin is
// not generating a route to the plugin form.
$this->drupalGet('admin/config/development/generate/media');
$this->assertSession()->statusCodeEquals(404);
// Enable the module and retry.
\Drupal::service('module_installer')->install(['media']);
$this->getSession()->reload();
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('Generate media');
// Create two media types.
$media_type1 = $this->createMediaType('image');
$media_type2 = $this->createMediaType('audio_file');
// Creating media items (non-batch mode).
$edit = [
'num' => 5,
'name_length' => 12,
sprintf('media_types[%s]', $media_type1->id()) => 1,
sprintf('media_types[%s]', $media_type2->id()) => 1,
'base_fields' => 'phish',
'kill' => 1,
];
$this->drupalGet('admin/config/development/generate/media');
$this->submitForm($edit, 'Generate');
$this->assertSession()->pageTextContains('Finished creating 5 media items.');
$this->assertSession()->pageTextContains('Generate process complete.');
$medias = \Drupal::entityQuery('media')->accessCheck(FALSE)->execute();
$this->assertCount(5, $medias);
$media = Media::load(end($medias));
$this->assertNotEmpty($media->get('phish')->getString());
// Creating media items (batch mode).
$edit = [
'num' => 56,
'name_length' => 6,
sprintf('media_types[%s]', $media_type1->id()) => 1,
sprintf('media_types[%s]', $media_type2->id()) => 1,
'base_fields' => 'phish',
'kill' => 1,
];
$this->drupalGet('admin/config/development/generate/media');
$this->submitForm($edit, 'Generate');
$this->assertSession()->pageTextContains('Finished 56 elements created successfully.');
$this->assertSession()->pageTextContains('Generate process complete.');
$this->assertCount(56, \Drupal::entityQuery('media')->accessCheck(FALSE)->execute());
}
/**
* Tests generating content in batch mode.
*/
public function testDevelGenerateBatchContent(): void {
// For 50 or more nodes, the processing will be done via batch.
$edit = [
'num' => 55,
'kill' => TRUE,
'node_types[article]' => TRUE,
'node_types[page]' => TRUE,
];
$this->drupalGet('admin/config/development/generate/content');
$this->submitForm($edit, 'Generate');
$this->assertSession()->pageTextContains('Finished 55 elements created successfully.');
$this->assertSession()->pageTextContains('Generate process complete.');
// Tests that the expected number of nodes have been created.
$count = count(Node::loadMultiple());
$this->assertEquals(55, $count, sprintf('The expected total number of nodes is %s, found %s', 55, $count));
// Create nodes with translations via batch.
$edit = [
'num' => 52,
'kill' => TRUE,
'node_types[article]' => TRUE,
'node_types[page]' => TRUE,
'add_language[]' => ['en'],
'translate_language[]' => ['de', 'ca'],
];
$this->drupalGet('admin/config/development/generate/content');
$this->submitForm($edit, 'Generate');
$this->assertCount(52, \Drupal::entityQuery('node')->accessCheck(FALSE)->execute());
// Only articles will have translations so get that number.
$articles = \Drupal::entityQuery('node')->accessCheck(FALSE)->condition('type', 'article')->execute();
$this->assertSession()->pageTextContains(sprintf('Finished 52 elements and %s translations created successfully.', 2 * count($articles)));
// Generate only articles.
$edit = [
'num' => 60,
'kill' => TRUE,
'node_types[article]' => TRUE,
'node_types[page]' => FALSE,
];
$this->drupalGet('admin/config/development/generate/content');
$this->submitForm($edit, 'Generate');
// Tests that all the created nodes were of the node type selected.
$nodeStorage = $this->container->get('entity_type.manager')->getStorage('node');
$type = 'article';
$count = $nodeStorage->getQuery()
->condition('type', $type)
->accessCheck(FALSE)
->count()
->execute();
$this->assertEquals(60, $count, sprintf('The expected number of %s is %s, found %s', $type, 60, $count));
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Drupal\Tests\devel_generate\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\devel_generate\Traits\DevelGenerateSetupTrait;
/**
* Base class for devel_generate functional browser tests.
*
* DevelGenerateCommandsTest should not extend this class so that it can remain
* independent and be used as a cut-and-paste example for other developers.
*/
abstract class DevelGenerateBrowserTestBase extends BrowserTestBase {
use DevelGenerateSetupTrait;
/**
* Modules to enable.
*
* @var string[]
*/
protected static $modules = [
'content_translation',
'devel',
'devel_generate',
'devel_generate_fields',
'language',
'menu_ui',
'node',
'comment',
'taxonomy',
'path',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Prepares the testing environment.
*/
public function setUp(): void {
parent::setUp();
$this->setUpData();
}
}

View File

@@ -0,0 +1,320 @@
<?php
namespace Drupal\Tests\devel_generate\Functional;
use Drupal\comment\Entity\Comment;
use Drupal\devel_generate\Drush\Commands\DevelGenerateCommands;
use Drupal\media\Entity\Media;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\node\Entity\Node;
use Drupal\system\Entity\Menu;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\devel_generate\Traits\DevelGenerateSetupTrait;
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
use Drupal\user\Entity\User;
use Drush\TestTraits\DrushTestTrait;
/**
* Test class for the Devel Generate drush commands.
*
* Note: Drush must be in the Composer project.
*
* @coversDefaultClass \Drupal\devel_generate\Drush\Commands\DevelGenerateCommands
* @group devel_generate
*/
class DevelGenerateCommandsTest extends BrowserTestBase {
use DrushTestTrait;
use DevelGenerateSetupTrait;
use MediaTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'comment',
'content_translation',
'devel',
'devel_generate',
'devel_generate_fields',
'language',
'media',
'menu_ui',
'node',
'path',
'taxonomy',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Prepares the testing environment.
*/
public function setUp(): void {
parent::setUp();
$this->setUpData();
}
/**
* Tests generating users.
*/
public function testDrushGenerateUsers(): void {
// Make sure users get created, and with correct roles.
$this->drush(DevelGenerateCommands::USERS, ['55'], [
'kill' => NULL,
'roles' => 'administrator',
]);
$user = User::load(55);
$this->assertTrue($user->hasRole('administrator'));
}
/**
* Tests generating terms.
*/
public function testDrushGenerateTerms(): void {
// Make sure terms get created, and with correct vocab.
$this->drush(DevelGenerateCommands::TERMS, ['55'], [
'kill' => NULL,
'bundles' => $this->vocabulary->id(),
]);
$term = Term::load(55);
$this->assertEquals($this->vocabulary->id(), $term->bundle());
// Make sure terms get created, with proper language.
$this->drush(DevelGenerateCommands::TERMS, ['10'], [
'kill' => NULL,
'bundles' => $this->vocabulary->id(),
'languages' => 'fr',
]);
$term = Term::load(60);
$this->assertEquals('fr', $term->language()->getId());
// Make sure terms gets created, with proper translation.
$this->drush(DevelGenerateCommands::TERMS, ['10'], [
'kill' => NULL,
'bundles' => $this->vocabulary->id(),
'languages' => 'fr',
'translations' => 'de',
]);
$term = Term::load(70);
$this->assertTrue($term->hasTranslation('de'));
$this->assertTrue($term->hasTranslation('fr'));
}
/**
* Tests generating vocabularies.
*/
public function testDrushGenerateVocabs(): void {
// Make sure vocabs get created.
$this->drush(DevelGenerateCommands::VOCABS, ['5'], ['kill' => NULL]);
$vocabs = Vocabulary::loadMultiple();
$this->assertGreaterThan(4, count($vocabs));
$vocab = array_pop($vocabs);
$this->assertNotEmpty($vocab);
}
/**
* Tests generating menus.
*/
public function testDrushGenerateMenus(): void {
$generatedMenu = NULL;
// Make sure menus, and with correct properties.
$this->drush(DevelGenerateCommands::MENUS, ['1', '5'], ['kill' => NULL]);
$menus = Menu::loadMultiple();
foreach ($menus as $menu) {
if (str_contains($menu->id(), 'devel-')) {
// We have a menu that we created.
$generatedMenu = $menu;
break;
}
}
$link = MenuLinkContent::load(5);
$this->assertNotNull($generatedMenu, 'Generated menu successfully.');
$this->assertNotNull($link, 'Generated link successfully.');
$this->assertEquals($generatedMenu->id(), $link->getMenuName(), 'Generated menu ID matches link menu name.');
}
/**
* Tests generating content.
*/
public function testDrushGenerateContent(): void {
// Generate content using the minimum parameters.
$this->drush(DevelGenerateCommands::CONTENT, ['21']);
$node = Node::load(21);
$this->assertNotEmpty($node);
// Make sure articles get comments. Only one third of articles will have
// comment status 'open' and therefore the ability to receive a comment.
// However, generating 30 articles will give the likelihood of test failure
// (i.e. no article gets a comment) as 2/3 ^ 30 = 0.00052% or 1 in 191751.
$this->drush(DevelGenerateCommands::CONTENT, ['30', '9'], [
'kill' => NULL,
'bundles' => 'article',
]);
$comment = Comment::load(1);
$this->assertNotEmpty($comment);
// Generate content with a higher number that triggers batch running.
$this->drush(DevelGenerateCommands::CONTENT, ['55'], ['kill' => NULL]);
$nodes = \Drupal::entityQuery('node')->accessCheck(FALSE)->execute();
$this->assertCount(55, $nodes);
$messages = $this->getErrorOutput();
$this->assertStringContainsStringIgnoringCase('Finished 55 elements created successfully.', $messages, 'devel-generate-content batch ending message not found');
// Generate specified language. Verify base field is populated.
$this->drush(DevelGenerateCommands::CONTENT, ['10'], [
'kill' => NULL,
'languages' => 'fr',
'base-fields' => 'phish',
]);
$nodes = \Drupal::entityQuery('node')->accessCheck(FALSE)->execute();
$node = Node::load(end($nodes));
$this->assertEquals('fr', $node->language()->getId());
$this->assertNotEmpty($node->get('phish')->getString());
// Generate content with translations.
$this->drush(DevelGenerateCommands::CONTENT, ['18'], [
'kill' => NULL,
'languages' => 'fr',
'translations' => 'de',
]);
// Only articles are enabled for translations.
$articles = \Drupal::entityQuery('node')
->accessCheck(FALSE)
->condition('type', 'article')
->execute();
$pages = \Drupal::entityQuery('node')
->accessCheck(FALSE)
->condition('type', 'page')
->execute();
$this->assertCount(18, $articles + $pages);
// Check that the last article has 'de' and 'fr' but no 'ca' translation.
$node = Node::load(end($articles));
$this->assertTrue($node->hasTranslation('de'));
$this->assertTrue($node->hasTranslation('fr'));
$this->assertFalse($node->hasTranslation('ca'));
// Generate just page content with option --add-type-label.
// Note: Use the -v verbose option to get the ending message shown when not
// generating enough to trigger batch mode.
// @todo Remove -v when the messages are shown for both run types.
$this->drush(DevelGenerateCommands::CONTENT . ' -v', ['9'], [
'kill' => NULL,
'bundles' => 'page',
'add-type-label' => NULL,
]);
// Count the page nodes.
$nodes = \Drupal::entityQuery('node')
->accessCheck(FALSE)
->condition('type', 'page')
->execute();
$this->assertCount(9, $nodes);
$messages = $this->getErrorOutput();
$this->assertStringContainsStringIgnoringCase('Created 9 nodes', $messages, 'batch end message not found');
// Load the final node and verify that the title starts with the label.
$node = Node::load(end($nodes));
$this->assertEquals('Basic Page - ', substr($node->title->value, 0, 13));
// Generate articles with a specified users.
$this->drush(DevelGenerateCommands::CONTENT . ' -v', ['10'], [
'kill' => NULL,
'bundles' => 'article',
'authors' => '2',
]);
// Count the nodes assigned to user 2. We have two other users (0 and 1) so
// if the code was broken and users were assigned randomly the chance that
// this fauly would be detected is 1 - (1/3 ** 10) = 99.998%.
$nodes = \Drupal::entityQuery('node')
->accessCheck(FALSE)
->condition('type', 'article')
->condition('uid', ['2'], 'IN')
->execute();
$this->assertCount(10, $nodes);
// Generate page content using the 'roles' option to select authors based
// on the roles that the user has. For this we need a new user with a
// distinct role.
$userA = $this->drupalCreateUser(['access content']);
$roleA = $userA->getRoles()[1];
$this->drush(DevelGenerateCommands::CONTENT . ' -v', ['8'], [
'kill' => NULL,
'bundles' => 'page',
'roles' => $roleA,
]);
// Count the number of nodes assigned to User A. There are three other users
// so if the code was broken and authors assigned randomly, the chance that
// this test would detect the fault is 1 - (1/4 ^ 8) = 99.998%.
$nodesA = \Drupal::entityQuery('node')
->accessCheck(FALSE)
->condition('type', 'page')
->condition('uid', $userA->id())
->execute();
$this->assertCount(8, $nodesA, 'User A should have all the generated content');
// Repeat the above using two roles and two users.
$userB = $this->drupalCreateUser(['create page content']);
$roleB = $userB->getRoles()[1];
$this->drush(DevelGenerateCommands::CONTENT . ' -v', ['20'], [
'kill' => NULL,
'bundles' => 'page',
'roles' => sprintf('%s, %s', $roleA, $roleB),
]);
// Count the nodes assigned to users A and B. There are three other users
// so if the code was broken and users were assigned randomly the chance
// that the test would detect the fault is 1 - (2/5 ^ 20) = 99.999%.
$nodesA = \Drupal::entityQuery('node')
->accessCheck(FALSE)
->condition('type', 'page')
->condition('uid', $userA->id())
->execute();
$nodesB = \Drupal::entityQuery('node')
->accessCheck(FALSE)
->condition('type', 'page')
->condition('uid', $userB->id())
->execute();
$this->assertGreaterThan(0, count($nodesA), 'User A should have some content');
$this->assertGreaterThan(0, count($nodesB), 'User B should have some content');
$this->assertCount(20, $nodesA + $nodesB);
}
/**
* Tests generating media.
*/
public function testDrushGenerateMedia(): void {
// Create two media types.
$media_type1 = $this->createMediaType('image');
$media_type2 = $this->createMediaType('audio_file');
// Make sure media items gets created with batch process.
$this->drush(DevelGenerateCommands::MEDIA, ['53'], [
'kill' => NULL,
'base-fields' => 'phish',
]);
$this->assertCount(53, \Drupal::entityQuery('media')
->accessCheck(FALSE)
->execute());
$messages = $this->getErrorOutput();
$this->assertStringContainsStringIgnoringCase('Finished 53 elements created successfully.', $messages, 'devel-generate-media batch ending message not found');
$medias = \Drupal::entityQuery('media')->accessCheck(FALSE)->execute();
$media = Media::load(end($medias));
// Verify that base field populates.
$this->assertNotEmpty($media->get('phish')->getString());
// Test also with a non-batch process. We're testing also --kill here.
$this->drush(DevelGenerateCommands::MEDIA, ['7'], [
'media-types' => $media_type1->id() . ',' . $media_type2->id(),
'kill' => NULL,
]);
$this->assertCount(7, \Drupal::entityQuery('media')
->accessCheck(FALSE)
->execute());
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Drupal\Tests\devel_generate\Traits;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Language\Language;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
/**
* Provides methods to assist Devel Generate testing.
*
* Referenced in DevelGenerateBrowserTestBase and DevelGenerateCommandsTest.
*/
trait DevelGenerateSetupTrait {
use CommentTestTrait;
use EntityReferenceFieldCreationTrait;
/**
* Vocabulary for testing generation of terms.
*
* @var \Drupal\taxonomy\VocabularyInterface
*/
protected $vocabulary;
/**
* Second vocabulary for testing generation of terms.
*
* @var \Drupal\taxonomy\VocabularyInterface
*/
protected $vocabulary2;
/**
* General set-up for all tests.
*/
public function setUpData(): void {
// Create user with devel_generate permissions and access to admin/content.
$admin_user = $this->drupalCreateUser([
'administer devel_generate',
'access devel information',
'access content overview',
]);
$this->drupalLogin($admin_user);
$entity_type_manager = $this->container->get('entity_type.manager');
// Create Basic page and Article node types.
if ($this->profile != 'standard') {
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic Page']);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
$this->addDefaultCommentField('node', 'article');
}
// Enable translation for article content type (but not for page).
\Drupal::service('content_translation.manager')->setEnabled('node', 'article', TRUE);
// Create languages for generated translations.
ConfigurableLanguage::createFromLangcode('ca')->save();
ConfigurableLanguage::createFromLangcode('de')->save();
ConfigurableLanguage::createFromLangcode('fr')->save();
// Creating a vocabulary to associate taxonomy terms generated.
$this->vocabulary = Vocabulary::create([
'name' => 'Vocab 1 ' . $this->randomString(15),
'description' => $this->randomMachineName(),
'vid' => 'vocab_1_' . mb_strtolower($this->randomMachineName()),
'langcode' => Language::LANGCODE_NOT_SPECIFIED,
]);
$this->vocabulary->save();
// Enable translation for terms in this vocabulary.
\Drupal::service('content_translation.manager')->setEnabled('taxonomy_term', $this->vocabulary->id(), TRUE);
// Creates a field of an entity reference field storage on article.
$field_name = 'taxonomy_' . $this->vocabulary->id();
$handler_settings = [
'target_bundles' => [
$this->vocabulary->id() => $this->vocabulary->id(),
],
'auto_create' => TRUE,
];
$this->createEntityReferenceField('node', 'article', $field_name, '', 'taxonomy_term', 'default', $handler_settings, FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
$entity_type_manager->getStorage('entity_form_display')
->load('node.article.default')
->setComponent($field_name, [
'type' => 'options_select',
])
->save();
$entity_type_manager->getStorage('entity_view_display')
->load('node.article.default')
->setComponent($field_name, [
'type' => 'entity_reference_label',
])
->save();
// Create the second vocabulary.
$this->vocabulary2 = Vocabulary::create([
'name' => 'Vocab 2 ' . $this->randomString(15),
'vid' => 'vocab_2_' . mb_strtolower($this->randomMachineName()),
'langcode' => Language::LANGCODE_NOT_SPECIFIED,
]);
$this->vocabulary2->save();
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Drupal\Tests\devel_generate\Unit;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\devel_generate\DevelGeneratePluginManager;
use Drupal\devel_generate_example\Plugin\DevelGenerate\ExampleDevelGenerate;
use Drupal\Tests\UnitTestCase;
use PHPUnit\Framework\MockObject\MockObject;
/**
* @coversDefaultClass \Drupal\devel_generate\DevelGeneratePluginManager
* @group devel_generate
*/
class DevelGenerateManagerTest extends UnitTestCase {
/**
* The plugin discovery.
*/
protected MockObject|DiscoveryInterface $discovery;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Mock the plugin discovery.
$this->discovery = $this->createMock(DiscoveryInterface::class);
$this->discovery->expects($this->any())
->method('getDefinitions')
->willReturnCallback(function (): array {
return $this->getMockDefinitions();
});
}
/**
* Test creating an instance of the DevelGenerateManager.
*/
public function testCreateInstance(): void {
$namespaces = new \ArrayObject(['Drupal\devel_generate_example' => realpath(__DIR__ . '/../../../modules/devel_generate_example/lib')]);
$cache_backend = $this->createMock(CacheBackendInterface::class);
$module_handler = $this->createMock(ModuleHandlerInterface::class);
$entity_type_manager = $this->createMock(EntityTypeManager::class);
$messenger = $this->createMock(MessengerInterface::class);
$language_manager = $this->createMock(LanguageManagerInterface::class);
$string_translation = $this->createMock(TranslationInterface::class);
$entityFieldManager = $this->createMock(EntityFieldManagerInterface::class);
$manager = new DevelGeneratePluginManager(
$namespaces,
$cache_backend,
$module_handler,
$entity_type_manager,
$messenger,
$language_manager,
$string_translation,
$entityFieldManager,
);
// Use reflection to set the protected discovery property.
$reflection = new \ReflectionClass($manager);
$property = $reflection->getProperty('discovery');
$property->setValue($manager, $this->discovery);
$container = new ContainerBuilder();
$time = $this->createMock(TimeInterface::class);
$container->set('entity_type.manager', $entity_type_manager);
$container->set('messenger', $messenger);
$container->set('language_manager', $language_manager);
$container->set('module_handler', $module_handler);
$container->set('string_translation', $string_translation);
$container->set('entity_field.manager', $entityFieldManager);
$container->set('datetime.time', $time);
\Drupal::setContainer($container);
$example_instance = $manager->createInstance('devel_generate_example');
$plugin_def = $example_instance->getPluginDefinition();
$this->assertInstanceOf(ExampleDevelGenerate::class, $example_instance);
$this->assertArrayHasKey('url', $plugin_def);
$this->assertTrue($plugin_def['url'] == 'devel_generate_example');
}
/**
* Callback function to return mock definitions.
*
* @return array
* The mock of devel generate plugin definitions.
*/
public function getMockDefinitions(): array {
return [
'devel_generate_example' => [
'id' => 'devel_generate_example',
'class' => ExampleDevelGenerate::class,
'url' => 'devel_generate_example',
'dependencies' => [],
],
];
}
}