-
Writing Tests for Drush Commands
There are plenty of examples of these in the wild, but I figured I’d show a stripped down version of an automated Kernel test that successfully tests a drush command. The trick here is making sure you set up a logger and that you stub a few methods (if you happen to use $this->logger() and dt() in your Drush commands). Also featured in this example is the use of Faker to generate realistic test data.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150<?phpdeclare(strict_types = 1);namespace Drupal\mymodule\Tests\Kernel {use Drupal\KernelTests\KernelTestBase;use Drupal\mymodule\Drush\Commands\QueueCommands;use Drush\Log\DrushLoggerManager;use Faker\Factory;use Faker\Generator;use Psr\Log\LoggerInterface;/*** Tests the QueueCommands Drush command file.*/final class QueueTest extends KernelTestBase {/*** {@inheritdoc}*/protected static $modules = [// Enable all the modules that this test relies on.'auto_entitylabel','block','datetime','eck','field','file','image','link','mymodule','mysql','options','path','path_alias','system','text','user','views','views_bulk_operations',];/*** The Faker generator.** @var \Faker\Generator*/protected Generator $faker;/*** The logger.** @var \Drush\Log\DrushLoggerManager|\Psr\Log\LoggerInterface*/protected DrushLoggerManager|LoggerInterface $logger;/*** The Drush queue commands.** @var \Drupal\mymodule\Drush\Commands\QueueCommands*/protected QueueCommands $drushQueueCommands;/*** Set up the test environment.*/protected function setUp(): void {parent::setUp();// Install required config.$this->installConfig(['mymodule']);$this->faker = Factory::create();$logger_class = class_exists(DrushLoggerManager::class) ? DrushLoggerManager::class : LoggerInterface::class;$this->logger = $this->prophesize($logger_class)->reveal();$drushQueueCommands = new QueueCommands();$drushQueueCommands->setLogger($this->logger);}/*** Tests the mymodule:close-all-registration-entities drush command.*/public function testCloseAllRegistrationEntities() {// Create a single registration entity.// Just demoing simple Faker usage for blog post.$registration = \Drupal::entityTypeManager()->getStorage('registration')->create(['type' => 'appt','field_echeckin_link' => $this->faker->url(),'field_epic_id' => $this->faker->randomNumber(9),'field_first_name' => $this->faker->firstName(),'field_has_echeckin_link' => $this->faker->boolean(),'field_outcome' => NULL,'field_phone_number' => $this->faker->phoneNumber(),'field_sequence_is_open' => 1,]);$registration->save();// This method exists in my module's Drush command file.// It sets field_sequence_is_open field to 0 for all open reg entities.// drush mymodule:close-all-registration-entities.$this->drushQueueCommands->closeAllRegistrationEntities();// Assert that the registration entity was updated.$this->assertCount(1, \Drupal::entityTypeManager()->getStorage('registration')->loadByProperties(['field_sequence_is_open' => 0]));}}}namespace {if (!function_exists('dt')) {/*** Stub for dt().** @param string $message* The text.* @param array $replace* The replacement values.** The text.*/function dt($message, array $replace = []): string {return strtr($message, $replace);}}if (!function_exists('drush_op')) {/*** Stub for drush_op.** @param callable $callable* The function to call.*/function drush_op(callable $callable) {$args = func_get_args();array_shift($args);return call_user_func_array($callable, $args);}}}I learned this via the migrate_tools project here.
-
Skip Empty Values During Concatenation in a Drupal 10 Migration
UPDATE. You can do the same as below without the need for a custom module function. (source)
123456789field_address_full:- plugin: callbackcallable: array_filtersource:- address_1- city- state- plugin: concatdelimiter: ', '
This quick example shows one method to ignore empty values in a Drupal 10 migration concatenate process.
Code
Migrate code:
12345678910111213141516process:temp_address_parts:-plugin: skip_on_emptymethod: processsource:- address_1- city- state-plugin: callbackcallable: mymodule_migrate_filter_emptyfield_address_full:plugin: concatdelimiter: ', 'source: '@temp_address_parts'Module code:
12345678/*** Filters out empty values from an array.*/function mymodule_migrate_filter_empty($values): array {return array_filter($values, function ($value) {return !empty($value);});}Result
Example data:
1234address_1,city,state1 Main St.,Portland,ME2 Main St.,Portland,Portland,MEResulting field_address_full plain text value
1231 Main St., Portland, ME1 Main St., PortlandPortland, ME -
Drupal Migrate API skip_on_multiple Process Plugin
Here’s a Migrate API process plugin I wrote in Drupal 9 that skips a property or row when more than one value exists. My use case:
- My source data has locations; each location has multiple associated organizations
- My new Drupal site has locations; each location has a parent organization
- I want to only populate the field_parent_org field (an entity reference) if there is a single organization value in the source data for the location
- I’m using a custom module called “pdms_migrate”
I’ve stripped out all of the noise from the examples below… hopefully it’s enough to help you understand the example:
-
Setting a LIMIT on an UPDATE query in Drupal 8 / 9
Here’s a quick tip regarding Drupal database manipulation. Specifically, I needed a way to flip a boolean value (from 1 to 0) on the first N rows in a database table that matched a specific set of conditions.
The Problem
It seems you cannot enforce a LIMIT on an UPDATE query using a static query in Drupal.
Here’s what I was trying to do:
1234567$db = \Drupal::database();$query = $db->query('UPDATE {mytable} SET ondemand=:zero WHERE ondemand=:one ORDER BY imported_at ASC LIMIT :limit', [':zero' => 0,':one' => 1,':limit' => $qtyToConvert,]);$query->execute();This throws an error. I believe the issue is caused by the variable substitution wrapping the limit value in quotes. The error message starts with:
1SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''10'' at line 1...If you run this query manually in MySQL, it works fine with
LIMIT 10
but doesn’t work withLIMIT '10'
(it throws the same error shown above).The Solution
Given dynamic queries are favored over static queries, and that this static query doesn’t work, I ended up getting to the finish line with two dynamic queries.
I have tested the solution on 10,000 records and it took < 6 seconds to process the entire request on my local dev lando site.
“code” is a unique identifier column in the database table.
12345678910111213$db = \Drupal::database();$query = $db->select('mytable', 'mt')->condition('ondemand', 1)->orderBy('imported_at')->fields('mt', ['code'])->range(0, $qtyToConvert);$codesToReset = $query->execute()->fetchCol();$db->update('mytable')->fields(['ondemand' => 0])->condition('code', $codesToReset, 'IN')->execute();return count($codesToReset); -
Missing Titles on Drupal 8 ECK Content Listings
UPDATE: I discovered the solution below was only working when the Title base field was enabled for the entity type. If you enable the title field on the entity type it works, but then the title is required to save a new record. I will look for an alternative solution to avoid using the Title field altogether. Also, I’ll look into https://www.drupal.org/project/eck/issues/2785297.
I’ve recently run into an issue with an ECK-created custom entity type. There was no reason for me to use the “Title” field on this particular entity type. Unfortunately, when you don’t have a title field several of the built-in entity listings and displays seem broken. Here’s what the Content List screen looks like, for example:
-
Adding Custom Fields to the Field Edit Screen in Drupal 8 / 9
Introduction
I’m building an API with Drupal 8/9. There is additional information I want to track against each field instance in each content type (or custom entity type) in the system. This information includes where the field’s data comes from, who is responsible for maintaining it, etc. This is useful for the site administrator and developers to track where things are coming from. Drupal’s Third Party Settings functionality makes this easy.
Result
This is what you see when you edit the First name field:
When you click Save settings the data is saved to this specific field’s instance configuration (meaning if you reuse this field you can fill the info out differently per instance).
Solution
There are two ways to achieve this. In both cases, we use hook_form_FORM_ID_alter() to introduce the new fields. I am doing this within a custom module called pdms. The code lives in pdms.module.
Method #1:
With this method, the system automatically handles storing the value into the field configuration’s third party settings.
This happens because we use the third_party_settings key.
123456789101112131415/*** Implements hook_form_FORM_ID_alter().*/function pdms_form_field_config_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) {$entity = $form_state->getFormObject()->getEntity();$form['third_party_settings']['pdms']['canonical_source'] = ['#type' => 'textarea','#title' => t('Canonical source'),'#description' => t('From where does this data originate?'),'#default_value' => $entity->getThirdPartySetting('pdms', 'canonical_source'),];... etc ...}The issue I had with this approach is that I could not get it to work if I put the fields in a fieldset (as shown in the screenshot).
I think it’d be a matter of getting $entity->getThirdPartySetting() to look into the fieldset somehow. If you know how to do this, please let me know!
Method #2:
This method gives us more control over what happens when the form is submitted.
Because we have more control we’re able to traverse the fieldset easily.
You can see, too, how I unset the third party setting if the value is empty. I am not sure if I’ll keep this in place; we’ll see if it causes any issues when I attempt to build a report showing all of the fields and their info.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869/*** Implements hook_form_FORM_ID_alter().*/function pdms_form_field_config_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) {$entity = $form_state->getFormObject()->getEntity();$form['#entity_builders'][] = 'pdms_set_third_party_settings_field_info';$form['pdms_field_info'] = ['#type' => 'fieldset','#title' => 'MySite Field Info',];$form['pdms_field_info']['canonical_source'] = ['#type' => 'textfield','#title' => t('Canonical source'),'#description' => t('From where does this data originate?'),'#default_value' => $entity->getThirdPartySetting('pdms', 'canonical_source'),];$form['pdms_field_info']['canonical_source_location'] = ['#type' => 'textfield','#title' => t('Canonical source location'),'#description' => t('Where within the canonical source can we find this data?'),'#default_value' => $entity->getThirdPartySetting('pdms', 'canonical_source_location'),];$form['pdms_field_info']['author'] = ['#type' => 'textfield','#title' => t('Author'),'#description' => t('Who maintains this data at the canonical source?'),'#default_value' => $entity->getThirdPartySetting('pdms', 'author'),];$form['pdms_field_info']['author_contact_info'] = ['#type' => 'textfield','#title' => t('Author contact info'),'#description' => t('How do we contact the author?'),'#default_value' => $entity->getThirdPartySetting('pdms', 'author_contact_info'),];$form['pdms_field_info']['known_usages'] = ['#type' => 'textarea','#title' => t('Known usages'),'#description' => t('Who/what is using this data and how are they using it?<br/>Separate each item with a blank line.<br/><strong>Format:</strong> source: description</strong>'),'#default_value' => $entity->getThirdPartySetting('pdms', 'known_usages'),];}/*** Entity builder for PDMS field info third party settings.*/function pdms_set_third_party_settings_field_info($entity_type, $entity, &$form, \Drupal\Core\Form\FormStateInterface $form_state) {$fields = ['canonical_source','canonical_source_location','author','author_contact_info','known_usages',];foreach ($fields as $field_name) {if ($form_state->getValue($field_name)) {$entity->setThirdPartySetting('pdms', $field_name, $form_state->getValue($field_name));}else {$entity->unsetThirdPartySetting('pdms', $field_name);}}