Drupal 10
Drupal 10
Selwyn Polit
This book is for sale at http://leanpub.com/drupal10
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing
process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and
many iterations to get reader feedback, pivot until you have the right book and build traction once
you do.
1: Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
2: Blocks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
2.1: Create a block with Drush generate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
2.2: Anatomy of a custom block with dependency injection . . . . . . . . . . . . . . . . . . . . 10
2.3: Create a block with an entityQuery . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.4: Create a Block with a corresponding config form . . . . . . . . . . . . . . . . . . . . . . . 14
2.4.1: The config form definition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.4.2: The routing.yml file . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.4.3: The Block definition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.5: Modify a block with hook_block_view_alter or hook_block_build_alter . . . . . . . . . 24
2.6: Disable caching in a block . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.7: Add a configuration form to your block . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.8: Block display not updating after changing block content . . . . . . . . . . . . . . . . . . . 29
2.9: Block Permission (blockAccess) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
2.9.1: Blocks shouldn’t talk to the router, NodeRouteContext and friends should . . . . 31
2.9.2: Values returned by blockAccess() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
7: CRON . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
7.1: Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
7.2: How does it work? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
7.3: Enable Drupal Cron . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
7.4: The cron command . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
7.5: Setting up cron . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
7.6: Disable Drupal cron . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
7.7: hook_cron() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
7.8: Common inquiries regarding cron jobs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
7.8.1: When did the cron job last run? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
7.8.2: How to stop Cron from continuously executing things? . . . . . . . . . . . . . . . . 96
7.8.3: Resolving the ip and name for cron . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
7.9: Resources: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
9: Debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
9.1: Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
9.2: Enable error reporting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
9.3: Disable caches and enable Twig debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
9.4: Enable/Disable Xdebug . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
9.5: Xdebug Port . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
9.6: Drupal code debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
9.7: Command line or drush debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
9.8: Add a breakpoint in code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
9.9: Troubleshooting Xdebug with DDEV . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
9.9.1: Could not connect to debugging client . . . . . . . . . . . . . . . . . . . . . . . . . . 129
9.9.2: PhpStorm refuses to debug . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
9.9.2.1: Curl . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
9.9.2.2: Logs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
9.9.2.3: Telnet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
9.9.2.4: Is Xdebug enabled? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
9.10: What is listening on the debug port? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
9.11: Enable twig debugging output in source . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
9.12: Devel and Devel Kint Extras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
9.12.1: Setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
9.12.2: Add kint to a custom module . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
9.12.3: Dump variables in a TWIG template . . . . . . . . . . . . . . . . . . . . . . . . . . 134
9.12.4: Kint::dump . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
9.12.5: Set max levels to avoid running out of memory . . . . . . . . . . . . . . . . . . . . 134
9.13: Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
23: Getting off the Island (formerly Reaching out of Drupal) . . . . . . . . . . . . . . . . . . . 386
23.1: Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 386
23.2: Guzzle example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 386
23.3: Guzzle POST example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 387
23.4: Magic methods to send synchronous requests . . . . . . . . . . . . . . . . . . . . . . . . . 387
23.5: HTTP basic authentication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 388
23.6: Exception handling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 389
CONTENTS
1 https://www.drupal.org/
2 https://stackoverflow.com/questions/tagged/drupal
3 https://events.drupal.org/neworleans2016/sessions/drupal-way-philosophy
4 https://www.drupal.org/project/examples
2: Blocks
Blocks are plugins, which are reusable pieces of code following design patterns. Plugins are also
used to define views arguments, field formatters, field widgets, etc. The source files for blocks are
found in each module’s /src/Plugin directory.
1 https://www.drupal.org/docs/drupal-apis/plugin-api/plugin-api-overview
2 https://www.drupal.org/docs/8/api/plugin-api/annotations-based-plugins
Blocks 3
44
45 �
46
47 Would you like to create block plugin? \[No\]:
48
49 �
50
51 Would you like to create a controller? \[No\]:
52
53 �
54
55 Would you like to create settings form? \[No\]:
56
57 �
58
59 The following directories and files have been created or updated:
60
61 ----------------------------------------------------------
62
63 •
64 /Users/selwyn/Sites/ddev93/web/modules/custom/block_module/block_module.info.yml
65
66 •
67 /Users/selwyn/Sites/ddev93/web/modules/custom/block_module/block_module.module
Use drush generate to create the code for a block. Specify the module name (e.g. block_module)
so Drush knows where to put the block code. We also must give the block an admin label, plugin
ID, and class.
15 Plugin ID \[block_module_block_module_example\]:
16
17 �
18
19 Plugin class \[BlockModuleExampleBlock\]:
20
21 �
22
23 Block category \[Custom\]:
24
25 �
26
27 Make the block configurable? \[No\]:
28
29 �
30
31 Would you like to inject dependencies? \[No\]:
32
33 �
34
35 Create access callback? \[No\]:
36
37 �
38
39 The following directories and files have been created or updated:
40
41 ----------------------------------------------------------
42
43 •
44 /Users/selwyn/Sites/ddev93/web/modules/block_module/src/Plugin/Block/BlockModuleExam\
45 pleBlock.php
1 <?php
2
3 namespace Drupal\block_module\Plugin\Block;
4
5 use Drupal\Core\Block\BlockBase;
6
7 /**
8 * Provides a block module example block.
9 *
10 * @Block(
11 * id = "block_module_block_module_example",
12 * admin_label = @Translation("Block Module Example"),
13 * category = @Translation("Custom")
14 * )
15 */
16 class BlockModuleExampleBlock extends BlockBase {
17
18 /**
19 * {@inheritdoc}
20 */
21 public function build() {
22 $build['content'] = [
23 '#markup' => $this->t('It works!'),
24 ];
25 return $build;
26 }
27
28 }
In Drupal, navigate to /admin/structure/block and place the block (”block module example”) in the
content area. See the diagram below on how to place the block in the content area.
Blocks 7
Blocks 8
You may have to clear the Drupal cache again to get the new block to show up in the list. After
clicking “Place block,” a “Configure block” screen appears. You can safely just click “Save block.”
Blocks 9
Navigate back to the home page of the site and you’ll see your block appearing. Screenshot below:
Blocks 10
You can safely remove the block via the block layout page, choose “remove” from the dropdown
next to your “Block Module Example”
Blocks 11
Specify namespace:
namespace Drupal\abc_wea\Plugin\Block;
Blocks always extend BlockBase but can also implement other interfaces… see below.
Class ImageGalleryBlock extends BlockBase
Be sure to include:
1 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
1 use Drupal\Core\Annotation\Translation;
1 /**
2 * Hello World Salutation block.
3 *
4 * @Block(
5 * id = "hello_world_salutation_block",
6 * admin_label = @Translation("Hello world salutation"),
7 * category = @Translation("Custom")
8 * )
9 */
Or like this:
Blocks 12
1 /**
2 * Provides an image gallery block.
3 *
4 * @Block(
5 * id = "ig_product_image_gallery",
6 * admin_label = @Translation("Product Image Gallery"),
7 * category = @Translation("Image Display"),
8 * context = {
9 * "node" = @ContextDefinition(
10 * "entity:node",
11 * label = @Translation("Current Node")
12 * )
13 * }
14 * )
15 */
In most cases you will implement ContainerFactoryPluginInterface. Plugins require this for
dependency injection. So don’t forget:
1 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
2
3 class HelloWorldSalutationBlock extends BlockBase implements ContainerFactoryPluginI\
4 nterface {
1 /**
2 * {@inheritdoc}
3 */
4 public static function create(ContainerInterface $container, array $configuration, $\
5 plugin_id, $plugin_definition) {
6 return new static(
7 $configuration,
8 $plugin_id,
9 $plugin_definition,
10 $container->get('hello_world.salutation')
11 );
12 }
Blocks 13
Here are your __constructor() and a build() functions. See the 4th param–
HelloWorldSalutationService $salutation–that’s the injected service.
1 /**
2 * Construct.
3 *
4 * @param array $configuration
5 * A configuration array containing information about the plugin instance.
6 * @param string $plugin_id
7 * The plugin_id for the plugin instance.
8 * @param string $plugin_definition
9 * The plugin implementation definition.
10 * @param \Drupal\hello_world\HelloWorldSalutation $salutation
11 */
12 public function __construct(array $configuration, $plugin_id, $plugin_definition, He\
13 lloWorldSalutationService $salutation) {
14 parent::__construct($configuration, $plugin_id, $plugin_definition);
15 $this->salutation = $salutation;
16 }
1 /**
2 * {@inheritdoc}
3 */
4 public function build() {
5 return [
6 '#markup' => $this->salutation->getSalutation(),
7 ];
8 }
TODO: NEED A BETTER EXAMPLE OF A D.I. BLOCK HERE especially showing a build()
1 namespace Drupal\opinions_module\Plugin\Block;
2
3 use Drupal\Core\Block\BlockBase;
4 use Drupal\Core\Annotation\Translation;
5
6 /**
7 * Provides OpinionLanding Block.
8 *
9 * @Block(
10 * id = "opinion_landing",
11 * admin_label = @Translation("Opinion landing block"),
12 * )
13 *
14 * @package Drupal\oag_opinions\Plugin\Block
15 */
16 class OpinionLanding extends BlockBase {
17
18 public function build() {
19
20 $entity_type = 'node';
21 $storage = \Drupal::entityTypeManager()->getStorage($entity_type);
22 $query = \Drupal::entityQuery('node')
23 ->condition('type', 'page')
24 ->condition('status', 1) ;
25 $nids = $query->execute();
26 $nodes = $storage->loadMultiple($nids);
27
28 $render_array = [];
29 foreach ($nodes as $node) {
30 $render_array[] = [
31 '#type' => 'markup',
32 '#markup' => '<p>' . $node->getTitle(),
33 ];
34 }
35
36 return $render_array;
In the class are the getFormId(), getEditableConfigName(), buildForm() and submitForm() func-
tions which are all pretty straightforward.
1 /**
2 * Provides a cart block.
3 *
4 * @Block(
5 * id = "quick_pivot_subscribe_block",
6 * admin_label = @Translation("QuickPivot Subscribe Block"),
7 * category = @Translation("QuickPivot Subscribe")
8 * )
9 */
10 class QuickPivotSubscribeBlock extends BlockBase implements ContainerFactoryPluginIn\
11 terface {
1 <?php
2
3 namespace Drupal\quick_pivot\Form;
4
5 use Drupal\Core\Form\FormBase;
6 use Drupal\Core\Form\FormStateInterface;
7 use Drupal\Core\Form\FormBuilderInterface;
8 use Drupal\Core\Ajax\AjaxResponse;
9 use Drupal\Core\Ajax\ReplaceCommand;
10 use Drupal\Core\Ajax\CssCommand;
11 use Drupal\Core\Ajax\HtmlCommand;
12 use Drupal\Core\Ajax\AppendCommand;
13 use Drupal\quick_pivot\QuickPivotApiInterface;
14 use Symfony\Component\DependencyInjection\ContainerInterface;
15
16 /**
17 * Provides a form for users to subscribe to QuickPivot.
18 */
19 class QuickPivotSubscribeForm extends FormBase {
20
21 /**
22 * {@inheritdoc}
23 */
24 public function getFormId() {
25 return 'quick_pivot_subscribe_form';
26 }
27
28 /**
29 * {@inheritdoc}
30 */
31 public function buildForm(array $form, FormStateInterface $form_state) {
32 $form['#id'] = 'quick-pivot-subscribe-form';
33 $form['#cache'] = ['max-age' => 0];
34 $form['#attributes'] = ['autocomplete' => 'off'];
35
36 $form['email'] = [
Blocks 18
1 <?php
2
3 namespace Drupal\quick_pivot\Form;
4
5 use Drupal\Core\Form\FormStateInterface;
6 use Drupal\Core\Form\ConfigFormBase;
7
8 /**
9 * Configure Websphere settings for this site.
10 */
11 class QuickPivotConfigForm extends ConfigFormBase {
12
13 /**
14 * {@inheritdoc}
15 */
16 public function getFormId() {
17 return 'quick_pivot_settings';
18 }
19
20 /**
21 * {@inheritdoc}
22 */
23 protected function getEditableConfigNames() {
24 return ['quick_pivot.settings'];
25 }
26
27 /**
28 * {@inheritdoc}
29 */
30 public function buildForm(array $form, FormStateInterface $form_state) {
31
32 $config = $this->config('quick_pivot.settings');
33
34 $form['quick_pivot_settings'] = [
35 '#type' => 'details',
36 '#title' => $this->t('Quick Pivot API Settings'),
37 '#open' => TRUE,
38 '#weight' => 1,
39 ];
40
41 $form['quick_pivot_settings']['api_end_point'] = [
42 '#type' => 'textfield',
43 '#title' => $this->t('API End point'),
Blocks 21
87 oint'))
88 ->set('quick_pivot_settings.user_guid', $form_state->getValue('user_guid'))
89 ->set('quick_pivot_settings.account', $form_state->getValue('account'))
90 ->set('quick_pivot_settings.sender', $form_state->getValue('sender'))
91 ->save();
92
93 parent::submitForm($form, $form_state);
94 }
95
96 }
1 <?php
2
3 namespace Drupal\quick_pivot\Plugin\Block;
4
5 use Drupal\Core\Block\BlockBase;
6 use Symfony\Component\DependencyInjection\ContainerInterface;
7 use Drupal\Core\Config\ConfigFactoryInterface;
8 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
9 use Drupal\Core\Form\FormBuilderInterface;
10
11 /**
12 * Provides a cart block.
13 *
14 * @Block(
15 * id = "quick_pivot_subscribe_block",
16 * admin_label = @Translation("QuickPivot Subscribe Block"),
17 * category = @Translation("QuickPivot Subscribe")
18 * )
19 */
20 class QuickPivotSubscribeBlock extends BlockBase implements ContainerFactoryPluginIn\
21 terface {
22
23 /**
24 * The configuration factory.
25 *
26 * @var \Drupal\Core\Config\ConfigFactoryInterface
27 */
28 protected $configFactory;
29
30 /**
Blocks 23
1 quick_pivot.config:
2 path: '/admin/config/quick_pivot/settings'
3 defaults:
4 _form: 'Drupal\quick_pivot\Form\QuickPivotConfigForm'
5 _title: 'Quick Pivot Settings'
6 requirements:
7 _permission: 'administer site configuration'
And for the icing, We also specify a menu item so users can access the configuration form via the
menu system at docroot/modules/custom/quick_pivot/quick_pivot.links.menu.yml.
1 quick_pivot.config:
2 title: 'QuickPivot API settings'
3 description: 'Configure the QuickPivot API Settings.'
4 parent: system.admin_config_services
5 route_name: quick_pivot.config
6 weight: 1
1 function themename_preprocess_block(&$variables)
2 {
3 if ($variables['plugin_id'] == 'entity_browser_block:department_info') {
4 $variables['#attached']['library'][] = 'drupal/libraryname';
5 }
What’s described below could potentially be done on a theme preprocess for the block.
If you need to modify a block, you can supposedly use hook_block_view_alter or hook_block_-
build_alter, although I haven’t been able to make this work… hmm.
There is a comment that may be worth exploring at https://api.drupal.org/api/drupal/core%21modules%21block%21b
block_view_alter/8.2.x.
To alter the block content you must add a #pre_render in the hook_block_view_alter hook.
In https://drupal.stackexchange.com/a/215948 there is an example which fills in the $build['#pre_-
render'][] array with a string.
In an example on that stackexchange site, this function is provided:
1 /**
2 * Implements hook_block_build_alter().
3 */
4 function plug_academy_core_block_build_alter(array &$build, \Drupal\Core\Block\Block\
5 PluginInterface $block) {
6 if ($block->getPluginId() == 'system_menu_block:account') {
7 $build['#cache']['contexts'][] = 'url';
8 }
9 // else if ($block->getBaseId() === 'block_content') {
10 // if ($block->label() === "Home Page Alert") {
11 // $build['content'] = '<p>New content built here!</p>';
12 //
13 // }
14 // }
15 }
And I discovered an example from a project where the $build['#pre_render'][] array is populated
with a function. I’m not sure what that function did–presumably returned some text to be rendered.
Blocks 26
1 /**
2 * Implements hook_block_view_alter().
3 */
4 function pega_academy_core_block_view_alter(array &$build, \Drupal\Core\Block\BlockP\
5 luginInterface $block) {
6 if ($block->getBaseId() === 'block_content') {
7 if ($block->label() === "Home Page Alert") {
8 $build['#pre_render'][] = 'Drupal\pega_academy_core\Controller\DashboardContro\
9 ller::home_page_alert_prerender';
10 // $build['content'] = '<p>New content built here!</p>';
11
12 }
13 }
14 }
1 /**
2 * {@inheritdoc}
3 */
4 public function getCacheMaxAge() {
5 return 0;
6 }
1. defaultConfiguration
2. blockForm
3. blockSubmit
1 /**
2 * {@inheritdoc}
3 */
4 public function defaultConfiguration() {
5 // By default, the block will display 5 thumbnails.
6 return [
7 'block_count' => 5,
8 ];
9 }
1 /**
2 * {@inheritdoc}
3 */
4 public function blockForm($form, FormStateInterface $form_state) {
5 $range = range(2, 20);
6 $form['block_count'] = [
7 '#type' => 'select',
8 '#title' => $this->t('Number of product images in block'),
9 '#default_value' => $this->configuration['block_count'],
10 '#options' => array_combine($range, $range),
11 ];
12 return $form;
13 }
And blockSubmit() handles the submission of the config form. You don’t need to save anything.
The data is saved automatically into the Drupal config system. You just specify a configuration key
like $this->configuration['block_count'] and the rest is handled for you.
1 /**
2 * {@inheritdoc}
3 */
4 public function blockSubmit($form, FormStateInterface $form_state) {
5 $this->configuration['block_count'] = $form_state->getValue('block_count');
6 }
The build() method does all the work of building a render array to display whatever your block
wants to display. Here is an example of a build() function.
Blocks 28
1 /**
2 * {@inheritdoc}
3 */
4 public function build() {
5 $build = [];
6 $node = $this->getContextValue('node');
7
8 // Determine if we are on a page that points to a product.
9 $product = $this->getProduct($node);
10 if ($product) {
11
12 // Retrieve the product images
13 $image_data = $this->productManagerService->retrieveProductImages($product);
14 $block_count = $this->configuration['block_count'];
15 $item_count = 0;
16 $build['list'] = [
17 '#theme' => 'item_list',
18 '#items' => [],
19 ];
20
21 $build['list']['#items'][0] = [
22 '#type' => 'markup',
23 '#markup' => $this->t('There were no product images to display.')
24 ];
25
26 while ($item_count < $block_count && isset($image_data[$item_count])) {
27 $file = File::load($image_data[$item_count]['target_id']);
28 $link_text = [
29 '#theme' => 'image_style',
30 '#uri' => $file->getFileUri(),
31 '#style_name' => 'product_thumbnail',
32 '#alt' => $image_data[$item_count]['alt'],
33 ];
34
35 // Modal dialog
36 // see https://www.drupal.org/node/2488192 for more on modals
37 $options = [
38 'attributes' => [
39 'class' => [
40 'use-ajax',
41 ],
42 'data-dialog-type' => 'modal',
43 'data-dialog-options' => Json::encode([
Blocks 29
One last item. Configuration expects a schema for things being saved. Here we create a <module_-
name>.schema.yml in <module_name>/config/schema and it looks like:
resolve this, define a view mode and call content | render and assign the result to a variable like
this:
Be sure to surround the above code with curly brace and percentage sign delimeters. Unfortunately
these don’t always render correctly in this document so I’ve had to remove them for now.
Adding this render call will cause Drupal to render the content for that node, which will cause a
check of the caches and make sure the most current content is rendered.
Then add your fields:
1 {content.field_one} etc.
1 use Drupal\Core\Access\AccessResult;
2
3 $account = \Drupal::currentUser();
4
5
6 /**
7 * {@inheritdoc}
8 */
9 protected function blockAccess(AccountInterface $account) {
10 $route_name = $this->routeMatch->getRouteName();
11 if ($account->isAnonymous() && !in_array($route_name, ['user.login', 'user.logout'\
12 ])) {
13 return AccessResult::allowed()
14 ->addCacheContexts(['route.name', 'user.roles:anonymous']);
15 }
16 return AccessResult::forbidden();
17 }
1 /**
2 * Provides a 'Node Context Test' block.
3 *
4 * @Block(
5 * id = "node_block_test_context",
6 * label = @Translation("Node Context Test"),
7 * context_definitions = {
8 * "node" = @ContextDefinition("entity:node", label = @Translation("Node"))
9 * }
10 * )
11 */
This causes the block to be available only on various node pages (view, edit etc.). This can be
changed:
Blocks 32
1 * context_definitions = {
2 * "node" = @ContextDefinition("entity:node", label = @Translation("Node"),
3 * required = FALSE)
4 * }
The order of named options passed to ContextDefinition after the first argument does not matter.
Then in the block we check to make sure the user is viewing a node and that the user has view
rsvplist permission. See the code below:
More at https://drupal.stackexchange.com/questions/145823/how-do-i-get-the-current-node-
id/314152#314152
Note. While this practice is not recommended, the RSVP module does have an example of a
block talking to the router i.e. \Drupal::routeMatch() - see https://git.drupalcode.org/project/rsvp_
module/-/blob/1.0.x/src/Plugin/Block/RSVPBlock.php where the blockAccess() function grabs the
node parameter and acts on it.
Blocks 33
1 /**
2 * {@inheritdoc}
3 */
4 public function blockAccess(AccountInterface $account) {
5 /** @var \Drupal\node\Entity\Node $node */
6 $node = \Drupal::routeMatch()->getParameter('node');
7 $nid = $node->nid->value;
8 /** @var \Drupal\rsvp_module\EnablerService $enabler */
9 $enabler = \Drupal::service('rsvp_module.enabler');
10 if(is_numeric($nid)) {
11 if($enabler->isEnabled($node)) {
12 return AccessResult::allowedIfHasPermission($account, 'view rsvp_module');
13 }
14 }
15 return AccessResult::forbidden();
16 }
1 return AccessResult::forbidden();
2 return AccessResult::allowed();
3 return AccessResult::allowedIf(TRUE);
3: Batch Processing and the Drupal
Queue System
3.1: Batch Processing Using the Batch API
3.1.1: Overview
The Batch API provides very useful functionality that lets you do work by breaking it into pieces
to avoid PHP timeouts, etc. Usually, you’ll do this by creating a group of node id’s (using array_-
chunk)to be processed and use the batch API to process those arrays (chunks) of ids. You can also
provide code that will let it figure out how much work to do and stop itself.
In addition, you create a function to handle things once all the chunks are complete. You can also
give the Batch API a bunch of work to do and have it figure out for itself when it is finished.
Also, it’s useful that the Batch API uses the Drupal Queue system, allowing it to pick up where it
left off in case of problems.
You can use the Batch API in controllers, forms, hook updates, and Drush commands. The
implementation of each one is slightly different, as you can see in the examples.
Most often you start a batch from a form where you fill in some options and click a button. In the
case of a controller, you activate a batch when you point the browser at a URL. Drush commands
are typed in the terminal.
In the code: For a drush command, you use drush_backend_batch_process() to kick off the batch
(from DirtSalesforceController.php):
Although it will be covered in more detail in the future, it is useful to know that there is now a
batch builder: The BatchBuilder class1 for batch API. It provides a more streamlined object-oriented
approach to creating batches.
1 /**
2 * {@inheritdoc}
3 */
4 public function buildForm(array $form, FormStateInterface $form_state): array {
5 $form['message'] = [
6 '#markup' => $this->t('Click the button below to kick off the batch!'),
7 ];
8 $form['actions']['#type'] = 'actions';
9 $form['actions']['submit'] = [
10 '#type' => 'submit',
11 '#value' => $this->t('Run Batch'),
1 https://www.drupal.org/node/2875389
Batch Processing and the Drupal Queue System 36
12 ];
13
14 return $form;
15 }
1 /**
2 * {@inheritdoc}
3 */
4 public function submitForm(array &$form, FormStateInterface $form_state): void {
5 $this->updateEventPresenters();
6 $this->messenger()->addStatus($this->t('The message has been sent.'));
7 $form_state->setRedirect('<front>');
8 }
1 batch_examples.batch:
2 path: '/batch-examples/batchform'
3 defaults:
4 _title: 'Batch Form'
5 _form: 'Drupal\batch_examples\Form\BatchForm'
6 requirements:
7 _permission: 'access content'
Here is the updateEventPresenters() method. Notice the $operations array, which contains the
function to call to do the work of each batch as well as the list of nids to process.
14 // Submit batches.
15 $operations = [];
16 for ($batch_id = 0; $batch_id < $num_chunks; $batch_id++) {
17 $operations[] = [
18 '\Drupal\batch_examples\Form\BatchForm::exampleProcessBatch',
19 [
20 $batch_id+1,
21 $chunks[$batch_id]],
22 ];
23 }
24 $batch = [
25 'title' => $this->t('Updating Presenters'),
26 'init_message' => $this->t('Starting to process events.'),
27 'progress_message' => $this->t('Completed @current out of @total batches.'),
28 'finished' => '\Drupal\batch_examples\Form\BatchForm::batchFinished',
29 'error_message' => $this->t('Event processing has encountered an error.'),
30 'operations' => $operations,
31 ];
32 batch_set($batch);
33 }
Here is the method that actually does the work. Most of the code is for information reporting. The
actual work is in the foreach $nids as $nid loop:
The Form API will take care of getting the batches executed. If you aren’t using a form, you use
batch_process() like the line shown below. For this method, you specify any valid alias and the
system will redirect to that alias after the batch completes.
Also you can set up a $batch array with a title and a progress message with some variables that will
get displayed.
You specify a finished index, which identifies a function to call after the batch is finished processing,
as in the example below.
Here is the batchFinished() method, which displays and logs the results.
Batch Processing and the Drupal Queue System 39
1 use Symfony\Component\HttpFoundation\RedirectResponse;
2
3 /**
4 * Handle batch completion.
5 *
6 * @param bool $success
7 * TRUE if all batch API tasks were completed successfully.
8 * @param array $results
9 * An array of processed node IDs.
10 * @param array $operations
11 * A list of the operations that had not been completed.
12 * @param string $elapsed
13 * Batch.inc kindly provides the elapsed processing time in seconds.
14 */
15 public static function batchFinished(bool $success, array $results, array $operatio\
16 ns, string $elapsed): RedirectResponse {
17 $messenger = \Drupal::messenger();
18 if ($success) {
19 $messenger->addMessage(t('@process processed @count nodes, skipped @skipped, upd\
20 ated @updated, failed @failed in @elapsed.', [
21 '@process' => $results['process'],
22 '@count' => $results['progress'],
23 '@skipped' => $results['skipped'],
24 '@updated' => $results['updated'],
25 '@failed' => $results['failed'],
26 '@elapsed' => $elapsed,
27 ]));
28 \Drupal::logger('d9book')->info(
29 '@process processed @count nodes, skipped @skipped, updated @updated, failed @\
30 failed in @elapsed.', [
31 '@process' => $results['process'],
32 '@count' => $results['progress'],
33 '@skipped' => $results['skipped'],
34 '@updated' => $results['updated'],
35 '@failed' => $results['failed'],
36 '@elapsed' => $elapsed,
37 ]);
38 }
39 else {
40 // An error occurred.
41 // $operations contains the operations that remained unprocessed.
42 $error_operation = reset($operations);
43 $message = t('An error occurred while processing %error_operation with arguments\
Batch Processing and the Drupal Queue System 40
44 : @arguments', [
45 '%error_operation' => $error_operation[0],
46 '@arguments' => print_r($error_operation[1], TRUE),
47 ]);
48 $messenger->addError($message);
49 }
50 // Optionally redirect back to the form.
51 return new RedirectResponse('/batch-examples/batchform');
52 }
1 return batch_process('node/1');
After the batch is complete, Drupal will redirect you to that url. E.g. /node/1
Drupal API | Batch operations2
In this example of a processing function, you can see error handling, logging, and tracking while
retrieving files from a remote source. This is fairly common when moving data between systems.
The rest of the code is almost identical to the previous example.
16 $query = \Drupal::entityQuery('node')
17 ->condition('status', 1)
18 ->condition('type', 'opinion_request');
19 $total_nids = $query->execute();
20 $context['sandbox']['max'] = count($total_nids);
21 }
22
23 // Keep track of progress.
24 $context['results']['progress'] += count($nids);
25 $context['results']['process'] = 'Import request files';
26 // Message above progress bar.
27 $context['message'] = t('Processing batch #@batch_id batch size @batch_size for to\
28 tal @count items.',[
29 '@batch_id' => number_format($batch_id),
30 '@batch_size' => number_format(count($nids)),
31 '@count' => number_format($context['sandbox']['max']),
32 ]);
33
34 $fileRepository = \Drupal::service('file.repository');
35 foreach ($nids as $nid) {
36 $filename = "";
37 $request_node = Node::load($nid);
38 if ($request_node) {
39 $file_id = $request_node->get('field_request_file')->target_id;
40 if (!empty($file_id)) {
41 // confirm that the file exists.
42 $file = File::load($file_id);
43 if ($file) {
44 $uri = $file->getFileUri();
45 if (file_exists($uri)) {
46 $context['results']['skipped']++;
47 continue;
48 }
49 }
50 }
51
52 //Skip retrieving file if there is no request date.
53 $request_date = $request_node->get('field_request_date')->value;
54 if (empty($request_date)) {
55 $context['results']['skipped']++;
56 continue;
57 }
58 $source_url = $request_node->get('field_pdf_file_with_path')->value;
Batch Processing and the Drupal Queue System 42
59 if ($source_url == "missing") {
60 $context['results']['skipped']++;
61 continue;
62 }
63 if (!empty($source_url)) {
64 $filename = basename($source_url);
65 if (empty($filename)) {
66 \Drupal::logger('oag_opinions')
67 ->error('file_import - Error retrieving file - invalid filename in' . $s\
68 ource_url);
69 $context['results']['skipped']++;
70 continue;
71 }
72 $file_contents = @file_get_contents($source_url);
73 if ($file_contents === FALSE) {
74 \Drupal::logger('oag_opinions')
75 ->error('file_import - Error retrieving file ' . $source_url);
76 \Drupal::messenger()->addError(t('Error retrieving file %filename.',['%fil\
77 ename' => $source_url]), FALSE);
78 $context['results']['failed']++;
79 continue;
80 }
81 $destination = "public://" . $filename;
82 $file = null;
83 try {
84 $file = $fileRepository->writeData($file_contents, $destination, FileSyste\
85 mInterface::EXISTS_REPLACE);
86 }
87 catch (FileException $e) {
88 \Drupal::logger('oag_opinions')->error('file_import - Error saving file ' \
89 . $destination);
90 \Drupal::messenger()->addError(t('Error saving file %filename', ['%filenam\
91 e' => $destination]), FALSE);
92 $context['results']['failed']++;
93 continue;
94 }
95
96 if (!$file) {
97 $context['results']['failed']++;
98 continue;
99 }
100 $fid = $file->id();
101 $request_node->set('field_request_file', $fid);
Batch Processing and the Drupal Queue System 43
Here is the code that creates the batches and submits them.
1 $nid = \Drupal\dirt_salesforce\Controller\DirtSalesforceController::lookupCommodityI\
2 tem($commodity_item_id);
You can’t use $this->my_function even if they are in the same class. Grab the namespace from the
top of the PHP file you are using. In this case:
1 namespace Drupal\dirt_salesforce\Controller;
1 * Example:
2 * @code
3 * $batch = array(
4 * 'title' => t('Exporting'),
5 * 'operations' => array(
6 * array('my_function_1', array($account->id(), 'story')),
7 * array('my_function_2', array()),
8 * ),
9 * 'finished' => 'my_finished_callback',
10 * 'file' => 'path_to_file_containing_my_functions',
11 * );
12 * batch_set($batch);
13 * // Only needed if not inside a form _submit handler.
14 * // Setting redirect in batch_process.
15 * batch_process('node/1');
{: .note } To execute the batch, the example shows a call to batch_process('node/1'). This could
be any valid url alias e.g., /admin/content.
So here are the arguments for my_function_1:
You call the batch finished function with the following arguments:
5 https://git.drupalcode.org/project/drupal/-/blob/10.1.x/core/includes/form.inc
Batch Processing and the Drupal Queue System 45
1 /**
2 * Handle batch completion.
3 *
4 * @param bool $success
5 * TRUE if all batch API tasks were completed successfully.
6 * @param array $results
7 * An array of processed node IDs. - or whatever you put in $context['results'][]
8 * @param array $operations
9 * A list of the operations that had NOT been completed.
10 * @param $elapsed
11 * Elapsed time for processing in seconds.
12 */
13 public static function batchFinished($success, array $results, array $operations, $e\
14 lapsed) {
1 $messenger = \Drupal::messenger();
2 if ($success) {
3 $messenger->addMessage(t('Processed @count nodes in @elapsed.', [
4 '@count' => count($results),
5 '@elapsed' => $elapsed,
6 ]));
7 }
You can load the $results array with all sorts of interesting data, such as:
1 $context['results']['skipped'] = $skipped;
2 $context['results']['updated'] = $updated;
Batch API provides a nice way to display detailed results using code like:
You can display an informative message above the progress bar this way.
I filled in the $context['sandbox']['max'] with a value, but I could have used
$context['sandbox']['whole-bunch'] or any variable here.
1 $context['sandbox']['max'] = count($max_nids);
An informative message above the progress bar using number_format puts commas in the number
if it is over 1,000.
Also you could show something about which batch number is running.
1 $operation_details = 'Yoyoma';
2 $id = 9;
3 $context['message'] = t('Running Batch "@id" @details',
4 ['@id' => $id, '@details' => $operation_details]
5 );
A queue is simply a list of stuff that gets worked through one by one, one analogy could be a
conveyor belt on a till in a supermarket, the cashier works through each item on the belt to scan
them.
Queues are handy in Drupal for chunking up large operations, like sending emails to many people.
By using a queue, you are trying to avoid overloading the servers resources which could cause the
site to go offline until the resources on the server are free’d up.
From Sarthak TTN7 on Feb 2017:
This is the submitForm() which creates an item and puts it in the queue.
1 /**
2 * {@inheritdoc}
3 */
4 public function submitForm(array &$form, FormStateInterface $form_state): void {
5 /** @var QueueFactory $queue_factory */
6 $queue_factory = \Drupal::service('queue');
7 /** @var QueueInterface $queue */
8 $queue = $queue_factory->get('email_processor');
9 $item = new \stdClass();
10 $item->username = $form_state->getValue('name');
11 $item->email = $form_state->getValue('email');
12 $item->query = $form_state->getValue('query');
13 $queue->createItem($item);
14 }
Then you create a Queue Worker that implements ContainerFactoryPluginInterface and in the
processItem() it processes a single item from the queue.
1 namespace Drupal\my_module\Plugin\QueueWorker;
2
3 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
4 use Drupal\Core\Queue\QueueWorkerBase;
5 use Drupal\Core\Mail\MailManager;
6 use Symfony\Component\DependencyInjection\ContainerInterface;
7
8 /**
9 * {@inheritdoc}
10 */
11 class EmailEventBase extends QueueWorkerBase implements ContainerFactoryPluginInterf\
12 ace {
13
7 https://www.tothenew.com/blog/how-to-implement-queue-workerapi-in-drupal-8
Batch Processing and the Drupal Queue System 48
14 /**
15 * @param Drupal\Core\Mail\MailManager $mail
16 * The mail manager.
17 */
18 public function __construct(protected MailManager $mail) {}
19
20 /**
21 * {@inheritdoc}
22 */
23 public static function create(ContainerInterface $container, array $configuration,\
24 $plugin_id, $plugin_definition) {
25 return new static($container->get('plugin.manager.mail'));
26 }
27
28 /**
29 * Processes a single item of Queue.
30 */
31 public function processItem($data) {
32 $params['subject'] = t('query');
33 $params['message'] = $data->query;
34 $params['from'] = $data->email;
35 $params['username'] = $data->username;
36 $to = \Drupal::config('system.site')->get('mail');
37 $this->mail->mail('my_module', 'query_mail', $to, 'en', $params, NULL, true);
38 }
39 }
Then you’ll need a cronEventProcessor which in annotation tells cron how often to run the job:
1 namespace Drupal\my_module\Plugin\QueueWorker;
2
3 /**
4 *
5 * @QueueWorker(
6 * id = "email_processor",
7 * title = "My custom Queue Worker",
8 * cron = {"time" = 10}
9 * )
10 */
11 class CronEventProcessor extends EmailEventBase { }
Batch Processing and the Drupal Queue System 49
3.3: Resources
Read more about batch processing at these sites:
• Karim Boudjema from August 2018 has some good examples using the queue API10
• Sarthak TTN from Feb 2017 shows some sample code on implementing cron and the queue
API11
• There is a somewhat incomplete example12 From Alan Saunders article on December 2021
8 https://www.weareaccess.co.uk/blog/2016/07/smack-my-batch-batch-processing-drupal-8
9 https://git.drupalcode.org/project/drupal/-/blob/10.1.x/core/includes/form.inc#L561
10 http://karimboudjema.com/en/drupal/20180807/create-queue-controller-drupal8
11 https://www.tothenew.com/blog/how-to-implement-queue-workerapi-in-drupal-8
12 https://www.alansaunders.co.uk/blog/queues-drupal-8-and-9
4: Caching and cache tags
4.1: How to uncache a particular page or node
This will cause Drupal to rebuild the page internally, but won’t stop browsers or CDN’s from caching.
1 \Drupal::service('page_cache_kill_switch')->trigger();
1 use Drupal\Core\Entity\EntityInterface;
2 use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
3
4 function ddd_node_view_alter(array &$build, EntityInterface $entity, EntityViewDispl\
5 ayInterface $display) {
6 $bundle = $entity->bundle();
7 if ($bundle == 'search_home') {
8 $build['#cache']['max-age'] = 0;
9 \Drupal::service('page_cache_kill_switch')->trigger();
10 }
11 }
1 requirements:
2 _permission: 'access content'
3 options:
4 no_cache: TRUE
1 use Drupal\Core\Entity\EntityInterface;
2 use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
3
4 /**
5 * Implements hook_ENTITY_TYPE_view_alter().
6 */
7 function ddd_node_view_alter(array &$build, EntityInterface $entity, EntityViewDispl\
8 ayInterface $display) {
9 $bundle = $entity->bundle();
10 if ($bundle == 'search_home') {
11 $build['#cache']['max-age'] = 0;
12 \Drupal::service('page_cache_kill_switch')->trigger();
13 }
14 }
1 $query = \Drupal::request()->query->get('name');
1 $name = \Drupal::request()->request->get('name');
1 $query = \Drupal::request()->query->all();
2 $search_term = $query['query'];
3 $collection = $query['collection'];
1 namespace Drupal\newday\Controller;
2
3 use Drupal\Core\Controller\ControllerBase;
4
5 class NewdayController extends ControllerBase {
6 public function new() {
7 $day= [
8 "#markup" => \Drupal::request()->query->get('id'),
9 ];
10 return $day;
11 }
12 }
The request is being cached, you need to tell the system to vary by the query argument:
1 $day = [
2 '#markup' => \Drupal::request()->query->get('id'),
3 '#cache' => [
4 'contexts' => ['url.query_args:id'],
5 ],
6 ];
1 parameters:
2 http.response.debug_cacheability_headers: true
in Chrome, the network tab, click on the doc and view the Headers. You will see the following two
headers showing both the cache contexts and the cache tags
Caching and cache tags 53
1. X-Drupal-Cache-Contexts:
2. X-Drupal-Cache-Tags:
1 $build = [
2 '#type' => 'markup',
3 '#markup' => $sMarkup,
4 '#cache' => [
5 'keys' => ['home-all','home'],
6 'tags'=> ['node_list'], // invalidate cache when any node content is added/chang\
7 ed etc.
8 'max-age' => '36600', // invalidate cache after 10h
9 ],
10 ];
It is possible to change this so the cache is invalidated only when a content type of book or magazine
is changed in two possible ways:
1. Include all node tags (node:), if doesn’t matter if a new node of a particular type was added.
Caching and cache tags 54
2. Create and control your own cache tag, and invalidate it when you want.
If you want a block to be rebuilt every time that a term from a particular vocab_id is added, changed,
or deleted you can cache the term list. If you need to cache a term list per vocab_id - i.e. every time
that a term from a particular vocab_id is added, changed, or deleted the cache tag is invalided using
Cache::invalidateTags($tag_id) then my block will be rebuild.
1 use Drupal\Core\Cache\Cache;
2
3 function filters_invalidate_vocabulary_cache_tag($vocab_id) {
4 Cache::invalidateTags(['filters_vocabulary:' . $vocab_id]);
5 }
If you want this to work for nodes, you may be able to just change $vocab_id for $node_type.
and scrolling down a bit at this link shows some more info about getting cache tags and merging
them. https://drupal.stackexchange.com/questions/145823/how-do-i-get-the-current-node-id
1 if (!empty($record)) {
2 $response = new ResourceResponse($record, 200);
3 $response->addCacheableDependency($record);
4 return $response;
5 }
1 /**
2 * Custom service to call APIs.
3 *
4 * @see \Drupal|cm_api|CmAPIClientInterface
5 */
6 class CmAPIClient implements CmAPIClientInterface {
7 ...
8 /**
9 * Internal static cache.
10 *
11 * @var array
12 */
13 protected static $cache = [];
The api call is made and the cache is checked. The index (or key) is built from the api call “getPolicy”
and the next key is the policy number with the version number attached. So the $response_data is
put in the cache with:
This relieves the back end load by rather getting the data from the cache if is in the cache. (If the
cache is warm.)
The entire function is shown below. It is from docroot/modules/custom/cm_-
api/src/CmAPIClient.php:
14
15 if (isset(self::$cache['getPolicy'][$policy_number . $version])) {
16 $response_data = self::$cache['getPolicy'][$policy_number . $version];
17 } else {
18 $response_data = $this->performRequest($endpoint_url, 'GET', $this->params);
19 self::$cache['getPolicy'][$policy_number . $version] = $response_data;
20 }
21 return $response_data;
22 }
1 /**
2 * Implements hook_preprocess_node().
3 */
4 function nzz_zzzzconnect_preprocess_node(&$variables) {
Notice the references to \Drupal::cache(). First we check if this is our kind of node to process.
Then we derive the $cid. We check the cache with a call to ->get($cid) and if it fails we:
In future requests, we can just use the data from the cache.
Caching and cache tags 58
44 }
Cache contexts, tags and max-age must always be set, because they affect the cacheability of the
entire response. Therefore they “bubble” and parents automatically receive them.
Cache keys must only be set if the render array should be cached.
There are more details at the link above
1 /**
2 * Enable local development services.
3 */
4 $settings['container_yamls'][] = DRUPAL_ROOT . '/sites/development.services.yml';
1 $settings['cache']['bins']['render'] = 'cache.backend.null';
1 $ cp sites/example.settings.local.php sites/default/settings.local.php
This will include the local settings file as part of Drupal’s settings file.
1 services:
2 cache.backend.null:
3 class: Drupal\Core\Cache\NullBackendFactory
NOTE: Do not create development.services.yml, it already exists under /sites. You can copy it from
there.
4. In settings.local.php change the following to be TRUE if you want to work with enabled
css- and js-aggregation:
1 $config['system.performance']['css']['preprocess'] = FALSE;
2 $config['system.performance']['js']['preprocess'] = FALSE;
5. Uncomment these lines in settings.local.php to disable the render cache and disable dynamic
page cache:
1 $settings['cache']['bins']['render'] = 'cache.backend.null';
2 $settings['cache']['bins']['dynamic_page_cache'] = 'cache.backend.null';
3 $settings['cache']['bins']['page'] = 'cache.backend.null';
If you do not want to install test modules and themes, set the following to FALSE:
1 $settings['extension_discovery_scan_tests'] = FALSE;
1 parameters:
2 twig.config:
3 debug: true
4 auto_reload: true
5 cache: false
NOTE: If the parameters block is already present in the yml file, append the twig.config block to it.
Afterwards rebuild the Drupal cache with drush cr otherwise your website will encounter an
unexpected error on page reload.
1 https://www.drupal.org/project/memcache
2 https://www.drupal.org/project/redis
3 https://www.php.net/manual/en/book.apcu.php
Caching and cache tags 63
1 cache.discovery:
2 class: Drupal\Core\Cache\CacheBackendInterface
3 tags:
4 - { name: cache.bin, default_backend: cache.backend.chainedfast }
5 factory: cache_factory:get
6 arguments: [discovery]
Independent of this, the $settings array can be used in settings.php to assign cache backends to
cache bins. For example:
1 $settings['cache']['bins']['discovery'] = 'cache.backend.memory';
2 $settings['cache']['default'] = 'cache.backend.redis';
Before Drupal 8.2.0, the order of steps through which the backend was selected for a given cache
bin was as follows:
The order has been changed, so that the cache bin services that explicitly set default_backends
are always used unless explicitly overridden with a per-bin configuration. In core, this means, fast
chained backend will be used for bootstrap, config, and discovery cache bins and memory backend
will be used for the static cache bin, unless they are explicitly overridden in settings.
For example, to ensure Redis is used for all cache bins, before 8.2.0, the following configuration
would have been enough:
1 $settings['cache']['default'] = 'cache.backend.redis';
However, now the following configuration in settings.php would be required to achieve the same
exact behavior:
Caching and cache tags 64
1 $settings['cache']['bins']['bootstrap'] = 'cache.backend.redis';
2 $settings['cache']['bins']['discovery'] = 'cache.backend.redis';
3 $settings['cache']['bins']['config'] = 'cache.backend.redis';
4 $settings['cache']['bins']['static'] = 'cache.backend.redis';
5 $settings['cache']['default'] = 'cache.backend.redis';
Before proceeding to override the cache bins that define fast cached default backends blindly, please
also read why they exist, particularly when using multiple webserver nodes. See ChainedFastBack-
end on api.drupal.org4 .
The above information is from https://www.drupal.org/node/2754947
Fabian Franz in his article at https://drupalsun.com/fabianx/2015/12/01/day-1-tweak-drupal-8-
performance-use-apcu-24-days-performance-goodies suggests that we can configure APCu to be
used for caches with the following:
1 $settings['cache']['default'] = 'cache.backend.apcu';
2 $settings['cache']['bins']['bootstrap'] = 'cache.backend.apcu';
3 $settings['cache']['bins']['config'] = 'cache.backend.apcu';
4 $settings['cache']['bins']['discovery'] = 'cache.backend.apcu';
Proceed with caution with the above as it seems that APCu may only suitable for single server
setups. TODO: I couldn’t find any references to using APCu with multi-server setups so I’m not
sure if that is a safe configuration.
Pantheon docs ask in their FAQ Can APCu be used as a cache backend on Pantheon? Yes, APCu
can be used as a cache backend or a “key-value store”; however, this is not recommended. APCu
lacks the ability to span multiple application containers. Instead, Pantheon provides a Redis-based
Object Cache as a caching backend for Drupal and WordPress, which has coherence across multiple
application containers. This was from Pantheon docs5 FAQ’s:
Drupal 8 has a so-called fast-chained backend6 as the default cache backend, which allows to store
data directly on the web server while ensuring it is correctly synchronized across multiple servers.
APCu is the user cache portion of APC (Advanced PHP Cache), which has served us well till PHP 5.5
got its own zend opcache. You can think of it as a key-value store that is stored in memory and the
basic operations are apc_store($key, $data), apc_fetch($keys) and apc_delete($keys). For windows
the equivalent on IIS would be WinCache (http://drupal.org/project/wincache).
In order to mitigate a network roundtrip for each cache get operation, this cache allows a fast
backend to be put in front of a slow(er) backend. Typically the fast backend will be something
like APCu, and be bound to a single web node, and will not require a network round trip to fetch a
cache item. The fast backend will also typically be inconsistent (will only see changes from one web
node). The slower backend will be something like Mysql, Memcached or Redis, and will be used by
all web nodes, thus making it consistent, but also require a network round trip for each cache get.
In addition to being useful for sites running on multiple web nodes, this backend can also be useful
for sites running on a single web node where the fast backend (e.g., APCu) isn’t shareable between
the web and CLI processes. Single-node configurations that don’t have that limitation can just use
the fast cache backend directly.
We always use the fast backend when reading (get()) entries from cache, but check whether they
were created before the last write (set()) to this (chained) cache backend. Those cache entries that
were created before the last write are discarded, but we use their cache IDs to then read them from
the consistent (slower) cache backend instead; at the same time we update the fast cache backend so
that the next read will hit the faster backend again. Hence we can guarantee that the cache entries
we return are all up-to-date, and maximally exploit the faster cache backend. This cache backend
uses and maintains a “last write timestamp” to determine which cache entries should be discarded.
Because this backend will mark all the cache entries in a bin as out-dated for each write to a bin, it
is best suited to bins with fewer changes.
Note that this is designed specifically for combining a fast inconsistent cache backend with a
slower consistent cache back-end. To still function correctly, it needs to do a consistency check
(see the “last write timestamp” logic). This contrasts with \Drupal\Core\Cache\BackendChain,
which assumes both chained cache backends are consistent, thus a consistency check being point-
less. This information is from https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%
21Cache%21ChainedFastBackend.php/class/ChainedFastBackend/9
4.14.2: APCu
APCu is the official replacement for the outdated APC extension. APC provided both opcode
caching (opcache) and object caching. As PHP versions 5.5 and above include their own opcache,
APC was no longer compatible, and its opcache functionality became useless. The developers of
APC then created APCu, which offers only the object caching (read “in memory data caching”)
functionality (they removed the outdated opcache). Read more at https://www.php.net/manual/en/
book.apcu.php
APCu is not the same as apc!
APCu support is built into Drupal Core. From this Change record Sep 20147 :
In order to improve cache performance, Drupal 8 now has:
7 https://www.drupal.org/node/2327507
Caching and cache tags 66
A cache.backend.apcu service that site administrators can assign as the backend of a cache bin via
$settings[‘cache’] in settings.php for sites running on a single server, with a PHP installation that
has APCu enabled, and that do not use Drush or other command line scripts.
This references single-server sites not needing Drush. TODO: I couldn’t find any references to using
APCu with multi-server setups so I’m not sure if that is a safe configuration.
A cache.backend.chainedfast service that combines APCu availability detection, APCu front
caching, and cross-server / cross-process consistency management via chaining to a secondary
backend (either the database or whatever is configured for $settings[‘cache’][‘default’]).
A default_backend service tag (the value of which can be set to a backend service name, such as
cache.backend.chainedfast) that module developers can assign to cache bin services to identify bins
that are good candidates for specialized cache backends.
The above tag assigned to the cache.bootstrap, cache.config, and cache.discovery bin services.
This means that by default (on a site with nothing set for $settings[‘cache’] in settings.php), the
bootstrap, config, and discovery cache bins automatically benefit from APCu caching if APCu is
available, and this is compatible with Drush usage (e.g., Drush can be used to clear caches and the
web process receives that cache clear) and multi-server deployments.
APCu will act as a very fast local cache for all requests. Other cache backends can act as bigger,
more general cache backend that is consistent across processes or servers.
For module developers creating custom cache bins
If you are defining a cache bin that is:
• relatively small (likely to have few enough entries to fit within APCu memory), and
• high-read (many cache gets per request, so reducing traffic to the database or other networked
backend is worthwhile), and
• low-write (because every write to the bin will invalidate the entire APCu cache of that bin)
then, you can add the default_backend tag to your bin, like so:
1 #example.services.yml
2 services:
3 cache.example:
4 class: Drupal\Core\Cache\CacheBackendInterface
5 tags:
6 - { name: cache.bin, default_backend: cache.backend.chainedfast }
7 factory_method: get
8 factory_service: cache_factory
9 arguments: [example]
Caching and cache tags 67
1 $settings['cache']['bins']['default'] = 'cache.backend.memcache';
2 $settings['cache']['bins']['bootstrap'] = 'cache.backend.chainedfast';
3 $settings['cache']['bins']['config'] = 'cache.backend.chainedfast';
4 $settings['cache']['bins']['discovery'] = 'cache.backend.chainedfast';
5 // ...
The bins set to use cache.backend.chainedfast will use APCu as the front cache to the default backend
(e.g., memcache in the above example).
For site administrators of single-server sites that don’t need Drush or other CLI access
This references single-server sites not needing Drush. TODO: I couldn’t find any references to using
APCu with multi-server setups so I’m not sure if that is a safe configuration.
Pantheon docs ask in their FAQ Can APCu be used as a cache backend on Pantheon? Yes, APCu
can be used as a cache backend or a “key-value store”; however, this is not recommended. APCu
lacks the ability to span multiple application containers. Instead, Pantheon provides a Redis-based
Object Cache as a caching backend for Drupal and WordPress, which has coherence across multiple
application containers. This was from Pantheon docs8 FAQ’s:
You can optimize further by using APCu exclusively for certain bins, like so:
1 $settings['cache']['bins']['bootstrap'] = 'cache.backend.apcu';
2 $settings['cache']['bins']['config'] = 'cache.backend.apcu';
3 $settings['cache']['bins']['discovery'] = 'cache.backend.apcu';
8 https://docs.pantheon.io/apcu
Caching and cache tags 68
1 #services.yml
2 services:
3 cache.backend.chainedfast:
4 class: Drupal\Core\Cache\ChainedFastBackendFactory
5 arguments: ['@settings', , 'cache.backend.eaccelerator']
6 calls:
7 - [setContainer, ['@service_container']]
4.15: Reference
• Drupal: cache tags for all, regardles of your backend From Matt Glaman 22, August 2022 https:
//mglaman.dev/blog/drupal-cache-tags-all-regardless-your-backend
• Debugging your render cacheable metadata in Drupal From Matt Glaman 14, February 2023
https://mglaman.dev/blog/debugging-your-render-cacheable-metadata-drupal
• Cache contexts overview on drupal.org https://www.drupal.org/docs/drupal-apis/cache-api/
cache-contexts
• Caching in Drupal 8 a quick overview of Cache tags, cache context and cache max-age with
simple examples https://zu.com/articles/caching-drupal-8
• Nedcamp video on caching by Kelly Lucas from November 2018 https://www.youtube.com/
watch?v=QCZe2K13bd0&list=PLgfWMnl57dv5KmHaK4AngrQAryjO_ylaM&t=0s&index=16
• #! code: Drupal 9: Debugging Cache Problems With The Cache Review Module, Septem-
ber 2022 https://www.hashbangcode.com/article/drupal-9-debugging-cache-problems-cache-
review-module
• #! code: Drupal 9: Using The Caching API To Store Data, April 2022 https://www.
hashbangcode.com/article/drupal-9-using-caching-api-store-data
• #! code: Drupal 8: Custom Cache Bin, September 2019 https://www.hashbangcode.com/article/
drupal-8-custom-cache-bins
• New cache backend configuration order, per-bin default before default configuration (How to
specify cache backend), June 2016 https://www.drupal.org/node/2754947
• Cache API Drupal Core9
9 https://api.drupal.org/api/drupal/core!core.api.php/group/cache/10.1.x
5: Composer, Updates and Patches
5.1: Creating a local patch to a contrib module
See Making a patch at https://www.drupal.org/node/707484
In this case, I had the file_entity module installed and wanted to hide the tab “[files.]{.underline}”
The tab item is provided by a task (read “menu tab”) in the web/modules/contrib/file_entity/file_-
entity.links.task.yml
1 entity.file.collection:
2 route_name: entity.file.collection
3 base_route: system.admin_content
4 title: 'Files'
5 description: 'Manage files for your site.'
1 $ git diff
10 - route_name: entity.file.collection
11 - base_route: system.admin_content
12 - title: 'Files'
13 - description: 'Manage files for your site.'
14 -
15 entity.file.add_form:
16 route_name: entity.file.add_form
17 base_route: entity.file.add_form
Add the patch to the patches section of composer.json. Notice below the line starting with
”drupal/file_entity”:
1 "patches": {
2 "drupal/commerce": {
3 "Allow order types to have no carts": "https://www.drupal.org/files/issues/2\
4 018-03-16/commerce-direct-checkout-50.patch"
5 },
6 "drupal/views_load_more": {
7 "Template change to keep up with core": "https://www.drupal.org/files/issues\
8 /views-load-more-pager-class-2543714-02.patch" ,
9 "Problems with exposed filters": "https://www.drupal.org/files/issues/views_\
10 load_more-problems-with-exposed-filters-2630306-4.patch"
11 },
12 "drupal/easy_breadcrumb": {
13 "Titles in breadcrumbs are double-escaped": "https://www.drupal.org/files/is\
14 sues/2018-06-21/2979389-7-easy-breadcrumb--double-escaped-titles.patch"
15 },
16 "drupal/file_entity": {
17 "Temporarily disable the files menu tab": "./patches/file_entity_disable_fil\
18 e_menu_tab.patch"
19 }
20 }
Revert the file in git and then try to apply the patch.
Here is the patch command way to un-apply or revert a patch (-R means revert)
Composer, Updates and Patches 71
1 "extra": {
2 "patches": {
3 "drupal/core": {
4 "Add startup configuration for PHP server": "https://www.drupal.org/files/is\
5 sues/add_a_startup-1543858-30.patch"
6 }
7 }
8 },
1 "extra": {
2 "patches": {
3 "drupal/core": {
4 "Ignore front end vendor folders to improve directory search performance": "\
5 https://www.drupal.org/files/issues/ignore_front_end_vendor-2329453-116.patch"",
6 "My custom local patch": "./patches/drupal/some_patch-1234-1.patch"
7 }
8 }
9 },
Some developers like adding the actual link to the issue in the description like this:
1 https://github.com/cweagans/composer-patches
Composer, Updates and Patches 72
1 "extra": {
2 "patches": {
3 "drupal/core": {
4 "Views Exposed Filter Block not inheriting the display handlers cache tags\
5 , causing filter options not to appear, https://www.drupal.org/project/drupal/issues
6 /3067937": "https://www.drupal.org/files/issues/2019-07-15/drupal-exposed_filter_blo
7 ck_cache_tags-3067937-4.patch",
8 "Cannot use relationship for rendered entity on Views https://www.drupal.o\
9 rg/project/drupal/issues/2457999": "https://www.drupal.org/files/issues/2021-05-13/9
10 .1.x-2457999-267-views-relationship-rendered-entity.patch"
11 },
See Drupal 9 and Composer Patches2 also Managing patches with Composer3
1 "extra": {
2 "patches": {
3 "drupal/core": {
4 "Add startup configuration for PHP server": "https://www.drupal.org/files/is\
5 sues/add_a_startup-1543858-30.patch"
6 },
7 "drupal/gin": {
8 "Improve content form detection - 3188521": "https://www.drupal.org/files/is\
9 sues/2021-05-19/3188521-8.patch"
10 }
11 }
12 }
5. use composer update --lock to apply the patch and watch the output.
If the patch was not applied or throws an error which is quite common (because they are no longer
compatible), try using -vvv (verbose mode) flag with composer to see the reason:
2 https://vazcell.com/blog/how-apply-patch-drupal-9-composer
3 https://acquia.my.site.com/s/article/360048081193-Managing-patches-with-Composer
4 https://www.drupal.org/project/gin/issues/3188521
Composer, Updates and Patches 73
1 "extra": {
2 "installer-paths": {
3 "web/core": ["type:drupal-core"],
4 "web/libraries/{$name}": ["type:drupal-library"],
5 "web/modules/contrib/{$name}": ["type:drupal-module"],
6 "web/profiles/contrib/{$name}": ["type:drupal-profile"],
7 "web/themes/contrib/{$name}": ["type:drupal-theme"],
8 "drush/Commands/contrib/{$name}": ["type:drupal-drush"],
9 "web/modules/custom/{$name}": ["type:drupal-custom-module"],
10 "web/themes/custom/{$name}": ["type:drupal-custom-theme"]
11 },
12 "drupal-scaffold": {
13 "locations": {
14 "web-root": "web/"
15 },
16 "excludes": [
17 "robots.txt",
18 ".htaccess"
19 ]
20 },
21 "patches-file": "patches/composer.patches.json"
22 }
This means the patch is trying to run the patch in the directory web/modules/contrib/addtocalendar
(notice the -d web/modules/contrib/addtocalendar above
In this case, recreate the patch with the --no-prefix option i.e.
1 "drupal-scaffold": {
2 "locations": {
3 "web-root": "web/"
4 },
5 "file-mapping": {
6 "[web-root]/sites/development.services.yml": false
7 }
8 },
Composer, Updates and Patches 75
1 "name": "my/project",
2 ...
3 "extra": {
4 "drupal-scaffold": {
5 "locations": {
6 "web-root": "web/"
7 },
8 "file-mapping": {
9 "[web-root]/robots.txt": false
10 "[web-root]/.htaccess": false,
11 "[web-root]/sites/development.services.yml": false
12 },
13 ...
14 }
15 }
More at https://drupal.stackexchange.com/questions/290989/composer-keeps-overwriting-
htaccess-and-other-files-every-time-i-do-anything
• >=1.0
• >=1.0 <2.0
• >=1.0 <1.1 || >=1.2
More at https://getcomposer.org/doc/articles/versions.md
Composer, Updates and Patches 77
They say: ”It is not possible to support both Drupal 9.x and 10.x in a single release of this module
due to a breaking change in EncoderInterface::encode() between Symfony 4.4 (D9) and Symfony 6.2
(D10). When preparing for an upgrade to Drupal 10 we recommend that you widen your Composer
version constraints to allow either 2.x or 3.x: composer require drupal/csv_serialization:^2.0
|| ^3.0. This will allow the module to be automatically upgraded when you upgrade Drupal core.”
5.10: Troubleshooting
Note --ignore-platform-reqs is only necessary if your php on your host computer is different to
the version in your DDEV containers.
You could always use this for DDEV:
7 https://www.drupal.org/project/csv_serialization
Composer, Updates and Patches 78
5.11: Reference
• Drupal 8 composer best practices - Jan 20188
• Making a patch - Dec 20229
• Composer Documentation10
• Composer documentation article on versions and constraints11
• Using Drupal’s Composer Scaffold updated Dec 202212
• Drupal 9 and Composer Patches by Adrian Vazquez Peligero June 202113
• Managing patches with Composer March 202214
8 https://www.lullabot.com/articles/drupal-8-composer-best-practices
9 https://www.drupal.org/node/707484
10 https://getcomposer.org/doc/
11 https://getcomposer.org/doc/articles/versions.md
12 https://www.drupal.org/docs/develop/using-composer/using-drupals-composer-scaffold#toc_6
13 https://vazcell.com/blog/how-apply-patch-drupal-9-composer
14 https://acquia.my.site.com/s/article/360048081193-Managing-patches-with-Composer
6: Configuration and Settings
Config is stored in yml files so it can be checked into git. It is loaded into the config table of
the database for performance. Use drush config-import (or drush cim) for this purpose. Config
includes database table definitions, views definitions and lots more. You can even use config to
store a little setting indicating your site is in a test mode which can trigger displaying some useful
information that only you can see.
Config files should be stored in a non-web accessible directory and specified in settings.php e.g.
1 $settings['config_sync_directory'] = '../config/sync';
Use the Configuration API main entry point \Drupal::config() to load the config item and then
use get() to retrieve the value you want. Config can have multiple values in a single yml file.
1 $pizzaEndpoint = \Drupal::config('pizza_academy_core.pbx.rest.endpoint');
2 $pizza_service_url = $pizzaEndpoint->get('pizza_rest_endpoint').$reg_id;
When you export the config, this information is stored in a file called pizza_academy_-
core.pbx.rest.endpoint.yml with a key pizza_rest_endpoint.
1 pizza_rest_endpoint: 'https://pbx.pizza.com/pbx-profile-service/'
You’ll find this in a file in the config sync directory specified in settings.php e.g.
1 config/sync/pizza_academy_core.pbx.rest.endpoint.yml
1 $settings['config_sync_directory'] = '../config/sync';
6.2: Views
For views, the config filenames are be in the form views.view.infofeeds for a view called
infofeeds.
1 $config = \Drupal::config('danamod.header_footer_settings');
1 url: 'https://pbx.pizza.com/'
2 langcode: 'en'
You can copy it to the config/sync directory, manually paste the contents into the config Drupal
u/i or import it into the db with drush. The drush way is the easiest in my opinion.
2 https://www.drupal.org/docs/creating-custom-modules/defining-and-using-your-own-configuration-in-drupal
3 https://api.drupal.org/api/drupal/core%21lib%21Drupal.php/function/Drupal%3A%3Aconfig/9.2.x
Configuration and Settings 81
1 $pbx_path_config = \Drupal::config('pizza_academy_core.pbxpath');
2 $pbx_path = $pbx_path_config->get('url');
3 $pbx_achievements_url = $pbx_path . "achievements?regid=".$reg_id;
Once you grab the url, you can use it later in your code.
Note. the @dev2 is a site alias. See Drush alias docs for more info4 . These are sooo useful.
4 https://www.drush.org/latest/site-aliases/
Configuration and Settings 82
1 use Drupal\Core\Form\FormStateInterface;
2 /**
3 * Implements hook_form_FORM_ID_alter().
4 */
5 function mymodule_form_system_site_information_settings_alter(&$form, FormStateInter\
6 face $form_state) {
7
8 $form['site_phone'] = [
9 '#type' => 'tel',
10 '#title' => t('Site phone'),
11 '#default_value' =>
12 Drupal::config('system.site')->get('phone'),
13 ];
14
15 $form['#submit'][] = 'mymodule_system_site_information_phone_submit';
16 }
The $form['#submit'] modification adds our callback to the form’s submit handlers. This allows
our module to interact with the form once it has been submitted. The mymodule_system_site_-
information_phone_submit callback is passed the form array and form state. We load the current
configuration factory to receive the configuration that can be edited. We then load system.site
and save phone based on the value from the form state.
Don’t forget there is a module called config pages5 which might save you some coding if you need
to add some config to a site.
1 // Get system site maintenance message text. This value may be overriden by
2 // default from global $config (as well as translations).
3 $message = \Drupal::config('system.maintenance')->get('message');
To override configuration values in global $config in settings.php, use a line like this (which
references the configuration keys:
1 $config['system.performance']['css']['preprocess'] = 0;
If you have a configuration change, for example, you have enabled google tag manager. When you
export the config drush cex -y and git diff to see what changed in config, you’ll see (in the last
2 lines) that status is changed from true to false.
1 $ git diff
2
3 diff --git a/config/sync/google_tag.container.default.yml b/config/sync/google_tag.c\
4 ontainer.default.yml
5 index 39e498c99..375bfb8af 100644
6 --- a/config/sync/google_tag.container.default.yml
7 +++ b/config/sync/google_tag.container.default.yml
8 @@ -1,6 +1,6 @@
9 uuid: 5919bbb9-95e3-4d8b-88c8-030e6a58ec6c
10 langcode: en
11 -status: false
12 +status: true
To put this in settings.php or settings.local.php, add a line and set the value to true or false:
1 $config['google_tag.container.default']['status'] = false;
1 test_mode: FALSE
1 test_mode: FALSE
and import. This will load the new value into the database.
Then in your docroot/sites/default/settings.local.php (to enable testing features) add
1 $config['tea_teks_srp.testing']['test_mode'] = TRUE;
This will override your config you added above so test_mode is true.
Then to use the test_mode, you can load it into a controller class (or form class) from the config (and
the value in the settings.local.php will override the default) with the following:
1 $test_mode = \Drupal::config('tea_teks_srp.testing')->get('test_mode');
1 if ($this->test_mode) {
2 $value = $this->t("Reject Citation $citation_nid");
3 }
Drilling down deeper, let’s say we want to view the credentials section. Notice that drush requires
a space instead of a colon:
Now to get down to the user name and password. And we are adding period back in. Huh?
and finally:
And
Configuration and Settings 86
And so
and while we’re here, we could always put these into the $config object via settings.php (or
settings.local.php :
1 $config['shield.settings']['credentials']['shield']['user'] = "nisor";
2 $config['shield.settings']['credentials']['shield']['pass'] = "blahblah";
e.g. in docroot/modules/custom/danamod/src/Form/HeaderFooterForm.php
In the buildform, you load the config object, then get each value from the object, load them into the
form (in #default_value array items) so the user can see the current value.
Configuration and Settings 87
In the submitForm() member, you extract the values from the $form_state, load an editable config
object and ->set() values and ->save() them.
1 $config = \Drupal::configFactory()->getEditable('danamod.header_footer_settings');
2 $values = $form_state->getValues();
3 $address1 = $values['footer_address1'];
4 $address2 = $values['footer_address2'];
5 $config->set('footer_address1', $address1);
6 $config->set('footer_address2', $address2);
7
8 $config->save();
9
10 \Drupal::messenger()->addMessage('Values have been saved.');
1 $config
2 ->set('display_stars', $display_stars)
3 ->set('display_summary_summative', $display_summary_summative)
4 ->save();
5
6 \Drupal::messenger()->addMessage('Values have been saved.');
This displays the basepath that is in the Drupal database. If you override the basepath in settings.php,
you have to use the special flag to see the overridden value.
6 https://www.drush.org/latest/commands/all/
7 https://www.drush.org/latest/commands/config_get/
Configuration and Settings 89
Also drush can execute php for a little more fun approach:
1 drush ev "var_dump(\Drupal::configFactory()->getEditable('system.site')->get('name')\
2 )"
or
1 drush cst
2 --------------------------------------------- ------------
3 Name State
4 --------------------------------------------- ------------
5 admin_toolbar.settings Only in DB
6 admin_toolbar_tools.settings Only in DB
7 automated_cron.settings Only in DB
8 block.block.bartik_account_menu Only in DB
9 block.block.bartik_branding Only in DB
10 block.block.bartik_breadcrumbs Only in DB
11 block.block.bartik_content Only in DB
12 ...
Only in DB means the config has not yet been exported. Best practice is to check the config info git
for loading onto the production site. Usually you would use drush cex at this point to export the
config and add it to git.
After exporting drush will report that everything has been exported and that there are no differences
between the database and the sync folder.
Configuration and Settings 90
1 drush cst
2 [notice] No differences between DB and sync directory.
1 $settings['config_sync_directory'] = '../config/sync';
1 $ drush cex -y
2 [success] Configuration successfully exported to ../config/sync.
3 ../config/sync
1 $ drush cst
2 ------------- -----------
3 Name State
4 ------------- -----------
5 system.site Different
and
Configuration and Settings 91
1 $ drush cim -y
2 +------------+-------------+-----------+
3 | Collection | Config | Operation |
4 +------------+-------------+-----------+
5 | | system.site | Update |
6 +------------+-------------+-----------+
7
8 // Import the listed configuration changes?: yes.
9
10 [notice] Synchronized configuration: update system.site.
11 [notice] Finalizing configuration synchronization.
12 [success] The configuration was imported successfully.
7: CRON
7.1: Overview
Cron is a time-based task scheduler that executes commands at specified intervals, called cron jobs.
Cron is available on Unix, Linux, and Mac servers, and Windows servers use a Scheduled Task to
execute commands. Cron jobs are used in Drupal to handle maintenance tasks such as cleaning up
log files and checking for updates.
1 /**
2 * Registers the methods in this class that should be listeners.
3 *
4 * @return array
5 * An array of event listener definitions.
6 */
7 public static function getSubscribedEvents(): array {
8 return [KernelEvents::TERMINATE => [['onTerminate', 100]]];
9 }
Drupal then keeps track of when the cron ran and ensures that the next time it runs is only after the
configured amount of time has elapsed.
1 https://symfony.com/doc/current/components/http_kernel.html#8-the-kernel-terminate-event
CRON 93
1 /**
2 * Run the automated cron if enabled.
3 *
4 * @param \Symfony\Component\HttpKernel\Event\TerminateEvent $event
5 * The Event to process.
6 */
7 public function onTerminate(TerminateEvent $event): void {
8 $interval = $this->config->get('interval');
9 if ($interval > 0) {
10 $cron_next = $this->state->get('system.cron_last', 0) + $interval;
11 if ((int) $event->getRequest()->server->get('REQUEST_TIME') > $cron_next) {
12 $this->cron->run();
13 }
14 }
15 }
So, in essence, if the cron is set to run every hour but the next visitor only comes in three hours, it
will only run then.
1 0 * * * * wget -O - -q -t 1 http://www.example.com/cron/<key>
In the above sample, the 0 * * * * represents when the task should happen. The first figure represents
minutes – in this case, on the ‘zero’ minute, or top of the hour. The other figures represent the hour,
day, month, and day of the week. A * is a wildcard, meaning ‘every time’. The minimum is every
minute * * * * *.
The rest of the line wget -O - -q -t 1 tells the server to request a URL, so the server executes the
cron script.
Here is a diagram of the general crontab syntax, for illustration:
Thus, the cron command example above means ping http://www.example.com/cron/<key> at the
zero minutes on every hour of every day of every month of every day of the week.
1 45 * * * * wget -O - -q -t 1 http://www.example.com/cron/<key>
2 45 * * * * curl -s http://example.com/cron/<key>
This would have a wget or curl visit your cron page 45 minutes after every hour.
3. Save and exit the file. Check the Drupal status report, which shows the time of the cron
execution.
Use crontab guru2 - it’s a quick and easy editor for cron schedule expressions.
2 https://crontab.guru
CRON 95
1. The preferred way to disable Drupal’s core automated cron module is by unchecking it at
/admin/modules.
2. To temporarily disable cron, set the ‘Run cron every’ value to ‘Never’ (e.g., at
Administration > Configuration > System > Cron (/admin/config/system/cron)).
3. For advanced reasons, another way to disable cron in Drupal is to add the following line to
your settings.php. Note that this fixes the setting at /admin/config/system/cron to ‘Never’,
and administrative users cannot override it.
1 $config['automated_cron.settings']['interval'] = 0;
7.7: hook_cron()
Gets fired every time the cron runs, so basically, Drupal’s cron is a collection of function calls to
various modules. For this reason, we must avoid overloading the request with heavy processing;
otherwise, the request might crash.
1 function announcements_feed_cron() {
2 $config = \Drupal::config('announcements_feed.settings');
3 $interval = $config->get('cron_interval');
4 $last_check = \Drupal::state()->get('announcements_feed.last_fetch', 0);
5 $time = \Drupal::time()->getRequestTime();
6 if ($time - $last_check > $interval) {
7 \Drupal::service('announcements_feed.fetcher')->fetch(TRUE);
8 \Drupal::state()->set('announcements_feed.last_fetch', $time);
9 }
10 }
1 // Find out when cron was last run; the key is 'system.cron_last'.
2 $cron_last = \Drupal::state()->get('system.cron_last');
1 $cron_last = $this->state->get('system.cron_last')
7.9: Resources:
•
•
•
•
•
•
•
•
8: Dates and Times
8.1: Overview
Drupal Date fields are stored as varchar 20 UTC date strings (e.g. 2022-06-30T12:00:00) while node
created and changed fields are stored as int 11 containing Unix epoch timestamps (e.g. 1656379475)
in the node_field_data table (fields: created and changed).
Accessing date fields comes in many flavors:
1 // Magic getters.
2 $start = $event_node->field_event_date_range->value
3 $end = $event_node->field_event_date_range->end_value
4
5 // Using get().
6 $start = $event_node->get('field_event_date_range')->value
7 $end = $event_node->get('field_event_date_range')->end_value
8
9 // Using getValue().
10 $start = $event_node->get('field_event_date_range')->getValue()[0]['value'];
11 $end = $event_node->get('field_event_date_range')->getValue()[0]['end_value'];
1 use Drupal\Core\Datetime\DrupalDateTime;
2
3 $date_string = "2020-08-24T15:28:04+00:00";
4 $ddt = new DrupalDateTime($date_string);
5 $newstring = $ddt->format("Y-m-d\Th:i:s");
6 $node->set('field_date', $newstring);
1 $from = $node->get('field_date')->getValue()[0]['value'];
2 $to = $node->get('field_date')->getValue()[0]['end_value'];
1 use Drupal\Core\Datetime\DrupalDateTime;
2
3 /**
4 * Implements hook_preprocess_node
5 *
6 * @param $variables
7 */
8 function vst_preprocess_node(&$variables) {
9 if (!empty($variables['content']['field_date'])) {
10 $date = $variables['content']['field_date'];
11
12 $from = new DrupalDateTime($variables["node"]->get('field_date')->getValue()[0][\
13 'value']);
14 $date_array = explode("-", $from);
Dates and Times 100
Now in the twig node template we can output the scrunch_date we created.
From /web/themes/verygood/templates/node/node--seminar--teaser.html.twig.
1 {% raw %}
2 {% if content.field_date %}
3 {% if scrunch_date %}
4 <div>
5 {{ scrunch_date }}
6 </div>
7 {% else %}
8 <div>
9 {{ content.field_date }}
10 </div>
11 {% endif %}
12 {% endif %}
13 {% endraw %}
Dates and Times 101
1 $node->set('field_date', '2025-12-31');
2 $node->set('field_datetime', '2025-12-31T23:59:59');
3 // Use this for storing created and changed.
4 $node->set('created', '1760140799');
5 $node->save();
UTC: https://en.wikipedia.org/wiki/Coordinated_Universal_Time
Nice article on writing date fields programmatically with more info on UTC timezone at
https://gorannikolovski.com/blog/set-date-field-programmatically#:~:text=Get%20the%20date%
20field%20programmatically,)%3B%20%2F%2F%20For%20datetime%20fields.
1 use Drupal\Core\Datetime\DrupalDateTime;
2
3 public function build() {
4 $date = new DrupalDateTime();
5 return [
6 '#markup' => t('Copyright @year© My Company', [
7 '@year' => $date->format('Y'),
8 ]), ];
9 }
1 $created_date = $node->getCreatedTime();
2
3 // Displays 05/04/2022 3:49 pm
4 $formatted_created_date = \Drupal::service('date.formatter')->format($created_date, \
5 'custom', 'm/d/Y g:i a');
6
7 // Displays 2022-05-04 15:49:30
8 $formatted_created_date = \Drupal::service('date.formatter')->format($created_date, \
9 'custom', 'Y-m-d H:i:s');
10
Dates and Times 103
1 use Drupal\Core\Datetime\DrupalDateTime;
2
3 $start_date_val = $node->get('field_cn_start_date')->value;
4 $days = intval($node->get('field_cn_suspension_length')->value) - 1;
5 $end_date = DrupalDateTime::createFromFormat('Y-m-d', $start_date_val );
6 $end_date->modify("+$days days");
7 $end_date = $end_date->format("Y-m-d");
8
9 $node->set('field_cn_end_date', $end_date);
10 $node->save();
The rest of this code does some convoluted wrangling to figure out end dates based on user
permissions, changes a node title, looks to see if this is an extension of a previously submitted notice
and grabs some date fields from the original notice for use those in the current node.
1 /**
2 * Implements hook_ENTITY_TYPE_presave().
3 */
4 function ogg_mods_node_presave(NodeInterface $node) {
5 switch ($node->getType()) {
6 case 'cat_notice':
7 $end_date = NULL != $node->get('field_cn_start_end_dates')->end_value ? $node-\
8 >get('field_cn_start_end_dates')->end_value : 'n/a';
9 $govt_body = NULL != $node->field_cn_governmental_body->value ? $node->field_c\
10 n_governmental_body->value : 'Unnamed Government Body';
11 $start_date_val = $node->get('field_cn_start_date')->value;
12
13 $accountProxy = \Drupal::currentUser();
14 $account = $accountProxy->getAccount();
15 // Anonymous users automatically fill out the end_date.
16 if (!$account->hasPermission('administer cat notice')) {
17 $days = intval($node->get('field_cn_suspension_length')->value) - 1;
18
19 $end_date = DrupalDateTime::createFromFormat('Y-m-d', $start_date_val);
20 $end_date->modify("+$days days");
21 $end_date = $end_date->format("Y-m-d");
22 $node->set('field_cn_end_date', $end_date);
23 }
24
25 // Always reset the title.
26 $title = substr($govt_body, 0, 200) . " - $start_date_val";
27 $node->setTitle($title);
28
29 /*
30 * Fill in Initial start and end dates if this is an extension of
31 * a previously submitted notice.
32 */
33 $extension = $node->get('field_cn_extension')->value;
34 if ($extension) {
Dates and Times 105
35 $previous_notice_nid = $node->get('field_cn_original_notice')->target_id;
36 $previous_notice = Node::load($previous_notice_nid);
37 if ($previous_notice) {
38 $initial_start = $previous_notice->get('field_cn_start_date')->value;
39 $initial_end = $previous_notice->get('field_cn_end_date')->value;
40 $node->set('field_cn_initial_start_date', $initial_start);
41 $node->set('field_cn_initial_end_date', $initial_end);
42 }
43 }
44
45 break;
46 }
47 }
1 date_default_timezone_set('Europe/London');
2
3 $d1 = new DateTime('2008-08-03 14:52:10');
4 $d2 = new DateTime('2008-01-03 11:11:10');
5 var_dump($d1 == $d2);
6 var_dump($d1 > $d2);
7 var_dump($d1 < $d2);
8
9 // Returns.
10 bool(false)
11 bool(true)
12 bool(false)
1 $source_node = $node_storage->load($nid);
2 $expiration_date = $source_node->field_expiration_date->value;
3
4 // Use expiration date to un-publish expired reseller nodes to hide them.
5 $status = 1;
6 if ($expiration_date) {
7 $expirationDate = DrupalDateTime::createFromFormat('Y-m-d', $expiration_date);
8 $now = new DrupalDateTime();
9 if ($expiration_date < $now) {
10 // When expired, unpublish the node.
11 $node->set('status', 0);
12 $node->save(); }
13 }
14 }
Dates and Times 107
It might be interesting to factor in the timezone as date fields are stored in UTC. See
https://en.wikipedia.org/wiki/Coordinated_Universal_Time
Note. This will handle epoch dates before 1970. You can also use $node->get('changed') to retrieve
the changed date.
1 use Drupal\Core\Datetime\DrupalDateTime;
2
3 protected function loadFirstOpinionYear($term_id) {
4 $storage = \Drupal::entityTypeManager()->getStorage('node');
5 $query = \Drupal::entityQuery('node')
6 ->condition('status', 1)
7 ->condition('type', 'opinion')
8 ->condition('field_category', $term_id, '=')
9 ->sort('title', 'ASC') // or DESC
10 ->range(0, 1);
11 $nids = $query->execute();
12 if ($nids) {
13 $node = $storage->load(reset($nids));
14 }
15 $time = $node->get('created')->value;
16
17 $d = DrupalDateTime::createFromTimestamp($time);
18 $str = $d->format('Y-m-d H:i:s');
19 return $str;
20 }
To find any nodes that were changed in the last 7 days, use the following:
Dates and Times 108
1 $query = \Drupal::entityQuery('node')
2 ->condition('field_tks_program_status', 'ready_for_release')
3 ->accessCheck(FALSE)
4 ->condition('type', 'teks_pub_program');
5 // Query changed date within 7 days of today.
6 $query->condition('changed', strtotime('-7 days'), '>=');
7
8 $program_nids = $query->execute();
9 $program_nids = array_values($program_nids);
27 $start_date_ts = $start_date->getTimestamp();
28 $end_date_ts = $end_date->getTimestamp();
29
30 $query = \Drupal::entityQuery('node')
31 ->condition('status', 1)
32 ->condition('type', 'event')
33 ->condition('field_event_category', $term_id, '=')
34 ->condition('created', $start_date_ts, '>=')
35 ->condition('created', $end_date_ts, '<=')
36 ->sort('title', 'DESC');
37 $nids = $query->execute();
38 $titles = [];
39 if ($nids) {
40 foreach ($nids as $nid) {
41 $node = Node::load($nid);
42 $titles[]= $node->getTitle();
43 }
44 }
45 return $titles;
46 }
Read more in the article: Date (range) fields and Entity Query from February 2018 at https://blog.
werk21.de/en/2018/02/05/date-range-fields-and-entity-query-update
and
this Stack exchange question at https://drupal.stackexchange.com/questions/198324/how-to-do-a-
date-range-entityquery-with-a-date-only-field-in-drupal-8
1 use Drupal\Core\Datetime\DrupalDateTime;
2 use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
3
4 public function test4() {
5
6 // Date-only fields are stored in the database like: '2021-12-27';
7
8 // Get a date string suitable for use with entity query.
9 $date = DrupalDateTime::createFromFormat('j-M-Y', '27-Dec-2021');
10 $date->setTimezone(new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE));
11 // NB. Specify the date-only storage format - not the datetime storage format!
12 $query_date = $date->format(DateTimeItemInterface::DATE_STORAGE_FORMAT);
13
14 // Query using =.
15 $query = \Drupal::entityQuery('node')
16 ->condition('type', 'event')
17 ->condition('status', 1)
18 ->condition('field_event_date.value', $query_date, '=')
19 ->sort('title', 'ASC');
20 $nids = $query->execute();
21 $count = count($nids);
22
23 $str = "Results";
24 $str .= "<br/>Found $count events for field_event_date = $query_date";
25 foreach ($nids as $nid) {
26 $event_node = Node::load($nid);
27 $title = $event_node->getTitle();
28 $date = $event_node->field_event_date->value;
29 $str .= "<br/>$title - date: $date";
30 }
31 $str .= "<br/>";
32
33 // Query using >.
34 $query = \Drupal::entityQuery('node')
35 ->condition('type', 'event')
36 ->condition('status', 1)
37 ->condition('field_event_date.value', $query_date, '>')
38 ->sort('title', 'ASC');
39 $nids = $query->execute();
40 $count = count($nids);
41
42 $str .= "<br/>Found $count events for field_event_date > $query_date";
43
Dates and Times 111
69 return $render_array;
70 }
More in the article: Date (range) fields and Entity Query from February 2018 at https://blog.werk21.
de/en/2018/02/05/date-range-fields-and-entity-query-update
and
this Stack exchange question at https://drupal.stackexchange.com/questions/198324/how-to-do-a-
date-range-entityquery-with-a-date-only-field-in-drupal-8
1 $start = $node->field_when->value;
2 $formatter = \Drupal::service('date.formatter');
3
4 //returns something like 12/21/2020 10:00 am
5 $start_time = $formatter->format($start, 'custom', 'm/d/Y g:ia');
Alternatively, you could load it, create a DrupalDateTime and then format it:
1 $start = $node->field_when->value;
2 $dt = DrupalDateTime::createFromTimestamp($start);
3 $start_date = $dt->format('m/d/y'); //returns 12/21/22
4 $start_time = $dt->format('g:ia'); // returns 10:00am
1 $start_ts = $node->field_when->value;
2 $start_dt = DrupalDateTime::createFromTimestamp($start_ts);
3 $start_date = $start_dt->format('m/d/Y');
4 $duration = $node->field_when->duration; //1439 = all day
5 if ($duration == 1439) {
6 $start_time = "all day";
7 }
8 else {
9 $start_time = $start_dt->format('g:ia');
10 }
You can also peek into the repeating rule and repeating rule index. These are in the smart_date_rule
table and I believe the index column identifies which item is in the “instances” column.
1 $rrule = $when->rrule;
2 $rrule_index = $when->rrule_index;
8.21: Reference
From https://git.drupalcode.org/project/drupal/-/blob/10.1.x/core/lib/Drupal/Component/Datetime/DateTimePlus.p
Dates and Times 116
1 /**
2 * Wraps DateTime().
3 *
4 * This class wraps the PHP DateTime class with more flexible initialization
5 * parameters, allowing a date to be created from an existing date object,
6 * a timestamp, a string with an unknown format, a string with a known
7 * format, or an array of date parts. It also adds an errors array
8 * and a __toString() method to the date object.
9 *
10 * This class is less lenient than the DateTime class. It changes
11 * the default behavior for handling date values like '2011-00-00'.
12 * The DateTime class would convert that value to '2010-11-30' and report
13 * a warning but not an error. This extension treats that as an error.
14 *
15 * As with the DateTime class, a date object may be created even if it has
16 * errors. It has an errors array attached to it that explains what the
17 * errors are. This is less disruptive than allowing datetime exceptions
18 * to abort processing. The calling script can decide what to do about
19 * errors using hasErrors() and getErrors().
20 *
21 * @method $this add(\DateInterval $interval)
22 * @method static array getLastErrors()
23 * @method $this modify(string $modify)
24 * @method $this setDate(int $year, int $month, int $day)
25 * @method $this setISODate(int $year, int $week, int $day = 1)
26 * @method $this setTime(int $hour, int $minute, int $second = 0, int $microseconds \
27 = 0)
28 * @method $this setTimestamp(int $unixtimestamp)
29 * @method $this setTimezone(\DateTimeZone $timezone)
30 * @method $this sub(\DateInterval $interval)
31 * @method int getOffset()
32 * @method int getTimestamp()
33 * @method \DateTimeZone getTimezone()
34 */
(More at https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Datetime%21DrupalDateTime.php/clas
)
8.21.3: UTC
Coordinated Universal Time or UTC is the primary time standard by which the world regulates
clocks and time. It is within about 1 second of mean solar time at 0° longitude (at the IERS Reference
Dates and Times 117
Meridian as the currently used prime meridian) such as UT1 and is not adjusted for daylight saving
time. It is effectively a successor to Greenwich Mean Time (GMT). From From https://en.wikipedia.
org/wiki/Coordinated_Universal_Time
8.21.5: Links
• Drupal API DrupalDateTime Class https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Datetim
• From php.net, the definitive documentation on date format strings https://www.php.net/
manual/en/datetime.format.php#:~:text=format%20parameter%20string-,format,-character
• Coordinated Universal Time or UTC Wikipedia article https://en.wikipedia.org/wiki/
Coordinated_Universal_Time
• Goran Nikolovski’s article: Set date programatically from January 2019 https:
//gorannikolovski.com/blog/set-date-field-programmatically#:~:text=Get%20the%20date%
20field%20programmatically,)%3B%20%2F%2F%20For%20datetime%20fields
• Patrick’s article: Date (range) fields and Entity Query from February 2018 https://blog.werk21.
de/en/2018/02/05/date-range-fields-and-entity-query-update
• Drupal APIS https://www.drupal.org/docs/drupal-apis
9: Debugging
9.1: Overview
Using a combination of PhpStorm, DDEV and Xdebug makes debugging a pleasure. PhpStorm is
not essential. Xdebug works fine with other IDE’s also. In my experience, many Drupal developers
have not experienced using a true debugger, but once they do, they wonder how they ever delivered
any code without it.
1 /**
2 * Show all error messages, with backtrace information.
3 *
4 * In case the error level could not be fetched from the database, as for
5 * example the database connection failed, we rely only on this value.
6 */
7 $config['system.logging']['error_level'] = 'verbose';
1 /**
2 * Error reporting level: display no errors.
3 */
4 const ERROR_REPORTING_HIDE = 'hide';
5
6 /**
7 * Error reporting level: display errors and warnings.
8 */
9 const ERROR_REPORTING_DISPLAY_SOME = 'some';
10
Debugging 119
11 /**
12 * Error reporting level: display all messages.
13 */
14 const ERROR_REPORTING_DISPLAY_ALL = 'all';
15
16 /**
17 * Error reporting level: display all messages, plus backtrace information.
18 */
19 const ERROR_REPORTING_DISPLAY_VERBOSE = 'verbose';
and
1 /**
2 * Enable local development services.
3 */
4 $settings['container_yamls'][] = DRUPAL_ROOT . '/sites/development.services.yml';
1 $config['system.performance']['css']['preprocess'] = FALSE;
2 $config['system.performance']['js']['preprocess'] = FALSE;
3 $settings['cache']['bins']['render'] = 'cache.backend.null';
4 $settings['cache']['bins']['page'] = 'cache.backend.null';
5 $settings['cache']['bins']['dynamic_page_cache'] = 'cache.backend.null';
1 $ ddev xdebug on
2
3 $ ddev xdebug off
Note. Enabling Xdebug will slow down your app because xdebug has a significant performance
impact so be sure to disable it when you are finished debugging.
Add this to your .zshrc or .bash file for ‘xon‘ and ‘xoff‘ shortcut
Debugging 121
1 [PHP]
2
3 xdebug.client_port=9000
Select a breakpoint:
Debugging 122
If you accidentally selected the wrong local path, in PhpStorm, go to Settings, PHP, Servers and
delete all servers that are displayed. The correct one will be recreated after you retry the operation
above.
Debugging 124
Once you accept the local path, you should see a highlighted line indicating the current line. The
debug window will appear below showing the call stack
Debugging 125
1 $ ddev ssh
2
3 $ vendor/bin/drush status
To setup command line debugging, follow the steps above to setup for Drupal Code debugging to
confirm that you have debugging working. Then look in PhpStorm’s: settings, PHP, Servers, and
select the server you set up from the previous steps. Specify the top level path as shown below.
Usually it will be /var/www/html
Debugging 127
Now in the terminal ssh into the DDEV container and execute drush:
1 $ ddev ssh
2
3 $ vendor/bin/drush status
PhpStorm will pop up and display the current line and you can debug to your heart’s content:
Debugging 129
More at https://ddev.readthedocs.io/en/stable/users/debugging-profiling/step-debugging/
1 $ vendor/bin/drush status
2
3 Xdebug: \[Step Debug\] Could not connect to debugging client. Tried: host.docker.int\
4 ernal:9003 (through xdebug.client_host/xdebug.client_port).
This means you have not clicked the PhpStorm button: ”Start listening for PHP Debug Connections”.
Just click it and try again
9.9.2.1: Curl
1 $ curl https://d9.ddev.site
9.9.2.2: Logs
If the IDE doesn’t respond, take a look at ddev logs. Use ddev logs to display current logs for
the project’s web server. See https://ddev.readthedocs.io/en/stable/users/basics/commands/#logs for
more.
If you see a message like
1 "PHP message: Xdebug: [Step Debug] Could not connect to debugging client. Tried: hos\
2 t.docker.internal:9000 (through xdebug.client_host/xdebug client_port)"
then php/xdebug (inside the container) is not able to make a connection to port 9000.
This means you have not clicked the “Start listening for PHP Debug Connections” button in
PhpStorm. Just click it and try again.
Note. Port 9003 is more current.
Debugging 131
9.9.2.3: Telnet
With PhpStorm NOT listening for a PHP Debug connection, try to telnet to see what is listening to
port 9003 with this:
You should get connection refused. If you get a connection (see below) something is listening on
port 9003 and you should either disable it or use a different port. Note. You must set that in both
PhpStorm as well as DDEV.
This shows a connection succeeding. You can try it with PhpStorm listening for a debug connection:
Use Control ] to exit and then type quit to return to exit telnet.
On a mac, use sudo lsof -i :9003 -sTCP:LISTEN to find out what is listening on that port and stop
it, or change the xdebug port and configure both DDEV and PhpStorm to use the new one .
More about changing ports at https://ddev.readthedocs.io/en/stable/users/debugging-profiling/step-
debugging/#using-xdebug-on-a-port-other-than-the-default-9003
Note. In the past, php-fpm was likely to be one of the apps using port 9000.
• To check to make sure that Xdebug is enabled, you can use php -i | grep xdebug inside the
container. You can also use other techniques to view the output of phpinfo(), including Drupal’s
admin/reports/status/php. Below you can see the expected output when Xdebug is enabled
assuming you have Xdebug v3.2.0 or later.
See https://ddev.readthedocs.io/en/stable/users/step-debugging
Debugging 132
1 lsof -i TCP:9003
2 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
3 phpstorm 78897 selwyn 209u IPv4 0x4d118c80f54422d7 0t0 TCP *:9003 (LISTEN)
You can also use nc and netstat but they is not quite as informative:
1 $ nc -z localhost 9003
2 Connection to localhost port 9003 [tcp/*] succeeded!
The message Connection to localhost port 9003 [tcp/*] succeeded! means that there is a
program listening on port 9003. If you get nothing when you type the command, then nothing is
listening.
Here netstat reports that something is listening on port 9003. Again, if you get nothing when you
type the command, then nothing is listening.
Here is an example running lsof and finding php-fpm listening on port 9000
1 $ lsof -i TCP:9000
2
3 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
4
5 php-fpm 732 selwyn 7u IPv4 0x4120ed57a07e871f 0t0 TCP
6 localhost:cslistener (LISTEN)
7
8 php-fpm 764 selwyn 8u IPv4 0x4120ed57a07e871f 0t0 TCP
9 localhost:cslistener (LISTEN)
10
11 php-fpm 765 selwyn 8u IPv4 0x4120ed57a07e871f 0t0 TCP
12 localhost:cslistener (LISTEN)
Debugging 133
1 /**
2 * Enable local development services.
3 */
4 $settings['container_yamls'][] = DRUPAL_ROOT . '/sites/development.services.yml';
1 $settings['cache']['bins']['render'] = 'cache.backend.null';
9.12.1: Setup
We need both the Devel and Devel Kint Extras module. Devel Kint Extras1 ships with the kint-php
library so installing this via Composer will take care of it automatically.
Install using Composer:
1 https://www.drupal.org/project/devel_kint_extras
Debugging 134
9.12.4: Kint::dump
From Migrate Devel contrib in module2 ,
docroot/modules/contrib/migrate_-
devel/src/EventSubscriber/MigrationEventSubscriber.php
2 https://www.drupal.org/project/migrate_devel
Debugging 135
9.13: Resources
• DDEV Documentation3
• Debugging with Xdebug in DDEV docs4
• Debug Drush commands with PhpStorm at5
• Configuring PhpStorm Drupal.org docs updated September 20226
• Debugging Drush commands7
• DDEV docs on using a different port for debugging8
• How to setup Devel and Kint on Drupal 9 by Alex Aug 20219
3 https://ddev.readthedocs.io/en/stable/
4 https://ddev.readthedocs.io/en/stable/users/debugging-profiling/step-debugging
5 https://www.jetbrains.com/help/phpstorm/drupal-support.html#view_drupal_api_documentation
6 https://www.drupal.org/docs/develop/development-tools/configuring-phpstorm
7 https://www.jetbrains.com/help/phpstorm/drupal-support.html#debugging-drush-commands
8 https://ddev.readthedocs.io/en/stable/users/debugging-profiling/step-debugging/#using-xdebug-on-a-port-other-than-the-default-
9003
9 https://www.altagrade.com/blog/how-install-devel-and-kint-drupal-9
10: Development
10.1: Local Drupal site setup
Local development is best done using containers and DDEV1 . Setting up a local site is a completely
painless process.
Pick one of these options after installing Docker and Ddev:
Using the DDEV Quickstart guides to install Drupal, Wordpress, TYPO3, Backdrop, Magento,
Laravel etc. at https://ddev.readthedocs.io/en/stable/users/quickstart/rupal2
1 mkdir my-drupal10-site
2 cd my-drupal10-site
3 ddev config --project-type=drupal10 --docroot=web --create-docroot
4 ddev start
5 ddev composer create "drupal/recommended-project" --no-install
6 ddev composer require drush/drush --no-install
7 ddev composer install
8 ddev drush site:install -y
9 ddev drush uli
10 ddev launch
OR
From https://www.drupal.org/docs/official_docs/en/_local_development_guide.html
Start by specifying your SITE_NAME using export:
1 export SITE_NAME=d9site
2 export SITE_NAME=clientsite
1 export SITE_NAME=my-drupal-site
2 composer create-project drupal/recommended-project $SITE_NAME
3 cd $SITE_NAME
4 ddev config --docroot=web --project-name=$SITE_NAME --project-type=drupal9
5 ddev start
6 ddev exec drush site-install --account-name=admin --account-pass=admin
To verify that you have the correct permissions, run this ls command with the a, l, h, and d switches
and check that your permissions match the following output:
You are now ready to develop a Drupal website on your local machine.
10.5: DDEV
For local Docker container development on any platform, there is no better tool than DDEV. This is
a well-documented6 , well-supported7 tool by the Amazing Randy Fay. You can get help from him
or some of the other friendly folks on Discord8 almost instantly.
From the docs:
• Lots of built-in help: ddev help and ddev help <command>. You’ll find examples and
explanations.
• DDEV Documentation9
• DDEV Stack Overflow10 for support and frequently asked questions. We respond quite quickly
here and the results provide quite a library of user-curated solutions.
• DDEV issue queue11 for bugs and feature requests
• Interactive community support on Discord12 for everybody, plus sub-channels for CMS-specific
questions and answers.
• ddev-contrib13 repo provides a number of vetted user-contributed recipes for extending and
using DDEV. Your contributions are welcome.
• awesome-ddev14 repo has loads of external resources, blog posts, recipes, screencasts, and the
like. Your contributions are welcome.
• Twitter with tag #ddev15 will get to us, but it’s not as good for interactive support, but we’ll
answer anywhere.
• You can override the config.yaml with extra files named config.*.yaml\. For example, many
teams use config.local.yaml for configuration that is specific to one environment, and that
is not intended to be checked into the team’s default config.yaml.
6 https://ddev.readthedocs.io/en/stable/
7 https://ddev.readthedocs.io/en/stable/#support-and-user-contributed-documentation
8 https://discord.gg/hCZFfAMc5k
9 https://ddev.readthedocs.io/en/stable/users/faq/
10 https://stackoverflow.com/questions/tagged/ddev
11 https://github.com/drud/ddev/issues
12 https://discord.gg/hCZFfAMc5k
13 https://github.com/drud/ddev-contrib
14 https://github.com/drud/awesome-ddev
15 https://twitter.com/search?q=%23ddev&src=typd&f=live
Development 139
In the endless quest for speed in local development, try using NFS or Mutagen on MAC OS.
Apparently the WSL2 setup on Windows 10/11 is the fastest performer for DDEV at th time of
this writing.
10.5.1.1: NFS
1 router_http_port: \"80\"
2 router_https_port: \"443\"
3 timezone: America/Chicago
4 # and for nfs
5 nfs_mount_enabled: true
10.5.1.2: Mutagen
Note. we usually use port 9000 for xdebug look in .ddev/php/xdebug_report_port.ini for the real
port settings. Recently for a project I found it set to 11011
The contents of the file are:
Development 141
1 [PHP]
2
3 xdebug.remote_port=11011
For phpstorm, if you start listening for a debug connection, it should automatically try to create a
debug server config for you. If it doesn’t manually create one
e.g name: tea.ddev.site
host tea.ddev.site
port: 80
debugger: xdebug
check use path mappings
for docroot specify: /var/www/html/docroot (i.e. wherever index.php is)
1 ddev ssh
1 export PHP_IDE_CONFIG=\"serverName=d8git.ddev.site\"
or
1 export PHP_IDE_CONFIG=\"serverName=inside-mathematics.ddev.site\"
1 ../vendor/drush/drush/drush fixmat
If it doesn’t seem to work, try enable Break at first line in PHP scripts - something will always stop
then.
more at https://stackoverflow.com/questions/50283253/how-can-i-step-debug-a-drush-command-
with-ddev-and-phpstorm
• To be able to call drush from anywhere, install the Drush Launcher18 . Launcher is a small
program which listens on your $PATH and hands control to a site-local Drush that is in the
/vendor directory of your Composer project.
Of course this works with any site where you’ve set up your drush aliases20 .
also
and
1 docker volume ls
1 services:
2 chromedriver
Use
ddev ssh -s chromedriver
21 https://github.com/drud/ddev/issues/1465
Development 144
Q. Deleting the images: Does that mean it will delete the db snapshots? A. No, docker images are
the versioned images that come from dockerhub, they’re are always replaceable.
Absolutely nothing you do with ddev will delete your snapshots - you have to remove them manually
They’re stored in .ddev/db_snapshots on the host (under each project)
also
and
prunes every single thing, destroys all ddev databases and your composer cache.
1 Pull failed: db container failed: log=, err=health check timed out: labels map[com.d\
2 dev.site-name:inside-mathematics com.docker.compose.service:db] timed out without be
3 coming healthy, status=
Or like this:
1 $ ddev start
2 Starting inside-mathematics...
3 Pushing mkcert rootca certs to ddev-global-cache
4 Pushed mkcert rootca certs to ddev-global-cache
5 Creating ddev-inside-mathematics-db ... done
6 Creating ddev-inside-mathematics-dba ... done
7 Creating ddev-inside-mathematics-web ... done
8
9 Creating ddev-router ... done
10
11 Failed to start inside-mathematics: db container failed: log=, err=health check time\
12 d out: labels map[com.ddev.site-name:inside-mathematics com.docker.compose.service:d
13 b] timed out without becoming healthy, status=
This is almost always caused by a corrupted database, most often in a larger database. Since v0.17.0,
this is generally only caused by docker being shut down in an ungraceful way. Unfortunately, both
Docker for Windows and Docker for Mac shut down without notifying the container during upgrade,
with a manual Docker exit, or at system shutdown. It can be avoided by stopping or removing your
projects before letting Docker exit.
To fix, ddev remove --remove-data, then ddev start. This may fail and suggest this bazooka
version:
ddev stop --remove-data --omit-snapshot
10.7: PHPStorm
All the PHPStorm Drupal magic is at https://www.jetbrains.com/help/phpstorm/drupal-support.
html#view_drupal_api_documentation
PHPStorm has a series of instructions for configuring PHPStorm with Xdebug23 but unfortunately,
nothing specifically on using it with DDEV. Fortunately it doesn’t require any special setup for it to
work.
Some settings I use
If phpstorm doesn’t stop when you set a breakpoint on some code, try deleting the server from the
config debug, php, servers.
Make sure PHPStorm is listening by clicking the listen button
When you try again it will be recreated but you will probably need to specify the path (from the
image above).
1 xdebug_break()
more at https://xdebug.org/docs/all_functions
Development 148
1 $ lsof -i TCP:9000
Another option is
1 nc -z localhost 9000
If it says:
Connection to localhost port 9000 [tcp/cslistener] succeeded!
this means something is listening. If you get nothing, then nothing is listening.
You can also run network utility, scan port 9000 to 9003 on 127.0.0.1 (localhost)
What could be listening on port 9000?
1 $ lsof -i TCP:9000
This will include the local settings file as part of Drupal’s settings file.
3. Open settings.local.php and make sure development.services.yml is enabled.
1 services:
2 cache.backend.null:
3 class: Drupal\Core\Cache\NullBackendFactory
1 $config['system.performance']['css']['preprocess'] = FALSE;
2 $config['system.performance']['js']['preprocess'] = FALSE;
5. Uncomment these lines in settings.local.php to disable the render cache and disable dynamic page
cache:
1 $settings['cache']['bins']['render'] = 'cache.backend.null';
2 $settings['cache']['bins']['dynamic_page_cache'] = 'cache.backend.null';
24 https://www.drupal.org/node/2598914
Development 151
1 $settings['cache']['bins']['page'] = 'cache.backend.null';
If you do not want to install test modules and themes, set the following to FALSE:
1 $settings['extension_discovery_scan_tests'] = FALSE;
6. Open sites/development.services.yml in the sites folder and add the following block to disable the
twig cache and enable twig debugging:
1 parameters:
2 twig.config:
3 debug: true
4 auto_reload: true
5 cache: false
NOTE: If the parameters section is already present in the development.services.yml file, append the
twig.config section to it.
7. Rebuild the Drupal cache (drush cr) otherwise your website will encounter an unexpected error
on page reload.
10.11: Development.services.yml
I usually develop with this in sites/default/development.services.yml
1 /**
2 * Enable local development services.
3 */
4 $settings['container_yamls'][] = DRUPAL_ROOT . '/sites/development.services.yml';
Development 153
1 /**
2 * Enable local development services.
3 */
4 $settings['container_yamls'][] = DRUPAL_ROOT . '/sites/development.services.yml';
1 $settings['cache']['bins']['render'] = 'cache.backend.null';
10.13: Kint
From https://www.webwash.net/how-to-print-variables-using-devel-and-kint-in-drupal/
10.13.1: Setup
We need both the Devel and Devel Kint Extras module. Devel Kint Extras25 ships with the kint-php
library so installing this via Composer will take care of it automatically.
25 https://www.drupal.org/project/devel_kint_extras
Development 154
10.13.4: Kint::dump
From Migrate Devel contrib module26 ,
in /docroot/modules/contrib/migrate_-
devel/src/EventSubscriber/MigrationEventSubscriber.php.
26 https://www.drupal.org/project/migrate_devel
Development 155
Sometimes, when drush cr throws errors like that try drush sqlc and then truncate cache_-
bootstrap and truncate cache_discovery.
10.17: Resources
• Composer best practices for Drupal 8 from Lullabot Jan 201827
• Why DDEV by Randy Fay (Author of DDEV) from Dec 202228
• How to setup Devel and Kint on Drupal 9 by Alex Aug 202129
27 https://www.lullabot.com/articles/drupal-8-composer-best-practices
28 https://opensource.com/article/22/12/ddev
29 https://www.altagrade.com/blog/how-install-devel-and-kint-drupal-9
11: Email
11.1: Send email
Function to send an email in your module with a hook_mail() to set the parameters.
1 use Drupal\Core\Mail\MailManagerInterface;
2 use Drupal\Core\Language\LanguageManagerInterface;
3
4 /**
5 * Function to send an email in your module.
6 *
7 * @param string $to
8 * The recipient of the email.
9 * @param array $params
10 * An array of parameters to use in the email.
11 * @param string $subject
12 * The subject of the email.
13 * @param string $body
14 * The body of the email.
15 *
16 * @return bool
17 * TRUE if the email was sent successfully, FALSE otherwise.
18 */
19 function send_my_email($to, $params, $subject, $body) {
20 // Get the language manager service.
21 $language_manager = \Drupal::service('language_manager');
22
23 // Get the default language.
24 $default_language = $language_manager->getDefaultLanguage()->getId();
25
26 // Get the mail manager service.
27 $mail_manager = \Drupal::service('plugin.manager.mail');
28
29 // Set up the email parameters.
30 $module = 'my_module';
31 $key = 'my_email_key';
32
Email 157
1 /**
2 * Implements hook_mail().
3 */
4 function hello_world_mail($key, &$message, $params) {
5 switch ($key) {
6 case 'hello_world_log':
7 $message['from'] = \Drupal::config('system.site')->get('mail');
8 $message['subject'] = t('There is an error on your website');
9 $message['body'][] = $params['message'];
10 if (isset($params['user'])) {
11 $user_message = 'The user that was logged in: [current-user:name]';
12 $message['body'][] = \Drupal::token()->replace($user_message, ['current-user\
13 ' => $params['user']]);
14 }
15
16 break;
17 }
18 }
11.3: Reference
• Sending html mails in Drupal 8/9 programmatically An example Drupal module including
Twig template by Joris Snoek - August 20201
• Sending Emails Using OOP and Dependency Injection in Drupal 8, 9 By Alex Novak -
November 2020.2
• How email works in Drupal - updated July 20213
• Sendgrid Integration Drupal module4
1 https://www.lucius.digital/en/blog/sending-html-mails-drupal-89-programmatically-example-drupal-module-including-twig-
template
2 https://www.drupalcontractors.com/blog/2020/11/09/sending-emails-using-oop-dependency-injection-drupal/
3 https://www.drupal.org/docs/contributed-modules/mime-mail/how-email-works-in-drupal
4 https://www.drupal.org/project/sendgrid_integration
12: Entities
12.1: Overview
The Entity API is used for manipulating entities (CRUD: create, read, update, delete). Entity
validation has its own API (which could validate an Entity saved via REST, rather than a form,
for example).
Entities come in two flavors: Content and Config(uration). The data storage mechanism moved
from being field-centric in Drupal 7 to entity-centric in Drupal 8, 9 and 10. This implies that all
fields attached to an entity share the same storage backend, making querying a lot easier. Entity
types are registered with Drupal as plugins.
While Drupal supports custom entities, I haven’t found a need for them in my projects. In most of
my experience, just using real nodes (sometimes unpublished) has sufficed admirably.
1 $query = $this->nodeStorage->getQuery()
2 ->accessCheck(FALSE)
3 ->condition('type', 'article')
4 ->condition('title', $name)
5 ->count();
6
7 $count_nodes = $query->execute();
8
9 if ($count_nodes == 0) {
If you know what the entity class name (bundle type) is, you can use it directly.
1 $node = Node::create(array(
2 'title' => 'New Article',
3 'body' => 'Article body',
4 'type' => 'article',
5 ));
1 $node->save();
1 use Drupal\node\Entity\Node;
2
3 $data = file_get_contents('https://www.drupal.org/files/druplicon-small.png');
4 $file = file_save_data($data, 'public://druplicon.png', FILE_EXISTS_RENAME);
5
6 $node = Node::create([
7 'type' => 'article',
8 'title' => 'A new article',
9 'field_image' => [
10 'target_id' => $file->id(),
11 'alt' => 'Drupal',
12 'title' => 'Drupal logo'
13 ],
14 ]);
15 assert($node->isNew(), TRUE);
16 $node->save();
17 assert($node->isNew(), FALSE);
12.9: Get the entity type and content type (or bundle
type)
Entities 162
1 use Drupal\Core\Entity\EntityInterface;
2
3 function hook_entity_presave(EntityInterface $entity) {
4 // getEntityTypeId() returns 'node'.
5 // $entity->bundle() returns 'article'.
6 if($entity->getEntityTypeId() == 'node' && $entity->bundle() == 'article') {
7 // Do your stuff here
8 }
9 }
1 switch (strtoupper($type)){
2 case 'PDF':
3 $mimetype = 'application/pdf';
4 break;
5 case 'JPG':
6 $mimetype = 'image/jpeg';
7 break;
8 case 'POW':
9 $mimetype = 'application/vnd.ms-powerpoint';
10 break;
11 }
12
13
14 // Create file entity.
15 $file = File::create();
16 $file->setFileUri($destination);
17 $file->setOwnerId(1);
18 $file->setMimeType($mimetype);
19 $file->setFileName($this->fileSystem->basename($final_destination));
20 $file->setPermanent();
21 $file->save();
22
23 $fid = $file->id();
24 return $fid;
While that was already possible to define validation constraints for base fields and for a field type,
there was no API to add constraints to a specific field. This has been addressed.
1 /**
2 * Implements hook_entity_bundle_field_info_alter().
3 */
4 function mymodule_entity_bundle_field_info_alter(&$fields, \Drupal\Core\Entity\Entit\
5 yTypeInterface $entity_type, $bundle) {
6 if ($entity_type->id() == 'node' && !empty($fields['myfield'])) {
7 $fields['myfield']->setPropertyConstraints('value', [
8 'Range' => [
9 'min' => 0,
10 'max' => 32,
11 ],
12 ]);
13 }
14 }
Custom validation constraints can also be defined. These links may be useful: ForumLeafCon-
straint3 / and ForumLeafConstraintValidator4
Also from https://drupalize.me/tutorial/entity-validation-api?p=2792 (you need a paid membership
to read the whole tutorial): Drupal includes the Symfony Validator component5 , and provides an
Entity Validation API to assist in validating the values of fields in an entity. By using the Entity
Validation API you can ensure that you’r validation logic is applied to Entity CRUD operations
regardless of how they are triggered. Whether editing an Entity via a Form API form, or creating a
new Entity via the REST API, the same validation code will be used.
12.13: Resources
• Entity API on Drupal.org updated Jan 20216
• Introduction to Drupal Entity API from Drupal.org updated Sep 20227
• Drupal entity API cheat sheet Updated July 20188
• Drupal 8 Entity query API cheat sheet by Keith Dechant , Software Architect - updated
February 20219
3 https://api.drupal.org/api/drupal/core%21modules%21forum%21src%21Plugin%21Validation%21Constraint%21ForumLeafConstraint.
php/class/ForumLeafConstraint/8
4 https://api.drupal.org/api/drupal/core%21modules%21forum%21src%21Plugin%21Validation%21Constraint%
21ForumLeafConstraintValidator.php/class/ForumLeafConstraintValidator/8
5 https://symfony.com/doc/2.8/components/validator.html
6 https://www.drupal.org/docs/drupal-apis/entity-api
7 https://www.drupal.org/docs/drupal-apis/entity-api/introduction-to-entity-api-in-drupal-8
8 https://drupalsun.com/zhilevan/2018/07/21/drupal-entity-api-cheat-sheet
9 https://www.metaltoad.com/blog/drupal-8-entity-api-cheat-sheet
Entities 165
• Entity Validation API tutorial from https://drupalize.me (requires paid membership to read the
whole tutorial) updated February 2022 at10
10 https://drupalize.me/tutorial/entity-validation-api?p=2792
13: Forms, Form API and A JAX
13.1: Overview
Forms can act just like controllers. You can set up a route and call the form in a very similar fashion.
See the example route below:
1 batch_examples.batch:
2 path: '/batch-examples/batchform'
3 defaults:
4 _title: 'Batch'
5 _form: 'Drupal\batch_examples\Form\BatchForm'
6 requirements:
7 _permission: 'access content'
The formid is comment_comment_form. Note dashes will need to become underscores in your code.
Alternatively, you can add a hook_form_alter and print_r or dsm the $form_id. If you prefer, you
could also log it to the watchdog log:
Forms, Form API and AJAX 167
Notice that a node add form looks like node_catastrophe_notice_form while a node edit form looks
more like this: node_catastrophe_notice_edit_form
1 $form['sanity_fieldset']['actions'] = [
2 '#type' => 'actions',
3 ];
4 $form['sanity_fieldset']['actions']['submit'] = [
5 '#type' => 'submit',
6 '#value' => $this->t('Submit'),
7 ];
8
9 $form['sanity_fieldset']['actions']['update_nodes'] = [
10 '#type' => 'submit',
11 '#value' => $this->t('Update'),
12 '#submit' => ['::updateNodes'],
13 ];
14
15 $form['sanity_fieldset']['actions']['cache_warmer'] = [
16 '#type' => 'submit',
17 '#value' => $this->t('Warm Caches'),
18 '#submit' => ['::warmCaches'],
19 ];
and here is the beginning of the submit function that goes along with the update_nodes submit
button:
Forms, Form API and AJAX 168
Note. The example above shows a batch function. You can read more in the Batch and
Queue chapter1
1 $form["topic"]["#options"][992] = "selwyn";
And in another .module file I nuke the contents of the dropdown topic and reload it with different
content. These are children of term with tid 2806 sorted by the term name:
1 $form['field_highlight_section']['#access'] = 0;
2 $form['field_accordion_section']['#access'] = 0;
1 $form['field_text2']['#disabled'] = true;
Here is the whole function where I also check what these is currently in use:
1 $form['revision_information']['#access'] = FALSE;
2 $form['moderation_state']['#access'] = FALSE;
1 /**
2 * Class SrpAddFeedbackForm.
3 */
4 class SrpAddFeedbackForm extends FormBase {
5
6 /**
7 * @var int $instanceId
8 * Used to make sure the getFormId is always unique.
9 */
10 private static int $instanceId;
11 ...
In the getFormId() method where you would normally just use return 'tea_teks_srp_feedback_-
add';, you can use the following code instead:
1 /**
2 * {@inheritdoc}
3 */
4 public function getFormId() {
5 if (empty(self::$instanceId)) {
6 self::$instanceId = 1;
7 }
8 else {
9 self::$instanceId++;
10 }
11 return 'tea_teks_srp_feedback_add' . self::$instanceId;
12 }
1. Create the field that will control the other field (radio buttons in this case)
2. Create the field that will be hidden (or manipulated in various ways)
3. Create the field with a ['#states'] index
Create the copies_yes_no field, then the how_many_oversized field and then finally the #states
attribute which will make the how_many_oversized field appear if you click Yes to the copies_yes_no
field.
Here is an Example form which will only show the how_many_oversized field when the copies_-
yes_no radio is set to yes:
1 $form['nonstandard']['copies_yes_no'] = [
2 '#type' => 'radios',
3 '#title' => $this->t('Did the requestor ask for copies of nonstandard documents (e\
4 .g., oversized paper, DVD, or VHS tape)?'),
5 '#default_value' => 0,
6 '#options' => [
7 0 => $this->t('Yes'),
8 1 => $this->t('No'),
9 ],
10 ];
11
12 $form['nonstandard']['how_many_oversized'] = [
13 '#type' => 'number',
14 '#title' => $this->t('Oversized paper copies (e.g., 11 inches by 17 inches, greenb\
15 ar, bluebar), # of pages'),
16 '#min' => 0,
17 '#max' => 65535,
18 '#size' => 6,
19 ];
20
21 $form['nonstandard']['how_many_oversized']['#states'] = [
22 // Only show when copies_yes_no is yes.
23 'visible' => [
24 ':input[name="copies_yes_no"]' => ['value' => '0'],
25 ],
26 ];
Forms, Form API and AJAX 173
Here is a variant on this theme for making a field visible or required. In this example, it isn’t using
Forms, Form API and AJAX 174
the [value] as part of the name. When I tried that above, it didn’t seem to work. Perhaps I didn’t
have the correct jquery selector.
1 $form['field_blah_blah']['#states']= [
2 // Only show when scoring_unavailable is not checked.
3 'visible' => [
4 ':input[name="scoring_unavailable"]' => ['unchecked' => TRUE],
5 ],
6 'required' => [
7 ':input[name="scoring_unavailable"]' => ['unchecked' => TRUE],
8 ],
9 ];
1 $key = $form_state->getValue('education_level');
2 $educationLevel = $form['education_level']['#options'][$key];
13.10: Autocomplete
1 $vid1 = 'media_tags';
2 $form['tags'] = [
3 '#type' => 'entity_autocomplete',
4 '#title' => $this->t('Tags'),
5 '#target_type' => 'taxonomy_term',
6 '#selection_settings' => [
7 'target_bundles' => [$vid1], //could be [$vid1, $vid2..].
8 ],
9 // '#tags' => TRUE,
10 ];
1 $form['user'] = [
2 '#type' => 'entity_autocomplete',
3 '#target_type' => 'user',
4 '#selection_handler' => 'views',
5 '#selection_settings' => [
6 'view' => [
7 'view_name' => 'users_view',
8 'display_name' => 'users',
9 'arguments' => []
10 ],
11 'match_operator' => 'CONTAINS'
12 ],
13 ];
from https://drupal.stackexchange.com/questions/308870/entity-autocomplete-form-api-field-
with-viewsselection-handler
1 /**
2 * Implements hook_form_FORM_ID_alter().
3 *
4 * Turn off autocomplete on login.
5 */
6 function dirt_form_user_login_form_alter(&$form, \Drupal\Core\Form\FormStateInterfac\
7 e $form_state, $form_id) {
8 $form['pass']['#attributes']['autocomplete'] = 'off';
9 $form['name']['#attributes']['autocomplete'] = 'off';
10 }
11 function dirt_form_user_pass_alter(&$form, \Drupal\Core\Form\FormStateInterface $for\
12 m_state, $form_id) {
13 $form['name']['#attributes']['autocomplete'] = 'off';
1 /**
2 * Custom form validation for catastrophe notices.
3 *
4 * Checks if a user enters a date that is more than
5 * 2 days earlier than the current date, but only if they didn't
6 * check the extension checkbox.
7 *
8 * @param $form
9 * @param \Drupal\Core\Form\FormStateInterface $form_state
10 */
11 function cn_form_validate($form, FormStateInterface $form_state) {
12 $extension = $form_state->getValue('field_cn_extension');
13 if (is_array($extension)) {
14 $extension = $extension['value'];
15 }
16 $start_date = $form_state->getValue('field_cn_start_date');
17 if ($start_date) {
18 $start_date = $start_date[0]['value'];
19 $start_date->setTime(0, 0, 0);
20 $now = new Drupal\Core\Datetime\DrupalDateTime();
21 $now->modify("-2 days");
22 $now->setTime(0, 0, 0);
23
24 if ($start_date < $now && !$extension) {
25 $form_state->setErrorByName('field_cn_start_date', t('The starting date is mor\
Forms, Form API and AJAX 178
1 /**
2 * Implements hook_form_alter().
3 */
4 function org_mods_form_alter(array &$form, FormStateInterface $form_state, $form_id)\
5 {
6 $accountProxy = \Drupal::currentUser();
7 $account = $accountProxy->getAccount();
8
9 // Add special validation for anonymous users (node add) only.
10 if (($accountProxy->isAnonymous() && ($form_id == 'node_catastrophe_notice_form'))\
11 ) {
12 $form['#validate'][] = 'cn_form_validate';
13 }
And here is the custom validate function which does some fun date arithmetic.
1 /**
2 * Custom form validation for catastrophe notices.
3 *
4 * Checks if a user enters a date that is more than
5 * 2 days earlier than the current date, but only if they didn't
6 * check the extension checkbox.
7 *
8 * @param $form
9 * @param \Drupal\Core\Form\FormStateInterface $form_state
10 */
11 function cn_form_validate($form, FormStateInterface $form_state) {
12 $extension = $form_state->getValue('field_cn_extension');
13 if (is_array($extension)) {
14 $extension = $extension['value'];
15 }
Forms, Form API and AJAX 179
16 $start_date = $form_state->getValue('field_cn_start_date');
17 if ($start_date) {
18 $start_date = $start_date[0]['value'];
19 $start_date->setTime(0, 0, 0);
20 $now = new Drupal\Core\Datetime\DrupalDateTime();
21 $now->modify("-2 days");
22 $now->setTime(0, 0, 0);
23
24 if ($start_date < $now && !$extension) {
25 $form_state->setErrorByName('field_cn_start_date', t('The starting date is mor\
26 e than 2 days in the past. Please select a later date'));
27 }
28 }
29 }
1 $form = \Drupal::formBuilder()->getForm('Drupal\test\Form\ExampleForm');
2 $build['egform'] = $form;
3 return $build;
In docroot/modules/custom/quick_pivot/src/Plugin/Block/QuickPivotSubscribeBlock.php we
use dependency injection to pass in the FormBuilderInterface and then get the form in a very
similar way.
The constructor grabbed the formbuilder like this:
1 <div
2 {{ attributes }}>
3 {{ title_prefix }}
4 {% if label %}
5 <h2{{ title_attributes }}>{{ label }}</h2>
6 {% endif %}
7 {{ title_suffix }}
8 {% block content %}
9 {{ content }}
10 {% endblock %}
11 </div>
1 {{ block content }}
For my custom theme called dprime, I added a new template file at themes/custom/dprime/templates/block/block-
and added lots of fun stuff to output the form in bits and pieces.
e.g. like here, to display the previous_clip item from the form’s render array which looks like this:
1 $form['previous_clip'] = [
2 '#type' => 'markup',
3 '#markup' => "(Clip $previous_clip_num/$clip_count)",
4 ];
And in the template, you can see content.previous_clip referencing this content.
Forms, Form API and AJAX 182
13.13: Redirecting
1 $cartUrl = Url::fromUri('internal:/cart');
2 $ajax_response->addCommand(
3 new RedirectCommand($cartUrl->toString())//Note this is a string!!
4 );
5 return $ajax_response;
In a non-ajax form, to redirect to the cart url, we would just use something like this:
1 $form_state->setRedirectUrl($cartUrl);
2 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Ajax%21RedirectCommand.php/class/RedirectCommand/9.4.x
Forms, Form API and AJAX 184
1 /**
2 * {@inheritdoc}
3 */
4 public function buildForm(array $form, FormStateInterface $form_state, $nojs = NUL\
5 L) {
6
7 // Get the form values and raw input (unvalidated values).
8 $values = $form_state->getValues();
9
10 // Define a wrapper id to populate new content into.
11 $ajax_wrapper = 'my-ajax-wrapper';
12
13 // Select element.
14 $form['my_select'] = [
15 '#type' => 'select',
16 '#empty_value' => '',
17 '#empty_option' => '- Select a value -',
18 '#default_value' => (isset($values['my_select']) ? $values['my_select'] : ''),
19 '#options' => [
20 '/node/1' => 'One',
21 '/node/2' => 'Two',
22 '/node/3' => 'Three'
23 ],
24 '#ajax' => [
25 'callback' => [$this, 'mySelectChange'],
26 'event' => 'change',
27 'wrapper' => $ajax_wrapper,
28 ],
29 ];
30 // Build a wrapper for the ajax response.
31 $form['my_ajax_container'] = [
32 '#type' => 'container',
33 '#attributes' => [
34 'id' => $ajax_wrapper,
35 ]
36 ];
37
38 return $form;
39 }
40
41 /**
42 * The callback function for when the `my_select` element is changed.
43 *
Forms, Form API and AJAX 185
44 */
45 public function mySelectChange(array $form, FormStateInterface $form_state) {
46
47 $values = $form_state->getValues();
48
49 $response = new AjaxResponse();
50 // $url = Url::fromUri('internal:/node/2');
51 $url = Url::fromUri('internal:' . $values['my_select']);
52
53 $command = new RedirectCommand($url->toString());
54 $response->addCommand($command);
55 return $response;
56 }
57
58 //Don't forget an empty submitForm().
59 public function submitForm(array &$form, FormStateInterface $form_state) {
60 // This function left blank intentionally.
61 }
1 return \Drupal::formBuilder()->getForm('Drupal\org_opinions\Form\IndividualOpinionFo\
2 rm');
1 use Drupal\Core\Form\FormBase;
2 use Drupal\Core\Form\FormStateInterface;
3 use Drupal\Core\Link;
4 use Drupal\Core\Url;
5 use Drupal\Core\Ajax\AjaxResponse;
6 use Drupal\Core\Ajax\RedirectCommand;
Let’s say you have several select elements on the form like my_select and my_select2 like this (Sorry
- not too creative, I know.):
Forms, Form API and AJAX 186
1 $ajax_wrapper = 'my-ajax-wrapper';
2 // Select.
3 $form['my_select'] = [
4 '#type' => 'select',
5 '#empty_value' => '',
6 '#empty_option' => '- Select a value -',
7 '#default_value' => (isset($values['my_select']) ? $values['my_select'] : ''),
8 '#options' => [
9 '/node/1' => 'One',
10 '/node/2' => 'Two',
11 '/node/3' => 'Three'
12 ],
13 '#ajax' => [
14 'callback' => [$this, 'mySelectChange'],
15 'event' => 'change',
16 'wrapper' => $ajax_wrapper,
17 ],
18 ];
19 $form['my_select2'] = [
20 '#type' => 'select',
21 '#empty_value' => '',
22 '#empty_option' => '- Select a value -',
23 '#default_value' => (isset($values['my_select']) ? $values['my_select'] : ''),
24 '#options' => [
25 '/node/4' => 'Four',
26 '/node/5' => 'Five',
27 '/node/6' => 'Six'
28 ],
29 '#ajax' => [
30 'callback' => [$this, 'mySelectChange'],
31 'event' => 'change',
32 'wrapper' => $ajax_wrapper,
33 ],
34 ];
Both use the same callback: mySelectChange. We can make the callback a little smarter by figuring
out internally which element called it.
Forms, Form API and AJAX 187
1 /**
2 * Callback function for changes to the `my_select`any select element.
3 */
4 public function mySelectChange(array $form, FormStateInterface $form_state) {
5 $values = $form_state->getValues();
6
7 //$elem stores the element info
8 $elem = $form_state->getTriggeringElement();
9 //$value[$elem["#name"]] stores the path like /node/2
10
11 $response = new AjaxResponse();
12 // Internal URLS must look like this: 'internal:/node/2'.
13 // $url = Url::fromUri('internal:' . $values['my_select']);
14 $url = Url::fromUri('internal:' . $values[$elem["#name"]]);
15
16 $command = new RedirectCommand($url->toString());
17 $response->addCommand($command);
18 return $response;
19
20 }
3 https://www.drupal.org/project/examples
Forms, Form API and AJAX 188
1 ajax_example.library:
2 version: 1.x
3 css:
4 base:
5 css/ajax-example-base.css: {}
6 js:
7 js/ajax-example.js: {}
Notice in the buildform() function that the code references the machine name for the library (not
the library’s filename which is ajax_example.libraries.yml.)
Here is the js for completeness:
1 (function ($) {
2
3 // Re-enable form elements that are disabled for non-ajax situations.
4 Drupal.behaviors.enableFormItemsForAjaxForms = {
5 attach: function () {
6 // If ajax is enabled, we want to hide items that are marked as hidden in
7 // our example.
8 if (Drupal.ajax) {
9 $('.ajax-example-hide').hide();
10 }
11 }
12 };
13
14 })(jQuery);
1 $form['#attached']['library'][] = 'core/drupal.dialog.ajax';
This line attaches the core/drupal.dialog.ajax library to the form and is necessary to render the
modal dialogs. Alternatively, you can include this as a dependency in your module’s *.info.yml
file.
There are some really sweet writeups about AJAX forms4 and AJAX Dialog boxes5
4 https://www.drupal.org/docs/8/api/javascript-api/ajax-forms
5 https://www.drupal.org/docs/drupal-apis/ajax-api/ajax-dialog-boxes
Forms, Form API and AJAX 189
1 $ajax_response->addCommand(
2 new OpenModalDialogCommand(t($popup_title), $content, ['width' => '60%', 'dialogCl\
3 ass' => 'product-cart-popup'])
4 );
5 return $ajax_response;
1 $addTocartFailed = $websphere_config->get('cart.add_to_cart_failed');
2 $success_modal_popup = [
3 '#theme' => 'add_cart_success_modal_popup',
4 '#data' => [
5 'addtocart_failed' => $addTocartFailed,
6 'product_id'=>$product_id,
7 'product_price'=>number_format($productDetails_p->price[1]->value, 2),
8 'product_category'=>'Singer',
9 'product_quantity'=>$product_qty,
10 ],
11 ];
12 $content['#markup'] = render($success_modal_popup);
13 $content['#attached']['library'][] = 'core/drupal.dialog.ajax';
14 $ajax_response->addCommand(
15 new OpenModalDialogCommand(t($popup_title), $content, ['width' => '60%', 'dialog\
16 Class' => 'product-cart-popup'])
17 );
18 return $ajax_response;
1 use Drupal\Core\Ajax\RedirectCommand;
2
3 class AddToCartForm extends FormBase {
4 // Create constructor and create functions for dependency injection
5
6 public function __construct(PrivateTempStoreFactory $temp_store_factory, SessionMana\
7 gerInterface $session_manager, AccountInterface $current_user) {
8 $this->tempStoreFactory = $temp_store_factory;
9 $this->sessionManager = $session_manager;
10 $this->currentUser = $current_user;
11 }
12
13 public static function create(ContainerInterface $container) {
14 return new static(
15 $container->get('user.private_tempstore'), $container->get('session_manager'),\
16 $container->get('current_user')
17 );
18 }
19
Forms, Form API and AJAX 191
1 $form['submit'] = [
2 '#type' => 'submit',
3 '#attributes' => ['class' => ['mobile-hide']),
4 '#id' => 'add_to_cart',
5 '#value' => $this->t('Add to cart'),
6 '#button_type' => 'primary',
7 '#ajax' => [
8 'callback' => '::add_to_cart_submit',
9 'event' => 'click',
10 'progress' => [
11 'type' => 'throbber',
12 'wrapper' => 'editor-settings-wrapper',
13 ],
14 ],
15 ];
16 return $form;
In this implementation, there is an empty submitForm() function. For the ajax submit callback we
use add_to_cart_submit(). Note how a new AjaxResponse is created.
Note. This should probably be a static function to avoid this symfony error:
If you want to redirect to the /cart url, you must add an AJAX command. See the RedirectCommand
in the API Reference6
6 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Ajax%21RedirectCommand.php/class/RedirectCommand/9.4.x
Forms, Form API and AJAX 192
1 $cartUrl = Url::fromUri('internal:/cart');
2 $ajax_response->addCommand(
3 new RedirectCommand($cartUrl->toString())//Note this is a string!!
4 );
5 return $ajax_response;
In a non-ajax form, to redirect to the cart url, we would just use something like this:
1 $form_state->setRedirectUrl($cartUrl);
1 /**
2 * {@inheritdoc}
3 */
4 public function buildForm(array $form, FormStateInterface $form_state, $nojs = NUL\
5 L) {
6
7 // Get the form values and raw input (unvalidated values).
8 $values = $form_state->getValues();
9
10 // Define a wrapper id to populate new content into.
11 $ajax_wrapper = 'my-ajax-wrapper';
12
13 // Select element.
14 $form['my_select'] = [
15 '#type' => 'select',
16 '#empty_value' => '',
17 '#empty_option' => '- Select a value -',
18 '#default_value' => (isset($values['my_select']) ? $values['my_select'] : ''),
19 '#options' => [
20 '/node/1' => 'One',
21 '/node/2' => 'Two',
22 '/node/3' => 'Three'
23 ],
24 '#ajax' => [
25 'callback' => [$this, 'mySelectChange'],
Forms, Form API and AJAX 193
1 return \Drupal::formBuilder()->getForm('Drupal\org_opinions\Form\IndividualOpinionFo\
2 rm');
Forms, Form API and AJAX 194
1 use Drupal\Core\Form\FormBase;
2 use Drupal\Core\Form\FormStateInterface;
3 use Drupal\Core\Link;
4 use Drupal\Core\Url;
5 use Drupal\Core\Ajax\AjaxResponse;
6 use Drupal\Core\Ajax\RedirectCommand;
Let’s say you have several select elements on the form like my_select and my_select2 like this (Sorry
- not too creative, I know.):
1 $ajax_wrapper = 'my-ajax-wrapper';
2 // Select.
3 $form['my_select'] = [
4 '#type' => 'select',
5 '#empty_value' => '',
6 '#empty_option' => '- Select a value -',
7 '#default_value' => (isset($values['my_select']) ? $values['my_select'] : ''),
8 '#options' => [
9 '/node/1' => 'One',
10 '/node/2' => 'Two',
11 '/node/3' => 'Three'
12 ],
13 '#ajax' => [
14 'callback' => [$this, 'mySelectChange'],
15 'event' => 'change',
16 'wrapper' => $ajax_wrapper,
17 ],
18 ];
19 $form['my_select2'] = [
20 '#type' => 'select',
21 '#empty_value' => '',
22 '#empty_option' => '- Select a value -',
23 '#default_value' => (isset($values['my_select']) ? $values['my_select'] : ''),
24 '#options' => [
25 '/node/4' => 'Four',
26 '/node/5' => 'Five',
27 '/node/6' => 'Six'
28 ],
29 '#ajax' => [
Forms, Form API and AJAX 195
Both use the same callback: mySelectChange. We can make the callback a little smarter by figuring
out internally which element called it.
1 /**
2 * Callback function for changes to the `my_select`any select element.
3 */
4 public function mySelectChange(array $form, FormStateInterface $form_state) {
5 $values = $form_state->getValues();
6
7
8 //$elem stores the element info
9 $elem = $form_state->getTriggeringElement();
10 //$value[$elem["#name"]] stores the path like /node/2
11
12 $response = new AjaxResponse();
13 // Internal URLS must look like this: 'internal:/node/2'.
14 // $url = Url::fromUri('internal:' . $values['my_select']);
15 $url = Url::fromUri('internal:' . $values[$elem["#name"]]);
16
17 $command = new RedirectCommand($url->toString());
18 $response->addCommand($command);
19 return $response;
20
21 }
From: txg/web/modules/custom/iamiwant/src/Form/IamiwantForm.php.
The code loads $nids from the database, loops thru them, putting them into an array $iam indexed
by node id. Then it defines the $form['iam'] element with all the good #ajax stuff including:
29 ];
30 $iwant = [];
31 // Updates iwant values according to iam.
32 if ($node = $form_state->get('node')) {
33 $iwant[] = $node->get('field_iwant')->getString();
34 }
35 $form['iwant_container'] = [
36 '#type' => 'container',
37 '#prefix' => '<div id="iwant-container">',
38 '#suffix' => '</div>',
39 ];
40 $form['iwant_container']['iwant'] = [
41 '#type' => 'select',
42 '#title' => $this->t('and I want to'),
43 '#options' => $iwant,
44 ];
45 $form['actions'] = [
46 '#type' => 'actions',
47 ];
48 $form['actions']['submit'] = [
49 '#type' => 'submit',
50 '#value' => $this->t('Go'),
51 ];
52
53 return $form;
54 }
Below is the callback submitSelectIam (referenced above) that gets called when the iamfield value
changes. The code grabs the iam value from the form, loads up that node and stores it in the $form_-
state with $form_state->set('node', $node); for use later to decide where to jump to.
In submitForm(), it grabs the node, pulls out the URI and does a setRedirectUrl() which causes a
jump to that URL.
In submitSelectIam(), it loads whichever value the user has put in the iam select. Then he stores
that node in the $form_state, then calls $form_state->setRebuild(). This is the code that runs
each time iam is chosen.
Forms, Form API and AJAX 198
1 /**
2 * Handles submit call when iam field is selected.
3 */
4 public function submitSelectIam(array $form, FormStateInterface $form_state) {
5 $iam = $form_state->getValue('iam');
6 $node = $this->entityTypeManager->getStorage('node')->load($iam);
7 $form_state->set('node', $node);
8 $form_state->setRebuild();
9 }
Then in the callback for the ajax iam element. It returns whatever needs to be replaced in the
wrapper container (from above). It simply returns the new value for that container (wrapper).
1 '#ajax' => [
2 'callback' => '::ajaxReplaceIwantForm',
3 'wrapper' => 'iwant-container',
4 'method' => 'replace',
1 /**
2 * Handles switching the iam selector.
3 */
4 public function ajaxReplaceIwantForm($form, FormStateInterface $form_state) {
5 return $form['iwant_container'];
6 }
And finally, this code does the actual redirecting when the user clicks go:
When this runs, it has the I am drop down showing ‘-select-’ and the I want dropdown is empty.
The I want dropdown is unresponsive. Hmm.
When I debug, as soon as I change the I am value, buildForm() runs, then submitSelectIam(),
then buildForm() again and then ajaxReplaceIwantForm(). On subsequent runs, it skips the first
buildForm() and runs through the sequence of
1. submitSelectIam()
2. buildForm()
3. ajaxReplaceIwantForm()
In submitSelectIam() this code $form_state->set('node', $node); stores the node into the form
state for later use. You can ->set() anything into a form for use later. This means that the buildForm
can pull it back out to figure out the value for the I want field, like this (in buildForm()). Note. You
can do this in a submit callback but not a callback defined as '#ajax'. For some reason, they are
ignored if you do that type.
This is the magic bullet as it limits the $iwant[] output to only 1 value and also provides the value
needed by the submit (go) button so it knows where to send the output. Unfortunately this just sets
the $iwant[] array to the only one possible iwant value. That is not as useful as one would want.
44 }
45 $iwantarray = $form_state->get('iwantarray');
46 if ($iwantarray) {
47 $iwant = [];
48 foreach ($iwantarray as $key => $value) {
49 $iwant[$key] = $value;
50 }
51 }
52 $form['iwant_container'] = [
53 '#type' => 'container',
54 '#prefix' => '<div id="iwant-container">',
55 '#suffix' => '</div>',
56 ];
57 $form['iwant_container']['iwant'] = [
58 '#type' => 'select',
59 '#title' => $this->t('and I want to'),
60 '#options' => $iwant,
61 '#empty_value' => '',
62 '#empty_option' => '- Select -',
63 '#prefix' => '<div class="cell imw-want">',
64 '#suffix' => '</div>',
65 '#submit' => ['::submitSelectIwant'],
66 '#executes_submit_callback' => TRUE,
67 '#ajax' => [
68 'callback' => '::ajaxUpdateActionsForm',
69 'wrapper' => 'actions-container',
70 'method' => 'replace',
71 'progress' => [
72 'type' => 'none',
73 ],
74 ],
75 ];
76
77 // Go button.
78 $form['actions'] = [
79 '#type' => 'actions',
80 '#prefix' => '<div class="cell imw-submit" id="actions-container">',
81 '#suffix' => '</div>',
82 ];
83 $form['actions']['submit'] = [
84 '#type' => 'submit',
85 '#value' => $this->t('Go'),
86 ];
Forms, Form API and AJAX 202
87
88 if (!$form_state->get('node')) {
89 $form['actions']['submit']['#attributes']['disabled'] = 'disabled';
90 }
91 return $form;
92 }
Notice that we are checking the $iwantarray variable which is set in the submitSelectIam() as
shown below. I pull the value from the iam select dropdown, look up all the possible values for
iwant and populate them in an indexed array. It isndexed by nid which is important later.
1 /**
2 * Handles submit call when iam field is selected.
3 */
4 public function submitSelectIam(array $form, FormStateInterface $form_state) {
5 $nodeStorage = $this->entityTypeManager->getStorage('node');
6 $iam = $form_state->getValue('iam');
7 $node = $nodeStorage->load($iam);
8 $iam_text = $node->get('field_iam')->value;
9 // Rebuild the iwant values
10 // Get i_am_i_want node ids.
11 $nids = $nodeStorage->getQuery()
12 ->accessCheck(TRUE)
13 ->condition('type', 'i_am_i_want')
14 ->condition('field_iam', $iam_text)
15 ->condition('field_iwant', NULL, 'IS NOT NULL')
16 ->condition('field_link', NULL, 'IS NOT NULL')
17 ->condition('status', NodeInterface::PUBLISHED)
18 ->execute();
19 $nodes = $nodeStorage->loadMultiple($nids);
20 foreach ($nodes as $node) {
21 $iwant[$node->id()] = $node->get('field_iwant')->getString();
22 }
23
24 $form_state->set('iwantarray', $iwant);
25 $form_state->setRebuild();
26 }
Here is the callback for when the iwant is selected. Most importantly, the line which sets the $node
into the form_state ($formstate->set('node', $node)) stores the node, so the submit function can
figure out where the go button should take us.
Forms, Form API and AJAX 203
1 /**
2 * Handles submit call when iwant field is selected.
3 */
4 public function submitSelectIwant(array $form, FormStateInterface $form_state) {
5 $nodeStorage = $this->entityTypeManager->getStorage('node');
6 $iwant = $form_state->getValue('iwant');
7 $node = $nodeStorage->load($iwant);
8 $form_state->set('node', $node);
9 $form_state->setRebuild();
10 }
These two little chaps handle updating the iwant container and the go button (enabling it when there
is a valid node set.). These get fired after the buildForm() is run so they can extract meaningful info
from the nice render array that is built and update just the container they are pointed at. How do I
know which container (or wrapper) that is? See below..
1 /**
2 * Fired from the iam selection to update the iwant select field.
3 */
4 public function ajaxReplaceIwantForm($form, FormStateInterface $form_state) {
5 return $form['iwant_container'];
6 }
7
8 /**
9 * Fired from the iwant selection to update the go button.
10 */
11 public function ajaxUpdateActionsForm($form, FormStateInterface $form_state) {
12 return $form['actions'];
13 }
In the buildForm, we specified '#ajax' and identified a callback function with a wrapper–this says
“replace” the 'iwant-container' by calling the ajaxReplaceIwantForm() function.
1 $form['iam'] = [
2 '#type' => 'select',
3 '#title' => $this->t('I am'),
4 '#options' => array_unique($iam),
5 '#required' => TRUE,
6 '#submit' => ['::submitSelectIam'],
7 '#executes_submit_callback' => TRUE,
8 '#ajax' => [
9 'callback' => '::ajaxReplaceIwantForm',
Forms, Form API and AJAX 204
And if you look at the definition of the form in buildForm, you’ll see that the $form['iwant_-
container'] is clearly defined like this:
1 $form['iwant_container'] = [
2 '#type' => 'container',
3 '#prefix' => '<div id="iwant-container">',
4 '#suffix' => '</div>',
5 ];
1 $form['iwant_container']['iwant'] = [
2 '#type' => 'select',
3 '#title' => $this->t('and I want to'),
4 '#options' => $iwant,
5 '#empty_value' => '',
6 '#empty_option' => '- Select -',
7 '#prefix' => '<div class="cell imw-want">',
8 '#suffix' => '</div>',
9 '#submit' => ['::submitSelectIwant'],
10 '#executes_submit_callback' => TRUE,
11 '#ajax' => [
12 'callback' => '::ajaxUpdateActionsForm',
13 'wrapper' => 'actions-container',
14 'method' => 'replace',
15 'progress' => [
16 'type' => 'none',
17 ],
18 ],
19 ];
1 return $form['iwant_container'];
ajaxUpdateActionsForm() cleverly enables the Go button when there is a node stored in the form,
otherwise it is disabled (from buildForm()). It actually just replaces the whole element with an
enabled version:
1 /**
2 * Fired from the iwant selection to update the go button.
3 */
4 public function ajaxUpdateActionsForm($form, FormStateInterface $form_state) {
5 return $form['actions'];
6 }
This only runs if there is a node, otherwise the Go button is enabled. It seems a little counterintuitive
to leave it enabled and only disable if there isn’t a node but it would probably work fine the other
way around
1 if (!$form_state->get('node')) {
2 $form['actions']['submit']['#attributes']['disabled'] = 'disabled';
3 }
Phew! The explanation is much longer than the actual code. Hopefully it is helpful in explaining
the intricasies of AJAX Drupal magic.
Here rather than just returning the part of the form we want to update, we can add a response
followed by some JavaScript that should do something. While it seems this should work, sadly it
doesn’t unless I wrap the function in (function($){})(jQuery);. More below.
1 use Drupal\Core\Ajax\AjaxResponse;
2 use Drupal\Core\Ajax\InvokeCommand;
3 use Drupal\Core\Ajax\ReplaceCommand;
4
5 ...
6
7 /**
8 * Fired from the iam selection to update the iwant select field.
9 */
10 public function ajaxReplaceIwantForm($form, FormStateInterface $form_state) {
11 // return $form['iwant_container'];
12 $response = new AjaxResponse();
13 $response->addCommand(new ReplaceCommand('#iwant-container', $form['iwant_contai\
14 ner']));
Forms, Form API and AJAX 206
1 (function($) {
2 // Argument passed from InvokeCommand.
3 $.fn.myAjaxCallback = function(argument) {
4 console.log('myAjaxCallback is called.');
5 // Set textfield's value to the passed arguments.
6 $('input#edit-output').attr('value', argument);
7 };
8 })(jQuery);
1 (function($) {
2 // Argument passed from InvokeCommand.
3 $.fn.myinitCustomForms = function(argument) {
4 console.log('myinitCustomForms is called.');
5 jcf.setOptions('Select', {
6 maxVisibleItems: 6,
7 wrapNative: false,
8 wrapNativeOnMobile: false
9
10 });
11 jcf.replaceAll();
12 };
13 })(jQuery);
13.15.6: How do you find all the possible A JAX commands to use
with addCommand()?
Just look in docroot/core/lib/Drupal/Core/Ajax
You will see a bunch of different classes - All the Commands you want to use with addCommand()
for AJAX response e.g. RedirectCommand or OpenModalDialogCommand.
This error seems to have something to do with memcache and anonymous users.
Forms, Form API and AJAX 209
1 use Drupal\Core\Form\ConfigFormBase;
The $config object has get(), set() and save() methods to access config information.
Create a module.settings.yml file in config/install directory for config objects that the module
needs.
Also create a module.schema.yml file in config/schema for schema info i.e. definitions and mappings
for config objects
Implement the following methods: buildForm(), getEditableConfigNames(), getFormId(),
submitForm(), validateForm().
1 /**
2 * @inheritDoc
3 */
4 public function buildForm(array $form, FormStateInterface $form_state) {
5 // $types = node_types_get_names();
6
7 $config = $this->config('rsvp.settings');
8 $node_types = \Drupal\node\Entity\NodeType::loadMultiple();
9 // If you need to display them in a drop down:
10 $options = [];
11 foreach ($node_types as $node_type) {
12 $options[$node_type->id()] = $node_type->label();
13 }
14
15 $form['rsvp_node_types'] = [
16 '#type' => 'checkboxes',
7 https://www.drush.org/latest/generators/form_config/
Forms, Form API and AJAX 211
17 '#title' => $this->t('The content types top enable rSVP collection for'),
18 '#default_value' => $config->get('allowed_types'),
19 '#options' => $options,
20 '#description' => $this->t('On the specified node types, an RSVP option will b\
21 e available and can be enabled while that node is being edited.'),
22 ];
23 $form['array_filter'] = ['#type' => 'value', '#value' => TRUE];
24 return parent::buildForm($form, $form_state);
25 }
1 /**
2 * @inheritDoc
3 */
4 public function submitForm(array &$form, FormStateInterface $form_state) {
5 $allowed_types = array_filter($form_state->getValue('rsvp_node_types'));
6 sort($allowed_types);
7 $this->config('rsvp.settings')
8 ->set('allowed_types', $allowed_types)
9 ->save();
10
11 parent::submitForm($form, $form_state);
12 }
To set a default value for when the module is first installed, create web/modules/custom/rsvp/con-
fig/install/rsvp.settings.yml
1 allowed_types:
2 - article
and to specify more details of what the config stores, create the /Users/selwyn/Sites/dev1/web/modules/custom/rs
13.17.1: Location
Forms are stored in modules/src/Form/MyClassForm.php e.g. /modules/custom/dmod/src/Form/HeaderFooterForm.
13.17.4.1: getFormId()
13.17.4.2: buildForm()
Create the render array representing the form elements. Here I am using a fieldset to group fields.
Also the '#default_value' provides a default value that users can edit.
Forms, Form API and AJAX 213
1 $form['instructions'] = [
2 '#type' => 'markup',
3 '#markup' => $this->t('After clicking submit, test changes on the header and foote\
4 r of the home page.<br/><br/>')
5 ];
6 $form['header'] = [
7 '#type' => 'fieldset',
8 '#title' => $this->t('Header'),
9 '#collapsible' => TRUE,
10 '#open' => TRUE,
11 ];
12 $form['header']['logo_url'] = [
13 '#type' => 'url',
14 '#title' => $this->t('Logo URL'),
15 '#size' => 64,
16 '#default_value' => $logo_url,
17 ];
18 $form['footer'] = [
19 '#type' => 'fieldset',
20 '#title' => $this->t('Footer'),
21 '#collapsible' => TRUE,
22 '#open' => TRUE,
23 ];
24 $form['footer']['footer_address1'] = [
25 '#type' => 'textfield',
26 '#title' => $this->t('Address line 1'),
27 '#default_value' => $address1,
28 ];
29
30 // You need a submit button so users can click on something.
31 $form['submit'] = [
32 '#type' => 'submit',
33 '#value' => $this->t('Save'),
34 ];
Don’t EVER EVER forget to return the $form render array you just created otherwise you get an
empty form. Not that it ever ever happened to me ;-))
1 return $form;
Forms, Form API and AJAX 214
13.17.4.3: submitForm()
When a user clicks on the submit button, this function is called. You can extract the entries from
the $form_state and decide what to do with them. In this example we write them into the database
using the State API. State API values are stored in the key_value table.
1 $values = $form_state->getValues();
2
3 $address1 = $values['footer_address1'];
4 $address2 = $values['footer_address2'];
5 $address3 = $values['footer_address3'];
6 $email = $values['email'];
7 $logo_url = $values['logo_url'];
8 $facebook = $values['facebook'];
9 $linkedin = $values['linkedin'];
10 $instagram = $values['instagram'];
11 $twitter = $values['twitter'];
12 $youtube = $values['youtube'];
Writing:
1 \Drupal::state()->set('logo_url', $logo_url);
2 \Drupal::state()->set('footer_address1', $address1);
3 \Drupal::state()->set('footer_address2', $address2);
4 \Drupal::state()->set('footer_address3', $address3);
5 \Drupal::state()->set('footer_email', $email);
6 \Drupal::state()->set('footer_facebook', $facebook);
7 \Drupal::state()->set('footer_instagram', $instagram);
8 \Drupal::state()->set('footer_linkedin', $linkedin);
9 \Drupal::state()->set('footer_twitter', $twitter);
10 \Drupal::state()->set('footer_youtube', $youtube);
11
12 \Drupal::messenger()->addMessage('Values updated');
To display the results of the form entry you can put this in submitForm().
1 // Display result.
2 foreach ($form_state->getValues() as $key => $value) {
3 \Drupal::messenger()
4 ->addMessage($key . ': ' . ($key === 'text_format' ? $value['value'] : $value));
5 }
Forms, Form API and AJAX 215
8 https://api.drupal.org/api/drupal/elements/10
Forms, Form API and AJAX 216
1 $form['email'] = [
2 '#title' => t('Email Address'),
3 '#type' => 'textfield',
4 '#size' => 25,
5 '#description' => t("We'll send updates to the email address"),
6 '#required' => TRUE,
7 ];
8 $form['submit'] = [
9 '#type' => 'submit',
10 '#value' => t('RSVP'),
11 ];
12 $form['nid'] = [
13 '#type' => 'hidden',
14 '#value' => $nid,
15 ];
9 https://api.drupal.org/api/drupal/elements/10
Forms, Form API and AJAX 217
Another example:
1 'first_name' => [
2 '#type' => 'textfield',
3 '#title' => 'First Name',
4 '#default_value' => '',
5 '#size' => 25,
6 '#maxlength' => 40,
7 '#attributes' => [
8 'class' => ['required name-on-card'],
9 ],
10 '#field_prefix' => '<span class="error-msg">*</span>',
Alternatively, you can get all the fields at one time with getValues(). And reference their value like
this.
Forms, Form API and AJAX 218
1 $values = $form_state->getValues();
2
3 if ($values['op'] == 'Goback') {
4 ...
5 }
If you define the form with fields like $form['header']['blah'] = ... then you can retrieve those
with $form_state->getValue('header','blah');
or
if you define a checkbox like this:
1 $form['actions']['delete_extras'] = [
2 '#type' => 'checkbox',
3 '#title' => t('Also delete extra items'),
4 '#required' => FALSE,
5 '#default_value' => FALSE,
6 '#description' => $this->t('Checking this box will delete extra items also.'),
1 $delete_extras = $form_state->getValue('delete_extras');
2
3 // And then use it.
4 if (!$delete_extras) {
5 $query->condition('field_extras', '', '<>');
6 }
13.18: Resources
• Drupal API Form Element Reference with examples10
• Drupal AJAX AJAX forms updated Dec 202211
• Drupal AJAX Dialog boxes updated Nov 202212
• Great tutorial from Mediacurrent on using modal forms in Drupal from Mar 201713
• Form API Internal Workflow updated Dec 202214
• #! code: Drupal 9: Creating A GET Form, July 202115
• Conditional fields module16
10 https://api.drupal.org/api/drupal/elements/10
11 https://www.drupal.org/docs/8/api/javascript-api/ajax-forms
12 https://www.drupal.org/docs/drupal-apis/ajax-api/ajax-dialog-boxes
13 https://www.mediacurrent.com/blog/loading-and-rendering-modal-forms-drupal-8/
14 https://www.drupal.org/docs/drupal-apis/form-api/form-api-internal-workflow
15 https://www.hashbangcode.com/article/drupal-9-creating-get-form
16 https://www.drupal.org/project/conditional_fields
14: General
14.1: Get the current user
Note this will not get the user entity, but rather a user proxy with basic info but no fields or entity-
specific data.
1 $user = \Drupal::currentUser();
1 $user = \Drupal\user\Entity\User::load(\Drupal::currentUser()->id());
Or
1 use \Drupal\user\Entity\User;
2 $user = User::load(\Drupal::currentUser()->id());
or
1 $account_proxy = \Drupal::currentUser();
2 //$account = $account_proxy->getAccount();
3
4 // load user entity
5 $user = User::load($account_proxy->id());
6
7 $user = User::load(\Drupal::currentUser()->id());
8 $name = $user->get('name')->value;
Email
General 220
1 $email = \Drupal::currentUser()->getEmail();
or
1 $user = User::load(\Drupal::currentUser()->id());
2 $email = $user->get('mail')->value;
The above statement will return either TRUE or FALSE. TRUE means you are on the front page.
Returns the node alias. Note. If a nice url is not set using pathauto, you get /node/1234
General 221
1 use Drupal\Core\Url;
2 $options = ['absolute' => true]; //false will return relative path.
3
4 $url = Url::fromRoute('entity.node.canonical', ['node' => 1234], $options);
5 $url = $url->toString(); // make a string
6
7 // OR
8
9 $node_path = "/node/1";
10 $alias = \Drupal::service('path_alias.manager')->getAliasByPath($node_path);
11
12 // OR
13
14 $current_path = \Drupal::service('path.current')->getPath();
To get the full path with the host etc. this returns: https://ddev93.ddev.site/node/1
1 $host = \Drupal::request()->getSchemeAndHttpHost();
2 $url = \Drupal\Core\Url::fromRoute('entity.node.canonical',['node'=>$lab_home_nid]);
3 $url_alias = $url->toString();
4 $full_url = $host . $url->toString();
You can get the hostname, e.g. ”drupal8.local”, directly from the getHost() request with:
1 $host = \Drupal::request()->getHost();
1 $currentPath = \Drupal::service('path.current')->getPath();
1 $node = \Drupal::request()->attributes->get('node');
2 $nid = $node->id();
OR
1 $node = \Drupal::routeMatch()->getParameter('node');
2 if ($node instanceof \Drupal\node\NodeInterface) {
3 // You can get nid and anything else you need from the node object.
4 $nid = $node->id();
5 $nodeType = $node->bundle();
6 $nodeTitle = $node->getTitle();
7 }
If you need to use the node object in hook_preprocess_page on the preview page, you will need to
use the node_preview parameter, instead of the node parameter:
1 function mymodule_preprocess_page(&$vars) {
2
3 $route_name = \Drupal::routeMatch()->getRouteName();
4
5 if ($route_name == 'entity.node.canonical') {
6 $node = \Drupal::routeMatch()->getParameter('node');
7 }
8 elseif ($route_name == 'entity.node.preview') {
9 $node = \Drupal::routeMatch()->getParameter('node_preview');
10 }
1 use Drupal\Core\Cache\Cache;
2
3 $node = \Drupal::routeMatch()->getParameter('node');
4 if ($node instanceof \Drupal\node\NodeInterface) {
5 $nid = $node->id();
6 }
7
8 // for cache
9 public function getCacheTags() {
10 //With this when your node changes your block will rebuild
11 if ($node = \Drupal::routeMatch()->getParameter('node')) {
12 //if there is node add its cachetag
13 return Cache::mergeTags(parent::getCacheTags(), ['node:' . $node->id()]);
14 }
15 else {
16 //Return default tags instead.
17 return parent::getCacheTags();
18 }
19 }
20
21 public function getCacheContexts() {
22 //if you depend on \Drupal::routeMatch()
23 //you must set context of this block with 'route' context tag.
24 //Every new route this block will rebuild
25 return Cache::mergeContexts(parent::getCacheContexts(), ['route']);
26 }
1 test.example:
2 path: '/test/example'
3 defaults:
4 _title: 'Example'
5 _controller: '\Drupal\test\Controller\TestController::build'
6 requirements:
7 _permission: 'access content'
8
9 test.settings_form:
10 path: '/admin/config/system/test'
11 defaults:
12 _title: 'Test settings'
13 _form: 'Drupal\test\Form\SettingsForm'
14 requirements:
15 _permission: 'administer test configuration'
This will return Drupal route. It returns entity.node.canonical for the nodes, system.404 for the 404
pages, entity.taxonomy_term.canonical for the taxonomy pages, entity.user.canonical for the users
and custom route name that we define in modulename.routing.yml file.
1 $current_route = \Drupal::routeMatch()->getRouteName();
1 $request = \Drupal::request();
2 if ($route = $request->attributes->get(\Symfony\Cmf\Component\Routing\RouteObjectI\
3 nterface::ROUTE_OBJECT)) {
4 $title = \Drupal::service('title_resolver')->getTitle($request, $route);}
1 $user = \Drupal::currentUser();
To get the user entity, use this which gets the user service (\Drupal::currentUser()), gets the uid
(->id()), then calls load() to load the real user object.
1 $user = \Drupal\user\Entity\User::load(\Drupal::currentUser()->id());
Or
1 use \Drupal\user\Entity\User;
2 $user = User::load(\Drupal::currentUser()->id());
1 $is_front = \Drupal::service('path.matcher')->isFrontPage();
1 if (\Drupal::state()->get('system.maintenance_mode')) {
1 $name = $_POST['name'];
1 $name = \Drupal::request()->request->get('name');
1 $query = \Drupal::request()->query->get('name');
1 $query = \Drupal::request()->query->all();
2 $search_term = $query['query'];
3 $collection = $query['collection'];
1 namespace Drupal\newday\Controller;
2 use Drupal\Core\Controller\ControllerBase;
3
4 class NewdayController extends ControllerBase {
5 public function new() {
6 $day= [
7 "#markup" => \Drupal::request()->query->get('id'),
8 ];
9 return $day;
10 }
11 }
The request is being cached, you need to tell the system to vary by the query arg:
1 $day = [
2 '#markup' => \Drupal::request()->query->get('id'),
3 '#cache' => [
4 'contexts' => ['url.query_args:id'],
5 ],
6 ];
1 $current_path = \Drupal::service('path.current')->getPath();
2 $path_args = explode('/', $current_path);
3 $term_name = $path_args[3];
For https://txg.ddev.site/newsroom/search/?country=1206
22 formats, $logger);
23 $this->currentLanguage = $language_manager->getCurrentLanguage();
24 }
25 }
Later in the class, we can retrieve the correct language version of the node:
This is part of the packt publishing Mastering Drupal 8 module development video series:
https://www.packtpub.com/product/mastering-drupal-8-development-video/9781787124493
Note. To test this in modules/custom/pseudo_client/get/
php -S localhost:8888
and put this in a browser:
http://localhost:8888/get_item_from_drupal_core.php?domain=dev1&item=2716
or
http://localhost:8888/get_items_from_custom_code.php?domain=dev1
OR just put this in browser without running php -S:
http://dev1/iai_wea/actions/2716?_format=json
1 function dprime_preprocess_page(&$variables) {
2 $language_interface = \Drupal::languageManager()->getCurrentLanguage();
3
4 $variables['footer_address1'] = [
5 '#type'=>'markup',
6 '#markup'=>'123 Disk Drive, Sector 439',
7 ];
8 $variables['footer_address2'] = [
9 '#type'=>'markup',
10 '#markup'=>'Austin, Texas 78759',
11 ];
1 function burger_burgler_preprocess_node(&$variables) {
2
3 $variables['content']['stock_field'] = [
4 '#type'=>'markup',
5 '#markup'=>'stock field here',
6 ];
7
8 $variables['content']['my_custom_field'] = [
9 '#type' => 'markup',
10 '#markup' => 'Hello - custom field here',
11 ];
12 }
General 230
If you’ve tweaked your node twig template, you’ll need to reference like this:
1 <div class="stock-field-class">
2 {% raw %}{{ content['stock_field'] }}{% endraw %}
3 </div>
1 $variables['abc'] = 'hello';
which can be referenced in the template as {% raw %}{{ abc }}{% endraw %} (or {% raw %}{{
kint(abc) }}{% endraw %} )
1 $node = $variables['node'];
Then to access a field in the node, you can just specify them by:
1 $node->field_ref_aof
2 $node->field_ref_topic
Here we grab a bunch of variables, cycles through them (for multi-value fields, which most of them
are and build an array that can be easily rendered by twig:
From: themes/custom/txg/txg.theme
1 function txg_preprocess_node(&$variables) {
2 $view_mode = $variables['view_mode']; // Retrieve view mode
3 $allowed_view_modes = ['full']; // Array of allowed view modes (for performance so\
4 as to not execute on unneeded nodes)
5 $node = $variables['node'];
6 if (($node->getType() == 'news_story') && ($view_mode == 'full')) {
7 $aofs = _txg_multival_ref_data($node->field_ref_aof, 'aof', 'target_id');
8 $units = _txg_multival_ref_data($node->field_ref_unit, 'unit', 'target_id');
9 $audiences = _txg_multival_ref_data($node->field_ref_audience, 'audience', 'targ\
10 et_id');
11 $collections = _txg_multival_ref_data($node->field_ref_program_collection, 'coll\
General 231
12 ection', 'target_id');
13 $topics = _txg_multival_ref_data($node->field_ref_topic, 'topic', 'target_id', \
14 'taxonomy');
15 $continents = _txg_multival_ref_data($node->field_continent, 'continent', 'value\
16 ', 'list');
17 $countries = _txg_multival_ref_data($node->field_ref_country, 'country', 'target\
18 _id');
19 $related_news_items = array_merge($topics, $aofs, $units, $audiences, $collectio\
20 ns, $continents, $countries);
21 $variables['related_news_items'] = $related_news_items;
22 }
23 }
24
25 /**
26 * Returns array of data for multivalue node reference fields
27 * ref_field = entity reference field
28 * param_name = parameter name to be passed as get value
29 * value_type = indicates which field to retrieve from database
30 * field_ref_type = variable to determine type of reference field
31 * field_term_category =
32 */
33 function _txg_multival_ref_data($ref_field, $param_name, $value_type, $field_ref_typ\
34 e = 'node') {
35 $values = [];
36 foreach($ref_field as $ref) {
37 if ($field_ref_type == 'taxonomy') {
38 $term = Drupal::entityTypeManager()->getStorage('taxonomy_term')->load($ref->\
39 $value_type);
40 $title = $term->getName();
41 }
42 else {
43 $title = $value_type == 'value' ? $ref->$value_type : $ref->entity->title->va\
44 lue;
45 }
46 $id = $ref->$value_type;
47 $values[] = [
48 'title' => $title,
49 'id' => str_replace(' ', '+', $id),
50 'param_name' => $param_name,
51 ];
52 }
53 return $values;
54 }
General 232
1 $node->field_sf_contract_ref->entity->field_how_to_order->value;
Where field_sf_contract_ref is the reference field, which points to an entity which has a field
called field_how_to_order. Then we can jam it into the $variables array and refer to it in the twig
template as {{ how_to_order }}
From web/themes/custom/dirt_bootstrap/dirt_bootstrap.theme
In function dirt_bootstrap_preprocess_node(&$variables)
1 function burger_burgler_preprocess_node(&$variables) {
2
3 $burger_list = [
4 ['name' => 'Cheesburger'],
5 ['name' => 'Mushroom Swissburger'],
6 ['name' => 'Jalapeno bugburger'],
7 ];
8 $variables['burgers'] = $burger_list;
9
10 }
1 <ol>
2 {% raw %}{% for burger in burgers %}
3 <li> {{ burger['name'] }} </li>
4 {% endfor %}{% endraw %}
5 </ol>
1 /**
2 * Implements hook_preprocess_field.
3 *
4 * Provides an index for these fields referenced as {{ paragraph.index }}
5 * in twig template.
6 *
7 * @param $variables
8 */
9 function dprime_preprocess_field(&$variables) {
10 if($variables['field_name'] == 'field_video_accordions'){
11 foreach($variables['items'] as $idx => $item) {
12 $variables['items'][$idx]['content']['#paragraph']->index = $idx;
13 }
14 }
15 }
General 234
field_video_accordions is the name of the field that holds the paragraph you want to count.
In the twig template for that paragraph, you can use the value paragraph.index as in:
1 /**
2 * Implements hook_preprocess_html().
3 */
4 function dir_bootstrap_preprocess_html(&$variables) {
5
6 $node = \Drupal::routeMatch()->getParameter('node');
7 if ($node instanceof \Drupal\node\NodeInterface) {
8 if ($node->getType() == 'contract') {
9
10 $brand_meta_tag[] = [[
11 '#tag' => 'meta',
12 '#attributes' => [
13 'name' => 'brand',
14 'content' => 'Dell',
15 ]],
16 'Dell',
17 ];
18 ..
19
20 $variables['page']['#attached']['html_head'][] = $brand_meta_tag;
Note that the extra “Dell” low down in the array appears to be a description of some kind–it isn’t
rendered. If you don’t include the second “Dell” you could rather use
General 235
1 $brand_meta_tags[] = [[
2 '#tag' => 'meta',
3 '#attributes' => [
4 'name' => 'brand',
5 'content' => 'Dell',
6 ]],
7 'Dell',
8 ];
9 $brand_meta_tags[] = [[
10 '#tag' => 'meta',
11 '#attributes' => [
12 'name' => 'brand',
13 'content' => 'Apple',
14 ]],
15 'Apple',
16 ];
17
18 foreach ($brand_meta_tags as $brand_meta_tag) {
19 $variables['page']['#attached']['html_head'][] = $brand_meta_tag;
20 }
And here I do a query and build some new meta tags from themes/custom/dirt_bootstrap/dirt_-
bootstrap.theme.
1 $brand_meta_tags = [];
2 $contract_id = $node->field_contract_id->value;
3 if ($contract_id) {
4
5 //Lookup dirt store brand records with this contract id.
6 $storage = \Drupal::entityTypeManager()->getStorage('node');
7 $query = \Drupal::entityQuery('node')
8 ->condition('type', 'sf_store_brands')
9 ->condition('status', 1)
10 ->condition('field_contract_id', $contract_id);
11 $nids = $query->execute();
12 foreach ($nids as $nid) {
13 $store_brand_node = Node::load($nid);
14 $brand = $store_brand_node->field_brand->value;
General 236
15 if ($brand) {
16 $brand_meta_tags[] = [[
17 '#tag' => 'meta',
18 '#attributes' => [
19 'name' => 'brand',
20 'content' => $brand,
21 ]],
22 $brand,
23 ];
24 }
25 }
26 foreach ($brand_meta_tags as $brand_meta_tag) {
27 $variables['page']['#attached']['html_head'][] = $brand_meta_tag;
28 }
29 }
https://www.drupal.org/project/remote_stream_wrapper_widget
https://www.drupal.org/project/remote_stream_wrapper
Hugo suggests using this to do migration:
1 $uri = 'http://example.com/somefile.mp3';
2 $file = File::Create(['uri' => $uri]);
3 $file->save();
4 $node->field_file->setValue(['target_id' => $file->id()]);
5 $node->save();
After
1 https://www.drupal.org/project/devel
2 https://www.drupal.org/node/2774931
15: Hooks
15.1: Overview
Drupal hooks allow modules to alter and extend the behavior of Drupal core, or another module.
They provide a way that code components in Drupal can communicate with one another. Using
hooks, a module developer can change how core, or other modules work without changing the
existing code. As a Drupal developer, understanding how to implement and invoke hooks is
essential. (More at https://drupalize.me/tutorial/what-are-hooks?p=2766)
According to ChapGPT: Hooks are a key aspect of Drupal’s module system, and allow developers
to interact with the core functionality of Drupal 9 or Drupal 10. They can be used to alter or extend
the behavior of Drupal’s core features, such as adding custom validation to a form, changing the
way content is displayed, or adding new actions to the administrative interface. Hooks provide a
powerful and flexible way to customize Drupal to meet the needs of a specific project or site, without
having to modify the core code. They are essential for developers who want to build custom modules
or themes, and are a fundamental part of the Drupal development process.
1 /**
2 * Implements hook_form_FORM_ID_alter().
3 */
4 function hook_examples_form_user_login_form_alter(&$form, \Drupal\Core\Form\FormStat\
5 eInterface $form_state) {
6 // Add a custom submit button to the login form.
7 $form['third_party_login'] = [
8 '#type' => 'submit',
9 '#value' => t('Login with Third-Party Provider'),
10 '#submit' => ['hook_examples_user_login_form_submit'], // Call the custom submit\
11 function.
12 ];
13 }
14
15 /**
Hooks 239
1 use Drupal\Core\Form\FormStateInterface;
2 use Drupal\node\Entity\Node;
3
4 /**
5 * Implements hook_form_alter().
6 */
7 function hook_examples_form_alter(array &$form, FormStateInterface $form_state, $for\
8 m_id) {
9 if ($form_id === 'node_event_edit_form') {
10 $node = $form_state->getFormObject()->getEntity();
11 if ($node instanceof Node && $node->bundle() === 'event') {
Hooks 240
This code uses the hook_form_alter hook to alter the node edit form and modify the value of the
submit button for nodes of type event. The $form_id argument is used to check if the form being
altered is the node edit form for nodes of type event, and if it is, the submit button’s value is changed
to ”Update Event”. A redundant check is added to ensure the node is of type ”event” for clarity.
1 /**
2 * Implements hook_ENTITY_TYPE_presave().
3 */
4 function ogg_mods_node_presave(NodeInterface $node) {
5 $type = $node->getType();
6 if ($type == 'catalog_notice') {
7 $end_date = NULL != $node->get('field_cn_start_end_dates')->end_value ? $node->g\
8 et('field_cn_start_end_dates')->end_value : 'n/a';
9 $govt_body = NULL != $node->field_cn_governmental_body->value ? $node->field_cn_\
10 governmental_body->value : 'Unnamed Government Body';
11 $start_date_val = $node->get('field_cn_start_date')->value;
12
13 $accountProxy = \Drupal::currentUser();
14 $account = $accountProxy->getAccount();
15 // Anonymous users automatically fill out the end_date.
16 if (!$account->hasPermission('administer catalog notice')) {
17 $days = (int) $node->get('field_cn_suspension_length')->value - 1;
18
19 $end_date = DrupalDateTime::createFromFormat('Y-m-d', $start_date_val);
20 $end_date->modify("+$days days");
21 $end_date = $end_date->format("Y-m-d");
22 $node->set('field_cn_end_date', $end_date);
23 }
24
Hooks 241
68 $tags[] = 'ogg:views:home_recent_news';
69 break;
70 default:
71 break;
72 }
73 if (!empty($tags)) {
74 Cache::invalidateTags($tags);
75 }
76 }
15.5: hook_update
Almost every project that runs for a while will require some hook_updates. This is the facility used
to do automated changes to your sites. Here is an example that updates a menu item with a title
’Support’.
This code comes from a .install file:
1 function partridge_update_8002() {
2
3 $mids = \Drupal::entityQuery('menu_link_content')
4 ->condition('menu_name', 'part-wide-utility')
5 ->execute();
6
7 foreach($mids as $mid) {
8 $menu_link = \Drupal::entityTypeManager()->getStorage('menu_link_content')->load\
9 ($mid);
10
11 $title = $menu_link->getTitle();
12 if ($title === 'Support') {
13 $menu_link->set('weight',2);
14 $menu_link->set('expanded', TRUE);
15 $menu_link->set('link', 'https://www.google.com');
16 $menu_link->save();
17
18 }
19 }
20 }
Hooks 243
• template_preprocess1 (&$variables, $hook): Creates a default set of variables for all theme
hooks with template implementations. Provided by Drupal Core.
• template_preprocess_HOOK(&$variables): Should be implemented by the module that
registers the theme hook, to set up default variables.
• MODULE_preprocess(&$variables, $hook): hook_preprocess() is invoked on all implement-
ing modules.
• MODULE_preprocess_HOOK(&$variables): hook_preprocess_HOOK() is invoked on all im-
plementing modules, so that modules that didn’t define the theme hook can alter the variables.
• ENGINE_engine_preprocess(&$variables, $hook): Allows the theme engine to set necessary
variables for all theme hooks with template implementations.
• ENGINE_engine_preprocess_HOOK(&$variables): Allows the theme engine to set neces-
sary variables for the particular theme hook.
• THEME_preprocess(&$variables, $hook): Allows the theme to set necessary variables for all
theme hooks with template implementations.
• THEME_preprocess_HOOK(&$variables): Allows the theme to set necessary variables
specific to the particular theme hook.
15.6.1: Hook_preprocess
Generall, .theme files will include the following to create or alter variables for :
1 function mytheme_preprocess_node(&$variables) {
2 $variables['custom_variable'] = "Bananas are yellow";
3 $variables['content']['field_image']['#suffix'] = "this suffix on the image";
4 kint($variables);
5 }
The render array you want to change will be in the content variable which shows up in the kint
output as $variables['content']
Usually fields such as field_image will be automatically rendered by the node template (unless
you’ve tweaked it to display in some other template.)
In your node’s Twig template you would specify {{ custom_variable }} to have it display on every
node.
1 $from = $variables["node"]->get('field_date')->getValue()[0]['value'];
2 $to = $variables["node"]->get('field_date')->getValue()[0]['end_value'];
1 use Drupal\Core\Datetime\DrupalDateTime;
2
3
4 /**
5 * Implements hook_preprocess_node
6 *
7 * @param $variables
8 */
9 function veryst_preprocess_node(&$variables) {
Hooks 245
10 if (!empty($variables['content']['field_date'])) {
11 $date = $variables['content']['field_date'];
12
13 $from = new DrupalDateTime($variables["node"]->get('field_date')->getValue()[0][\
14 'value']);
15 $date_array = explode("-", $from);
16 $from_day = substr($date_array[2], 0, 2);
17 $from_month = $date_array[1];
18
19 $to = new DrupalDateTime($variables["node"]->get('field_date')->getValue()[0]['e\
20 nd_value']);
21 $date_array = explode("-", $to);
22 $to_day = substr($date_array[2], 0, 2);
23 $to_month = $date_array[1];
24
25 if ($from_month === $to_month && $from_day != $to_day) {
26 $variables['scrunch_date'] = [
27 '#type' => 'markup',
28 '#markup' => $from->format("M j-") . $to->format("j, Y"),
29 ];
30 }
31
32 }
33 // kint($variables);
34 }
Now in the twig template we can output the scrunch_date we created in the template file:
web/themes/mytheme/templates/node/node--seminar--teaser.html.twig
1 {% if content.field_date %}
2 {% if scrunch_date %}
3 <div>
4 {{ scrunch_date }}
5 </div>
6 {% else %}
7 <div>
8 {{ content.field_date }}
9 </div>
10 {% endif %}
11 {% endif %}
Hooks 246
1 use Drupal\Core\Form\FormStateInterface;
2
3 /**
4 * Implements hook_form_BASE_FORM_ID_alter().
5 */
6 function MY_MODULE_form_node_article_edit_form_alter(&$form, FormStateInterface $for\
7 m_state, $form_id) {
8 // Your code here the two following lines just an examples.
9 // Hide some fields.
10 $form['field_SOME_FIELD_NAME']['#access'] = FALSE;
11 // Attach some library ....
12 $form['#attached']['library'][] = 'MY_MODULE/SOME_LIBRARY';
13 }
You can create a class called NodeArticleEditFormHandler inside your src folder like the following:
1 <?php
2
3 namespace Drupal\MY_MODULE;
4
5
6 use Drupal\Core\Form\FormStateInterface;
7
8 /**
9 * Class NodeArticleEditFormHandler
10 *
11 * @package Drupal\MY_MODULE
12 */
13 class NodeArticleEditFormHandler {
14
15 /**
16 * Alter Form.
17 *
2 https://www.berramou.com/blog/drupal-8-how-organise-your-hooks-code-classes-object-oriented-way
3 https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Form!form.api.php/function/hook_form_FORM_ID_alter/8.2.x
Hooks 247
In case you need other services you can inject your dependencies by make your class implements
ContainerInjectionInterface4 here is an example with current user service injection:
1 <?php
2
3 namespace Drupal\MY_MODULE;
4
5
6 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
7 use Drupal\Core\Form\FormStateInterface;
8 use Drupal\Core\Session\AccountProxyInterface;
9 use Symfony\Component\DependencyInjection\ContainerInterface;
10
11 /**
12 * Class NodeArticleEditFormHandler
13 *
14 * @package Drupal\MY_MODULE
15 */
16 class NodeArticleEditFormHandler implements ContainerInjectionInterface {
17
18 /**
19 * The current user account.
20 *
21 * @var \Drupal\Core\Session\AccountProxyInterface
4 https://api.drupal.org/api/drupal/core!lib!Drupal!Core!DependencyInjection!ContainerInjectionInterface.php/interface/
ContainerInjectionInterface/8.2.x
Hooks 248
22 */
23 protected $currentUser;
24
25 /**
26 * NodeArticleEditFormHandler constructor.
27 *
28 * @param \Drupal\Core\Session\AccountProxyInterface $current_user
29 * The current user.
30 */
31 public function __construct(AccountProxyInterface $current_user) {
32 $this->currentUser = $current_user;
33 }
34
35 /**
36 * @inheritDoc
37 */
38 public static function create(ContainerInterface $container) {
39 return new static(
40 $container->get('current_user')
41 );
42 }
43
44 /**
45 * Alter Form.
46 *
47 * @param array $form
48 * Form array.
49 * @param \Drupal\Core\Form\FormStateInterface $form_state
50 * The current state of the form.
51 * @param $form_id
52 * String representing the id of the form.
53 */
54 public function alterForm(array &$form, FormStateInterface $form_state, $form_id) {
55 // Example to get current user.
56 $currentUser = $this->currentUser;
57 // Your code here the two following lines just an examples.
58 // Hide some fields.
59 $form['field_SOME_FIELD_NAME']['#access'] = FALSE;
60 // Attach some library ....
61 $form['#attached']['library'][] = 'MY_MODULE/SOME_LIBRARY';
62 }
63
64 }
Hooks 249
1 use Drupal\MY_MODULE\NodeArticleEditFormHandler;
2
3 /**
4 * Implements hook_form_BASE_FORM_ID_alter().
5 */
6 function MY_MODULE_form_node_article_edit_form_alter(&$form, FormStateInterface $for\
7 m_state, $form_id) {
8 return \Drupal::service('class_resolver')
9 ->getInstanceFromDefinition(NodeArticleEditFormHandler::class)
10 ->alterForm($form, $form_state, $form_id);
11 }
We are done! Now your .module file is more clean, readable and maintainable with less code. You
can do that with every hook for instance EntityHandler like:
1 use Drupal\MY_MODULE\EntityHandler;
2
3 /**
4 * Implements hook_entity_presave().
5 */
6 function MY_MODULE_entity_presave(EntityInterface $entity) {
7 return \Drupal::service('class_resolver')
8 ->getInstanceFromDefinition(EntityHandler::class)
9 ->entityPresave($entity);
10 }
And so on! You can see another example in Drupal core from the content moderation form5
15.8: Reference
To create an entity:
$entity = $storage->create6 ();
// Add code here to set properties on the entity. // Until you call save(), the entity is just in memory.
$entity->save7 ();
There is also a shortcut method on entity classes, which creates an entity with an array of provided
property values: \Drupal\Core\Entity::create().
Hooks invoked during the create operation:
• hook_ENTITY_TYPE_create8 ()
• hook_entity_create9 ()
• When handling content entities, if a new translation is added to the entity object:
– hook_ENTITY_TYPE_translation_create10 ()
– hook_entity_translation_create11 ()
See Save operations12 below for the save portion of the operation.
When an entity is loaded, normally the default entity revision is loaded. It is also possible to load a
different revision, for entities that support revisions, with this code:
$entity = $storage->loadRevision($revision_id);
This involves the same hooks and operations as regular entity loading.
The ”latest revision” of an entity is the most recently created one, regardless of it being default or
pending. If the entity is translatable, revision translations are not taken into account either. In other
words, any time a new revision is created, that becomes the latest revision for the entity overall,
regardless of the affected translations. To load the latest revision of an entity:
$revision_id = $storage->getLatestRevisionId($entity_id);
$entity = $storage->loadRevision($revision_id);
As usual, if the entity is translatable, this code instantiates into $entity the default translation of the
revision, even if the latest revision contains only changes to a different translation:
$is_default = $entity->isDefaultTranslation(); // returns TRUE
The ”latest translation-affected revision” is the most recently created one that affects the specified
translation. For example, when a new revision introducing some changes to an English translation
is saved, that becomes the new ”latest revision”. However, if an existing Italian translation was not
affected by those changes, then the ”latest translation-affected revision” for Italian remains what it
was. To load the Italian translation at its latest translation-affected revision:
$revision_id = $storage->getLatestTranslationAffectedRevisionId($entity_id, ’it’); $it_translation
= $storage ->loadRevision($revision_id) ->getTranslation(’it’);
To update an existing entity, you will need to load it, change properties, and then save; as described
above, when creating a new entity, you will also need to save it. Here is the order of hooks and
other events that happen during an entity save:
– hook_ENTITY_TYPE_translation_insert18 ()
– hook_entity_translation_insert19 ()
16 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_ENTITY_TYPE_presave/10
17 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_entity_presave/10
18 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_ENTITY_TYPE_translation_
insert/10
19 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_entity_translation_insert/10
Hooks 252
– hook_ENTITY_TYPE_translation_delete20 ()
– hook_entity_translation_delete21 ()
Some specific entity types invoke hooks during preSave() or postSave() operations. Examples:
Note that all translations available for the entity are stored during a save operation. When saving a
new revision, a copy of every translation is stored, regardless of it being affected by the revision.
When an entity’s add/edit form is used to add or edit an entity, there are several hooks that are
invoked:
• hook_entity_prepare_form24 ()
• hook_ENTITY_TYPE_prepare_form25 ()
• hook_entity_form_display_alter26 () (for content entities only)
To delete one or more entities, load them and then delete them:
$entities = $storage->loadMultiple($ids);
$storage->delete27 ($entities);
During the delete operation, the following hooks and other events happen:
20 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_ENTITY_TYPE_translation_
delete/10
21 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_entity_translation_delete/10
22 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_ENTITY_TYPE_insert/10
23 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_entity_insert/10
24 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_entity_prepare_form/10
25 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_ENTITY_TYPE_prepare_
form/10
26 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_entity_form_display_alter/
10
27 https://api.drupal.org/api/drupal/10/search/delete
Hooks 253
Some specific entity types invoke hooks during the delete process. Examples:
• hook_entity_view_mode_alter36 ()
28 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_ENTITY_TYPE_predelete/10
29 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_entity_predelete/10
30 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_ENTITY_TYPE_delete/10
31 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_entity_delete/10
32 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/group/entity_crud/10#load
33 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_ENTITY_TYPE_revision_
delete/10
34 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_entity_revision_delete/10
35 https://api.drupal.org/api/drupal/10/search/view
36 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_entity_view_mode_alter/10
Hooks 254
• hook_ENTITY_TYPE_build_defaults_alter37 ()
• hook_entity_build_defaults_alter38 ()
During the rendering operation, the default entity viewer runs the following hooks and operations
in the pre-render step:
• hook_entity_view_display_alter39 ()
• hook_entity_prepare_view40 ()
• Entity fields are loaded, and render arrays are built for them using their formatters.
• hook_entity_display_build_alter41 ()
• hook_ENTITY_TYPE_view42 ()
• hook_entity_view43 ()
• hook_ENTITY_TYPE_view_alter44 ()
• hook_entity_view_alter45 ()
After this point in rendering, the theme system takes over. See the Theme system and render API
topic46 for more information.
37 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_ENTITY_TYPE_build_
defaults_alter/10
38 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_entity_build_defaults_alter/
10
39 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_entity_view_display_alter/
10
40 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_entity_prepare_view/10
41 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_entity_display_build_alter/
10
42 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_ENTITY_TYPE_view/10
43 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_entity_view/10
44 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_ENTITY_TYPE_view_alter/
10
45 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_entity_view_alter/10
46 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21theme.api.php/group/theme_render/10
Hooks 255
• Searching nodes:
– hook_ranking47 ()
– Query is executed to find matching nodes
– Resulting node is loaded
– Node render array is built
– comment_node_update_index48 () is called (this adds ”N comments” text)
– hook_node_search_result49 ()
– Node is loaded
– Node render array is built
– hook_node_update_index50 ()
47 https://api.drupal.org/api/drupal/core%21modules%21node%21node.api.php/function/hook_ranking/10
48 https://api.drupal.org/api/drupal/core%21modules%21comment%21comment.module/function/comment_node_update_index/10
49 https://api.drupal.org/api/drupal/core%21modules%21node%21node.api.php/function/hook_node_search_result/10
50 https://api.drupal.org/api/drupal/core%21modules%21node%21node.api.php/function/hook_node_update_index/10
51 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21Renderer.php/function/Renderer%3A%3AdoRender/10
52 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Theme%21ThemeManagerInterface.php/function/
ThemeManagerInterface%3A%3Arender/10
Hooks 256
1 return array(
2 'search_result' => array(
3 'variables' => array(
4 'result' => NULL,
5 'plugin_id' => NULL,
6 ),
7 'file' => 'search.pages.inc',
8 ),
9 );
Given this definition, the template file with the default implementation is search-result.html.twig53 ,
which can be found in the core/modules/search/templates directory, and the variables for rendering
are the search result and the plugin ID. In addition, there is a function template_preprocess_-
search_result(), located in file search.pages.inc54 , which preprocesses the information from the input
variables so that it can be rendered by the Twig template; the processed variables that the Twig
template receives are documented in the header of the default Twig template file.
Themes may register new theme hooks within a hook_theme55 () implementation, but it is more
common for themes to override default implementations provided by modules than to register
entirely new theme hooks. Themes can override a default implementation by creating a template
file with the same name as the default implementation; for example, to override the display of search
results, a theme would add a file called search-result.html.twig56 to its templates directory. A good
starting point for doing this is normally to copy the default implementation template, and then
modifying it as desired.
Several functions are called before the template file is invoked to modify the variables that are
passed to the template. These make up the ”preprocessing” phase, and are executed (if they exist),
in the following order (note that in the following list, HOOK indicates the hook being called or a
less specific hook. For example, if ’#theme’=>’node__article’ is called, hook is node__article and
node. MODULE indicates a module name, THEME indicates a theme name, and ENGINE indicates
a theme engine name). Modules, themes, and theme engines can provide these functions to modify
how the data is preprocessed, before it is passed to the theme template:
• template_preprocess57 (&$variables, $hook): Creates a default set of variables for all theme
hooks with template implementations. Provided by Drupal Core.
53 https://api.drupal.org/api/drupal/10/search/search-result.html.twig
54 https://api.drupal.org/api/drupal/core%21modules%21search%21search.pages.inc/10
55 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21theme.api.php/function/hook_theme/10
56 https://api.drupal.org/api/drupal/10/search/search-result.html.twig
57 https://api.drupal.org/api/drupal/core%21includes%21theme.inc/function/template_preprocess/10
Hooks 257
In some cases, instead of calling the base theme hook implementation (either the default provided
by the module that defined the hook, or the override provided by the theme), the theme system
will instead look for ”suggestions” of other hook names to look for. Suggestions can be specified in
several ways:
• In a render array, the ’#theme’ property (which gives the name of the hook to use) can be an
array of theme hook names instead of a single hook name. In this case, the render system will
look first for the highest-priority hook name, and if no implementation is found, look for the
second, and so on. Note that the highest-priority suggestion is at the end of the array.
• In a render array, the ’#theme’ property can be set to the name of a hook with a ’__SUGGES-
TION’ suffix. For example, in search results theming, the hook ’item_list__search_results’ is
given. In this case, the render system will look for theme templates called item-list--search-
results.html.twig58 , which would only be used for rendering item lists containing search results,
and if this template is not found, it will fall back to using the base item-list.html.twig59 template.
This type of suggestion can also be combined with providing an array of theme hook names
as described above.
• A module can implement hook_theme_suggestions_HOOK(). This allows the module that
defines the theme template to dynamically return an array containing specific theme hook
names (presumably with ’__’ suffixes as defined above) to use as suggestions. For example,
the Search module does this in search_theme_suggestions_search_result() to suggest search_-
result__PLUGIN as the theme hook for search result items, where PLUGIN is the machine
name of the particular search plugin type that was used for the search (such as node_search or
user_search).
Modules can also alter the theme suggestions provided using the mechanisms of the previous section.
There are two hooks for this: the theme-hook-specific hook_theme_suggestions_HOOK_alter() and
the generic hook_theme_suggestions_alter(). These hooks get the current list of suggestions as input,
and can change this array (adding suggestions and removing them).
60 https://drupalize.me/tutorial/what-are-hooks?p=2766
61 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21theme.api.php/group/themeable/10
62 https://www.berramou.com/blog/drupal-8-how-organise-your-hooks-code-classes-object-oriented-way
16: Learning and keeping up with
Drupal
16.1: Free videos
• Saranya Ashokkumar is a prolific youtube content creator. Check out some nice short
form videos on how to use various modules, create modules etc. https://www.youtube.com/
@d4drupal324/featured
1 https://drupalize.me/
2 https://www.lullabot.com/
3 https://symfonycasts.com/
Learning and keeping up with Drupal 260
16.7: Books
There are many books about Drupal that are worth checking out.
• Daniel Sipos’s Drupal 10 Module Development: Develop and deliver engaging and intuitive
enterprise-level apps, 4th Edition. https://www.packtpub.com/product/drupal-10-module-
development-fourth-edition/9781837631803
• Adam Bergstein’s Drupal 10 Masterclass: Build responsive Drupal applications to deliver
custom and extensible digital experiences to users. https://www.amazon.com/Drupal-
Masterclass-responsive-applications-experiences-ebook/dp/B0BNNS7JCM/ref=d_pd_sbs_
vft_none_sccl_1_1/146-2345249-6494660
• Matt Glaman and Kevin Quillen’s Drupal 10 Development Cookbook. Published in
Feb 2023. https://www.packtpub.com/product/drupal-10-development-cookbook-third-
edition/9781803234960
• Daniel Sipos’s Drupal 9 Module Development Third Edition Published in Aug 2020. https:
//www.packtpub.com/product/drupal-9-module-development-third-edition/9781800204621
• Fran Gil’s Expert in Drupal 9 Front-End Development. Updated in Sep 2022 https://www.
forcontu.com/en/books/expert-in-drupal-9-front-end-development
• Fran Gil’s Expert in Drupal 9 Back-End Development. Updated in Sep 2022 https://www.
forcontu.com/en/books/expert-in-drupal-9-back-end-development
• Check out more older Drupal books from Packt publishing at https://subscription.packtpub.
com/search?query=Drupal
• Drupal Book.org is an online only book by Ivan. Updated Sep 2018. While it is not complete,
it is a valuable resource. https://drupalbook.org/
17: Links, Aliases and URLs
17.1: Create an external url
1 use Drupal\Core\Url
2 $url = Url::fromUri('http://testsite.com/go/here');
1 use Drupal\Core\Url
2
3 $url = Url::fromUri('internal:/reports/search');
4
5 // Or.
6
7 $url = Url::fromUri('internal:/dashboard/achievements')
8
9 // Or Using link generator to create a GeneratedLink.
10 $url = = Url::fromUri('internal:/node/1');
11 $link = \Drupal::service('link_generator')->generate('My link', $url);
12
13 // OR
14 $url = Url::fromUri('internal:/');
15 // OR
16 'url' => Url::fromRoute('<front>'),
17 // OR
18 'url' => Url::fromRoute('hello_world.hello'),
1 $option = [
2 'query' => ['user' => 'admin'],
3 ];
4 $url = Url::fromUri('internal:/reports/search', $option);
1 $option = [
2 'query' => ['user' => 'admin'],
3 ];
4 $url = Url::fromUri('internal:/reports/search', $option);
5
6 // use the Link class.
7 $link = Link::fromTextAndUrl('My link', $url);
8 $renderable_array = $link->toRenderable();
9 return $renderable_array;
1 $correction_node = Node::load($nid);
2 $current_url = $correction_node->get('field_link')->uri;
1 use Drupal\paragraphs\Entity\Paragraph;
2
3 $para = Paragraph::load($target_id);
4 $link = $para->field_link;
5 $link_uri = $para->field_link->uri;
Or a more convoluted example that extracts the url string for display from a link field.
1 if ($sf_contract) {
2 // first() returns a Drupal\link\Plugin\FieldType\LinkItem
3 $vendor_url = $sf_contract->field_vendor_url->first();
4 if ($vendor_url) {
5 // returns a Drupal\Core\Url.
6 $vendor_url = $vendor_url->getUrl();
7 $vendor_url_string = $vendor_url->toString();
8 }
1 $vendor_url = $sf_contract->field_vendor_url;
returns a Drupal\Core\Field\FieldItemList which is a list of fields so you then would have to pull
out the first field and extract the URI out of that. I’m not sure why Drupal considers it multiple
values instead of just one. This was not set up as a multivalue field.
1 $vendor_url = $node->field_sf_contract_ref->entity->field_vendor_url->first()->getUr\
2 l();
3 if ($vendor_url) {
4 $vendor_url = $vendor_url->getUri();
5 //OR
6 $vendor_url = $vendor_url->toString();
7 }
1 $citation_link = $citation->get('field_link');
2 if (!$citation_link->isEmpty()) {
3 $citation_link = $citation->field_link->first()->getUrl()->toString();
4 }
1 $instructions_node = Node::load($order_type_instructions_nid);
2 if ($instructions_node) {
3 $order_link = $instructions_node->field_link->first();
4 if ($order_link) {
5 $uri = $order_link->uri;
6 $variables['order_link_title'] = $order_link->title;
7 $order_url = $order_link->getUrl();
8 if ($order_url) {
9 $variables['order_type_link'] = $order_url;
10 }
11 }
12 }
OR
If you have a URL for a node and you want its nid
1 $route = $url->getRouteParameters();
2 // first check if it's a node.
3 if (isset($route['node'])) {
4 $nid = $route["node"];
5 }
1 $term_path_with_tid = \Drupal::service('path_alias.manager')->getPathByAlias('/hunge\
2 r-strike');
1 //User
2 $user_path_with_uid = \Drupal::service('path_alias.manager')->getPathByAlias('/selwy\
3 n-the-chap');
1 $node_path = '/node/32';
2 $node32_alias = \Drupal::service('path_alias.manager')->getAliasByPath($node_path);
Use this code if you need the absolute URL . If node/32 has a URL alias set to ”/test-node” it returns
”https://d9book2.ddev.site/test-node” . If you specify absolute => FALSE, it returns ”/test-node” .
1 use Drupal\Core\Url;
2
3 // Note. If a pathauto url alias is not set, it returns '/node/32'
4 $nid = 32;
5 $options = ['absolute' => TRUE];
6 $url = Url::fromRoute('entity.node.canonical', ['node' => $nid], $options);
7 // make a string
8 $url_string = $url->toString();
1 $node_path = "/node/32";
2 $new_alias = "/test-node";
3
4 /** @var \Drupal\path_alias\PathAliasInterface $path_alias */
5 $my_node_alias = \Drupal::entityTypeManager()->getStorage('path_alias')->create([
6 'path' => $node_path,
7 'alias' => $new_alias,
8 'langcode' => 'en',
9 ]);
10 $my_node_alias->save();
1 $currentPath = \Drupal::service('path.current')->getPath();
2 // Or with alias and query string.
3 $alias = \Drupal::request()->getRequestUri();
4 // Or
5 $url_string = Url::fromRoute('<current>')->toString();
1 $node = \Drupal::request()->attributes->get('node');
2 $nid = $node->id();
OR
1 $node = \Drupal::routeMatch()->getParameter('node');
2 if ($node instanceof \Drupal\node\NodeInterface) {
3 // You can get nid and anything else you need from the node object.
4 $nid = $node->id();
5 $nodeType = $node->bundle();
6 $nodeTitle = $node->getTitle();
7 }
If you need to use the node object in hook_preprocess_page() on the preview page, you need to use
the ”node_preview” parameter, instead of the ”node” parameter:
1 function mymodule_preprocess_page(&$vars) {
2
3 $route_name = \Drupal::routeMatch()->getRouteName();
4
5 if ($route_name == 'entity.node.canonical') {
6 $node = \Drupal::routeMatch()->getParameter('node');
7 }
8 elseif ($route_name == 'entity.node.preview') {
9 $node = \Drupal::routeMatch()->getParameter('node_preview');
10 }
1 use Drupal\Core\Cache\Cache;
2
3 $node = \Drupal::routeMatch()->getParameter('node');
4 if ($node instanceof \Drupal\node\NodeInterface) {
5 $nid = $node->id();
6 }
7
8 // for cache
9 public function getCacheTags() {
10 //With this when your node changes your block will rebuild
11 if ($node = \Drupal::routeMatch()->getParameter('node')) {
12 //if there is node add its cachetag
13 return Cache::mergeTags(parent::getCacheTags(), ['node:' . $node->id()]);
14 }
15 else {
16 //Return default tags instead.
17 return parent::getCacheTags();
18 }
19 }
20
21 public function getCacheContexts() {
22 //if you depend on \Drupal::routeMatch()
23 //you must set context of this block with 'route' context tag.
24 //Every new route this block will rebuild
25 return Cache::mergeContexts(parent::getCacheContexts(), ['route']);
26 }
1 $current_route = \Drupal::routeMatch()->getRouteName();
It returns ”entity.node.canonical” for the nodes, ”system.404” for the 404 pages, ”entity.taxonomy_-
term.canonical” for the taxonomy pages, ”entity.user.canonical” for the users and custom route name
that we define in modulename.routing.yml file.
1 $image_path = \Drupal::service('file_system')->realpath();
1 $current_path = \Drupal::service('path.current')->getPath();
2 $path_args = explode('/', $current_path);
3 $term_name = $path_args[3];
For https://txg.ddev.site/newsroom/search/?country=1206
1 $query = \Drupal::request()->query->get('name');
2 $name = $_GET['abc'];
1 $name = \Drupal::request()->request->get('name');
2 //or
3 $name = $_POST['abc'];
1 $query = \Drupal::request()->query->all();
2 $search_term = $query['query'];
3 $collection = $query['collection'];
1 namespace Drupal\newday\Controller;
2
3 use Drupal\Core\Controller\ControllerBase;
4
5 class NewdayController extends ControllerBase {
6 public function new() {
7 $day= [
8 "#markup" => \Drupal::request()->query->get('id'),
9 ];
10 return $day;
11 }
12 }
The request is being cached, you need to tell the system to vary by the query arguments:
1 $day = [
2 '#markup' => \Drupal::request()->query->get('id'),
3 '#cache' => [
4 'contexts' => ['url.query_args:id'],
5 ],
6 ];
1 /**
2 * Implements hook_pathauto_alias_alter().
3 *
4 * Note. This function is a stopgap measure to handle pathauto
5 * token failing to return the parent menu item alias.
6 * Using the pattern:
7 * [node:menu-link:parent:url:path]/[node:title]
8 * never returns the parent alias for some inexplicable reason.
9 * This works fine on a fresh Drupal 9 site. Much research
10 * yielded no results so this function hijacks pathauto's
11 * efforts to create the alias. It checks for the presence
12 * of the parent menu alias.
13 * That parent alias is inserted in the path if it isn't found.
14 * If the parent alias is correctly inserted, it should not impact
15 * the correct functioning of pathauto and token.
16 */
17 function dirt_pathauto_alias_alter(&$alias, array &$context) {
18
19 // Change the alias if it doesn't include the parent item.
20 /** @var Drupal\pathauto\Entity\PathautoPattern $pattern*/
21 $pattern = $context["pattern"];
22 /* The pattern will be something like this
23 * [node:menu-link:parent:url:path]/[node:title]
24 * so I could test for it
25 * but it doesn't seem necessary.
26 * Of course, if they change the pattern later this
27 * pattern will overwrite it.
28 *
29 */
30 $bundles = ['page', 'audience', 'overview', 'program_area'];
31 if (in_array($context["bundle"], $bundles)) {
32 $op = $context['op'];
33 if (in_array($op, ['insert', 'update', 'bulkupdate'])) {
34 $parts = explode('/', $alias);
35 if (count($parts) == 2) {
36 if ($parts[0] == "") {
37 //Missing the parent link.
38 $id = $pattern->id();
39 //Is this the default pathauto pattern?
40 if ($id == "default") {
41 //Is there a parent link?
42 $nid = $context["data"]["node"]->id();
43 $parent_link = _findParentMenuItem($nid);
Links, Aliases and URLs 274
44 if ($parent_link) {
45 $parent_alias = $parent_link['#url']->toString();
46 //Update the alias to include the parent alias.
47 if ($parent_alias) {
48 $alias = $parent_alias . $alias;
49 }
50 }
51 }
52 }
53 }
54 }
55 }
56 }
1 use Drupal\Core\Url;
2 use Drupal\Core\Link;
3
4 $url = Url::fromRoute('entity.node.edit_form', ['node' => NID]);
5 $project_link = Link::fromTextAndUrl(t('Open Project'), $url);
6 $project_link = $project_link->toRenderable();
7 // If you need some attributes.
8 $project_link['#attributes'] = ['class' => ['button', 'button-action', 'button--prim\
9 ary', 'button--small']];
10 print render($project_link);
The parameter “test” used above is typically the module name. It is stored in the “type” field.
You can call difference methods such as info, warning etc. which populate the severity field with
an integer indicating the severity of the issue.
The methods are defined in Drupal\Core\Logger\RfcLoggerTrait:
• emergency($message, $context)
• alert($message, $context)
• critical($message, $context)
• error($message, $context)
• warning($message, $context)
• notice($message, $context)
• info($message, $context)
• debug($message, $context)
More at https://www.drupal.org/docs/8/api/logging-api/overview
Logging 276
1 services:
2 websphere_commerce.address:
3 class: Drupal\websphere_commerce\WebSphereAddressService
4 arguments: ['@config.factory', '@logger.factory']
1 use Drupal\Core\Logger\LoggerChannelFactory;
2 use Drupal\Core\Logger\LoggerChannelFactoryInterface;
1 /**
2 * @var Drupal\Core\Logger\LoggerChannelFactory
3 */
4 protected $logger;
1 /**
2 * WebsphereAddress constructor.
3 *
4 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
5 * Config factory.
6 * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $channel_factory
7 * Logger factory.
8 */
9 public function __construct(ConfigFactoryInterface $config_factory, LoggerChannelFac\
10 toryInterface $channel_factory) {
11 $this->websphereConfig = $config_factory->get('websphere_commerce.api_settings');
12 }
Log errors.
1 if ($response['status'] == API_ERROR) {
2 $this->logger->get('websphere_commerce')->alert("Error saving Shipping info to Web\
3 sphere.");
4 }
Note. You can find the factory info with Drupal console:
1 https://symfonycasts.com/tracks/drupal
2 https://symfonycasts.com/screencast/drupal8-under-the-hood
Logging 278
one of the results specifies the factory which you can use below:
1 logger.factory Drupal\\Core\\Logger\\LoggerChannelFactory
1 dino_roar.dino_listener:
2 class: Drupal\dino_roar\Jurassic\DinoListener
3 arguments: ['@logger.factory']
4 tags:
5 - {name: event_subscriber}
1 namespace Drupal\dino_roar\Jurassic;
2
3
4 use Drupal\Core\Logger\LoggerChannelFactoryInterface;
5 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
6 use Symfony\Component\HttpKernel\Event\GetResponseEvent;
7 use Symfony\Component\HttpKernel\KernelEvents;
8
9 class DinoListener implements EventSubscriberInterface {
10
11 /**
12 * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
13 */
14 private $loggerChannelFactory;
15
16 public function __construct(LoggerChannelFactoryInterface $loggerChannelFactory) {
17
Logging 279
18 $this->loggerChannelFactory = $loggerChannelFactory;
19 }
20
21 public function onKernelRequest(GetResponseEvent $event) {
22 $request = $event->getRequest();
23 $shouldRoar = $request->query->get('roar');
24 if ($shouldRoar) {
25 $this->loggerChannelFactory->get('default')
26 ->debug('Roar Requested ROOOOAAAARRR!');
27 }
28 }
29
30 public static function getSubscribedEvents() {
31 return [
32 KernelEvents::REQUEST => 'onKernelRequest',
33 ];
34 }
35 }
17 return $build;
18 }
19
20 function test() {
21 throw new \Exception("blah", 7);
22 }
1 $messenger = \Drupal::messenger();
2 $messenger->addMessage("a message");
3 $messenger->addError("error message");
Or
1 \Drupal::messenger()->addError("migration failed");
2
3 \Drupal::messenger()->addMessage($message, $type, $repeat);
Don’t forget
1 use Drupal\Core\Messenger\MessengerInterface;
e.g.
Logging 281
1 $is_front = \Drupal::service('path.matcher')->isFrontPage();
2 $is_front = $is_front == TRUE ? "YEP" : "NOPE";
3 $messenger->addMessage("is_front = $is_front");
4 var_dump($is_front);
5 print_r($is_front);
Logging 282
18.8: Reference
• How to Log Messages in Drupal 8 by Amber Matz of Drupalize.me Updated October 2015
https://drupalize.me/blog/201510/how-log-messages-drupal-8
• Logging API updated January 2023 https://www.drupal.org/docs/8/api/logging-api/overview
• Drupal APIs https://www.drupal.org/docs/drupal-apis
19: Menus
19.1: Dynamically change menu items with
hook_preprocess_menu
In the example below, we’re changing the labels of items in the user menu. The labels are changed
from “login” and “log out” to “log the flock in” and “log the flock out.” This can be implemented in
a theme file as it is here.
1 /**
2 * Implements hook_preprocess_menu().
3 */
4 function pega_academy_theme_preprocess_menu(&$vars, $hook) {
5 if ($hook == 'menu__account') {
6 $items = $vars['items'];
7 foreach ($items as $key => $item) {
8 if ($key == 'user.page') {
9 $vars['items'][$key]['title'] = [
10 '#markup' => 'Log the <i>flock</i> in!',
11 '#allowed_tags' => ['i'],
12 ];
13 }
14 if ($key == 'user.logout') {
15 $vars['items'][$key]['title'] = [
16 '#markup' => 'Log the <i>flock</i> out!',
17 '#allowed_tags' => ['i'],
18 ];
19 }
20 }
21 }
22 }
1 function pdq_academy_core_update_8002() {
2
3 $mids = \Drupal::entityQuery('menu_link_content')
4 ->condition('menu_name', 'pdq-wide-utility')
5 ->execute();
6
7 foreach($mids as $mid) {
8 $menu_link = \Drupal::entityTypeManager()->getStorage('menu_link_content')->load\
9 ($mid);
10
11 $title = $menu_link->getTitle();
12 if ($title === 'Support') {
13 $menu_link->set('weight',2);
14 $menu_link->set('expanded', TRUE);
15 // $menu_link->set('title','yomama');
16 $menu_link->set('link', 'https://www.google.com');
17 $menu_link->save();
18
19 }
20 }
21 }
1 function pdq_archive_core_update_8001() {
2 $items = [
3 '1' => ['Training', 'https://abc.pdq.com/', -51, TRUE],
4 '2' => ['Support', 'https://abc.pdq.com/search/product-support', -50, TRUE],
5 ];
6
7 foreach($items as $item) {
8 $menu_link = Drupal\menu_link_content\Entity\MenuLinkContent::create([
9 'title' => $item[0],
10 'link' => ['uri' => $item[1]],
11 'menu_name' => 'pdq-wide-utility',
12 'weight' => $item[3],
13 'expanded' => $item[4],
14 ]);
Menus 285
15 $menu_link->save();
16 }
17 }
1 function pdq_archive_core_update_8001() {
2
3 // Update and remove unused menu items
4 $mids = \Drupal::entityQuery('menu_link_content')
5 ->condition('menu_name', 'pdq-wide-utility')
6 ->execute();
7
8 foreach($mids as $mid) {
9
10 $menu_link = \Drupal::entityTypeManager()->getStorage('menu_link_content')->load\
11 ($mid);
12
13 $title = $menu_link->getTitle();
14 if ($title === 'Pdq.com' || $title === 'PDQ Community' || $title === 'Careers') {
15 $menu_link->delete();
16 }
17 if ($title === 'Support') {
18 $menu_link->set('weight',2);
19 $menu_link->set('expanded', TRUE);
20 // $menu_link->set('title','yomama');
21 // $menu_link->set('link', 'https://www.google.com');
22 $menu_link->save();
23 }
24 }
25 }
If you need to get the parent value so you can make a menu item a child, use:
1 $parent_id = $menu_link->getPluginId();
and
Menus 286
1 $menu_link->set('parent', $parent_id);
1 function txg_preprocess_node(&$variables) {
2 $node = $variables['node'];
3
4 if (($node->getType() == 'page') && ($view_mode == 'full')) {
5 // This will only be true if it is a node route.
6 if ($node = \Drupal::request()->attributes->get('node')) {
7 if (is_string($node)) {
8 $node_id = $node;
9 }
10 else {
11 $node_id = $node->id();
12 }
13 }
14 $show_sidebar = _check_ancestry_for_unit($node_id);
15 $variables['show_sidebar_menu'] = $show_sidebar;
16 }
1 /**
2 * Go look up the menu chain for a unit.
3 *
4 * @param int $node_id
5 * Start with this node.
6 * @param int $unit_nid
7 * Fill in the unit nid that you found.
8 *
9 * @return bool
10 * Return TRUE if there is a unit in the lineage.
11 *
12 * @throws \Drupal\Component\Plugin\Exception\PluginException
13 */
14 function _check_ancestry_for_unit(int $node_id, &$unit_nid = 0, &$menu_item_title = \
15 '') {
16 /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
17 $menu_link_manager = \Drupal::service('plugin.manager.menu.link');
18 $links = $menu_link_manager->loadLinksByRoute('entity.node.canonical', ['node' => \
19 $node_id]);
20
21 /** @var \Drupal\Core\Menu\MenuLinkContent $link */
22 $link = reset($links);
23 if (!empty($link)) {
24 while ($parent_menu_id = $link->getParent()) {
25
26 // Create a menu item.
27 /** @var \Drupal\Core\Menu\MenuLinkInterface $parent */
28 $parent_menu_item = $menu_link_manager->createInstance($parent_menu_id);
29 $parent_url = $parent_menu_item->getUrlObject();
30
31 // Is the parent an External link e.g. nytimes.com?
32 $external = $parent_url->isExternal();
33 if ($external) {
34 return FALSE;
35 }
36
37 // Internal links (could be <nolink>)
38 $internal = $parent_url->isRouted();
39 $parent_route = $parent_url->getRouteParameters();
40 // When $parent_route is <nolink>.
41 if (empty($parent_route)) {
42 return FALSE;
43 }
Menus 288
44
45 // Parent is an internal link.
46 // Does parent menu item refer to a unit?
47 if (isset($parent_route['node'])) {
48 $parent_nid = $parent_route['node'];
49 $parent_node = Node::load($parent_nid);
50 $parent_type = $parent_node->getType();
51 if ($parent_type == 'unit') {
52 $unit_nid = $parent_nid;
53 $menu_item_title = $parent_menu_item->getTitle();
54 return TRUE;
55 }
56 $links = $menu_link_manager->loadLinksByRoute('entity.node.canonical', ['nod\
57 e' => $parent_nid]);
58 $link = reset($links);
59 }
60 else {
61 // Some other kind of internal link.
62 return FALSE;
63 }
64 }
65 }
66 return FALSE;
67 }
1 use Drupal\Core\Menu\MenuTreeParameters;
2
3
4 function get_menutree($menu_name) {
5 $parameters = new MenuTreeParameters();
6
7 // Only enabled items.
8 $parameters->onlyEnabledLinks();
9
10 // Load the tree.
11 $tree = \Drupal::menuTree()->load($menu_name, $parameters);
Menus 289
12 return $tree;
13 }
And here we call get_menutree() and then loop through the menu items to build some arrays to be
rendered via a twig template. We loop through the top level, skipping non-nodes:
1 function get_offices() {
2
3 //Get drupal main menu
4 $tree = get_menutree('main');
5 $menu_tree = \Drupal::menuTree();
6
7 // Build a renderable array from the tree which fills in all the children in
8 // item['below']
9 $menu_render_array = $menu_tree->build($tree);
10 $storage = [];
11 $nid = 0;
12 foreach ($menu_render_array['#items'] as $item) {
13 $url = $item["url"];
14 $route = $url->getRouteParameters();
15 // Skip menu items that aren't nodes.
16 if (!isset($route['node'])) {
17 continue;
18 }
19 $nid = $route["node"];
20 $node = Node::load($nid);
21 $type = $node->getType();
22 if ($type = "area_of_focus") {
23 $storage[] = [
24 'title' => $item['title'],
25 'value' => $nid
26 ];
27 }
28
29 // Process children:
30 foreach ($item['below'] as $child) {
31 $url = $child["url"];
32 $route = $url->getRouteParameters();
Menus 290
33 $nid = 0;
34 $type = '';
35 if (isset($route['node'])) {
36 $nid = $route["node"];
37 $node = Node::load($nid);
38 $type = $node->getType();
39
40 }
41 // Only add units.
42 if ($type == 'unit') {
43 $storage[] = [
44 'title' => '--' . $child['title'] . ' (' . $type . ')',
45 'value' => $nid
46 ];
47 }
48 }
49 }
50 return $storage;
51 }
22 {% endfor %}
23 </div>
24 </form>
It is called with:
1 https://www.drupal.org/project/twig_tweak
Menus 293
1 namespace Drupal\custom_module\Twig;
2
3 class RenderMenuExtension extends \Twig_Extension {
4
5 /**
6 * @return array
7 */
8 public function getFunctions() {
9 return [
10 new \Twig_SimpleFunction('renderMenu', [$this, 'renderMenu']),
11 ];
12 }
13
14 /**
15 * Provides function to programmatically rendering a menu
16 *
17 * @param String $menu_name
18 * The machine configuration id of the menu to render
19 */
20 public function renderMenu(string $menu_name) {
21 $menu_tree = \Drupal::menuTree();
22
23 // Build the typical default set of menu tree parameters.
24 $parameters = $menu_tree->getCurrentRouteMenuTreeParameters($menu_name);
25
26 // Load the tree based on this set of parameters.
27 $tree = $menu_tree->load($menu_name, $parameters);
28
29 // Transform the tree using the manipulators you want.
30 $manipulators = [
31 // Only show links that are accessible for the current user.
32 ['callable' => 'menu.default_tree_manipulators:checkAccess'],
33 // Use the default sorting of menu links.
34 ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
35 ];
36 $tree = $menu_tree->transform($tree, $manipulators);
37
38 // Finally, build a renderable array from the transformed tree.
39 $menu = $menu_tree->build($tree);
40
41 $menu['#attributes']['class'] = 'menu ' . $menu_name;
42
43 return ['#markup' => drupal_render($menu)];
Menus 294
44 }
45
46 }
Oh, and you do need to implement the getFunctions() in your class. It looks something like this:
In custom_module.services.yml:
1 services:
2 custom_module.render_menu_extension:
3 class: Drupal\custom_module\Twig\RenderMenuExtension
4 tags:
5 - { name: twig.extension }
To render your menu in the template via this twig function call:
1 pageexample_description:
2 title: 'Page Example'
3 route_name: pageexample_description
4 parent: system.admin_reports
5 pageexample.simple:
6 title: 'Simple page example - no arguments'
7 route_name: pageexample_simple
8 parent: system.admin_reports
The parent values are defined in other *.links.menu.yml files and especially the Structure link, which
is defined in the core System module in system.links.menu.yml. Here we are adding a link to appear
under the Reports menu of Drupal.
To create menu links programmatically, see https://drupal.stackexchange.com/questions/197073/
how-do-i-create-menu-links-programmatically/197076#197076
To edit menu links programmatically, see https://drupal.stackexchange.com/questions/235516/how-
do-i-programmatically-update-or-delete-menu-items
First you will have to load the entity. Either way works:
1 $menu_link = MenuLinkContent::load($menu_link_id);
or ...
1 $menu_link = \Drupal::entityTypeManager()->getStorage('menu_link_content')->load($me\
2 nu_link_id);
Next you can update value using set() method or through the magic method __set...
1 $menu_link->expanded = TRUE;
To save, simply call the save() method. To delete, call the delete() method.
19.12: Resources
• To create menu links programmatically see https://drupal.stackexchange.com/questions/
197073/how-do-i-create-menu-links-programmatically/197076#197076
• To edit menu links programmatically, see https://drupal.stackexchange.com/questions/235516/
how-do-i-programmatically-update-or-delete-menu-items
• Active Trail https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Menu%
21MenuTreeParameters.php/property/MenuTreeParameters%3A%3AactiveTrail/9.3.x
• The Twig_Tweak module2 can do some great menu magic. Cheat sheet at https://www.drupal.
org/docs/8/modules/twig-tweak/cheat-sheet
• #! code: Drupal 9: Creating A Category Menu Using Derivers, August 20223
2 https://www.drupal.org/project/twig_tweak
3 https://www.hashbangcode.com/article/drupal-9-creating-category-menu-using-derivers
20: Migration
20.1: Import content from another Migration into
Paragraphs
This migration Process snippet example demonstrates how to populate the fields of a Drupal
Paragraph. Whereas standard fields can be simply mapped 1:1, and its value attribute (value) is
implied (derived), a Paragraph requires both a target_id and target_revision_id. Annotations
included inline to demonstrate what is going on within.
29 index:
30 - 1
20.2: Resources
• 31 days of Drupal migrations by Mauricio Dinarte August 2019 https://understanddrupal.com/
migrations
• Stop waiting for Feeds module: how to import RSS in Drupal 8 by Campbell Vertesi June
2017 https://ohthehugemanatee.org/blog/2017/06/07/stop-waiting-for-feeds-module-how-to-
import-remote-feeds-in-drupal-8
• Issue on Drupal.org where Mike Ryan, the author of the migrate module, addresses how to
start a migration programmatically https://www.drupal.org/project/drupal/issues/2764287
• Video from Twin Cities Drupal community where Mauricio Dinarte and Benjamin Melançon
demonstrate how to implement migrations - June 20187. https://www.youtube.com/watch?v=
eBP2vQIwx-o
• Migrating to Drupal From Alternate Sources by Joshua Turton showing migrations from CSV,
XML/RSS and Wordpress into Drupal 8 - November 2018 https://www.phase2technology.com/
blog/migrating-drupal-alternate-sources
• Nice intro to Drupal 8 migration by Chris of Redfin Solutions from Nov 2017. https://
redfinsolutions.com/blog/understanding-drupal-8s-migrate-api
• Migrating files and images on Drupal.org updated Feb 2023. https://www.drupal.org/docs/
drupal-apis/migrate-api/migrate-destination-plugins-examples/migrating-files-and-images
• Drupal 8 content migrations from CSV or spreadsheet August 2020 https://atendesigngroup.
com/articles/drupal-8-content-migrations-csv-spreadsheet
21: Modal Dialogs
21.1: Overview
There are several ways to create modal dialogs in Drupal. Modal dialogs provide a way to display
additional information without having to reload the entire page. These dialogs can display anything
you can imagine including static text, an image, a node, a form, a view or any custom markup.
1 taa.srp_delete_citation:
2 path: '/taa/admin /citation/delete/citation/{citation}'
3 defaults:
4 _title: 'Delete Citation'
5 _form: 'Drupal\taa\Form\DeleteCitationForm'
Modal Dialogs 301
1 use Drupal\Component\Serialization\Json;
2
3 $submit_for_review_link = [
4 '#type' => 'link',
5 '#title' => t('Submit for Review'),
6 '#url' => Url::fromRoute('taa_teks_publisher.submit_for_review', ['node' => $node-\
7 >id(), 'return_type' => 'program']),
8 '#attributes' => [
9 'class' => ['use-ajax', 'button'],
10 'id' => 'edit-program-' . $node->id(),
11 'data-dialog-renderer' => 'off_canvas',
12 'data-dialog-type' => 'dialog',
13 'data-dialog-options' => Json::encode(
14 [
15 'width' => '600',
16 ]
17 ),
18 ],
19 ];
Notice the data-dialog- options. ‘off_canvas’ is the magic sauce. Also note that the parameters that
are passed to the form are ‘node’ and ‘return_type’ which are specified in the route below also.
Note the class ‘button’ will make this link show up as a button. If you remove it, this will instead
just be a link.
You could easily make this a modal dialog by specifying:
24
25
26 # Second modal
27 # You can't use this route directly
28 # route for modal to display a teaser of a node in a dialog
29 modal_examples.modal2:
30 path: '/modal-examples/create-my-dialog'
31 defaults:
32 _controller: '\Drupal\modal_examples\Controller\ModalExamplesController2::create\
33 DialogFromNode'
34 requirements:
35 _access: 'TRUE'
1 <?php
2
3 namespace Drupal\modal_examples\Controller;
4
5 use Drupal\Component\Serialization\Json;
6 use Drupal\Core\Controller\ControllerBase;
7 use Drupal\Core\Url;
8
9 /**
10 * Returns responses for Modal Examples routes.
11 */
12 class ModalExamplesController extends ControllerBase {
13
14 public function buildExample1() {
15
16 $route_name = \Drupal::routeMatch()->getRouteName();
17 $build['content'] = [
18 '#type' => 'item',
Modal Dialogs 304
62 $build['content'] = [
63 '#type' => 'item',
64 '#markup' => $this->t('Some useful information!'),
65 ];
66 $build['other_content'] = [
67 '#type' => 'item',
68 '#markup' => $this->t('Program id: @program_id. Type: @type', [
69 '@program_id' => $program_id,
70 '@type' => $type,
71 ]),
72 ];
73
74 return $build;
75 }
76
77 }
The second modal is in ModalExamplesController2.php. This dialog uses the example from an
example at https://www.hashbangcode.com/article/drupal-9-creating-ajax-dialogs which displays
a teaser view of a node:
1 <?php
2
3 /**
4 * @file
5 *
6 * Contains \Drupal\modal_examples\Controller\ModalExamplesController2.
7 */
8 namespace Drupal\modal_examples\Controller;
9
10 use Drupal\Core\Ajax\OpenDialogCommand;
11 use Drupal\Core\Entity\EntityTypeManagerInterface;
12 use Symfony\Component\DependencyInjection\ContainerInterface;
13 use Drupal\Core\Ajax\AjaxResponse;
14 use Drupal\Core\Ajax\OpenModalDialogCommand;
15 use Drupal\Core\Controller\ControllerBase;
16 use Drupal\Core\Form\FormBuilder;
17
18
19 /**
20 * ModalExamplesController2 class.
21 */
22 class ModalExamplesController2 extends ControllerBase {
Modal Dialogs 306
23
24 /**
25 * The form builder.
26 *
27 * @var \Drupal\Core\Form\FormBuilder
28 */
29 protected $formBuilder;
30
31 /**
32 * The entity type manager.
33 *
34 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
35 */
36 protected $entityTypeManager;
37
38 /**
39 * The ModalFormExampleController constructor.
40 *
41 * @param \Drupal\Core\Form\FormBuilder $formBuilder
42 * The form builder.
43 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
44 * The entity type manager.
45 */
46 public function __construct(FormBuilder $formBuilder, EntityTypeManagerInterface $\
47 entity_type_manager) {
48 $this->formBuilder = $formBuilder;
49 $this->entityTypeManager = $entity_type_manager;
50 }
51
52 /**
53 * {@inheritdoc}
54 *
55 * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
56 * The Drupal service container.
57 *
58 * @return static
59 */
60 public static function create(ContainerInterface $container) {
61 return new static(
62 $container->get('form_builder'),
63 $container->get('entity_type.manager'),
64 );
65 }
Modal Dialogs 307
66
67
68 /*
69 * Phil's open a dialog to display a node teaser.
70 */
71 public function createDialogFromNode() {
72 // Load a specific node.
73 $node = $this->entityTypeManager->getStorage('node')->load(34);
74
75 // Convert node into a render array.
76 $viewBuilder = $this->entityTypeManager->getViewBuilder('node');
77 $content = $viewBuilder->view($node, 'teaser');
78
79 // Get the title of the node.
80 $title = $node->getTitle();
81
82 // Create the AjaxResponse object.
83 $response = new AjaxResponse();
84
85 // Attach the library needed to use the OpenDialogCommand response.
86 $attachments['library'][] = 'core/drupal.dialog.ajax';
87 $response->setAttachments($attachments);
88
89 // Add the open dialog command to the ajax response.
90 $response->addCommand(new OpenDialogCommand('#my-dialog-selector', $title, $cont\
91 ent, ['width' => '70%']));
92 return $response;
93 }
94
95 }
Here is the first modal dialog as it appears on the site when the modal1 link is clicked:
Modal Dialogs 308
1 taa.view_votes:
2 path: '/srp/program/{program}/vote-view/correlation/{correlation}/{type}'
3 defaults:
4 _controller: '\Drupal\taa\Controller\ViewVotesController::content'
5 _title: 'Votes for Citation'
6 requirements:
7 _permission: 'view any saa item+view own saa item'
8 options:
9 parameters:
10 program:
11 type: entity:node
12 correlation:
13 type: entity:node
14 no_cache: 'TRUE'
The controller will look like this. Notice how we can call entity functions on the $program and
$correlation entities.
1 modal_examples.form1:
2 path: '/modal-examples/form1'
3 defaults:
4 _form: 'Drupal\modal_examples\Form\ExampleForm'
5 _title: 'Form with a button to popup a modal form'
6 requirements:
7 _permission: 'administer site configuration'
8
9 modal_examples.modal_form:
10 path: '/modal-examples/modal_form'
11 defaults:
12 _title: 'The Modal Form'
13 _controller: '\Drupal\modal_examples\Controller\ModalExamplesController2::openMo\
14 dalForm'
15 requirements:
16 _permission: 'administer site configuration'
17 options:
18 _admin_route: TRUE
When we navigate to /modal-example/form, we see the following form. If we click the button
labelled, ”Click to see the Modal Form”, a modal dialog will appear with a form in it.
Here is the initial form:
1 <?php
2
3 namespace Drupal\modal_examples\Form;
4
5 use Drupal\Core\Form\FormBase;
6 use Drupal\Core\Form\FormStateInterface;
7 use Drupal\Core\Url;
8
9 /**
10 * ExampleForm class.
11 */
12 class ExampleForm extends FormBase {
13
14 /**
15 * {@inheritdoc}
16 */
17 public function buildForm(array $form, FormStateInterface $form_state, $options = \
18 NULL) {
19
20 $form['#prefix'] = '<div id="example_form">';
Modal Dialogs 312
21 $form['#suffix'] = '</div>';
22
23 $form['info']['instructions'] = [
24 '#type' => 'markup',
25 '#markup' => $this->t('Please fill out the form below and click the button for\
26 more info.'),
27 ];
28
29 $form['info']['name'] = [
30 '#type' => 'textfield',
31 '#title' => $this->t('Name'),
32 '#size' => 20,
33 '#default_value' => 'Mary Vasquez',
34 '#required' => FALSE,
35 ];
36
37 $form['actions']['open_modal'] = [
38 '#type' => 'link',
39 '#title' => $this->t('Click to see the Modal Form'),
40 '#url' => Url::fromRoute('modal_examples.modal_form'),
41 '#attributes' => [
42 'class' => [
43 'use-ajax',
44 'button',
45 ],
46 ],
47 ];
48
49 $form['actions']['submit'] = [
50 '#type' => 'submit',
51 '#value' => $this->t('Submit'),
52 '#attributes' => [
53 ],
54 ];
55
56 return $form;
57 }
58
59 /**
60 * {@inheritdoc}
61 */
62 public function submitForm(array &$form, FormStateInterface $form_state) {
63 // @TODO.
Modal Dialogs 313
64 }
65
66 /**
67 * {@inheritdoc}
68 */
69 public function getFormId() {
70 return 'modal_examples_example_form';
71 }
72
73 }
Here is the controller that is used to load the modal form at web/modules/custom/modal_-
examples/src/Controller/ModalExamplesController2.php
1 <?php
2
3 /**
4 * @file
5 *
6 * Contains \Drupal\modal_examples\Controller\ModalExamplesController2.
7 */
8 namespace Drupal\modal_examples\Controller;
9
10 use Symfony\Component\DependencyInjection\ContainerInterface;
11 use Drupal\Core\Ajax\AjaxResponse;
12 use Drupal\Core\Ajax\OpenModalDialogCommand;
13 use Drupal\Core\Controller\ControllerBase;
14 use Drupal\Core\Form\FormBuilder;
15
16 /**
17 * ModalExamplesController2 class.
18 */
19 class ModalExamplesController2 extends ControllerBase {
20
21 /**
22 * The form builder.
23 *
24 * @var \Drupal\Core\Form\FormBuilder
25 */
26 protected $formBuilder;
27
28 /**
29 * The ModalFormExampleController constructor.
Modal Dialogs 314
30 *
31 * @param \Drupal\Core\Form\FormBuilder $formBuilder
32 * The form builder.
33 */
34 public function __construct(FormBuilder $formBuilder) {
35 $this->formBuilder = $formBuilder;
36 }
37
38 /**
39 * {@inheritdoc}
40 *
41 * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
42 * The Drupal service container.
43 *
44 * @return static
45 */
46 public static function create(ContainerInterface $container) {
47 return new static(
48 $container->get('form_builder')
49 );
50 }
51
52 /**
53 * Callback for opening the modal form.
54 */
55 public function openModalForm() {
56 $response = new AjaxResponse();
57
58 // Get the modal form using the form builder.
59 $modal_form = $this->formBuilder->getForm('Drupal\modal_examples\Form\ExampleMod\
60 alForm');
61
62 // Add an AJAX command to open a modal dialog with the form as the content.
63 $response->addCommand(new OpenModalDialogCommand('My Modal Form', $modal_form, [\
64 'width' => '800']));
65
66 return $response;
67 }
68
69 }
1 <?php
2
3 namespace Drupal\modal_examples\Form;
4
5 use Drupal\Core\Ajax\CloseModalDialogCommand;
6 use Drupal\Core\Form\FormBase;
7 use Drupal\Core\Form\FormStateInterface;
8 use Drupal\Core\Ajax\AjaxResponse;
9 use Drupal\Core\Ajax\OpenModalDialogCommand;
10 use Drupal\Core\Ajax\ReplaceCommand;
11
12 /**
13 * ModalForm class.
14 */
15 class ExampleModalForm extends FormBase {
16
17 /**
18 * {@inheritdoc}
19 */
20 public function getFormId() {
21 return 'modal_form_example_modal_form';
22 }
23
24 /**
25 * {@inheritdoc}
26 */
27 public function buildForm(array $form, FormStateInterface $form_state, $options = \
28 NULL) {
29 $form['#prefix'] = '<div id="modal_example_form">';
30 $form['#suffix'] = '</div>';
31
32 // The status messages that will contain any form errors.
33 $form['status_messages'] = [
34 '#type' => 'status_messages',
35 '#weight' => -10,
36 ];
37
38 // A required checkbox field.
39 $form['our_checkbox'] = [
40 '#type' => 'checkbox',
41 '#title' => $this->t('I Agree: modal forms are awesome!'),
42 '#default_value' => TRUE,
43 '#required' => TRUE,
Modal Dialogs 316
44 ];
45
46 $form['name'] = [
47 '#type' => 'textfield',
48 '#title' => $this->t('Name'),
49 '#size' => 20,
50 '#default_value' => 'Jane Martinez',
51 '#required' => FALSE,
52 ];
53
54 $form['email'] = [
55 '#type' => 'email',
56 '#title' => $this->t('Email'),
57 '#size' => 30,
58 '#default_value' => '[email protected]',
59 '#required' => FALSE,
60 ];
61
62
63 $form['actions'] = array('#type' => 'actions');
64 $form['actions']['send'] = [
65 '#type' => 'submit',
66 '#value' => $this->t('Submit'),
67 '#attributes' => [
68 'class' => [
69 'use-ajax',
70 ],
71 ],
72 '#ajax' => [
73 'callback' => [$this, 'submitModalFormAjax'],
74 'event' => 'click',
75 ],
76 ];
77 $form['actions']['cancel'] = [
78 '#type' => 'submit',
79 '#value' => $this->t('cancel'),
80 '#attributes' => [
81 'class' => [
82 'use-ajax',
83 ],
84 ],
85 '#ajax' => [
86 'callback' => [$this, 'closeModalForm'],
Modal Dialogs 317
130
131 return $response;
132 }
133
134
135 /**
136 * {@inheritdoc}
137 */
138 public function validateForm(array &$form, FormStateInterface $form_state) {
139 // @TODO.
140 }
141
142 /**
143 * {@inheritdoc}
144 */
145 public function submitForm(array &$form, FormStateInterface $form_state) {
146 // @TODO.
147 }
148
149 }
24 no_cache: 'TRUE'
25
26
27 # Simple form for slide-in modals
28 modal_examples.form2:
29 path: '/modal-examples/form2'
30 defaults:
31 _form: 'Drupal\modal_examples\Form\ExampleForm2'
32 _title: 'Simple example form (form2)'
33 requirements:
34 _permission: 'access content'
35
36 # Form with buttons to open modals
37 modal_examples.login_form:
38 path: '/modal-examples/login-form'
39 defaults:
40 _title: 'Simple example form (form2)'
41 _controller: 'Drupal\modal_examples\Controller\ModalExamplesController2::buildLo\
42 ginForm'
43 requirements:
44 _permission: 'access content'
1 <?php
2
3 /**
4 * @file
5 *
6 * Contains \Drupal\modal_examples\Controller\ModalExamplesController2.
7 */
8 namespace Drupal\modal_examples\Controller;
9
10 use Drupal\Component\Serialization\Json;
11 use Drupal\Core\Ajax\OpenDialogCommand;
12 use Drupal\Core\Entity\EntityTypeManagerInterface;
13 use Drupal\Core\Url;
14 use Symfony\Component\DependencyInjection\ContainerInterface;
15 use Drupal\Core\Ajax\AjaxResponse;
16 use Drupal\Core\Ajax\OpenModalDialogCommand;
17 use Drupal\Core\Controller\ControllerBase;
Modal Dialogs 321
18 use Drupal\Core\Form\FormBuilder;
19
20
21 /**
22 * ModalExamplesController2 class.
23 */
24 class ModalExamplesController2 extends ControllerBase {
25
26 /**
27 * The form builder.
28 *
29 * @var \Drupal\Core\Form\FormBuilder
30 */
31 protected $formBuilder;
32
33 /**
34 * The entity type manager.
35 *
36 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
37 */
38 protected $entityTypeManager;
39
40 /**
41 * The ModalFormExampleController constructor.
42 *
43 * @param \Drupal\Core\Form\FormBuilder $formBuilder
44 * The form builder.
45 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
46 * The entity type manager.
47 */
48 public function __construct(FormBuilder $formBuilder, EntityTypeManagerInterface $\
49 entity_type_manager) {
50 $this->formBuilder = $formBuilder;
51 $this->entityTypeManager = $entity_type_manager;
52 }
53
54 /**
55 * {@inheritdoc}
56 *
57 * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
58 * The Drupal service container.
59 *
60 * @return static
Modal Dialogs 322
61 */
62 public static function create(ContainerInterface $container) {
63 return new static(
64 $container->get('form_builder'),
65 $container->get('entity_type.manager'),
66 );
67 }
68
69
70 public function buildExample2() {
71
72 $route_name = \Drupal::routeMatch()->getRouteName();
73 $build['content'] = [
74 '#type' => 'item',
75 '#markup' => $this->t('Route: %route', ['%route' => $route_name]),
76 ];
77
78 $build['link-to-modal1'] = [
79 '#type' => 'link',
80 '#prefix' => '<div class="pqrst">',
81 '#suffix' => '</div>',
82 '#title' => t('Link to off canvas/slide-in custom modal dialog (modal1)'),
83 '#url' => Url::fromRoute('modal_examples.modal1', [
84 'program_id' => 123,
85 'type' => 'all',
86 ]),
87 '#attributes' => [
88 'id' => 'view-correlation-' . 12345,
89 'class' => ['use-ajax'],
90 'aria-label' => 'View useful information pertaining to item ' . '12345',
91 '#prefix' => '<div class="abcdef">',
92 '#suffix' => '</div>',
93 'data-dialog-type' => 'dialog.off_canvas',
94 'data-dialog-options' => Json::encode(
95 [
96 'width' => 'auto',
97 ]
98 ),
99 ],
100 ];
101
102 $build['link-to-modal1-top'] = [
103 '#type' => 'link',
Modal Dialogs 323
147 ),
148 ],
149 ];
150
151 $build['link-to-example-login-form'] = [
152 '#type' => 'link',
153 '#prefix' => '<div class="pqrst">',
154 '#suffix' => '</div>',
155 '#title'=> t('Link to off canvas/slide-in login form (login_form) '),
156 '#url' => Url::fromRoute('modal_examples.login_form'),
157 '#attributes' => [
158 'id' => 'important-id-' . 12345,
159 'class' => ['use-ajax'],
160 '#prefix' => '<div class="abcdef">',
161 '#suffix' => '</div>',
162 'data-dialog-type' => 'dialog.off_canvas',
163 'data-dialog-options' => Json::encode(
164 [
165 'width' => 'auto',
166 ]
167 ),
168 ],
169 ];
170
171
172 $build['#attached']['library'][] = 'core/drupal.dialog.ajax';
173 $build['#attached']['library'][] = 'core/drupal.dialog.off_canvas';
174
175 return $build;
176 }
177
178 public function buildLoginForm() {
179 //$form = \Drupal::formBuilder()->getForm(\Drupal\user\Form\UserLoginForm::class\
180 );
181 $form = $this->formBuilder()->getForm(\Drupal\user\Form\UserLoginForm::class);
182 return $form;
183 }
184
185 }
And the second which shows the dialog sliding in from the top:
Modal Dialogs 326
1 custom_modal.modal:
2 path: 'modal-example/modal'
3 defaults:
4 _title: 'Modal'
5 _controller: '\Drupal\custom_modal\Controller\CustomModalController::modal'
6 requirements:
7 _permission: 'access content'
Here we define a block in ModalBlock.php that will present a button, which when clicked,
will bring up a modal dialog. Note in the build() method that we get the route using
Url::fromRoute('custom_modal.modal').
1 /**
2 * Provides a 'Modal' Block
3 *
4 * @Block(
5 * id = "modal_block",
6 * admin_label = @Translation("Modal block"),
7 * )
8 */
9 class ModalBlock extends BlockBase {
10 /**
11 * {@inheritdoc}
12 */
13 public function build() {
14 $link_url = Url::fromRoute('custom_modal.modal');
15 $link_url->setOptions([
16 'attributes' => [
17 'class' => ['use-ajax', 'button', 'button--small'],
18 'data-dialog-type' => 'modal',
19 'data-dialog-options' => Json::encode(['width' => 400]),
20 ]
21 ]);
22
23 return array(
Modal Dialogs 329
Then in the controller, CustomModalController.php the modal() function builds the dialog using a
new AjaxResponse() object.
To see this work, place the “modal block” in a region via the u/i, then view a page. When you click
the button, a modal dialog will pop up showing “the modal text.”
And to show a node instead of the search dialog (any valid drupal path will do here)
21.10: Resources
• Excellent tutorial on using modal forms in Drupal from March 20141
• Phil Norton’s great article on Creating AJAX dialogs from October 2022 at2
• Article about creating a modal dialog in a custom module from Jan 20183
1 https://www.mediacurrent.com/blog/loading-and-rendering-modal-forms-drupal-8/
2 https://www.hashbangcode.com/article/drupal-9-creating-ajax-dialogs
3 http://befused.com/drupal/modal-module
22: Nodes and Fields
22.1: Load a node and get a formatted text field
1 $nodeStorage = $this->entityTypeManager->getStorage('node');
2 $node = $nodeStorage->load(10706);
3 $body = $node->get('body')->value;
4 $body = $node->body->value;
5
6 //after text filters have done their magic!
7 $body = $node->body->processed;
1 $accepted_votes = $feedback_error_node->get('field_accepted_votes')->value;
2 // Returns 0 if no value was entered into the field.
1 $request = \Drupal::request();
2 if ($route = $request->attributes->get(\Symfony\Cmf\Component\Routing\RouteObjectInt\
3 erface::ROUTE_OBJECT)) {
4 $title = \Drupal::service('title_resolver')->getTitle($request, $route);}
1 $node = \Drupal::request()->attributes->get('node');
2 $nid = $node->id();
OR
1 $node = \Drupal::routeMatch()->getParameter('node');
2 if ($node instanceof \Drupal\node\NodeInterface) {
3 // You can get nid and anything else you need from the node object.
4 $nid = $node->id();
5 $nodeType = $node->bundle();
6 $nodeTitle = $node->getTitle();
7 }
If you need to use the node object in hook_preprocess_page() on the preview page, you need to use
the node_preview parameter, instead of the node parameter:
Nodes and Fields 333
1 function mymodule_preprocess_page(&$vars) {
2
3 $route_name = \Drupal::routeMatch()->getRouteName();
4
5 if ($route_name == 'entity.node.canonical') {
6 $node = \Drupal::routeMatch()->getParameter('node');
7 }
8 elseif ($route_name == 'entity.node.preview') {
9 $node = \Drupal::routeMatch()->getParameter('node_preview');
10 }
1 use Drupal\Core\Cache\Cache;
2
3 $node = \Drupal::routeMatch()->getParameter('node');
4 if ($node instanceof \Drupal\node\NodeInterface) {
5 $nid = $node->id();
6 }
7
8 // for cache
9 public function getCacheTags() {
10 //With this when your node changes your block will rebuild
11 if ($node = \Drupal::routeMatch()->getParameter('node')) {
12 //if there is node add its cachetag
13 return Cache::mergeTags(parent::getCacheTags(), ['node:' . $node->id()]);
14 }
15 else {
16 //Return default tags instead.
17 return parent::getCacheTags();
18 }
19 }
20
21 public function getCacheContexts() {
22 //if you depend on \Drupal::routeMatch()
23 //you must set context of this block with 'route' context tag.
24 //Every new route this block will rebuild
25 return Cache::mergeContexts(parent::getCacheContexts(), ['route']);
26 }
Nodes and Fields 334
1 $current_path = \Drupal::service('path.current')->getPath();
Also
1 $node = \Drupal::routeMatch()->getParameter('node');
2 if ($node instanceof \Drupal\node\NodeInterface) {
3 // You can get nid and anything else you need from the node object.
4 $nid = $node->id();
5 $nodeTitle = $node->getTitle();
6 $nodeType = $node->bundle();
7 if ($nodeType != 'unit') {
8 return;
9 }
1 $node = \Drupal::routeMatch()->getParameter('node');
2 if ($node instanceof \Drupal\node\NodeInterface) {
3 // You can get nid and anything else you need from the node object.
4 $nid = $node->id();
5 if ($node->getType() == 'unit') {
6 $contract_id = $node->field_contract_id->value;
7 $facebook = $node->field_facebook->value;
8 }
9 }
22.9: Load the current node and get it’s node id (nid),
field, type
To grab some information from the currently displayed node, use the \Drupal
object::routeMatch().
Nodes and Fields 335
22.10: Load a node by nid and get its title, type and a
field
From: https://www.drupal.org/docs/8/api/entity-api/working-with-the-entity-api
1 use Drupal\node\Entity\Node;
2 // Using the node id = 1.
3 $node = Node::load(1);
Or
1 $node = \Drupal::entityTypeManager()->getStorage('node')->load(1);
2 $headline = $node->getTitle();
3
4 // What type of node is that?
5 $type = $node->getType();
6
7 // Get the url field.
8 $url = $node->get('field_url')->value;
Or
Nodes and Fields 336
1 $node_storage = \Drupal::entityTypeManager()->getStorage('node');
2 $node = $node_storage->load(1);
3 $url = $node->get('field_url')->value;
Or
1 $body = $node->field_srp_pc_change_type->value;
And multivalue fields are similar. They are loaded when you use get & getValue or the magic
getter & getValue. You can then loop through the returned values:
or
22.11: Load the current node and get the nid, field,
type
1 // For content type unit grab their facebook/instagram etc.
2 $node = \Drupal::routeMatch()->getParameter('node');
3 if ($node instanceof \Drupal\node\NodeInterface) {
4 // You can get nid and anything else you need from the node object.
5 $nid = $node->id();
6 if ($node->getType() == 'unit') {
7 $sm_heading = $node->get('field_footer_social_media_headin')->value;
8 if (!empty($sm_heading)) {
9 ...
10 }
11 $fb = $node->get('field_facebook')->value;
12 if (!empty($fb)) {
13 ...
14 }
1 $node->get('field_facebook')->getValue();
You can call $node->uid but that returns an EntityReferenceFieldItemList with all sorts of juicy
information. The user id is in there but more challenging to extract.
1 $sf_contract_node = Node::load($sf_contract_nid);
2 if ($sf_contract_node) {
3 if (!$sf_contract_node->get('field_vendor_url')->isEmpty()) {
4 $url = $sf_contract_node->field_vendor_url->first()->getUrl();
5 $url = $url->getUri();
6 $variables['vendor_url'] = $url;
7 }
And more concisely, using a new feature of PHP 8 we can use the following:
1 $url = $sf_contract_node?->field_vendor_url?->first()?->getUrl();
2 if (!is_null($url)) {
3 $uri = $url->getUri();
4 }
1 $node = \Drupal::entityTypeManager()->getStorage('node')->load(1);
2 $url = $node->set('field_url', 'the-blahblah');
3 $node->save();
1 $node->get('field_cn_start_end_dates')->value
2 $node->get('field_cn_start_end_dates')->end_value
1 $data = $node->get('field_condiment')->getValue();
2 // returns:
3 $data[0]['value'] = 'ketchup'
4 $data[1]['value'] = 'mayo'
5 $data[2]['value'] = 'reference'
However a multivalue entity reference field (including taxonomy) will return values with the
target_id array key. e.g.
1 $data = $node->get('field_event_ref')->getValue();
2 // returns:
3 $data[0]['target_id'] = 1
4 $data[1]['target_id'] = 2
5 $data[2]['target_id'] = 14
1 $items = $node->field_condiment;
2 foreach ($items as $item) {
3 $result = $item->value;
4 // For entity reference fields use
5 // $result_nid = $item->target_id;
6 }
1 // get FieldItemList.
2 $condiments = $node->get('field_condiment');
3 $vote_number = 1;
4 if (isset($condiments[$vote_number])) {
5 $result = $condiments[$vote_number]->value;
6 }
7 // $result is ketchup.
8
9 // Or the PHP 8 way:
10
11 $result= $node->field_condiment?->get($vote_number)?->value;
12 // OR
13 $result = $node->field_condiment[$vote_number]?->value;
1 $status = $node->get('field_voting_status')[0]->value;
2 $status = $node->get('field_voting_status')[1]->value;
Note. If the value for delta 1 is empty, Drupal will throw a warning message *Warning*: Attempt
to read property "value" on null in ...
So rather than reading the [1]->value directly, you should check if there is a value using isset()
and then, you can read the ->value. Note. When you do the isset() test, you don’t add the ->value
at the end e.g.
Nodes and Fields 340
1 if(!is_null($node->get('field_voting_status')[$vote_number])) {
2 $voting_status = $correlation_node->field_voting_status[$vote_number]->value;
3 }
1 $data = $node->get('field_event_ref')->getValue();
2
3 $result0 = $data[0]['value'];
4 $result1 = $data[1]['value'];
1 $values = $node->get('field_voting_status')->getValue();
2 $values[5]['value'] = 'incomplete';
3 $node->set('field_srp_voting_status', $values);
4 $node->save();
Example of using the above function for reading data and then writing the data without using it:
12 $narrative_status_values[0]['value'] = $narrative_status;
13
14 $node->set('field_narrative_status', $narrative_status_values);
15 $node->set('field_activity_status', $activity_status_values);
16 $node->save();
1 $citation_node->field_srp_voting_status[$vote_number] = 'incomplete';
If you are going to write index 2 and there is a possibility that there isn’t an index 0 and 1, you need
something like this (in protected function correlationSanityCheckFix(Node $correlation_-
node)):
1 $narrative_status = '';
2
3 if (!is_null($correlation_node->get('field_narrative_status')[$vote_number])) {
4 $narrative_status = $correlation_node->get('field_narrative_status')[$vote_number]\
5 ->value;
6 }
7
8 if (empty($narrative_status)) {
9 // Grab all the statuses and add mine.
10 $statuses = $correlation_node->get('field_narrative_status');
11 $new_statuses = [];
12 foreach ($statuses as $status) {
13 $new_statuses[] = $status->value;
14 }
15 $new_statuses[$vote_number] = 'incomplete';
16 $correlation_node->set('field_narrative_status', $new_statuses);
17 $save_correlation = TRUE;
18 }
Nodes and Fields 343
1 $teams = $new_program_node->get('field_srp_team_ref');
2 $new_teams = [];
3 foreach ($teams as $team) {
4 $new_teams[] = $team->target_id;
5 }
6 $new_teams[$new_program_vote_number] = $team_id;
7 $new_program_node->set('field_srp_team_ref', $new_teams);
1 /**
2 * Smart multi value field setter.
3 *
4 * Example calls:
5 *
6 * Set the index 2 to incomplete, keep old values:
7 * smartMultiValueFieldSetter($node, 'field_srp_voting_status', 'incomplete', 2);
8 *
9 * Set the index 1 to incomplete, overwrite the old values to 'placeholder'
10 * smartMultiValueFieldSetter($node, 'field_srp_voting_status', 'incomplete', 1,\
11 'placeholder', TRUE);
12 *
13 *
14 * @param \Drupal\node\Entity\Node $node
15 * Node.
16 * @param string $field_name
17 * Field name.
18 * @param string $value
19 * Value to be put in $node->field[$index]->value.
20 * @param int $index
21 * The delta i.e. $node->field[$index]
22 * @param string $default_value
23 * The default values that will be written into the previous indexes.
24 * @param bool $overwrite_old_values
Nodes and Fields 344
25 * TRUE to ignore previous index values and overwrite them with $default_value.
26 */
27 public static function smartMultiValueFieldSetter(Node $node, string $field_name, \
28 string $value, int $index, string $default_value="", bool $overwrite_old_values=FALS
29 E) {
30 $old_values = $node->get($field_name)->getValue();
31
32 // Grab old values and put them into $new_values array.
33
34 $field_type = $node->get($field_name)->getFieldDefinition()->getType();
35 if ($field_type == 'entity_reference') {
36 foreach ($old_values as $key=>$old_value) {
37 $new_values[$key] = $old_values[$key];
38 }
39 }
40 else {
41 $new_values = [];
42 foreach ($old_values as $old_value) {
43 $new_values[]["value"] = $old_value["value"];
44 }
45 }
46
47 // Ignore what was in the old values and put my new default value in.
48 if ($overwrite_old_values) {
49 for ($i = 0; $i < $index; $i++) {
50 $new_values[$i] = $default_value;
51 }
52 }
53
54 // Pad missing items.
55 for ($i = 0;$i<$index; $i++) {
56 if (!isset($new_values[$i])) {
57 if ($field_type == 'entity_reference') {
58 $new_values[$i] = $default_value;
59 }
60 else {
61 $new_values[$i]['value'] = $default_value;
62 }
63 }
64 }
65
66 if ($field_type == 'entity_reference') {
67 $new_values[$index]['target_id'] = $value;
Nodes and Fields 345
68 }
69 else {
70 $new_values[$index]["value"] = $value;
71 }
72
73 // Trim off extras from testing.
74 // TODO: this isn't trimming correctly for entity ref fields.
75 if (count($new_values)>($index+1)) {
76 $chunk = array_chunk($new_values, $index+1);
77 $new_values = $chunk[0];
78 }
79
80 $node->set($field_name, $new_values);
81 }
Here is an example of using the above function to write values to the field_condiment which is a
multivalue text field.
1 $node = Node::load(35);
2
3 // Write to index 0, 1, 2.
4 self::smartMultiValueFieldSetter($node, 'field_condiment', 'ketchup', 0);
5 self::smartMultiValueFieldSetter($node, 'field_condiment', 'mayo', 1);
6 self::smartMultiValueFieldSetter($node, 'field_condiment', 'mustard', 2);
7 $node->save();
8
9
10 // Write into index position 2 and pad the previous values with "dummy".
11 self::smartMultiValueFieldSetter($node, 'field_condiment', 'mustard', 2, 'dummy', TR\
12 UE);
13 $node->save();
14
15 // Write into index position 1 and pad position 0 with "dummy", and remove the conte\
16 nts of index 2.
17 self::smartMultiValueFieldSetter($node, 'field_condiment', 'ketchup', 1, 'dummy', TR\
18 UE);
19 $node->save();
Here is a complete function from the controller GeneralController.php. There are a wide variety of
calls to smartMultiValueFieldSetter() showing it’s use with multivalue text and entity-reference
fields (including a taxonomy field):
Nodes and Fields 346
87 // Returns FieldItemList.
88 $data = $node->get($field_name);
89 $data = $node->field_condiment;
90 // Returns simple array.
91 $data = $node->get($field_name)->getValue();
92 $data = $node->field_condiment->getValue();
93
94 // Multivalue entity reference field.
95 $field_name = 'field_event';
96 $data = $node->get($field_name)->getValue();
97 $data = $node->field_event;
98
99 // Multivalue taxonomy entity reference field.
100 $field_name = 'field_category';
101 $data = $node->get($field_name)->getValue();
102 // Yes, you can use a variable for a magic field getter!
103 $data = $node->$field_name;
104
105 // Loop thru results.
106 $items = $node->field_condiment;
107 foreach ($items as $item) {
108 $x = $item->value;
109 }
110
111 // get array of results.
112 $condiments = $node->get('field_condiment');
113 $vote_number = 1;
114 if (isset($condiments[$vote_number])) {
115 $result = $condiments[$vote_number]->value;
116 }
117
118 $result = $node->get('field_condiment')[0]->value;
119 $result = $node->get('field_condiment')[5]->value;
120 if (isset($node->get('field_condiment')[2]->value)) {
121 $result = $node->get('field_condiment')[2]->value;
122 }
123
124 $render_array['content'] = [
125 '#type' => 'item',
126 '#markup' => $str,
127 ];
128
129 return $render_array;
Nodes and Fields 349
130 }
1 $entity->hasField('abc');
e.g. Here we load a field field_library_media from a node, grab it’s target id (which we happen
to know is a media entity.). We load the media entity and check if there is a field called field_-
media_document. This rather convoluted example is used to get the file size of the file in the media
field.
1 use Drupal\media\Entity\Media;
2 use Drupal\file\Entity\File;
3
4 $media_id = $node->field_library_media->target_id;
5 if ($media_id) {
6 $media_item = Media::load($media_id);
7 // Get the file.
8 if ($media_item->hasField('field_media_document')) {
9 $file_id = $media_item->field_media_document->getValue()[0]['target_id'];
10 }
11 if (isset($file_id)) {
12 $file = File::load($file_id);
13 if ($file) {
14 // Get file size.
15 $file_size = format_size($file->getSize());
16 // Set file size variable.
17 $variables['file_size'] = $file_size;
18 }
19 }
20 }
1 $node = Node::load($nid);
2 if ($node) {
3 $id = $node->field_banner_image->target_id;
4 $media = Media::load($id);
5 //Not always correct
6 //$fid = $media->field_media_image->target_id;
7 //Better way:
8 $fid = $media->getSource()->getSourceFieldValue($media);
9 $file = File::load($fid);
10 if ($file) {
11 $uri = $file->getFileUri();//uri e.g. public://2020-12/atom.jpg
12
13 $media_url = file_create_url($uri); //Full URL of uploaded image.
14 //E.g. https://dir.ddev.site/sites/default/files/2020-12/atom.jpg
15 }
16 \Drupal::messenger()->addMessage("field_banner image url is $media_url");
17 $variables['program_area_banner_image_url'] = $media_url;
18 }
1 $file = File::load($fid);
2 if ($file) {
3 $uri = $file->getFileUri(); //returns drupal filename e.g. public://2020-12/atom.j\
4 pg
5
6 $file_url = file_create_url($uri); //returns absolute file url e.g. https://...fil\
7 e.jpg
8
9 //also
10 $absolute_file_url = $file->createFileUrl(FALSE); //returns absolute file path (ur\
11 l)
12
13 $relative_file_url = $file->createFileUrl(TRUE); //returns relative file path (url)
1 $file = $para->field_materials_file;
2 $filename = $file->entity->getFileName();
3 $uri = $file->entity->getFileUri();
Since File is an entity, we can also look in EntityBase.php to find more useful functions like id(),
label(), bundle().
1 $link = $para->field_link;
2 $link_uri = $para->field_link->uri;
Or
1 $current_url = $correction->get('field_link')->uri;
Or
In a .module file, first() returns a Drupal\link\Plugin\FieldType\LinkItem
1 if ($sf_contract) {
2 $vendor_url = $sf_contract->field_vendor_url->first();
3 if ($vendor_url) {
4 // this works too: $vendor_url = $vendor_url->uri;
5 $vendor_url = $vendor_url->getUrl(); // returns a Drupal\Core\Url.
6 $vendor_url = $vendor_url->toString();
7 _add_single_metatag("vendor_url", $vendor_url, $variables);
8 }
Leaving off the ->first() (like this) returns a Drupal\Core\Field\FieldItemList which is a list of
fields so you then would have to pull out the first field and extract the uri out of that.
1 $vendor_url = $sf_contract->field_vendor_url;
1 $entity->hasField('abc');
e.g. Here we load a field field_library_media from a node, grab it’s target id (which we happen
to know is a media entity.). We load the media entity and check if there is a field called field_-
media_document. This rather convoluted example is used to get the file size of the file in the media
field.
1 $media_id = $node->field_library_media->target_id;
2 if ($media_id) {
3 $media_item = Media::load($media_id);
4 // Get the file.
5 if ($media_item->hasField('field_media_document')) {
6 $file_id = $media_item->field_media_document->getValue()[0]['target_id'];
7 }
8 if (isset($file_id)) {
9 $file = File::load($file_id);
10 if ($file) {
11 // Get file size.
12 $file_size = format_size($file->getSize());
13 // Set file size variable.
14 $variables['file_size'] = $file_size;
15 }
16 }
17 }
1 use \Drupal\node\Entity\Node;
2 use \Drupal\file\Entity\File;
3
4 // Create file object from remote URL.
5 $data = file_get_contents('https://www.drupal.org/files/druplicon.small_.png');
6 $file = file_save_data($data, 'public://druplicon.png', FILE_EXISTS_REPLACE);
7
8 // Create node object with attached file.
9 $node = Node::create([
10 'type' => 'article',
11 'title' => 'Druplicon test',
12 'field_image' => [
13 'target_id' => $file->id(),
14 'alt' => 'Hello world',
15 'title' => 'Goodbye world'
16 ],
17 ]);
18 $node->save();
To populate the fields of an entity you can either use the $entity->set($key, $value) method on
the entity object or pass a key=>value array to the entity constructor. As such:
Nodes and Fields 354
1 //DateTime:
2 $dateTime = \DateTime::createFromFormat('Y-m-d','2000-01-30');
3 $newDateTimeString = $dateTime->format('Y-m-d\TH:i:s');
4
5 // Create node object
6 $node = Node::create([
7 'type' => 'workshop',
8 'title' => 'selwyn test' . (string) random_int(1,200),
9 'field_workshop_start_date' => ['2017-12-05',], //date only
10 'field_workshop_end_date' => [$newDateTimeString,], //DateTime
1 $node->set('field_tks_subchapter', $subchapterFullStatement);
2 $node->set('field_tks_standard_type', "TEKS");
3 $node->save();
1 $node->field_tks_subchapter = $subchapterFullStatement;
2 $node->field_tks_standard_type = “TEKS";
3 $node->save();
Also
1 $node->field_tks_subchapter->value = $subchapterFullStatement;
2 $node->field_tks_standard_type->value = “TEKS";
3 $node->save();
1 //The multivalue fields are no different. You just have to use arrays. So, instead o\
2 f this:
3 $entity->field_name_muti->value = ['foo', 'bar', 'baz'];
4 $entity->field_name_multi->target_id = [1, 2, 3]
5 $entity->field_name_multi->target_id = [$another_entity1, $another_entity2, $another\
6 _entity3]
7
8 // You can use this:
9
10 $entity->field_name_muti = ['foo', 'bar', 'baz'];
11 $entity->field_name_multi = [1, 2, 3]
12 $entity->field_name_multi = [$another_entity1, $another_entity2, $another_entity3]
Also if you just want to update a specific one, specify the delta like this
Or with variables...
1 $expectation_node->set('field_srp_teacher_voting_status',[$current_vote_number => $\
2 voting_status]);
1 $node->set('field_subtitle', NULL);
1 $this_node->body[$this_node->language] = [];
1 $node = Node::load(35);
2 // Get an array of nodes.
3 $node_array = $node->get('field_event')->referencedEntities();
4 // Pop one off with reset.
5 $ref_node = reset($node_array);
6 $nid = $ref_node->id();
7 $title = $ref_node->getTitle();
As $ref_node is a node object, you retrieve any field value with get() e.g. $val = $ref_-
node->get('field_status')->value;
Often, you’ve just loaded the node with Node::load() so you have a node object and refer to
reference fields like:
1 $vendor_url = $node->field_sf_contract_ref->entity->field_vendor_url->value;
1 if ($node) {
2 $topics = $node->get('field_ref_tax')->referencedEntities();
3 foreach ($topics as $topic) {
4 $term_name = $topic->getName();
5 }
6 }
1 $vendor_url = $node->field_sf_contract_ref->entity->field_vendor_url->first()->getUr\
2 l();
3 if ($vendor_url) {
4 $vendor_url = $vendor_url->getUri();
5 //OR
6 $vendor_url = $vendor_url->toString();
7 }
1 $citation_link = $citation->get('field_link');
2 if (!$citation_link->isEmpty()) {
3 $citation_link = $citation->field_link->first()->getUrl()->toString();
4 }
1 $instructions_node = Node::load($order_type_instructions_nid);
2 if ($instructions_node) {
3 $order_link = $instructions_node->field_link->first();
4 if ($order_link) {
5 $uri = $order_link->uri;
6 $variables['order_link_title'] = $order_link->title;
7 $order_url = $order_link->getUrl();
8 if ($order_url) {
9 $variables['order_type_link'] = $order_url;
10 }
11 }
12 }
1 $video_collection_node = Node::load($video_collection_nid);
2
3 //This gives you a bunch of \Drupal\entity_reference_revisions\EntityReferenceRevisi\
4 onsFieldItemList items
5 $lessons = $video_collection_node->field_related_lessons;
6 //or
7 $lessons = $video_collection_node->get('field_related_lessons');
8
9 foreach ($lessons as $lesson) {
10 $paragraph_revision_ids[] = $lesson->target_revision_id;
11 }
12
13 Paragraphs use the contributed Entity Reference Revisions module to reference
14 paragraphs and it is very important to use the `target_revision_id` property
15 when referencing paragraphs. Alternatively, the `entity` computed property can
16 be used to retrieve the paragraph entity itself.
17
18 OR
Nodes and Fields 361
19
20 //This gives you an array of arrays [['target_id' => '348','target_revision_id' => '\
21 348'],['target_id' => '349','target_revision_id' => '349'] ]
22 $lessons = $video_collection_node->get('field_related_lessons')->getValue();
23 foreach ($lessons as $lesson) {
24 $paragraph_revision_ids[] = $lesson['target_revision_id'];
25 }
26
27 Collecting them like this is only an example, while the `loadMultiple` method
28 exists on entity storage objects, there is no `loadMultipleRevisions` method.
29
30 EH!
31
32 // This gives you null! - don't do this.
33 $lessons = $video_collection_node->get('field_related_lessons')->value;
Note. getValue() here will get you the nid buried in a result array of arrays like
result[0]['target_revision_id'] - quicker to just grab ->target_revision_id
1 ## Load a node and grab a paragraph field to find the nid in an entity reference fie\
2 ld
3
4 From
5 /Users/selwyn/Sites/inside-mathematics/themes/custom/danaprime/danaprime.theme -
6 Continuing from above, I load a node, grab it's field
7 `field_related_lessons` which holds paragraphs of type `related_lessons`
8 and grab it's field `field_lesson.` That field has a target_id which is
9 the nid for the entity reference field. Phew!
10
11 ```php
12 //Grab the related lessons from the collection.
13 $video_collection_node = Node::load($video_collection_nid);
14 $lessons = $video_collection_node->field_related_lessons;
15 $storage = \Drupal::entityTypeManager()->getStorage('paragraph');
16 foreach ($lessons as $lesson) {
17 //Load each paragraph and get the nids from them.
18 $paragraph = $lesson->entity;
19 $related_lessons_nid = $paragraph->field_lesson->target_id;
20 $related_lessons_nids[] = $related_lessons_nid;
21 }
22
23 //This should have an array of nids for video_details.
Nodes and Fields 362
24 $variables['related_lessons_pids'] = $pids;
25 $variables['related_lessons_nids'] = $related_lessons_nids;
1 $node = Node::load(1234)
2 $url_object = $node->toUrl();
1 $href = $url_object->toString();
1 $href = $url_object->setAbsolute()->toString();
1 $current_path = \Drupal::service('path.current')->getPath();
1 $node_path = "/node/$nid";
2 $new_url = $parent_url_alias . $url_alias;
3
4 /** @var \Drupal\path_alias\PathAliasInterface $path_alias */
5 $path_alias = \Drupal::entityTypeManager()->getStorage('path_alias')->create([
6 'path' => $node_path,
7 'alias' => $new_url,
8 'langcode' => 'en',
9 ]);
10 $path_alias->save();
1 /**
2 * Tries to get node for given uuid if it exists.
3 *
4 * @param string $uuid
5 * UUID to search for existing node.
6 *
7 * @return \Drupal\Core\Entity\EntityInterface[]|null
8 * Node object or NULL.
9 *
10 * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
11 * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
12 */
13 public function getExistingUpdateNode(string $uuid) {
14 try {
15 $existing_node = \Drupal::entityTypeManager()
16 ->getStorage('node')
17 ->loadByProperties(['uuid' => $uuid]);
18 return $existing_node;
19 } catch (RequestException $e) {
20 watchdog_exception('ncs_infoconnect', $e);
21 return NULL;
22 }
23 }
1 //Node
2 $path = \Drupal::service('path.alias_manager')->getPathByAlias('/alias-path');
3 if (preg_match('/node\/(\d+)/', $path, $matches))
4 $nodeID = $matches[1];
5
6 //Taxonomy term
7 $path = \Drupal::service('path.alias_manager')->getPathByAlias('/alias-path');
8 if (preg_match('/taxonomy\/term\/(\d+)/', $path, $matches))
9 {
Nodes and Fields 365
10 $termID = $matches[1];
11 }
12
13 //User
14 $path = \Drupal::service('path.alias_manager')->getPathByAlias('/alias-path');
15 if (preg_match('/user\/(\d+)/', $path, $matches))
16 {
17 $userID = $matches[1];
18 }
1 \Drupal::service('page_cache_kill_switch')->trigger();
1 use Drupal\Core\Entity\EntityInterface;
2 use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
3
4 function drt_node_view_alter(array &$build, EntityInterface $entity, EntityViewDispl\
5 ayInterface $display) {
6 $bundle = $entity->bundle();
7 if ($bundle == 'search_home') {
8 $build['#cache']['max-age'] = 0;
9 \Drupal::service('page_cache_kill_switch')->trigger();
10 }
Note. The above custom module is called drt. You would use your own module name instead of drt
when naming the function.
1 $val = $contract_node->field_erate_certification->value;
2 if ($val) {
3 $erate_certification = "Yes";
4 }
1 $end_date = $contract_node->field_contract_end_date->value;
2 $end_date = $contract_node->field_contract_end_date->date->getTimestamp();
3 $end_date = $contract_node->field_contract_end_date->date;
4 $formatted_date = $end_date->format('m/d/y');
5
6 field_contract_end_date ->value; //returns whatever the string is e.g. 2024-08-31
7 field_contract_end_date->date->getTimestamp(); // returns unix timestamp e.g. 172510\
8 5600
9 field_contract_end_date->date; // returns a DrupalDateTime object with all it's good\
10 ness
11
12 $formatted_date = $end_date->format('m/d/y'); //Format the date nicely for output
From https://drupal.stackexchange.com/questions/252333/how-to-get-formatted-date-string-from-
a-datetimeitem-object
A date field has two properties,
1 $d = date("m/d/Y",$timestamp);
1 $date = DrupalDateTime::createFromDateTime($datetime);
2 $date = DrupalDateTime::createFromArray($array);
3 $date = DrupalDateTime::createFromTimestamp($timestamp);
4 $date = DrupalDateTime::createFromFormat($format, $time);
In the database the created and changed fields use a unix timestamp. e.g look in node_field_revision
This is an int 11 field in the db with a value like 1525302749 (Note. Negative values are dates before
1970 or epoch which require some slightly special magic)
If you add a Date field to a content type, it’s data looks like 2019-05-15T21:32:00 (varchar 20)
See the next item below for how to query the created and changed fields and also the fun you have
to go through to query date fields.
When writing Drupal dates to the database, you need to go thru some machinations. Here are some
details explain the timezone.
1 $date_string = "2020-08-24T15:28:04+00:00";
2 $tz = \Drupal::currentUser()->getTimezone();
3 // specify timezone as "America/Chicago"
4 $given = new \Drupal\Core\Datetime\DrupalDateTime($date_string, $tz);
5 // specify timezone as "UTC"
6 $given = new \Drupal\Core\Datetime\DrupalDateTime($date_string, "UTC");
7 // specify UTC as the timezone
8 $given->setTimezone(new \DateTimeZone("UTC"));
9 // The date string appears adjusted to UTC
10 $newstring = $given->format("Y-m-d\Th:i:s");
If you simply used {% raw %}{{ content }}{% endraw %} all fields are displayed correctly–timezone
is correct.
If you use {% raw %}{{ content.field_date_start }}{% endraw %}–timezone show correctly also
But I want to grab the time only to display separately from the date and put this in the twig template:
It fails.
From
The solution is to create a variable in the template_preprocess_node. Append ‘UTC’ to the datetime
string and make a timestamp so you get the UTC time (which matches what is in the Drupal DB.).
Grab the user’s timezone for conversion purposes. Use the Drupal date.formatter service to format
a timestamp (using the timezone) and you are good to go.
1 function txglobal_preprocess_node(&$variables) {
2 ..
3
4 if ( ($node_type == 'event') ) {
5
6 $node = $variables['node'];
7 $timezone = drupal_get_user_timezone();
8 $formatter = \Drupal::service('date.formatter');
9
10 $dates = $node->get('field_event_date')->getValue();
11
12 $date_start = $dates[0]['value'];
13 $time = strtotime($date_start.' UTC');
14 $start_time = $formatter->format($time, 'custom', 'g:ia', $timezone);
15 // $start_time = date("g:ia", $time);
16
17 $date_end = $dates[0]['end_value'];
18 $time = strtotime($date_end.' UTC');
19 $end_time = $formatter->format($time, 'custom', 'g:ia', $timezone);
20
21 // $date_start = new DateTime($date_start);
22 // $timestamp_start = $date_start->getTimestamp();
23 // $start_time = $formatter->format($timestamp_start, 'custom', 'g:ia', $timezone\
24 );
Nodes and Fields 370
Interestingly, if you use the smart_date module1 , you might use this version. Notice that dates are
already stored as timestamps so you don’t have to first convert them.
1 https://www.drupal.org/project/smart_date
Nodes and Fields 371
1 $node->get('field_cn_start_end_dates')->value
2 $node->get('field_cn_start_end_dates')->end_value
1 $node->get('field_cn_start_end_dates')->value
2
3 $node->set('field_cn_end_date', $end_date);
If you want to manipulate them, convert them to DrupalDateTime objects, then convert them back
to strings
Creating a new DrupalDateTime object is done like this:
1 use Drupal\Core\Datetime\DrupalDateTime;
2
3 $date = DrupalDateTime::createFromFormat('j-M-Y', '20-Jul-2019');
4
5 $date = new DrupalDateTime('now'); // grab current dateTime using NON static
6 $date->format('l, F j, Y - H:i'); // format it
7 // prints out nicely formatted version: Tue, Jul 16, 2019 - 11:34:am
8
9 $date = new DrupalDateTime('now'); // grab current dateTime
10 // Or print $date->format('d-m-Y: H:i A');
11 // prints out: 16-07-2019: 11:43 AM
The code below shows adding $days (an integer) to the date value retrieved from the field: field_-
cn_start_date.
Don’t forget to add the use statement.
1 use Drupal\Core\Datetime\DrupalDateTime;
2
3
4 $start_date_val = $node->get('field_cn_start_date')->value;
5 $days = intval($node->get('field_cn_suspension_length')->value) - 1;
6
7 //$end_date = DrupalDateTime::createFromFormat('Y-m-d H:i:s', $start_date_val . " 00\
8 :00:00");
9 $end_date = DrupalDateTime::createFromFormat('Y-m-d', $start_date_val );
10 $end_date->modify("+$days days");
11 $end_date = $end_date->format("Y-m-d");
12
13 $node->set('field_cn_end_date', $end_date);
1 date_default_timezone_set('Europe/London');
2
3 $d1 = new DateTime('2008-08-03 14:52:10');
4 $d2 = new DateTime('2008-01-03 11:11:10');
5 var_dump($d1 == $d2);
6 var_dump($d1 > $d2);
7 var_dump($d1 < $d2);
outputs:
1 bool(false)
2 bool(true)
3 bool(false)
1 use Drupal\Core\Datetime\DrupalDateTime;
2
3 $date_string = "2020-08-24T15:28:04+00:00";
4 $given = new DrupalDateTime($date_string);
5 $newstring = $given->format("Y-m-d\Th:i:s");
1 $source_node = $node_storage->load($nid);
2
3 $expiration_date = $source_node->field_expiration_date->value;
4
5 // Use expiration date to un-publish expired resellers to hide them.
6 $status = 1;
7 if ($expiration_date) {
8 $expirationDate = DrupalDateTime::createFromFormat('Y-m-d', $expiration_date);
9 $now = new DrupalDateTime();
10 if ($expiration_date < $now) {
11 $status = 0;
12 }
13 }
1 $node->get('field_cn_start_end_dates')->value
2
3 $node->set('field_cn_end_date', $end_date);
If you want to manipulate them, convert them to DrupalDateTime objects, then convert them back
to strings like this. $days here is an int and we are adding that to the date value. Don’t forget to add
the use statement.
Nodes and Fields 374
1 use Drupal\Core\Datetime\DrupalDateTime;
2
3
4 $start_date_val = $node->get('field_cn_start_date')->value;
5 $days = intval($node->get('field_cn_suspension_length')->value) - 1;
6
7 // $end_date = DrupalDateTime::createFromFormat('Y-m-d H:i:s', $start_date_val . " \
8 00:00:00");
9 $end_date = DrupalDateTime::createFromFormat('Y-m-d', $start_date_val );
10 $end_date->modify("+$days days");
11 $end_date = $end_date->format("Y-m-d");
12
13 $node->set('field_cn_end_date', $end_date);
Here you have a slightly funny date string, you make it into a DrupalDateTime and then format it
into a usable string you can store in the database.
1 use Drupal\Core\Datetime\DrupalDateTime;
2
3 $date_string = "2020-08-24T15:28:04+00:00";
4 $given = new \Drupal\Core\Datetime\DrupalDateTime($date_string);
5 $newstring = $given->format("Y-m-d\Th:i:s");
1 use Drupal\Core\Datetime\DrupalDateTime;
2
3
4 protected function loadFirstOpinionYear($term_id) {
5 $storage = \Drupal::entityTypeManager()->getStorage('node');
6 $query = \Drupal::entityQuery('node')
7 ->condition('status', 1)
8 ->condition('type', 'opinion')
9 ->condition('field_category', $term_id, '=')
10 ->sort('title', 'ASC') //DESC
11 ->range(0, 1);
12 $nids = $query->execute();
Nodes and Fields 375
13 if ($nids) {
14 $node = $storage->load(reset($nids));
15 }
16 $time = $node->get('created')->value;
17
18 $d = new DrupalDateTime("@$time"); //can use either this
19 $d = new \DateTime("@$time"); // or this..
20
21 $str = $d->format('Y-m-d H:i:s');
22 return $str;
23 }
1 use Drupal\Core\Datetime\DrupalDateTime;
2
3 protected function loadFirstOpinionYear($term_id) {
4 $storage = \Drupal::entityTypeManager()->getStorage('node');
5 $query = \Drupal::entityQuery('node')
6 ->condition('status', 1)
7 ->condition('type', 'opinion')
8 ->condition('field_category', $term_id, '=')
9 ->sort('title', 'ASC') //DESC
10 ->range(0, 1);
11 $nids = $query->execute();
12 if ($nids) {
13 $node = $storage->load(reset($nids));
14 }
15 $time = $node->get('created')->value;
16
17 $d = new DrupalDateTime("@$time"); //can use either this
18 $d = new \DateTime("@$time"); // or this..
19
20 $str = $d->format('Y-m-d H:i:s');
21 return $str;
22 }
Nodes and Fields 376
1 $start = $node->field_when->value;
2 $formatter = \Drupal::service('date.formatter');
3 $start_time = $formatter->format($start, 'custom', 'm/d/Y g:ia'); //12/21/2020 10:00\
4 am
5
6 Alternatively, you could load it, create a `DrupalDateTime` and then
7 format it
8
9 ```php
10 $start = $node->field_when->value;
11 $dt = DrupalDateTime::createFromTimestamp($start);
Nodes and Fields 377
1 $start_ts = $node->field_when->value;
2 $start_dt = DrupalDateTime::createFromTimestamp($start_ts);
3 $start_date = $start_dt->format('m/d/Y');
4 $duration = $node->field_when->duration; //1439 = all day
5 if ($duration == 1439) {
6 $start_time = "all day";
7 }
8 else {
9 $start_time = $start_dt->format('g:ia');
10 }
You can also peek into the repeating rule and repeating rule index. These are in the smart_date_rule
table and I believe the index identifies which item is in the instances column.
1 $rrule = $when->rrule;
2 $rrule_index = $when->rrule_index;
Nodes and Fields 378
22.63: hook_node_presave or
hook_entity_type_presave
When you want to fiddle with a node as it is being saved, use hook_node_presave()
Note there is some good date arithmetic here
1 /**
2 * Implements hook_ENTITY_TYPE_presave().
3 */
4 function ogg_mods_node_presave(NodeInterface $node) {
5 switch ($node->getType()) {
6 case 'catastrophe_notice':
7 $end_date = NULL != $node->get('field_cn_start_end_dates')->end_value ? $node-\
8 >get('field_cn_start_end_dates')->end_value : 'n/a';
9 $govt_body = NULL != $node->field_cn_governmental_body->value ? $node->field_c\
10 n_governmental_body->value : 'Unnamed Government Body';
11 $start_date_val = $node->get('field_cn_start_date')->value;
12
13 $accountProxy = \Drupal::currentUser();
14 $account = $accountProxy->getAccount();
15 // Anonymous users automatically fill out the end_date.
16 if (!$account->hasPermission('administer catastrophe notice')) {
17 $days = intval($node->get('field_cn_suspension_length')->value) - 1;
18
19 $end_date = DrupalDateTime::createFromFormat('Y-m-d', $start_date_val);
20 $end_date->modify("+$days days");
21 $end_date = $end_date->format("Y-m-d");
22 $node->set('field_cn_end_date', $end_date);
23 }
24
25 // Always reset the title.
26 $title = substr($govt_body, 0, 200) . " - $start_date_val";
27 $node->setTitle($title);
28
29 /*
30 * Fill in Initial start and end dates if this is an extension of
31 * a previously submitted notice.
32 */
33 $extension = $node->get('field_cn_extension')->value;
34 if ($extension) {
35 $previous_notice_nid = $node->get('field_cn_original_notice')->target_id;
Nodes and Fields 379
36 $previous_notice = Node::load($previous_notice_nid);
37 if ($previous_notice) {
38 $initial_start = $previous_notice->get('field_cn_start_date')->value;
39 $initial_end = $previous_notice->get('field_cn_end_date')->value;
40 $node->set('field_cn_initial_start_date', $initial_start);
41 $node->set('field_cn_initial_end_date', $initial_end);
42 }
43 }
44
45 break;
46 }
47 }
1 use Drupal\Core\Entity\EntityInterface;
2 use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
3
4
5 use Drupal\Core\Entity\EntityInterface;
6 use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
7
8 /**
9 * Implements hook_ENTITY_TYPE_view_alter().
10 */
11 function dir_node_view_alter(array &$build, EntityInterface $entity, EntityViewDispl\
12 ayInterface $display) {
13 $bundle = $entity->bundle();
14 if ($bundle == 'search_home') {
15 $build['#cache']['max-age'] = 0;
16 \Drupal::service('page_cache_kill_switch')->trigger();
17 }
18 }
where they check the view mode and dynamically add a field to the node. There is also a
post_render callback function added to do some more magic. To implement, you’d have to replace
ENTITY_TYPE with your entity type such as node. See below:
1 $aggregate_values = [
2 'citation_status' => $citation_status,
3 'accepted_votes' => $accepted_votes,
4 'rejected_votes' => $rejected_votes,
5 ];
6 $aggregate_data = Json::encode($aggregate_values);
7 $citation_node->field_srp_agg_data_json[$vote_number] = $aggregate_data;
8 $citation_node->save();
The JSON data is written into a multivalue long text field using the $vote_number index. So if
$vote_number is 0, this will be the first value. If it is 1, then this will be written into the second
value. This is what it looks like in Drupal on a node edit screen.
Nodes and Fields 381
22.67: Paragraphs
Paragraphs are those special things that allow you to blend a couple of field together e.g. count and
unit of measure so you can store values like 5 kilograms or 7 years etc. Often they are used like
Nodes and Fields 382
nodes where you define the fields and fill them with data that get displayed on the screen for things
like carousels or events.
1 use Drupal\taxonomy\Entity\Term;
2
3 foreach ($node->get('field_my_para')->referencedEntities() as $ent){
4 $term = Term::load($ent->$field_in_paragraph->target_id);
5 $name = $term->getName();
6 print_r($name);
7 }
1 <?php
2
3 namespace Drupal\ncs_infoconnect\Plugin\Field\FieldFormatter;
4
5 use Drupal\Core\Field\FormatterBase;
6 use Drupal\Core\Field\FieldItemListInterface;
7 use Drupal\Core\Form\FormStateInterface;
8 use Drupal\Core\Site\Settings;
9
10 /**
11 * Plugin implementation of the 'ncs_thumbnail' formatter.
12 *
13 * @FieldFormatter(
14 * id = "ncs_thumbnail",
15 * label = @Translation("NCS Thumbnail"),
16 * field_types = {
17 * "string"
18 * }
19 * )
20 */
21 class NcsThumbnailFormatter extends FormatterBase {
I override the settingsSummary() which is mostly informative, and viewElements() which has the
meat of the plugin. In viewElements(), we loop thru the items (i.e. the values coming in from the
field) and build an image_uri, jam each one into a render element and return the bunch.
Nodes and Fields 384
1 /**
2 * {@inheritdoc}
3 */
4 public function settingsSummary() {
5 $summary = [];
6 $summary[] = $this->t('Specify size of the thumbnail to display.');
7 return $summary;
8 }
9
10 /**
11 * {@inheritdoc}
12 */
13 public function viewElements(FieldItemListInterface $items, $langcode) {
14 $elements = [];
15 $markup = "";
16 $width = $this->getSetting('image_width');
17 $height = $this->getSetting('image_height');
18 $ncs_auth_settings = Settings::get('ncs_api_auth', []);
19 $base_url = $ncs_auth_settings['default']['imageserver'];
20
21 foreach ($items as $delta => $item) {
22 $image_uri = $base_url . "/?uuid=" . $item->value . "&function=original&type=t\
23 humbnail";
24 $markup = '<img src="' . $image_uri . '" width="' . $width . '" height="' . $h\
25 eight . '">';
26
27 // Render each element as markup.
28 $elements[$delta] = [
29 '#markup' => $markup,
30 ];
31 }
32
33 return $elements;
34 }
Note. Retrieving the config settings for a particular situation happens with a call to getSetting()
as in:
1 $width = $this->getSetting('image_width');
2 $height = $this->getSetting('image_height');
To use this we need to edit the display for the infofeed content type, make sure we have the image_-
uuid field displayed (i.e. not disabled) for Format, select NCS Thumbnail, click the gear to the right
Nodes and Fields 385
to specify the thumbnail size and save. Displaying nodes will then include the thumbnails.
You can do the same with a view: Add the field, specify the formatter (and dimensions) and the
thumbnail will appear.
22.70: Puzzles
1 $refs = $node_to_update->get('field_sf_account_ref')->referencedEntities();
2 if ($refs) {
3 $ref = reset($refs);
4 $node_to_update_sf_account_nid = $ref->id();
However, if I call ->first(), I get an EntityReferenceItem. I’m curious how I could use this–would
I want to?
11 $response = $client->get($uri);
12
13 // Returns a GuzzleHttp\Psr7\Stream.
14 $stream = $response->getBody();
15 $json_data = Json::decode($stream);
16
17 $help = $json_data['help'];
18 $success = $json_data['success'];
19 $result = $json_data['result'][0];
20
21 $msg = "<br>URI: " . $uri;
22 $msg .= "<br>Help: " . $help;
23 $msg .= "<br>Success: " . $success;
24 $msg .= "<br>Result: " . $result;
25
26 $build['content'] = [
27 '#type' => 'item',
28 '#markup' => $this->t($msg),
29 ];
30
31 return $build;
32 }
24 }
25
26 $build['content'] = [
27 '#type' => 'item',
28 '#markup' => $this->t($msg),
29 ];
30
31 return $build;
32 }
25
26 $build['content'] = [
27 '#type' => 'item',
28 '#markup' => $this->t($msg),
29 ];
30
31 return $build;
32 }
• BadResponseException.php
• ClientException.php - use this to handle a 4xx error
• ConnectException.php
• GuzzleException.php
• RequestException.php
• SeekException.php
• ServerException.php
• TooManyRedirectsException.php
• TransferException.php
24 ]);
25 } catch (RequestException $e) {
26 watchdog_exception('nard_myconnect', $e);
27 \Drupal::logger('nard_myconnect')
28 ->info("API error retrieving item $id with uuid=$uuid.");
29 // TODO: Deal with this error
30 $this->setUpdateStatus($uuid, 'error');
31 return FALSE;
32 }
33
34 // Retrieve the details from the API call.
35 try {
36 $response = $response->getBody()->getContents();
37 } catch (ClientException $e) {
38 watchdog_exception('nard_myconnect', $e);
39 \Drupal::logger('nard_myconnect')
40 ->info("API error retrieving body for item $id with uuid=$uuid.");
41 $this->setUpdateStatus($uuid, 'error');
42 return FALSE;
43 }
44
45 // Parse retrieved JSON into fields.
46 $response = json_decode($response, TRUE);
47 if (!is_array($response)) {
48 $response = [];
49 }
50
51 }
1 class DrupalHTTPClient {
2
3 public function initRequest($url, $headers = [],$content = "", $method = "POST", $\
4 msg = true, $type = "Third Party", $requestContentType = "json") {
5 try {
6
7 $client = \Drupal::httpClient();
8 $params = ['http_errors' => FALSE];
9 $params[$requestContentType] = $content;
10
11 $params = array_merge($headers,$params);
12
13 switch ($method) {
14 case 'POST':
15 $response = $client->post($url, $params);
16 break;
17 case 'PUT':
18 $response = $client->put($url, $params);
19 break;
20 case 'GET':
21 $response = $client->get($url);
22 break;
23 default:
24 $response = $client->post($url, $params);
25 break;
26 }
27 if ($response->getStatusCode() == '200') {
28 $data = $response->getBody()->getContents();
29 //\Drupal::messenger()->addMessage(serialize($data));
30 $result = json_decode($data, true);
31 if($msg) {
32 $setMessage = is_array($result) && isset($result['message']) ? $result['me\
33 ssage'] :
34 (is_string($data) ? $data : "Request is done successfully ");
35 \Drupal::messenger()->addMessage($type.":".$setMessage);
36 }
37 if(isset($result['exception'])){
38 $result_message = '';
39 foreach($result as $key => $value){
40 $result_message .= $key .': '.$value.', ';
41 }
42 \Drupal::logger('invalid_exception')->error(" \"Invalid Response: <i>`<str\
43 ong>".$method."</strong> ".$url."`</i> resulted in a <strong>`status:".$response->ge
Getting off the Island (formerly Reaching out of Drupal) 394
1 <?php
2
3 // Username or E-mail
4 $login = 'username';
5 // Password
6 $password = 'password';
7 // API Request
8 $url = 'https://example.com/api';
9 // POST data
10 $data = array('someTask', 24);
11 // Convert POST data to json
12 $data_string = json_encode($data);
13 // initialize cURL
14 $ch = curl_init();
15 curl_setopt($ch, CURLOPT_URL,$url);
16 curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
17 curl_setopt($ch, CURLOPT_USERPWD, "$login:$password");
18 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
19 curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
20 curl_setopt($ch, CURLOPT_HEADER, 1);
21 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
22
23 // Execute cURL and store the response in a variable
24 $file = curl_exec($ch);
25
26 // Get the Header Size
27 $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
28 // Get the Header from response
29 $header = substr($file, 0, $header_size);
30 // Get the Body from response
31 $body = substr($file, $header_size);
32 // Explode Header rows into an array
33 $header_items = explode("\n", $header);
34 // Close cURL handler
35 curl_close($ch);
36
37 // define new variable for the File name
38 $file_name = null;
39
40 // find the filname in the headers.
12 https://stackoverflow.com/questions/6177661/how-to-download-a-file-using-curl-in-php
Getting off the Island (formerly Reaching out of Drupal) 397
Also there were other examples on that page that are probably worth looking at.
Someone responded with this shorter paste in November 2020 at http://paste.debian.net/1170460/
with the following comment: this code is not downloading a file with PHP, it is PROXYING a file
with PHP, and it’s not doing a good job either, being way slower and more memory-hungry than
required, this script would do it much faster, benchmark it
Getting off the Island (formerly Reaching out of Drupal) 398
1 <?php
2
3 declare(strict_types=1);
4 $ch = curl_init();
5 curl_setopt_arrah($ch, [
6 CURLOPT_URL => 'http://example.org',
7 CURLOPT_HEADERFUNCTION => function ($ch, string $header): int {
8 $header_trimmed = trim($header);
9 if (strlen($header_trimmed) > 0) {
10 header($header, FALSE);
11 }
12 return strlen($header);
13 }
14 ]);
15 header('Content-Description: File Transfer');
16 header('Content-Type: application/octet-stream');
17
18 curl_exec($ch);
19 curl_close($ch);
23.12: Resources
• Guzzle docs https://docs.guzzlephp.org/en/stable/overview.html
• Guzzle project https://github.com/guzzle/guzzle
• Article at Drupalize.me by William Hetherington from December 2015 on using Drupal 8 to
speak http (using guzzle): https://drupalize.me/blog/201512/speak-http-drupal-httpclient
• Nice little code snippet from J M Olivas showing how to use dependency injection with Guzzle
at https://gist.github.com/jmolivas/ca258d7f2742d9e1aae4
• PSR-7: HTTP message interfaces describes common interfaces for representing HTTP messages
and URIs https://www.php-fig.org/psr/psr-7/
24: Queries
For most work, I use entityQueries. There are a few circumstances where I’ve needed to get into the
SQL which meant using static or dynamic queries. There are examples of these different techniques
below.
24.1: entityQuery
1 function getProductId($sku) {
2 $productId = false;
3 $query = \Drupal::entityQuery('node')
4 ->condition('type', 'ws_product')
5 ->condition('field_product_sku', $sku);
6
7 $nids = $query->execute();
8 if ($nids) {
9 $nid = array_values($nids);
10 $node = Node::load($nid[0]);
11 $productId = $node->get('field_product_id')->value;
12 }
13 return $productId;
14 }
14 }
15 elseif ($count_nodes > 0) {
16 $str = "Found $count_nodes articles";
17 }
18 $render_array['content'] = [
19 '#type' => 'item',
20 '#markup' => $str,
21 ];
22
23 return $render_array;
24 }
1 $query = \Drupal::entityQuery('node')
2 ->condition('type', 'contract')
3 ->condition('status', 1)
4 ->sort('title', 'ASC');
5 $nids = $query->execute();
6 $nid_count = count($nids);
7
8 // Grab 100 nids at a time to batch process.
9 $batches = [];
10 for ($i=0;$i<=$nid_count;$i+=100) {
11 $batches[] = array_slice($nids, $i, 100);
12 }
11 $end_date = $end_date->getTimestamp();
12
13 // $start_date->setTimezone(new \DateTimeZone(DateTimeItemInterface::STORAGE_TIME\
14 ZONE));
15 // $end_date->setTimezone(new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZO\
16 NE));
17 // $start_date = $start_date->format(DateTimeItemInterface::DATETIME_STORAGE_FORM\
18 AT);
19 // $end_date = $end_date->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
20
21 // Set the condition.
22 // $query->condition('field_date.value', $start_date, '>=');
23 // $query->condition('field_date.value', $end_date, '<=');
24
25
26 $query = \Drupal::entityQuery('node')
27 ->condition('status', 1)
28 ->condition('type', 'opinion')
29 ->condition('field_category', $term_id, '=')
30 ->condition('created', $start_date, '>=')
31 ->condition('created', $end_date, '<=')
32 ->sort('title', 'DESC');
33 $nids = $query->execute();
34 $titles = [];
35 if ($nids) {
36 $nodes = $storage->loadMultiple($nids);
37 foreach ($nodes as $node) {
38 $titles[]= $node->getTitle();
39 }
40 }
41 return $titles;
42
43 }
1 function park_academy_update_8002() {
2
3 $mids = \Drupal::entityQuery('menu_link_content')
4 ->condition('menu_name', 'park-wide-utility')
5 ->execute();
6
7 foreach($mids as $mid) {
8
9 $menu_link = \Drupal::entityTypeManager()->getStorage('menu_link_content')->load\
10 ($mid);
11
12 $title = $menu_link->getTitle();
13 if ($title === 'Support') {
14 $menu_link->set('weight',2);
15 $menu_link->set('expanded', TRUE);
16 // $menu_link->set('title','yomama');
17 $menu_link->set('link', 'https://www.google.com');
18 $menu_link->save();
19
20 }
21 }
22 }
1 $vote_number = 1;
2 $query = \Drupal::entityQuery('node')
3 ->condition('type', 'correlation', '=')
4 ->accessCheck(FALSE)
5 ->condition('field_program', $this->programNid, '=')
6 ->condition('field_voting_status.%delta', $vote_number, '=')
7 ->condition('field_voting_status.%delta.value', [
8 'accepted',
9 'rejected',
10 'incomplete'
11 ], 'IN');
12 $correlation_nids = $query->execute();
13 $correlation_nids = array_values($correlation_nids);
14 return $correlation_nids;
1 $query = \Drupal::entityQuery('node')
2 ->condition('type', 'srp_voting_record')
3 ->accessCheck(FALSE);
4 if ($vote_type == 'citation') {
5 // Check for empty entity reference field.
6 $query->notExists('field_ref_error_feedback');
7 }
8 if ($vote_type == 'feedback_error'){
9 // Check for filled entity reference field.
10 $query->exists('field_ref_error_feedback');
11 }
For querying for a user id, we query the field_voter.entity:user.uid value. See code below:
1 /**
2 * Converts imported eventlog item/s from UPDATE to ADD by uuid.
3 *
4 * @param string $uuid
5 * UUID for events to convert from UPDATE to ADD.
6 *
7 * @return mixed
8 * Results of update query.
9 */
10 public function convertUpdateToAddEvent(string $uuid) {
11 $update_connection = \Drupal::database();
12 $result = $update_connection->update('nocs_connect')
13 ->fields([
14 'event' => 'ADD',
15 ])
16 ->condition('uuid', $uuid)
17 ->condition('event', 'UPDATE')
18 ->condition('status', 'new')
19 ->execute();
20
21 return $result;
22 }
1 //use Drupal\Core\Database\Database;
2
3 public function updateQuery1() {
4 $database = \Drupal::database();
5 $query_string = "Update {donors} set amount=amount+1 where id<=10 ";
6 $affectedRows = $database->query($query_string,[],
7 ['return' => Database::RETURN_AFFECTED]);
8 $str = "Affected rows = $affectedRows";
9 $render_array['content'] = [
10 '#type' => 'item',
11 '#markup' => $str,
12 ];
13
14 return $render_array;
15 }
Queries 411
• insert() has each column specified as a separate entry in the fields array and the code can clean
each column value. query() has an SQL string with no way of checking individual columns.
• If you use query() with placeholders, the code can check the column values but placeholders
are just an option, there is no way to ensure your SQL does not contain values not passed
through placeholders.
• insert() passes the request through a set of hooks to let other modules check and modify your
requests. This is the right way to work with other modules.
• query() is slightly faster because query() does not pass the request through the hooks. You
might save processing time but your code will not let other modules help your code.
• insert() is more likely to work with other databases and future versions of Drupal.
16 ])
17 ->execute();
18 // Note. there is an auto-increment field so insert() returns the value
19 // for the new row in $result.
20 $str = "Single insert returned auto-increment value of $result";
21
22 // Multi-insert1.
23 $result = $connection->insert('donors')
24 ->fields(['name', 'amount',])
25 ->values(['name' => 'Multiton1', 'amount' => 11,])
26 ->values(['name' => 'Multiton1', 'amount' => 22,])
27 ->execute();
28 $str .= "<br/>Multi-insert1 added 2 rows";
29
30 // Multi-insert2.
31 $values = [
32 ['name' => 'Multiton2', 'amount' => 111,],
33 ['name' => 'Multiton2', 'amount' => 222,],
34 ['name' => 'Multiton2', 'amount' => 333,],
35 ['name' => 'Multiton2', 'amount' => 444,],
36 ['name' => 'Multiton2', 'amount' => 555,],
37 ];
38 $query = $connection->insert('donors')
39 ->fields(['name', 'amount',]);
40 foreach ($values as $record) {
41 $query->values($record);
42 }
43 $result = $query->execute();
44 $str .= "<br/>Multi-insert2 added 5 rows";
45
46 $render_array['content'] = [
47 '#type' => 'item',
48 '#markup' => $str,
49 ];
50
51 return $render_array;
52 }
More at https://www.drupal.org/docs/drupal-apis/database-api/insert-queries
1 use Drupal\Core\Database\Database;
2
3 public function deleteQuery2() {
4
5 $database = \Drupal::database();
6 $query_string = "Delete FROM {donors} where id>10 ";
7 $affectedRows = $database->query($query_string,[],['return' => Database::RETURN_AF\
8 FECTED]);
9
10 $str = "Affected rows = $affectedRows";
11 $render_array['content'] = [
12 '#type' => 'item',
13 '#markup' => $str,
14 ];
15
16 return $render_array;
17 }
1 function txg_preprocess_paragraph__simple_card(&$variables) {
2 $card_parent = $variables['paragraph']->getParentEntity();
3 if ($card_parent->bundle() == 'home_aof_card') {
4 $variables['parent_delta'] = 0;
5 $database = Database::getConnection();
6 $result = $database->query("
7 Select delta from node__field_para_aofs n where n.bundle = 'home_page' AND n.f\
8 ield_para_aofs_target_id = :target_id", [':target_id' => $card_parent->id()]
9 );
10 if ($result) {
11 while ($row = $result->fetchAssoc()) {
12 $variables['parent_delta'] = $row['delta'];
13 }
14 }
Queries 414
15 $parent_label = $card_parent->field_ref_aof->entity->label();
16 $parent_path = $card_parent->field_ref_aof->entity->toUrl()->toString();
17 $variables['parent_label'] = $parent_label;
18 $variables['parent_path'] = $parent_path;
19 }
20 }
1 function nocs_connect_schema() {
2 $schema['nocs_connect'] = [
3 'description' => 'Stores data from event log used to import/update content to si\
4 te.',
5 'fields' => [
6 'id' => [
7 'type' => 'int',
8 'not null' => TRUE,
9 'description' => 'Primary Key: Unique ID of event log event.',
10 ],
11 'uuid' => [
12 'type' => 'varchar',
13 'length' => 255,
14 'not null' => TRUE,
15 'description' => "Unique ID for content created or updated.",
16 ],
17 'nid' => [
18 'type' => 'varchar',
19 'length' => 255,
20 'not null' => FALSE,
21 'description' => "Unique ID for content created or updated.",
22 ],
23 'event' => [
24 'type' => 'varchar',
25 'length' => 255,
26 'not null' => TRUE,
27 'description' => 'Type of event e.g. UPDATE or ADD.',
28 ],
29 'created' => [
Queries 415
24.3: Reference
• API documentation for query conditions: https://api.drupal.org/api/drupal/core%21lib%
21Drupal%21Core%21Database%21Query%21Condition.php/class/Condition/9.3.x
• Entity query cheat sheet: https://www.metaltoad.com/blog/drupal-8-entity-api-cheat-sheet
• Static queries: https://www.drupal.org/docs/drupal-apis/database-api/static-queries
• Dynamic Queries: https://www.drupal.org/docs/8/api/database-api/dynamic-queries/
introduction-to-dynamic-queries
• Insert: https://www.drupal.org/docs/drupal-apis/database-api/insert-queries
• Querying date fields: https://blog.werk21.de/en/2018/02/05/date-range-fields-and-entity-
query-update and https://drupal.stackexchange.com/questions/198324/how-to-do-a-date-
range-entityquery-with-a-date-only-field-in-drupal-8
25: Redirects
25.1: Redirect to an internal url
In a controller when we return a RedirectResponse instead of a render array, Symfony redirects to
the URL specified in the RedirectResponse.
For example:
1 use \Symfony\Component\HttpFoundation\RedirectResponse;
2
3 return new RedirectResponse('node/1');
4
5 // Or to redirect to the front page of the site.
6
7 $url = Url::fromRoute('<front>');
8 return new RedirectResponse($url->toString());
Note. You can also redirect to a specific id (anchor) on the page by adding the fragment parameter
1 $form_state->setRedirect('tea_teks_admin.timeline_detail',
2 ['node'=>$parent_nid],
3 ['fragment' => 'milestone-' . $nid,]
4 );
Or here in submitForm():
Redirects 418
e.g.
1 Use Drupal\Core\Routing\TrustedRedirectResponse;
2
3 $absolute_url = 'https://google.com';
4 $response = new RedirectResponse($absolute_url, 301);
5 //OR
6 $response = new \Drupal\Core\Routing\TrustedRedirectResponse($absolute_url, 301);
7 $response->send();
8
9 exit(0);
1 $form_state->setRedirect('tra_teks_admin.timeline_detail',
2 ['node'=>$parent_nid],
3 ['fragment' => 'milestone-' . $nid,]
4 );
1 $form_state->setRedirectUrl(Url::fromRoute('tra_teks_srp.confidential_voting', [
2 'citadel_nid' => $citadel_nid,
3 'performance_nid' => $performance_nid,
4 'action' => 'vote',
5 'type' =>$type,
6 ]));
1 use Drupal\Core\Session\AccountInterface;
2 use Drupal\Core\Url;
3
4 /**
5 * Implements hook_user_login().
6 */
7 function greenacorn_user_login(AccountInterface $account) {
8 $roles = $account->getRoles();
9 $route_name = \Drupal::routeMatch()->getRouteName();
10 if ($route_name != 'user.reset.login' && in_array('client', $roles))
11 {
12 $destination = Url::fromUserInput('/my-issues')->toString();
13 \Drupal::service('request_stack')->getCurrentRequest()->query->set('destination'\
14 , $destination);
15 }
16 }
And here is the submit handler where the work gets done and you are redirected to /thanks-your-
submission.
Redirects 422
1 /**
2 * Submit handler to redirect user to thank-you page.
3 *
4 * @param $form
5 * @param \Drupal\Core\Form\FormStateInterface $form_state
6 */
7 function cn_submit_handler( $form, FormStateInterface $form_state) {
8 $url = Url::fromUri('internal:/thanks-your-submission');
9 $form_state->setRedirectUrl($url);
10 }
More at https://drupal.stackexchange.com/questions/163626/how-to-perform-a-redirect-to-
custom-page-after-node-save-or-delete
Then in the submitForm() method, once we’ve completed the work we needed to do, we can redirect
back to where we came from using the code below. We can optionally add a fragment which refers
to an ID on the page.
Redirects 423
1 $referrer_alias = $form_state->get('referrer_alias');
2
3 // Add the fragment so they drop back on the item they came from.
4 $url = Url::fromUri('internal:' . $referrer_alias, ['fragment' => "item_$feedbac\
5 k_error_nid"]);
6 $form_state->setRedirectUrl($url);
26: Render Arrays
26.1: Overview
Render Arrays are the building blocks of a Drupal page. A render array is an associative array which
conforms to the standards and data structures used in Drupal’s Render API. The Render API is also
integrated with the Theme API.
In many cases, the data used to build a page (and all parts of it) is kept as structured arrays until
the final stage of generating a response. This provides enormous flexibility in extending, slightly
altering or completely overriding parts of the page.
Render arrays are nested and thus form a tree. Consider them Drupal’s ”render tree” —- Drupal’s
equivalent of the DOM.
Note: While render arrays and arrays used by the Form API share elements, properties and structure,
many properties on form elements only have meaning for the Form API, not for the Render API.
Form API arrays are transformed into render arrays by FormBuilder. Passing an unprocessed Form
API array to the Render API may yield unexpected results.
Here is a simple render array that displays some text.
1 $my_render_array['some_item'] = [
2 '#type' => markup,
3 '#markup' => "This is a test",
4 ];
All forms are Render arrays. This is important when need to use the form API to create forms.
This is mostly from https://www.drupal.org/docs/drupal-apis/render-api/render-arrays
26.3: Caching
You can specify caching information when creating render arrays. Cache keys, cache contexts, cache
tags and cache max-age can all be defined.
The Drupal rendering process has the ability to cache rendered output at any level in a render array
hierarchy. This allows expensive calculations to be done infrequently, and speeds up page loading.
See the Cache API topic1 for general information about the cache system.
In order to make caching possible, the following information needs to be present:
• Cache keys: Identifiers for cacheable portions of render arrays. These should be created
and added for portions of a render array that involve expensive calculations in the rendering
process.
• Cache contexts: Contexts that may affect rendering, such as user role and language. When
no context is specified, it means that the render array does not vary by any context.
• Cache tags: Tags for data that rendering depends on, such as for individual nodes or user
accounts, so that when these change the cache can be automatically invalidated. If the
data consists of entities, you can use \Drupal\Core\Entity\EntityInterface::getCacheTags2 () to
generate appropriate tags; configuration objects have a similar method.
• Cache max-age: The maximum duration for which a render array maybe cached. Defaults
to \Drupal\Core\Cache\Cache::PERMANENT3 (permanently cacheable).
Cache information is provided in the #cache property in a render array. In this property, always
supply the cache contexts, tags, and max-age if a render array varies by context, depends on some
modifiable data, or depends on information that’s only valid for a limited time, respectively. Cache
keys should only be set on the portions of a render array that should be cached. Contexts are
automatically replaced with the value for the current request (e.g. the current language) and
combined with the keys to form a cache ID. The cache contexts, tags, and max-age will be propagated
up the render array hierarchy to determine cacheability for containing render array sections.
Here’s an example of what a #cache property might contain:
1 '#cache' => [
2 'keys' => ['entity_view', 'node', $node->id()],
3 'contexts' => ['languages'],
4 'tags' => $node->getCacheTags(),
5 'max-age' => Cache::PERMANENT,
6 ],
1 https://api.drupal.org/api/drupal/core%21core.api.php/group/cache/10.0.x
2 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Cache%21CacheableDependencyInterface.php/function/
CacheableDependencyInterface%3A%3AgetCacheTags/10.0.x
3 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Cache%21Cache.php/constant/Cache%3A%3APERMANENT/10.0.x
Render Arrays 426
26.4: Properties
Elements that start with # are properties and can include the following: #type, #theme, #markup,
#prefix, #suffix, #plain_text or #allowed_tags.
Render arrays (at any level of the hierarchy) will usually have one of the following properties defined:
• #type: Specifies that the array contains data and options for a particular type of ”render
element” (for example, ’form’, for an HTML form; ’textfield’, ’submit’, for HTML form element
types; ’table’, for a table with rows, columns, and headers). See Render elements4 below for
more on render element types.
• #theme: Specifies that the array contains data to be themed by a particular theme hook.
Modules define theme hooks by implementing hook_theme(), which specifies the input
”variables” used to provide data and options; if a hook_theme() implementation specifies
variable ’foo’, then in a render array, you would provide this data using property ’#foo’.
Modules implementing hook_theme() also need to provide a default implementation for each
of their theme hooks, normally in a Twig file. For more information and to discover available
theme hooks, see the documentation of hook_theme() and the Default theme implementations
topic.5
• #markup: Specifies that the array provides HTML markup directly. Unless the markup is
very simple, such as an explanation in a paragraph tag, it is normally preferable to use
#theme or #type instead, so that the theme can customize the markup. Note that the value
is passed through \Drupal\Component\Utility\Xss::filterAdmin6 (), which strips known XSS
vectors while allowing a permissive list of HTML tags that are not XSS vectors. (For example,
<script> and <style> are not allowed.) See \Drupal\Component\Utility\Xss7 ::$adminTags for
the list of allowed tags. If your markup needs any of the tags not in this list, then you
can implement a theme hook and/or an asset library. Alternatively, you can use the key
#allowed_tags to alter which tags are filtered.
• #plain_text: Specifies that the array provides text that needs to be escaped. This value takes
precedence over #markup.
• #allowed_tags: If #markup is supplied, this can be used to change which tags are allowed in
the markup. The value is an array of tags that Xss::filter() would accept. If #plain_text is set,
this value is ignored.
Usage example:
4 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21theme.api.php/group/theme_render/10.0.x#elements
5 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21theme.api.php/group/themeable/10.0.x
6 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Component%21Utility%21Xss.php/function/Xss%3A%3AfilterAdmin/10.0.x
7 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Component%21Utility%21Xss.php/class/Xss/10.0.x
Render Arrays 427
1 $output['admin_filtered_string'] = [
2 '#markup' => '<em>This is filtered using the admin tag list</em>',
3 ];
4 $output['filtered_string'] = [
5 '#markup' => '<video><source src="v.webm" type="video/webm"></video>',
6 '#allowed_tags' => [
7 'video',
8 'source',
9 ],
10 ];
11 $output['escaped_string'] = [
12 '#plain_text' => '<em>This is escaped</em>',
13 ];
JavaScript and CSS assets are specified in the render array using the #attached property (see At-
taching libraries in render arrays8 ).
From https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21theme.api.php/
group/theme_render/10.0.x
And
Or
You can make up your own properties e.g. see the #selwyn_id below for use elsewhere.
1 $citation_nid = 25;
2 $form['actions']['accept'] = [
3 '#type' => 'submit',
4 '#value' => $this->t("Accept Citation $citation_nid"),
5 '#citation_nid' => $citation_nid,
6 '#voting_action' => 'Accept',
7 '#name' => "accept_citation_$citation_nid",
8 '#selwyn_id' => ['edit-accept-' . $citation_nid],
9 '#attributes' => [
10 'class' => [
11 'hilited-button',
12 'blue-button',
8 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21theme.api.php/group/theme_render/10.0.x#sec_attached
Render Arrays 428
13 ],
14 'id' => ['edit-accept-' . $citation_nid],
15 ],
16 ];
26.5: Image
1 $image = [
2 '#theme'=>'image',
3 '#uri' => 'public://photo.jpg',
4 '#alt' => 'hello'
5 ];
1 $build['my_element'] = [
2 '#markup' => 'Something about @foo',
3 '#attached' => [
4 'placeholders' => [
5 '@foo' => ['#markup' => 'replacement'],
6 ],
7 ];
26.10: Date
To create a date object and return the year in a render array, use this code. Here is the code from a
block build method:
Render Arrays 430
26.11: Image
Load an image and display it with the alt text
1 use Drupal\Core\Url;
2
3
4 public function build(){
5 $result = $this->nodeStorage->getQuery()
6 ->accessCheck(TRUE)
7 ->condition('type', 'water_action')
8 ->condition('status', '1')
9 ->range(0, $this->configuration['block_count'])
10 ->sort('title', 'ASC')
11 ->execute();
12
13 if ($result) {
14 //Only display block if there are items to show.
15 $items = $this->nodeStorage->loadMultiple($result);
16
17 $build['list'] = [
18 '#theme' => 'item_list',
19 '#items' => [],
20 ];
21 foreach ($items as $item) {
22 $translatedItem = $this->entityRepository->getTranslationFromContext($item);
23 $nid = $item->id();
24 $url = Url::fromUri("internal:/node/$nid");
25
26 $build['list']['#items'][$item->id()] = [
27 '#title' => $translatedItem->label(),
28 '#type' => 'link',
29 '#url' => $url,
30 ];
31 }
1 $rArray = [
2 'first_para' => [
3 '#type' => 'markup',
4 '#markup' => '...para 1 here....<br>',
5 ],
6 'second_para' => [
7 '#type' => 'markup',
8 '#markup' => '...para 2 here....<br>',
9
10 ],
11 ];
12 return $rArray;
26.15: A link
Here is a simple link
use Drupal\Core\Url;
Render Arrays 433
1 $form['noaccount'] = [
2 '#type' => 'link',
3 '#title' => $this->t('Continue without account'),
4 '#url' => Url::fromRoute('<front>'),
5 ];
1 $form['button'] = [
2 '#type' => 'link',
3 '#url' => Url::fromUri('internal:/dashboard'),
4 '#title' => $this->t('Go to My Training'),
5 '#attributes' => ['class' => ['button', 'button-action', 'button--primary', 'butto\
6 n--small']],
7 ],
1 use Drupal\Core\Url;
2
3 $back_home_link = [
4 '#type' => 'link',
5 '#title' => $this->t('Continue without account'),
6 '#url' => Url::fromRoute('<front>'),
7 ];
8
9 $variables['back_home_link'] = $back_home_link
1 team_abc.correctional_voting:
2 path: '/team/abc/admin//program/{program}/expectation/{expectation}/correlation/{c\
3 orrelation}/{action}/{type}'
4 defaults:
5 _controller: '\Drupal\team_abc\Controller\CorrelationVotingController::content'
6 _title: 'Correctional Voting'
7 requirements:
8 _permission: 'manage voting process'
9 options:
10 parameters:
11 program:
12 type: entity:node
13 expectation:
14 type: entity:node
15 correlation:
16 type: entity:node
17 no_cache: 'TRUE'
Note the options in the routing.yml file which automatically convert the node ids to actual entities
(Drupal loads the nodes internally) and passes those to the controller.
Then in the controller, we build a URL, specifying the parameters:
1 $url = Url::fromRoute('team_abc.correctional_voting', [
2 'program' => $program->id(),
3 'expectation' => $next_breakout_path_item['expectation_nid'],
4 'correlation' => $next_breakout_path_item['correlation_nid'],
5 'action' => 'vote',
6 'type' => 'narrative'
7 ]);
8 $next_breakout = [
9 '#type' => 'link',
10 '#title' => t('Next Breakout'),
11 '#url' => $url,
12 ];
Render Arrays 435
13
14 // ...
15
16 $next_links[] = $next_breakout;
1 $content = [
2 // ...
3 'program' => $program_info,
4 'previous_links' => $previous_links,
5 'next_links' => $next_links,
6 'expectation_cfitem_text' => strip_tags($expectation_cfitem_text),
7 ];
8 return $this->buildDetails($content, $breadcrumbs, $management_links, $correlation\
9 _info, $citations);
10 }
Which wraps the content in an array for rendering in twig. Note below that the #theme property
which identifies the template filename. The #theme: team_abc__correctional_voting translates to
the twig template file: team-abc--correctional-voting.html.twig where the underscores become
dashes.
1 $content = [
2 '#theme' => 'item_list',
3 '#list_type' => 'ul',
4 '#title' => 'My List',
5 '#items' => ['item 1', 'item 2'],
6 '#attributes' => ['class' => 'mylist'],
7 '#wrapper_attributes' => ['class' => 'container'],
8 ];
1 $content['tabs'] = [
2 '#theme' => 'item_list',
3 '#list_type' => 'ul',
4 '#items' => [
5 [
6 '#type' => 'link',
7 '#title' => $this->t('My Plumbing Training'),
8 '#url' => Url::fromRoute('abc_academy.dashboard_tab', ['tab' => 'online'])
9 ],
10 [
Render Arrays 437
1 // Select element.
2 $options = [
3 '/node/360' =>'Above ground pool 1',
4 '/node/362' =>'Above ground pool 2',
5 '/node/364' =>'Underground pool',
6 '/node/359' =>'Patio pool',
7 ];
8
9 // Set the default value - it must be the key.
10 $default = '/node/364';
11 $form['select'] = [
12 '#type' => 'select',
13 '#title' => $this->t('Select video'),
14 '#description' => 'Test Description',
15 '#default_value' => $default,
16 '#options' => $options,
17 ];
1 $form['select'] = [
2 '#type' => 'select',
3 // '#title' => $this->t('Select video'),
4 '#description' => 'Test Description',
5 '#default_value' => $default,
6 '#options' => $options,
7 '#ajax' => [
8 'callback' => [$this, 'videoSelectChange'],
9 'event' => 'change',
10 'wrapper' => $ajax_wrapper,
11 ],
12 ];
13
14
15 /**
16 * Callback function for changes to the select elements.
17 *
18 */
19 public function videoSelectChange(array $form, FormStateInterface $form_state) {
20 $values = $form_state->getValues();
21 $elem = $form_state->getTriggeringElement();
22 $response = new AjaxResponse();
23 $url = Url::fromUri('internal:' . $values[$elem["#name"]]);
24 $command = new RedirectCommand($url->toString());
25 $response->addCommand($command);
26 return $response;
27 }
1 /**
2 * Implements hook_preprocess_menu().
3 */
4 function postal_theme_preprocess_menu(&$vars, $hook) {
5 if ($hook == 'menu__account') {
6 $items = $vars['items'];
7 foreach ($items as $key => $item) {
8 if ($key == 'user.page') {
9 $vars['items'][$key]['title'] = [
10 '#markup' => 'Log the <i>flock</i><sup>TM</sup> in!',
11 '#allowed_tags' => ['i'],
12 ];
13 }
14 if ($key == 'user.logout') {
15 $vars['items'][$key]['title'] = [
16 '#markup' => 'Log the <i>flock</i> <sup>TM</sup>out!',
17 '#allowed_tags' => ['i', 'sup'],
18 ];
19 }
20 }
21 }
22 }
1 $form['accept'] = [
2 '#type' => 'submit',
3 '#value' => $value,
4 '#name' => "accept_$feedback_error_nid",
5 '#voting_action' => 'accepted',
6 '#prefix' => '<div class="srp-vote-form">',
7 '#id' => 'edit-accept-' . $feedback_error_nid,
8 '#attributes' => [
9 'class' => [
10 'hilited-button',
11 'blue-button',
12 'accept-button',
13 ],
14 ],
Render Arrays 441
15 ];
16
17 if (strtolower($my_current_vote) == 'accepted') {
18 $form['accept']['#attributes']['class'][] = 'selected';
19 $form['reject']['#disabled'] = TRUE;
20 }
26.26: Resources
• Render API overview for Drupal 10 https://api.drupal.org/api/drupal/core%21lib%21Drupal%
21Core%21Render%21theme.api.php/group/theme_render/10.0.x
• Render Arrays from Drupal.org updated August 2022 https://www.drupal.org/docs/drupal-
apis/render-api/render-arrays
27: Routes and Controllers
27.1: Overview
27.1.1: Route
A route connects a URL path to a controller. In hello_world.routing.yml ( e.g. in
modules/custom/hello_world/hello_world.routing.yml) The path /hello maps to the controller
HelloWorldController and the member function: helloWorld(). When a user visits /hello,
Drupal checks to see that the user has access content permission and the helloWorld() function
is executed.
1 hello_world.hello:
2 path: '/hello'
3 defaults:
4 _controller: '\Drupal\hello_world\Controller\HelloWorldController::helloWorld'
5 _title: 'Our first route'
6 requirements:
7 _permission: 'access content'
27.1.2: Controller
This is the PHP function that takes info from the HTTP request and constructs and returns an HTTP
response (as a Symfony ResponseObject). The controller contains your logic to render the content
of the page.
The controller will usually return a render array but they can return an HTML page, an XML
document, a serialized JSON array, an image, a redirect, a 404 error or almost anything else.
A simple render array looks like this:
1 return [
2 '#markup' => 'blah',
3 ]
Routes and Controllers 443
1 return [
2 '#theme' => 'abc_teks_srp__correlation_voting',
3 '#content' => $content,
4 '#breadcrumbs' => $breadcrumbs,
5 '#management_links' => $management_links,
6 '#correlation' => $correlation_info,
7 '#citations' => $citations,
8 ];
In a module file, there is a hook_theme function which corresponds to the abc_teks_srp_theme and
identifies the template name as abc-teks-srp-correlation-voting. Here is the significant part of
the hook_theme() function
1 /**
2 * Implements hook_theme().
3 */
4 function abc_teks_srp_theme() {
5 $variables = [
6 'abc_teks_srp' => [
7 'render element' => 'children',
8 ],
9 'abc_teks_srp__correlation_voting' => [
10 'variables' => [
11 'content' => NULL,
12 'breadcrumbs' => NULL,
13 'management_links' => NULL,
14 'correlation' => NULL,
15 'citations' => NULL,
16 ],
17 'template' => 'abc-teks-srp--correlation-voting',
18 ],
1 # Since the parameters are passed to the function after the match, the
2 # function can do additional checking or make use of them before executing
3 # the callback function. The placeholder names "first" and "second" are
4 # arbitrary but must match the variable names in the callback method, e.g.
5 # "$first" and "$second".
6 page_example_arguments:
7 path: 'examples/page-example/arguments/{first}/{second}'
8 defaults:
9 _controller: '\Drupal\page_example\Controller\PageExampleController::arguments'
10 requirements:
11 _permission: 'access arguments page'
1 rsvp.form:
2 path: '/rsvplist'
3 defaults:
4 _form: 'Drupal\rsvp\Form\RSVPForm'
5 _title: 'RSVP to this Event'
6 requirements:
7 _permission: 'view rsvplist'
1 rsvp.admin_settings:
2 path: '/admin/config/content/rsvp'
3 defaults:
4 _form: 'Drupal\rsvp\Form\RSVPConfigurationForm'
5 _title: 'RSVP Configuration Settings'
6 requirements:
7 _permission: 'administer rsvplist'
8 options:
9 _admin_route: TRUE
1 requirements:
2 _user_is_logged_in: 'TRUE'
1 requirements:
2 _access: 'TRUE'
1 requirements:
2 _permission: 'administer rsvplist'
1 requirements:
2 _permission: 'vote on own squishy item+manage squishy process'
1 org_onions_summary:
2 path: 'onions/{term_id}'
3 defaults:
4 _controller: '\Drupal\org_onions\Controller\OnionsController::buildOnionsSummary'
5 # Static Title
6 # _title: 'Opinions Summary'
7 # Dynamic Title
8 _title_callback: '\Drupal\org_onions\Controller\OnionsController::getTitle'
9 requirements:
10 _permission: 'access content'
In your controller, add the function getTitle(). This function can actually be called whatever you
like.
1 /**
2 * Returns a page title.
3 */
4 public function getTitle() {
5 $current_path = \Drupal::service('path.current')->getPath();
6 $path_args = explode('/', $current_path);
7 $boss_name = $path_args[2];
8 $boss_name = ucwords(str_replace("-", " ", $boss_name));
9
10 $config = \Drupal::config('system.site');
Routes and Controllers 447
11 $site_name = $config->get('name');
12 return $boss_name . ' Onions | ' . $site_name;
13
14 //or
15 return $boss_name . ' onions | ' . \Drupal::config('system.site')->get('name');
16 }
1 requirements:
2 _permission: 'access content'
3 options:
4 no_cache: TRUE
1 general.book_example:
2 path: '/general/book_example'
3 defaults:
4 _title: 'Book Example'
5 _controller: '\Drupal\general\Controller\ExampleController::build'
6 requirements:
7 _permission: 'access content'
1 <?php
2
3 namespace Drupal\general\Controller;
4
5 use Drupal\Core\Controller\ControllerBase;
6
7 /**
8 * Returns responses for General routes.
9 */
Routes and Controllers 449
1 $ drush route
2 $ drush route --path=/user/1
3 $ drush route --name=update.status
4 $ sh route --url=https://example.com/node/1
more at https://www.drush.org/latest/commands/core_route/
1 $ drush route
2
3 '<button>': /
4 '<current>': /<current>
5 '<front>': /
6 '<nolink>': /
7 '<none>': /
8 admin_toolbar.run.cron: /run-cron
9 admin_toolbar.settings: /admin/config/user-interface/admin-toolbar
10 admin_toolbar_tools.cssjs: /admin/flush/cssjs
11 admin_toolbar_tools.flush: /admin/flush
12 admin_toolbar_tools.flush_menu: /admin/flush/menu
13 admin_toolbar_tools.flush_rendercache: /admin/flush/rendercache
14 admin_toolbar_tools.flush_static: /admin/flush/static-caches
15 admin_toolbar_tools.flush_twig: /admin/flush/twig
16 admin_toolbar_tools.flush_views: /admin/flush/views
17 admin_toolbar_tools.plugin: /admin/flush/plugin
18 admin_toolbar_tools.settings: /admin/config/user-interface/admin-toolbar-tools
19 admin_toolbar_tools.theme_rebuild: /admin/flush/theme_rebuild
20 batch_examples.batch: /batch-examples/batchform
21 ...
To which it replied:
1 https://openai.com/
Routes and Controllers 452
1 <?php
2
3 namespace Drupal\mymodule\Controller;
4
5 use Drupal\Core\Controller\ControllerBase;
6
7 class TemperatureController extends ControllerBase {
8
9 /**
10 * Display a table of temperatures.
11 */
12 public function displayTable() {
13 $rows = [];
14 for ($i = 0; $i <= 100; $i++) {
15 $rows[] = [
16 'celsius' => $i,
17 'fahrenheit' => $this->celsiusToFahrenheit($i),
18 ];
19 }
20 $header = [
21 ['data' => $this->t('Celsius'), 'field' => 'celsius'],
22 ['data' => $this->t('Fahrenheit'), 'field' => 'fahrenheit'],
23 ];
24 $build['table'] = [
25 '#type' => 'table',
26 '#header' => $header,
27 '#rows' => $rows,
28 '#empty' => $this->t('No temperatures to display.'),
29 ];
30 return $build;
31 }
32
33 /**
34 * Convert a temperature from Celsius to Fahrenheit.
35 *
36 * @param float $celsius
37 * The temperature in Celsius.
38 *
39 * @return float
40 * The temperature in Fahrenheit.
41 */
42 protected function celsiusToFahrenheit($celsius) {
43 return ($celsius * 9 / 5) + 32;
Routes and Controllers 453
44 }
45
46 }
1 To use this controller, you would need to define a route in your module's routing fi\
2 le that points to the displayTable method. For example:
1 mymodule.temperature_table:
2 path: '/temperature-table'
3 defaults:
4 _controller: '\Drupal\mymodule\Controller\TemperatureController::displayTable'
5 _title: 'Temperature Table'
6 requirements:
7 _permission: 'access content'
It concluded with:
1 This would define a route at the path /temperature-table that displays the table of \
2 temperatures when accessed.
27.12: Resources
• Introductory Drupal routes and controllers example from Drupal documentation updated
October 2022 https://www.drupal.org/docs/drupal-apis/routing-system/introductory-drupal-
routes-and-controllers-example
• Structure of Routes from Drupal documentation updated September 2022 https://www.drupal.
org/docs/drupal-apis/routing-system/structure-of-routes
• Symfonycasts.com excellent course in Drupal 8: https://symfonycasts.com/screencast/drupal8-
under-the-hood
• Controllers article from Drupalize.me updated December 2021 https://drupalize.me/topic/
controllers
28: Services and Dependency
Injection
28.1: Overview
Services provide a ”decoupled” way to access classes and members. Services are pluggable and
replaceable by registering them with a service container which makes them well suited to test with
PHPUnit tests.
Services can be accessed using two possible methods: static and injected (using dependency
injection).
28.2: Static
For .module files and classes which are not exposed to the service container (see below for a
definition of the service container), you have to use the static method of retrieving the service:
You can also get the container and use that to get a service (and then use the service) e.g.:
1 $container = \Drupal::getContainer()
2 $this->keyValue = $container->get('keyvalue')->get($collection);
Services and Dependency Injection 455
1 /*
2 * \Drupal::configFactory() retrieves the configuration factory.
3 *
4 * This is mostly used to change the override settings on the configuration
5 * factory. For example, changing the language, or turning all overrides on
6 * or off.
7 */
8
9 /**
10 * Implements hook_install().
11 */
12 function hello_world_install() {
13 $config = \Drupal::configFactory()->getEditable('system.mail');
14 $mail_plugins = $config->get('interface');
15 if (in_array('hello_world', array_keys($mail_plugins))) {
16 return; }
17 $mail_plugins['hello_world'] = 'hello_world_mail';
18 $config->set('interface', $mail_plugins);
19 $config->save();
20 }
1 $values = $form_state->getValues();
2 $address1 = $values['footer_address1'];
3 $address2 = $values['footer_address2'];
4
5 $config = \Drupal::configFactory()>getEditable('dat.header_footer_settings');
6 $config->set('footer_address1', $address1);
7 $config->set('footer_address2', $address2);
8 $config->save();
Here we use the config.factory service (via ::config() shorthand method) to load some values
from Drupal config:
1 $config = \Drupal::config('dat.header_footer_settings');
2
3 $address1 = $config->get('footer_address1');
4 $address2 = $config->get('footer_address2');
5 $email = $config->get('footer_email');
6 $logo_url = $config->get('logo_url');
Get the email address for the site using the config.factory service:
Services and Dependency Injection 457
1 // returns [email protected]
2 $to = \Drupal::configFactory()->getEditable('system.site')->get('mail');
This allows quick access from within your controllers to these services if you need to do things like:
1 // Make an entityQuery
2 $storage = $this->entityTypeManager()->getStorage('node');
3 $query = $storage->getQuery();
4 $query
5 ->accessCheck(TRUE)
6 ->condition('type', 'article')
7 ->condition('title', $name)
8 ->count();
9 $count_nodes = $query->execute();
10
11 // Or.
12
13 // Get info about the current user.
14 $account = $this->currentUser();
15 $username = $account->getAccountName();
16 $uid = $account->id();
17 $message = "<br>Account info user id: " . $uid . " username: " . $username;
Services and Dependency Injection 458
You must also add the extra parameters to the create() function and the constructor i.e. $plugin_id
and $plugin_definition e.g.
public static function create(ContainerInterface $container, array $configuration,
$plugin_id, $plugin_definition)
1 /**
2 *
3 * The CmAPIClient.
4 *
5 * @var \Drupal\cm_api\CmAPIClient
6 */
7 protected $cmAPIClient;
3. You need a create() function. This will get passed the $container so it can call it’s get() member
function to instantiate the service you need. This function then calls the constructor and passes it’s
parameters to it.
Note, you can pass multiple services by adding additional $container->get() calls like this:
1 /**
2 * ApiTestController constructor.
3 */
4 public function __construct(CmAPIClient $cmAPIClient) {
5 $this->cmAPIClient = $cmAPIClient;
6 }
Similarly, if you are getting multiple services, add the additional parameters to the constructor, as
well as assigning the variables. E.g.
Services and Dependency Injection 460
5. In your code, use the protected variable ($cmAPIClient) to call functions in the service:
Rejoice! Note. No need to make any routing changes. Drupal handles all the parameters with the
instructions provided. etc.
1 <?php
2
3 namespace Drupal\di_examples\Controller;
4
5 use Drupal\Core\Controller\ControllerBase;
6 use Drupal\Core\Session\AccountProxyInterface;
7 use Symfony\Component\DependencyInjection\ContainerInterface;
8
9 class DiExamplesController extends ControllerBase {
10
11 protected AccountProxyInterface $account;
12
13 /**
14 * Builds the response.
15 */
16 public function build() {
17
18 $account = $this->account->getAccount();
19 $username = $account->getAccountName();
20 $uid = $account->id();
21
22 $message = "<br>Account info user id: " . $uid . " username: " . $username;
23
24 $build['content'] = [
Services and Dependency Injection 461
1 <?php
2
3 namespace Drupal\di_examples\Controller;
4
5 use Drupal\Core\Controller\ControllerBase;
6 use Drupal\Core\Path\CurrentPathStack;
7 use Drupal\Core\Path\PathValidatorInterface;
8 use Drupal\Core\Session\AccountProxyInterface;
9 use Symfony\Component\DependencyInjection\ContainerInterface;
10
11 /**
12 * Returns responses for DI Examples routes.
13 */
14 class DiExamplesController extends ControllerBase {
15
16 protected AccountProxyInterface $account;
17 protected CurrentPathStack $pathStack;
18 protected PathValidatorInterface $pathValidator;
19
20 public static function create(ContainerInterface $container) {
21 return new static(
Services and Dependency Injection 462
22 $container->get('current_user'),
23 $container->get('path.current'),
24 $container->get('path.validator'),
25 );
26 }
27
28 public function __construct(AccountProxyInterface $account, CurrentPathStack $path\
29 _stack, PathValidatorInterface $path_validator) {
30 $this->account = $account;
31 $this->pathStack = $path_stack;
32 $this->pathValidator = $path_validator;
33 }
34
35
36 /**
37 * Builds the response.
38 */
39 public function build() {
40
41 // Use the injected account.
42 $account = $this->account->getAccount();
43
44 // Use the ControllerBase static version.
45 $account = $this->currentUser();
46
47 $username = $account->getAccountName();
48 $uid = $account->id();
49
50 $message = "<br>Account info user id: " . $uid . " username: " . $username;
51
52 $name = 'hello';
53
54 // Use the ControllerBase static version to create an entityQuery.
55 $storage = $this->entityTypeManager()->getStorage('node');
56 $query = $storage->getQuery();
57 $query
58 ->condition('type', 'article')
59 ->condition('title', $name)
60 ->count();
61 $count_nodes = $query->execute();
62 $message .= "<br>Retrieved " . $count_nodes . " nodes";
63
64 $path = $this->pathStack->getPath();
Services and Dependency Injection 463
This tells you to that the service is in core.services.yml and that it is implemented in the
EntityTypeManager class.
1 entity_type.manager:
2 class: Drupal\Core\Entity\EntityTypeManager
3 arguments: ['@container.namespaces', '@module_handler', '@cache.discovery', '@stri\
4 ng_translation', '@class_resolver', '@entity.last_installed_schema.repository']
5 parent: container.trait
6 tags:
7 - { name: plugin_manager_cache_clear }
1 /**
2 * Retrieves the entity type manager.
3 *
4 * @return \Drupal\Core\Entity\EntityTypeManagerInterface
5 * The entity type manager.
6 */
7 public static function entityTypeManager() {
8 return static::getContainer()->get('entity_type.manager');
9 }
1 $storage = \Drupal::entityTypeManager()->getStorage($entity_type);
2 $query = $storage->getQuery();
3 $query = \Drupal::entityQuery('node')
4 ->accessCheck(TRUE)
5 ->condition('type', 'page')
6 ->condition('status', 1);
7 $nids = $query->execute();
To do this using dependency injection you will need to inject entity_type.manager. Follow the
procedure outlined above in Controller details.
1 services:
2 kitchen_product.product_manager_service:
3 class: Drupal\kitchen_product\ProductManagerService
28.8.1: Arguments
You can specify optional arguments to pass to your service which might be needed when Drupal
instantiates your service. Here we pass the keyvalue service as well as a Boolean value:
1 services:
2 highway.road_generator:
3 class: Drupal\highway\RoadGenerator
4 arguments:
5 - '@keyvalue'
6 - "%highway.road.use_key_value_cache%"
1 parameters:
2 highway.road.use_key_value_cache: true
3
4 services:
5 dino_roar.roar_generator:
6 class: Drupal\dino_roar\Jurassic\RoarGenerator
7 arguments:
8 - '@keyvalue'
9 - "%highway.road.use_key_value_cache%"
This means that it is configurable. If you need to set it to false during development, you can easily
override it on your local machine. Simply add it to the development.services.yml file like this:
Note you can also pass strings in the form ’blah’ surrounded by single quotes.
Services and Dependency Injection 466
1 config.factory:
2 class: Drupal\Core\Config\ConfigFactory
3 tags:
4 - { name: event_subscriber }
5 - { name: service_collector, tag: 'config.factory.override', call: addOverride }
6 arguments: ['@config.storage', '@event_dispatcher', '@config.typed']
1 services:
2 hello_world.salutation:
3 class: Drupal\hello_world\HelloWorldSalutation
4 arguments: ['@config.factory']
1 services:
2 taxonomy_tree.taxonomy_term_tree:
3 class: Drupal\taxonomy_tree\TaxonomyTermTree
4 arguments: ['@entity_type.manager']
1 <?php
2
3 namespace Drupal\taxonomy_tree;
4
5 use Drupal\Core\Entity\EntityTypeManager;
6
7 /**
8 * Loads taxonomy terms in a tree
9 */
10 class TaxonomyTermTree {
11
12 /**
13 * @var \Drupal\Core\Entity\EntityTypeManager
14 */
15 protected $entityTypeManager;
16
17 /**
18 * TaxonomyTermTree constructor.
19 *
20 * @param \Drupal\Core\Entity\EntityTypeManager $entityTypeManager
21 */
22 public function __construct(EntityTypeManager $entityTypeManager) {
23 $this->entityTypeManager = $entityTypeManager;
24 }
25
26 /**
27 * Loads the tree of a vocabulary.
28 *
29 * @param string $vocabulary
30 * Machine name
31 *
32 * @return array
33 */
34 public function load($vocabulary) {
35 $terms = $this->entityTypeManager->getStorage('taxonomy_term')->loadTree($vocabu\
36 lary);
37 $tree = [];
38 foreach ($terms as $tree_object) {
39 $this->buildTree($tree, $tree_object, $vocabulary);
40 }
41
42 return $tree;
43 }
Services and Dependency Injection 468
44
45 /**
46 * Populates a tree array given a taxonomy term tree object.
47 *
48 * @param $tree
49 * @param $object
50 * @param $vocabulary
51 */
52 protected function buildTree(&$tree, $object, $vocabulary) {
53 if ($object->depth != 0) {
54 return;
55 }
56 $tree[$object->tid] = $object;
57 $tree[$object->tid]->children = [];
58 $object_children = &$tree[$object->tid]->children;
59
60 $children = $this->entityTypeManager->getStorage('taxonomy_term')->loadChildren(\
61 $object->tid);
62 if (!$children) {
63 return;
64 }
65
66 $child_tree_objects = $this->entityTypeManager->getStorage('taxonomy_term')->loa\
67 dTree($vocabulary, $object->tid);
68
69 foreach ($children as $child) {
70 foreach ($child_tree_objects as $child_tree_object) {
71 if ($child_tree_object->tid == $child->id()) {
72 $this->buildTree($object_children, $child_tree_object, $vocabulary);
73 }
74 }
75 }
76 }
77 }
3. Add a create() function to get the service(s) from the service container
4. Add a constructor which stores a link to each service so you can call functions in those services.
Note. Follow the slightly different steps for injecting services into blocks when using your service
for blocks or plugins.
28.10.1: Overview
Dependency injection is the practice of ”injecting” services. A service is any object managed by the
Drupal Service container.
Drupal introduces the concept of services to decouple reusable functionality and makes these services
pluggable and replaceable by registering them with a service container.
It is best practice to access any of the services provided by Drupal via the service container to ensure
the decoupled nature of these systems is respected.
Services are used to perform operations like accessing the database or sending an e-mail. Rather
than use PHP’s native MySQL functions, we use the core-provided service via the service container
to perform this operation so that our code can simply access the database without having to worry
about whether the database is MySQL or SQLlite, or if the mechanism for sending e-mail is SMTP
or something else.
From https://www.drupal.org/docs/drupal-apis/services-and-dependency-injection/services-and-
dependency-injection-in-drupal-8.
1 email.validator:
2 class: Drupal\Component\Utility\EmailValidator
Looking in the EmailValidator.php file, there is an isValid() function which you can call to validate
email addresses. E.g.
1 https://symfony.com/doc/current/service_container.html
Services and Dependency Injection 470
1 $this->emailValidator->isValid()
1 current_route_match:
2 class: Drupal\Core\Routing\CurrentRouteMatch
3 arguments: ['@request_stack']
1 $this->currentRouteMatch->getRouteName()
1 <?php
2
3 namespace Drupal\di_examples\Controller;
4
5 use Drupal\Core\Controller\ControllerBase;
6 use Drupal\Core\Session\AccountProxyInterface;
7 use Symfony\Component\DependencyInjection\ContainerInterface;
8
9 class DiExamplesController extends ControllerBase {
10
11 protected AccountProxyInterface $account;
12
13 /**
14 * Builds the response.
15 */
16 public function build() {
17
18 $account = $this->account->getAccount();
19 $username = $account->getAccountName();
20 $uid = $account->id();
21
22 $message = "<br>Account info user id: " . $uid . " username: " . $username;
Services and Dependency Injection 471
23
24 $build['content'] = [
25 '#type' => 'item',
26 '#markup' => $this->t($message),
27 ];
28
29 return $build;
30 }
31
32 public static function create(ContainerInterface $container) {
33 return new static($container->get('current_user'));
34 }
35
36 public function __construct(AccountProxyInterface $account) {
37 $this->account = $account;
38 }
39
40 }
1 <?php
2
3 namespace Drupal\di_examples\Controller;
4
5 use Drupal\Core\Controller\ControllerBase;
6 use Drupal\Core\Path\CurrentPathStack;
7 use Drupal\Core\Path\PathValidatorInterface;
8 use Drupal\Core\Session\AccountProxyInterface;
9 use Symfony\Component\DependencyInjection\ContainerInterface;
10
11 /**
12 * Returns responses for DI Examples routes.
13 */
14 class DiExamplesController extends ControllerBase {
15
16 protected AccountProxyInterface $account;
17 protected CurrentPathStack $pathStack;
18 protected PathValidatorInterface $pathValidator;
19
Services and Dependency Injection 472
You must also add the extra parameters to the create() and __construct() function i.e. $plugin_id
and $plugin_definition e.g.
Here is an example of a block constructor with the AccountProxyInterface parameter added as this
is the service we want to inject:
Services and Dependency Injection 474
See more about using dependency injection for blocks and other plugins at https://chromatichq.com/
blog/dependency-injection-drupal-8-plugins
1 /**
2 * Static Service Container wrapper.
3 *
4 * Generally, code in Drupal should accept its dependencies via either
5 * constructor injection or setter method injection. However, there are cases,
6 * particularly in legacy procedural code, where that is infeasible. This
7 * class acts as a unified global accessor to arbitrary services within the
8 * system in order to ease the transition from procedural code to injected OO
9 * code.
10 *
11 * The container is built by the kernel and passed in to this class which stores
12 * it statically. The container always contains the services from
13 * \Drupal\Core\CoreServiceProvider, the service providers of enabled modules and an\
14 y other
15 * service providers defined in $GLOBALS['conf']['container_service_providers'].
16 *
17 * This class exists only to support legacy code that cannot be dependency
18 * injected. If your code needs it, consider refactoring it to be object
19 * oriented, if possible. When this is not possible, for instance in the case of
20 * hook implementations, and your code is more than a few non-reusable lines, it
21 * is recommended to instantiate an object implementing the actual logic.
22 *
23 * @code
24 * // Legacy procedural code.
25 * function hook_do_stuff() {
26 * $lock = lock()->acquire('stuff_lock');
27 * // ...
28 * }
Services and Dependency Injection 475
29 *
30 * // Correct procedural code.
31 * function hook_do_stuff() {
32 * $lock = \Drupal::lock()->acquire('stuff_lock');
33 * // ...
34 * }
35 *
36 * // The preferred way: dependency injected code.
37 * function hook_do_stuff() {
38 * // Move the actual implementation to a class and instantiate it.
39 * $instance = new StuffDoingClass(\Drupal::lock());
40 * $instance->doStuff();
41 *
42 * // Or, even better, rely on the service container to avoid hard coding a
43 * // specific interface implementation, so that the actual logic can be
44 * // swapped. This might not always make sense, but in general it is a good
45 * // practice.
46 * \Drupal::service('stuff.doing')->doStuff();
47 * }
48 *
49 * interface StuffDoingInterface {
50 * public function doStuff();
51 * }
52 *
53 * class StuffDoingClass implements StuffDoingInterface {
54 * protected $lockBackend;
55 *
56 * public function __construct(LockBackendInterface $lock_backend) {
57 * $this->lockBackend = $lock_backend;
58 * }
59 *
60 * public function doStuff() {
61 * $lock = $this->lockBackend->acquire('stuff_lock');
62 * // ...
63 * }
64 * }
65 * @endcode
66 *
67 * @see \Drupal\Core\DrupalKernel
68 */
Services and Dependency Injection 476
drush dcs | grep "PART OF SERVICE NAME" can find a service e.g. drush dcs | grep "access"
will find all services with access in the name.
1 $ drush devel:services
2 - access_arguments_resolver_factory
3 - access_check.contact_personal
4 - access_check.cron
5 - access_check.csrf
6 - access_check.custom
7 - access_check.db_update
8 - access_check.default
9 - access_check.entity
10 - access_check.entity_bundles
11 - access_check.entity_create
12 - access_check.entity_create_any
13 - access_check.entity_delete_multiple
14 - access_check.field_ui.form_mode
15 - access_check.field_ui.view_mode
16 - access_check.header.csrf
17 - access_check.node.add
18 - access_check.node.preview
19 - access_check.node.revision
20 - access_check.permission
21 - access_check.quickedit.entity_field
22 - access_check.theme
23 - access_check.update.manager_access
24 - access_check.user.login_status
25 - access_check.user.register
26 - access_check.user.role
27 - access_manager
28 - account_switcher
29 - admin_toolbar_tools.helper
30 - ajax_response.attachments_processor
Services and Dependency Injection 477
31 - ajax_response.subscriber
32 …
etc.
28.13: Resources
• Services and Dependency Injection in the Drupal.org API documentation at https://api.drupal.
org/api/drupal/core%21core.api.php/group/container/10
• Drupal 8: Properly injecting dependencies using DI by Danny Sipos from May 2016 at https:
//code.tutsplus.com/tutorials/drupal-8-properly-injecting-dependencies-using-di--cms-26314
• Dependency injection in Drupal 8 plugins (or blocks) by Märt Matoo from March 2017 https:
//chromatichq.com/insights/dependency-injection-drupal-8-plugins/
• Drupal 8: Properly Injecting Dependencies Using DI by Danny Sipos May 2016 https://code.
tutsplus.com/tutorials/drupal-8-properly-injecting-dependencies-using-di--cms-26314
• Inject a service in Drupal 8 showing an example of injecting http_client (Guzzle) by J M Olivas
July 2015 https://gist.github.com/jmolivas/ca258d7f2742d9e1aae4
29: State API, TempStore and
UserData
29.1: Overview
The State API, TempStore API, and UserData are all storage mechanisms in Drupal. There is
some overlap in their capabilities however they can be used for different purposes. Here is a brief
explanation of each:
State API: This provides a global way to store key-value pairs of data that need to persist between
page loads or can be shared across different parts of the website. It is used to persist data such as
cron key, last cron run, system last check for updates, installation time and whether the system is
in maintenance mode. It is like configuration data except it can’t be exported (and imported) and
stored in source code, thereby making it a little more secure. Typically, configuration settings are
exportable values used in modules, features, or installation profiles e.g. front page path.
UserData: This allows you to store user-specific data (in key-value pairs) in a similar manner as the
State API. Because the data is specific to each user, it is useful for custom user preferences or other
user-specific information.
TempStore: This provides a way to store user-specific data (also key-value pairs) that may be needed
for a short period of time but does not need to be permanently stored. For example, it can be used
to store data that is being edited in a form, allowing users to continue working on the form even if
they navigate away from the page before saving the changes. It is also ideal for storing data such as
the contents of a shopping cart. I’ve used tempstore to quickly access a list of nodes that need to be
voted on by each voter. This means they don’t need to run a complex set of queries more than once.
Similarly to TempStore, you can also use the Drupal cache system to load complicated data really
quickly. I use this for storing arrays of nodes and data for a complicated voting application to
improve scalability and performance.
1 \Drupal::configFactory()
2 ->getEditable('system.site')
3 ->set('page.front', 'node/1')
4 ->save();
In Drupal 7, this was done using variable_set(). If you are upgrading from a Drupal 7 site, check out
this article from March 2021 about upgrading to the Drupal 8 State system at https://www.drupal.
org/node/1787318
Writing state data looks like this:
1 \Drupal::state()->set('selwyn.important.string', 'abc');
2 \Drupal::state()->set('selwyn.more.important.string', 'def');
In the screenshot below, you can see where this is stored in the key_value table. Notice that the
collection column is set to ”state” indicating these are State API values.
State API, TempStore and UserData 480
1 <?php
2
3 namespace Drupal\state_examples\Controller;
4
5 use Drupal\Core\Controller\ControllerBase;
6
7 /**
8 * Returns responses for State API, TempStore and UserData Examples routes.
9 */
10 class StateExamplesController extends ControllerBase {
11
12 /**
13 * Builds the response.
14 */
15 public function build1() {
16
17 // Set single value.
18 \Drupal::state()->set('selwyn1', 'abc');
19 \Drupal::state()->set('selwyn2', 'def');
20 $build['content'][] = [
21 '#type' => 'item',
22 '#markup' => $this->t('Retrieved State API data: selwyn1: %selwyn1 and selwyn2\
23 : %selwyn2 .', [
24 '%selwyn1' => \Drupal::state()->get('selwyn1'),
25 '%selwyn2' => \Drupal::state()->get('selwyn2'),
26 ]),
27 ];
28
29 // Delete single value.
30 \Drupal::state()->delete('selwyn1');
31 \Drupal::state()->delete('selwyn2');
32 $build['content'][] = [
33 '#type' => 'item',
34 '#markup' => $this->t('Deleted (single) State API data.'),
35 ];
36
37 // Set multiple values.
38 \Drupal::state()->setMultiple(['selwyn1' => 'ghi', 'selwyn2' => 'jkl']);
39 $build['content'][] = [
40 '#type' => 'item',
41 '#markup' => $this->t('Retrieved State API data: selwyn1: %selwyn1 and selwyn2\
42 : %selwyn2 .', [
43 '%selwyn1' => \Drupal::state()->get('selwyn1'),
State API, TempStore and UserData 482
1 $str = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque portti\
2 tor urna consequat dolor convallis iaculis. Nullam blandit ipsum eget odio semper rh
3 oncus.
4 Aenean libero tortor, ullamcorper vitae porttitor sit amet, posuere a ante. Vestibul\
5 um ipsum tellus, porta eu ligula at, porttitor congue nunc. Suspendisse felis lacus,
6 tristique vel aliquam eget, congue non mi.';
7
8 \Drupal::state()->set('selwyn.long.string', $str);
9
10 // Explode string into array of paragraphs.
11 $test2 = explode( "\n", \Drupal::state()->get('selwyn.long.string'));
29.3: UserData
UserData is stored permanently in the users_data table. This is typically used for unstructured
information pertaining to the user eg. user preferences, flags etc. I’ve used it to keep track of a
user’s progress in a complicated web application.
Here is an example where a value of COMPLETED is stored, read back and then deleted for the key
program.123.vote.0.finalized:
State API, TempStore and UserData 483
Here is the data stored in the users_data table. Notice that there is also some user data from the
contact module as well as the data from the state_examples module that was created from the code
above.
29.4: TempStore
TempStore is used to keep temporary data across multiple requests. The data is intended to be
non-cache data (i.e. not easily be rebuilt) and is stored in the key_value_expire table.
TempStore has two flavors, private and shared. The difference between them is that the private
TempStore entries are connected to a specific user (via their user id) whereas shared TempStore
State API, TempStore and UserData 484
entries can be shared between multiple users. Shared TempStore could for example be used to
trigger a locking mechanism.
29.4.1: PrivateTempStore
The Drupal tempstore.private service is used to allow temporary user data that is available from one
web request to the next. It is intended to be used for non-cache data that cannot easily be rebuilt.
This includes work-in-progress data that isn’t ready to be saved permanently.
The temporary store is a key/value store and can therefore store anything from a single Boolean
value to a serialized object. (adapted from https://www.hashbangcode.com/article/drupal-9-using-
private-temporary-store-service)
Stores and retrieves temporary data for a given owner.
A PrivateTempStore can be used to make temporary, non-cache data available across requests. The
data for the PrivateTempStore is stored in one key/value collection. PrivateTempStore data expires
automatically after a given timeframe.
By default, data is stored for one week (604800 seconds) before expiring.
The PrivateTempStore is different from a cache because the data in it is not yet saved permanently
and so it cannot be rebuilt. Typically, the PrivateTempStore might be used to store work in progress
that is later saved permanently elsewhere, e.g. autosave data, multistep forms, or in-progress
changes to complex configuration that are not ready to be saved.
The PrivateTempStore differs from the SharedTempStore in that all keys are ensured to be
unique for a particular user and users can never share data. If you want to be able to
share data between users or use it for locking, use \Drupal\Core\TempStore\SharedTempStore.
(Adapted from https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21TempStore%
21PrivateTempStore.php/class/PrivateTempStore/9.0.x)
In this example, we can see writing, reading, and deleting a private TempStore value:
13 ]),
14 ];
15
16 // $tempstore->delete('selwyn.important.string');
17
18 return $build;
19 }
Drupal automatically inserts the user id (e.g. 1:) into the front of the name field so that the data
is connected with that user. Notice in the database screenshot below that the name field (i.e.
1:selwyn.important.string) is preceded by 1: which makes it unique. Even though the code specified
”selwyn.important.string” each user gets a unique copy. I was logged in as user 1 when I ran this
program initially, and then I ran it again logged in as user 2.
In the next example we’re storing an array with the key ”selwyn.important.array”. The array
is automatically serialized into the value field. Incidentally, you don’t have to specify the
type of content (i.e. array) as I have. Instead of using selwyn.important.array, you could use
selwyn.important.banana or selwyn.important.kiwi (depending on your fruit preference).
1 // Store an array.
2 $array = [
3 'id' => '123',
4 'name' => 'Dave',
5 ];
6 $tempstore->set('selwyn.important.array', $array);
7 $array = $tempstore->get('selwyn.important.array');
8
9 $build['content'][] = [
10 '#type' => 'item',
11 '#markup' => $this->t('Retrieved Private TempStore API data: id = %value1, name= %\
12 value2.', [
13 '%value1' => $array['id'],
14 '%value2' => $array['name'],
State API, TempStore and UserData 486
15 ]),
16 ];
17
18 // Delete the data.
19 $tempstore->delete('selwyn.important.array');
This is a screenshot of the value for the array field above. Notice the id = 123 and name = Dave.
There is no need to manipulate the data to get it stored away safely. Drupal will serialize it for you:
Also see this article showing how to save values from a form and then later retrieve and process
them in a controller. Saving temporarily values of a form with Private Tempstore in Drupal 8
by Karim Boudjema Mar 2019 http://karimboudjema.com/en/drupal/20190315/saving-temporary-
values-form-private-tempstore-drupal-8
29.4.2: SharedTempStore
Stores and retrieves temporary data for a given owner.
State API, TempStore and UserData 487
A SharedTempStore can be used to make temporary, non-cache data available across requests.
The data for the SharedTempStore is stored in a key/value pair. SharedTempStore data expires
automatically after a given timeframe.
By default, data is stored for one week (604800 seconds) before expiring.
The SharedTempStore is different from a cache because the data in it is not yet saved permanently
and so it cannot be rebuilt. Typically, the SharedTempStore might be used to store work in progress
that is later saved permanently elsewhere, e.g. autosave data, multistep forms, or in-progress
changes to complex configuration that are not ready to be saved.
Each SharedTempStore belongs to a particular owner (e.g. a user, session, or process). Multiple
owners may use the same key/value collection, and the owner is stored along with the key/value
pair in the value field.
Every key is unique within the collection, so the SharedTempStore can check whether a particular
key is already set by a different owner. This is useful for informing one owner that the data is already
in use by another; for example, to let one user know that another user is in the process of editing
certain data, or even to restrict other users from editing it at the same time. It is the responsibility
of the implementation to decide when and whether one owner can use or update another owner’s
data.
If you want to be able to ensure that the data belongs to the current user, use \Dru-
pal\Core\TempStore\PrivateTempStore. (Adapted from https://api.drupal.org/api/drupal/core%
21lib%21Drupal%21Core%21TempStore%21PrivateTempStore.php/class/PrivateTempStore/9.0.x)
Here is example code showing writing, reading and deleting data from SharedTempStore:
19 $build['content'][] = [
20 '#type' => 'item',
21 '#markup' => $this->t('Retrieved Private TempStore API data: id = %value1, nam\
22 e= %value2.', [
23 '%value1' => $array['id'],
24 '%value2' => $array['name'],
25 ]),
26 ];
27
28 // Delete the data.
29 // $tempstore->delete('selwyn.important.agent');
30
31 return $build;
32 }
And you can see the data owner in the screen shot below:
If you want to inject the service rather than use it statically, you have to inject tempstore.shared.
e.g. In the module.services.yml file below, we inject 3 services, including the tempstore.shared.
Note that this is actually the SharedTempStoreFactory (and not the SharedTempStore class itself).
Remember to derive the “collection” as shown in the php snippet below:
State API, TempStore and UserData 489
1 services:
2 tea_teks_srp.vote_processor:
3 class: Drupal\tea_teks_srp\VotingProcessor
4 arguments: ['@entity_type.manager', '@current_user', '@tempstore.shared']
Here is an example of actually writing and then reading the value from the shared tempstore.
In core.services.yml, you can see that tempstore.shared uses the SharedTempStoreFactory class:
State API, TempStore and UserData 490
1 tempstore.shared:
2 class: Drupal\Core\TempStore\SharedTempStoreFactory
3 arguments: ['@keyvalue.expirable', '@lock', '@request_stack', '@current_user', '\
4 %tempstore.expire%']
5 tags:
6 - { name: backend_overridable }
29.5: Reference
• State API Overview updated September 2022 https://www.drupal.org/docs/8/api/state-api/
overview
• If you are upgrading from a Drupal 7 site which uses variable_set, check out this article from
March 2021 about upgrading to the Drupal 8 State system at https://www.drupal.org/node/
1787318
• Drupal API documentation for UserData https://api.drupal.org/api/drupal/core%21modules%
21user%21src%21UserData.php/class/UserData/8.2.x
• Drupal API documentation for UserData::get https://api.drupal.org/api/drupal/core%
21modules%21user%21src%21UserData.php/function/UserData%3A%3Aget/8.2.x
• Storing user data such as preferences in Drupal 8 using the UserData service by Daniel Sipos
Mar 2017 https://www.webomelette.com/storing-user-data-such-preferences-drupal-8-using-
userdata-service
• Drupal Documentation on PrivateTempStore https://api.drupal.org/api/drupal/core%21lib%
21Drupal%21Core%21TempStore%21PrivateTempStore.php/class/PrivateTempStore/9.0.x
• Drupal 8: Tempstore (with code snippets) by Oleksii Raiu https://alexrayu.com/snippets/
drupal-8-tempstore
• Drupal 9: Using The Private Temporary Store Service by Phil Norton July 2022 https://www.
hashbangcode.com/article/drupal-9-using-private-temporary-store-service
• Saving temporarily values of a form with Private Tempstore in Drupal 8 by Karim Boudjema
Mar 2019 http://karimboudjema.com/en/drupal/20190315/saving-temporary-values-form-
private-tempstore-drupal-8
30: Taxonomy
30.1: Lookup term by name
1 public function loadTermByName() {
2 $vocabulary_id = 'event_category';
3 $term_name = 'protest';
4 $terms = \Drupal::entityTypeManager()
5 ->getStorage('taxonomy_term')
6 ->loadByProperties(['name' => $term_name]);
7
8 if (empty($terms)) {
9 $build['result'] = [
10 '#type' => 'item',
11 '#markup' => $this->t('No terms found'),
12 ];
13 }
14 else {
15 /** @var Term $term */
16 foreach ($terms as $term) {
17 $term_name = $term->getName();
18 $build[$term_name] = [
19 '#type' => 'item',
20 '#markup' => $this->t('Term name: @term_name. Term id: @term_id', [
21 '@term_name' => $term_name,
22 '@term_id' => $term->id(),
23 ]),
24 ];
25 }
26 }
27
28 return $build;
29 }
30
31 // Or the deprecated version.
32
33 $terms = taxonomy_term_load_multiple_by_name($term_name, 'opinion_categories');
34 if (empty($terms)) {
35 $ra = [
Taxonomy 492
1 $vid = 'event_format';
2 $terms =\Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadTree($vid\
3 );
4 foreach ($terms as $term) {
5 $options[$term->tid] = $term->name;
6 }
7
8 $form['event_format']['active'] = [
9 '#type' => 'radios',
10 '#title' => $this->t('Event Format'),
11 '#default_value' => 1,
12 '#options' => $options,
13 // '#options' => [
14 // 0 => $this->t('In-Person'),
15 // 1 => $this->t('Online'),
16 // 2 => $this->t('In-Person & Online'),
17 // ],
18 ];
1 $term = Term::create([
2 'name' => 'protest',
3 'vid' => 'event_category',
4 ])->save();
1 {{ content.title }}
Brace and percent are used to put logic into Twig templates e.g. if, then, else or for loops. E.g.
1 {% if content.price is defined %}
2 <h2>Price: {{ content.price }} </h2>
3 {% endif %}
Here are some of the Twig functions that you can use in twig templates: https://www.drupal.org/
docs/8/theming/twig/functions-in-twig-templates There are lots of them e.g.
• file_url($uri)
• link($text, $uri, $attributes)
• path($name, $parameters, $options)
• url($name, $parameters, $options)
1 {{ items }}
and
1 {{ item.content }}
1 <section>
2 {{ page.content }}
3 </section>
1 {{ drupal_field('field_image', 'node') }}
2
3 <h1>{{ node.label }}</h1>
4 <div>For: {{ node.field_for.0.value }}</div>
5 <div>DATE: {{ node.field_event_date.0.value|date('n/j/Y') }}</div>
6 <div>Time: {{ node.field_event_date.0.value|date('h:ia') }} - {{ node.field_event_da\
7 te.0.end_value|date('h:ia') }}</div>
8
9 <div>
10 Location:
11 {% if node.field_event_location_link.0.url %}
12 <a href="{{ node.field_event_location_link.0.url }}">{{ node.field_event_locatio\
13 n.0.value }}</a>
14 {% else %}
15 {{ node.field_event_location.0.value }}
16 {% endif %}
17 </div>
18
19 {% if node.field_event_cta_link.0.url %}
20 CTA:<div class="button"><a href="{{ node.field_event_cta_link.0.url }}">{{ node.fi\
21 eld_event_cta_link.0.title }}</a></div>
22 {% endif %}
Here is the same basic stuff (as above) but implemented in the node template at
txg/web/themes/custom/txg/templates/content/node--event.html.twig:
1 {{ content }}
1 {{ label }}
1 {{ node.label }}
31.2.5: Fields
There are many ways to limit things and only show some of the content. Mostly often you will need
to show specific fields. Note. This will include rendered info such as labels etc.
1 {{ content.field_yomama }}
or
1 {{ content.field_ref_topic }}
1 {{ content.field_intl_students_and_scholars }}
You can also grab node specific fields if content. type fields don’t do the trick.
In a node template, you can display specific node fields by prefacing them with node e.g.:
1 {{ node.id }}
2 {{ node.label }}
3 {{ node.field_date.value }}
4 {{ node.field_date.end_value }}
1 termid0: {{ paragraph.field_ref_tax.0.target_id }}
2 termid1: {{ paragraph.field_ref_tax.1.target_id }}
and we get this result if we have selected two terms 13 and 16.
1 termid0: 13
2 termid1: 16
To dump a taxonomy reference field for debugging purposes use the code below. The pre tags format
it a little nicer than if we don’t have them.
1 <pre>
2 {{ dump(paragraph.field_ref_tax.value) }}
3 </pre>
1 termid0: {{ paragraph.field_ref_tax.0.target_id }}
2 termid1: {{ paragraph.field_ref_tax.1.target_id }}
and to make this more useful, here we build a string of them to pass to a view.
From: dirt/web/themes/custom/dirt_bootstrap/templates/paragraphs/paragraph--news-
preview.html.twig
TWIG 506
31.2.8: Body
1 {{ content.body }}
Or
1 {{ node.body.value }}
1 {{ node.body.summary | raw }}
1 {{ node.field_iso_n3_country_code.0.value }}
5 https://twig.symfony.com/doc/3.x/filters/striptags.html
TWIG 507
1 {{ word|striptags('<b>')|raw }}
1 {{ word|striptags('<b>,<a>,<pre>')|raw }}
1 <pre>
2 Created: {{ node.created.value }}
3 Created: {{ node.createdtime }}
4 Created: {{ node.created.value|date('Y-m-d') }}
5
6 Modified: {{ node.changed.value }}
7 Modified: {{ node.changedtime }}
8 Modified: {{ node.changed.value|date('Y-m-d') }}
9
10 Published: {{ node.published_at.value }}
11 Published: {{ node.published_at.value|date('Y-m-d') }}
12 </pre>
Here is the output you might see. Note. The first published is apparently blank because I didn’t use
the drupal scheduling to publish the node (maybe?) and the second one seems to have defaulted to
today’s date.
TWIG 508
1 Created: 1604034000
2 Created: 1604034000
3 Created: 2020-10-30
4 Modified: 1604528207
5 Modified: 1604528207
6 Modified: 2020-11-04
7 Published:
8 Published: 2020-11-20
Updated/changed
1 {{ node.createdtime }}
1 {{ node.createdtime\|date('M d, Y') }}
Also
1 {{ content.field_blog_date }}
1 {{ content.field_blog_date.0 }}
1 {{ node.field_blog_date.value|date('U')|format_date('short_mdyyyy') }}
1 {{ node.field_blog_date.value|date('n/j/Y') }}
1 {{ content.field_when }}
The output will include whichever formatting you specify in Drupal. While I assume there is a way
to pass a smart date7 formatting string to twig, I haven’t discovered it yet. Here are ways to format
a smart date8 .
Specify the index (the 0 indicating the first value, or 1 for the second) e.g. node.field.0.value and
pipe the twig date9 function for formatting:
Date as in July 18, 2023
1 {{ node.field_when.0.value|date('F j, Y') }}
End date
1 {{ node.field_when.0.end_value|date('F j, Y') }}
Timezone as in America/Chicago
1 {{ node.field_when.0.value|date('e') }}
Timezone as in CDT
6 https://www.drupal.org/project/smart_date
7 https://www.drupal.org/project/smart_date
8 https://www.drupal.org/project/smart_date
9 https://twig.symfony.com/doc/3.x/filters/date.html
TWIG 510
1 {{ node.field_when.0.value|date('T') }}
1 {{ node.field_ref_topic.0.target_id }}
Note. This will show the node id of the entity reference field. See below to see the content that the
entity reference field points to.
1 {{ node.field_sf_contract_ref.entity.field_contract_overview.value }}
Or
1 {{ content.field_sf_contract_ref.entity.field_contract_overview }}
The field in the contract node is called field_sf_contract_ref. The field in the referenced entity
is called field_contract_overview. Notice how with the node. style, you must specify .value at the
end.
Here is an example of a taxonomy term where the title of the term will be displayed.
TWIG 511
1 <pre>
2 Dump category:
3 {{ dump(node.field_ref_tax.entity.label) }}
4 </pre>
1 <pre>
2 Dump category: {{ dump(node.field_ref_tax.entity.label) }}
3 </pre>
1 {%
2 set classes = [
3 'block',
4 'block-' ~ configuration.provider|clean_class,
5 'block-' ~ plugin_id|clean_class,
6 ]
7 %}
8 <div{{ attributes.addClass(classes) }}>
9 {{ title_prefix }}
10 {% if label %}
11 <h2{{ title_attributes }}>{{ label }}</h2>
12 {% endif %}
13 {{ title_suffix }}
14 {% block content %}
15 {{ content }}
16 {% endblock %}
17 also powered by <a href="http://austinprogressivecalendar.com">Austin Progressive \
18 Calendar</a>
19 </div>
1 function burger_theme_preprocess_node(&$variables) {
2
3 $burger_list = [
4 ['name' => 'Cheesburger'],
5 ['name' => 'Mushroomburger'],
6 ['name' => 'Chickenburger'],
7 ];
8 $variables['burgers'] = $burger_list;
9 }
1 <ol>
2 {% for burger in burgers %}
3 <li>{{ burger['name'] }}</li>
4 {% endfor %}
5 </ol>
31.2.19: Links
There are a bajillion kertrillion or more ways to render a link
Link field (URL)
This is the simplest way. Just set the display mode to link
1 {{ content.field_suggest_button }}
If you need a little more control you might use this version which allows classes etc. We are adding
several classes onto the anchor to make it look like a button. In this case with an internal link, it
shows up using the alias of the link i.e. it shows /contracts instead of node/7 when you hover over
the link.
Using .uri causes the link (internal only. External links are fine) to show up as node/7 when you
hover over the link.
TWIG 513
1 //bad
2 {{ node.field_suggest_button.url }}.
3 //bad
Relative link
See path vs url:
1 $vendor_url = $node->field_sf_contract_ref->entity->field_vendor_url->first();
2 if ($vendor_url) {
3 $vendor_url = $vendor_url->getUrl();
4 if ($vendor_url) {
5 $variables['vendor_url'] = $vendor_url->getUri();
6 }
7 }
Here we check if there is a target value and output that also. E.g. target="_blank" and also display
the title–this is the anchor title as in the words “Vendor Website” below
From inside-marthe/themes/custom/dp/templates/paragraph/paragraph--sidebar-product-card.html.twig
we wrap some stuff in a link:
1 {% if node.field_event_location_link.0.url %}
2 <a href="{{ node.field_event_location_link.0.url }}">{{ node.field_event_locatio\
3 n.0.value }}</a>
4 {% else %}
5 {{ node.field_event_location.0.value }}
6 {% endif %}
1 $instructions_node = Node::load($order_type_instructions_nid);
2 if ($instructions_node) {
3 $order_link = $instructions_node->field_link->first();
4 if ($order_link) {
5 $uri = $order_link->uri;
6 $variables['order_link_title'] = $order_link->title;
7 $order_url = $order_link->getUrl();
8 if ($order_url) {
9 $variables['order_type_link'] = $order_url;
10 }
11 }
12 }
Or for a media field, set the image style on the display mode and use this:
1 {{ content.field_banner_image.0 }}
1 {% if related_lessons_nids|length %}
2 <div class="section section--featured">
3 <div class="grid-container">
4 <h2 class="section-header text-center large-text-left">Related Lessons</h2>
5 <div class="grid-x grid-margin-x" data-equalizer data-equalize-on="large">
6 {{ drupal_view('video', 'embed_collection_related_lessons', related_lessons_\
7 nids|join(', ')) }}
8 </div>
9 </div>
10 </div>
11 {% endif %}
Not empty:
1 {% if content.field_teacher_commentary_image|render %}
2 <img src="{{file_url( content.field_teacher_commentary_image['#items'].entity.uri.\
3 value ) }}" width="420" height="255" alt="" class="left">
4 {% endif %}
31.2.27: Attributes
From https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes:
Elements in HTML have attributes; these are additional values that configure the elements or adjust
their behavior in various ways to meet the criteria the users want.
Read more about using attributes in templates https://www.drupal.org/docs/8/theming-drupal-8/
using-attributes-in-templates
To add a data attribute use:
1 {{ attributes.setAttribute('data-myname','tommy') }}
e.g.
TWIG 517
Produces:
1 <article
2 data-history-node-id="3224"
3 data-quickedit-entity-id="node/3224"
4 role="article"
5 class="contextual-region node node--type-article node--promoted node--view-mode-fu\
6 ll"
7 about="/burger1"
8 typeof="schema:Article"
9 my-name="Selwyn"
10 data-quickedit-entity-instance-id="0"
11 ></article>
1 <img
2 id="specific-id"
3 class="red blue"
4 src="https://www.drupal.org/files/powered-blue-135x42.png"
5 />
1 {{ attributes.hasClass($class) }}
Remove an attribute
TWIG 518
1 {{ attributes.removeAttribute() }}
1 {{ attributes.toArray () }}
1 {% if not node.published %}
2 <p class="node--unpublished">{{ 'Unpublished'|t }}</p>
3 {% endif %}
1 <a href="{{ url('entity.node.canonical', {node: 3223}) }}">Link to WEA node 3223 </a>
1 <a href="{{ path('entity.node.canonical', {node: 3223}) }}">Link to WEA node 3223 </\
2 a>
1 <div>
2 {%- if content.author -%}
3 by
4 {%- for author in content.author -%}
5 {% if loop.last %}
6 {% set separator = '' %}
7 {% else %}
8 {% set separator = ' and ' %}
9 {% endif %}
10 {{ author }} {{ separator }}
11 {%- endfor -%}
12 {%- endif -%}
13 </div>
1 {% if label_hidden %}
2 {% if multiple %}
3 {% for item in items %}
4 {%if loop.index > 1 %}, {% endif %}{{ item.content }}
5 {% endfor %}
6 {% else %}
7 {% for item in items %}
8 {%if loop.index > 1 %}, {% endif %}{{ item.content }}
9 {% endfor %}
10 {% endif %}
11 {% else %}
12 <div{{ title_attributes }}>{{ label }}</div>
13 {% for item in items %}
14 {%if loop.index > 1 %}, {% endif %}{{ item.content }}
15 {% endfor %}
16 {% endif %}
31.4: Control/Logic
31.4.4: IF OR
If there is a value in field_event_date or field_display_date, then display it/them.
1 {% if content.body|render %}
2 <li><a class="scroll" href="#section-overview">Overview</a></li>
3 {% endif %}
1 {% if attributes is empty %}
2 {{ link(item.title, item.url) }}
3 {% else %}
4 {{ link(item.title, item.url, attributes) }}
5 {% endif %}
10 https://twig.symfony.com/doc/3.x/tests/empty.html
TWIG 523
Or this much simpler version which also comes from the same issue page above (it doesn’t seem to
work as well as the version above):
TWIG 524
1 {% if content.field_related_lessons.value %}
2 {{ content.field_related_lessons}}
3 {% endif %}
1 {{ include('node--teaser.html.twig') }}
11 https://twig.symfony.com/doc/3.x/tests/sameas.html
TWIG 525
1 {{ attributes.toArray () }}
31.5: Views
To use a field value in a view as an argument, using twig_tweak12 , you can render the view and its
arguments/parameters. In the example below, these are the contextual filters defined in the view.
Note. Using content.field as a parameter doesn’t work because content.fields get rendered
so they are usually filled with HTML or labels or both. Parameters need to simply be
numbers or strings.
Other examples. Here an entity reference field is passed as a parameter. This works for taxonomy
terms like this also.
1 {{ drupal_view('news_stories_for_a_topic','block_1', node.field_ref_topic.0.target_i\
2 d) }}
Or
Note. If you ever see a 502 bad gateway error when embedding a drupal_view, delete the display
and create a new one and it may just work fine.
Or even nicer, we could loop thru an unlimited number of terms, build a string of them to pass to a
view.
From: /Users/selwyn/Sites/dirt/web/themes/custom/dirt_bootstrap/templates/paragraphs/paragraph--news
1 views-view--foobar--page.html.twig
2 views-view--page.html.twig
3 views-view--foobar.html.twig
4 views-view.html.twig
5
6 views-view-unformatted--foobar--page.html.twig
7 views-view-unformatted--page.html.twig
8 views-view-unformatted--foobar.html.twig
9 views-view-unformatted.html.twig
10
11 views-view-fields--foobar--page.html.twig
12 views-view-fields--page.html.twig
13 views-view-fields--foobar.html.twig
14 views-view-fields.html.twig
1 function txg_preprocess_views_view(&$variables) {
2 $view = $variables['view'];
3 $id = $view->storage->id();
4 $display_id = $view->current_display;
5
6 // Build /newsroom/search
7 if ($id == 'news_events_search' && $display_id == 'page_news') {
8 $variables['filter_data'] = generate_search_filter_data();
9 }
10 }
Here is the code that builds the data for the select controls:
TWIG 529
1 /**
2 * Populate dropdowns.
3 *
4 * Builds the search filter data to populate select dropdowns on
5 * /newsroom, /newsroom-search etc. pages.
6 *
7 * @return array
8 * Array of values for dropdown.
9 *
10 * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
11 * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
12 */
13 function generate_search_filter_data() {
14
15 // Grab the Get parameters.
16 $country_nid = \Drupal::request()->query->get('country');
17 $aof_nid = \Drupal::request()->query->get('aof');
18 $unit_nid = \Drupal::request()->query->get('unit');
19 $continent_arg = \Drupal::request()->query->get('continent');
20 $topic_tid = \Drupal::request()->query->get('topic');
21
22 // Office: AOF and child units from main menu.
23 $office_nid = 0;
24 if (!empty($aof_nid)) {
25 if (is_numeric($aof_nid)) {
26 $office_nid = $aof_nid;
27 }
28 }
29 if (!empty($unit_nid)) {
30 if (is_numeric($unit_nid)) {
31 $office_nid = $unit_nid;
32 }
33 }
34
35 $storage = get_offices($office_nid);
36 $data[] = [
37 'type' => 'office',
38 'info' => $storage,
39 ];
40
41 // Topic taxonomy terms.
42 $vid = 'topic';
43 $terms = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadTree($vid);
TWIG 530
44 $storage = [];
45 foreach ($terms as $term) {
46 $storage[] = [
47 "value" => $term->tid,
48 "title" => $term->name,
49 ];
50 }
51 // Update the select to show the current value from the url.
52 foreach ($storage as &$item) {
53 if ($item['value'] == $topic_tid) {
54 $item['selected'] = 'selected';
55 break;
56 }
57 }
58 $data[] = [
59 'type' => 'topic',
60 'info' => $storage,
61 ];
62
63 // Continent.
64 $continents = [
65 ['title' => 'Africa', 'value' => 'Africa'],
66 ['title' => 'Antarctica', 'value' => 'Antarctica'],
67 ['title' => 'Asia', 'value' => 'Asia'],
68 ['title' => 'Europe', 'value' => 'Europe'],
69 ['title' => 'North America', 'value' => 'north%20america'],
70 ['title' => 'Oceania', 'value' => 'Oceania'],
71 ['title' => 'South America', 'value' => 'south%20america'],
72 ];
73 // Update the select to show the current value from the url.
74 foreach ($continents as &$continent) {
75 if (!empty($continent_arg)) {
76 if (strtolower($continent['title']) == strtolower($continent_arg)) {
77 $continent['selected'] = 'selected';
78 }
79 }
80 }
81 $data[] = [
82 'type' => 'continent',
83 'info' => $continents,
84 ];
85
86 // Country.
TWIG 531
87 $data_storage = Drupal::entityTypeManager()->getStorage('node');
88 $query = Drupal::entityQuery('node')
89 ->condition('status', 1)
90 ->condition('type', 'country')
91 ->sort('title', 'ASC');
92 $nids = $query->execute();
93 $nodes = $data_storage->loadMultiple($nids);
94 $storage = [];
95 foreach ($nodes as $node) {
96 $storage[] = [
97 'title' => $node->getTitle(),
98 'value' => $node->id(),
99 ];
100 }
101 // Set the default country so it appears in the select dropdown.
102 foreach ($storage as &$item) {
103 if ($item['value'] == $country_nid) {
104 $item['selected'] = 'selected';
105 break;
106 }
107 }
108 $data[] = [
109 'type' => 'country',
110 'info' => $storage,
111 ];
112
113 return $data;
114 }
Here the output for all views uses the views-view.html.twig template
If we want to override the frontpage view we can copy the template from above to our theme and
rename it views-view--frontpage.html.twig
Notice that it will override all displays (in this case the page and the feed displays–“page_1” and
“feed_1” respectively) so we can be more specific
Rename it to views-view--frontpage--page_1.html.twig
From https://api.drupal.org/api/drupal/core%21modules%21views%21views.theme.inc/group/
views_templates/8.5.x
All views templates can be overridden with a variety of names, using the view, the display ID of the
view, the display type of the view, or some combination thereof.
TWIG 532
For each view, there will be a minimum of two templates used. The first is used for all views: views-
view.html.twig13 .
The second template is determined by the style selected for the view. Note that certain aspects of
the view can also change which style is used; for example, arguments which provide a summary
view might change the style to one of the special summary styles.
The default style for all views is views-view-unformatted.html.twig14 .
Many styles will then farm out the actual display of each row to a row style; the default row style
is views-view-fields.html.twig.
Here is an example of all the templates that will be tried in the following case:
View: foobar Style: unformatted Row style: Fields. Display:Page.
1 - views-view--foobar--page.html.twig
2 - views-view--page.html.twig
3 - views-view--foobar.html.twig
4 - views-view.html.twig
5 - views-view-unformatted--foobar--page.html.twig
6 - views-view-unformatted--page.html.twig
7 - views-view-unformatted--foobar.html.twig
8 - views-view-unformatted.html.twig
9 - views-view-fields--foobar--page.html.twig
10 - views-view-fields--page.html.twig
11 - views-view-fields--foobar.html.twig
12 - views-view-fields.html.twig
When adding a new template to your theme, be sure to flush the theme registry cache!
From https://api.drupal.org/api/drupal/core%21modules%21views%21views.theme.inc/function/
template_preprocess_views_view_field/8.2.x:
When changing the value of a field in a view, use preprocess_views_view_field().
13 https://api.drupal.org/api/drupal/core%21modules%21views%21templates%21views-view.html.twig/8.2.x
14 https://api.drupal.org/api/drupal/core%21modules%21views%21templates%21views-view-unformatted.html.twig/8.2.x
TWIG 533
1 function mytheme_preprocess_views_view_field(&$variables) {
2 $view = $variables['view'];
3 if($view->id() == 'my_view') {
4 if($variables['field']->field == 'field_my_field') {
5 $my_field_value = $variables['field']->getValue($variables['row']);
6 $my_altered_value = 'xx';
7 $variables['output'] = $my_altered_value; // Default variable in Twig file is \
8 "output"
9 // OR
10 $variables['my_output'] = $my_altered_value; // New variable in Twig file (vie\
11 ws-view-field--my-view--field-my-field.html.twig)
12 }
13 }
14 }
Note. If you use the same field twice in a view i.e. if you need to display different parts of the same
field in different places, views names them something like this: field_library_media and field_-
library_media_1. In that circumstance, you have to refer to them in the function like this:
// if($variables[‘field’]->field == ‘field_library_media’) { if($variables[‘field’]->options[‘id’] ==
‘field_library_media_1’) {
Here is a real example where a media field id is being displayed and I switch it out with the formatted
size of the media file.
1 /**
2 * Implements hook_preprocess_views_view_field().
3 */
4 function dirt_bootstrap_preprocess_views_view_field(&$variables) {
5 $view = $variables['view'];
6 if($view->id() == 'resource_library') {
7 // if($variables['field']->field == 'field_library_media') {
8 if($variables['field']->options['id'] == 'field_library_media_1') {
9 $target_id = $variables['field']->getValue($variables['row']);
10 $file_size = 0;
11 if ($target_id) {
12 $media_item = Media::load($target_id);
13 // Get the file.
14 if ($media_item->hasField('field_media_document')) {
15 $file_id = $media_item->field_media_document->getValue()[0]['target_id'];
16 }
TWIG 534
17 elseif ($media_item->hasField('field_media_image')) {
18 $file_id = $media_item->field_media_image->getValue()[0]['target_id'];
19 }
20 elseif ($media_item->hasField('field_media_audio_file')) {
21 $file_id = $media_item->field_media_audio_file->getValue()[0]['target_id'];
22 }
23 elseif ($media_item->hasField('field_media_video_file')) {
24 $file_id = $media_item->field_media_video_file->getValue()[0]['target_id'];
25 }
26 if (isset($file_id)) {
27 $file = File::load($file_id);
28 if ($file) {
29 // Get file size.
30 $file_size = format_size($file->getSize());
31 }
32 }
33 }
34 $variables['output'] = $file_size; // Default variable in Twig file is "output"
35 }
36 }
37 }
Filters
• abs16
• batch17
• capitalize18
• column19
15 https://www.drupal.org/docs/contributed-modules/twig-tweak/cheat-sheet#s-view-filter
16 https://twig.symfony.com/doc/3.x/filters/abs.html
17 https://twig.symfony.com/doc/3.x/filters/batch.html
18 https://twig.symfony.com/doc/3.x/filters/capitalize.html
19 https://twig.symfony.com/doc/3.x/filters/column.html
TWIG 536
• convert_encoding20
• country_name21
• currency_name22
• currency_symbol23
• data_uri24
• date25
• date_modify26
• default27
• escape28
• filter29
• first30
• format31
• format_currency32
• format_date33
• format_datetime34
• format_number35
• format_time36
• html_to_markdown37
• inky_to_html38
• inline_css39
• join40
• json_encode41
• keys42
• language_name43
• last44
20 https://twig.symfony.com/doc/3.x/filters/convert_encoding.html
21 https://twig.symfony.com/doc/3.x/filters/country_name.html
22 https://twig.symfony.com/doc/3.x/filters/currency_name.html
23 https://twig.symfony.com/doc/3.x/filters/currency_symbol.html
24 https://twig.symfony.com/doc/3.x/filters/data_uri.html
25 https://twig.symfony.com/doc/3.x/filters/date.html
26 https://twig.symfony.com/doc/3.x/filters/date_modify.html
27 https://twig.symfony.com/doc/3.x/filters/default.html
28 https://twig.symfony.com/doc/3.x/filters/escape.html
29 https://twig.symfony.com/doc/3.x/filters/filter.html
30 https://twig.symfony.com/doc/3.x/filters/first.html
31 https://twig.symfony.com/doc/3.x/filters/format.html
32 https://twig.symfony.com/doc/3.x/filters/format_currency.html
33 https://twig.symfony.com/doc/3.x/filters/format_date.html
34 https://twig.symfony.com/doc/3.x/filters/format_datetime.html
35 https://twig.symfony.com/doc/3.x/filters/format_number.html
36 https://twig.symfony.com/doc/3.x/filters/format_time.html
37 https://twig.symfony.com/doc/3.x/filters/html_to_markdown.html
38 https://twig.symfony.com/doc/3.x/filters/inky_to_html.html
39 https://twig.symfony.com/doc/3.x/filters/inline_css.html
40 https://twig.symfony.com/doc/3.x/filters/join.html
41 https://twig.symfony.com/doc/3.x/filters/json_encode.html
42 https://twig.symfony.com/doc/3.x/filters/keys.html
43 https://twig.symfony.com/doc/3.x/filters/language_name.html
44 https://twig.symfony.com/doc/3.x/filters/last.html
TWIG 537
• length45
• locale_name46
• lower47
• map48
• markdown_to_html49
• merge50
• nl2br51
• number_format52
• raw53
• reduce54
• replace55
• reverse56
• round57
• slice58
• slug59
• sort60
• spaceless61
• split62
• striptags63
• timezone_name64
• title65
• trim66
• u67
• upper68
• url_encode69
45 https://twig.symfony.com/doc/3.x/filters/length.html
46 https://twig.symfony.com/doc/3.x/filters/locale_name.html
47 https://twig.symfony.com/doc/3.x/filters/lower.html
48 https://twig.symfony.com/doc/3.x/filters/map.html
49 https://twig.symfony.com/doc/3.x/filters/markdown_to_html.html
50 https://twig.symfony.com/doc/3.x/filters/merge.html
51 https://twig.symfony.com/doc/3.x/filters/nl2br.html
52 https://twig.symfony.com/doc/3.x/filters/number_format.html
53 https://twig.symfony.com/doc/3.x/filters/raw.html
54 https://twig.symfony.com/doc/3.x/filters/reduce.html
55 https://twig.symfony.com/doc/3.x/filters/replace.html
56 https://twig.symfony.com/doc/3.x/filters/reverse.html
57 https://twig.symfony.com/doc/3.x/filters/round.html
58 https://twig.symfony.com/doc/3.x/filters/slice.html
59 https://twig.symfony.com/doc/3.x/filters/slug.html
60 https://twig.symfony.com/doc/3.x/filters/sort.html
61 https://twig.symfony.com/doc/3.x/filters/spaceless.html
62 https://twig.symfony.com/doc/3.x/filters/split.html
63 https://twig.symfony.com/doc/3.x/filters/striptags.html
64 https://twig.symfony.com/doc/3.x/filters/timezone_name.html
65 https://twig.symfony.com/doc/3.x/filters/title.html
66 https://twig.symfony.com/doc/3.x/filters/trim.html
67 https://twig.symfony.com/doc/3.x/filters/u.html
68 https://twig.symfony.com/doc/3.x/filters/upper.html
69 https://twig.symfony.com/doc/3.x/filters/url_encode.html
TWIG 538
Functions
• attribute70
• block71
• constant72
• country_names73
• country_timezones74
• currency_names75
• cycle76
• date77
• dump78
• html_classes79
• include80
• language_names81
• locale_names82
• max83
• min84
• parent85
• random86
• range87
• script_names88
• source89
• template_from_string90
• timezone_names91
70 https://twig.symfony.com/doc/3.x/functions/attribute.html
71 https://twig.symfony.com/doc/3.x/functions/block.html
72 https://twig.symfony.com/doc/3.x/functions/constant.html
73 https://twig.symfony.com/doc/3.x/functions/country_names.html
74 https://twig.symfony.com/doc/3.x/functions/country_timezones.html
75 https://twig.symfony.com/doc/3.x/functions/currency_names.html
76 https://twig.symfony.com/doc/3.x/functions/cycle.html
77 https://twig.symfony.com/doc/3.x/functions/date.html
78 https://twig.symfony.com/doc/3.x/functions/dump.html
79 https://twig.symfony.com/doc/3.x/functions/html_classes.html
80 https://twig.symfony.com/doc/3.x/functions/include.html
81 https://twig.symfony.com/doc/3.x/functions/language_names.html
82 https://twig.symfony.com/doc/3.x/functions/locale_names.html
83 https://twig.symfony.com/doc/3.x/functions/max.html
84 https://twig.symfony.com/doc/3.x/functions/min.html
85 https://twig.symfony.com/doc/3.x/functions/parent.html
86 https://twig.symfony.com/doc/3.x/functions/random.html
87 https://twig.symfony.com/doc/3.x/functions/range.html
88 https://twig.symfony.com/doc/3.x/functions/script_names.html
89 https://twig.symfony.com/doc/3.x/functions/source.html
90 https://twig.symfony.com/doc/3.x/functions/template_from_string.html
91 https://twig.symfony.com/doc/3.x/functions/timezone_names.html
TWIG 539
31.7.1: Documentation
There are some docs at https://www.drupal.org/docs/contributed-modules/twig-tweak/rendering-
blocks-with-twig-tweak
and a cheat sheet at https://git.drupalcode.org/project/twig_tweak/-/blob/3.x/docs/cheat-sheet.md
1 {{ drupal_block('plugin_id') }}
It looks like the best source of information is really in the source file92 or at: web/modules/con-
trib/twig_tweak/src/TwigExtension.php
All the variations are listed there including the instructions to get drush to list all the blocks on your
site.
1 drush ev "print_r(array_keys(\Drupal::service('plugin.manager.block')->getDefinition\
2 s()));"
92 https://git.drupalcode.org/project/twig_tweak/-/blob/3.x/src/TwigTweakExtension.php
TWIG 540
1 {{ drupal_block('views_exposed_filter_block:news_listing_for_news_landing-page_1\
2 ') }}
1 <div class="l-sidebar-content">
2 {% include '@danaprime/partials/subnav.html.twig' %}
3 {{content.field_ref_sidebars}}
4 {{ drupal_view('news', 'embed_page_sidebar', content.field_news_categories|render|\
5 trim) }}
6 </div>
You can, also specify additional parameters which map to contextual filters you have configured in
your view.
93 https://www.drupal.org/project/twig_tweak
TWIG 541
1 <div>
2 {% for filter in filter_data %}
3 {% if filter.type == 'office' %}
4 {% for item in filter.info %}
5 type={{ filter.type }}, title = {{ item.title }}, value = {{ item.value }}, \
6 selected= {{ item.selected }}, item.type = {{ item.type }}<br>
7 {% set officetype = '' %}
8 {% if item.type is defined and item.type %}
9 {% set office_type = 'type=\"' ~ item.type ~ '"' %}
10 Office_type:{{ office_type }} <br>
11 {% endif %}
12 {% endfor %}
13 {% endif %}
14 {% endfor %}
15 </div>
31.8: Troubleshooting
and
1 /**
2 * Enable local development services.
3 */
4 $settings['container_yamls'][] = DRUPAL_ROOT . '/sites/development.services.yml';
You also need to disable the render cache in settings.local.php. Here all caching is disabled with:
1 $config['system.performance']['css']['preprocess'] = FALSE;
2 $config['system.performance']['js']['preprocess'] = FALSE;
3 $settings['cache']['bins']['render'] = 'cache.backend.null';
4 $settings['cache']['bins']['page'] = 'cache.backend.null';
5 $settings['cache']['bins']['dynamic_page_cache'] = 'cache.backend.null';
1 <pre>
2 Dump node.created.value:
3 {{ dump(node.created.value) }}
4 Dump node.changed.value:
5 {{ dump(node.changed.value) }}
6 Dump node.published_at.value:
7 {{ dump(node.published_at.value) }}
8 </pre>
The output might look like this. Note the published value may be null as I didn’t use Drupal
scheduling to publish the node:
1 Dump node.created.value:
2 string(10) \"1604034000\"
3
4 Dump node.changed.value:
5 string(10) \"1604528207\"
6
7 Dump node.published_at.value:
8 NULL
1 <pre>
2 {{ dump(paragraph.field_ref_tax.value) }}
3 </pre>
1 array(2) {
2 [0]=>
3 array(4) {
4 ["target_id"]=>
5 string(2) "13"
6 ["_attributes"]=>
7 array(0) {
8 }
9 ["_loaded"]=>
10 bool(true)
11 ["_accessCacheability"]=>
12 object(Drupal\Core\Cache\CacheableMetadata)#7683 (3) {
13 ["cacheContexts":protected]=>
14 array(1) {
15 [0]=>
16 string(16) "user.permissions"
17 }
18 ["cacheTags":protected]=>
19 array(0) {
20 }
21 ["cacheMaxAge":protected]=>
22 int(-1)
23 }
24 }
25 [1]=>
26 array(4) {
27 ["target_id"]=>
28 string(2) "16"
29 ["_attributes"]=>
30 array(0) {
31 }
32 ["_loaded"]=>
33 bool(true)
34 ["_accessCacheability"]=>
35 object(Drupal\Core\Cache\CacheableMetadata)#7705 (3) {
36 ["cacheContexts":protected]=>
37 array(1) {
TWIG 545
38 [0]=>
39 string(16) "user.permissions"
40 }
41 ["cacheTags":protected]=>
42 array(0) {
43 }
44 ["cacheMaxAge":protected]=>
45 int(-1)
46 }
47 }
48 }
1 {{ kint(content) }}
1 {{ dump(content) }}
And dump a value from a paragraph field. The pre tags will format the output a little more sanely.
1 <pre>
2 {{ dump(paragraph.field_ref_tax.value) }}
3 </pre>
1 {{ kint(content['body']) }}
1 {{ kint(content['field_tags']) }}
1 {% if content.field_landing_opinion_page_type|render|striptags|trim == 'ORD' %}
2 {{ drupal_block('opinion_landing', wrapper=false) }}
3 {% endif %}
31.9: Reference
• Drupal 10 uses Twig 3. Drupal 9 uses Twig 2. Drupal 8 used Twig 1.
• Theme system overview on api.drupal.org94
• Twig 3 documentation95
• Drupal.org Theming documentation96
• Handy Twig functions you can use directly in templates - Updated Jan 202397
• Twig Tweak 3 cheat sheet Updated October 202298
• Twig Tweak 2 cheat sheet Updated May 202299
• Using attributes in templates updated March 2023100
• Twig tweaks and Views has some useful notes on using twig tweak with views - Updated
November 2020101
94 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21theme.api.php/group/themeable/10
95 https://twig.symfony.com/doc/3.x/
96 https://www.drupal.org/docs/theming-drupal
97 https://www.drupal.org/docs/theming-drupal/twig-in-drupal/functions-in-twig-templates
98 https://git.drupalcode.org/project/twig_tweak/-/blob/3.x/docs/cheat-sheet.md
99 https://www.drupal.org/docs/contributed-modules/twig-tweak-2x/cheat-sheet#s-view-filter
100 https://www.drupal.org/docs/8/theming-drupal-8/using-attributes-in-templates
101 https://www.drupal.org/docs/8/modules/twig-tweak/twig-tweak-and-views