-
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.
-
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:
-
Conditionally Triggering Salesforce Push Operations in Drupal 8
The user interface for the Salesforce Suite in Drupal 8 is fantastic. The suite itself is incredibly powerful. Among the many options, for each mapping you create you can specify what operations should trigger a synchronization. Here’s an example:
We recently faced an issue with the “Drupal entity update” trigger being called too often. The salesforce_push module implements hook_entity_update() , which gets called a lot. After looking at how the functions are called in salesforce_push.module I realized there were two choices:
-
Patch for Drupal 8.7.14 SA-CORE-2020-004, SA-CORE-2020-005, and SA-CORE-2020-006
I have a site that’s temporarily stuck on 8.7.14; it’s not worth the risk to update to 8.8.8 right now.
I was able to diff 8.8.7 and 8.8.8 to figure out what changes were made for these security announcements:
- Drupal core – Critical – Cross-Site Request Forgery – SA-CORE-2020-004
- Drupal core – Critical – Arbitrary PHP code execution – SA-CORE-2020-005
- Drupal core – Less critical – Access bypass – SA-CORE-2020-006
I could then compare the changes with 8.7.14 to create the patch below, which applies against 8.7.14 without any issues.
DISCLAIMER: The formal recommendation is to update to 8.8.8 if at all possible. Use the patch below at your own risk.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.incindex 348724d6..e9b7cbc1 100644--- a/core/includes/bootstrap.inc+++ b/core/includes/bootstrap.inc@@ -673,11 +673,17 @@ function drupal_valid_test_ua($new_prefix = NULL) {// Ensure that no information leaks on production sites.$test_db = new TestDatabase($prefix);$key_file = DRUPAL_ROOT . '/' . $test_db->getTestSitePath() . '/.htkey';- if (!is_readable($key_file)) {+ if (!is_readable($key_file) || is_dir($key_file)) {header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');exit;}$private_key = file_get_contents($key_file);+ // The string from drupal_generate_test_ua() is 74 bytes long. If we don't+ // have it, tests cannot be allowed.+ if (empty($private_key) || strlen($private_key) < 74) {+ header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');+ exit;+ }// The file properties add more entropy not easily accessible to others.$key = $private_key . filectime(__FILE__) . fileinode(__FILE__);$time_diff = REQUEST_TIME - $time;diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.phpindex 4bf64196..7bacc473 100644--- a/core/lib/Drupal/Core/Form/FormBuilder.php+++ b/core/lib/Drupal/Core/Form/FormBuilder.php@@ -18,6 +18,7 @@use Drupal\Core\Theme\ThemeManagerInterface;use Symfony\Component\EventDispatcher\EventDispatcherInterface;use Symfony\Component\HttpFoundation\FileBag;+use Symfony\Component\HttpFoundation\ParameterBag;use Symfony\Component\HttpFoundation\RequestStack;use Symfony\Component\HttpFoundation\Response;@@ -955,8 +956,16 @@ public function doBuildForm($form_id, &$element, FormStateInterface &$form_state// This value is checked in self::handleInputElement().$form_state->setInvalidToken(TRUE);+ // Ignore all submitted values.+ $form_state->setUserInput([]);++ $request = $this->requestStack->getCurrentRequest();+ // Do not trust any POST data.+ $request->request = new ParameterBag();// Make sure file uploads do not get processed.- $this->requestStack->getCurrentRequest()->files = new FileBag();+ $request->files = new FileBag();+ // Ensure PHP globals reflect these changes.+ $request->overrideGlobals();}}}diff --git a/core/lib/Drupal/Core/Form/FormValidator.php b/core/lib/Drupal/Core/Form/FormValidator.phpindex 57d24cc2..2afcb52b 100644--- a/core/lib/Drupal/Core/Form/FormValidator.php+++ b/core/lib/Drupal/Core/Form/FormValidator.php@@ -124,10 +124,8 @@ public function validateForm($form_id, &$form, FormStateInterface &$form_state)* {@inheritdoc}*/public function setInvalidTokenError(FormStateInterface $form_state) {- $url = $this->requestStack->getCurrentRequest()->getRequestUri();-// Setting this error will cause the form to fail validation.- $form_state->setErrorByName('form_token', $this->t('The form has become outdated. Copy any unsaved work in the form below and then <a href=":link">reload this page</a>.', [':link' => $url]));+ $form_state->setErrorByName('form_token', $this->t('The form has become outdated. Press the back button, copy any unsaved work in the form, and then reload the page.'));}/**diff --git a/core/modules/file/tests/src/Functional/FileManagedFileElementTest.php b/core/modules/file/tests/src/Functional/FileManagedFileElementTest.phpindex 0b6254d8..db8e7fbe 100644--- a/core/modules/file/tests/src/Functional/FileManagedFileElementTest.php+++ b/core/modules/file/tests/src/Functional/FileManagedFileElementTest.php@@ -45,7 +45,7 @@ public function testManagedFile() {$file_field_name => \Drupal::service('file_system')->realpath($test_file->getFileUri()),];$this->drupalPostForm(NULL, $edit, t('Save'));- $this->assertText('The form has become outdated. Copy any unsaved work in the form below');+ $this->assertText('The form has become outdated.');$last_fid = $this->getLastFileId();$this->assertEqual($last_fid_prior, $last_fid, 'File was not saved when uploaded with an invalid form token.');diff --git a/core/modules/jsonapi/src/Controller/EntityResource.php b/core/modules/jsonapi/src/Controller/EntityResource.phpindex 5c43d3ee..e21903c2 100644--- a/core/modules/jsonapi/src/Controller/EntityResource.php+++ b/core/modules/jsonapi/src/Controller/EntityResource.php@@ -315,7 +315,7 @@ public function patchIndividual(ResourceType $resource_type, EntityInterface $en));}$data += ['attributes' => [], 'relationships' => []];- $field_names = array_merge(array_keys($data['attributes']), array_keys($data['relationships']));+ $field_names = array_map([$resource_type, 'getInternalName'], array_merge(array_keys($data['attributes']), array_keys($data['relationships'])));array_reduce($field_names, function (EntityInterface $destination, $field_name) use ($resource_type, $parsed_entity) {$this->updateEntityField($resource_type, $parsed_entity, $destination, $field_name);diff --git a/core/modules/jsonapi/src/Controller/FileUpload.php b/core/modules/jsonapi/src/Controller/FileUpload.phpindex b07dd51d..f8517fd7 100644--- a/core/modules/jsonapi/src/Controller/FileUpload.php+++ b/core/modules/jsonapi/src/Controller/FileUpload.php@@ -112,6 +112,7 @@ public function __construct(AccountInterface $current_user, EntityFieldManagerIn* created file entity.*/public function handleFileUploadForExistingResource(Request $request, ResourceType $resource_type, $file_field_name, FieldableEntityInterface $entity) {+ $file_field_name = $resource_type->getInternalName($file_field_name);$field_definition = $this->validateAndLoadFieldDefinition($resource_type->getEntityTypeId(), $resource_type->getBundle(), $file_field_name);static::ensureFileUploadAccess($this->currentUser, $field_definition, $entity);@@ -138,7 +139,7 @@ public function handleFileUploadForExistingResource(Request $request, ResourceTy$entity->save();$route_parameters = ['entity' => $entity->uuid()];- $route_name = sprintf('jsonapi.%s.%s.related', $resource_type->getTypeName(), $file_field_name);+ $route_name = sprintf('jsonapi.%s.%s.related', $resource_type->getTypeName(), $resource_type->getPublicName($file_field_name));$related_url = Url::fromRoute($route_name, $route_parameters)->toString(TRUE);$request = Request::create($related_url->getGeneratedUrl(), 'GET', [], $request->cookies->all(), [], $request->server->all());return $this->httpKernel->handle($request, HttpKernelInterface::SUB_REQUEST);@@ -161,6 +162,7 @@ public function handleFileUploadForExistingResource(Request $request, ResourceTy* Thrown when there are validation errors.*/public function handleFileUploadForNewResource(Request $request, ResourceType $resource_type, $file_field_name) {+ $file_field_name = $resource_type->getInternalName($file_field_name);$field_definition = $this->validateAndLoadFieldDefinition($resource_type->getEntityTypeId(), $resource_type->getBundle(), $file_field_name);static::ensureFileUploadAccess($this->currentUser, $field_definition);@@ -182,7 +184,7 @@ public function handleFileUploadForNewResource(Request $request, ResourceType $r/* $self_link = new Link(new CacheableMetadata(), $this->entity->toUrl('jsonapi'), ['self']); */$links = new LinkCollection(['self' => $self_link]);- $relatable_resource_types = $resource_type->getRelatableResourceTypesByField($file_field_name);+ $relatable_resource_types = $resource_type->getRelatableResourceTypesByField($resource_type->getPublicName($file_field_name));$file_resource_type = reset($relatable_resource_types);$resource_object = ResourceObject::createFromEntity($file_resource_type, $file);return new ResourceResponse(new JsonApiDocumentTopLevel(new ResourceObjectData([$resource_object], 1), new NullIncludedData(), $links), 201, []);diff --git a/core/modules/system/tests/src/Functional/Form/FormTest.php b/core/modules/system/tests/src/Functional/Form/FormTest.phpindex 849944b0..62ae3c54 100644--- a/core/modules/system/tests/src/Functional/Form/FormTest.php+++ b/core/modules/system/tests/src/Functional/Form/FormTest.php@@ -244,21 +244,27 @@ public function testInputWithInvalidToken() {$this->assertSession()->elementExists('css', 'input[name="form_token"]')->setValue('invalid token');+ $random_string = $this->randomString();$edit = [- 'textfield' => $this->randomString(),+ 'textfield' => $random_string,'checkboxes[bar]' => TRUE,'select' => 'bar','radios' => 'foo',];$this->drupalPostForm(NULL, $edit, 'Submit');$this->assertFieldByXpath('//div[contains(@class, "error")]', NULL, 'Error message is displayed with invalid token even when required fields are filled.');- $this->assertText('The form has become outdated. Copy any unsaved work in the form below');- // Verify that input elements retained the posted values.- $this->assertFieldByName('textfield', $edit['textfield']);++ $assert = $this->assertSession();+ $element = $assert->fieldExists('textfield');+ $this->assertEmpty($element->getValue());+ $assert->responseNotContains($random_string);+ $this->assertText('The form has become outdated.');+ // Ensure that we don't use the posted values.+ $this->assertFieldByName('textfield', '');$this->assertNoFieldChecked('edit-checkboxes-foo');- $this->assertFieldChecked('edit-checkboxes-bar');- $this->assertOptionSelected('edit-select', 'bar');- $this->assertFieldChecked('edit-radios-foo');+ $this->assertNoFieldChecked('edit-checkboxes-bar');+ $this->assertOptionSelected('edit-select', '');+ $this->assertNoFieldChecked('edit-radios-foo');// Check another form that has a textarea input.$this->drupalGet(Url::fromRoute('form_test.required'));@@ -271,9 +277,9 @@ public function testInputWithInvalidToken() {];$this->drupalPostForm(NULL, $edit, 'Submit');$this->assertFieldByXpath('//div[contains(@class, "error")]', NULL, 'Error message is displayed with invalid token even when required fields are filled.');- $this->assertText('The form has become outdated. Copy any unsaved work in the form below');- $this->assertFieldByName('textfield', $edit['textfield']);- $this->assertFieldByName('textarea', $edit['textarea']);+ $this->assertText('The form has become outdated.');+ $this->assertFieldByName('textfield', '');+ $this->assertFieldByName('textarea', '');// Check another form that has a number input.$this->drupalGet(Url::fromRoute('form_test.number'));@@ -281,12 +287,14 @@ public function testInputWithInvalidToken() {->elementExists('css', 'input[name="form_token"]')->setValue('invalid token');$edit = [- 'integer_step' => mt_rand(1, 100),+ // We choose a random value which is higher than the default value,+ // so we don't accidentally generate the default value.+ 'integer_step' => mt_rand(6, 100),];$this->drupalPostForm(NULL, $edit, 'Submit');$this->assertFieldByXpath('//div[contains(@class, "error")]', NULL, 'Error message is displayed with invalid token even when required fields are filled.');- $this->assertText('The form has become outdated. Copy any unsaved work in the form below');- $this->assertFieldByName('integer_step', $edit['integer_step']);+ $this->assertText('The form has become outdated.');+ $this->assertFieldByName('integer_step', 5);// Check a form with a Url field$this->drupalGet(Url::fromRoute('form_test.url'));@@ -298,8 +306,9 @@ public function testInputWithInvalidToken() {];$this->drupalPostForm(NULL, $edit, 'Submit');$this->assertFieldByXpath('//div[contains(@class, "error")]', NULL, 'Error message is displayed with invalid token even when required fields are filled.');- $this->assertText('The form has become outdated. Copy any unsaved work in the form below');- $this->assertFieldByName('url', $edit['url']);+ $this->assertText('The form has become outdated.');+ $this->assertFieldByName('url', '');+}/**diff --git a/core/modules/system/tests/src/Functional/Form/ValidationTest.php b/core/modules/system/tests/src/Functional/Form/ValidationTest.phpindex 094a0af7..50451a98 100644--- a/core/modules/system/tests/src/Functional/Form/ValidationTest.php+++ b/core/modules/system/tests/src/Functional/Form/ValidationTest.php@@ -68,7 +68,7 @@ public function testValidate() {$this->drupalPostForm(NULL, ['name' => 'validate'], 'Save');$this->assertNoFieldByName('name', '#value changed by #validate', 'Form element #value was not altered.');$this->assertNoText('Name value: value changed by setValueForElement() in #validate', 'Form element value in $form_state was not altered.');- $this->assertText('The form has become outdated. Copy any unsaved work in the form below');+ $this->assertText('The form has become outdated.');}/**diff --git a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.phpindex 8d20db5c..d2dec0c6 100644--- a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php+++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php@@ -799,12 +799,30 @@ public function testInvalidToken($expected, $valid_token, $user_is_authenticated$expected_form = $form_id();$form_arg = $this->getMockForm($form_id, $expected_form);+ // Set up some request data so we can be sure it is removed when a token is+ // invalid.+ $this->request->request->set('foo', 'bar');+ $_POST['foo'] = 'bar';+$form_state = new FormState();$input['form_id'] = $form_id;$input['form_token'] = $form_token;+ $input['test'] = 'example-value';$form_state->setUserInput($input);- $this->simulateFormSubmission($form_id, $form_arg, $form_state, FALSE);+ $form = $this->simulateFormSubmission($form_id, $form_arg, $form_state, FALSE);$this->assertSame($expected, $form_state->hasInvalidToken());+ if ($expected) {+ $this->assertEmpty($form['test']['#value']);+ $this->assertEmpty($form_state->getValue('test'));+ $this->assertEmpty($_POST);+ $this->assertEmpty(iterator_to_array($this->request->request->getIterator()));+ }+ else {+ $this->assertEquals('example-value', $form['test']['#value']);+ $this->assertEquals('example-value', $form_state->getValue('test'));+ $this->assertEquals('bar', $_POST['foo']);+ $this->assertEquals('bar', $this->request->request->get('foo'));+ }}public function providerTestInvalidToken() {diff --git a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php b/core/tests/Drupal/Tests/Core/Form/FormTestBase.phpindex ef08ab77..36605065 100644--- a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php+++ b/core/tests/Drupal/Tests/Core/Form/FormTestBase.php@@ -173,7 +173,7 @@ protected function setUp() {->getMock();$this->account = $this->getMock('Drupal\Core\Session\AccountInterface');$this->themeManager = $this->getMock('Drupal\Core\Theme\ThemeManagerInterface');- $this->request = new Request();+ $this->request = Request::createFromGlobals();$this->eventDispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');$this->requestStack = new RequestStack();$this->requestStack->push($this->request);diff --git a/core/tests/Drupal/Tests/Core/Form/FormValidatorTest.php b/core/tests/Drupal/Tests/Core/Form/FormValidatorTest.phpindex 18f0be28..10f54905 100644--- a/core/tests/Drupal/Tests/Core/Form/FormValidatorTest.php+++ b/core/tests/Drupal/Tests/Core/Form/FormValidatorTest.php@@ -131,7 +131,7 @@ public function testValidateInvalidFormToken() {->getMock();$form_state->expects($this->once())->method('setErrorByName')- ->with('form_token', 'The form has become outdated. Copy any unsaved work in the form below and then <a href="/test/example?foo=bar">reload this page</a>.');+ ->with('form_token', 'The form has become outdated. Press the back button, copy any unsaved work in the form, and then reload the page.');$form_state->setValue('form_token', 'some_random_token');$form_validator->validateForm('test_form_id', $form, $form_state);$this->assertTrue($form_state->isValidationComplete()); -
Storing First and Last Name with Social API for Drupal 8
I recently worked on a project that relied on Social API for Google, Facebook, and Yahoo authentication. Out of the box everything worked great. Our site stores the user’s first name and last name in two fields on the user entity: field_first_name and field_last_name. There are a few ways to tap into the Social API authentication process (which comes from the Social Auth component).
Originally we leveraged the SocialAuthEvents::USER_CREATED event in an event subscriber. We set the first name and last name, then re-saved the user. This, however, executes after the user is created. The issue we had was that we also have Salesforce integration and both name fields are required in Salesforce. When the user is first saved it was pushing to Salesforce without the name fields, which triggered an error.
The solution was to implement a different event handler: SocialAuthEvents::USER_FIELDS
This lets you manipulate the user fields before the user is saved. I poked through the Social Auth code and figured out how to get the first and last name values. Here’s the working solution:
web/modules/custom/mymodule/mymodule.services.yml
12345services:mymodule.social_auth_subscriber:class: Drupal\mymodule\EventSubscriber\MymoduleSocialAuthSubscribertags:- { name: 'event_subscriber' }web/modules/custom/mymodule/src/EventSubscriber/MymoduleSocialAuthSubscriber.php
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758<?phpnamespace Drupal\mymodule\EventSubscriber;use Drupal\social_auth\Event\SocialAuthEvents;use Symfony\Component\EventDispatcher\EventSubscriberInterface;use Drupal\social_auth\Event\UserFieldsEvent;/*** Subscribe to Social API events.** @package Drupal\social_auth_subscriber\EventSubscriber*/class MymoduleSocialAuthSubscriber implements EventSubscriberInterface {/*** {@inheritdoc}** Returns an array of event names this subscriber wants to listen to.*/public static function getSubscribedEvents() {$events[SocialAuthEvents::USER_FIELDS] = ['onUserFields'];return $events;}/*** Populates name fields before user creation.** @param \Drupal\social_auth\Event\UserFieldsEvent $event* The Social Auth user event object.*/public function onUserFields(UserFieldsEvent $event) {$fields = $event->getUserFields();$user = $event->getSocialAuthUser();$first_name = $user->getFirstName();$last_name = $user->getLastName();$fullname = $user->getName();list($first_name_fallback, $last_name_fallback) = explode(' ', $fullname);if (!empty($first_name)) {$fields['field_first_name'] = ucfirst($first_name);}elseif (!empty($first_name_fallback)) {$fields['field_first_name'] = ucfirst($first_name_fallback);}if (!empty($last_name)) {$fields['field_last_name'] = ucfirst($last_name);}elseif (!empty($last_name_fallback)) {$fields['field_last_name'] = ucfirst($last_name_fallback);}$event->setUserFields($fields);}}After creating/updating the files above, just clear the caches and test.
-
Overriding “Required Message” on Composite Fields in Webform (Drupal 8)
The more commonly used fields in Webform (text, radios, etc.) offer the ability to set a custom Required message string. This is the text that shows in the error message when a required field is not filled.
Here’s what that screen looks like when you’re configuring a field on the Build screen:
There are several element types that do not provide a UI field to set this Required message value. The composite Name and Advanced Address field are two examples I encountered. For these fields you have to dig a little deeper, but it’s easy once you get the hang of it. Visit the Advanced tab for the field, then scroll to the bottom where you’ll see a Custom Settings fieldset.
-
Using Drupal 8 Persistent Login Module on Platform.sh
The Persistent Login module relies an extra cookie to maintain a Drupal user’s session. The configuration screen for this module lets you specify a prefix for the cookie. The default is “PL”. If you are using the site on HTTPS, the module prepends an “S” to the cookie it creates. Here’s an example cookie name: SPLfa0650d6d985433d455a3b15cc70fd9b .Platform.sh lets you configure cookie cache settings via routes.yaml .If you’re using Persistent Login you must tell the system not to ignore this cookie.Here’s a standard (and simple) routes.yaml file on a Drupal 8 site:
12345678"https://{all}/":type: upstreamupstream: "app:http"cache:enabled: true# Base the cache on the session cookie.# Ignore all other cookies.cookies: ['/^SS?ESS/', '/^Drupal.visitor/']
12345678"https://{all}/":type: upstreamupstream: "app:http"cache:enabled: true# Base the cache on the session cookie and persistent login cookie.# Ignore all other cookies.cookies: ['/^SS?ESS/', '/^S?PL/', '/^Drupal.visitor/']There is a bit more discussion around how these cookies are handled here: https://www.drupal.org/node/2898065/discuss
-
Overriding a Webform Confirmation Message in Drupal 8
There are a few reasons you may need to override a webform confirmation:
- Out of the box webform doesn’t allow you to add some tags (e.g., <button>) to a webform confirmation message. When you save it strips them out.
- What if you needed to alter the webform message based on the values submitted in the webform?
- What if you needed to alter the webform message based on where the user saw the webform?
- etc…
Thankfully, you can use a standard preprocess hook to override the confirmation output.
In this particular example I needed to override the output of the message to include a button tag. I needed this to only happen if specific GET params had specific values.
First, I modified the confirmation message settings as shown here: