-
-
Notifications
You must be signed in to change notification settings - Fork 28
[#2401] Added drupal_helpers, generated_content, and testmode modules #2414
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e372992
4320de3
dd29a92
a065933
9437570
536b76e
6c4818b
b43c966
a7721fa
5b23845
7a6a857
38baad2
4d535be
05921ac
865abb7
f9a863a
022d1b4
42a1e12
1b1a410
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| --- | ||
| sidebar_position: 6 | ||
| --- | ||
|
|
||
| # Drupal helpers | ||
|
|
||
| [Drupal helpers](https://www.drupal.org/project/drupal_helpers) is a utility | ||
| library that provides static facade helpers for common Drupal development tasks, | ||
| primarily intended for use within deploy hooks and update scripts. | ||
|
|
||
| ## Helper facade | ||
|
|
||
| The `Helper` class provides convenient access to helper services without needing | ||
| dependency injection: | ||
|
|
||
| ```php | ||
| use Drupal\drupal_helpers\Helper; | ||
|
|
||
| // Create taxonomy terms. | ||
| Helper::term()->createTree('tags', ['News', 'Events', 'Blog']); | ||
|
|
||
| // Create menu links. | ||
| Helper::menu()->createTree('main', [ | ||
| 'About' => '/about', | ||
| 'Contact' => '/contact', | ||
| ]); | ||
|
|
||
| // Delete all entities of a type. | ||
| Helper::entity()->deleteAll('node', 'article'); | ||
| ``` | ||
|
|
||
| ## Available helpers | ||
|
|
||
| | Helper | Access | Common operations | | ||
| |--------|--------|-------------------| | ||
| | Term | `Helper::term()` | `createTree()`, `deleteAll()`, `find()` | | ||
| | Menu | `Helper::menu()` | `createTree()`, `deleteTree()`, `findItem()`, `updateItem()` | | ||
| | Entity | `Helper::entity()` | `deleteAll()`, `batch()` | | ||
| | Config | `Helper::config()` | Import and manage config YAML | | ||
| | User | `Helper::user()` | Create accounts, assign roles | | ||
| | Redirect | `Helper::redirect()` | Create redirects, import from CSV | | ||
| | Field | `Helper::field()` | Delete fields with data purging | | ||
|
|
||
| ## Batched operations | ||
|
|
||
| For large datasets, pass a `$sandbox` array to enable automatic batching: | ||
|
|
||
| ```php | ||
| function ys_base_deploy_update_articles(array &$sandbox): ?string { | ||
| return Helper::entity($sandbox)->batch('node', 'article', function ($node) { | ||
| $node->set('field_migrated', TRUE); | ||
| $node->save(); | ||
| }); | ||
| } | ||
| ``` | ||
|
|
||
| ## Example in Vortex | ||
|
|
||
| The [`ys_demo.deploy.php`](https://github.com/drevops/vortex/blob/main/web/modules/custom/ys_demo/ys_demo.deploy.php) | ||
| file demonstrates using drupal_helpers to create a menu link for the articles | ||
| page during deployment. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| --- | ||
| sidebar_position: 7 | ||
| --- | ||
|
|
||
| # Generated content | ||
|
|
||
| [Generated content](https://www.drupal.org/project/generated_content) provides | ||
| a plugin-based system for programmatically generating deterministic content | ||
| entities. Unlike random dummy content, generated content produces reproducible | ||
| sets useful for visual regression testing and consistent demo environments. | ||
|
|
||
| ## Plugin system | ||
|
|
||
| Content generators are PHP classes placed in a module's | ||
| `src/Plugin/GeneratedContent/` directory, annotated with the `#[GeneratedContent]` | ||
| attribute. | ||
|
|
||
| ### Creating a plugin | ||
|
|
||
| ```php | ||
| namespace Drupal\ys_demo\Plugin\GeneratedContent; | ||
|
|
||
| use Drupal\generated_content\Attribute\GeneratedContent; | ||
| use Drupal\generated_content\Plugin\GeneratedContent\GeneratedContentPluginBase; | ||
| use Drupal\taxonomy\Entity\Term; | ||
|
|
||
| #[GeneratedContent( | ||
| id: 'ys_demo_taxonomy_term_tags', | ||
| entity_type: 'taxonomy_term', | ||
| bundle: 'tags', | ||
| weight: 10, | ||
| )] | ||
| class TaxonomyTermTags extends GeneratedContentPluginBase { | ||
|
|
||
| public function generate(): array { | ||
| $entities = []; | ||
|
|
||
| foreach (['Technology', 'Science', 'Health'] as $name) { | ||
| $term = Term::create(['vid' => 'tags', 'name' => $name]); | ||
| $term->save(); | ||
| $entities[] = $term; | ||
| } | ||
|
|
||
| return $entities; | ||
| } | ||
|
|
||
| } | ||
| ``` | ||
|
|
||
| ### Attribute parameters | ||
|
|
||
| | Parameter | Type | Required | Description | | ||
| |-----------|------|----------|-------------| | ||
| | `id` | string | yes | Unique plugin ID | | ||
| | `entity_type` | string | yes | Target entity type (`node`, `taxonomy_term`, etc.) | | ||
| | `bundle` | string | yes | Target bundle | | ||
| | `weight` | int | no | Execution order (lower = earlier) | | ||
| | `tracking` | bool | no | Track created entities for cleanup (default: `TRUE`) | | ||
| | `helper` | string | no | Custom helper class extending `GeneratedContentHelper` | | ||
|
|
||
| ### Cross-referencing entities | ||
|
|
||
| Use `weight` to control execution order and `$this->helper` to reference | ||
| previously generated entities: | ||
|
|
||
| ```php | ||
| // In a node plugin with weight: 20 (runs after terms at weight: 10). | ||
| $tags = $this->helper::randomTerms('tags', 3); | ||
| $node->set('field_tags', $tags); | ||
| ``` | ||
|
|
||
| ## Triggering generation | ||
|
|
||
| ### Drush command | ||
|
|
||
| ```shell | ||
| drush generated-content:create-content | ||
| drush generated-content:create-content node article | ||
| ``` | ||
|
|
||
| ### Admin UI | ||
|
|
||
| Visit `/admin/config/development/generated-content` to generate content through | ||
| the admin interface. | ||
|
|
||
| ### Environment variable | ||
|
|
||
| Set `GENERATED_CONTENT_CREATE=1` before provisioning to auto-generate content | ||
| on module install. Optionally filter: | ||
|
|
||
| ```shell | ||
| GENERATED_CONTENT_ITEMS="taxonomy_term-tags,node-article" | ||
| ``` | ||
|
|
||
| ## Example in Vortex | ||
|
|
||
| The `ys_demo` module ships two generated content plugins: | ||
|
|
||
| - `TaxonomyTermTags` — generates 5 taxonomy terms in the `tags` vocabulary | ||
| - `NodeArticle` — generates 20 article nodes referencing generated tags |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| --- | ||
| sidebar_position: 8 | ||
| --- | ||
|
|
||
| # Test mode | ||
|
|
||
| [Testmode](https://www.drupal.org/project/testmode) filters site content during | ||
| Behat tests, preventing live or generated content from interfering with test | ||
| assertions. | ||
|
|
||
| ## How it works | ||
|
|
||
| 1. Test content follows a naming convention — titles prefixed with `[TEST]` | ||
| 2. Views are registered in testmode configuration | ||
| 3. Behat scenarios tagged with `@testmode` automatically enable/disable filtering | ||
| 4. When enabled, registered views only show content matching the `[TEST]` pattern | ||
|
|
||
| ## Configuration | ||
|
|
||
| Testmode is configured via `testmode.settings`: | ||
|
|
||
| | Key | Type | Description | | ||
| |-----|------|-------------| | ||
| | `views_node` | string[] | Node view machine names to filter | | ||
| | `views_term` | string[] | Term view machine names to filter | | ||
| | `views_user` | string[] | User view machine names to filter | | ||
| | `pattern_node` | string[] | MySQL LIKE patterns for node titles | | ||
| | `pattern_term` | string[] | MySQL LIKE patterns for term names | | ||
| | `pattern_user` | string[] | MySQL LIKE patterns for user emails | | ||
|
|
||
| ### Registering a view programmatically | ||
|
|
||
| Use deploy hooks to register views with testmode: | ||
|
|
||
| ```php | ||
| function ys_demo_deploy_configure_testmode(): string { | ||
| $testmode = \Drupal\testmode\Testmode::getInstance(); | ||
|
|
||
| $views = $testmode->getNodeViews(); | ||
| if (!in_array('my_view', $views)) { | ||
| $views[] = 'my_view'; | ||
| $testmode->setNodeViews($views); | ||
| } | ||
|
|
||
| return 'Configured testmode for my_view.'; | ||
| } | ||
| ``` | ||
|
|
||
| ## Behat integration | ||
|
|
||
| The `@testmode` tag activates test mode for individual scenarios via | ||
| `TestmodeTrait` from | ||
| [behat-steps](https://github.com/drevops/behat-steps): | ||
|
|
||
| ```gherkin | ||
| @testmode | ||
| Scenario: Articles view shows only test content | ||
| Given article content: | ||
| | title | status | | ||
| | [TEST] Test mode article | 1 | | ||
| | Regular production article | 1 | | ||
| When I visit "/articles" | ||
| Then I should see "[TEST] Test mode article" | ||
| And I should not see "Regular production article" | ||
| ``` | ||
|
|
||
| The `[TEST]` prefix in content titles matches the default `[TEST%` pattern | ||
| configured in testmode. Only matching content appears in registered views. | ||
|
|
||
| ## Example in Vortex | ||
|
|
||
| The `ys_demo` module: | ||
|
|
||
| - Ships an articles view at `/articles` | ||
| - Registers it with testmode via a deploy hook | ||
| - Includes a Behat feature demonstrating the `@testmode` tag |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -76,6 +76,7 @@ | |
| "seckit", | ||
| "shellvar", | ||
| "simpletest", | ||
| "testmode", | ||
| "standardise", | ||
| "updatedb", | ||
| "uselagoon", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -162,7 +162,7 @@ public function process(): void { | |
| File::remove($path); | ||
| } | ||
|
|
||
| File::remove($t . '/tests/behat/features/counter.feature'); | ||
| static::removeDemoBehatFeatures($t); | ||
| } | ||
|
|
||
| if (!in_array(self::SEARCH, $selected)) { | ||
|
|
@@ -203,4 +203,27 @@ protected function discoverModulePrefix(): ?string { | |
| return empty($path) ? NULL : str_replace(['_base', '_core'], '', basename($path)); | ||
| } | ||
|
|
||
| /** | ||
| * Remove Behat feature files tagged with @demo. | ||
| * | ||
| * Scans the Behat features directory for .feature files whose first line | ||
| * contains the @demo tag and removes them. | ||
| * | ||
| * @param string $dir | ||
| * The base directory to search in. | ||
| */ | ||
| protected static function removeDemoBehatFeatures(string $dir): void { | ||
| $features_dir = $dir . '/tests/behat/features'; | ||
|
|
||
| if (!is_dir($features_dir)) { | ||
| return; | ||
| } | ||
|
|
||
| $files = File::findContainingInDir($features_dir, '@demo'); | ||
|
|
||
| foreach ($files as $file) { | ||
| File::remove($file); | ||
| } | ||
| } | ||
|
Comment on lines
+206
to
+227
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify File::findContainingInDir implementation for matching behavior
# Find the File utility class
fd -t f "File.php" --exec grep -l "findContainingInDir" {} \;
# Show the implementation
ast-grep --pattern $'function findContainingInDir($$$) {
$$$
}'
# Alternative: search for the method implementation with context
rg -n -A 30 'function findContainingInDir' --type phpRepository: drevops/vortex Length of output: 40 🏁 Script executed: # First, let's look at the CustomModules.php file to understand the context
cat -n ".vortex/installer/src/Prompts/Handlers/CustomModules.php" | head -50Repository: drevops/vortex Length of output: 1582 🏁 Script executed: # Search for File class references in the codebase
rg -n "class File" --type phpRepository: drevops/vortex Length of output: 40 🏁 Script executed: # Search for findContainingInDir method anywhere in the repository
rg -n "findContainingInDir" --type phpRepository: drevops/vortex Length of output: 40 🏁 Script executed: # Check imports in CustomModules.php
rg -n "use.*File" ".vortex/installer/src/Prompts/Handlers/CustomModules.php"Repository: drevops/vortex Length of output: 99 🏁 Script executed: # Find the File.php in the Utils directory
fd -t f "File.php" -p "*Utils*"Repository: drevops/vortex Length of output: 152 🏁 Script executed: # List all methods in the File class
find ".vortex/installer/src/Utils" -name "File.php" -exec cat {} \;Repository: drevops/vortex Length of output: 3574 🏁 Script executed: # Search for all method definitions in File class
rg -n "public|protected|private" ".vortex/installer/src/Utils/File.php" | grep -E "(function|public|protected|private)"Repository: drevops/vortex Length of output: 542 🌐 Web query:
💡 Result:
Signature public static function findContainingInDir(
string $directory,
string $needle,
array $excluded = []
): arrayParameters
Return
Citations:
🌐 Web query:
💡 Result:
Sources: [1] [2] Fix docblock and add word-boundary validation for Two issues confirmed:
🤖 Prompt for AI Agents |
||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix first-line heading level (MD041).
First line should be an H1 to satisfy markdownlint.
Suggested fix
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 1-1: First line in a file should be a top-level heading
(MD041, first-line-heading, first-line-h1)
🤖 Prompt for AI Agents