0% found this document useful (0 votes)
37 views

Drupal 10

Uploaded by

anonym
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
37 views

Drupal 10

Uploaded by

anonym
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 566

Drupal at your fingertips

A Drupal 9 & 10 developer’s quick code reference

Selwyn Polit
This book is for sale at http://leanpub.com/drupal10

This version was published on 2023-09-12

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.

© 2023 Selwyn Polit


Contents

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

3: Batch Processing and the Drupal Queue System . . . . . . . . . . . . . . . . . . . . . . . . . 34


3.1: Batch Processing Using the Batch API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.1.1: Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.1.2: Using the Batch API with a form . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.1.3: Using the Batch API from a controller . . . . . . . . . . . . . . . . . . . . . . . . . . 40
3.1.4: Using the Batch API with hook_update . . . . . . . . . . . . . . . . . . . . . . . . . 43
3.1.5: Important rules about functions when using Batch API . . . . . . . . . . . . . . . . 43
3.1.6: Looking at the source . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
3.1.6.1: Passing parameters to the functions in a batch operation . . . . . . . . . . 44
3.2: Queue System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
3.3: Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48

4: Caching and cache tags . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50


4.1: How to uncache a particular page or node . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
4.2: Don’t cache data returned from a controller . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
4.3: Disable caching for a content type . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
CONTENTS

4.4: Considering caching when retrieving query, get or post parameters . . . . . . . . . . . . 51


4.5: Debugging Cache tags . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
4.6: Using cache tags . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
4.7: Setting cache keys in a block . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
4.8: Getting Cache Tags and Contexts for a block . . . . . . . . . . . . . . . . . . . . . . . . . . 54
4.9: Caching REST Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
4.10: Caching in an API class wrapper . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
4.11: Caching in a .module file . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
4.12: Logic for caching render arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
4.13: Development Setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
4.13.1: Disable caching and enable TWIG debugging . . . . . . . . . . . . . . . . . . . . . 59
4.13.2: Disable Cache for development . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
4.14: How to specify the cache backend . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
4.14.1: class ChainedFastBackend . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
4.14.2: APCu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
4.15: Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68

5: Composer, Updates and Patches . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69


5.1: Creating a local patch to a contrib module . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
5.2: Patch modules using patches on Drupal.org . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
5.2.1: Step by step . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
5.3: Patches from a Gitlab merge request . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
5.4: composer.json patches in separate file . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
5.5: Stop files being overwritten during composer operations . . . . . . . . . . . . . . . . . . . 74
5.6: Updating Drupal Core . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
5.7: Test composer (dry run) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
5.8: Version constraints . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
5.9: Allowing multiple versions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
5.10: Troubleshooting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
5.10.1: Composer won’t update Drupal core . . . . . . . . . . . . . . . . . . . . . . . . . . 77
5.10.2: The big reset button . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
5.11: Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78

6: Configuration and Settings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79


6.1: Load some config . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
6.2: Views . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
6.3: Add config to an existing module . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
6.4: Import something you changed in your module . . . . . . . . . . . . . . . . . . . . . . . . 81
6.5: Config Storage in the database . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
6.6: Add some config to site config form . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
6.7: Override config in settings.php . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
6.8: Setup a testing variable in config for a project . . . . . . . . . . . . . . . . . . . . . . . . . 83
6.9: Getting and setting configuration with drush . . . . . . . . . . . . . . . . . . . . . . . . . . 84
CONTENTS

6.10: Creating a module allowing users to edit/update some config . . . . . . . . . . . . . . . 86


6.11: Drush config commands . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
6.11.1: View config . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
6.11.2: Viewing overridden config values . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
6.11.3: Delete from config . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
6.11.4: Check what has changed with config:status . . . . . . . . . . . . . . . . . . . . . . 89
6.11.5: Export entire config . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
6.11.6: Import config changes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90

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

8: Dates and Times . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97


8.1: Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
8.2: Retrieve a date field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
8.3: Retrieve date range field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
8.4: Formatting date range fields . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
8.5: Formatting a date string with an embedded timezone . . . . . . . . . . . . . . . . . . . . . 98
8.6: Formatting a date range for display . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
8.7: Saving date fields . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
8.8: Create DrupalDateTime objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
8.8.1: Create DrupalDateTime objects with timezones . . . . . . . . . . . . . . . . . . . . 101
8.9: Create a DrupalDateTime object and display as a year only . . . . . . . . . . . . . . . . . 102
8.10: Formatting node created time with Drupal date.formatter service . . . . . . . . . . . . . 102
8.11: Date arithmetic example 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
8.12: Date arithmetic example 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
8.13: Comparing DrupalDateTime values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
8.14: Comparing dates (without comparing times) . . . . . . . . . . . . . . . . . . . . . . . . . . 105
8.15: Comparing Dates to see if a node has expired . . . . . . . . . . . . . . . . . . . . . . . . . 106
8.16: Node creation and changed dates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
8.17: Query the creation date using entityQuery . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
8.18: Query a date field with no time . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
CONTENTS

8.19: Query a date field with a time . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111


8.20: Smart Date . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
8.20.1: Smart date: Load and format . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
8.20.2: Smart date: all-day . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
8.20.3: Smart date: Range of values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
8.21: Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
8.21.1: Date field storage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
8.21.2: DrupalDateTime API reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
8.21.3: UTC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
8.21.4: Unix epoch timestamps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
8.21.5: Links . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117

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

10: Development . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136


10.1: Local Drupal site setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
10.1.0.1: First Option . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
10.1.0.2: Second Option . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
10.2: Checking Your Permissions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
CONTENTS

10.3: Converting existing site (non-composer based) to use composer . . . . . . . . . . . . . . 137


10.4: Composer best practices for Drupal 8 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
10.5: DDEV . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
10.5.1: Local config - your .ddev/config.local.yaml . . . . . . . . . . . . . . . . . . . . . . 138
10.5.1.1: NFS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
10.5.1.2: Mutagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
10.5.2: setup aliases in ddev . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
10.5.3: Upgrading ddev . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
10.5.4: Show others your ddev local site . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
10.5.5: Email Capture and Review . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
10.5.6: DDEV and Xdebug . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
10.5.7: Command line or drush debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
10.5.8: Use drush commands in your shell with DDEV . . . . . . . . . . . . . . . . . . . . 142
10.5.9: Load your data from an Acquia site . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
10.5.10: Cleanup some disk space . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
10.5.11: Accessing specific containers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
10.6: DDEV Troubleshooting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
10.6.1: Running out of docker disk space . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
10.6.2: DDEV won’t start . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
10.7: PHPStorm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
10.7.1: Setting up PHPStorm and Drupal . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
10.7.2: PHPStorm and Xdebug . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
10.7.2.1: add a breakpoint in code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
10.7.3: Collecting PhpStorm debugging logs . . . . . . . . . . . . . . . . . . . . . . . . . . 147
10.8: Troubleshooting Xdebug with DDEV . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
10.9: What is listening on port 9000? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
10.10: Setup settings.local.php and disable Cache . . . . . . . . . . . . . . . . . . . . . . . . . . 149
10.11: Development.services.yml . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
10.12: Enable twig debugging output in source . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
10.13: Kint . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
10.13.1: Setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
10.13.2: Add kint to a custom module . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
10.13.3: Dump variables in a TWIG template . . . . . . . . . . . . . . . . . . . . . . . . . . 154
10.13.4: Kint::dump . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
10.13.5: Set max levels to avoid running out of memory . . . . . . . . . . . . . . . . . . . 154
10.14: Replacing deprecated functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
10.15: Missing module . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
10.16: You have requested a non-existent service . . . . . . . . . . . . . . . . . . . . . . . . . . 155
10.17: Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155

11: Email . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156


11.1: Send email . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
11.2: Using tokens in hook_mail . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
CONTENTS

11.3: Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158

12: Entities . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159


12.1: Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
12.2: Config entity types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
12.3: Content entity types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
12.4: Query an entity by title and type . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
12.5: Create an entity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
12.6: Save an entity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
12.7: Create article node entity with attached image . . . . . . . . . . . . . . . . . . . . . . . . 160
12.8: Update a node entity and add some terms . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
12.9: Get the entity type and content type (or bundle type) . . . . . . . . . . . . . . . . . . . . 161
12.10: Identify entities . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
12.11: Create a file entity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
12.12: Entity Validation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
12.13: Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164

13: Forms, Form API and AJAX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166


13.1: Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
13.2: Find a form id in the page source . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
13.3: Add buttons to your custom forms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
13.4: Modify a button on a form with hook_form_alter . . . . . . . . . . . . . . . . . . . . . . 168
13.5: Hide a field with hook_form_alter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
13.6: Hide revision info and moderation state . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
13.7: Multiple fields on the same controller/page . . . . . . . . . . . . . . . . . . . . . . . . . . 170
13.8: Conditional fields and field states API (#states) . . . . . . . . . . . . . . . . . . . . . . . . 171
13.8.1: Conditional fields in a form . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
13.8.2: Conditional fields in node add or edit form . . . . . . . . . . . . . . . . . . . . . . 172
13.9: Get the key and value from a select drop-down . . . . . . . . . . . . . . . . . . . . . . . . 174
13.10: Autocomplete . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
13.10.1: Add an autocomplete taxonomy field . . . . . . . . . . . . . . . . . . . . . . . . . 174
13.10.2: Add a views-driven entity autocomplete field . . . . . . . . . . . . . . . . . . . . 175
13.10.3: Disable autocomplete for user login and password fields . . . . . . . . . . . . . . 175
13.11: Validating input . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
13.11.1: Validate string length . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
13.11.2: Validate an email . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
13.11.3: Validate date . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
13.11.4: Validate a node add or edit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
13.12: Displaying Forms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
13.12.1: Embedding a form: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
13.12.2: Show a form in a block . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
13.12.3: Provide a block template for a form in a block . . . . . . . . . . . . . . . . . . . . 180
13.13: Redirecting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
CONTENTS

13.13.1: Form submission with redirect . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182


13.13.2: Ajax redirect . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
13.13.3: AJAX redirect from a select element (dropdown) . . . . . . . . . . . . . . . . . . 183
13.14: Add Javascript to a form . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187
13.15: AJAX Forms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
13.15.1: Popup an AJAX modal dialog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
13.15.2: AJAX modal dialog with redirect example . . . . . . . . . . . . . . . . . . . . . . 190
13.15.2.1: Ajax submit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191
13.15.2.2: Ajax redirect . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191
13.15.3: AJAX redirect from a select element (dropdown) . . . . . . . . . . . . . . . . . . 192
13.15.4: Update a value in another field(I am I want) using AJAX . . . . . . . . . . . . . 195
13.15.5: I Am I Want revisited . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
13.15.5.1: Custom responses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205
13.15.6: How do you find all the possible AJAX commands to use with addCommand()? 207
13.15.7: Another AJAX Submit example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
13.16: Config Forms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209
13.16.1: Generate a config form with drush . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
13.16.2: Config forms overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
13.17: The basics of implementing forms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
13.17.1: Location . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
13.17.2: Base Classes for forms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
13.17.3: Create your form class by extending Formbase . . . . . . . . . . . . . . . . . . . 212
13.17.4: The main methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
13.17.4.1: getFormId() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
13.17.4.2: buildForm() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
13.17.4.3: submitForm() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
13.17.5: Form validation example #1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214
13.17.6: Form Validation example #2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215
13.17.7: Field attributes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215
13.17.8: Form Elements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216
13.17.9: Retrieving field values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217
13.18: Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218

14: General . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219


14.1: Get the current user . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
14.2: Get the logged in user name and email . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
14.3: Check if you are on the Front page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220
14.4: Check if site is in system maintenance mode . . . . . . . . . . . . . . . . . . . . . . . . . . 220
14.5: Get Node URL alias or Taxonomy Alias by Node id or Term ID . . . . . . . . . . . . . . 220
14.6: Taxonomy alias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
14.7: Get current Path . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
14.8: Get current nid, node type and title . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222
14.9: How to check whether a module is installed or not . . . . . . . . . . . . . . . . . . . . . . 223
CONTENTS

14.10: Get current Route name . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223


14.11: Get the current page title . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
14.12: Get the current user . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
14.13: Check if you are on the Front page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
14.14: Check if site in system maintenance mode . . . . . . . . . . . . . . . . . . . . . . . . . . 225
14.15: Retrieve query and get or post parameters ($_POST and $_GET) . . . . . . . . . . . . . 225
14.16: Retrieve URL argument parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 226
14.17: Get Current Language in a constructor . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227
14.18: Add a variable to any page on the site . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228
14.19: Add a variable to be rendered in a node. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229
14.20: Add a bunch of variables to be rendered in a node . . . . . . . . . . . . . . . . . . . . . 230
14.21: Grabbing entity reference fields in hook_preprocess_node for injection into the twig
template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232
14.22: Render a list created in the template_preprocess_node() . . . . . . . . . . . . . . . . . . 232
14.23: Indexing paragraphs so you can theme the first one . . . . . . . . . . . . . . . . . . . . . 233
14.24: Add meta tags using template_preprocess_html . . . . . . . . . . . . . . . . . . . . . . . 234
14.25: How to strip % characters from a string . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236
14.26: Remote media entities . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236
14.27: Deprecated functions like drupal_set_message . . . . . . . . . . . . . . . . . . . . . . . . 237

15: Hooks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238


15.1: Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238
15.2: Modify the login form . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238
15.3: Modify the node edit form . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239
15.4: Modify fields in a node . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
15.5: hook_update . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242
15.6: Theme hooks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242
15.6.1: Hook_preprocess . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243
15.6.2: hook_preprocess_node example 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243
15.6.3: hook_preprocess_node example 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 244
15.7: Organizing your hooks code the OOP way . . . . . . . . . . . . . . . . . . . . . . . . . . . 245
15.8: Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249
15.8.1: Entity hooks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249
15.8.1.1: Create operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249
15.8.1.2: Read/Load operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250
15.8.1.3: Save operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251
15.8.1.4: Editing operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 252
15.8.1.5: Delete operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 252
15.8.1.6: View/render operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253
15.8.1.7: Other entity hooks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254
15.8.2: Theme hooks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255
15.8.2.1: Overriding Theme Hooks . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256
15.8.2.2: Preprocessing for Template Files . . . . . . . . . . . . . . . . . . . . . . . . 256
CONTENTS

15.8.2.3: Theme hook suggestions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257


15.8.2.4: Altering theme hook suggestions . . . . . . . . . . . . . . . . . . . . . . . 257
15.8.3: Reference Links . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 258

16: Learning and keeping up with Drupal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259


16.1: Free videos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259
16.2: Blogs and articles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259
16.3: Pay videos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259
16.4: Drupal Training . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259
16.5: Keep up with Drupal news . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 260
16.6: Drupal Podcasts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 260
16.7: Books . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 260

17: Links, Aliases and URLs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262


17.1: Create an external url . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262
17.2: Create an internal url . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262
17.3: The Drupal Core Url Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263
17.4: The Drupal Core Link Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263
17.4.1: Create a link to a node . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263
17.4.2: Create a link to a path with parameters . . . . . . . . . . . . . . . . . . . . . . . . . 263
17.5: Another way to create a link to a node: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264
17.6: Create a link from an internal URL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264
17.7: Check if a link field is empty . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264
17.8: Retrieve a link field from a node or a paragraph . . . . . . . . . . . . . . . . . . . . . . . . 264
17.9: Retrieve a URL field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265
17.9.1: External links . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265
17.9.2: Internal links . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
17.10: Get the NID from a URL Alias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
17.11: Get the Taxonomy Term ID from a URL alias . . . . . . . . . . . . . . . . . . . . . . . . 267
17.12: Get URL alias for a taxonomy term . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
17.13: Get the User ID from a URL alias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
17.14: Get the URL alias for a node . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
17.15: Create a Node Alias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268
17.16: Get the current Path . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268
17.17: Get current nid, node type and title . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269
17.18: How to get current Route name . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270
17.19: Get current Document root path . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270
17.20: Retrieve URL argument parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271
17.21: Retrieve query and GET or POST parameters ($_POST and $_GET) . . . . . . . . . . . 271
17.22: Modify URL Aliases programmatically with hook_pathauto_alias_alter . . . . . . . . 272
17.23: Drupal l() is deprecated . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274
17.24: Reference links . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274

18: Logging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275


CONTENTS

18.1: Quick log to watchdog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275


18.2: Log an email notification was sent to the the email address for the site. . . . . . . . . . 275
18.3: Logging from a service using dependency injection . . . . . . . . . . . . . . . . . . . . . 276
18.4: Another example using the logging via dependency injection . . . . . . . . . . . . . . . 277
18.5: Logging exceptions from a try catch block . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
18.6: Display a message in the notification area . . . . . . . . . . . . . . . . . . . . . . . . . . . 280
18.7: Display a variable while debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281
18.8: Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281

19: Menus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283


19.1: Dynamically change menu items with hook_preprocess_menu . . . . . . . . . . . . . . 283
19.2: Permanently update menu links in a hook_update using entityQuery . . . . . . . . . . 283
19.3: Add menu items with hook_update . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284
19.4: Permanently modify or delete menu items with hook_update . . . . . . . . . . . . . . . 285
19.5: Peer up a menu to its parents to see if it is a child of a content type . . . . . . . . . . . . 286
19.6: Find all the children of a menu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 288
19.7: Build a menu and all its children . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291
19.8: Create custom Twig extension for rendering a menu . . . . . . . . . . . . . . . . . . . . . 292
19.9: Active Trail . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 294
19.10: Get a node’s menu item and more . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 296
19.11: Create menu items in your custom module . . . . . . . . . . . . . . . . . . . . . . . . . . 296
19.12: Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 297

20: Migration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 298


20.1: Import content from another Migration into Paragraphs . . . . . . . . . . . . . . . . . . 298
20.2: Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 299

21: Modal Dialogs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300


21.1: Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300
21.2: Dialog title . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300
21.3: Links to slide-in dialogs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300
21.4: Modal dialog example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301
21.4.1: Passing entities as parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 308
21.5: Modal form example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 309
21.6: Slide-in dialog/Off-canvas dialog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 318
21.7: Slide-in Dialog Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 318
21.8: Block with a link to popup a custom modal dialog . . . . . . . . . . . . . . . . . . . . . . 327
21.9: No-code modal dialogs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 329
21.10: Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330

22: Nodes and Fields . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331


22.1: Load a node and get a formatted text field . . . . . . . . . . . . . . . . . . . . . . . . . . . 331
22.2: Load a numeric field value . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331
22.3: Set field values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331
CONTENTS

22.4: Get current page title . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331


22.5: Test if variable is a node . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332
22.6: Get the current nid, node type and title . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332
22.7: Retrieve current node id (nid) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 333
22.8: Retrieve node info from current path . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 334
22.9: Load the current node and get it’s node id (nid), field, type . . . . . . . . . . . . . . . . . 334
22.10: Load a node by nid and get its title, type and a field . . . . . . . . . . . . . . . . . . . . 335
22.11: Load the current node and get the nid, field, type . . . . . . . . . . . . . . . . . . . . . . 336
22.12: Load the user id (uid) for a node . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
22.13: Test if a field is empty . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
22.14: Load a node and update a field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
22.15: Load values from a date range field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 338
22.16: Load multivalue field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 338
22.16.1: Iterate through results . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 338
22.16.2: Read a specific instance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
22.17: Update a multivalue field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340
22.17.1: Function to read and write multivalue fields . . . . . . . . . . . . . . . . . . . . . 340
22.17.2: Save multivalue field, entity reference field . . . . . . . . . . . . . . . . . . . . . . 342
22.17.3: Update a multivalue entity reference fields . . . . . . . . . . . . . . . . . . . . . . 342
22.17.4: Generic Multivalue field writer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343
22.18: Does this field exist in my entity? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 349
22.19: Get URL for an image or file in a media reference field . . . . . . . . . . . . . . . . . . . 349
22.20: Retrieve info about a file field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 350
22.21: Retrieve a link field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351
22.22: Does this field exist in my entity? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351
22.23: Create a node and write it to the database . . . . . . . . . . . . . . . . . . . . . . . . . . 352
22.24: Create a node with an image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
22.25: Write a node with an attached file . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
22.26: Write a date or datetime to a node . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 354
22.27: Or just a date (no time) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 354
22.28: Set field values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 354
22.29: Set an entity reference field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 355
22.30: Set multivalue fields (regular and entity reference) . . . . . . . . . . . . . . . . . . . . . 355
22.31: Clear a text field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
22.32: Set or clear a body field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
22.33: Load a node and retrieve an entity reference node and nid (target_id) . . . . . . . . . 357
22.33.1: Load a multivalue reference field. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357
22.34: Entity reference nodes and their fields . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357
22.35: Load the taxonomy terms from a term reference field . . . . . . . . . . . . . . . . . . . 358
22.36: Load a node and find the terms referenced in a paragraph in a term reference field . 358
22.37: Retrieve a URL field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 359
22.37.1: External links . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 359
22.37.2: Internal links . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 359
CONTENTS

22.38: Load a node and retrieve a paragraph field . . . . . . . . . . . . . . . . . . . . . . . . . . 360


22.39: How to get Node URL alias or Taxonomy Alias by Node id or Term ID . . . . . . . . . 362
22.40: How to set a URL Alias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 362
22.41: Get a node’s menu item and more . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 363
22.42: Find a node using it’s uuid . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 363
22.43: Retrieve Node ID(NID) or Taxonomy term ID from a Drupal alias or path . . . . . . . 364
22.44: Retrieve all nodes with a matching taxonomy term . . . . . . . . . . . . . . . . . . . . . 365
22.45: How to uncache a particular page or node . . . . . . . . . . . . . . . . . . . . . . . . . . 365
22.46: Get boolean Field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 366
22.47: Date Field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 366
22.48: Date Fields . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 367
22.49: Date Range Field doesn’t display correct timezone . . . . . . . . . . . . . . . . . . . . . 368
22.50: Date Range . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 370
22.51: Date Range fields: Load start and end values . . . . . . . . . . . . . . . . . . . . . . . . . 370
22.52: Date Fields: Load or save them . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
22.53: Comparing DrupalDateTime values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372
22.54: Date with embedded timezone . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372
22.55: Has something expired? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 373
22.56: Load or save Drupal Date fields . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 373
22.57: Retrieve node creation date and format it . . . . . . . . . . . . . . . . . . . . . . . . . . . 374
22.58: Retrieve node creation or changed date and format it . . . . . . . . . . . . . . . . . . . . 375
22.59: Date Field and Date with no time (remove time) . . . . . . . . . . . . . . . . . . . . . . . 375
22.60: Smart date (smart_date) load and format . . . . . . . . . . . . . . . . . . . . . . . . . . . 376
22.61: Smart date (smart_date) all-day . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377
22.62: Smart date (smart_date) range of values . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377
22.63: hook_node_presave or hook_entity_type_presave . . . . . . . . . . . . . . . . . . . . . . 377
22.64: Disable caching for a content type . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
22.65: Writing some JSON data into a long text field . . . . . . . . . . . . . . . . . . . . . . . . 380
22.66: Create a node with an image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 381
22.67: Paragraphs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 381
22.68: Load a node and find the terms referenced in a paragraph in a term reference field . 382
22.69: Custom Field Formatter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382
22.70: Puzzles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 385
22.70.1: What can I do with a call to first() on an entity reference field? . . . . . . . . . 385
22.71: Great Cheat sheets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 385

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

23.7: Guzzle Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 390


23.8: HTTP response status codes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 390
23.9: Reading from an API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 391
23.10: Download a file using guzzle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 394
23.11: Download a file using curl in PHP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 395
23.12: Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 398

24: Queries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 399


24.1: entityQuery . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 399
24.1.1: Find matching nodes - example 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 399
24.1.2: Find matching nodes - example 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 399
24.1.3: Find matching article nodes–example 3 . . . . . . . . . . . . . . . . . . . . . . . . . 400
24.1.4: Find nodes that match a taxonomy term . . . . . . . . . . . . . . . . . . . . . . . . 401
24.1.5: Find 5 nodes that have a matching taxonomy term . . . . . . . . . . . . . . . . . . 401
24.1.6: Find matching nodes and delete them . . . . . . . . . . . . . . . . . . . . . . . . . . 402
24.1.7: Slice up entityQuery results into batches of 100 nodes . . . . . . . . . . . . . . . . 402
24.1.8: Query the creation date (among other things) using entityQuery . . . . . . . . . 403
24.1.9: entityQuery frequently used conditions . . . . . . . . . . . . . . . . . . . . . . . . . 404
24.1.10: Update menu items programatically . . . . . . . . . . . . . . . . . . . . . . . . . . 405
24.1.11: Query multi-value fields . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 405
24.1.12: Query entity reference fields if they have a value or no value . . . . . . . . . . . 406
24.1.13: Query entity reference fields . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 406
24.2: Static and dynamic Queries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 407
24.2.1: Static Queries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 407
24.2.2: Get a connection object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 407
24.2.3: SQL select example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 408
24.2.4: Find the biggest value in a field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 408
24.2.5: SQL update query - example 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 409
24.2.6: SQL update query - example 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 409
24.2.7: SQL update query - example 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 410
24.2.8: SQL insert . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 411
24.2.9: SQL Insert Query . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 411
24.2.10: SQL Delete query . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 412
24.2.11: Paragraph query . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 413
24.2.12: Create a custom table for your module . . . . . . . . . . . . . . . . . . . . . . . . 414
24.3: Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 415

25: Redirects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 417


25.1: Redirect to an internal url . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 417
25.2: Redirect in a form . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 417
25.3: Redirect off-site (to a third-party URL) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 418
25.4: Redirect to an existing route with an anchor (or fragment) . . . . . . . . . . . . . . . . . 418
25.5: Redirect to a complex route . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 418
CONTENTS

25.6: Redirect in a controller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 419


25.7: Redirect user after login . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 420
25.8: Redirect to the 403 or 404 page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 420
25.9: Redirect to a new page after node operation . . . . . . . . . . . . . . . . . . . . . . . . . . 421
25.10: Redirect dynamically to wherever you came from . . . . . . . . . . . . . . . . . . . . . . 422

26: Render Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 424


26.1: Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 424
26.2: Overview of the Theme system and Render API. . . . . . . . . . . . . . . . . . . . . . . . 424
26.3: Caching . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 424
26.4: Properties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 426
26.5: Image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 428
26.6: Simple Text . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 428
26.7: Text with variable substitution (Placeholders) . . . . . . . . . . . . . . . . . . . . . . . . . 428
26.8: Wrap an element with a div with a class . . . . . . . . . . . . . . . . . . . . . . . . . . . . 429
26.9: Prefix and suffix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 429
26.10: Date . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 429
26.11: Image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 430
26.12: Several Url’s. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 430
26.13: Two paragraphs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 431
26.14: A button that opens a modal dialog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 432
26.15: A link . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 432
26.16: A link with a class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 433
26.17: A link and its TWIG template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 433
26.18: A link with parameters and a template file . . . . . . . . . . . . . . . . . . . . . . . . . . 434
26.19: Simple unordered list . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436
26.20: Unordered list of links for a menu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436
26.21: Nested Unordered List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 437
26.22: Select (dropdown) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
26.23: Select (dropdown) Ajaxified . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
26.24: Limit allowed tags in markup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 439
26.25: Disable an element . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 440
26.26: Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 441

27: Routes and Controllers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 442


27.1: Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 442
27.1.1: Route . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 442
27.1.2: Controller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 442
27.1.3: Connecting to a twig template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 442
27.2: Simple page without arguments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 443
27.3: Page with arguments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 444
27.4: Simple form . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 444
27.5: Admin form (or settings form) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445
CONTENTS

27.6: Routing permissions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445


27.6.1: A specific permission . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445
27.6.2: Multiple permissions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 446
27.7: Set the page title dynamically . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 446
27.8: Disable caching on a route . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 447
27.9: Generate route and controller with Drush . . . . . . . . . . . . . . . . . . . . . . . . . . . 447
27.10: Finding routes with Drush . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 449
27.10.1: All routes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 449
27.10.2: Specific path . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 450
27.10.3: Specific route name . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 450
27.11: Getting some help from Chat GPT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 451
27.12: Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 453

28: Services and Dependency Injection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 454


28.1: Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 454
28.2: Static . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 454
28.3: Static Shorthand methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 454
28.4: Services in action . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 455
28.5: ControllerBase shortcuts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 457
28.6: Injected/Dependency Injection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 457
28.6.1: Controller details . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 458
28.6.2: Controller Example 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 460
28.6.3: Controller Example 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 461
28.7: Finding services . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 463
28.8: Creating a custom service . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 464
28.8.1: Arguments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 465
28.8.2: Passing the config factory to our service . . . . . . . . . . . . . . . . . . . . . . . . 465
28.8.3: Taxonomy Tree Custom Service . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 466
28.9: Using your custom service . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 468
28.10: Dependency Injection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 469
28.10.1: Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 469
28.10.2: Service Container . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 469
28.10.3: Controller Example 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 470
28.10.4: Controller Example 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 471
28.10.5: Blocks and other plugins . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473
28.11: Procedural to Class-based dependency injection . . . . . . . . . . . . . . . . . . . . . . . 474
28.12: Drush services commands . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 475
28.12.1: List all services . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 476
28.12.2: Generate custom service . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 477
28.13: Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 477

29: State API, TempStore and UserData . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478


29.1: Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478
CONTENTS

29.2: State API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478


29.2.1: Using Drush to read the State API . . . . . . . . . . . . . . . . . . . . . . . . . . . . 480
29.2.2: Example accessing State API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 480
29.2.3: Long strings broken into paragraphs . . . . . . . . . . . . . . . . . . . . . . . . . . . 482
29.3: UserData . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 482
29.4: TempStore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 483
29.4.1: PrivateTempStore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 484
29.4.2: SharedTempStore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 486
29.4.2.1: Injecting tempstore.shared . . . . . . . . . . . . . . . . . . . . . . . . . . . 488
29.5: Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 490

30: Taxonomy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 491


30.1: Lookup term by name . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 491
30.2: Lookup term name using its tid . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 492
30.3: Lookup term using its uuid . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 492
30.4: Load terms from a term reference field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 493
30.5: Find terms referenced in a paragraph in a term reference field . . . . . . . . . . . . . . . 494
30.6: Get URL alias from a term ID . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 494
30.7: Load all terms for a vocabulary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 495
30.8: Load all terms for a vocabulary and put them in a select (dropdown) . . . . . . . . . . . 496
30.9: Create taxonomy term programatically . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 497
30.10: Find all nodes with a matching term . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 497
30.11: Find nodes with a matching term using entityQuery . . . . . . . . . . . . . . . . . . . . 498

31: TWIG . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 500


31.1: Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 500
31.1.1: Theme System Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 500
31.1.2: Twig Templating Engine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 500
31.2: Displaying Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 500
31.2.1: Fields or Logic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 501
31.2.2: Which template, which variables? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 501
31.2.3: Display fields or variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 503
31.2.4: Node Title with and without a link . . . . . . . . . . . . . . . . . . . . . . . . . . . 503
31.2.5: Fields . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 504
31.2.6: Paragraph field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 504
31.2.7: Loop thru paragraph reference fields . . . . . . . . . . . . . . . . . . . . . . . . . . 505
31.2.8: Body . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 506
31.2.9: Multi-value fields . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 506
31.2.10: Fields with HTML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 506
31.2.11: The date/time a node is published, updated or created . . . . . . . . . . . . . . . 507
31.2.12: Format a date field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 508
31.2.13: Smart date field formatting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 509
31.2.14: Entity Reference field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 510
CONTENTS

31.2.15: Entity reference destination content . . . . . . . . . . . . . . . . . . . . . . . . . . 510


31.2.16: Taxonomy term . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 511
31.2.17: Render a block . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 511
31.2.18: Render a list created in the template_preprocess_node() . . . . . . . . . . . . . . 511
31.2.19: Links . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 512
31.2.20: Links to other pages on site . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 513
31.2.21: Link to a user using user id . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 513
31.2.22: External link in a field via an entity reference . . . . . . . . . . . . . . . . . . . . 513
31.2.23: Render an internal link programatically . . . . . . . . . . . . . . . . . . . . . . . . 514
31.2.24: Render an image with an image style . . . . . . . . . . . . . . . . . . . . . . . . . 515
31.2.25: Hide if there is no content in a field or image . . . . . . . . . . . . . . . . . . . . 515
31.2.26: Hide if there is no image present . . . . . . . . . . . . . . . . . . . . . . . . . . . . 516
31.2.27: Attributes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 516
31.2.28: Output the content but leave off the field_image . . . . . . . . . . . . . . . . . . 518
31.2.29: Add a class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 518
31.2.30: Add a class conditionally . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 518
31.2.31: Links to other pages on site . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 518
31.2.32: Loop.index in a paragraph twig template . . . . . . . . . . . . . . . . . . . . . . . 519
31.2.33: Loop thru an array of items with a separator . . . . . . . . . . . . . . . . . . . . . 519
31.3: Add Javascript into a twig template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 520
31.4: Control/Logic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 520
31.4.1: Concatenate values into a string with join . . . . . . . . . . . . . . . . . . . . . . . 520
31.4.2: Include partial templates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 521
31.4.3: Loop through entity reference items . . . . . . . . . . . . . . . . . . . . . . . . . . . 521
31.4.4: IF OR . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 521
31.4.5: Test if a formatted text field is empty . . . . . . . . . . . . . . . . . . . . . . . . . . 521
31.4.6: Test empty variable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 522
31.4.7: Conditionals (empty, defined, even) . . . . . . . . . . . . . . . . . . . . . . . . . . . 522
31.4.8: Test if a paragraph is empty using striptags . . . . . . . . . . . . . . . . . . . . . . 523
31.4.9: Comparing strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 524
31.4.10: Include other templates as partials . . . . . . . . . . . . . . . . . . . . . . . . . . . 524
31.4.11: Check if an attribute has a class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 524
31.4.12: Remove an attribute . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 524
31.4.13: Convert attributes to array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 524
31.5: Views . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 525
31.5.1: Render a view with contextual filter . . . . . . . . . . . . . . . . . . . . . . . . . . . 525
31.5.2: Count how many rows returned from a view . . . . . . . . . . . . . . . . . . . . . 525
31.5.3: If view results empty, show a different view . . . . . . . . . . . . . . . . . . . . . . 526
31.5.4: Selectively pass 1 termid or 2 to a view as the contextual filter . . . . . . . . . . . 526
31.5.5: Views templates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 527
31.5.6: Inject variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 528
31.5.6.1: Same field used twice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 533
31.5.7: Concatenate values into a string with join . . . . . . . . . . . . . . . . . . . . . . . 534
31.5.8: Loop through entity reference items . . . . . . . . . . . . . . . . . . . . . . . . . . . 534
31.6: Twig filters and functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 535
31.7: Twig Tweak . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 538
31.7.1: Documentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 539
31.7.2: Display a block with twig_tweak . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 539
31.7.3: Display filter form block . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 539
31.7.4: Embed view in twig template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 540
31.7.5: Some tricky quotes magic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 540
31.8: Troubleshooting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 541
31.8.1: Enable Twig debugging and disable caches . . . . . . . . . . . . . . . . . . . . . . . 541
31.8.2: Debugging - Dump a variable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 543
31.8.3: Dump taxonomy reference field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 543
31.8.4: Using kint or dump to display variable in a template . . . . . . . . . . . . . . . . . 545
31.8.5: 502 bad gateway error . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 545
31.8.6: Views error . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 545
31.8.7: Striptags (when twig debug info causes if to fail) . . . . . . . . . . . . . . . . . . . 546
31.9: Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 546
1: Introduction
Welcome to Drupal at your fingertips. A Drupal 9 & 10 developer’s quick code reference
This book is a quick reference for developers creating Drupal sites. While working on many Drupal
sites, I gathered a large collection of notes and code which I use for my reference. Here is that
collection, curated and cleaned up to the best of my knowledge. I hope it will be useful for you, too.
During my hunting through examples on Drupal.org1 , Stack Overflow2 and many other places,
I often wished there was a succinct set of current annotated code samples explaining how to do
things the Drupal way3 . Of course, there is the excellent examples module4 , which is very handy.
But this book is designed to otherwise fill that gap. Please forgive the todo items, but I’ve found
that if I want to get it perfect, it won’t ever get published.
This book has it’s origins as an online editable Github project where members of the Drupal
community are invited to contribute.

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.

• Plugin API overview1


• Annotations-based plugins2

2.1: Create a block with Drush generate


Use Drush’s code generation ability to quickly generate the code you need to create your own custom
block.
First generate a module if you don’t have one. Here we generate a module called Block Module with
a machine name: block_module.

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

1 $ drush generate module


2
3 Welcome to module generator!
4
5 ------------------------------------------------------------
6
7 Module name \[Web\]:
8
9 � Block Module
10
11 Module machine name \[block_module\]:
12
13 �
14
15 Module description \[Provides additional functionality for the site.\]:
16
17 � Custom module to explore Drupal blocks
18
19 Package \[Custom\]:
20
21 �
22
23 Dependencies (comma separated):
24
25 �
26
27 Would you like to create module file? \[No\]:
28
29 � yes
30
31 Would you like to create install file? \[No\]:
32
33 �
34
35 Would you like to create libraries.yml file? \[No\]:
36
37 �
38
39 Would you like to create permissions.yml file? \[No\]:
40
41 �
42
43 Would you like to create event subscriber? \[No\]:
Blocks 4

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.

1 $ drush generate block


2
3 Welcome to block generator!
4
5 ----------------------------------------------------------
6
7 Module machine name \[web\]:
8
9 � block_module
10
11 Block admin label \[Example\]:
12
13 � Block Module Example
14
Blocks 5

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

This generates a file at web/modules/custom/block_module/src/Plugin/Block/BlockModuleExampleBlock.php


that looks like this:
Blocks 6

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 }

Enable the module with:


ddev drush en block_module

clear the cache with:


ddev drush cr

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

2.2: Anatomy of a custom block with dependency


injection
The block class PHP file is usually in \<Drupal web root\>/modules/custom/mymodule/src/Plugin/Block.
e.g. dev1/web/modules/custom/image_gallery/src/Plugin/Block/ImageGalleryBlock.php
or
dev1/web/modules/contrib/examples/block_example/src/Plugin/Block/ExampleConfigurableTextBlock.php

Specify namespace:
namespace Drupal\abc_wea\Plugin\Block;

Blocks always extend BlockBase but can also implement other interfaces… see below.
Class ImageGalleryBlock extends BlockBase

If you want to use Dependency Injection, implement: ContainerFactoryPluginInterface


e.g.

1 class ImageGalleryBlock extends BlockBase implements


2 ContainerFactoryPluginInterface {

Be sure to include:

1 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;

And for annotation translation:

1 use Drupal\Core\Annotation\Translation;

You can annotate like this:

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 {

If you want dependency injection, you will need a create() function.


This will call the constructor (to do lazy loading) and call the container to ->get() the service you
need. In the example below $container->get('hello_world.salutation') does the trick. return
new static() calls your class constructor.
Be sure to add your service to the list of parameters in the constructor: $container->get('hello_-
world.salutation').

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()

2.3: Create a block with an entityQuery


You often need to query some data from Drupal and display it in a block.
Here is a simple block that loads all published content of type “page” and renders the titles. You could
sort them by creation date by adding this to the $query variable: ->sort('created' , 'DESC');
Blocks 14

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;

2.4: Create a Block with a corresponding config form


Here is an example which includes a block and a corresponding config form that controls what
is displayed in the block. The block can be placed using the Block Layout system in Drupal at
/admin/structure/block (shown below) or via twig in a template file.
Blocks 15

2.4.1: The config form definition


The config form is defined in docroot/modules/custom/quick_pivot/src/Form/QuickPivotConfigForm.php
with a class which extends ConfigFormBase because this form is there for configuring its block:
class QuickPivotConfigForm extends ConfigFormBase {

In the class are the getFormId(), getEditableConfigName(), buildForm() and submitForm() func-
tions which are all pretty straightforward.

2.4.2: The routing.yml file


Then in docroot/modules/custom/quick_pivot/quick_pivot.routing.yml we specify the route
where we invoke the form.
Besides the quick_pivot.info.yml (module info) file, that should be all you need to make the config
for the block.

2.4.3: The Block definition


Now for the block that users see (also the one that pops up in the block configuration) in
docroot/modules/custom/quick_pivot/src/Plugin/Block/QuickPivotSubscribeBlock.php
Blocks 16

We define the block with its annotation:

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 {

It implements ContainerFactoryPluginInterface to allow dependency injection. This is critical for


plugins or blocks. More at https://chromatichq.com/blog/dependency-injection-drupal-8-plugins.
All this interface defines is the create() method. Because we are using dependency injection, we
need both a create() and a __constructor().
Here is the create()

1 public static function create(ContainerInterface $container, array $configuration, $\


2 plugin_id, $plugin_definition) {
3 return new static(
4 $configuration,
5 $plugin_id,
6 $plugin_definition,
7 $container->get('config.factory'),
8 $container->get('form_builder')
9 );
10 }

Here is the constructor:

1 public function __construct(array $configuration, $plugin_id, $plugin_definition, Co\


2 nfigFactoryInterface $config_factory, FormBuilderInterface $form_builder) {
3 parent::__construct($configuration, $plugin_id, $plugin_definition);
4
5 $this->configFactory = $config_factory;
6 $this->formBuilder = $form_builder;
7 }

And finally the build() method:


Blocks 17

1 public function build() {


2 return $this->formBuilder->getForm('Drupal\quick_pivot\Form\QuickPivotSubscribeFor\
3 m');
4 }

Here is the docroot/modules/custom/quick_pivot/src/Form/QuickPivotSubscribeForm.php:

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

37 '#type' => 'textfield',


38 '#id' => 'quick-pivot-email',
39 '#placeholder' => $this->t('Email address'),
40 '#attributes' => ['class' => ['edit-quick-pivot-email']],
41 '#prefix' => '<div class="subscriber-email-msg">',
42 '#suffix' => '</div>',
43 ];
44 $form['actions']['subscribe_submit'] = [
45 '#type' => 'submit',
46 '#value' => $this->t('Sign Up'),
47 '#name' => 'quick_pivot_subscribe_form_submit_button',
48 '#ajax' => [
49 'callback' => 'Drupal\quick_pivot\Form\QuickPivotSubscribeForm::quickPivotAj\
50 axSubmit',
51 'wrapper' => 'quick-pivot-subscribe-form',
52 'progress' => ['type' => 'throbber', 'message' => NULL],
53 ],
54 ];
55 $form['message'] = [
56 '#type' => 'markup',
57 '#markup' => '<div id="quick-pivot-message-area"></div>',
58 ];
59
60 return $form;
61 }
62
63 /**
64 * {@inheritdoc}
65 */
66 public function validateForm(array &$form, FormStateInterface $form_state) {
67
68 }
69
70 /**
71 * {@inheritdoc}
72 */
73 public function submitForm(array &$form, FormStateInterface $form_state) {
74
75 }
76
77 /**
78 * {@inheritdoc}
79 */
Blocks 19

80 public static function quickPivotAjaxSubmit(array &$form, FormStateInterface $form\


81 _state) {
82 $validate = TRUE;
83 $email = trim($form_state->getValue('email'));
84 if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
85 $message = t('Please enter a valid email address.');
86 $validate = FALSE;
87 $css_border = ['border' => '1px solid red'];
88 $css_color = ['color' => 'red'];
89 }
90 if ($validate) {
91 $css_border = ['border' => '1px solid green'];
92 $css_color = ['color' => 'green'];
93 $response = \Drupal::service('quick_pivot.api')->subscribeEmail($email);
94 if (strpos(reset($response), 'Success') !== FALSE) {
95 $message = t('Thank you for signing up. Your subscription has been activated\
96 .');
97 }
98 else {
99 $message = t('Your subscription could not be processed.');
100 }
101 }
102
103 $response = new AjaxResponse();
104
105 $quick_pivot_form = \Drupal::formBuilder()->rebuildForm('quick_pivot_subscribe_f\
106 orm', $form_state);
107 if ($validate) {
108 $quick_pivot_form['email']['#value'] = '';
109 $quick_pivot_form['email']['#placeholder'] = t('Email address');
110 }
111 $response->addCommand(new ReplaceCommand('#quick-pivot-subscribe-form', $quick_p\
112 ivot_form));
113 $response->addCommand(new CssCommand('#edit-quick-pivot-email', $css_border));
114 $response->addCommand(new HtmlCommand('#quick-pivot-message-area', $message));
115 $response->addCommand(new CssCommand('#quick-pivot-message-area', $css_color));
116 return $response;
117 }
118 }

Here is the entire QuickPivotConfigForm.php file:


Blocks 20

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

44 '#description' => $this->t("Enter the API end point URL."),


45 '#default_value' => $config->get('quick_pivot_settings.api_end_point'),
46 '#required' => TRUE,
47 '#size' => 100,
48 ];
49
50 $form['quick_pivot_settings']['user_guid'] = [
51 '#type' => 'textfield',
52 '#title' => $this->t('User GUID'),
53 '#description' => $this->t("SOAP API User GUID"),
54 '#default_value' => $config->get('quick_pivot_settings.user_guid'),
55 '#required' => TRUE,
56 '#size' => 100,
57 ];
58
59 $form['quick_pivot_settings']['account'] = [
60 '#type' => 'textfield',
61 '#title' => $this->t('Account'),
62 '#description' => $this->t("SOAP API Account"),
63 '#default_value' => $config->get('quick_pivot_settings.account'),
64 '#required' => TRUE,
65 '#size' => 100,
66 ];
67
68 $form['quick_pivot_settings']['sender'] = [
69 '#type' => 'textfield',
70 '#title' => $this->t('Sender'),
71 '#description' => $this->t("SOAP API Sender"),
72 '#default_value' => $config->get('quick_pivot_settings.sender'),
73 '#required' => TRUE,
74 '#size' => 100,
75 ];
76
77 return parent::buildForm($form, $form_state);
78 }
79
80 /**
81 * {@inheritdoc}
82 */
83 public function submitForm(array &$form, FormStateInterface $form_state) {
84
85 $this->config('quick_pivot.settings')
86 ->set('quick_pivot_settings.api_end_point', $form_state->getValue('api_end_p\
Blocks 22

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 }

And the QuickPivotSubscribeBlock.php:

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

31 * The form builder.


32 *
33 * @var \Drupal\Core\Form\FormBuilderInterface
34 */
35 protected $formBuilder;
36
37 /**
38 * Constructor for the QuickPivot subscribe block.
39 *
40 * @param array $configuration
41 * The block configuration.
42 * @param string $plugin_id
43 * The plugin_id for the plugin instance.
44 * @param mixed $plugin_definition
45 * The plugin implementation definition.
46 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
47 * The configuration factory.
48 * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
49 * The form builder.
50 */
51 public function __construct(array $configuration, $plugin_id, $plugin_definition, \
52 ConfigFactoryInterface $config_factory, FormBuilderInterface $form_builder) {
53 parent::__construct($configuration, $plugin_id, $plugin_definition);
54
55 $this->configFactory = $config_factory;
56 $this->formBuilder = $form_builder;
57 }
58
59 /**
60 * {@inheritdoc}
61 */
62 public static function create(ContainerInterface $container, array $configuration,\
63 $plugin_id, $plugin_definition) {
64 return new static(
65 $configuration,
66 $plugin_id,
67 $plugin_definition,
68 $container->get('config.factory'),
69 $container->get('form_builder')
70 );
71 }
72
73 /**
Blocks 24

74 * Builds the cart block.


75 *
76 * @return array
77 * A render array.
78 */
79 public function build() {
80 return $this->formBuilder->getForm('Drupal\quick_pivot\Form\QuickPivotSubscribeF\
81 orm');
82 }
83
84 }

And here is the routing file: docroot/modules/custom/quick_pivot/quick_pivot.routing.yml

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

2.5: Modify a block with hook_block_view_alter or


hook_block_build_alter
Some drupal hooks only run inside a contributed modules and some only inside a theme and some
both.
Blocks 25

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 function yourmodule_block_view_alter(array &$build, \Drupal\Core\Block\BlockPluginIn\


2 terface $block) {
3 if ($block->getBaseId() === 'system_powered_by_block') {
4 $build['#pre_render'][] = '_yourmodule_block_poweredby_prerender';
5 }

I think this is the version I tried:

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 }

2.6: Disable caching in a block


From docroot/modules/custom/websphere_commerce/modules/cart/src/Plugin/Block/CartSummary.php:

1 /**
2 * {@inheritdoc}
3 */
4 public function getCacheMaxAge() {
5 return 0;
6 }

2.7: Add a configuration form to your block


Making a block configurable means it has a form where you can specify its settings, e.g., the
configuration form for the menu block module allows you to specify menu levels. Ignore this if
your block does not need any configuration.
To make your block configurable, override 3 methods from BlockBase.

1. defaultConfiguration
2. blockForm
3. blockSubmit

Here defaultConfiguration() returns a block_count of 5.


Blocks 27

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 }

blockForm() is used to create a configuration form:

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

44 'width' => 700,


45 ]),
46 ],
47 ];
48 $url = Url::fromRoute('abc_prg.display_product_image', ['node' => $product->ni\
49 d->value, 'delta' => $item_count]);
50 $url->setOptions($options);
51 $build['list']['#items'][$item_count] = [
52 '#type' => 'markup',
53 '#markup' => Link::fromTextAndUrl($link_text, $url)
54 ->toString(),
55 ];
56 $item_count++;
57 }
58 $build['#attached']['library'][] = 'core/drupal.dialog.ajax';
59 }
60 }
61 return $build;
62 }

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:

1 # Schema for the configuration files for my module.


2
3 block.settings.alchemy_block:
4 type: block_settings
5 label: 'Alchemy block'
6 mapping:
7 block_count:
8 type: integer
9 label: 'Block count'

2.8: Block display not updating after changing block


content
From Nedcamp video on caching by Kelly Lucas, November 20183
In a twig template, if you just want to render one or more fields (instead of the entire node), Drupal
may not be aware if the content has changed, and will sometimes show old cached content. To
3 https://www.youtube.com/watch?v=QCZe2K13bd0&list=PLgfWMnl57dv5KmHaK4AngrQAryjO_ylaM&t=0s&index=16
Blocks 30

resolve this, define a view mode and call content | render and assign the result to a variable like
this:

1 set blah = content|render

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.

2.9: Block Permission (blockAccess)


This code is taken from the Drupal core user_login_block (UserLoginBlock.php). It allows access to
the block if the user is logged out and is not on the login or logout page. The access is cached based
on the current route name and the user’s current role being anonymous. If these are not passed, the
access returned is forbidden and the block is not built.

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 }

Another example from the Drupal core Copyright.php file:


Blocks 31

1 // $account comes from


2 $account = \\Drupal::currentUser();
3
4 //Get the route.
5 $route_name = \Drupal::routeMatch()->getRouteName();
6
7 // not on the user login and logout pages
8 if (!in_array($route_name, ['user.login', 'user.logout'])) {
9 return AccessResult::allowed();
10 }
11
12 //Authenticated user
13 if ($account->isAuthenticated()) {
14 return AccessResult::allowed();
15 }
16 //Anonymous user.
17 if ($account->isAnonymous()) {
18 return AccessResult::forbidden();
19 }

2.9.1: Blocks shouldn’t talk to the router, NodeRouteContext and


friends should
While it is possible for blocks to talk to the router, you can’t always count that they will be on a
meaningful route i.e. are they being displayed on a node? So we should use context definition in
the block annotation like this:

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:

1 protected function blockAccess(AccountInterface $account) {


2 /** @var \Drupal\Core\Plugin\Context\Context $node */
3 $cacheContext = $this->getContext('node');
4 /** @var \Drupal\Core\Entity\Plugin\DataType\EntityAdapter $data */
5 $data = $cacheContext->getContextData();
6 /** @var \Drupal\node\NodeInterface $node */
7 $node = $data->getValue();
8 if ($node) {
9 $nid = $node->id();
10 if (is_numeric($nid)) {
11 // See rsvp.permissions.yml for the permission string.
12 return AccessResult::allowedIfHasPermission($account, 'view rsvplist');
13 }
14 }
15 return AccessResult::forbidden();
16 }

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 }

2.9.2: Values returned by blockAccess()


Some options that can be returned from blockAccess() are:

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):

1 if ($browser === FALSE) {


2 drush_backend_batch_process();
3 }

Here is an explanation from the source of what it does:


Batch Processing and the Drupal Queue System 35

1 * Process a Drupal batch by spawning multiple Drush processes.


2 *
3 * This function will include the correct batch engine for the current
4 * major version of Drupal, and will make use of the drush_backend_invoke
5 * system to spawn multiple worker threads to handle the processing of
6 * the current batch, while keeping track of available memory.
7 *
8 * The batch system will process as many batch sets as possible until
9 * the entire batch has been completed or 60% of the available memory
10 * has been used.
11 *
12 * This function is a drop in replacement for the existing batch_process()
13 * function of Drupal.

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.

3.1.2: Using the Batch API with a form


This example replaces a multivalue field with some new values processing 10 nodes at a time. The
decision to process 10 at a time is arbitrary, but be aware that the more nodes you process at a time,
the higher the possibility that the process will time out causing the batch will fail.
The form example is in the accompanying source and is accessed at https://d9book.ddev.site/batch-
examples/batchform
The source file is at web/modules/custom/batch_examples/src/Form/BatchForm.php and is pre-
sented in pieces below:
Here is a simple form with a button used to kick off the batch operation.

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 }

The submitForm() method calls updateEventPresenters().

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 }

The route is:

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.

1 function updateEventPresenters(): void {


2 $query = \Drupal::entityQuery('node')
3 ->condition('status', 1)
4 ->condition('type', 'event')
5 ->sort('title', 'ASC')
6 ->accessCheck(TRUE);
7 $nids = $query->execute();
8
9 // Create batches.
10 $chunk_size = 10;
11 $chunks = array_chunk($nids, $chunk_size);
12 $num_chunks = count($chunks);
13
Batch Processing and the Drupal Queue System 37

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:

1 public static function exampleProcessBatch(int $batch_id, array $nids, array &$conte\


2 xt): void {
3 if (!isset($context['sandbox']['progress'])) {
4 $context['sandbox']['progress'] = 0;
5 $context['sandbox']['current_node'] = 0;
6 $context['sandbox']['max'] = 0;
7 }
8 if (!isset($context['results']['updated'])) {
9 $context['results']['updated'] = 0;
10 $context['results']['skipped'] = 0;
11 $context['results']['failed'] = 0;
12 $context['results']['progress'] = 0;
13 }
14
15 // Keep track of progress.
16 $context['results']['progress'] += count($nids);
17 $context['results']['process'] = 'Import request files';
18 // Message above progress bar.
19 $context['message'] = t('Processing batch #@batch_id batch size @batch_size for to\
Batch Processing and the Drupal Queue System 38

20 tal @count items.',[


21 '@batch_id' => number_format($batch_id),
22 '@batch_size' => number_format(count($nids)),
23 '@count' => number_format($context['sandbox']['max']),
24 ]);
25
26 foreach ($nids as $nid) {
27 $filename = "";
28 /** @var \Drupal\node\NodeInterface $event_node */
29 $event_node = Node::load($nid);
30 if ($event_node) {
31 $array = ['Mary Smith', 'Fred Blue', 'Elizabeth Queen'];
32 shuffle($array);
33 $event_node->field_presenter = $array;
34 $event_node->save();
35 }
36 }
37 }

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.

1 // node/1 should be a valid node.


2 return batch_process('node/1');

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.

1 'finished' => '\Drupal\batch_examples\Form\BatchForm::batchFinished',

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 }

3.1.3: Using the Batch API from a controller


The Batch API is often used in connection with forms. If you’re using a page callback, you will need
to setup all the items, submit them to the batch API, and then call batch_process() with a url as
the argument.

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.

1 public static function fileImportProcessBatch(int $batch_id, array $nids, array &$co\


2 ntext): void {
3 if (!isset($context['sandbox']['progress'])) {
4 $context['sandbox']['progress'] = 0;
5 $context['sandbox']['current_node'] = 0;
6 $context['sandbox']['max'] = 0;
7 }
8 if (!isset($context['results']['updated'])) {
9 $context['results']['updated'] = 0;
10 $context['results']['skipped'] = 0;
11 $context['results']['failed'] = 0;
12 $context['results']['progress'] = 0;
13 }
14 // Total records to process for all batches.
15 if (empty($context['sandbox']['max'])) {
2 https://api.drupal.org/api/drupal/core%21includes%21form.inc/group/batch/10.0.x
Batch Processing and the Drupal Queue System 41

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

102 //$request_node->field_request_file->target_id = $fid;


103 $request_node->save();
104 $context['results']['updated']++;
105 }
106 }
107 }
108 }

Here is the code that creates the batches and submits them.

1 public function summaryImport() {


2 $this->summaryCreateBatches();
3 return batch_process('/admin/content');
4 }

3.1.4: Using the Batch API with hook_update


If you want to update the default value of a field for all nodes using the Batch API and hook_-
update_N checkout the following links:

• Using the Batch API and hook_update_N in Drupal 83


• Drupal API | batch_example_update_8001 | batch_example.install4

3.1.5: Important rules about functions when using Batch API


All batch functions must be public static functions and all functions calling those must be
explicitly namespaced like:

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;

You can however refer to the functions with self:: e.g.


3 https://www.thirdandgrove.com/insights/using-batch-api-and-hookupdaten-drupal-8/
4 https://api.drupal.org/api/examples/batch_example%21batch_example.install/function/batch_example_update_8001/8.x-1.x
Batch Processing and the Drupal Queue System 44

1 $node_to_update_dirt_contact_nid = self::getFirstRef($node_to_update, 'field_sf_dirt\


2 _contact_ref');

3.1.6: Looking at the source


The source code for the Batch API5 is really well commented and worth reading.

3.1.6.1: Passing parameters to the functions in a batch operation

In this file https://git.drupalcode.org/project/drupal/-/blob/10.1.x/core/includes/form.inc#L562-678,


there is an example batch that defines two operations that call my_function_1 and my_function_2.
Notice how parameters can be passed to my_function_1 separated by commas. From https://git.
drupalcode.org/project/drupal/blob/8.7.8/core/includes/form.inc#L570:

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:

1 * function my_function_1($uid, $type, &$context) {

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) {

The results are displayed:

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:

1 $messenger->addMessage(t('Processed @count nodes, skipped @skipped, updated @updated\


2 in @elapsed.', [
3 '@count' => $results['nodes'],
4 '@skipped' => $results['skipped'],
5 '@updated' => $results['updated'],
6 '@elapsed' => $elapsed,
7 ]));

Which produce the following output:


Batch Processing and the Drupal Queue System 46

1 Processed 50 nodes, skipped 45, updated 5 in 3 sec.

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.

1 $context['message'] = t('Processing total @count nodes',


2 ['@count' => number_format($context['sandbox']['max'])]
3 );

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 );

You do have to provide your own info for the variables.


You can also stop the batch engine yourself with something like this. If you don’t know beforehand
how many records you need to process, you could use code like this.

1 // Inform the batch engine that we are not finished,


2 // and provide an estimation of the completion level we reached.
3 if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
4 $context['finished'] = ($context['sandbox']['progress'] >= $context['sandbox']['ma\
5 x']);
6 }

3.2: Queue System


From Alan Saunders article6 on December 2021:
6 https://www.alansaunders.co.uk/blog/queues-drupal-8-and-9
Batch Processing and the Drupal Queue System 47

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:

• Smack My Batch Up : Batch Processing In Drupal 88 by Phil Norton July 2016


• Highly commented source code for batch operations around line 561 for Drupal 109 (or search
for ‘batch operations’)

Read more about the Queue API 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();

Use this statement in node_preprocess, controller, etc.


Create a custom module to implement setting max-age to 0. For example in ddd.module file:

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 }

4.2: Don’t cache data returned from a controller


From dev1/web/modules/custom/rsvp/src/Controller/ReportController.php

1 // Don't cache this page.


2 $content['#cache']['max-age'] = 0;
3 return $content;
4 }

Disable caching for a route in the module.routing.yml file.


Caching and cache tags 51

1 requirements:
2 _permission: 'access content'
3 options:
4 no_cache: TRUE

4.3: Disable caching for a content type


If someone tries to view a node of content type search_home (i.e. an entity of bundle search_home)
caching is disabled and Drupal and the browser will always re-render the page. This is necessary
for a page that is retrieving data from a third party source and you almost always expect it to be
different. It wouldn’t work for a search page to show results from a previous search.

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 }

4.4: Considering caching when retrieving query, get or


post parameters
For get variables use:

1 $query = \Drupal::request()->query->get('name');

For post variables use:

1 $name = \Drupal::request()->request->get('name');

For all items in get:


Caching and cache tags 52

1 $query = \Drupal::request()->query->all();
2 $search_term = $query['query'];
3 $collection = $query['collection'];

Be wary about caching. From https://drupal.stackexchange.com/questions/231953/get-in-drupal-


8/231954#231954 the code provided only works the first time so it is important to add a ‘#cache’
context in the markup.

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 ];

More about caching render arrays at https://www.drupal.org/docs/8/api/render-api/cacheability-


of-render-arrays

4.5: Debugging Cache tags


In development.services.yml set these parameters

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:

languages:language_interface route session theme timezone url.path url.query_args


url.site user

2. X-Drupal-Cache-Tags:

block_view config:block.block.bartik_account_menu config:block.block.bartik_branding


config:block.block.bartik_breadcrumbs config:block.block.bartik_content con-
fig:block.block.bartik_footer config:block.block.bartik_help config:block.block.bartik_-
local_actions config:block.block.bartik_local_tasks config:block.block.bartik_-
main_menu config:block.block.bartik_messages config:block.block.bartik_-
page_title config:block.block.bartik_powered config:block.block.bartik_-
search config:block.block.bartik_tools config:block.block.helloworldsalutation
config:block.block.modalblock config:block.block.productimagegallery con-
fig:block.block.rsvpblock config:block.block.views_block__aquifer_listing_block_1
config:block.block.views_block__related_videos_block_1 config:block.block.views_-
block__user_guide_pages_referencing_a_product_block_1 config:block.block.views_-
block__workshop_count_proposed_workshop_block config:bloc

4.6: Using cache tags


If you are generating a list of cached node teasers and you want to make sure your list is always
accurate, use cache tags. To refresh the list every time a node is added, deleted or edited you could
use a render array like this:

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.

4.7: Setting cache keys in a block


If you add some code to a block that includes the logged in user’s name, you may find that the
username will not be displayed correctly–rather it may show the prior users name. This is because
the cache context of user doesn’t bubble up to the display of the container (e.g. the node that is
displayed along with your custom block.) Add this to bubble the cache contexts up.

1 public function getCacheContexts() {


2 Return Cache::mergeContexts(parent::getCacheContexts(),['user']);
3 }

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

4.8: Getting Cache Tags and Contexts for a block


In this file /modules/custom/dart_pagination/src/Plugin/Block/VideoPaginationBlock.php I have a
block that renders a form. The form queries some data from the database and will need to be updated
depending on the node that I am on.
I added the following two functions:
Caching and cache tags 55

1 public function getCacheTags() {


2 //When my node changes my block will rebuild
3 if ($node = \Drupal::routeMatch()->getParameter('node')) {
4 //if there is node add its cachetag
5 return Cache::mergeTags(parent::getCacheTags(), ['node:' . $node->id()]);
6 } else {
7 //Return default tags instead.
8 return parent::getCacheTags();
9 }
10 }
11
12 public function getCacheContexts() {
13 //if you depend on \Drupal::routeMatch()
14 //you must set context of this block with 'route' context tag.
15 //Every new route this block will rebuild
16 return Cache::mergeContexts(parent::getCacheContexts(), ['route']);
17 }

4.9: Caching REST Resources


Interesting article about caching REST resources at http://blog.dcycle.com/blog/2018-01-24/caching-
drupal-8-rest-resource/
We can get Drupal to cache our rest resource e.g. in dev1 /custom/iai_wea/src/Plugin/rest/re-
source/WEAResource.php where we add this to our response:

1 if (!empty($record)) {
2 $response = new ResourceResponse($record, 200);
3 $response->addCacheableDependency($record);
4 return $response;
5 }

4.10: Caching in an API class wrapper


From docroot/modules/custom/cm_api/src/CmAPIClient.php
Here a member is set up in the class
Caching and cache tags 56

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:

1 self::$cache['getPolicy'][$policy_number . $version] = $response_data;

and retrieved with:

1 $response_data = self::$cache['getPolicy'][$policy_number . $version];

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:

1 public function getPolicy($policy_number, $version = 'v2') {


2 // Api action type.
3 $this->params['api_action'] = 'Get policy';
4 // Add policy number to display in watchdog.
5 $this->params['policynumber'] = $policy_number;
6 $base_api_url = $this->getBaseApiUrl();
7 if (empty($policy_number) || !is_numeric($policy_number)) {
8 $this->logger->get('cm_api_get_policy')
9 ->error('Policy number must be a number.');
10 return FALSE;
11 }
12 $endpoint_url = $base_api_url . '/' . $version . '/Policies/group?policies=' . $po\
13 licy_number . '&include_ref=false&include_hist=true';
Caching and cache tags 57

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 }

4.11: Caching in a .module file


From docroot/modules/custom/ncs_infoconnect/nzz_zzzzconnect.module.
In the hook_preprocess_node function, we are calling an api to get some data.

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:

1. call the api with $client->request('GET')


2. pull out the body with $response->getBody()
3. set the whole body into the cache with:

1 \Drupal::cache()->set($cid, $contents, REQUEST_TIME + (300));

In future requests, we can just use the data from the cache.
Caching and cache tags 58

1 if ($node_type == 'zzzzfeed' && $published) {


2
3 $uuid = $variables['node']->field_uuid->getValue();
4 $nid = $variables['node']->id();
5
6 $nzz_auth_settings = Settings::get('nzz_api_auth', []);
7 $uri = $ncs_auth_settings['default']['server'] . ':' . $ncs_auth_settings['default\
8 ']['port'];
9 $uri .= '/blahcontent/search';
10 $client = \Drupal::httpClient();
11
12 $cid = 'zzzzfeed-' . $nid;
13 try {
14 if ($cache = \Drupal::cache()->get($cid)) {
15 $contents = $cache->data;
16 }
17 else {
18 $response = $client->request('GET', $uri, [
19 'auth' => [$nzz_auth_settings['default']['username'], $nzz_auth_settings['de\
20 fault']['password']],
21 'query' => [
22 'uuid' => $uuid[0]['value'],
23 ],
24 'timeout' => 1,
25 ]);
26 $contents = $response->getBody()->getContents();
27 \Drupal::cache()->set($cid, $contents, REQUEST_TIME + (300));
28 }
29 }
30 catch (RequestException $e) {
31 watchdog_exception('nzz_zzzzconnect', $e);
32 return FALSE;
33 }
34 catch (ClientException $e) {
35 watchdog_exception('nzz_zzzzconnect', $e);
36 return FALSE;
37 }
38
39 $contents = json_decode($contents, TRUE);
40 $body = $contents['hits']['hits'][0]['versions'][0]['properties']['Text'][0];
41 $variables['content']['body'] = [
42 '#markup' => $body,
43 ];
Caching and cache tags 59

44 }

4.12: Logic for caching render arrays


From https://www.drupal.org/docs/8/api/render-api/cacheability-of-render-arrays
Whenever you are generating a render array, use the following 5 steps:

1. I’m rendering something. That means I must think of cacheability.


2. Is this something that’s expensive to render, and therefore is worth caching? If the answer is
yes, then what identifies this particular representation of the thing I’m rendering? Those are
the cache keys.
3. Does the representation of the thing I’m rendering vary per combination of permissions, per
URL, per interface language, per … something? Those are the cache contexts. Note: cache
contexts are completely analogous to HTTP’s Vary header.
4. What causes the representation of the thing I’m rendering become outdated? I.e., which things
does it depend upon, so that when those things change, so should my representation? Those
are the cache tags.
5. When does the representation of the thing I’m rendering become outdated? I.e., is the data
valid for a limited period of time only? That is the max-age (maximum age). It defaults to
“permanently (forever) cacheable” (Cache::PERMANENT). When the representation is only valid
for a limited time, set a max-age, expressed in seconds. Zero means that it’s not cacheable at
all.

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

4.13: Development Setup

4.13.1: Disable caching and enable TWIG debugging


Generally I enable twig debugging and disable caching while developing a site.
To enable TWIG debugging output in source, in sites/default/development.services.yml set
twig.config debug:true. See core.services.yml for lots of other items to change for development
Caching and cache tags 60

1 # Local development services.


2 #
3 # To activate this feature, follow the instructions at the top of the
4 # 'example.settings.local.php' file, which sits next to this file.
5 parameters:
6 http.response.debug_cacheability_headers: true
7 dino.roar.use_key_value_cache: true
8 twig.config:
9 debug: true
10 auto_reload: true
11 cache: false
12
13 # To disable caching, you need this and a few other items
14 services:
15 cache.backend.null:
16 class: Drupal\Core\Cache\NullBackendFactory

to enable put this in settings.local.php:

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 with:

1 $settings['cache']['bins']['render'] = 'cache.backend.null';

4.13.2: Disable Cache for development


From https://www.drupal.org/node/2598914

1. Copy, rename, and move the sites/example.settings.local.php to sites/default/settings.local.php


with:

1 $ cp sites/example.settings.local.php sites/default/settings.local.php

2. Edit sites/default/settings.php and uncomment these lines:


Caching and cache tags 61

1 if (file_exists($app_root . '/' . $site_path . '/settings.local.php')) {


2 include $app_root . '/' . $site_path . '/settings.local.php';
3 }

This will include the local settings file as part of Drupal’s settings file.

1. In settings.local.php make sure development.services.yml is enabled with:

1 $settings['container_yamls'][] = DRUPAL_ROOT . '/sites/development.services.yml';

By default development.services.yml contains the settings to disable Drupal caching:

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. In sites/development.services.yml add the following block to disable the twig cache:


Caching and cache tags 62

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.

4.14: How to specify the cache backend


This information is relevant for using Memcache1 , Redis2 and also APCu3 . By default, Drupal caches
information in the database. Tables includes cache_default, cache_render, cache_page, cache_config
etc. By using the configuration below, Drupal can instead store this info in memory to increase
performance.
Summary
Drupal will no longer automatically use the custom global cache backend specified in
$settings['cache']['default'] in settings.php on certain specific cache bins that define
their own default_backend in their service definition. In order to override the default backend, a
line must be added explicitly to settings.php for each specific bin that provides a default_backend.
This change has no effect for users that do not use a custom cache backend configuration like Redis
or Memcache, and makes it possible to remove workarounds that were previously necessary to
keep using the default fast chained backend for some cache bins defined in Drupal core.
Detailed description with examples
In Drupal 8 there are several ways to specify which cache backend is used for a certain cache bin
(e.g. the discovery cache bin or the render cache bin).
In Drupal, cache bins are defined as services and are tagged with name: cache.bin. Additionally,
some cache bins specify a default_backend service within the tags. For example, the discovery
cache bin from Drupal core defines a fast chained default backend:

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:

• First look for a specific bin definition in settings. E.g., $settings['cache']['bins']['discovery']


• If not found, then use the global default defined in settings. I.e., $settings['cache']['default']
• If a global default is not defined in settings, then use the default_backend from the tag in the
service definition.

This was changed to:

• First look for a specific bin definition in settings. E.g., $settings['cache']['bins']['discovery']


• If not found, then use the the default_backend from the tag in the service definition.
• If no default_backend for the specific bin was provided, then use the global default defined in
settings. I.e., $settings['cache']['default'] The old order resulted in unexpected behaviors,
for example, the fast chained backend was no longer used when an alternative cache backend
was set as default.

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).

4.14.1: class ChainedFastBackend


Defines a backend with a fast and a consistent backend chain.
4 http://api.drupal.org/ChainedFastBackend
5 https://docs.pantheon.io/apcu
6 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Cache%21ChainedFastBackend.php/class/ChainedFastBackend/9
Caching and cache tags 65

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

For site administrators customizing $settings[‘cache’] Any entry for $settings[‘cache’][‘default’]


takes precedence over the default_backend service tag values, so you can disable all APCu
caching by setting $settings[‘cache’][‘default’] = ‘cache.backend.database’. If you have $set-
tings[‘cache’][‘default’] set to some alternate backend (e.g., memcache), but would still like to benefit
from APCu front caching of some bins, you can add those assignments, like so:

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';

For site administrators wanting a different front cache than APCu


You can copy the cache.backend.chainedfast service definition from core.services.yml to sites/de-
fault/services.yml and add arguments to it. For example:

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.'

For my patch, I want to remove this section of the file_entity.links.task.yml file.


First get the repo/git version of the module

1 $ composer update drupal/file_entity --prefer-source

Change the file in the text editor


Run git diff to see the changes:

1 $ git diff

The output shows:

1 diff --git a/file_entity.links.task.yml b/file_entity.links.task.yml


2 index 3ea93fc..039f7f9 100644
3 --- a/file_entity.links.task.yml
4 +++ b/file_entity.links.task.yml
5 @@ -15,12 +15,6 @@ entity.file.edit_form:
6 base_route: entity.file.canonical
7 weight: 0
8
9 -entity.file.collection:
Composer, Updates and Patches 70

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

Create the patch

1 git diff >file_entity_disable_file_menu_tab.patch

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 patch -p1 -R < ./patches/fix_scary_module.patch

To apply the patch:

1 patch -p1 < ./patches/fix_scary_module.patch

5.2: Patch modules using patches on Drupal.org


Patches can be applied by referencing them in the composer.json file, in the following format.
cweagans/composer-patches1 can then be used to apply the patches on any subsequent website
builds.
In order to install and manage patches using composer we need to require the “composer-patches”
module:

1 composer require cweagans/composer-patches

Examples of patches to core look like:

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

5.2.1: Step by step


1. Find the issue and patch in the issue queue on Drupal.org
2. Use the title and ID of the issue to be able to locate this post in the future. E.g. Using an issue
for the Gin admin theme4 “Improve content form detection - 3188521”
3. Scroll down the issue to find the specific patch you want to apply e.g. for comment #8 grab the
file link for 3188521-8.patch. It is https://www.drupal.org/files/issues/2021-05-19/3188521-
8.patch
4. Add the module name, description and URL for the patch into the extra patches section of json:

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 composer update -vvv

5.3: Patches from a Gitlab merge request


Using the URL of the merge request, add .patch at the end of the URL and that will be the path to
the latest patch.
e.g. for a merge request at https://git.drupalcode.org/project/alt_stream_wrappers/-
/merge_requests/2 or https://git.drupalcode.org/project/alt_stream_wrappers/-/merge_requests/2/
diffs?view=parallel
The patch is at https://git.drupalcode.org/project/alt_stream_wrappers/-/merge_requests/2.patch

5.4: composer.json patches in separate file


To separate patches into a different file other than composer json add "patches-file" section under
"extra". See example below:

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 }

If composer install fails, try composer -vvv for verbose output


If the issue is that it can’t find the file for example if it displays the following:
Composer, Updates and Patches 74

1 - Applying patches for drupal/addtocalendar


2 ./patches/add_to_calendar_smart_date_handling.patch (Add support for smart_date \
3 fields)
4 patch '-p1' --no-backup-if-mismatch -d 'web/modules/contrib/addtocalendar' < '/Users\
5 /selwyn/Sites/txglobal/patches/add_to_calendar_smart_date_handling.patch'
6 Executing command (CWD): patch '-p1' --no-backup-if-mismatch -d 'web/modules/contrib\
7 /addtocalendar' < '/Users/selwyn/Sites/txglobal/patches/add_to_calendar_smart_date_h
8 andling.patch'
9 can't find file to patch at input line 5
10 Perhaps you used the wrong -p or --strip option?

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 git diff --no-prefix >./patches/patch2.patch

Then composer install will apply the patch correctly


More at https://github.com/cweagans/composer-patches/issues/146

5.5: Stop files being overwritten during composer


operations
Depending on your composer.json, files like development.services.yml may be overwritten from
during scaffolding. To prevent certain scaffold files from being overwritten every time you run a
Composer command you can specify them in the “extra” section of your project’s composer.json.
See the docs on Excluding scaffold files.
The following snippet prevents the development.services.yml from being regularly overwritten:

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

The code above is from https://www.drupal.org/docs/develop/development-tools/disable-caching -


beware-of-scaffolding5
and from https://www.drupal.org/docs/develop/using-composer/using-drupals-composer-
6
scaffoldoc_6 : Sometimes, a project might prefer to entirely replace a scaffold file provided
by a dependency, and receive no further updates for it. This can be done by setting the value
for the scaffold file to exclude to false. In the example below, three files are excluded from being
overwritten:

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

5.6: Updating Drupal Core


if there is drupal/core-recommended in your composer.json use:

1 $ composer update drupal/core-recommended -W

if there is no drupal/core-recommended in your composer.json use:

1 $ composer update drupal/core -W

Note composer update -W is the same as composer update --with-dependencies


5 https://www.drupal.org/docs/develop/development-tools/disable-caching#s-beware-of-scaffolding
6 https://www.drupal.org/docs/develop/using-composer/using-drupals-composer-scaffold#toc_6
Composer, Updates and Patches 76

5.7: Test composer (dry run)


If you want to run through an installation without actually installing a package, you can use —dry-
run. This will simulate the installation and show you what would happen.

1 composer update --dry-run "drupal/*"

produces something like:

1 Package operations: 0 installs, 4 updates, 0 removals


2 - Updating drupal/core (8.8.2) to drupal/core (8.8.4)
3 - Updating drupal/config_direct_save (1.0.0) to drupal/config_direct_save (1.1.0)
4 - Updating drupal/core-recommended (8.8.2) to drupal/core-recommended (8.8.4)
5 - Updating drupal/crop (1.5.0) to drupal/crop (2.0.0)

5.8: Version constraints


1. The caret constraint (^): this will allow any new versions except BREAKING ones—-in other
words, the first number in the version cannot increase, but the others can. drupal/foo:^1.0 would
allow anything greater than or equal to 1.0 but less than 2.0.x. If you need to specify a version, this
is the recommended method.
2. The tilde constraint (~): this is a bit more restrictive than the caret constraint. It means composer
can download a higher version of the last digit specified only. For example, drupal/foo:~1.2 will
allow anything greater than or equal to version 1.2 (i.e., 1.2.0, 1.3.0, 1.4.0,…,1.999.999), but it won’t
allow that first 1 to increment to a 2.x release. Likewise, drupal/foo:~1.2.3 will allow anything from
1.2.3 to 1.2.999, but not 1.3.0.
3. The other constraints are a little more self-explanatory. You can specify a version range with
operators, a specific stability level (e.g., -stable or -dev ), or even specify wildcards with *.
Version range: By using comparison operators you can specify ranges of valid versions. Valid
operators are >, >=, <, <=, !=.
You can define multiple ranges. Ranges separated by a space ( ) or comma (,) will be treated as a
logical AND. A double pipe (||) will be treated as a logical OR. AND has higher precedence than OR.
Note: Be careful when using unbounded ranges as you might end up unexpectedly installing versions
that break backwards compatibility. Consider using the caret operator instead for safety.
Examples:

• >=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

5.9: Allowing multiple versions


You can use double pipe (||) to specify multiple version.
For the CSV serialization7 module the author recommends using the following to install the module:

1 composer require drupal/csv_serialization:^2.0 || ^3.0

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

5.10.1: Composer won’t update Drupal core


The prohibits command tells you which packages are blocking a given package from being installed.
Specify a version constraint to verify whether upgrades can be performed in your project, and if not
why not.
Why won’t composer install Drupal version 8.9.1?

1 composer why-not drupal/core:8.9.1

5.10.2: The big reset button


If composer barfs with a bunch of errors, try removing vendor, /core, modules/contrib (and
optionally composer.lock using:

1 $ rm -fr core/ modules/contrib/ vendor/

Then try run composer install again to see how it does:

1 $ composer install --ignore-platform-reqs

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

1 $ ddev composer install

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';

More about Defining and using your own configuration in Drupal1

6.1: Load some config


This example shows how to load a rest endpoint from config. This is very similar to Drupal 7
variable_get().

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.

The contents of the file are simply:

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

The config sync directory location is specified in settings.php like this


1 https://www.drupal.org/docs/creating-custom-modules/defining-and-using-your-own-configuration-in-drupal
Configuration and Settings 80

1 $settings['config_sync_directory'] = '../config/sync';

More on creating custom modules: Using your own configuration2


Drupal::config API Reference3
You can override config items in a settings.php or local.settings.php using the $config global
variable.

6.2: Views
For views, the config filenames are be in the form views.view.infofeeds for a view called
infofeeds.

6.3: Add config to an existing module


You simply create a yml file in the module’s /config/install directory.
The config file should start with the module name then a period and the thing you want to
store the config about. So modulename.something.yml e.g. dir_salesforce.cron.yml for cron
information, dir.funnelback.yml for funnelback information or tea_teks_spr.testing.yml for
testing information.
If the module name is pizza_academy_core and the thing I want to store config about is the pbxpath,
I would create a file called pizza_academy_core.pbxpath.yml.
The yml filename is passed as a parameter into \Drupal::config('...') without the .yml
extension. e.g. if the filename is danamod.header_footer_settings.yml then use:

1 $config = \Drupal::config('danamod.header_footer_settings');

Here we add some configuration to a module called pizza_academy_core:


In docroot/modules/custom/pizza_academy_core/config/install/pizza_academy_core.pbxpath.yml
We have a file with the contents:

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 drush config-import --source=modules/custom/pizza_academy_core/config/install/ --par\


2 tial -y

Then you can access it from a controller at docroot/modules/custom/pizza_academy_-


core/src/Controller/VerifyCertificationPage.php using the following code:

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.

6.4: Import something you changed in your module


During module development, you might find you want to add some configuration. This is very
useful as part of that workflow.

1 drush @dev2 config-import --source=modules/migrate/test1/config/install/ --partial -y

Note. the @dev2 is a site alias. See Drush alias docs for more info4 . These are sooo useful.

6.5: Config Storage in the database


Config is also kept in the config table of the database.
The name field stores the config id e.g. views.view.infofeeds (the definition of a view called
infofeeds)
The data field stores the stuff in the config serialized into a blob

6.6: Add some config to site config form


Here we add a phone number to the site config. This is put in a .module file.

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.

1 function mymodule_system_site_information_phone_submit(array &$form, FormStateInter\


2 face $form_state) {
3 $config = Drupal::configFactory()->getEditable('system.site');
4 $config->set('phone', $form_state->getValue('site_phone'))
5 ->save();
6 }

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.

6.7: Override config in settings.php


This can be useful for local development environment (where you might put these changes into
settings.local.php) or on each one of your servers where you might need some configuration to be
slightly different. e.g. dev/test/prod.
Drupal 9 allows global $config overrides (similar to drupal 7) The configuration system integrates
these override values via the Drupal\Core\Config\ConfigFactory::get() implementation. When
you retrieve a value from configuration, the global $config variable gets a chance to change the
returned value:
5 https://www.drupal.org/project/config_pages
Configuration and Settings 83

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.maintenance']['message'] = 'Sorry, our site is down now.';

For nested values, use nested array 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;

6.8: Setup a testing variable in config for a project


First create the yml file in your module/config/install e.g. tea_teks_srp.testing.yml with this
as the contents:
Configuration and Settings 84

1 test_mode: FALSE

This will be the default state of the app


In the Drupal U/I under config, devel, configuration synchronization, import, single item i.e. at
/admin/config/development/configuration/single/import select simple configuration. In the
configuration name field, put tea_teks_srp.testing.
Paste in the text of the file

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');

And then just use the $test_mode variable as needed e.g.

1 if ($this->test_mode) {
2 $value = $this->t("Reject Citation $citation_nid");
3 }

6.9: Getting and setting configuration with drush


Here we are fiddling with the shield module settings
In config, synchronize, we see an item: shield.settings
So we can load it with drush:
Configuration and Settings 85

1 $ drush cget shield.settings


2 credential_provider: shield
3 credentials:
4 shield:
5 user: nistor
6 pass: blahblah
7 print: 'Please provide credentials for access.'
8 allow_cli: true
9 _core:
10 default_config_hash: c1dcnGFTXFeMq2-Z8e7H6Qxp6TTJe-ZhSA126E3bQJ4

Drilling down deeper, let’s say we want to view the credentials section. Notice that drush requires
a space instead of a colon:

1 $ drush cget shield.settings credentials


2 'shield.settings:credentials':
3 shield:
4 user: nisor
5 pass: blahblah

Now to get down to the user name and password. And we are adding period back in. Huh?

1 $ drush cget shield.settings credentials.shield


2 'shield.settings:credentials.shield':
3 user: nisor
4 pass: blahblah

and finally:

1 $ drush cget shield.settings credentials.shield.pass


2 'shield.settings:credentials.shield.pass': blahblah

So if you want to set these:

1 drush cset shield.settings credentials.shield.pass yomama


2
3 Do you want to update credentials.shield.pass key in shield.settings
4 config? (y/n): y

And
Configuration and Settings 86

1 drush cset shield.settings credentials.shield.user fred


2
3 Do you want to update credentials.shield.pass key in shield.settings
4 config? (y/n): y

And there is that message

1 drush cget shield.settings print


2 'shield.settings:print': 'Please provide credentials for access.'

And so

1 drush cset -y shield.settings print "Credentials or I won't let you in"

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";

Similarly, for setting stage_file_proxy origin:

1 drush config-set stage_file_proxy.settings


2 origin https://www.mudslinger.com

6.10: Creating a module allowing users to edit/update


some config
When you want to add a form to allow the user to update the config, create a module with a form
as you would anywhere else. The form will need the standard buildForm(), submitForm() and
getFormId() methods.

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

1 // Load the values from config.


2 $config = \Drupal::config('danamod.header_footer_settings');
3
4 $address1 = $config->get('footer_address1');
5 $address2 = $config->get('footer_address2');
6 $address3 = $config->get('footer_address3');
7 $email = $config->get('footer_email');
8 $logo_url = $config->get('logo_url');
9
10 // And put them into the form render array.
11
12 $form['footer']['footer_address1'] = [
13 '#type' => 'textfield',
14 '#title' => $this->t('Address line 1'),
15 '#default_value' => $address1,
16 ];
17 $form['footer']['footer_address2'] = [
18 '#type' => 'textfield',
19 '#title' => $this->t('Address line 2'),
20 '#default_value' => $address2,
21 ];

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.');

And a little shorthand


Configuration and Settings 88

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.');

6.11: Drush config commands


Drush will provide you with all the tools you need to fiddle with config from the command line.
Check out the drush docs6

6.11.1: View config


Note. when you view the value in config, drush cleverly will ignore values overidden in
settings.php. More below.
cget is short for config:get.

From the drush docs7

• drush config:get system.site - displays the system.site config.


• drush config:get system.site page.front - displays what Drupal is using for the front page
of the site: e.g.

1 $ drush config:get system.site page.front


2 'system.site:page.front': /node

6.11.2: Viewing overridden config values


When you view the value in config, drush cleverly will ignore values overidden in settings.php.

• drush cget narcs_infoconnect.imagepath basepath

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

1 drush cget narcs_infoconnect.imagepath basepath --include-overridden

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 ev "print \Drupal::config('narcs_inferconnect.imagepath')->get('basepath');"

6.11.3: Delete from config


cdel is short for config:delete.

• drush \@dev2 cdel migrate_plus.migration.test1


• drush \@dev2 cdel migrate_plus.migration_group.default

6.11.4: Check what has changed with config:status


cst is short for config:status

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.

6.11.5: Export entire config


cex is short for config:export This will dump the entire config, a bunch of yml files into the config
sync folder which is specified in settings.php (or settings.local.php) as

1 $settings['config_sync_directory'] = '../config/sync';

• drush cex -y - export entire config.

1 $ drush cex -y
2 [success] Configuration successfully exported to ../config/sync.
3 ../config/sync

6.11.6: Import config changes


If you change the site name (for example) by mistake and want to restore it , you can re-import the
values from the last export.
First check what changed with drush cst then use drush cim to restore the config to it’s previous
glory. cim is short for config:import.
Drupal cleverly notices which config items have changed and loads only those changes into the
database.

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.

7.2: How does it work?


Drupal provides an automated cron system that works with all operating systems because it does
not involve the operating system’s cron daemon. Instead, it works by checking at the end of each
Drupal request to see when the cron last ran. If it has been too long, cron tasks are processed as part
of that request.
Module automated_cron subscribes to the onTerminate1 event for request. You can read more about
how this component works in Symfony [here].
File core/modules/automated_cron/src/EventSubscriber/AutomatedCron.php:

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.

7.3: Enable Drupal Cron


• One way to enable cron is through the administration page. By default, Drupal has a built-in
core automated cron system that manages cron. You can access this system by navigating
to Configuration > System > Cron (/admin/config/system/cron). If you have just installed
Drupal, this option should be enabled by default. You can confirm this by checking the status
of the Automated Cron module at /admin/modules.
• Another way to enable cron is to run it manually from the Reports > Status report page. By
default, cron runs every 3 hours, but you can change this to run every hour or every 6 hours.
You can also use contributed modules for additional cron functions.
• To run cron using Drush, open a terminal or command prompt and navigate to your Drupal
site’s root directory. Then, enter the command drush cron. This will run cron for your site.

7.4: The cron command


To get Drupal to take care of its maintenance you should have the server execute Drupal’s cron
periodically. This is done by logging in to the server directly and settings the crontab file.
Crontab (CRON TABle) - is a text file that contains the schedule of cron entries to be run at specified
times This file can be created and edited either through the command line interface.
In the following example, the crontab command shown below will activate the cron tasks automat-
ically on the hour:
CRON 94

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:

1 # +-------------- minute (0 - 59)


2 # | +----------- hour (0 - 23)
3 # | | +-------- day of the month (1 - 31)
4 # | | | +----- month (1 - 12)
5 # | | | | +-- day of the week (0 - 6) (Sunday=0)
6 # | | | | |
7 * * * * * command to be executed

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.

7.5: Setting up cron


To edit a crontab through the command line, type:

1. At the Linux command prompt, type: sudo crontab -e


2. Add ONE of the following lines:

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

7.6: Disable Drupal cron


For performance reasons, or if you want to ensure that cron can only ever run from an external
trigger (not from Drupal), it may be desirable to disable Drupal’s automated cron system, in one of
three ways:

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 }

7.8: Common inquiries regarding cron jobs

7.8.1: When did the cron job last run?


We can use this in .module files (which don’t allow dependency injection) in this way.
CRON 96

1 // Find out when cron was last run; the key is 'system.cron_last'.
2 $cron_last = \Drupal::state()->get('system.cron_last');

Or in another file, we need to use dependency injection.

1 $cron_last = $this->state->get('system.cron_last')

7.8.2: How to stop Cron from continuously executing things?


To stop cron from endlessly executing pending cron tasks truncate the queue table e.g. if you have
queued up work such as in the salesforce module.

7.8.3: Resolving the ip and name for cron


Here is a Drupal cron job on a prod server where it uses a --resolve param to resolve the IP and
the name. This task runs every 15 minutes.

1 */15 * * * * curl -svo /dev/null http://prod.ddd.test.gov:8080/cron/<key> --resolve \


2 prod.ddd.test.gov:8080:201.86.28.12

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 // Returns 2021-12-27 for a date only field.


2 $event_date = $event_node->field_event_date->value;
3
4 // Returns 2021-12-28T16:00:00 for a date field with time.
5 $event_datetime = $event_node->field_event_datetime->value;
6
7 // Return a Unix epoch timestamp.
8 $timestamp = $event_node->field_date->date->getTimestamp();
9
10 // Return a formatted date string.
11 $date_formatted = $event_node->field_date->date->format('Y-m-d H:i:s');

Using $node->field_mydatefield->date is ideal as it returns a DrupalDateTime class which gives


you all sorts of goodness including date math capabilities and formatting.
If you need to do calculations involving Unix timestamps, then using $node->field_-
mydatefield->getTimestamp() is useful although DrupalDateTime is probably better. More about
DrupalDateTime at https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Datetime%
21DrupalDateTime.php/class/DrupalDateTime/9.4.x. Also at https://drupal.stackexchange.com/
questions/252333/how-to-get-formatted-date-string-from-a-datetimeitem-object
See Nodes and Fields chapter Date fields section 1 for more on date fields

8.2: Retrieve a date field


You can retrieve date fields a few different ways. They are stored as varchar 20 UTC date strings
e.g. 2022-06-30T12:00:00
1 book/nodes_n_fields.html#date-fields
Dates and Times 98

1 // For a date only field, this returns a string like: 2024-08-31.


2 // For a date field with time, this returns: 2021-12-28T16:00:00.
3 $end_date = $contract_node->field_contract_date->value;
4
5 // Returns unix timestamp e.g. 1725105600
6 $end_date = $contract_node->field_contract_date->date->getTimestamp();
7
8 // Returns a DrupalDateTime object with all its goodness which you can format.
9 $end_date = $contract_node->field_contract_date->date;
10 $formatted_date = $end_date->format('m/d/y');

8.3: Retrieve date range field


To retrieve a date range field from a node, use value and end_value for the stand and end dates:

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'];

8.4: Formatting date range fields


Here are two different examples for formatting date fields:

1 // formatted start date


2 $start_date_formatted = $node->field_date->start_date->format('Y-m-d H:i:s');
3 // formatted end date
4 $end_date_formatted = $node->field_date->end_date->format('Y-m-d H:i:s');

Use this link at php.net for date format strings https://www.php.net/manual/en/datetime.format.


php#:~:text=format%20parameter%20string-,format,-character
Dates and Times 99

8.5: Formatting a date string with an embedded


timezone
Here a date string with an embedded timezone is used to create a DrupalDateTime object which is
then converted to be stored into a node.

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);

8.6: Formatting a date range for display


This code shows how to load a date range field from a node. It will ordinarily display like 3/30/2019
- 3/31/2023 however we want it to display like Mar 30-31, 2023.

First we retrieve the starting and ending value like this:

1 $from = $node->get('field_date')->getValue()[0]['value'];
2 $to = $node->get('field_date')->getValue()[0]['end_value'];

Here is the entire function as implemented as a hook_preprocess_node function in a .theme file. We


are creating a scrunch_date variable to be rendered via a Twig template as shown below:

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

15 $from_day = substr($date_array[2], 0, 2);


16 $from_month = $date_array[1];
17
18 $to = new DrupalDateTime($variables["node"]->get('field_date')->getValue()[0]['e\
19 nd_value']);
20 $date_array = explode("-", $to);
21 $to_day = substr($date_array[2], 0, 2);
22 $to_month = $date_array[1];
23
24 if ($from_month === $to_month && $from_day != $to_day) {
25 $variables['scrunch_date'] = [
26 '#type' => 'markup',
27 '#markup' => $from->format("M j-") . $to->format("j, Y"),
28 ];
29 }
30
31 }
32 // For debugging
33 // kint($variables);
34 // or
35 // kpm($variables);
36 }

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

8.7: Saving date fields


Date fields in Drupal are stored as UTC date strings (e.g. 2022-06-30T12:00:00) and when
you use get() or set(), they return strings. If you want to manipulate them, convert them to
DrupalDateTime objects, then convert them back to strings for saving.

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();

8.8: Create DrupalDateTime objects


1 use Drupal\Core\Datetime\DrupalDateTime;
2
3 $date = DrupalDateTime::createFromFormat('j-M-Y', '20-Jul-2022');
4
5 // Use current date and time
6 $date = new DrupalDateTime('now');
7 // Format like Tue, Jul 16, 2022 - 11:34:am
8 print $date->format('l, F j, Y - H:i');
9 // OR
10 // Format like 16-07-2022: 11:43 AM
11 print $date->format('d-m-Y: H:i A');

8.8.1: Create DrupalDateTime objects with timezones


1 // Use current date & time.
2 $date = new DrupalDateTime();
3 $date->setTimezone(new \DateTimeZone('America/Chicago'));
4 // Print current time for the given time zone e.g. 01/23/2023 10:00 pm
5 print $date->format('m/d/Y g:i a');
6
7 // Another variation using specific date and UTC zone
8 $date = new DrupalDateTime('2019-07-31 11:30:00', 'UTC');
9 $date->setTimezone(new \DateTimeZone('America/Chicago'));
10 // prints 07/31/2019 6:30 am
11 print $date->format('m/d/Y g:i a');
Dates and Times 102

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.

8.9: Create a DrupalDateTime object and display as a


year only
This code creates a Drupal\Core\Datetime\DrupalDateTime object and returns the year in a render
array with some markup. DrupalDateTimes are derived from DateTimePlus which is a wrapper for
PHP DateTime class.

1 use Drupal\Core\Datetime\DrupalDateTime;
2
3 public function build() {
4 $date = new DrupalDateTime();
5 return [
6 '#markup' => t('Copyright @year&copy; My Company', [
7 '@year' => $date->format('Y'),
8 ]), ];
9 }

8.10: Formatting node created time with Drupal


date.formatter service
If you want to use a custom date format for your created node date/time you can use one of the
methods shown below:

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

11 // Displays Wed, 05/04/2022 - 15:49


12 $formatted_created_date = \Drupal::service('date.formatter')->format($created_date);
13
14
15 // Create a DrupalDateTime object and use ->format()
16 $created_date = $event_node->getCreatedTime();
17 $cdt = DrupalDateTime::createFromTimestamp($created_date);
18 $formatted_created_date = $cdt->format('m/d/Y g:i a');

See PHP Date format strings: https://www.php.net/manual/en/datetime.format.php#:~:


text=format%20parameter%20string-,format,-character

8.11: Date arithmetic example 1


The code below shows how to add $days (an integer) to the date value retrieved from the field:
field_cn_start_date and save that to the field field_cn_end_date.

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();

8.12: Date arithmetic example 2


Here is an example from a module showing a hook_entity_type_presave() where some data is
changed as the node is being saved. The date arithmetic is pretty simple but the rest of the code is
kinda messy.
This is the date arithmetic part:
Dates and Times 104

1 $end_date = DrupalDateTime::createFromFormat('Y-m-d', $start_date_val);


2 $end_date->modify("+$days days");
3 $end_date = $end_date->format("Y-m-d");
4 $node->set('field_cn_end_date', $end_date);

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 }

8.13: Comparing DrupalDateTime values


The DrupalDateTime class extends the DateTimePlus class which is a wrapper for PHP DateTime
class. That functionality allows you to do comparisons. It is probably better manners to use
DrupalDateTime instead of DateTime but here is some DateTime code showing how to compare
DateTimes.

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)

8.14: Comparing dates (without comparing times)


Use the setTime() function to remove the time part of a datetime so we can make comparisons of
just the date.
From a form validation in a .module file.
Dates and Times 106

1 function ogg_mods_cn_form_validate($form, FormStateInterface $form_state) {


2 $start_date = $form_state->getValue('field_cn_start_date');
3 if ($start_date) {
4 $start_date = $start_date[0]['value'];
5 $start_date->setTime(0, 0, 0);
6 $now = new Drupal\Core\Datetime\DrupalDateTime();
7 // Subtract 2 days.
8 $now->modify("-2 days");
9 // Clear the time.
10 $now->setTime(0, 0, 0);
11
12 \Drupal::messenger()->addMessage("Start date = $start_date");
13 \Drupal::messenger()->addMessage("Now date - 2 days = $now");
14
15 if ($start_date < $now) {
16 $form_state->setErrorByName('edit-field-cn-start-date-0-value-date', t('The \
17 starting date is more than 2 days in the past. Please select a later date'));
18 }
19 }
20 }

8.15: Comparing Dates to see if a node has expired


This code is used to check if the value in the field field_expiration_date has passed. The field_-
expiration_date is a standard Drupal date field in a node. In this example, the client wanted to be
able to specify the expiration date for nodes.

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

8.16: Node creation and changed dates


Here is a function which does an entityQuery for a node and returns a formatted string version of
the creation date. Both created and changed are stored as Unix epoch timestamps in the node_-
field_data table (int 11).

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);

8.17: Query the creation date using entityQuery


This controller example queries for any events for year 2022 with a matching taxonomy term id of
5.

1 public function test2() {


2
3 $str = "Results";
4 $year = '2022';
5 $term_id = 5;
6 $titles = $this->eventsForYear($year, $term_id);
7 $count = count($titles);
8 $str .= "<br/>Found $count titles for query $year, term_id $term_id";
9
10 foreach ($titles as $title) {
11 $str .= "<br/>$title";
12 }
13 $render_array['content'] = [
14 '#type' => 'item',
15 '#markup' => $str,
16 ];
17 return $render_array;
18 }
19
20
21 private function eventsForYear($year, $term_id): array {
22 // Build valid range of start dates/times.
23 $format = 'Y-m-d H:i';
24 $start_date = DrupalDateTime::createFromFormat($format, $year . "-01-01 00:00");
25 $end_date = DrupalDateTime::createFromFormat($format, $year . "-12-31 23:59");
26
Dates and Times 109

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

8.18: Query a date field with no time


Drupal date fields can be configured to store the date and time or only the date. Looking in the
database, you might notice that a date-only field is stored like: 2021-12-27 which means you can
query just using a string. It probably is wiser to use DrupalDateTime like this:
Dates and Times 110

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

44 foreach ($nids as $nid) {


45 $event_node = Node::load($nid);
46 $title = $event_node->getTitle();
47 $date = $event_node->field_event_date->value;
48 $str .= "<br/>$title - date: $date";
49 }
50
51 $render_array['content'] = [
52 '#type' => 'item',
53 '#markup' => $str,
54 ];
55 return $render_array;
56 }

8.19: Query a date field with a time


1 use Drupal\Core\Datetime\DrupalDateTime;
2 use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
3
4 /*
5 * Query a date field with a time.
6 */
7 public function test3() {
8
9 // Get a date string suitable for use with entity query.
10 $date = new DrupalDateTime();
11 // This is a date/time from my local timezone.
12 $date = DrupalDateTime::createFromFormat('d-m-Y: H:i A', '28-12-2021: 10:00 AM');
13 $date->setTimezone(new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE));
14 $query_date = $date->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
15
16 $str = "Results";
17
18 $query = \Drupal::entityQuery('node')
19 ->condition('type', 'event')
20 ->condition('status', 1)
21 ->condition('field_event_datetime.value', $query_date, '=')
22 ->sort('title', 'ASC');
23 $nids = $query->execute();
24 $count = count($nids);
25
Dates and Times 112

26 $print_date = $date->format('d-m-Y: H:i A');


27 $str .= "<br/><strong>$count event(s) for field_event_datetime = $print_date (UT\
28 C)</strong> ";
29 $date->setTimezone(new \DateTimeZone('America/Chicago'));
30 $print_date = $date->format('d-m-Y: H:i A');
31 $str .= "<br/>For my timezone (America/Chicago), that is $print_date";
32
33 foreach ($nids as $nid) {
34 $event_node = Node::load($nid);
35 $title = $event_node->getTitle();
36 $display_date = $event_node->field_event_datetime->value;
37 $str .= "<br/>$title - date: $display_date";
38 }
39 $str .= "<br/>";
40
41 $query = \Drupal::entityQuery('node')
42 ->condition('type', 'event')
43 ->condition('status', 1)
44 ->condition('field_event_datetime.value', $query_date, '>')
45 ->sort('title', 'ASC');
46 $nids = $query->execute();
47 $count = count($nids);
48
49 $print_date = $date->format('d-m-Y: H:i A');
50 $str .= "<br/><strong>$count event(s) for field_event_datetime > $print_date (UT\
51 C)</strong> ";
52 $date->setTimezone(new \DateTimeZone('America/Chicago'));
53 $print_date = $date->format('d-m-Y: H:i A');
54 $str .= "<br/>For my timezone (America/Chicago), that is $print_date";
55
56 foreach ($nids as $nid) {
57 $event_node = Node::load($nid);
58 $title = $event_node->getTitle();
59 $display_date = $event_node->field_event_datetime->value;
60 $str .= "<br/>$title - date: $display_date";
61 }
62 $str .= "<br/>";
63
64
65 $render_array['content'] = [
66 '#type' => 'item',
67 '#markup' => $str,
68 ];
Dates and Times 113

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

8.20: Smart Date


This module provides the date field that fills in all the gaps in functionality that Drupal core dates
lack. Maybe someday it will make it into Drupal core.
This module attempts to provide a more user-friendly date field, by upgrading the functionality of
core in several ways:
Easy Admin UI: Includes the concept of duration, so that a field can have a configurable default
duration (e.g. 1 hour) and the end time will be auto-populated based on the start. The overall goal is
to provide a smart interface for time range/event data entry, more inline with calendar applications
which editors will be familiar with.
All Day Events Most calendar applications provide a one-click option to make a an event,
appointment, or other time-related content span a full day. This module brings that same capability
to Drupal.
Zero Duration Events Show only a single time for events that don’t need a duration.
Formatting: More sophisticated output formatting, for example to show the times as a range but
with a single output of the date. In the settings a site builder can control how date the ranges will
be output, at a very granular level.
Performance: Dates are stored as timestamps to improve performance, especially when filtering or
sorting. Concerns with the performance of core’s date range have been documented in #3048072:
Date Range field creates very slow queries in Views2 .
Overall, the approach in this module is to leverage core’s existing Datetime functionality, using the
timestamp storage capability also in core, with some custom Javascript to add intelligence to the
admin interface, and a suite of options to ensure dates can be formatted to suit any site’s needs.
Display configuration is managed through translatable Smart Date Formats, so your detailed display
setup is easily portable between fields, views, and so on. (From https://www.drupal.org/project/
smart_date)
2 https://www.drupal.org/project/drupal/issues/3048072
Dates and Times 114

8.20.1: Smart date: Load and format


Load the smart date field and use the Drupal date formatting service (date.formatter). Smart date
fields are always stored as unix timestamp values e.g. 1608566400 which need conversion for human
consumption.

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

8.20.2: Smart date: all-day


To check if a smart date is set to all day, check the duration. If it is 1439, that means all day.

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 }

8.20.3: Smart date: Range of values


Dates and Times 115

1 //Event start date.


2
3 //returns a SmartDateFieldItemList
4 $whens = $node->get('field_when');
5
6 // Each $when is a \Drupal\smart_date\Plugin\Field\FieldType\SmartDateItem.
7 foreach ($whens as $when) {
8 $start = $when->value;
9 $end = $when->end_value;
10 $duration = $when->duration; //1439 = all day
11 $tz = $when->timezone; //"" means default. Uses America/Chicago type format.
12 }

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

8.21.1: Date field storage


Note. The node created and changed fields (int 11) use a Unix epoch timestamp stored in the node_-
field_data table. These have values like 1525302749. Drupal date fields (with times) are stored as
UTC strings in varchar 20 fields which look like 2019-05-15T21:32:00.

8.21.2: DrupalDateTime API reference


The DrupalDateTime class extends the DateTimePlus class which is a wrapper for PHP DateTime
class. It extends the basic component and adds in Drupal-specific handling, like translation of the
format() method.
DateTimePlus has some static methods to create DrupalDateTime objects e.g.

1 DrupalDateTime::createFromArray(['year' => 2010, 'month' => 9, 'day' => 28]);

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.4: Unix epoch timestamps


From https://www.unixtimestamp.com/ - The unix time stamp is a way to track time as a running
total of seconds. This count starts at the Unix Epoch on January 1st, 1970 at UTC. Therefore, the
unix time stamp is merely the number of seconds between a particular date and the Unix Epoch.
It should also be pointed out (thanks to the comments from visitors to this site) that this point
in time technically does not change no matter where you are located on the globe. This is very
useful to computer systems for tracking and sorting dated information in dynamic and distributed
applications both online and client side.

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.

9.2: Enable error reporting


If you experience a WSOD (White Screen Of Death), enabling verbose error messages can often give
you some useful clue:

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';

For more, check out https://drupal.stackexchange.com/questions/127182/how-do-i-enable-


developer-debug-mode#:~:text=%24config%5B’system.,can%20always%20comment%20them%
20out. Also in https://github.com/drupal/drupal/blob/10.1.x/core/includes/bootstrap.inc Error
reporting levels are defined:

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';

9.3: Disable caches and enable Twig debugging


This will cause twig debugging information to be displayed in the HTML code like the following:

1 <!-- THEME DEBUG -->


2 <!-- THEME HOOK: 'toolbar' -->
3 <!-- BEGIN OUTPUT from 'core/themes/stable/templates/navigation/toolbar.html.twig' -\
4 ->

and

1 <!-- THEME DEBUG -->


2 <!-- THEME HOOK: 'page' -->
3 <!-- FILE NAME SUGGESTIONS:
4 * page--teks--admin--srp--program--expectation--correlation--vote-all.html.twig
5 * page--teks--admin--srp--program--expectation--correlation--852136.html.twig
6 * page--teks--admin--srp--program--expectation--correlation--%.html.twig
7 * page--teks--admin--srp--program--expectation--correlation.html.twig
8 * page--teks--admin--srp--program--expectation--852131.html.twig
9 * page--teks--admin--srp--program--expectation--%.html.twig
10 * page--teks--admin--srp--program--expectation.html.twig
11 * page--teks--admin--srp--program--852061.html.twig
12 * page--teks--admin--srp--program--%.html.twig
13 * page--teks--admin--srp--program.html.twig
14 * page--teks--admin--srp.html.twig
15 x page--teks--admin.html.twig
16 * page--teks.html.twig
17 * page.html.twig
18 -->

In sites/default/development.services.yml in the parameters, twig.config, set debug:true. See


core.services.yml for lots of other items to change for development.
Debugging 120

1 # Local development services.


2 #
3 parameters:
4 http.response.debug_cacheability_headers: true
5 twig.config:
6 debug: true
7 auto_reload: true
8 cache: false
9
10 # To disable caching, you need this and a few other items
11 services:
12 cache.backend.null:
13 class: Drupal\Core\Cache\NullBackendFactory

You also need this in settings.local.php:

1 /**
2 * Enable local development services.
3 */
4 $settings['container_yamls'][] = DRUPAL_ROOT . '/sites/development.services.yml';

Disable caches in settings.local.php:

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';

9.4: Enable/Disable Xdebug


To enable or disable Xdebug when using DDEV use:

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 alias xon='ddev xdebug on'


2 alias xoff='ddev xdebug off'

9.5: Xdebug Port


DDEV now sets the default port to 9003. If you want to change it to another port, use
the steps below. From https://ddev.readthedocs.io/en/stable/users/debugging-profiling/step-
debugging/#using-xdebug-on-a-port-other-than-the-default-9003:
To override the port, add an override file in the project’s .ddev/php directory. For example, add the
file .ddev/php/xdebug_client_port.ini like this to specify the legacy port 9000:

1 [PHP]
2
3 xdebug.client_port=9000

9.6: Drupal code debugging


Phpstorm and DDEV make this process as painless as possible. Once you enable Xdebug in DDEV,
simply click the ”start listening for PHP Debug Connections” button.
To start debugging, open the index.php file and set a breakpoint by clicking on a line number.

Select a breakpoint:
Debugging 122

Next refresh the Drupal home page in a browser


You should immediately see a dialog pop up in PhpStorm asking you to to configure your local path.
Be sure to click the site/web/index php and click Accept:
Note. If you select one of the other lines you will see a different php file pop up and you won’t be
debugging Drupal, but probably some Symfony file.
Debugging 123

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

You will also see the variables and watch pane:


Debugging 126

9.7: Command line or drush debugging


For command line or drush debugging you must run your code from within the DDEV container.
This means you must ’ssh’ into the DDEV (Docker) container and execute the command you want
to debug this way:

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

Next open vendor/drush/drush/src/Drush.php and specify a breakpoint like this:


Debugging 128

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/

9.8: Add a breakpoint in code


To add a breakpoint in code, you can use: xdebug_break()
more at https://xdebug.org/docs/all_functions

9.9: Troubleshooting Xdebug with DDEV

9.9.1: Could not connect to debugging client


When debugging command line e.g. drush commands etc. if you see:
Debugging 130

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: PhpStorm refuses to debug


Here are some steps to try:

9.9.2.1: Curl

Use curl or a browser to create a web request. For example:

1 $ curl https://d9.ddev.site

Replace d9.ddev.site with the correct URL for your 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:

1 $ telnet d9book2.ddev.site 9003

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:

1 $ telnet d9book2.ddev.site 9003


2
3 Trying 127.0.0.1\...
4
5 Connected to d9book2.ddev.site.
6
7 Escape character is \'\^\]\'.

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.

9.9.2.4: Is Xdebug enabled?

• 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.

1 `$ php -i | grep "xdebug.remote_enable"`


2
3 xdebug.remote_enable => (setting renamed in Xdebug 3) => (setting renamed in Xdebug \
4 3)

See https://ddev.readthedocs.io/en/stable/users/step-debugging
Debugging 132

9.10: What is listening on the debug port?


To check if something is listening on port 9003, it’s best to use lsof as it will actually list the name
of the process listening.
i.e. Here we see phpstorm listening:

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.

1 $ netstat -an | grep 9003


2 tcp4 0 0 *.9003 *.* LISTEN

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

9.11: Enable twig debugging output in source


In sites/default/development.services.yml in the parameters, twig.config, set debug:true. See
core.services.yml for lots of other items to change for development.

1 # Local development services.


2 #
3 parameters:
4 http.response.debug_cacheability_headers: true
5 twig.config:
6 debug: true
7 auto_reload: true
8 cache: false
9
10 # To disable caching, you need this and a few other items
11 services:
12 cache.backend.null:
13 class: Drupal\Core\Cache\NullBackendFactory

You also need this in settings.local.php:

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 with:

1 $settings['cache']['bins']['render'] = 'cache.backend.null';

9.12: Devel and Devel Kint Extras


From https://www.webwash.net/how-to-print-variables-using-devel-and-kint-in-drupal/

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

1 composer require drupal/devel drupal/devel_kint_extras

Enable both with the following Drush command:

1 $ ddev drush en devel_kint_extras -y

Finally, enable Kint Extended as the Variables Dumper. To do this go to:


admin/config/development/devel

and select Kint Extender and Save the configuration

9.12.2: Add kint to a custom module


1 function custom_kint_preprocess_page(&$variables) {
2 kint($variables['page']);
3 }

9.12.3: Dump variables in a TWIG template


{{ kint(attributes) }}

9.12.4: Kint::dump
From Migrate Devel contrib in module2 ,
docroot/modules/contrib/migrate_-
devel/src/EventSubscriber/MigrationEventSubscriber.php

This is used in migrate to dump the source and destination values.

1 // We use kint directly here since we want to support variable naming.


2 kint_require();
3 \Kint::dump($Source, $Destination, $DestinationIDValues);

9.12.5: Set max levels to avoid running out of memory


Kint can run really slowly so you may have to set maxLevels this way:
Add this to settings.local.php

2 https://www.drupal.org/project/migrate_devel
Debugging 135

1 // Change kint maxLevels setting:


2 include_once(DRUPAL_ROOT . '/modules/contrib/devel/kint/kint/Kint.class.php');
3 if(class_exists('Kint')){
4 // Set the maxlevels to prevent out-of-memory. Currently there doesn't seem to be \
5 a cleaner way to set this:
6 Kint::$maxLevels = 4;
7 }

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:

10.1.0.1: First Option

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

10.1.0.2: Second Option

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

Here are all the steps:


1 https://github.com/drud/ddev
2 https://ddev.readthedocs.io/en/stable/users/quickstart/#drupal
Development 137

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

10.2: Checking Your Permissions


During the wizard installation, or when your welcome page first loads, you might see a warning
about the permissions settings on your /sites/web/default directory and one file inside that
directory: settings.php.
After the installation script runs, Drupal will try to set the web/sites/default directory permissions
to read and execute for all groups3 : this is a 555 permissions setting. It will also attempt to set
permissions for default/settings.php to read-only, or 444. If you encounter this warning, run these
two chmod commands from your project’s root directory. Failure to do so poses a security risk:

1 chmod 555 web/sites/default

1 chmod 444 web/sites/default/settings.php

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:

1 $ ls -alhd web/sites/default web/sites/default/settings.php


2
3 dr-xr-xr-x 8 sammy staff 256 Jul 21 12:56 web/sites/default
4 -r--r--r-- 1 sammy staff 249 Jul 21 12:12 web/sites/default/settings.php

You are now ready to develop a Drupal website on your local machine.

10.3: Converting existing site (non-composer based) to


use composer
Composerize Drupal4
Also for manual steps5
3 https://www.drupal.org/docs/7/install/step-3-create-settingsphp-and-the-files-directory
4 https://github.com/grasmash/composerize-drupal
5 https://drupalize.me/tutorial/use-composer-your-drupal-project?p=3233
Development 138

10.4: Composer best practices for Drupal 8


https://www.lullabot.com/articles/drupal-8-composer-best-practices

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.

10.5.1: Local config - your .ddev/config.local.yaml


From https://ddev.readthedocs.io/en/stable/users/extend/config_yaml

• 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

• You could add a config.selwyn.yaml for Selwyn-specific values.


• Use ddev start (or ddev restart) after making changes to get the changes to take effect.

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

1 # instead of nfs, use mutagen


2 nfs_mount_enabled: false
3 mutagen_enabled: true

10.5.2: setup aliases in ddev


I love short linux aliases like ll (or just l) for listing files. If you spend time poking around the file
system in your containers this makes life so much better. A cool new feature since Ddev v15.1 lets
you add aliases using this technique
Use ddev ssh to “ssh” into the container and then type ll to list the files in a directory.
Either copy .ddev/homeadditions/bash_aliases.example to .ddev/homeadditions/bash_aliases
and add them there!
OR
Create a file .ddev/homeadditions/.bash_aliases with these contents: note. those are the letter L
lower case (as in lima).

1 alias ll=\"ls -lhAp"


2 alias l=\"ls -lhAp"

Note. don’t use .homeadditions - use the homeadditions.


Development 140

10.5.3: Upgrading ddev


After you install a new version of ddev, run ddev stop and then ddev config to reconfigure things
for your project. Just press enter for all the questions. It keeps things rolling smoothly. Run ddev
start to start it all back up again

10.5.4: Show others your ddev local site


Sharing your DDEV-Local site via a public URL using ddev share and ngrok. by Mike Anello
https://www.drupaleasy.com/blogs/ultimike/2019/06/sharing-your-ddev-local-site-public-url-
using-ddev-share-and-ngrok

10.5.5: Email Capture and Review


MailHog is a mail catcher which is configured to capture and display emails sent by PHP in the
development environment.
After your project is started, access the MailHog web interface at its default port:
http://mysite.ddev.site:8025
Please note this will not intercept emails if your application is configured to use SMTP or a 3rd-party
ESP integration. If you are using SMTP for outgoing mail handling (Swiftmailer or SMTP modules
for example), update your application configuration to use localhost on port 1025 as the SMTP server
locally in order to use MailHog.
ddev launch -m will launch the MailHog UI.

10.5.6: DDEV and Xdebug


This is a magical match made in heaven. To enable or disable Xdebug use
$ ddev xdebug on
and
$ ddev xdebug off
Note. This will slow everything down because xdebug has a significant performance impact so be
sure to disable it when you are finished with your debugging session.
In phpstorm, you can uncheck the following settings:

• force break at first line when no path mapping is specified


• force break at first line when a script is outside the project

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)

10.5.7: Command line or drush debugging


For command line or drush debugging (xdebug, phpstorm)

1 ddev ssh

1 export PHP_IDE_CONFIG=\"serverName=d8git.ddev.site\"

or

1 export PHP_IDE_CONFIG=\"serverName=inside-mathematics.ddev.site\"

confirm debug is turned on

1 php -i | grep debug

You should see:

1 xdebug support => enabled

Also you can confirm the port


set a server in phpstorm that matches the name d8git.ddev.site or inside-mathematics.ddev.site.
Configure the server to use path mappings
/Users/selwyn/Sites/ddev 82 ---> /var/www/html
click listen for debug connections button
set breakpoint and run
replace d8git.ddev.site with the name of your project
NOTE!!!!. You must execute drush from the vendor dir or you will always be ignored:
Development 142

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

10.5.8: Use drush commands in your shell with DDEV


If you do local development, you can use syntax like ddev drush cst to execute drush commands
in the container. This is slower than running on your native system because they are executed in
the container. I prefer using drush directly on the host computer.
To do this install PHP as well drush launcher. Once these are working, you can cd into the project
directory and issue commands like drush cr, drush cst or drush cim -y etc. It is so very quick
and smooth. (Note. this is the case with MacOS and Linux but I don’t really know how it works on
Windows.)
Details for Drush Launcher16
From Installation of Drush Launcher17

• 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.

Luckily, DDEV enables this functionality by default (Thanks Randy!)

10.5.9: Load your data from an Acquia site


Using the drush aliases19 assuming the site is called abc and you want the prod (production) database:

1 $ drush @abc.prod sql-dump >dbprod.sql


2 $ gzip dbprod.sql
3 $ ddev import-db --src=dbprod.sql.gz

Of course this works with any site where you’ve set up your drush aliases20 .

10.5.10: Cleanup some disk space


Free up disk space used by previous docker image versions. This does no harm.
16 https://github.com/drush-ops/drush-launcher
17 https://www.drush.org/latest/install/
18 https://github.com/drush-ops/drush-launcher
19 https://www.drush.org/latest/site-aliases/
20 https://www.drush.org/latest/site-aliases/
Development 143

1 ddev delete images

also

1 docker system prune

and

1 docker image prune -a

List all docker volumes

1 docker volume ls

DDEV General cleanup topic21

10.5.11: Accessing specific containers


To ssh into a specific service e.g. from a docker-composer.chromedriver.yml the service is listed
under “services:” like:

1 services:
2 chromedriver

Use
ddev ssh -s chromedriver

or for selenium, use:


ddev ssh -s selenium

10.6: DDEV Troubleshooting

10.6.1: Running out of docker disk space


if ddev won’t start and shows:

21 https://github.com/drud/ddev/issues/1465
Development 144

1 Creating ddev-router ... done


2 Failed to start ddev82: db container failed: log=, err=container exited, please use \
3 'ddev logs -s db` to find out why it failed

Looking in the log, you might see:

1 preallocating 12582912 bytes for file ./ibtmp1 failed with error 28


2 2020-03-16 14:27:54 140144158233920 [ERROR] InnoDB: Could not set the file size of '\
3 ./ibtmp1'. Probably out of disk space

That is the clue.


You can kill off images using

1 ddev delete images

or the more drastic

1 docker rmi -f $(docker images -q)

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

1 docker system prune

and

1 docker system prune --volumes

prunes every single thing, destroys all ddev databases and your composer cache.

10.6.2: DDEV won’t start


ddev pull or ddev start failed with error something like:
Development 145

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

10.7.1: Setting up PHPStorm and Drupal


https://www.drupal.org/docs/develop/development-tools/configuring-phpstorm

10.7.2: PHPStorm and Xdebug


Debugging drush commands at https://www.jetbrains.com/help/phpstorm/drupal-
support.htmlebugging-drush-commands22
22 https://www.jetbrains.com/help/phpstorm/drupal-support.html#debugging-drush-commands
Development 146

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

And for this project


23 https://www.jetbrains.com/help/phpstorm/configuring-xdebug.html#configure-xdebug-wsl
Development 147

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).

10.7.2.1: add a breakpoint in code

To add a breakpoint in code, use

1 xdebug_break()

more at https://xdebug.org/docs/all_functions
Development 148

10.7.3: Collecting PhpStorm debugging logs


• In the Settings/Preferences dialog (� ,) , go to PHP.
• From the PHP executable list, choose the relevant PHP interpreter and click next to it. In
the CLI Interpreters dialog that opens, click the Open in Editor link next to the Configuration
file: <path to php.ini> file. Close all the dialogs and switch to the tab where the php.ini file is
opened.
• In the php.ini, enable Xdebug logging by adding the following line:
• For Xdebug 3xdebug.log=”path_to_log/xdebug.log”The log file contains the raw communica-
tion between PhpStorm and Xdebug as well as any warnings or errors:
• https://www.jetbrains.com/help/phpstorm/troubleshooting-php-debugging.html#collecting-
logs

10.8: Troubleshooting Xdebug with DDEV


• Use curl or a browser to create a web request. For example, curl https://d9.ddev.site
• If the IDE doesn’t respond, take a look at ddev logs (ddev logs). If you see a message
like ””PHP message: Xdebug: [Step Debug] Could not connect to debugging client. Tried:
host.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.
• In PhpStorm, disable the ”listen for connections” button so it won’t listen. Or just exit PhpStorm.
With another IDE like vscode, stop the debugger from listening.
• ddev ssh: Can telnet host.docker.internal 9000 connect? If it does, you have something else running
on port 9000, probably php-fpm. On the host, use sudo lsof -i :9000 -sTCP:LISTEN to find out what
is there and stop it, or change the xdebug port and configure PhpStorm to use the new one . Don’t
continue debugging until your telnet command does not connect.
• Check to make sure that Xdebug is enabled. You can use php -i | grep Xdebug inside the
container, or use any other technique you want that gives the output of phpinfo(), including Drupal’s
admin/reports/status/php. You should see with Xdebug v2.9.6, Copyright (c) 2002-2020 and php -i |
grep ”xdebug.remote_enable” should give you xdebug.remote_enable: On.
https://ddev.readthedocs.io/en/stable/users/step-debugging/

10.9: What is listening on port 9000?


To check if something is listening on port 9000 (the default port for xdebug) it’s best to use
Development 149

1 $ lsof -i TCP:9000

it will actually list the name of the process listening


i.e.

1 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME


2 phpstorm 13361 selwyn 81u IPv6 0x5d4d30caf0be07d 0t0 TCP *:cslistener (LIST\
3 EN)

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 $ netstat -an | grep 9000

1 tcp4 0 0 127.0.0.1.9000 \*.\* LISTEN

Other options include:

1 $ lsof -i TCP:9000

Which reports that php-fpm is listening.

1 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME


2
3 php-fpm 732 selwyn 7u IPv4 0x4120ed57a07e871f 0t0 TCP
4 localhost:cslistener (LISTEN)
5
6 php-fpm 764 selwyn 8u IPv4 0x4120ed57a07e871f 0t0 TCP
7 localhost:cslistener (LISTEN)
8
9 php-fpm 765 selwyn 8u IPv4 0x4120ed57a07e871f 0t0 TCP
10 localhost:cslistener (LISTEN)
Development 150

10.10: Setup settings.local.php and disable Cache


From Disabling cache during development24
1. Copy, rename, and move the sites/example.settings.local.php to sites/default/settings.local.php:
$ cp sites/example.settings.local.php sites/default/settings.local.php

2. Open sites/default/settings.php and uncomment these lines:

1 if (file_exists($app_root . '/' . $site_path . '/settings.local.php')) {


2 include $app_root . '/' . $site_path . '/settings.local.php';
3 }

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 $settings['container_yamls'][] = DRUPAL_ROOT . '/sites/development.services.yml';

By default development.services.yml contains the settings to disable Drupal caching:

1 services:
2 cache.backend.null:
3 class: Drupal\Core\Cache\NullBackendFactory

NOTE: Do not create development.services.yml, it exists under /sites


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';

Add the following lines to your sites/default/settings.local.php

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 # Local development services.


2 #
3 # To activate this feature, follow the instructions at the top of the
4 # 'example.settings.local.php' file, which sits next to this file.
5 parameters:
6 http.response.debug_cacheability_headers: true
7 twig.config:
8 # Twig debugging:
9 #
10 # When debugging is enabled:
11 # - The markup of each Twig template is surrounded by HTML comments that
12 # contain theming information, such as template file name suggestions.
13 # - Note that this debugging markup will cause automated tests that directly
14 # check rendered HTML to fail. When running automated tests, 'debug'
15 # should be set to FALSE.
16 # - The dump() function can be used in Twig templates to output information
17 # about template variables.
Development 152

18 # - Twig templates are automatically recompiled whenever the source code


19 # changes (see auto_reload below).
20 #
21 # For more information about debugging Twig templates, see
22 # https://www.drupal.org/node/1906392.
23 #
24 # Not recommended in production environments
25 # @default false
26 debug: true
27 # Twig auto-reload:
28 #
29 # Automatically recompile Twig templates whenever the source code changes.
30 # If you don't provide a value for auto_reload, it will be determined
31 # based on the value of debug.
32 #
33 # Not recommended in production environments
34 # @default null
35 # auto_reload: null
36 auto_reload: true
37 # Twig cache:
38 #
39 # By default, Twig templates will be compiled and stored in the filesystem
40 # to increase performance. Disabling the Twig cache will recompile the
41 # templates from source each time they are used. In most cases the
42 # auto_reload setting above should be enabled rather than disabling the
43 # Twig cache.
44 #
45 # Not recommended in production environments
46 # @default true
47 cache: false
48 services:
49 cache.backend.null:
50 class: Drupal\Core\Cache\NullBackendFactory

Make sure the following is in docroot/sites/default/settings.local.php

1 /**
2 * Enable local development services.
3 */
4 $settings['container_yamls'][] = DRUPAL_ROOT . '/sites/development.services.yml';
Development 153

10.12: Enable twig debugging output in source


In sites/default/development.services.yml set twig.config debug:true. See
core.services.yml for lots of other items to change for development

1 # Local development services.


2 #
3 parameters:
4 http.response.debug_cacheability_headers: true
5 twig.config:
6 debug: true
7 auto_reload: true
8 cache: false
9
10 # To disable caching, you need this and a few other items
11 services:
12 cache.backend.null:
13 class: Drupal\Core\Cache\NullBackendFactory

to enable put the following in settings.local.php:

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 with:

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

Install using Composer:


$ composer require drupal/devel drupal/devel_kint_extras

Enable both with the following Drush command:


$ drush en devel_kint_extras -y

Finally, enable Kint Extended as the Variables Dumper. To do this go to admin/config/development/devel


and select Kint Extender and Save the configuration.

10.13.2: Add kint to a custom module


1 function custom_kint_preprocess_page(&$variables) {
2 kint($variables['page']);
3 }

10.13.3: Dump variables in a TWIG template


1 {% raw %}{{ kint(attributes) }}{% endraw %}

10.13.4: Kint::dump
From Migrate Devel contrib module26 ,
in /docroot/modules/contrib/migrate_-
devel/src/EventSubscriber/MigrationEventSubscriber.php.

This is used in migrate to dump the source and destination values.

1 // We use kint directly here since we want to support variable naming.


2 kint_require();
3 \Kint::dump($Source, $Destination, $DestinationIDValues);

10.13.5: Set max levels to avoid running out of memory


Kint can run really slowly so you may have to set maxLevels this way:
Add this to settings.local.php

26 https://www.drupal.org/project/migrate_devel
Development 155

1 // Change kint maxLevels setting:


2 include_once(DRUPAL_ROOT . '/modules/contrib/devel/kint/kint/Kint.class.php');
3 if(class_exists('Kint')){
4 // Set the maxlevels to prevent out-of-memory. Currently there doesn't seem to be \
5 a cleaner way to set this:
6 Kint::$maxLevels = 4;
7 }

10.14: Replacing deprecated functions


https://drupal.stackexchange.com/questions/144147/get-taxonomy-terms

10.15: Missing module


from: https://www.drupal.org/node/2487215
If you see a PHP warning such as The following module is missing from the file system...
(or similar) on your site, You can remove it with:

1 $ drush sql-query "DELETE FROM key_value WHERE name='module_name';"

10.16: You have requested a non-existent service


1 Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException: You have r\
2 equested a non-existent service "lingotek.content_translation". in /var/www/vendor/s
3 ymfony/dependency-injection/ContainerBuilder.php on line 1063 #0

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

33 // Get the email headers.


34 $headers = [
35 'Content-Type' => 'text/html; charset=UTF-8; format=flowed; delsp=yes',
36 'From' => '[email protected]',
37 'Reply-To' => '[email protected]',
38 ];
39
40 // Set up the email body.
41 $body_params = [
42 'message' => $params['message'],
43 'name' => $params['name'],
44 ];
45
46 // Send the email.
47 $result = $mail_manager->mail($module, $key, $to, $default_language, $body_params,\
48 NULL, TRUE, $headers);
49
50 // Return the result.
51 return $result['result'];
52 }
53
54 /**
55 * Implements hook_mail().
56 */
57 function my_module_mail($key, &$message, $params) {
58 switch ($key) {
59 case 'my_email_key':
60 $message['subject'] = t('My Email Subject');
61 $message['body'][] = $params['message'];
62 $message['body'][] = t('From: @name', ['@name' => $params['name']]);
63 break;
64 }
65 }

11.2: Using tokens in hook_mail


Here is an example in a hook_mail call where tokens are used:
Email 158

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.

12.2: Config entity types


They store configuration information e.g: views, imagestyles, roles, NodeType (which define node
bundles).

• They are not revisionable


• They don’t have fields/are not fieldable.
• They don’t support entity translation interface(TranslatableInterface), but can still be translated
using config’s translation API.

12.3: Content entity types


e.g. comment, user, taxonomy term, node and now media (bundles for media are file, image, audio,
video, remote video etc.)
more at https://www.drupal.org/docs/drupal-apis/entity-api/introduction-to-entity-api-in-drupal-
8

12.4: Query an entity by title and type


This example counts the number of entities of type article with the name $name. Note that access
checking must be explicitly specified on content entity queries1 .
1 https://www.drupal.org/node/3201242
Entities 160

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) {

12.5: Create an entity


To create a new entity object, use the entity_create. NOTE that this only creates an entity object
and does not persist it.

1 $node = entity_create('node', array(


2 'title' => 'New Article',
3 'body' => 'Article body',
4 'type' => 'article',
5 ));

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 ));

12.6: Save an entity


Entity save is done by calling the instance’s save method.

1 $node->save();

12.7: Create article node entity with attached image


Entities 161

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.8: Update a node entity and add some terms


1 use Drupal\node\Entity\Node;
2 use Drupal\taxonomy\Entity\Term;
3
4 $node = Node::load(4);
5 $term1 = Term::load(1);
6 $term2 = Term::load(2);
7 $node->field_tags->setValue([$term1, $term2]);
8 $node->save();

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 }

12.10: Identify entities


from https://www.drupal.org/docs/drupal-apis/entity-api/working-with-the-entity-api

1 // Make sure that an object is an entity.


2 if ($entity instanceof \Drupal\Core\Entity\EntityInterface) {
3 }
4
5 // Make sure it's a content entity.
6 if ($entity instanceof \Drupal\Core\Entity\ContentEntityInterface) {
7 }
8 // or:
9 if ($entity->getEntityType()->getGroup() == 'content') {
10 }
11
12 // Get the entity type or the entity type ID.
13 $entity->getEntityType();
14 $entity->getEntityTypeId();
15
16 // Make sure it's a node.
17 if ($entity instanceof \Drupal\node\NodeInterface) {
18 }
19
20 // Using entityType() works better when the needed entity type is dynamic.
21 $needed_type = 'node';
22 if ($entity->getEntityTypeId() == $needed_type) {
23 }
Entities 163

12.11: Create a file entity


Note. Drupal is ok with files existing (for example in sites/default/files) without file entities but it
probably makes more sense to have file entities connected with those files. When you create file
entities, Drupal tracks the files in the tables: file_managed and file_usage. . You can also create
media entities where bundle=‘file’.
Once the file already exists, I write the file entity. I do need a mime type

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;

12.12: Entity Validation


This looks quite interesting although I haven’t had an opportunity to try it yet.
From Entity Validation API overview at https://www.drupal.org/node/2015613
The entity validation API2 should be used to validate values on a node, like any other content entity.
2 https://www.drupal.org/docs/drupal-apis/entity-api/entity-validation-api
Entities 164

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'

13.2: Find a form id in the page source


When you need to make changes to a form, it can take a little time to find the form. You often need
to find the form id as the first step. To find the form_id for a node comment form, start by editing
an article node with the comment form displaying. Inspect code in chrome and look for something
like this:

1 <form class="comment-comment-form comment-form" data-drupal-selector="comment-form" \


2 action="/comment/reply/node/1/comment" method="post" id="comment-form" accept-charse
3 t="UTF-8" data-drupal-form-fields="edit-subject-0-value,edit-comment-body-0-value,ed
4 it-comment-body-0-format--2,edit-submit,edit-preview">

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

1 function nisto_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, \


2 $form_id){
3 \Drupal::messenger()->addMessage('Form ID: ' . $form_id);
4 // Or log it to watchdog log.
5 \Drupal::logger('nisto_form_alter')->notice('Form ID: ' . $form_id);

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

13.3: Add buttons to your custom forms


Many forms need extra buttons. Fortunately Drupal allows this to be done easily. In the code below
there is a submit button which will execute the submitForm function. There are two additional
buttons: update_nodes which (on click) calls the updateNodes function and cache_warmer which
(one click) will execute the warmCaches function.

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

1 public function updateNodesFromCache(array &$form, FormStateInterface $form_state)\


2 {
3 $program_nid = $form_state->get('program_nid');
4 $vote_number = $form_state->get('vote_number');
5 $program_node = $form_state->get('program');
6 $program_title = $program_node->getTitle();
7 $team_nid = $program_node->get('field_srp_team_ref')[$vote_number]->target_id;
8
9 $chunk_size = 20;
10 $operations = [];
11
12 $cache_id = "expectations.program.$program_nid.vote.$vote_number.team.$team_nid";
13 $cache_data = \Drupal::cache()->get($cache_id);
14 if ($cache_data) {
15 $expectations = $cache_data->data;
16 $chunks = array_chunk($expectations, $chunk_size);
17 for ($i = 0; $i < count($chunks); $i++) {
18 $operations[] = [
19 '\Drupal\tea_teks_voting\Batch\VotingCacheBatch::updateExpectations',
20 [$chunks[$i]],
21 ];
22 }
23 }
24
25 $batch = [
26 'title' => $this->t("Updating nodes from voting caches for $program_title"),
27 'progress_message' => $this->t('Completed @current out of @total chunks.'),
28 'finished' => '\Drupal\tea_teks_voting\Batch\VotingCacheBatch::batchFinished',
29 'operations' => $operations,
30 ];
31 batch_set($batch);
32

Note. The example above shows a batch function. You can read more in the Batch and
Queue chapter1

13.4: Modify a button on a form with hook_form_alter


in a .module file like web/modules/custom/mymod/mymod.module we can change the caption on
the “save” button to “Comment”
1 /d9book/book/bq.html
Forms, Form API and AJAX 169

1 function mymod_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, \


2 $form_id) {
3 if ($form_id == 'comment_comment_form') {
4 $form['actions']['submit']['#value'] = t('Comment');
5 }
6 }

Here is a dropdown form element that I added an item to:

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 function nisto_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, \


2 $form_id){
3 // xdebug_break();
4 if ($form_id == "views_exposed_form") {
5 // Clear the current options.
6 $form["topic"]["#options"] = [];
7
8 $tid = 2806;
9 $terms = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadChildren\
10 ($tid);
11 foreach($terms as $term) {
12 $childTerm = $term->get('tid')->value;
13 $termName = ucfirst(strtolower($term->get("name")->value));
14 $tid = $term->get("tid")->value;
15 $form["topic"]["#options"][$tid] = $termName;
16 }
17 natcasesort($form["topic"]["#options"]);
18 }
19 }

13.5: Hide a field with hook_form_alter


In a .module file, I used this code to remove access to these fields:
Forms, Form API and AJAX 170

1 $form['field_highlight_section']['#access'] = 0;
2 $form['field_accordion_section']['#access'] = 0;

This grays out a field:

1 $form['field_text2']['#disabled'] = true;

Here is the whole function where I also check what these is currently in use:

1 function dan_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $f\


2 orm_id) {
3 $activeThemeName = \Drupal::service('theme.manager')->getActiveTheme();
4 if ($activeThemeName->getName() == 'seven') {
5 $form['#attributes']['novalidate'] = 'novalidate';
6 }
7 if ($form_id == 'views-exposed-form-site-search-page-search') {
8
9 $form['#theme'] = ['header_search_form'];
10
11 }
12 // User is editing or adding content of type overview.
13 if ($form_id == 'node_overview_edit_form' || $form_id == 'node_overview_form') {
14 \Drupal::messenger()->addMessage("blah");
15 $form['field_text2']['#disabled'] = true;
16 $form['field_highlight_section']['#access'] = 0;
17 $form['field_accordion_section']['#access'] = 0;
18 }
19 }

13.6: Hide revision info and moderation state


In a .module file you can turn off (or hide) revision information and moderation state like this.

1 $form['revision_information']['#access'] = FALSE;
2 $form['moderation_state']['#access'] = FALSE;

13.7: Multiple fields on the same controller/page


If you need to have the same form appear multiple times on a page, you need to add a little special
logic. In one example, I had several items displayed on a page, and each one needed an option to
add feedback by the user. This required the use of a static class variable to uniquely identify each
instance of the form on the page.
Forms, Form API and AJAX 171

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 }

13.8: Conditional fields and field states API (#states)


There are some nice articles at https://mushtaq.ch/blog/11/drupal-8-conditionally-hide-a-form-
field and https://www.lullabot.com/articles/form-api-states that go into more detail.
Note. There is a very workable conditional fields module https://www.drupal.org/project/
conditional_fields which lets you do the same sort of thing without any code.
The magic sauce is to use the jQuery selector to identify the field that will control the states. You
can see the left side of the => has the jQuery code to select a checkbox or radio button.

1 ':input[name="copies_yes_no"]' => ['value' => '0']


Forms, Form API and AJAX 172

13.8.1: Conditional fields in a form


When creating a form, follow these steps:

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

13.8.2: Conditional fields in node add or edit form


To customize how fields are handled in an existing form, use hook_form_alter and check to see if
you are a node add or edit form for our type of node. Set the state to visible for field_cn_original_-
notice (so the user can see it) if the field_cn_extension has a value of checked=TRUE. This has
the effect of dynamically displaying the field once the checkbox for field_cn_extension is clicked.
The rest of the function has various other field tweaks
This is from a .module file

1 function org_mods_form_alter(array &$form, FormStateInterface $form_state, $form_id)\


2 {
3 $accountProxy = \Drupal::currentUser();
4 $account = $accountProxy->getAccount();
5 if (($form_id == 'node_catastrophe_notice_form') || ($form_id == 'node_catastrophe\
6 _notice_edit_form')) {
7
8 //Disable field.
9 $form['field_cn_end_date']['widget'][0]['value']['#attributes']['disabled'] = 'd\
10 isabled';
11 //Or the simpler.
12 $form['field_cn_end_date']['#disabled'] = TRUE;
13
14 if (!$account->hasPermission('administer catastrophe notice')) {
15 $form['field_cn_notes']['#access'] = FALSE;
16 $form['field_cn_initial_start_date']['#access'] = FALSE;
17 $form['field_cn_initial_end_date']['#access'] = FALSE;
18 $form['revision_information']['#access'] = FALSE;
19 $form['moderation_state']['#access'] = FALSE;
20 }
21
22 // Hides Title since it is automatically populated on presave.
23 $form['title']['#access'] = FALSE;
24
25 // When the value of field_cn_extension is checked, show this field.
26 $form['field_cn_original_notice']['#states'] = [
27 'visible' => [
28 ':input[name="field_cn_extension[value]"]' => ['checked' => TRUE],
29 ],
30 ];
31 }
32 }

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 ];

13.9: Get the key and value from a select drop-down


To get the key and the value that the user sees in the dropdown use the following. When the
dropdown was created, we gave it array of strings so the key is a zero-based number and the value
($educationLevel) is the string. This shows how to get both.

1 $key = $form_state->getValue('education_level');
2 $educationLevel = $form['education_level']['#options'][$key];

13.10: Autocomplete

13.10.1: Add an autocomplete taxonomy field


This makes a field on your form that automagically starts populating with terms when you start
typing. Here the $vid is a vocabulary machine name like media_tags. Not sure what #tags does–It
doesn’t seem to be required. Notice vocab id (vid) is the taxonomy machine name not a number.
Forms, Form API and AJAX 175

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 ];

13.10.2: Add a views-driven entity autocomplete field


This allows you to create a field on your form with a user field. This will be an autocomplete field
which uses the view: users_view and the display users. It allows you to start typing a username in
the field and all matching users will be displayed in the dropdown below the field:

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

13.10.3: Disable autocomplete for user login and password fields


In a .module file use the following code.
Forms, Form API and AJAX 176

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';

13.11: Validating input

13.11.1: Validate string length


Check a string length for the company_name field.

1 public function validateForm(array &$form, FormStateInterface $formState) {


2 if (!$formState->isValueEmpty('company_name')) {
3 if (strlen($formState->getValue('company_name')) <= 5) {
4 //Set validation error.
5 $formState->setErrorByName('company_name', t('Company name is less than 5 char\
6 acters'));
7 }
8 }
9 }

13.11.2: Validate an email


From web/modules/custom/rsvp/src/Form/RSVPForm.php we call Drupal’s email.validator ser-
vice and if it fails, setErrorByName()
Forms, Form API and AJAX 177

1 public function validateForm(array &$form, FormStateInterface $form_state) {


2 $value = $form_state->getValue('email');
3 if (!\Drupal::service('email.validator')->isValid($value)) {
4 $form_state->setErrorByName('email', t('The email %mail is not valid.', ['%mail'\
5 => $value]));
6 }
7
8 parent::validateForm($form, $form_state);
9 }

13.11.3: Validate date


You can also add a custom validation in a .module file. Here we use setTime()to remove the time
part of a datetime so we can make comparisons of just the date.
From a .module file.

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

26 e than 2 days in the past. Please select a more recent date.'));


27 }
28 }
29 }

13.11.4: Validate a node add or edit


In org_mods.module we implement a hook_form_alter, and add a validate callback function for
anonymous users only.

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 }

13.12: Displaying Forms

13.12.1: Embedding a form:


In this example, there is a form called ExampleForm at web/modules/custom/test/src/Form/ExampleForm.php.
To render a form programmatically, either inside a Controller or a block, use the FormBuilder service.
The form builder can be injected using the form_builder service key or used statically to then build
the form (which returns a render array)

1 $form = \Drupal::formBuilder()->getForm('Drupal\test\Form\ExampleForm');
2 $build['egform'] = $form;
3 return $build;

13.12.2: Show a form in a block


In dev1/web/modules/custom/rsvp/src/Plugin/Block/RSVPBlock.php you can see in the build()
method, we invoke a form like this:
Forms, Form API and AJAX 180

1 class RSVPBlock extends BlockBase {


2
3 /**
4 * @inheritDoc
5 */
6 public function build() {
7 return \Drupal::formBuilder()->getForm('Drupal\rsvp\Form\RSVPForm');
8 }

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 public function __construct(array $configuration, $plugin_id, $plugin_definition, Co\


2 nfigFactoryInterface $config_factory, FormBuilderInterface $form_builder) {
3 parent::__construct($configuration, $plugin_id, $plugin_definition);
4
5 $this->configFactory = $config_factory;
6 $this->formBuilder = $form_builder;
7 }
8
9 public function build() {
10 return $this->formBuilder->getForm('Drupal\quick_pivot\Form\QuickPivotSubscribeFor\
11 m');
12 }

13.12.3: Provide a block template for a form in a block


In /modules/custom/dan_pagination/src/Form/VideoPaginationForm.php I have a form which is
displayed in a block. The usual block template file provided by the theme is block.html.twig and
looks like this:
(Here is an image of this source code. Strangely, Jekyll/Github requires me to jump through some
Forms, Form API and AJAX 181

hoops for TWIG source. I’m still experimenting.)


Here is the source:

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>

The template outputs the guts of the block as

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

1 <div class="cell medium-6 medium-order-2 text-center">


2 {% raw %}
3 {{ content.select }}
4 {% endraw %}
5 </div>
6 <div class="cell small-6 medium-order-1 medium-3">
7 <a href="{{ content.previous_url }}" class="linkPager linkPager--prev">
8 <span class="linkPager-icon">
9 <span class="icon--arrowLeft" data-grunticon-embed=""></span>
10 </span>
11 <span class="linkPager-text">
12 <span class="linkPager-title">Prev</span>
13 {% raw %}
14 {{ content.previous_clip }}
15 {% endraw %}
16 </span>
17 </a>
18 </div>

13.13: Redirecting

13.13.1: Form submission with redirect


Here is a submitForm() function from docroot/modules/custom/websphere_commerce/mod-
ules/checkout/src/Form/ReviewForm.php
The call to $form_state->getValues() retrieves all the values in the form. The rest of the logic
checks a value and redirects the user to a specific page.

1 public function submitForm(array &$form, FormStateInterface $form_state) {


2 $values = $form_state->getValues();
3
4 if ($values['op'] == 'Goback') {
5 redirectUser('/checkout/' . $values['cart_id'] . '/billing');
6 }

Here is the source for the redirectUser() call from above.


Forms, Form API and AJAX 183

1 function redirectUser($path, $route = FALSE) {


2 if (!$route) {
3 $redirectUrl = Url::fromUserInput($path)->toString();
4 $response = new RedirectResponse($redirectUrl);
5 $response->send();
6 }
7 return;
8 }

13.13.2: Ajax redirect


If you want to redirect to the /cart url, you must add an AJAX command. See the RedirectCommand
in the API Reference2

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);

13.13.3: A JAX redirect from a select element (dropdown)


Here I set up a dropdown with the url’s and when the user makes a change in the dropdown, the
browser goes to that url. The url’s are /node/1 /node/2 etc. For the correct url to be built, we have
to prefix “internal:” to them and that happens in the callback function mySelectChange().

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 }

Invoke the form from the controller with:


return

1 return \Drupal::formBuilder()->getForm('Drupal\org_opinions\Form\IndividualOpinionFo\
2 rm');

The form is at docroot/modules/custom/org_opinions/src/Form/IndividualOpinionForm.php.


And don’t forget these:

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 }

13.14: Add Javascript to a form


This code is in the examples3 module in the DependentDropdown example where one field depends
on the value from another
From web/modules/contrib/examples/ajax_example/src/Form/DependentDropdown.php the form
has some Javascript included via a library:

1 public function buildForm(array $form, FormStateInterface $form_state, $nojs = NULL)\


2 {
3 // Add our CSS and tiny JS to hide things when they should be hidden.
4 $form['#attached']['library'][] = 'ajax_example/ajax_example.library';

The ajax_example.libraries.yml looks like this:

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);

13.15: A JAX Forms


When adding ajax to a form, you will need the code:

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

13.15.1: Popup an A JAX modal dialog


If you want to have a form pop up a modal dialog or do something via ajax you have to do some
slightly special stuff.
First define what will appear on the dialog

1 $checkoutLink = '/checkout/' . $get_order_item_id . '/shipping';


2 $success_modal_popup = [
3 '#theme' => 'add_cart_success_modal_popup',
4 '#data' => [
5 'product_title' => $productDetails->title->value . $popup_title,
6 'checkout_link' => $checkoutLink,
7 'cart_link' => '/cart',
8 'continue_shopping_link' => '<span class="continue-shopping>"Continue Shopping</\
9 p>',
10 'product_id'=>$product_id,
11 'product_price'=>number_format($productDetails_p->price[1]->value, 2),
12 'product_category'=>'Singer',
13 'product_quantity'=>$product_qty,
14 ],
15 ];
16 $content['#markup'] = render($success_modal_popup);
17 $content['#attached']['library'][] = 'core/drupal.dialog.ajax';
18 $content['#attached']['library'][] = 'websphere_commerce_cart/minicart';

Then add an ajax command to open the dialog:

1 $ajax_response->addCommand(
2 new OpenModalDialogCommand(t($popup_title), $content, ['width' => '60%', 'dialogCl\
3 ass' => 'product-cart-popup'])
4 );
5 return $ajax_response;

Don’t forget to return the $ajax_response;


Here is a slightly example displaying a modal dialog:
Forms, Form API and AJAX 190

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;

13.15.2: A JAX modal dialog with redirect example


From docroot/modules/custom/websphere_commerce/modules/product/src/Form/AddToCartForm.php.

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

20 // Use buildForm() function to create the elements on the form:


21 public function buildForm(array $form, FormStateInterface $form_state, $nid = NULL, \
22 $productId = NULL) {

13.15.2.1: Ajax submit

Submit element is a little special. See all that ‘#ajax’ stuff?

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.

1 public function add_to_cart_submit(array &$form, FormStateInterface $form_state) {


2 global $user_status;
3 $inputs = $form_state->getUserInput();
4 $ajax_response = new AjaxResponse();
5 $product_qty = $inputs['product_qty'];

Note. This should probably be a static function to avoid this symfony error:

1 TypeError: Argument 1 passed to Drupal\Core\Routing\RequestContext::fromRequest() mu\


2 st be an instance of Symfony\Component\HttpFoundation\Request, null given

13.15.2.2: Ajax redirect

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);

13.15.3: A JAX redirect from a select element (dropdown)


Here I set up a dropdown with the url’s and when the user makes a change in the dropdown, the
browser goes to that url. The url’s are /node/1 /node/2 etc. For the correct url to be built, we have
to prefix “internal:” to them and that happens in the callback function mySelectChange().

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

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 *
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 }

Invoke the form from the controller with:


return

1 return \Drupal::formBuilder()->getForm('Drupal\org_opinions\Form\IndividualOpinionFo\
2 rm');
Forms, Form API and AJAX 194

The form is at docroot/modules/custom/org_opinions/src/Form/IndividualOpinionForm.php.


And don’t forget these:

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

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.

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 }

13.15.4: Update a value in another field(I am I want) using A JAX


This was used for a web page that had 2 dropdown fields. It showed I am a ____ and I want ____.
The code makes the assumption that there is only 1 matching I want for each I am which is way too
limiting but it does illuminate the techniques somewhat. Therefor, if you select an I am, you only
get 1 choice for an I want.
There is nothing too interesting in the IamiwantBlock.php but in the iamiwantForm.php it gets a
little more juicy. The form presents two dropdown fields, I am and I want and a go button. The
value in the I am dropdown controls the value in the I want.
Forms, Form API and AJAX 196

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:

1. The ‘#submit’ callback: ::submitSelectIam


2. The #executes_submit_callback => TRUE
3. The ‘callback’ => ‘::ajaxReplaceIwantForm’ specifies what function to call when this field is
“submitted”
4. The ‘wrapper’ => ‘iwant-container’ is the container that will get replaced
5. The ‘method’ => ‘replace’ which I guess means replace everything in said wrapper.

Then it defines the $form['iwant_container'] and the $form['iwant_container']['iwant'] ele-


ment in readiness for the AJAX magic. Lastly it defines the submit go button.

1 public function buildForm(array $form, FormStateInterface $form_state) {


2 $nodeStorage = $this->entityTypeManager->getStorage('node');
3 // Get i_am_i_want node ids.
4 $nids = $nodeStorage->getQuery()
5 ->accessCheck(TRUE)
6 ->condition('field_iam', NULL, 'IS NOT NULL')
7 ->condition('field_iwant', NULL, 'IS NOT NULL')
8 ->condition('field_link', NULL, 'IS NOT NULL')
9 ->condition('status', NodeInterface::PUBLISHED)
10 ->execute();
11 $nodes = $nodeStorage->loadMultiple($nids);
12 $iam = [];
13 foreach ($nodes as $node) {
14 /* @var \Drupal\node\NodeInterface $node */
15 $iam[$node->id()] = $node->get('field_iam')->getString();
16 }
17 $form['iam'] = [
18 '#type' => 'select',
19 '#title' => $this->t('I am'),
20 '#options' => $iam,
21 '#required' => TRUE,
22 '#submit' => ['::submitSelectIam'],
23 '#executes_submit_callback' => TRUE,
24 '#ajax' => [
25 'callback' => '::ajaxReplaceIwantForm',
26 'wrapper' => 'iwant-container',
27 'method' => 'replace',
28 ],
Forms, Form API and AJAX 197

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',

Then, add the ajax callback.

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:

1 public function submitForm(array &$form, FormStateInterface $form_state) {


2 /* @var \Drupal\node\NodeInterface $node */
3 $node = $form_state->get('node');
4 $uri = $node->get('field_link')->getString();
5 $url = Url::fromUri($uri);
6 $form_state->setRedirectUrl($url);
7 }

This version handles external URL’s


Forms, Form API and AJAX 199

1 public function submitForm(array &$form, FormStateInterface $form_state) {


2 /* @var \Drupal\node\NodeInterface $node */
3 $node = $form_state->get('node');
4 // Get redirect uri from node.
5 $fieldLink = $node->get('field_link')->getString();
6 $url = Url::fromUri($fieldLink);
7 if (UrlHelper::isExternal($fieldLink)) {
8 $response = new TrustedRedirectResponse($url->toString());
9 $form_state->setResponse($response);
10 }
11 else {
12 $form_state->setRedirectUrl($url);
13 }
14 }

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.

1 // Updates iwant values according to iam.


2 if ($node = $form_state->get('node')) {
3 $iwant[] = $node->get('field_iwant')->getString();
4 }

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.

13.15.5: I Am I Want revisited


Revisiting this code to make it limit the iwant field to only valid values:
From ddev81/web/modules/custom/iamiwant/src/Form/IamiwantForm.php here is buildForm() .
Forms, Form API and AJAX 200

1 public function buildForm(array $form, FormStateInterface $form_state) {


2 $nodeStorage = $this->entityTypeManager->getStorage('node');
3 // Get i_am_i_want node ids.
4 $nids = $nodeStorage->getQuery()
5 ->accessCheck(TRUE)
6 ->condition('field_iam', NULL, 'IS NOT NULL')
7 ->condition('field_iwant', NULL, 'IS NOT NULL')
8 ->condition('field_link', NULL, 'IS NOT NULL')
9 ->condition('status', NodeInterface::PUBLISHED)
10 ->execute();
11
12 // Display the message if nodes don't exist.
13 if (empty($nids)) {
14 $form['error'] = [
15 '#type' => 'item',
16 '#markup' => $this->t('No "I am I want" nodes found.'),
17 ];
18 }
19
20 $nodes = $nodeStorage->loadMultiple($nids);
21 $iam = [];
22 // Build I am select values.
23 foreach ($nodes as $node) {
24 $iam[$node->id()] = $node->get('field_iam')->getString();
25 }
26 $form['iam'] = [
27 '#type' => 'select',
28 '#title' => $this->t('I am'),
29 '#options' => array_unique($iam),
30 '#required' => TRUE,
31 '#submit' => ['::submitSelectIam'],
32 '#executes_submit_callback' => TRUE,
33 '#ajax' => [
34 'callback' => '::ajaxReplaceIwantForm',
35 'wrapper' => 'iwant-container',
36 'method' => 'replace',
37 ],
38 ];
39 $iwant = [];
40 // Updates iwant values according to iam.
41 $node = $form_state->get('node');
42 if ($node) {
43 $iwant[] = $node->get('field_iwant')->getString();
Forms, Form API and AJAX 201

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

10 'wrapper' => 'iwant-container',


11 'method' => 'replace',
12 ],
13 ];

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 ];

With a form element in it

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 ];

So the code that “replaces” the iwant_container simply does a

1 return $form['iwant_container'];

after buildform has run and filled in the $form['iwant_container']


Magic!
Forms, Form API and AJAX 205

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.

13.15.5.1: Custom responses

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

15 $response->addCommand(new InvokeCommand(NULL, 'initCustomForms'));


16 return $response;
17 }

I made a new function called myinitCustomForms in app.js (web/themes/custom/txg/foundation/s-


rc/assets/js/app.js).

1 $response->addCommand(new InvokeCommand(NULL, 'myinitCustomForms'));


2 return $response;

And it works a treat!!


At https://www.drupal.org/docs/8/api/javascript-api/ajax-forms they suggest an example (which I
tried in txg/web/themes/custom/txg/foundation/src/assets/js/app.js).

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);

This works fine when called with:

1 $response->addCommand(new InvokeCommand(NULL, 'myAjaxCallback', ['This is the new te\


2 xt!']));

So my old JavaScript function:

1 // initialize custom form elements


2 function initCustomForms() {
3 jcf.setOptions('Select', {
4 maxVisibleItems: 6,
5 wrapNative: false,
6 wrapNativeOnMobile: false
7
8 });
9 jcf.replaceAll();
10 }

Should be wrapped in (function($){}


Forms, Form API and AJAX 207

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.

13.15.7: Another A JAX Submit example


There is another example of a form with an AJAX submit in docroot/modules/custom/quick_-
pivot/src/Form/QuickPivotSubscribeForm.php.

Note how the callback is explicitly spelled out as:

1 'callback' => 'Drupal\quick_pivot\Form\QuickPivotSubscribeForm::quickPivotAjaxSubmit\


2 ',

Build the form:


Forms, Form API and AJAX 208

1 public function buildForm(array $form, FormStateInterface $form_state) {


2 $form['#id'] = 'quick-pivot-subscribe-form';
3 $form['#cache'] = ['max-age' => 0];
4 $form['#attributes'] = ['autocomplete' => 'off'];
5
6 $form['email'] = [
7 '#type' => 'textfield',
8 '#id' => 'quick-pivot-email',
9 '#placeholder' => $this->t('Email address'),
10 '#attributes' => ['class' => ['edit-quick-pivot-email']],
11 '#prefix' => '<div class="subscriber-email-msg">',
12 '#suffix' => '</div>',
13 ];
14 $form['actions']['subscribe_submit'] = [
15 '#type' => 'submit',
16 '#value' => $this->t('Sign Up'),
17 '#name' => 'quick_pivot_subscribe_form_submit_button',
18 '#ajax' => [
19 'callback' => 'Drupal\quick_pivot\Form\QuickPivotSubscribeForm::quickPivotAjax\
20 Submit',
21 'wrapper' => 'quick-pivot-subscribe-form',
22 'progress' => ['type' => 'throbber', 'message' => NULL],
23 ],
24 ];
25 $form['message'] = [
26 '#type' => 'markup',
27 '#markup' => '<div id="quick-pivot-message-area"></div>',
28 ];
29
30 return $form;
31 }

Leave the validateForm() and submitForm() functions empty.


Here is the ajax callback. Note the function has to be static to avoid the possible error:

1 TypeError: Argument 1 passed to Drupal\Core\Routing\RequestContext::fromRequest() mu\


2 st be an instance of Symfony\Component\HttpFoundation\Request, null given

This error seems to have something to do with memcache and anonymous users.
Forms, Form API and AJAX 209

1 public static function quickPivotAjaxSubmit(array &$form, FormStateInterface $form_s\


2 tate) {
3 $validate = TRUE;
4 $email = trim($form_state->getValue('email'));
5 if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
6 $message = t('Please enter a valid email address.');
7 $validate = FALSE;
8 $css_border = ['border' => '1px solid red'];
9 $css_color = ['color' => 'red'];
10 }
11 if ($validate) {
12 $css_border = ['border' => '1px solid green'];
13 $css_color = ['color' => 'green'];
14 $response = \Drupal::service('quick_pivot.api')->subscribeEmail($email);
15 if (strpos(reset($response), 'Success') !== FALSE) {
16 $message = t('Thank you for signing up. Your subscription has been activated.'\
17 );
18 }
19 else {
20 $message = t('Your subscription could not be processed.');
21 }
22 }
23
24 $response = new AjaxResponse();
25
26 $quick_pivot_form = \Drupal::formBuilder()->rebuildForm('quick_pivot_subscribe_for\
27 m', $form_state);
28 if ($validate) {
29 $quick_pivot_form['email']['#value'] = '';
30 $quick_pivot_form['email']['#placeholder'] = t('Email address');
31 }
32 $response->addCommand(new ReplaceCommand('#quick-pivot-subscribe-form', $quick_piv\
33 ot_form));
34 $response->addCommand(new CssCommand('#edit-quick-pivot-email', $css_border));
35 $response->addCommand(new HtmlCommand('#quick-pivot-message-area', $message));
36 $response->addCommand(new CssCommand('#quick-pivot-message-area', $css_color));
37 return $response;
38 }
Forms, Form API and AJAX 210

13.16: Config Forms

13.16.1: Generate a config form with drush


Use the drush command7 drush generate form:config for a quick boilerplate version.

13.16.2: Config forms overview


Configuration forms extend ConfigFormBase so be sure to

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().

from dev1/web/modules/custom/rsvp/src/Form/RSVPConfigurationForm.php here is the


buildForm() function.

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 }

And the submitForm()

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

1 #Schema for config file of rsvp module


2 rsvp.admin_settings:
3 type: config_object
4 label: 'RSVP Content Type Settings'
5 mapping:
6 allowed_types:
7 type: sequence
8 label: 'Content types RSVP form can display on'
9 sequence:
10 type: string
11 label: 'Content type'
Forms, Form API and AJAX 212

13.17: The basics of implementing forms

13.17.1: Location
Forms are stored in modules/src/Form/MyClassForm.php e.g. /modules/custom/dmod/src/Form/HeaderFooterForm.

13.17.2: Base Classes for forms


• FormBase–for any old form
• ConfirmFormBase–for generic confirmation form
• ConfigFormBase–for config forms

13.17.3: Create your form class by extending Formbase


1 class HeaderFooterForm extends FormBase {

13.17.4: The main methods


Forms typically need a buildForm(), submitForm() and getFormId() member function. Validation
is handled with a validateForm() member function. Each is explained with more detail below:

13.17.4.1: getFormId()

It only returns a simple string identifying the form, e.g.

1 public function getFormId() {


2 return 'dan_header_footer_form';
3 }

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

13.17.5: Form validation example #1


Check a string length for the company_name field.

1 public function validateForm(array &$form, FormStateInterface $formState) {


2 if (!$formState->isValueEmpty('company_name')) {
3 if (strlen($formState->getValue('company_name')) <= 5) {
4 //Set validation error.
5 $formState->setErrorByName('company_name', t('Company name is less than 5 char\
6 acters'));
7 }
8 }
9 }

13.17.6: Form Validation example #2


From web/modules/custom/rsvp/src/Form/RSVPForm.php we call Drupal’s email.validator ser-
vice and if it fails, setErrorByName()

1 public function validateForm(array &$form, FormStateInterface $form_state) {


2 $value = $form_state->getValue('email');
3 if (!\Drupal::service('email.validator')->isValid($value)) {
4 $form_state->setErrorByName('email', t('The email %mail is not valid.', ['%mail'\
5 => $value]));
6 }
7
8 parent::validateForm($form, $form_state);
9 }

13.17.7: Field attributes


They always begin with #. You can find all the possible attributes at Form Element Reference with
examples8

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 ];

13.17.8: Form Elements


Form Element Reference with examples9
You can add markup to a form e.g. at web/modules/custom/modal_form_example/src/Form/Exam-
pleForm.php

1 public function buildForm(array $form, FormStateInterface $form_state, $options = NU\


2 LL) {
3
4 $form['zzz'] = [
5 '#type' => 'markup',
6 '#markup' => $this->t('this is a test'),
7 ];
8
9 $form['name'] = [
10 '#type' => 'textfield',
11 '#title' => $this->t('Name'),
12 '#size' => 20,
13 '#default_value' => 'Joe Blow',
14 '#required' => FALSE,
15 ];

and prefixes and suffices

9 https://api.drupal.org/api/drupal/elements/10
Forms, Form API and AJAX 217

1 public function buildForm(array $form, FormStateInterface $form_state, $options = NU\


2 LL) {
3
4 $form['#prefix'] = '<div id="example_form">';
5 $form['#suffix'] = '</div>';

Hidden or required via a custom class

1 '#maxlength' => 40,


2 '#attributes' => [
3 'class' => ['hidden'],
4 ],

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>',

13.17.9: Retrieving field values


When you go to grab a field value from a form, use getValue(). The values that come back from this
are arrays so you have to extract them like this (from org_mods.module)

1 function cn_form_validate($form, FormStateInterface $form_state) {


2 $extension = $form_state->getValue('field_cn_extension');
3 if (is_array($extension)) {
4 $extension = $extension['value'];
5 }

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.'),

You can retrieve the value of the checkbox with:

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());

14.2: Get the logged in user name and email


1 $username = \Drupal::currentUser()->getAccountName();

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;

14.3: Check if you are on the Front page


1 $is_front = \Drupal::service('path.matcher')->isFrontPage();

The above statement will return either TRUE or FALSE. TRUE means you are on the front page.

14.4: Check if site is in system maintenance mode


1 $is_maint_mode = \Drupal::state()->get('system.maintenance_mode');

14.5: Get Node URL alias or Taxonomy Alias by Node id


or Term ID
Sometimes we need a relative path and sometimes we need an absolute path. There is an $options
parameter in the fromRoute() function where specify which you need.
Parameters:

• absolute true will return absolute path.


• absolute false will return relative path.

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();

14.6: Taxonomy alias


Return taxonomy alias

1 $options = ['absolute' => true]; //false will return relative path.


2 $url = Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => 1234], $\
3 options);

14.7: Get current Path


For node pages this will return node/{node id}, for taxonomy taxonomy/term/{term id}, for user
user/{user id} if exists otherwise it will return the current request URI.
General 222

1 $currentPath = \Drupal::service('path.current')->getPath();

14.8: Get current nid, node type and title


There are two ways to retrieve the current node–via the request or the 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 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 }

And from https://drupal.stackexchange.com/questions/145823/how-do-i-get-the-current-node-id


when you are using or creating a custom block then you have to follow this code to get current
node id. Not sure if it is correct.
General 223

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 }

14.9: How to check whether a module is installed or


not
1 $moduleHandler = \Drupal::service('module_handler');
2 $module_name = “views”;
3 if ($moduleHandler->moduleExists($module_name)) {
4 echo "$module_name installed";
5 }
6 else {
7 echo "$module_name not installed";
8 }
General 224

14.10: Get current Route name


Routes are in the form: view.files_browser.page_1, test.example or test.settings_form
E.g from test.routing.yml

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();

14.11: Get the current page title


You can use this in a controller, to return the current page title.

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);}

14.12: 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.
General 225

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());

14.13: Check if you are on the Front page


This will return true for the front page otherwise false.

1 $is_front = \Drupal::service('path.matcher')->isFrontPage();

14.14: Check if site in system maintenance mode


From web/core/modules/system/src/Access/CronAccessCheck.php

1 if (\Drupal::state()->get('system.maintenance_mode')) {

14.15: Retrieve query and get or post parameters


($_POST and $_GET)
Old style was:

1 $name = $_POST['name'];

Now use this for post vars:

1 $name = \Drupal::request()->request->get('name');

And this for gets


General 226

1 $query = \Drupal::request()->query->get('name');

For all items in get:

1 $query = \Drupal::request()->query->all();
2 $search_term = $query['query'];
3 $collection = $query['collection'];

Be wary about caching. From https://drupal.stackexchange.com/questions/231953/get-in-drupal-


8/231954#231954 the code provided only works the first time so it is important to add a ‘#cache’
context in the markup.

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 ];

More about caching render arrays: https://www.drupal.org/docs/8/api/render-api/cacheability-of-


render-arrays

14.16: Retrieve URL argument parameters


You can extract the url arguments with
General 227

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

14.17: Get Current Language in a constructor


In dev1 - /modules/custom/iai_wea/src/Plugin/rest/resource/WEAResource.php we create the
WeaResource class and using dependency injection, get the LanguageManagerInterface service
passed in, then we call getgetCurrentLanguage(). This allows us to later retrieve the node

1 class WEAResource extends ResourceBase {


2
3 /**
4 * @var \Drupal\Core\Language\Language
5 */
6 protected $currentLanguage;
7
8 /**
9 * WEAResource constructor.
10 *
11 * @param array $configuration
12 * @param string $plugin_id
13 * @param mixed $plugin_definition
14 * @param array $serializer_formats
15 * @param \Psr\Log\LoggerInterface $logger
16 * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
17 */
18 public function __construct(array $configuration, string $plugin_id, mixed $plugin\
19 _definition, array $serializer_formats, \Psr\Log\LoggerInterface $logger, LanguageMa
20 nagerInterface $language_manager) {
21 parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_\
General 228

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:

1 public function get($id) {


2 if ($node = Node::load($id)) {
3 $translatedNode = $node->getTranslation($this->currentLanguage->getId());

Of course, you can also get the language statically by using:

1 Global $language = Drupal::languageManager()->getLanguage(Language:TYPE_INTERFACE)

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

14.18: Add a variable to any page on the site


In the .theme file of the theme, add a hook_preprocess_page function like in themes/custom/d-
prime/dprime.theme:
General 229

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 ];

Then in the template file e.g. themes/custom/dprime/templates/partials/footer.html.twig

1 <div class="cell xlarge-3 medium-4">


2 <address>
3 {{ footer_address1 }}<br />
4 {{ footer_address2 }}<br />
5 Campus mail code: D9000<br />
6 <a href="mailto:[email protected]">[email protected] </a>
7 </address>
8 </div>

14.19: Add a variable to be rendered in a node.


From dev1 custom theme burger_burgler.
Here two vars stock_field and my_custom_field are added and will be rendered by a normal node
twig file. The function hook_preprocess_node is in the .theme file at themes/custom/burger_-
burgler/burger_burgler.theme.

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>

Note. You can always just add a variable like

1 $variables['abc'] = 'hello';

which can be referenced in the template as {% raw %}{{ abc }}{% endraw %} (or {% raw %}{{
kint(abc) }}{% endraw %} )

14.20: Add a bunch of variables to be rendered in a


node
You can easily grab the node from the $variables with:

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

14.21: Grabbing entity reference fields in


hook_preprocess_node for injection into the twig
template
You can easily pull in referenced fields by referring to them as

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 if ($type === 'contract') {


2 if ($view_mode === 'full') {
3 $how_to_order_lookup = $node->field_sf_contract_ref->entity->field_how_to_order_\
4 lookup->value;
5 $variables['how_to_order_lookup'] = $how_to_order_lookup;
6 $contract_type = $node->get('field_contract_type')->value;
7 if ($how_to_order_lookup === "Custom Text") {
8 if ($contract_type === "DIRT") {
9 $variables['how_to_order'] = $node->field_sf_contract_ref->entity->field_how\
10 _to_order->value;
11 }
12 else {
13 $variables['how_to_order'] = $node->field_sf_contract_ref->entity->field_how\
14 _to_order_custom->value;
15 }
16 }
17 }
18 }

14.22: Render a list created in the


template_preprocess_node()
Here we create a list in the preprocess_node custom theme burger_burgler):
General 233

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 }

and render it in the twig template node--article--full.html.twig

1 <ol>
2 {% raw %}{% for burger in burgers %}
3 <li> {{ burger['name'] }} </li>
4 {% endfor %}{% endraw %}
5 </ol>

14.23: Indexing paragraphs so you can theme the first


one
Posted on https://www.drupal.org/project/paragraphs/issues/2881460#comment-13291215
From themes/custom/dprime/dprime.theme
Add this to the theme:

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 {% raw %}{% if paragraph.index == 0 %}


2 <li class="accordion-item is-active" data-accordion-item="">
3 {% else %}
4 <li class="accordion-item" data-accordion-item="">
5 {% endif %}{% endraw %}

14.24: Add meta tags using template_preprocess_html


Also covered at https://drupal.stackexchange.com/questions/217880/how-do-i-add-a-meta-tag-in-
inside-the-head-tag
If you need to make changes to the <head> element, the hook_preprocess_html is the place to do
it in the .theme file. Here we check to see that the content type is contract and then we create a
fake array of meta tags and jam them into the $variables['page']['#attached']['html_head']
element. They are then rendered on the page.

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 $page['#attached']['html_head'][] = [$description, 'description'];

For multiple tags, I had to do this version:

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 }

14.25: How to strip % characters from a string


1 $str = "threatgeek/2016/05/welcome-jungle-tips-staying-secure-when-you%E2%80%99re-ro\
2 ad";
3 echo $str . "\n";
4 //echo htmlspecialchars_decode($str) . "\n";
5
6 echo (urldecode($str)) . "\n";
7 echo urlencode("threatgeek/2016/05/welcome-jungle-tips-staying-secure-when-you're-ro\
8 ad");
9
10 echo urldecode('We%27re%20proud%20to%20introduce%20the%20Amazing');

14.26: Remote media entities


For this project, I had to figure out a way to make media entities that really were remote images.
i.e. the API provided images but we didn’t want to store them in Drupal
I started by looking at https://www.drupal.org/sandbox/nickhope/3001154. which was based on
https://www.drupal.org/project/media_entity_flickr.
I tweaked the nickhope module (media_entity_remote_file) so it worked but it had some trouble
with image styles and thumbnails
A good solution (thanks to Hugo) is:
General 237

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();

There was no documentation so I added some at https://www.drupal.org/project/remote_stream_


wrapper/issues/2875444#comment-12881516

14.27: Deprecated functions like drupal_set_message


Note. drupal_set_message() has been removed from the codebase so you should use messenger()
but you can also use dsm() which is provided by the devel1 contrib module. This is useful when
working through a problem if you want to display a message on a site during debugging.
From https://github.com/mglaman/drupal-check/wiki/Deprecation-Error-Solutions
Before

1 drupal_set_message($message, $type, $repeat);

After

1 \Drupal::messenger()->addMessage($message, $type, $repeat);

Read more on Drupal.org2

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.

15.2: Modify the login form


Here is code from hook_examples.module that modifies the user login form by adding a button. It
passes the username and password that were entered to the mythical third party login endpoint.

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

16 * Custom submit function for the login form.


17 */
18 function hook_examples_user_login_form_submit(array &$form, \Drupal\Core\Form\FormSt\
19 ateInterface $form_state) {
20 // Get the username and password from the form state.
21 $username = $form_state->getValue('name');
22 $password = $form_state->getValue('pass');
23
24 // Build the URL for the third-party login page, including the username and passwo\
25 rd as query parameters.
26 $login_url = 'https://thirdpartyprovider.com/login?username=' . urlencode($usernam\
27 e) . '&password=' . urlencode($password);
28
29 // Redirect the user to the third-party login page.
30 $form_state->setRedirect($login_url);
31 }

In the above code, the hook_examples_form_user_login_form_alter() function implements the


hook_form_FORM_ID_alter() hook, where FORM_ID is the ID of the form being altered, in this
case user_login_form. The function modifies the login form by adding a custom submit button,
with a submit handler function of hook_examples_user_login_form_submit().
When the button is clicked, the hook_examples_user_login_form_submit() function gets the
username and password from the form state , builds the URL for the third-party login page, including
the username and password as query parameters. Finally, the user is redirected to this URL using
the $form_state->setRedirect() method.

15.3: Modify the node edit form


In this example, the save button is changed from saying ”save” to ”update event”

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

12 $form['actions']['submit']['#value'] = t('Update Event');


13 }
14 }
15 }

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.

15.4: Modify fields in a node


This example does all sorts of interesting things to the node as it is about to be saved.
It grabs some dates, fills out some fields if the user is anonymous, does some date calculations,
changes the title of the node, looks up if to see if there is a connected node and grabs some info from
it and updates a date field. Finally it invalidates some cache tags. Phew!

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

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;
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 $tags[]= 'ogg:node:' . $node->getType();
46 $tags[]= 'ogg:node:' . $node->id();
47 ogg_mods_invalidate_node($node);
48 Cache::invalidateTags($tags);
49 }
50
51 /**
52 * Invalidate cache associated with various nodes.
53 *
54 * @param \Drupal\node\NodeInterface $node
55 *
56 * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
57 * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
58 */
59 function ogg_mods_invalidate_node(NodeInterface $node) {
60 $tags = [];
61 $node_type = $node->getType();
62 $tags[] = 'ogg:node:' . $node->id();
63 switch ($node_type) {
64 case 'news':
65 $tags[] = 'ogg:node:home';
66 $tags[] = 'ogg:node:landing_page_news';
67 $tags[] = 'ogg:node:news';
Hooks 242

68 $tags[] = 'ogg:views:home_recent_news';
69 break;
70 default:
71 break;
72 }
73 if (!empty($tags)) {
74 Cache::invalidateTags($tags);
75 }
76 }

You can read more about using hook_entity_presave at https://api.drupal.org/api/drupal/core%


21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_entity_presave/10 as well as
hook_ENTITY_TYPE_presave at https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%
21Entity%21entity.api.php/function/hook_ENTITY_TYPE_presave/10

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

15.6: Theme hooks


Here is an excerpt from the Theme System Overview at https://api.drupal.org/api/drupal/core%
21lib%21Drupal%21Core%21Render%21theme.api.php/group/themeable/10:
Preprocessing for Template Files
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_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 :

• hook_preprocess_html() the html template


• hook_preprocess_page() the page template
• hook_preprocess_node() the node template. Note hook_node_type_preprocess_node() also
works where you can specify the node type e.g. wc_product_preprocess_node() which expects
a content type of wc_product.
1 https://api.drupal.org/api/drupal/core%21includes%21theme.inc/function/template_preprocess/10
Hooks 244

15.6.2: hook_preprocess_node example 1


To add a custom variable to be displayed in your template, add a function in your .theme file like
the one listed below. This example also adds a #suffix to the field_image which renders that string
after the field_image is rendered.

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.

15.6.3: hook_preprocess_node example 2


This code is used to make a date range like 3/30/2023 -- 3/31/2023 appear as Mar 30-31, 2023
The date values are stored in the field_date which is a date range field. This code is from the .theme
file. Here we retrieve the starting and ending date values:

1 $from = $variables["node"]->get('field_date')->getValue()[0]['value'];
2 $to = $variables["node"]->get('field_date')->getValue()[0]['end_value'];

Here is the hook_preprocess_node()


Notice that we are creating a Twig variable called “scrunch_date” which we want to display.

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

15.7: Organizing your hooks code the OOP way


from Drupal 8 How to organise your hooks code in classes (Object-oriented way)2
To implement a hook example like hook_form_FORM_ID_alter3 for node_article_edit_form.
So instead of do something like:

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

18 * @param array $form


19 * Form array.
20 * @param \Drupal\Core\Form\FormStateInterface $form_state
21 * The current state of the form.
22 * @param $form_id
23 * String representing the id of the form.
24 */
25 public function alterForm(array &$form, FormStateInterface $form_state, $form_id) {
26 // Your code here the two following lines just an examples.
27 // Hide some fields.
28 $form['field_SOME_FIELD_NAME']['#access'] = FALSE;
29 // Attach some library ....
30 $form['#attached']['library'][] = 'MY_MODULE/SOME_LIBRARY';
31 }
32
33 }

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

And after that change your hook into:

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

15.8.1: Entity hooks


Here is an excerpt from the Drupal API at https://api.drupal.org/api/drupal/core%21lib%21Drupal%
21Core%21Entity%21entity.api.php/group/entity_crud/10:
5 https://git.drupalcode.org/project/drupal/-/blob/9.2.x/core/modules/content_moderation/content_moderation.module#L164
Hooks 250

15.8.1.1: Create operations

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.

15.8.1.2: Read/Load operations

To load (read) a single entity:


$entity = $storage->load13 ($id);
To load multiple entities: $entities = $storage->loadMultiple($ids);
Since load() calls loadMultiple(), these are really the same operation. Here is the order of hooks and
other operations that take place during entity loading:

• Entity is loaded from storage.


• postLoad() is called on the entity class, passing in all of the loaded entities.
• hook_entity_load14 ()
• hook_ENTITY_TYPE_load15 ()
6 https://api.drupal.org/api/drupal/10/search/create
7 https://api.drupal.org/api/drupal/10/search/save
8 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_ENTITY_TYPE_create/10
9 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_entity_create/10
10 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_ENTITY_TYPE_translation_
create/10
11 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_entity_translation_create/10
12 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/group/entity_crud/10#save
13 https://api.drupal.org/api/drupal/10/search/load
14 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_entity_load/10
15 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_ENTITY_TYPE_load/10
Hooks 251

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’);

15.8.1.3: Save operations

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:

• preSave() is called on the entity object, and field objects.


• hook_ENTITY_TYPE_presave16 ()
• hook_entity_presave17 ()
• Entity is saved to storage.
• For updates on content entities, if there is a translation added that was not previously present:

– 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

• For updates on content entities, if there was a translation removed:

– hook_ENTITY_TYPE_translation_delete20 ()
– hook_entity_translation_delete21 ()

• postSave() is called on the entity object.


• hook_ENTITY_TYPE_insert22 () (new) or hook_ENTITY_TYPE_update() (update)
• hook_entity_insert23 () (new) or hook_entity_update() (update)

Some specific entity types invoke hooks during preSave() or postSave() operations. Examples:

• Field configuration preSave(): hook_field_storage_config_update_forbid()


• Node postSave(): hook_node_access_records() and hook_node_access_records_alter()
• Config entities that are acting as entity bundles in postSave(): hook_entity_bundle_create()
• Comment: hook_comment_publish() and hook_comment_unpublish() as appropriate.

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.

15.8.1.4: Editing operations

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)

15.8.1.5: Delete operations

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

• preDelete() is called on the entity class.


• hook_ENTITY_TYPE_predelete28 ()
• hook_entity_predelete29 ()
• Entity and field information is removed from storage.
• postDelete() is called on the entity class.
• hook_ENTITY_TYPE_delete30 ()
• hook_entity_delete31 ()

Some specific entity types invoke hooks during the delete process. Examples:

• Entity bundle postDelete(): hook_entity_bundle_delete()

Individual revisions of an entity can also be deleted:


$storage->deleteRevision($revision_id);
This operation invokes the following operations and hooks:

• Revision is loaded (see Read/Load operations32 above).


• Revision and field information is removed from the database.
• hook_ENTITY_TYPE_revision_delete33 ()
• hook_entity_revision_delete34 ()

15.8.1.6: View/render operations

To make a render array for a loaded entity:


// You can omit the language ID if the default language is being used.
$build = $view_builder
->view35 ($entity, ’view_mode_name’, $language->getId());
You can also use the viewMultiple() method to view multiple entities.
Hooks invoked during the operation of building a render array:

• 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 ()

View builders for some types override these hooks, notably:

• The Tour view builder does not invoke any hooks.


• The Block view builder invokes hook_block_view_alter() and hook_block_view_BASE_-
BLOCK_ID_alter(). Note that in other view builders, the view alter hooks are run later in the
process.

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 ()

Some specific builders have specific hooks:

• The Node view builder invokes hook_node_links_alter().


• The Comment view builder invokes hook_comment_links_alter().

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

15.8.1.7: Other entity hooks

Some types of entities invoke hooks for specific operations:

• 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 ()

• Search indexing nodes:

– Node is loaded
– Node render array is built
– hook_node_update_index50 ()

15.8.2: Theme hooks


Here is an excerpt from the Theme System Overview at https://api.drupal.org/api/drupal/core%
21lib%21Drupal%21Core%21Render%21theme.api.php/group/themeable/10:
The theme system is invoked in \Drupal\Core\Render\Renderer::doRender51 () by calling the \Dru-
pal\Core\Theme\ThemeManagerInterface::render52 () function, which operates on the concept of
”theme hooks”. Theme hooks define how a particular type of data should be rendered. They
are registered by modules by implementing hook_theme(), which specifies the name of the
hook, the input ”variables” used to provide data and options, and other information. Modules
implementing hook_theme() also need to provide a default implementation for each of their theme
hooks in a Twig file, and they may also provide preprocessing functions. For example, the core
Search module defines a theme hook for a search result item in search_theme():

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.

15.8.2.1: Overriding Theme Hooks

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.

15.8.2.2: Preprocessing for Template Files

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

• 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.8.2.3: Theme hook suggestions

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).

For further information on overriding theme hooks see https://www.drupal.org/node/2186401


58 https://api.drupal.org/api/drupal/10/search/item-list--search-results.html.twig
59 https://api.drupal.org/api/drupal/10/search/item-list.html.twig
Hooks 258

15.8.2.4: Altering theme hook suggestions

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).

15.8.3: Reference Links


• What are hooks? from Drupalize.me March 202260
• Theme system overview on api.drupal.org61
• How to organize your hooks the object oriented way by Azz-eddine BERRAMOU Mar 202062

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

16.2: Blogs and articles


• Phil Norton of #!code writes some amazing articles at https://www.hashbangcode.com/ and
has useful code snippets at https://www.hashbangcode.com/snippets
• Martin Anderson-Clutz’s sandbox for a wide variety of fantastic Drupal, Acquia and module
information at https://www.mandclu.com/. He is also the author and maintainer of the and
about 8 bazillion other modules.
• Matt Glaman posts regularly about fascinating aspects of Drupal (and is the author of existing
and upcoming Drupal books). Keep up with him at https://mglaman.dev/

16.3: Pay videos


• drupalize.me1 is an amazing training facility spun off from Lullabot2 which provides on-
demand training and articles on all things Drupal. This is definitely worth a subscription
if you want to fully grasp Drupal development or bring new people up to speed.
• Symfonycasts3 is another incredibly good service that teaches all about the nuts and bolts of
Symfony, PHPUnit and many other tools. I think the Drupal course they have might be a little
dated now.

1 https://drupalize.me/
2 https://www.lullabot.com/
3 https://symfonycasts.com/
Learning and keeping up with Drupal 260

16.4: Drupal Training


• Mike Anello of Drupal Easy has some training courses worth checking out at https://www.
drupaleasy.com/academy . These include: Drupal Career Online and Professional Module
Development. Mike also offers project coaching and consulting.

16.5: Keep up with Drupal news


Everything used to happen on IRC, but now most of that seems to be on Slack at https://drupal.
slack.com/
Other great sources of news are:

• Planet drupal at https://www.drupal.org/planet


• The Weekly Drop, a Drupal newsletter The Drupal Association has partnered with TheWeek-
lyDrop to bring up to date News and Events to the Drupal community. Free subscription at
http://www.theweeklydrop.com/
• TheDropTimes is a news website started with the vision of contributing to the growth of a
vibrant community of users and contributors around Drupal through the process of covering
and promoting everything happening around Drupal. https://www.thedroptimes.com/
• Feed of Drupal related articles from all over the web https://drupalsun.com
• Mastodon is starting to show signs of Drupal life with a Mastodon instance focused on Drupal
at https://drupal.community/explore
• Twitter has many useful Drupal posts - https://twitter.com/search?q=%23drupal&src=typed_
query

16.6: Drupal Podcasts


• Talking Drupal is a long standing weekly conversation about and around Drupal development.
It was started by Stephen Cross, John Picozzi, and Nic Lafin in 2013 and is still going strong.
Check it out at https://www.talkingdrupal.com
• Lullabot Podcast: News, interviews, and tips about Drupal and Open Source from the team at
Lullabot. https://www.lullabot.com/podcasts/lullabot-podcast
• Mike Anello has a Drupal Easy podcast at https://www.drupaleasy.com/podcast
Learning and keeping up with Drupal 261

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');

17.2: Create an internal url


First a simple URL:

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'),

Then something more complicated like this URL to /reports/search?user=admin


Links, Aliases and URLs 263

1 $option = [
2 'query' => ['user' => 'admin'],
3 ];
4 $url = Url::fromUri('internal:/reports/search', $option);

17.3: The Drupal Core Url Class


The Drupal\Core\Url class is often used to create URL’s. Two important methods are:
Url::fromRoute() which takes a route name and parameters and

Url::fromUri() which takes an internal or external URL

See how these are used in some of the examples below.

17.4: The Drupal Core Link Class


Closely related and often used in conjunction with the Drupal Core URL class is the Dru-
pal\Core\Link class.
You can generate links several different ways.

17.4.1: Create a link to a node


1 //Using link generator to create a GeneratedLink.
2 $url = Url::fromUri('internal:/node/1');
3 $link = \Drupal::service('link_generator')->generate('My link', $url);

17.4.2: Create a link to a path with parameters


To create a link to a path like /reports/search?user=admin use this code.
Links, Aliases and URLs 264

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;

17.5: Another way to create a link to a node:


1 $nid = $item->id();
2 $options = ['relative' => TRUE]; //could be absolute instead of relative.
3
4 $url = Url::fromRoute('entity.node.canonical',['node' => $nid], $options);
5 $link = \Drupal::service('link_generator')->generate('My link', $url);

17.6: Create a link from an internal URL


1 use Drupal\Core\Url
2
3 $url = Url::fromUri('internal:/reports/search');
4 $link = \Drupal::service('link_generator')->generate('My link', $url);
5
6 // ->toString() will extract the string of the URL.
7 $url_string = Url::fromUri('internal:/node/' . $id)->toString();

17.7: Check if a link field is empty


1 if (!$citation_node->field_link->uri) {
2 // Empty.
3 }

17.8: Retrieve a link field from a node or a paragraph


The link field field_link is extracted from the node and a valid uri is extracted from that field.
Links, Aliases and URLs 265

1 $correction_node = Node::load($nid);
2 $current_url = $correction_node->get('field_link')->uri;

Or from a paragraph field

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 }

Removing ->first() as in:

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.

17.9: Retrieve a URL field

17.9.1: External links


You can get the URL (for external links) and then just the text part.
Note this doesn’t work for internal links. Note also this slightly convoluted example has a reference
field field_sf_contract_ref which has a link to another entity and the field_vendor_url->first()-
>getUrl() is the important part. Also note, this is a single-value field (not a multivalue field - so
the first() call may be a little disturbing to those who expect things to be a little clearer.)
Links, Aliases and URLs 266

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 }

A slightly simpler example from a form

1 $citation_link = $citation->get('field_link');
2 if (!$citation_link->isEmpty()) {
3 $citation_link = $citation->field_link->first()->getUrl()->toString();
4 }

17.9.2: Internal links


For internal links, use getUrl()for the URL and ->title for the title.

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 }

17.10: Get the NID from a URL Alias


To get the nid for a node, you can pass the URL alias to getPathByAlias.
Links, Aliases and URLs 267

1 // Given "/test-node, returns "/node/32".


2 $alias = "/test-node";
3 $path = \Drupal::service('path_alias.manager')->getPathByAlias($alias);

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 }

17.11: Get the Taxonomy Term ID from a URL alias


Returns taxonomy/term/5

1 $term_path_with_tid = \Drupal::service('path_alias.manager')->getPathByAlias('/hunge\
2 r-strike');

17.12: Get URL alias for a taxonomy term


This returns term/5 if no alias is set, otherwise it returns the alias.

1 $term5_url = Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => 5]\


2 , $options);
3 $term5_alias = $term5_url->toString();

17.13: Get the User ID from a URL alias


Returns ”/user/2”

1 //User
2 $user_path_with_uid = \Drupal::service('path_alias.manager')->getPathByAlias('/selwy\
3 n-the-chap');

17.14: Get the URL alias for a node


If no alias is set, this will return ”/node/32”. Note. If there are multiple aliases, you will get the most
recently created one.
Links, Aliases and URLs 268

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();

17.15: Create a Node Alias


URL aliases are entities so you create them like you would any entity. Be sure to save() them.

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();

17.16: Get the current Path


This returns the current relative path. For node pages, the return value will be in the form ”/node/32”
For taxonomy ”taxonomy/term/5”, for user ”user/2” if it exists otherwise it will return the current
request URI.
Links, Aliases and URLs 269

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();

17.17: Get current nid, node type and title


There are two ways to retrieve the current node - via the request or via the 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:

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 }

And from https://drupal.stackexchange.com/questions/145823/how-do-i-get-the-current-node-id


when you are using or creating a custom block then you have to follow this code to get current
node id. Not sure if it is correct
Links, Aliases and URLs 270

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 }

17.18: How to get current Route name


A Drupal route is returned in the form of a string e.g. view.files_browser.page_1

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.

17.19: Get current Document root path


This will return the current document root path like ”/var/www/html/project1”.
Links, Aliases and URLs 271

1 $image_path = \Drupal::service('file_system')->realpath();

17.20: Retrieve URL argument parameters


You can extract the url arguments with

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

17.21: Retrieve query and GET or POST parameters


($_POST and $_GET)
For get variables

1 $query = \Drupal::request()->query->get('name');
2 $name = $_GET['abc'];

For POST variables:

1 $name = \Drupal::request()->request->get('name');
2 //or
3 $name = $_POST['abc'];

For all items in a GET:


Links, Aliases and URLs 272

1 $query = \Drupal::request()->query->all();
2 $search_term = $query['query'];
3 $collection = $query['collection'];

Be wary about caching. From https://drupal.stackexchange.com/questions/231953/get-in-drupal-


8/231954#231954 the code provided only works the first time so it is important to add a ‘#cache’
context in the markup.

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 ];

More about caching render arrays: https://www.drupal.org/docs/8/api/render-api/cacheability-of-


render-arrays

17.22: Modify URL Aliases programmatically with


hook_pathauto_alias_alter
The pathauto1 contrib module includes a nice hook that you can use to modify url aliases on the fly.
You just do your necessary checks (the current entity is stored in \context['data']) and change the
alias that is passed. Pathauto does the rest.
As implemented in a module file.
1 https://www.drupal.org/project/pathauto
Links, Aliases and URLs 273

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 }

Also see an example at https://makedrupaleasy.com/articles/drupal-version-7-9-how-update-alias-


programmatically-using-value-field

17.23: Drupal l() is deprecated


Deprecated The l() method (lower case letter L) was a convenience wrapper for the link generator
service’s generate() method. So do this instead:

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);

17.24: Reference links


• Good reference from 2017 for creating links in Drupal at https://agaric.coop/blog/creating-
links-code-drupal-8
• Hashbang code: Drupal 9: Programmatically Creating And Using URLs And Links, March
20222
2 https://www.hashbangcode.com/article/drupal-9-programmatically-creating-and-using-urls-and-links
18: Logging
18.1: Quick log to watchdog
With the Database Logging (dblog) module enabled, you can easily log messages to the database log
(watchdog table)

1 //Class with method name


2 $method = __METHOD__;
3
4 // Function name only.
5 $function = __FUNCTION__;
6
7 // Function name, filename, line number.
8 $str = __FUNCTION__." in ".__FILE__." at ".__LINE__;
9
10 \Drupal::logger('test')->info("method = $method");
11 \Drupal::logger('test')->info("Something goofed up at $str");
12 \Drupal::logger('test')->debug("Something goofed up at $str");
13 \Drupal::logger('test')->critical("Something goofed up at $str");
14
15 \Drupal::service('logger.factory')->get('test')->error('This is my error message');

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

18.2: Log an email notification was sent to the the


email address for the site.
1 $email_config = \Drupal::config('system.site');
2 $to = $email_config->get('mail');
3 // Display message to screen.
4 $messenger->addMessage("sent a message to $to");
5 // Log it.
6 \Drupal::logger('DIR')->info("Email notification send to $to succeeded");

Incidentally, calling \Drupal::logger like this

1 \Drupal::logger('my_module')->error('This is my error message');

actually does this under the covers:

1 \Drupal::service('logger.factory')->get('hello_world')->error('This is my error mes\


2 sage');

18.3: Logging from a service using dependency


injection
From a controller e.g. WebsphereAddress.php
In the websphere_commerce.services.yml specify the @logger.factory to be passed into the
constructor.

1 services:
2 websphere_commerce.address:
3 class: Drupal\websphere_commerce\WebSphereAddressService
4 arguments: ['@config.factory', '@logger.factory']

In the WebsphereAddress.php file specify use statements:

1 use Drupal\Core\Logger\LoggerChannelFactory;
2 use Drupal\Core\Logger\LoggerChannelFactoryInterface;

Create a protected var to store the logger service:


Logging 277

1 /**
2 * @var Drupal\Core\Logger\LoggerChannelFactory
3 */
4 protected $logger;

Here is the constructor:

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 }

18.4: Another example using the logging via


dependency injection
From the excellent folks at symfonycasts.com1 who have a sweet Drupal 8 course2 which is still
relevant and worth checking out.
In your dino_roar.services.yml file, add the listener and specify the arguments of
['@logger.factory']

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

1 $ drupal debug:container | grep log

one of the results specifies the factory which you can use below:

1 logger.factory Drupal\\Core\\Logger\\LoggerChannelFactory

or with Drush and devel

1 $ drush dcs log


2
3 - logger.dblog
4 - logger.drupaltodrush
5 - logger.factory

So dino_roar.dino_listener will pass the logger.factory service to your DinoListener class.

1 dino_roar.dino_listener:
2 class: Drupal\dino_roar\Jurassic\DinoListener
3 arguments: ['@logger.factory']
4 tags:
5 - {name: event_subscriber}

in your DinoListener.php specify a constructor argument of LoggerChannelFactoryInterface and


store it.

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 }

18.5: Logging exceptions from a try catch block


In this controller, the try block calls the test() method which throws an exception. The catch block
catches the exception and logs the message (and for fun displays a message in the notification area
also.)

1 public function build() {


2
3 try {
4 $this->test();
5 }
6 catch (\Exception $e) {
7 watchdog_exception('nuts_connect', $e);
8 $messenger->addMessage("No, I got caught!");
9 }
10
11 $build['content'] = [
12
13 '#type' => 'item',
14 '#markup' => $str,
15 ];
16
Logging 280

17 return $build;
18 }
19
20 function test() {
21 throw new \Exception("blah", 7);
22 }

18.6: Display a message in the notification area


You can display a message with:

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);

Use $repeat = FALSE to suppress duplicate messages.


Specify MessengerInterface::TYPE_STATUS,MessengerInterface::TYPE_WARNING, or
MessengerInterface::TYPE_ERROR to indicate the severity.

Don’t forget

1 use Drupal\Core\Messenger\MessengerInterface;

Note. addMessage() adds class="messages messages--status" to the div surrounding your


message while addError adds class="messages messages--status" . Use these classes to format
the message appropriately.
When you need to display a message in a form, use the $this->messenger() that is provided by the
Drupal\Core\Messenger\MessengerTrait;

1 $this->messenger()->addStatus($this->t('Running in Destructive Mode - Changes ARE co\


2 mmitted to the database!'));

e.g.
Logging 281

1 \Drupal::messenger()->addMessage('Program pending, please assign team and initialize\


2 . ', MessengerInterface::TYPE_WARNING);

18.7: Display a variable while debugging


You can use var_dump and print_r but sometimes it is difficult to see where they display.

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 }

19.2: Permanently update menu links in a


hook_update using entityQuery
To update menu item links, you can use the following code (from a .install file).
Menus 284

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 }

19.3: Add menu items with hook_update


Menus are config entities while menu items are content entities. Here, a hook_update creates some
menu items and adds them to an existing menu.

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 }

19.4: Permanently modify or delete menu items with


hook_update
Below, we use hook_update to grab all the menu items in the menu called pdf-wide-utility, then
loop thru them, delete some, and change the weight of some.

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);

19.5: Peer up a menu to its parents to see if it is a child


of a content type
Here, we need to display a sidebar if the current node is both a page and a child (or any level of
offspring, e.g., grandchild, great-grandchild, etc.) of a content type “unit.” This means we need to
“peer” up the menu chain to see what kind of parent the node has.
This example was implemented in a .theme file.
When hook_preprocess_node is called for my content type, and we are viewing a full node, we grab
the nid and call the _check_ancestry_for_unit(), which looks up the menu chain for a menu item
that points to a node of type “unit”. If there is one, we display the sidebar, i.e., we set the $variables
[‘show_sidebar_menu’] to TRUE.

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 }

Here is the function _check_ancestry_for_unit():


Menus 287

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 }

19.6: Find all the children of a menu


From a .module file, I needed to load a dropdown with items from the main menu.
You can load the menu up with this:

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 }

This is basically the same as:

1 $sub_nav = $menu_tree->load('main', new \Drupal\Core\Menu\MenuTreeParameters());

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 }

And the template that is used to display the dropdown, node--news-stories-landing-page.html.twig:

1 <form action="#" class="filter-form">


2 <div class="search-form">
3 <input type="search" placeholder="Search Events by Keyword" title="Type search tex\
4 t here">
5 <button type="button"><i class="icon icon-search"></i><span class="show-for-sr">Se\
6 arch</span></button>
7 </div>
8 <div class="grid-x grid-margin-x align-justify">
9 {% for filter in filter_data %}
10 <div class="cell medium-3 xlarge-2">
11 <label for="by-{{ filter.type }}" class="show-for-medium">By {{ filter.type|\
12 title }}</label>
13 <select id="by-{{ filter.type }}">
14 <option value="/search-news?{{ filter.type }}=all">Select {{filter.type|ti\
15 tle}}</option>
16 {% for item in filter.info %}
17 <option value="/search-news?{{ filter.type }}={{ item.value }}">{{ item.\
18 title }}</option>
19 {% endfor %}
20 </select>
21 </div>
Menus 291

22 {% endfor %}
23 </div>
24 </form>

And the form looks like this:

19.7: Build a menu and all its children


This also looks pretty interesting, but I haven’t tried it. It is from https://stackoverflow.com/
questions/54245942/drupal-8-menulinkcontent-get-all-children-via-loadbyproperties/54254491

1 function generateSubMenuTree(&$output, &$input, $parent = FALSE) {


2 $input = array_values($input);
3 foreach ($input as $key => $item) {
4 //If menu element disabled skip this branch
5 if ($item->link->isEnabled()) {
6 $key = 'submenu-' . $key;
7 $name = $item->link->getTitle();
8 $url = $item->link->getUrlObject();
9 $url_string = $url->toString();
10
11 //If not root element, add as child
12 if ($parent === FALSE) {
13 $output[$key] = [
14 'name' => $name,
15 'tid' => $key,
16 'url_str' => $url_string
17 ];
18 }
19 else {
20 $parent = 'submenu-' . $parent;
21 $output['child'][$key] = [
22 'name' => $name,
23 'tid' => $key,
Menus 292

24 'url_str' => $url_string


25 ];
26 }
27
28 if ($item->hasChildren) {
29 if ($item->depth == 1) {
30 generateSubMenuTree($output[$key], $item->subtree, $key);
31 }
32 else {
33 generateSubMenuTree($output['child'][$key], $item->subtree, $key);
34 }
35 }
36 }
37 }
38 }

It is called with:

1 //Get drupal menu


2 $sub_nav = \Drupal::menuTree()->load('main', new \Drupal\Core\Menu\MenuTreeParameter\
3 s());
4
5 //Generate array
6 generateSubMenuTree($menu_tree2, $sub_nav);

19.8: Create custom Twig extension for rendering a


menu
Note. The module twig_tweak module1 can do all this with one line of code:

1 {% raw %}{{ drupal_menu('main', 2, 3, TRUE) }}{% endraw %}

More at: https://www.drupal.org/docs/8/modules/twig-tweak/cheat-sheet


From https://www.drupal.org/forum/support/theme-development/2015-01-29/rendering-a-menu-
in-twig-drupal-8, Peter from Dusseldorf shows how to render a menu into a render array. He runs
through the whole load, transform (with manipulators) and build. He does this through the magic
of a twig extension. So, in modules/custom/custom_module/src/Twig/RenderMenuExtension.php:

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:

1 public function getFunctions() {


2 $context_options = ['needs_context' => TRUE];
3 $all_options = ['needs_environment' => TRUE, 'needs_context' => TRUE];
4 return [
5 new \Twig_SimpleFunction('drupal_render', [$this, 'renderMenu']),
6 ];
7 }

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 {% raw %}{{ renderMenu('main') }}{% endraw %}

19.9: Active Trail


From https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Menu%21MenuTreeParameters.php/prope
The IDs from the currently active menu link to the root of the whole tree.
Active trail is an array of menu link plugin IDs, representing the trail from the currently active menu
link to the (”real”) root of that menu link’s menu. This does not affect the way the tree is built. It is
only used to set the value of the inActiveTrail property for each tree element.
In the code below, I grab the active trail for an item that is in a menu. Then, I grab all
the links for the current node_id, pull off the first one, grab the plugin (which is menu_link_-
content:957297e4-38eb-4502-868a-668407c71a44–the id from the menu_tree table), and get the
parameters (all the juicy goodness about this menu item). You can find a nice trail back up the
menu chain along the active_trail. See activeTrail in the debug variable dump below.
Menus 295

1 // Get current item's menu


2 /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
3 $menu_link_manager = \Drupal::service('plugin.manager.menu.link');
4 $links = $menu_link_manager->loadLinksByRoute('entity.node.canonical', ['node' => $n\
5 ode_id]);
6 /** @var \Drupal\Core\Menu\MenuLinkContent $currentMenuItem */
7 $currentMenuItem = reset($links);
8
9 // grab plugin for the current item
10 $pluginId = $currentMenuItem->getPluginId();
11
12 /** @var \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree */
13 $menu_tree = \Drupal::service('menu.link_tree');
14 $parameters = $menu_tree->getCurrentRouteMenuTreeParameters($menu_name);
15
16 // grab the active trail.
17 $active_trail = array_keys($parameters->activeTrail);

Extracting out the active trail gives this useful information:


Menus 296

19.10: Get a node’s menu item and more


Here we get the current route’s menu item using its nid then pull the link from the array using reset,
and we can extract the URL as well as other exciting things. Mostly we want to check its children,
parents etc. In the code below, we grab its URL as well as its parent and its title.

1 /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */


2 $menu_link_manager = \Drupal::service('plugin.manager.menu.link');
3 $links = $menu_link_manager->loadLinksByRoute('entity.node.canonical', ['node' => $n\
4 id]);
5 $link = reset($links);
6 $urlobject = $link->getUrlObject();
7 $url_string = $urlobject->toString();
8 $x = $link->getParent(); //get the parent menu item GUID.
9 $y = $link->getTitle(); // get the title of the menu item.
10
11 $menu_name = $link->getMenuName(); // get the menu name e.g. "main"

19.11: Create menu items in your custom module


When creating a menu for your module, you need a YAML file like this from dev1
pageexample. The names (e.g. pageexample.description and pageexample.simple) are
arbitrary, the title is the menu text, and the route_name comes from the routing yml file
(web/modules/custom/pageexample/pageexample.routing.yml). The parent value is interesting.
Menus 297

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.

1 # Field name is `field_paragraph_authors`, specific property `target_id`:


2 field_paragraph_authors/target_id:
3 - plugin: migration_lookup
4 # Dependent Migration called `migration_paragraph_linked_author`
5 migration: migration_paragraph_linked_author
6 # Don't create stub content if the row currently being processes does not map to\
7 an item in the earlier-run Migration
8 no_stub: true
9 # How to map this Migration with the earlier-run Migration
10 source: sku
11 - plugin: skip_on_empty
12 # Method: If empty, skip only this field mapping (`process`), not the entire Row\
13 (`row`)
14 method: process
15 - plugin: extract
16 # This destination property is 1st element in the migration-lookup array.
17 index:
18 - 0
19 # Other half of `field_paragraph_authors`, specific property `target_revision_id`:
20 field_paragraph_authors/target_revision_id:
21 - plugin: migration_lookup
22 migration: migration_paragraph_linked_author
23 no_stub: true
24 source: sku
25 - plugin: skip_on_empty
26 method: process
27 - plugin: extract
28 # This destination property is the 2nd element in the migration-lookup array.
Migration 299

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.

21.2: Dialog title


You define the title for the dialog by specifying it in the *.routing.yml file. In the file below the title
is in the _title key:

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

21.3: Links to slide-in dialogs


In your controller (or in a form) you can define links in your render array which can fire off the
slide-in dialogs. E.g.

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:

1 'data-dialog-type' => 'modal',


2 'data-dialog-options' => Json::encode([ 'width' => 'auto',]),

21.4: Modal dialog example


This example displays a page with links to pop up two different modal dialogs. Clicking on
either of the links will display a slightly different modal dialogs. The page looks like this:
Modal Dialogs 302

Here is the routing file at web/modules/custom/modal_examples/modal_examples.routing.yml.


The first route (modal_examples.example1) displays the links. The second route (modal_exam-
ples.modal1) allows the modal be displayed on a page on its own. This route also has parameters
which can be useful when you want to display some relevant variable information in the dialog.

1 # Controller with buttons to open modals


2 modal_examples.example1:
3 path: '/modal-examples/example1'
4 defaults:
5 _title: 'Modal Examples (example1)'
6 _controller: '\Drupal\modal_examples\Controller\ModalExamplesController::buildEx\
7 ample1'
8 requirements:
9 _permission: 'access content'
10 # first modal
11 modal_examples.modal1:
12 path: '/modal-examples/modal1/{program_id}/{type}'
13 defaults:
14 _title: 'Modal 1 with parameters'
15 _controller: '\Drupal\modal_examples\Controller\ModalExamplesController::buildMo\
16 dal1'
17 requirements:
18 _permission: 'access content'
19 options:
20 parameters:
21 program_id:
22 type:
23 no_cache: 'TRUE'
Modal Dialogs 303

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'

In web/modules/custom/modal_examples/src/Controller/ModalExamplesController.php are the


functions for both the page (which displays the link) and first modal.
Add the parameters to the end of the Url::fromRoute() call to pass them to the modal. You will
probably do this a lot.

1 '#url' => Url::fromRoute('modal_examples.modal1', [


2 'program_id' => 123,
3 'type' => 'all',
4 ]),

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

19 '#markup' => $this->t('Route: %route', ['%route' => $route_name]),


20 ];
21
22 $build['link-to-modal1'] = [
23 '#type' => 'link',
24 '#title' => t('Link to a custom modal (modal1)'),
25 '#url' => Url::fromRoute('modal_examples.modal1', [
26 'program_id' => 123,
27 'type' => 'all',
28 ]),
29 '#attributes' => [
30 'id' => 'view-correlation-' . 12345,
31 'class' => ['use-ajax'],
32 'aria-label' => 'View useful information pertaining to item ' . '12345',
33 '#prefix' => '<div class="abcdef">',
34 '#suffix' => '</div>',
35 'data-dialog-type' => 'modal',
36 'data-dialog-options' => Json::encode(
37 [
38 'width' => 'auto',
39 ]
40 ),
41 ],
42 ];
43
44 $build['link-to-modal2'] = [
45 '#type' => 'link',
46 '#url' => new Url('modal_examples.modal2'),
47 '#title' => 'Link to a node in a dialog (modal2)',
48 '#prefix' => '<div class="pqrst">',
49 '#suffix' => '</div>',
50 '#attributes' => [
51 'class' => ['use-ajax'],
52 ],
53 ];
54
55 $build['#attached']['library'][] = 'core/drupal.dialog.ajax';
56
57 return $build;
58 }
59
60 public function buildModal1(int $program_id, string $type) {
61
Modal Dialogs 305

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

And the second

21.4.1: Passing entities as parameters


Sometimes it is useful to pass a node as a parameter so you can quickly grab fields from it. To do
this, specify type: entity: node (or other type of entity) in the *.routing.yml file. See the example
below where parameters: program and correlation are identified as nodes. In your code, you pass
the node id (not the entire node) and Drupal will automatically load the node for you and pass it to
the controller:
Modal Dialogs 309

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 public function content(EntityInterface $program, EntityInterface $correlation, stri\


2 ng $type = 'all'): array {
3 $program_nid = $program->id();
4 $correlation_nid = $correlation->id();
5 $vote_number = $program->get('field_srp_vote_number')->value;

21.5: Modal form example


This example displays a form with a button. When a user presses the button, a dialog containing
another form appears.
This code is a little convoluted as the link you click to open the modal form calls a controller function,
ModalExamplesController2::openModalForm(), which loads the form and opens it with an ajax
command, OpenModalDialogCommand. You can directly point a link to a form route (as you can find
in other examples below.)
Also notice that controller uses dependency injection to load the form builder (which is the
recommended way to do things.).
In web/modules/custom/modal_examples/modal_examples.routing.yml we first set up a route for
the form which will be initially displayed. This is identified as modal_examples.form1.
Secondly, we set up a route for the popup form, modal_examples.modal_form.
Modal Dialogs 310

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:

And here is the modal popped up over it:


Modal Dialogs 311

Here is the code for the first form in web/modules/custom/modal_examples/src/Form/ExampleForm.php.


Notice the link at $form['actions']['open_modal'] which is used to invoke the modal form. Note.
It is essential that you specify the use-ajax class for this link.

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 }

And finally, here is the ”modal” form itself at web/modules/custom/modal_examples/src/Form/ExampleModalForm.p


Modal Dialogs 315

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

87 'event' => 'click',


88 ],
89 ];
90
91 $form['#attached']['library'][] = 'core/drupal.dialog.ajax';
92
93 return $form;
94 }
95
96 /**
97 * AJAX callback handler that displays any errors or a success message.
98 */
99 public function submitModalFormAjax(array $form, FormStateInterface $form_state) {
100 $response = new AjaxResponse();
101
102 // If there are any form errors, re-display the form.
103 if ($form_state->hasAnyErrors()) {
104 $response->addCommand(new ReplaceCommand('#modal_example_form', $form));
105 }
106 else {
107 //Close the modal.
108 $command = new CloseModalDialogCommand();
109 $response->addCommand($command);
110
111 // Pop up a new modal with a title success etc.
112 $response->addCommand(new OpenModalDialogCommand("Success!", 'The modal form h\
113 as been submitted.', ['width' => 800]));
114 }
115
116 return $response;
117 }
118
119
120 /**
121 * @return \Drupal\Core\Ajax\AjaxResponse
122 */
123 public function closeModalForm() {
124 $command = new CloseModalDialogCommand();
125 $response = new AjaxResponse();
126
127 $response->addCommand($command);
128 // $response->addCommand(new OpenModalDialogCommand("Cancel!", 'Why did you canc\
129 el me?', ['width' => 400]));
Modal Dialogs 318

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 }

21.6: Slide-in dialog/Off-canvas dialog


Slide in dialogs are modals that appear to slide-in from the right side or top of the screen. Like any
modal dialogs they can contain forms

21.7: Slide-in Dialog Example


This example shows 4 different types of links which fire off variations of slide-in dialogs. Check out
the links below:
Modal Dialogs 319

In web/modules/custom/modal_examples/modal_examples.routing.yml we specify the routes for


our controller, example2 as well as modal1, form2 and login_form:

1 # Controller with buttons to open modals


2 modal_examples.example2:
3 path: '/modal-examples/example2'
4 defaults:
5 _title: 'Modal Examples (example2)'
6 _controller: '\Drupal\modal_examples\Controller\ModalExamplesController2::buildE\
7 xample2'
8 requirements:
9 _permission: 'access content'
10
11 # modal1
12 modal_examples.modal1:
13 path: '/modal-examples/modal1/{program_id}/{type}'
14 defaults:
15 _title: 'Modal 1 with parameters'
16 _controller: '\Drupal\modal_examples\Controller\ModalExamplesController::buildMo\
17 dal1'
18 requirements:
19 _permission: 'access content'
20 options:
21 parameters:
22 program_id:
23 type:
Modal Dialogs 320

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'

Here is the controller for example2, web/modules/custom/modal_examples/src/Controller/ModalExamplesControl


You will find the functions buildExample2() which creates the 4 links and buildLoginForm() which
uses Drupal’s formbuilder to build the login form.

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

104 '#prefix' => '<div class="pqrst">',


105 '#suffix' => '</div>',
106 '#title' => t('Link to off canvas/slide-in custom modal dialog (modal1) from t\
107 op'),
108 '#url' => Url::fromRoute('modal_examples.modal1', [
109 'program_id' => 123,
110 'type' => 'all',
111 ]),
112 '#attributes' => [
113 'id' => 'view-correlation-' . 12345,
114 'class' => ['use-ajax'],
115 'aria-label' => 'View useful information pertaining to item ' . '12345',
116 '#prefix' => '<div class="abcdef">',
117 '#suffix' => '</div>',
118 'data-dialog-type' => 'dialog.off_canvas_top',
119 'data-dialog-options' => Json::encode(
120 [
121 'width' => 'auto',
122 ]
123 ),
124 ],
125 ];
126
127
128 $build['link-to-example2-form'] = [
129 '#type' => 'link',
130 '#prefix' => '<div class="pqrst">',
131 '#suffix' => '</div>',
132 '#title'=> t('Link to off canvas/slide-in form (form2) '),
133 '#url' => Url::fromRoute('modal_examples.form2', [
134 'program_id' => 123,
135 'type' => 'all',
136 ]),
137 '#attributes' => [
138 'id' => 'important-id-' . 12345,
139 'class' => ['use-ajax'],
140 '#prefix' => '<div class="abcdef">',
141 '#suffix' => '</div>',
142 'data-dialog-type' => 'dialog.off_canvas',
143 'data-dialog-options' => Json::encode(
144 [
145 'width' => 'auto',
146 ]
Modal Dialogs 324

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 }

BuildModal1() is in ModalExamplesController.php (above):


Modal Dialogs 325

1 public function buildModal1(int $program_id, string $type) {


2
3 $build['content'] = [
4 '#type' => 'item',
5 '#markup' => $this->t('Some useful information!'),
6 ];
7 $build['other_content'] = [
8 '#type' => 'item',
9 '#markup' => $this->t('Program id: @program_id. Type: @type', [
10 '@program_id' => $program_id,
11 '@type' => $type,
12 ]),
13 ];
14
15 return $build;
16 }

This is what to expect when you click the first link:

And the second which shows the dialog sliding in from the top:
Modal Dialogs 326

Here is our custom form sliding in:


Modal Dialogs 327

And finally the login form sliding in:


Modal Dialogs 328

21.8: Block with a link to popup a custom modal dialog


This is an example from http://befused.com/drupal/modal-module
In the module custom_modal, there is a custom_modal.routing.yml which specifies the route and
controller.

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

24 '#type' => 'markup',


25 '#markup' => Link::fromTextAndUrl(t('Open modal'), $link_url)->toString(),
26 '#attached' => ['library' => ['core/drupal.dialog.ajax']]
27 );
28 }
29
30 }

Then in the controller, CustomModalController.php the modal() function builds the dialog using a
new AjaxResponse() object.

1 class CustomModalController extends ControllerBase {


2
3 public function modal() {
4 $options = [
5 'dialogClass' => 'popup-dialog-class',
6 'width' => '50%',
7 ];
8 $response = new AjaxResponse();
9 $response->addCommand(new OpenModalDialogCommand(t('Modal title'), t('The modal \
10 text'), $options));
11
12 return $response;
13 }
14
15 }

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.”

21.9: No-code modal dialogs


Modal dialogs are incredibly useful on websites as they allow the user to do something without
having to leave the web page they are on. Drupal has a Dialog API in core, which greatly reduces
the amount of code you need to write to create a modal dialog. Dialogs in Drupal 8 leverage jQuery
UI. This is from an article at http://befused.com/drupal/modal .
This capability puts some awesome power in the hands of site builders.
In a custom block with full HTML enabled use the following HTML.
Modal Dialogs 330

1 <p><a class="use-ajax" data-dialog-type="modal" href="/search/node">Search</a></p>

Or to make this dialog a little wider. E.g. 800 pixels.

1 <p><a class="use-ajax" data-dialog-options="{&quot;width&quot;:800}" data-dialog-typ\


2 e="modal" href="/search/node">Search</a></p>

And to show a node instead of the search dialog (any valid drupal path will do here)

1 <p><a class="use-ajax" data-dialog-options="{&quot;width&quot;:800}" data-dialog-typ\


2 e="modal" href="/node/2">See node 2</a></p>

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;

22.2: Load a numeric field value


When you load a numberic field, Drupal returns a number i.e. 0 even if that field was never
initialized with a value.

1 $accepted_votes = $feedback_error_node->get('field_accepted_votes')->value;
2 // Returns 0 if no value was entered into the field.

22.3: Set field values


1 $node->set('field_tks_subchapter', $subchapterFullStatement);
2 $node->set('field_tks_standard_type', "TEKS");
3 $node->save();

22.4: Get current page title


Use this in a controller, to return the current page title.
Nodes and Fields 332

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);}

22.5: Test if variable is a node


1 // Is this variable a node?
2 if ($ref instanceof EntityInterface && $ref->getEntityTypeId === 'node') {
3 // yep
4 }

22.6: Get the current nid, node type and title


Here are two ways to retrieve the current node–via the request or using the 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 }

And from https://drupal.stackexchange.com/questions/145823/how-do-i-get-the-current-node-id


when you are using or creating a custom block then you have to follow this code to get current
node id. Not sure if it is correct

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

22.7: Retrieve current node id (nid)


If the site is currently displaying node/123, this will return a path node/123.

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 }

22.8: Retrieve node info from current path


You don’t need to load the node

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

1 // Grab current node.


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 $v = $node->get('field_footer_social_media_headin')->value;
8 if (!empty($v)) {
9 ...
10 }
11 $v = $node->get('field_facebook')->value;
12 if (!empty($v)) {
13 ...
14 }

Note you can also $node->get('field_facebook')->getValue() which returns an array of values.


This is a good way to retrieve unlimited value fields i.e. fields that have more than 1 value.

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:

1 $topics = $node->get('field_news_topics_for_listing')->getValue(); //get.

or

1 $topics = $node->field_news_topics_for_listing->getValue(); //magic getter.


2
3 foreach ($topics as $topic) {
4 // Do something.
5 }

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 }

Note you can also


Nodes and Fields 337

1 $node->get('field_facebook')->getValue();

Which returns an array of values.

22.12: Load the user id (uid) for a node


1 $my_node->getOwnerId();

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.

22.13: Test if a field is empty


1 $entity->get('field_name')->isEmpty()

For an entity field use:

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 }

22.14: Load a node and update a field


Nodes and Fields 338

1 $node = \Drupal::entityTypeManager()->getStorage('node')->load(1);
2 $url = $node->set('field_url', 'the-blahblah');
3 $node->save();

22.15: Load values from a date range field


Start date and then end date

1 $node->get('field_cn_start_end_dates')->value
2 $node->get('field_cn_start_end_dates')->end_value

22.16: Load multivalue field


Multivalue fields can be loaded with get('fieldname') or using a magic field getter like
$node->field_my_field. Adding ->getValue() to the end of either of these calls returns a simple
array.
For example, a multivalue text field, this will return an array of values like this:

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

22.16.1: Iterate through results


Using $node->field_condiment or $node->get('field_condiment') returns a Drupal\Core\Field\FieldItemList
which is iteratable. You can loop through the results and retrieve the values like this:
Nodes and Fields 339

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 }

For entity reference fields, use target_id rather than ->value.


And you can check if there is a particular item in the array like this:

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;

22.16.2: Read a specific instance


You can directly reference an item by specifying an array offset. The index key (0 or 1 below) can
also be referred to as the delta.

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 }

22.17: Update a multivalue field


This can be a little tricky especially if you want to preserve the existing values in the field.
Here I used the getValue() to load all the values. This returns an array of values like:

1 $data = $node->get('field_event_ref')->getValue();
2
3 $result0 = $data[0]['value'];
4 $result1 = $data[1]['value'];

So to update one of them, I want to do something like this: $data[1]['value']='flour';.


e.g. I load up the current values, fill in the 6th [5]item and save them. This preserves the existing
values in positions [0]through [4].

1 $values = $node->get('field_voting_status')->getValue();
2 $values[5]['value'] = 'incomplete';
3 $node->set('field_srp_voting_status', $values);
4 $node->save();

22.17.1: Function to read and write multivalue fields


Here is a function which reads and writes multivalue fields safely. You pass it the $node->field_-
name, the index (vote_number) etc. and then it builds and returns an array formatted for updating
the field data. It can also update the array if you pass in a value.
It could probably be genericized further.
Nodes and Fields 341

1 public static function getMultivalueFieldArrayOfValues($values_field_data, $vote_num\


2 ber = 0, $new_value = NULL, $field_type = 'value', $default_value = NULL) {
3 $values_array = [];
4 foreach($values_field_data as $data) {
5 $values_array[] = [
6 $field_type => $data->value,
7 ];
8 }
9 if(!is_null($new_value)) {
10 //CHECK FOR PREVIOUS VALUES. IF THEY DO NOT EXIST SET TO NEW VALUE
11 for($i=0; $i<=$vote_number; $i++) {
12 if(!isset($values_array[$i])) {
13 if(!is_null($default_value)) {
14 $values_array[$i] = $default_value;
15 }
16 else {
17 $values_array[$i] = $new_value;
18 }
19 }
20 }
21 if(isset($values_array[$vote_number])) {
22 $values_array[$vote_number] = [$field_type => $new_value];
23 }
24 else {
25 $values_array[] = [$field_type => $new_value];
26 }
27 }
28 return $values_array;
29 }

Example of using the above function for reading data and then writing the data without using it:

1 // Retrieve current field values for correlation narrative/activity status fields


2
3 $activity_status = 'accepted';
4 $activity_statuses = $node->field_activity_status;
5 $activity_status_values = self::getMultivalueFieldArrayOfValues($activity_statuses);
6 $activity_status_values[0]['value'] = $activity_status;
7
8 $narrative_status = 'accepted';
9 $narrative_statuses = $node->field_narrative_status;
10 $narrative_status_values = self::getMultivalueFieldArrayOfValues($narrative_statuses\
11 );
Nodes and Fields 342

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();

22.17.2: Save multivalue field, entity reference field


When you really care which delta/index/offset, you can specify that offset. It’s a bit confusing how
exactly it works. For text or numeric fields, it works like you’d expect. You can just specify the
offset. For entity reference fields, you have to do some fiddling. Don’t use $node->set() as this
overwrites everything in the field, rather use the magic field setter variable and specify the offset.
Here $vote_number represents the index so if $vote_number = 0, this write the first item in the
multivalue field. If $vote_number = 1, then write the second item, and so on.

1 $citation_node->field_srp_voting_status[$vote_number] = 'incomplete';

Be cautions, you might think this would work but it doesn’t

1 $program_node->set('field_srp_team_ref', [$vote_number => 1234;

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

22.17.3: Update a multivalue entity reference fields


When writing multivalue entity reference fields, you have to load up the previous values, build an
array of target_ids (node ids) and then write them all in one pass with a $node->set() method.

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);

22.17.4: Generic Multivalue field writer


Here is a generic function that knows how to write values in a “sane” way.

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

1 public function multiTest() {


2
3 $str = '<h2>Results</h2>';
4
5 $node = Node::load(35);
6
7 // Write to index 0, 1, 2.
8 self::smartMultiValueFieldSetter($node, 'field_condiment', 'ketchup', 0);
9 self::smartMultiValueFieldSetter($node, 'field_condiment', 'mayo', 1);
10 self::smartMultiValueFieldSetter($node, 'field_condiment', 'mustard', 2);
11 //$node->save();
12
13 $field_name = 'field_condiment';
14 $field_type = $node->get($field_name)->getFieldDefinition()->getType();
15
16 $contents = $node->get($field_name)->getValue();
17 $str .= "<br/><strong>Field:</strong> " . $field_name;
18 $str .= ", type: " . $field_type;
19 $str .= "<br/><strong>Values: </strong>";
20 foreach ($contents as $item) {
21 $str .= $item['value'] . ', ';
22 }
23
24 self::smartMultiValueFieldSetter($node, 'field_condiment', 'mustard', 2, 'dummy'\
25 , TRUE);
26 $contents = $node->get($field_name)->getValue();
27 $str .= "<br/><strong>Values: </strong>";
28 foreach ($contents as $item) {
29 $str .= $item['value'] . ', ';
30 }
31
32 self::smartMultiValueFieldSetter($node, 'field_condiment', 'ketchup', 1, 'dummy'\
33 , TRUE);
34 $contents = $node->get($field_name)->getValue();
35 $str .= "<br/><strong>Values: </strong>";
36 foreach ($contents as $item) {
37 $str .= $item['value'] . ', ';
38 }
39
40 $field_name = 'field_event';
41 $field_type = $node->get($field_name)->getFieldDefinition()->getType();
42 $contents = $node->get($field_name)->getValue();
43 //kint($contents);
Nodes and Fields 347

44 $str .= "<br/><strong>Field:</strong> " . $field_name;


45 $str .= ", type: " . $field_type;
46 $str .= "<br/><strong>Values: </strong>";
47 foreach ($contents as $item) {
48 $str .= $item['target_id'] . ', ';
49 }
50
51 // 17, 18
52 //14, 18, 19
53 self::smartMultiValueFieldSetter($node, $field_name, 14, 0);
54 self::smartMultiValueFieldSetter($node, $field_name, 18, 1);
55 self::smartMultiValueFieldSetter($node, $field_name, 19, 2);
56 $contents = $node->get($field_name)->getValue();
57 $str .= "<br/><strong>Values: </strong>";
58 foreach ($contents as $item) {
59 $str .= $item['target_id'] . ', ';
60 }
61
62 $field_name = 'field_category';
63 $field_type = $node->get($field_name)->getFieldDefinition()->getType();
64 $contents = $node->get($field_name)->getValue();
65 //kint($contents);
66 $str .= "<br/><strong>Field:</strong> " . $field_name;
67 $str .= ", type: " . $field_type;
68 $str .= "<br/><strong>Values: </strong>";
69 foreach ($contents as $item) {
70 $str .= $item['target_id'] . ', ';
71 }
72
73 // 3, 2, 1
74 // 1, 2, 3.
75 self::smartMultiValueFieldSetter($node, $field_name, 1, 0);
76 self::smartMultiValueFieldSetter($node, $field_name, 2, 1);
77 self::smartMultiValueFieldSetter($node, $field_name, 3, 2);
78 $contents = $node->get($field_name)->getValue();
79 //kint($contents);
80 $str .= "<br/><strong>Values: </strong>";
81 foreach ($contents as $item) {
82 $str .= $item['target_id'] . ', ';
83 }
84
85 // Multivalue Text Field.
86 $field_name = 'field_condiment';
Nodes and Fields 348

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 }

22.18: Does this field exist in my entity?


To check if a field exists in a node.

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 }

22.19: Get URL for an image or file in a media


reference field
Nodes and Fields 350

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 }

You can get relative or absolute paths using one of these

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)

22.20: Retrieve info about a file field


Here we have a file field called field_materials_file that we loaded from a paragraph. Look-
ing in core/modules/file/src/Entity/File.php we can see a series of useful functions like
getFileName(), getFileUri(),getSize(), getCreatedTime(). To use these, we have to append
->entity after the field as shown below
Nodes and Fields 351

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().

22.21: Retrieve a link field


Here we have a link field: field_link which we load and get a valid uri from it using:

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;

22.22: Does this field exist in my entity?


Nodes and Fields 352

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 }

22.23: Create a node and write it to the database


1 use \Drupal\node\Entity\Node;
2 // Create node object.
3 $node = Node::create([
4 'type' => 'article',
5 'title' => 'Druplicon test',
6 ]);
7 $node->save();

22.24: Create a node with an image


Nodes and Fields 353

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();

22.25: Write a node with an attached file


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 $foo = new Foo([


2 'name' => 'bar',
3 'baz' => TRUE,
4 'multi_value' => [
5 'first',
6 'second',
7 'third',
8 ]
9 ]);

22.26: Write a date or datetime to a node


note the [] array stuff is optional

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

22.27: Or just a date (no time)


1 $dateTime = \DateTime::createFromFormat('Y-m-d','2000-01-30');
2 $newDateString = $dateTime->format('Y-m-d');
3 ...
4 'field_workshop_other_date' => [$newDateString,],

22.28: Set field values


Nodes and Fields 355

1 $node->set('field_tks_subchapter', $subchapterFullStatement);
2 $node->set('field_tks_standard_type', "TEKS");
3 $node->save();

And the shortened version (from https://gorannikolovski.com/blog/various-ways-updating-field-


values-drupal-8-and-9 )

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();

22.29: Set an entity reference field


1 //That is the node id.
2 $node->field_name->target_id = 123;
3
4 // You can assign an entity object.
5 $entity->set('field_name', $another_entity);
6
7 $node->field_tks_standard_type->value = “TEKS";
8 $node->save();

22.30: Set multivalue fields (regular and entity


reference)
Nodes and Fields 356

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

1 $expectation_node->set('field_name_multi',[0 => 'foo']);


2 $expectation_node->set('field_name_multi',[1 => 'bar']);

Or with variables...

1 $expectation_node->set('field_srp_teacher_voting_status',[$current_vote_number => $\
2 voting_status]);

22.31: Clear a text field


Single text field

1 $node->set('field_subtitle', NULL);

22.32: Set or clear a body field


1 $node->set('body', [
2 'summary' => $summary,
3 'value' => $body,
4 'format' => 'links_bullets_headings_and_images',
5 ]);

To empty a body field, use this:


Nodes and Fields 357

1 $this_node->body[$this_node->language] = [];

22.33: Load a node and retrieve an entity reference


node and nid (target_id)
1 $node_storage = \Drupal::entityTypeManager()->getStorage('node');
2 $node = $node_storage->load($nid);
3 //Same
4 $ref_nid = $node->get('field_sf_contract_ref')->target_id;
5 //or
6 $ref_nid = $node->field_sf_contract_ref->target_id;

22.33.1: Load a multivalue reference field.


Here node 35 has a field_event (multivalue entity reference field) with several values. This code
shows how to retrieve the first one, get it’s nid and it’s title:

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;

You can also loop thru the referenced entities with:

1 foreach ($node_array as $node) {


2 $nid = $node->id();
3 }

22.34: Entity reference nodes and their fields


You can look at the referenced node’s fields but you have to be aware of how you set the display
mode.
Nodes and Fields 358

1 foreach ($node->field_my_entity_reference as $reference) {


2
3 // if you chose "Entity ID" as the display mode for the entity reference field,
4 // the target_id is the ONLY value you will have access to
5 echo $reference->target_id; // 1 (a node's nid)
6
7 // if you chose "Rendered Entity" as the display mode, you'll be able to
8 // access the rest of the node's data.
9 echo $reference->entity->title->value; // "Moby Dick"
10 }

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;

22.35: Load the taxonomy terms from a term


reference field
The referencedEntities returns an array of term objects, so no need to load() them separately.

1 if ($node) {
2 $topics = $node->get('field_ref_tax')->referencedEntities();
3 foreach ($topics as $topic) {
4 $term_name = $topic->getName();
5 }
6 }

22.36: Load a node and find the terms referenced in a


paragraph in a term reference field
Here we loop thru all the instances of my paragraph reference and grab the term in the paragraph.
Nodes and Fields 359

1 foreach ($node->get('field_my_para')->referencedEntities() as $ent){


2 $term = $ent->$field_in_paragraph->entity;
3 $name = $term->getName();
4 print_r($name);
5 }

22.37: Retrieve a URL field

22.37.1: External links


You can get the URL (for external links) and then just the text part. Note this doesn’t work for
internal links. Note also this slightly convoluted example has a reference field field_sf_contract_ref
which has a link to another entity and the field_vendor_url->first()->getUrl() is the important
part. Also note, this is a single-value field (not a multivalue field–so the first() call is a little
disturbing)

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 }

A slightly simpler example from modules/custom/tea_teks/modules/tea_teks_-


srp/src/Form/SrpAddEditCitationForm.php

1 $citation_link = $citation->get('field_link');
2 if (!$citation_link->isEmpty()) {
3 $citation_link = $citation->field_link->first()->getUrl()->toString();
4 }

22.37.2: Internal links


For internal links, use getUrl()for the URL and ->title for the title.
Nodes and Fields 360

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 }

22.38: Load a node and retrieve a paragraph field


Because paragraphs and nodes are both entities, the pattern is the same. You load the entity (node
or paragraph) and then simply reference the field name e.g. myentity->field_blah
From /Users/selwyn/Sites/inside-mathematics/themes/custom/danaprime/danaprime.theme
These are a little different from regular fields. Generally you want to get their target_id which will
tell you the pid or paragraph id. Here are two different ways to load a video_collection_node and
go to retrieve a field field_related_lessons which holds paragraphs of type related_lessons

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;

22.39: How to get Node URL alias or Taxonomy Alias by


Node id or Term ID
Drupal 8 uses Url objects to easily generate and manipulate paths. The easiest way is to load the
entity and convert it to a URL object:

1 $node = Node::load(1234)
2 $url_object = $node->toUrl();

This now can be converted to a string

1 $href = $url_object->toString();

If an absolute URL is needed, that’s easy too:

1 $href = $url_object->setAbsolute()->toString();

This will return a path in the form node/123

1 $current_path = \Drupal::service('path.current')->getPath();

22.40: How to set a URL Alias


Since Drupal 9, url aliases are entities so:
From: /Users/selwyn/Sites/dir/web/modules/custom/dir/dir.module
Nodes and Fields 363

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();

22.41: Get a node’s menu item and more


Here we get the current route’s menu item using it’s nid, reset the link off the array and we can
extract the URL and other exciting things. Mostly you want to check its children, parents etc. Here
we grab it’s URL..

1 /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */


2 $menu_link_manager = \Drupal::service('plugin.manager.menu.link');
3 $links = $menu_link_manager->loadLinksByRoute('entity.node.canonical', ['node' => $n\
4 id]);
5 $link = reset($links);
6 $urlobject = $link->getUrlObject();
7 $url_string = $urlobject->toString();
8 $x = $link->getParent(); //get the parent menu item GUID.
9 $y = $link->getTitle(); // get the title of the menu item.
10
11 $menu_name = $link->getMenuName(); // get the menu name e.g. "main"

22.42: Find a node using it’s uuid


Nodes and Fields 364

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 }

22.43: Retrieve Node ID(NID) or Taxonomy term ID


from a Drupal alias or path
If alias-path does not exist, it will return the same argumented string.
The first example will work for the node, second for the taxonomy and third for the users. For
Drupal 8.8.0 and later use path_alias.manager

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 }

22.44: Retrieve all nodes with a matching taxonomy


term
1 $nodes = \Drupal::entityTypeManager()->getStorage('node')->loadByProperties([
2 'field_tags' => $term_id,
3 ]);
4
5 ## Load first 5 nodes that have a matching taxonomy term
6
7
8 ```php
9 protected function loadFirstOpinion($term_id) {
10 $storage = \Drupal::entityTypeManager()->getStorage('node');
11 $query = \Drupal::entityQuery('node')
12 ->condition('status', 1)
13 ->condition('type', 'opinion')
14 ->condition('field_category', $term_id, '=')
15 ->range(0, 5);
16 $nids = $query->execute();
17 $nodes = $storage->loadMultiple($nids);
18
19 $ra = [];
20 foreach ($nodes as $node) {
21 $ra[] = [
22 '#type' => 'markup',
23 '#markup' => '<p>' . $node->getTitle(),
24 ];
25 }
26 return $ra;
Nodes and Fields 366

22.45: 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();

You can use this statement in node_preprocess, controller, etc.


You will need a custom module to implement setting max-age to 0 like this. In a .module file:

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.

22.46: Get boolean Field


Boolean fields show up as 0 or 1 so a simple test using if $val will return true for yes/on and false
for no/off.

1 $val = $contract_node->field_erate_certification->value;
2 if ($val) {
3 $erate_certification = "Yes";
4 }

22.47: Date Field


Date fields in Drupal are stored in UTC date strings. When you load them, you have some options.
Probably grabbing the field->date is the best so you can use all the goodness of the DrupalDateTime
class. If you need to do calculations involving unix timestamps, then ->getTimestamp is useful
although DrupalDateTime can do all kinds of calculations too.
Nodes and Fields 367

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,

• value to store the date in UTC and


• date, a computed field returning a DrupalDateTime object, on which you can use the methods
getTimestamp() or format():

1 // get unix timestamp


2 $timestamp = $node->field_date->date->getTimestamp();
3 // get a formatted date
4 $date_formatted = $node->field_date->date->format('Y-m-d H:i:s');

22.48: Date Fields


See also Queries: “Query the creation date (among other things) using entityQuery”

These are stored in the Drupal database as varchar 20 strings.

1 $d = date("m/d/Y",$timestamp);

printing this date will return: 08/16/2018


From https://www.drupal.org/node/1834108
There is a DrupalDateTime object which is used like this:
Nodes and Fields 368

1 $date = DrupalDateTime::createFromDateTime($datetime);
2 $date = DrupalDateTime::createFromArray($array);
3 $date = DrupalDateTime::createFromTimestamp($timestamp);
4 $date = DrupalDateTime::createFromFormat($format, $time);

e.g. to create a DrupalDateTime

1 $format = 'Y-m-d H:i';


2 $start_date = DrupalDateTime::createFromFormat($format, “2019-01-01 00:00");

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");

22.49: Date Range Field doesn’t display correct


timezone
If you use a date range field in your content type and try to display the time part in a template,
Drupal will cleverly display the UTC version (not your current timezone version) so the time will
be off by potentially ~6 hours.
Using the field field_event_date, here is the twig template code to display the date. You’d expect it
to show the correct timezone
Nodes and Fields 369

1 {% raw %}{{ node.field_event_date.0.value|date('g:ia') }} - {{ node.field_event_date\


2 .0.end_value|date('g:ia') }}{% endraw %}

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:

1 {% raw %}{{ node.field_date_start.value|date("g:ia") }}{% endraw %}

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

25 // $end_time = $formatter->format($date_end, 'custom', 'g:ia', $timezone);


26
27 $variables['start_time'] = $start_time;
28 $variables['end_time'] = $end_time;
29 }

and then in the template, use


{{ start_time }} - {{ end_time }}

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 if ( $node_type == 'event' && $view_mode == 'default' ) {


2 $timezone = drupal_get_user_timezone();
3 $formatter = \Drupal::service('date.formatter');
4
5 $d = $node->get('field_smart_event_date')->getValue();
6 $d0s = $d[0]["value"]; //first date start time
7 $d0e = $d[0]["end_value"]; //first date end time
8 $start_time = $formatter->format($d0s, 'custom', 'g:ia');
9 $end_time = $formatter->format($d0e, 'custom', 'g:ia', $timezone);
10 $variables['start_time'] = $start_time;
11 $variables['end_time'] = $end_time;
12 }

22.50: Date Range


1 // formatted start date
2 $start_date_formatted = $node->field_date->start_date->format('Y-m-d H:i:s');
3 // formatted end date
4 $end_date_formatted = $node->field_date->end_date->format('Y-m-d H:i:s');

22.51: Date Range fields: Load start and end values


Start date and end date.

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

22.52: Date Fields: Load or save them


Because date fields in Drupal are stored in strings, when you use get() or set(), they are still strings.

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

How about with timezones?

1 $date = new DrupalDateTime();


2 $date->setTimezone(new \DateTimeZone('America/Chicago'));
3 print $date->format('m/d/Y g:i a');
4 // The above prints current time for given Timezone
5 // prints : 07/16/2019 10:59 am
6
7 // Another variations of the above except it takes specific date and UTC zone
8 $date = new DrupalDateTime('2019-07-31 11:30:00', 'UTC');
9 $date->setTimezone(new \DateTimeZone('America/Chicago'));
10 print $date->format('m/d/Y g:i a');
11 // prints 07/31/2019 6:30 am
Nodes and Fields 372

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);

22.53: Comparing DrupalDateTime values


DrupalDateTimes are derived from DateTimePlus which are wrappers for PHP DateTime class. So
you can use that functionality to do comparisons

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)

22.54: Date with embedded timezone


Here you have a date string with an embedded timezone so you can make it into a DrupalDateTime
and then format it into a usable string you can store in the db.
Nodes and Fields 373

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");

22.55: Has something expired?


Checking to see if the expiration_date has passed. The field_expiration_date is a standard Drupal
date field in the sf_resellers content type.

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 }

TODO: date fields are stored in UTC. Need to factor in timezone.

22.56: Load or save Drupal Date fields


Date fields in Drupal are stored in strings
If you want to load them, just get(), as below and to save them, use set.

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");

22.57: Retrieve node creation date and format it


Here I query the database for a node and return a string version of the date. It probably makes more
sense to return the date and do the formatting at the theme level. Note. This will handle epoch dates
before 1970.

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 }

22.58: Retrieve node creation or changed date and


format it
Both created and changed are stored as Unix epoch timestamps the node_field_data table. Here I
query the database for a node and return a string version of the date. Note. This will handle epoch
dates before 1970.

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

22.59: Date Field and Date with no time (remove time)


Use the setTime() function to remove the time part of a datetime so we can make comparisons of
just the date.

1 function ogg_mods_cn_form_validate($form, FormStateInterface $form_state) {


2 $start_date = $form_state->getValue('field_cn_start_date');
3 if ($start_date) {
4 $start_date = $start_date[0]['value'];
5 $start_date->setTime(0, 0, 0);
6 $now = new Drupal\Core\Datetime\DrupalDateTime();
7 $now->modify("-2 days");
8 $now->setTime(0, 0, 0);
9
10 \Drupal::messenger()->addMessage("Start date = $start_date");
11 \Drupal::messenger()->addMessage("Now date - 2 days = $now");
12
13 if ($start_date < $now) {
14 $form_state->setErrorByName('edit-field-cn-start-date-0-value-date', t('The \
15 starting date is more than 2 days in the past. Please select a later date'));
16 }
17 }
18 }

22.60: Smart date (smart_date) load and format


Load the smart date field and use the drupal date formatting service. Smart date fields are always
stored as unix timestamp values e.g. 1608566400 which need conversion for human consumption.

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

12 $start_date = $dt->format('m/d/y'); //returns 12/21/20


13 $start_time = $dt->format('g:ia'); // returns 10:00am

22.61: Smart date (smart_date) all-day


To check if a smart date is set to all day, check the duration. If it is 1439, that means all day.

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 }

22.62: Smart date (smart_date) range of values


1 //Event start date.
2 $whens = $node->get('field_when'); //produces SmartDateFieldItemList
3
4 // Each $when is a \Drupal\smart_date\Plugin\Field\FieldType\SmartDateItem
5 foreach ($whens as $when) {
6 $start = $when->value;
7 $end = $when->end_value;
8 $duration = $when->duration; //1439 = all day
9 $tz = $when->timezone; //"" means default. Uses America/Chicago type format.
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 }

22.64: Disable caching for a content type


If someone tries to view a node of content type search_home (entity of bundle search_home) caching
is disabled and Drupal and the browser will always re-render the page. This is necessary for a page
that is retrieving data from a third party source and you almost always expect it to be different. It
wouldn’t work for a search page to show results from a previous search.

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 }

In the API docs ( https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%


21entity.api.php/function/hook_ENTITY_TYPE_view_alter/9.2.x ) They have an example
Nodes and Fields 380

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 function hook_ENTITY_TYPE_view_alter(array &$build, Drupal\Core\Entity\EntityInterfa\


2 ce $entity, \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display) {
3 if ($build['#view_mode'] == 'full' && isset($build['an_additional_field'])) {
4
5 // Change its weight.
6 $build['an_additional_field']['#weight'] = -10;
7
8 // Add a #post_render callback to act on the rendered HTML of the entity.
9 $build['#post_render'][] = 'my_module_node_post_render';
10 }
11 }

22.65: Writing some JSON data into a long text field


It is sometimes useful to store JSON data into a long text field. I’ve used it for storing formatted
status information about a complex process.

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.66: Create a node with an image


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();

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.

22.68: Load a node and find the terms referenced in a


paragraph in a term reference field
Here we loop thru all the instances of my paragraph reference and grab the term in the paragraph.

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 }

22.69: Custom Field Formatter


Custom field formatters can be used in display modes or in views. These are very powerful.
Some basic info is available at https://www.drupal.org/docs/8/creating-custom-modules/create-a-
custom-field-formatter
This example is a custom formatter that takes a value from a field (in this case a uuid) and builds a
url which essentially retrieves an image (via an API call.) It looks for some config info (in the node
display mode for the node, or in the views setup for the usage in a view.).
For the node called infofeed, the config data is stored in an entity called core.entity_view_dis-
play.node.infofeed.default
For a view called infofeeds, the config data is stored in a config entity called views.view.infofeeds.
(You can find them by browsing thru the config table and looking for your info in the data field i.e.
in sequel pro, look for data like %image_width% )
It is pretty reasonable that the custom field formatter will require some configuration, so this means
we will need a module/config/schema/module.schema.yml file
So at /Users/selwyn/Sites/ncs/docroot/modules/custom/ncs_infoconnect/config/schema/ncs_info-
connect.schema.yml we have the following file which defines a config_entity called NCS Thumbnail
settings and specifically two integer values for image_width and image height. I use these to specify
the size of the thumbnail I generate:
Nodes and Fields 383

1 # Schema for configuring NCS thumbnail formatter.


2
3 field.formatter.settings.ncs_thumbnail:
4 type: config_entity
5 label: 'NCS thumbnail settings'
6 mapping:
7 image_width:
8 label: 'Image width'
9 type: integer
10 image_height:
11 label: 'Image Height'
12 type: integer

I create the fieldformatter as a fairly unexciting plugin at /Users/selwyn/Sites/ncs/docroot/mod-


ules/custom/ncs_infoconnect/src/Plugin/Field/FieldFormatter/NcsThumbnailFormatter.php
The annotation shows what will be seen in Drupal when configuring the formatter.

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

22.70.1: What can I do with a call to first() on an entity reference


field?
After loading a node, I want to see the value in an entity reference field. I can call referencedEntities
to pull out it’s values and loop thru them–I get Nodes in that instance.

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?

1 $refs = $node->get('field_sf_dir_contact_ref')->first(); //returns an EntityReferen\


2 ceItem.

22.71: Great Cheat sheets


Various ways of updating field values in Drupal 8 and 9
https://gorannikolovski.com/blog/various-ways-updating-field-values-drupal-8-and-9
Entity query cheat sheet:
https://www.metaltoad.com/blog/drupal-8-entity-api-cheat-sheet
Drupal entity API cheat sheet
https://drupalsun.com/zhilevan/2018/07/21/drupal-entity-api-cheat-sheet
23: Getting off the Island (formerly
Reaching out of Drupal)
23.1: Overview
To communicate with external websites or web services we can make web requests via the
Drupal::httpClient1 class. This is a wrapper for the Guzzle HTTP Client2 .
From https://www.php-fig.org/psr/psr-7/ HTTP messages are the foundation of web development.
Web browsers and HTTP clients such as cURL create HTTP request messages that are sent to a
web server, which provides an HTTP response message. Server-side code receives an HTTP request
message, and returns an HTTP response message.
HTTP messages are typically abstracted from the end-user consumer, but as developers, we typically
need to know how they are structured and how to access or manipulate them in order to perform our
tasks, whether that might be making a request to an HTTP API, or handling an incoming request.
Guzzle utilizes PSR-73 as the HTTP message interface. PSR-74 describes common interfaces for
representing HTTP messages. This allows Guzzle to work with any other library that utilizes PSR-7
message interfaces.

23.2: Guzzle example


This is an example which retrieves data from within a controller using a GET:

1 public function example1() {


2
3 //Initialize client;
4 $client = \Drupal::httpClient();
5 $uri = 'https://demo.ckan.org/api/3/action/package_list';
6
7 // Returns a GuzzleHttp\Psr7\Response.
8 $response = $client->request('GET', 'https://demo.ckan.org/api/3/action/package_\
9 list');
10 // Or using the magic method.
1 https://api.drupal.org/Drupal::httpClient
2 https://github.com/guzzle/guzzle
3 http://www.php-fig.org/psr/psr-7/
4 http://www.php-fig.org/psr/psr-7/
Getting off the Island (formerly Reaching out of Drupal) 387

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 }

23.3: Guzzle POST example


1 //Initialize client;
2 $client = \Drupal::httpClient();
3 $uri = 'http://demo.ckan.org/api/3/action/group_list';
4 $request = $client->post($uri, [
5 'json' => [
6 'id'=> 'data-explorer'
7 ]
8 ]);
9 $stream = $request->getBody();
10 $json_data = Json::decode($stream);
11 $help = $json_data['help'];
12 $success = $json_data['success'];
13 $result = $json_data['result'][0] . ' and ' . $json_data['result'][1];

Guzzle takes care of adding a ’Content-Type’,’application/json’ header, as well as json_encoding the


’json’ array.
Getting off the Island (formerly Reaching out of Drupal) 388

23.4: Magic methods to send synchronous requests


1 $response = $client->get('http://httpbin.org/get');
2 $response = $client->delete('http://httpbin.org/delete');
3 $response = $client->head('http://httpbin.org/get');
4 $response = $client->options('http://httpbin.org/get');
5 $response = $client->patch('http://httpbin.org/patch');
6 $response = $client->post('http://httpbin.org/post');
7 $response = $client->put('http://httpbin.org/put');

From https://docs.guzzlephp.org/en/latest/quickstart.html ending-requests5

23.5: HTTP basic authentication


This shows a failed attempt to authenticate with Github’s API with exception handling. It will log
the error to Drupal’s watchdog and display it on screen:

1 public function example2() {


2
3 $msg = "";
4 $client = \Drupal::httpClient();
5 $uri = 'https://api.github.com/user';
6
7 try {
8 $request = $client->get($uri, [
9 'auth' => ['username', 'password']
10 ]);
11 $response = $request->getBody();
12 $msg .= "<br><strong>GET</strong>";
13 $msg .= "<br>URI: " . $uri;
14 }
15
16 catch (ClientException $e) {
17 \Drupal::messenger()->addError($e->getMessage());
18 watchdog_exception('guzzle_examples', $e);
19 }
20
21 catch (\Exception $e) {
22 \Drupal::messenger()->addError($e->getMessage());
23 watchdog_exception('guzzle_examples', $e);
5 https://docs.guzzlephp.org/en/latest/quickstart.html#sending-requests
Getting off the Island (formerly Reaching out of Drupal) 389

24 }
25
26 $build['content'] = [
27 '#type' => 'item',
28 '#markup' => $this->t($msg),
29 ];
30
31 return $build;
32 }

See https://docs.guzzlephp.org/en/latest/request-options.html#auth for more.

23.6: Exception handling


When using Drupal::httpClient, you should always wrap your requests in a try/catch block, to
handle any exceptions. Here is an example of logging Drupal::httpClient request exceptions via
watchdog_exception. This example will fail with a 401 error and display it on screen.

1 public function example2() {


2
3 $msg = "";
4 $client = \Drupal::httpClient();
5 $uri = 'https://api.github.com/user';
6
7 try {
8 $request = $client->get($uri, [
9 'auth' => ['username', 'password']
10 ]);
11 $response = $request->getBody();
12 $msg .= "<br><strong>GET</strong>";
13 $msg .= "<br>URI: " . $uri;
14 }
15
16 catch (ClientException $e) {
17 \Drupal::messenger()->addError($e->getMessage());
18 watchdog_exception('guzzle_examples', $e);
19 }
20
21 catch (\Exception $e) {
22 \Drupal::messenger()->addError($e->getMessage());
23 watchdog_exception('guzzle_examples', $e);
24 }
Getting off the Island (formerly Reaching out of Drupal) 390

25
26 $build['content'] = [
27 '#type' => 'item',
28 '#markup' => $this->t($msg),
29 ];
30
31 return $build;
32 }

23.7: Guzzle Exceptions


You can get a full list of Exception types simply by listing the contents of the directory: \<drupal_-
root\>/vendor/guzzlehttp/guzzle/src/Exception. Utilizing this list allows you to provide differ-
ent behavior based on exception type.
At the time of writing, the contents of that directory is:

• 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

23.8: HTTP response status codes


From https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
HTTP response status codes indicate whether a specific HTTP6 request has been successfully
completed. Responses are grouped in five classes:

1. Informational responses7 (100 — 199)


2. Successful responses8 (200 — 299)
3. Redirection messages9 (300 — 399)
6 https://developer.mozilla.org/en-US/docs/Web/HTTP
7 https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#information_responses
8 https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#successful_responses
9 https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages
Getting off the Island (formerly Reaching out of Drupal) 391

4. Client error responses10 (400 — 499)


5. Server error responses11 (500 — 599)

23.9: Reading from an API


In this example, a class was created which extends SqlBase ( docroot/core/modules/migrate/sr-
c/Plugin/migrate/source/SqlBase php). The code below is from the prepareRow() function which
retrieves a row of data from the data source. In this case, rather than a SQL database, it is retrieved
from an API. It uses basic http authentication during the GET call and if there are any errors, it
updates a status elsewhere with a call to setUpdateStatus(). The following code is not included
below, but it may be interesting to know what it does. If the GET succeeds, the data is parsed out and
put into variables to be returned to the called. This acts just like prepareRow() does when retrieving
a row from a SQL source. Taxonomy terms are looked up and added if they don’t already exist (so
taxonomy term id’s can be returned) and the status is updated showing this row was successfully
retrieved.

1 $nard_auth_settings = Settings::get('nard_api_auth', []);


2 $uri = $nard_auth_settings['default']['server'] . ':' . $nard_auth_settings['default\
3 ']['port'];
4 $uri .= '/sourcecontent/search';
5 $client = \Drupal::httpClient();
6
7 switch (strtolower($row->getSourceProperty('type'))) {
8 case 'article':
9 $uuid = $row->getSourceProperty('uuid');
10 $id = $row->getSourceProperty('id');
11
12 // Retrieve the article from the API.
13 try {
14 $response = $client->request('GET', $uri, [
15 'auth' => [
16 $nard_auth_settings['default']['username'],
17 $nard_auth_settings['default']['password']
18 ],
19 'query' => [
20 'uuid' => $row->getSourceProperty('uuid'),
21 'properties' => 'Byline,updated,created,uuid',
22 ],
23 'timeout' => 3,
10 https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses
11 https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#server_error_responses
Getting off the Island (formerly Reaching out of Drupal) 392

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 }

Curl Request using Drupal httpClient


From the now defunct link: http://btobac.com/blog/how-do-curl-request-using-drupal-httpclient-
drupal-8
Here the author has an example of a function which takes a few parameters and can execute a POST,
PUT or GET. There is no security code which you almost always need, but there is exception handling
and error logging to Drupal watchdog.
Drupal HTTP client for curl HTTP request like POST, PUT, GET Method even for DELETE, you can add
one type in the below switch case in the class method
Getting off the Island (formerly Reaching out of Drupal) 393

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

44 tStatusCode().", ". $result_message ."`</strong> response \" in <i>".__METHOD__."()


45 (line ".__LINE__." of ".__FILE__."</i>).");
46 return NULL;
47 }
48 return $result;
49 } else {
50 \Drupal::messenger()->addError($type.' exception contact administrator');
51 \Drupal::logger('server_exception')->error(" \"Server error: <i>`<strong>".$\
52 method."</strong> ".$url."`</i> resulted in a <strong>`".$response->getStatusCode().
53 " ".$response->getReasonPhrase()."`</strong> response \" in <i>".__METHOD__."() (lin
54 e ".__LINE__." of ".__FILE__."</i>).");
55 }
56 }
57 catch (\GuzzleHttp\Exception\ConnectException $e) {
58 \Drupal::messenger()->addError('Cannot contact '.$type);
59 \Drupal::logger('connect_exception')->error(" \"Connection error: <i>`<strong>\
60 ".$method."</strong> ".$url."`</i> resulted in a <strong>`".$e->getMessage()."`</str
61 ong> response \" in <i>".__METHOD__."() (line ".__LINE__." of ".__FILE__."</i>).");
62 }
63 return NULL;
64 }
65
66 }
67
68 $cl = new DrupalHTTPClient();
69
70 $url = "http://api.adadasdadadasd/adasd/ad/ad";
71 $content = ["key" => "value"];
72 $data = $cl->initRequest($url,[],$content, "POST", TRUE, "anytime just flag");

23.10: Download a file using guzzle


Details are borrowed from https://gist.github.com/edutrul/9d04d7742545dbedd1a36f7b17632b7a
Getting off the Island (formerly Reaching out of Drupal) 395

1 public function example3() {


2
3 $msg = "";
4 $client = \Drupal::httpClient();
5 $uri = 'https://api.github.com/user';
6
7 try {
8
9 $source_uri = 'https://www.austinprogressivecalendar.com/sites/default/files/sty\
10 les/medium/public/inserted-images/2018-04-02_5.jpg';
11 // Note sites/default/files/abc directory must exist for this to succeed.
12 $destination_uri = 'sites/default/files/abc/test.png';
13 /** @var \GuzzleHttp\Psr7\Response $response */
14 $response = $client->get($source_uri, ['sink' => $destination_uri]);
15 // file gets downloaded to /sites/default/files/abc/test.png
16
17 $msg .= "<br><strong>Retrieve File via Guzzle</strong>";
18 $msg .= "<br>Source: " . $source_uri;
19 $msg .= "<br>Dest: " . $destination_uri;
20 }
21 catch (ClientException $e) {
22 \Drupal::messenger()->addError($e->getMessage());
23 watchdog_exception('guzzle_examples', $e);
24 }
25
26 catch (\Exception $e) {
27 \Drupal::messenger()->addError($e->getMessage());
28 watchdog_exception('guzzle_examples', $e);
29 }
30
31 $build['content'] = [
32 '#type' => 'item',
33 '#markup' => $this->t($msg),
34 ];
35
36 return $build;
37 }

23.11: Download a file using curl in PHP


For comparison, I’ve included an example of how to download a file using curl.
Getting off the Island (formerly Reaching out of Drupal) 396

From Stackoverflow: How to download a file using curl in php?12

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

41 if(!preg_match('/filename="(.*?)"/', $header, $matches)){


42 // If filename not found do something...
43 echo "Unable to find filename.<br>Please check the Response Headers or Header pars\
44 ing!";
45 exit();
46 } else {
47 // If filename was found assign the name to the variable above
48 $file_name = $matches[1];
49 }
50 // Check header response, if HTTP response is not 200, then display the error.
51 if(!preg_match('/200/', $header_items[0])){
52 echo '<pre>'.print_r($header_items[0], true).'</pre>';
53 exit();
54 } else {
55 // Check header response, if HTTP response is 200, then proceed further.
56
57 // Set the header for PHP to tell it, we would like to download a file
58 header('Content-Description: File Transfer');
59 header('Content-Type: application/octet-stream');
60 header('Content-Transfer-Encoding: binary');
61 header('Expires: 0');
62 header('Cache-Control: must-revalidate');
63 header('Pragma: public');
64 header('Content-Disposition: attachment; filename='.$file_name);
65
66 // Echo out the file, which then should trigger the download
67 echo $file;
68 exit;
69 }

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

24.1.1: Find matching nodes - example 1


In this EntityQuery example we search for nodes of content type (bundle) ws_product and match
field_product_sku with the $sku variable.
use Drupal\node\Entity\Node;

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 }

24.1.2: Find matching nodes - example 2


In this entityQuery we search for published nodes of type contract with field_contract_status having
the value “Active”. This puts the resulting nids and node titles in a render array for display.
This is a simple query which outputs a bunch of nids and titles
Queries 400

1 public function loadRawSalesforceData() {


2 $node_storage = \Drupal::entityTypeManager()->getStorage('node');
3 $query = \Drupal::entityQuery('node')
4 ->condition('type', 'contract')
5 ->condition('status', 1)
6 ->condition('field_contract_status', 'Active')
7 ->sort('title', 'DESC');
8
9 $nids = $query->execute();
10 if ($nids) {
11 $nodes = $node_storage->loadMultiple($nids);
12 foreach ($nodes as $node) {
13 $nid = $node->id();
14 $titles[] = [
15 '#type' => 'markup',
16 '#markup' => "<p>" . "nid=$nid " . "Title=" . $node->getTitle(). "</p>",
17 ];
18 }
19 return $titles;
20 }
21 return [
22 '#markup' => $this->t('nothing, nada, not a sausage'),
23 ];
24 }

24.1.3: Find matching article nodes–example 3


This example looks for an entity of type ‘article’ with the name $name

1 public function entityExists() {


2
3 $name = 'hello';
4 // See if the article named hello exists.
5 $query = \Drupal::entityQuery('node')
6 ->condition('type', 'article')
7 ->condition('title', $name)
8 ->count();
9
10 $count_nodes = $query->execute();
11
12 if ($count_nodes == 0) {
13 $str = "Found no articles";
Queries 401

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 }

24.1.4: Find nodes that match a taxonomy term


Find all nodes that match a term_id and retrieve the first 5 nodes sorted by title. This code also puts
them into a render array for display.

1 protected function loadFirstOpinion($term_id) {


2 $storage = \Drupal::entityTypeManager()->getStorage('node');
3 $query = \Drupal::entityQuery('node')
4 ->condition('status', 1)
5 ->condition('type', 'opinion')
6 ->condition('field_category', $term_id, '=')
7 ->sort('title', 'ASC') //or DESC
8 ->range(0, 5);
9 $nids = $query->execute();
10 $nodes = $storage->loadMultiple($nids);
11
12 $render_array = [];
13 foreach ($nodes as $node) {
14 $render_array[] = [
15 '#type' => 'markup',
16 '#markup' => '<p>' . $node->getTitle(),
17 ];
18 }
19 return $render_array;

24.1.5: Find 5 nodes that have a matching taxonomy term


We look for published nodes of node type opinion that have a term in the category field, sorted by
title ascending, starting with the first result and giving us 5 results. The resulting titles are put into
a render array.
Queries 402

1 protected function loadFirstOpinion($term_id) {


2 $storage = \Drupal::entityTypeManager()->getStorage('node');
3 $query = \Drupal::entityQuery('node')
4 ->condition('status', 1)
5 ->condition('type', 'opinion')
6 ->condition('field_category', $term_id, '=')
7 ->sort('title', 'ASC') //or DESC
8 ->range(0, 5);
9 $nids = $query->execute();
10 $nodes = $storage->loadMultiple($nids);
11
12 $render_array = [];
13 foreach ($nodes as $node) {
14 $render_array[] = [
15 '#type' => 'markup',
16 '#markup' => '<p>' . $node->getTitle(),
17 ];
18 }
19 return $render_array;

24.1.6: Find matching nodes and delete them


1 public function deleteQuery1() {
2 $results = \Drupal::entityQuery('node')
3 ->condition('type', 'event')
4 ->range(0, 10)
5 ->execute();
6
7 if ($results) {
8 foreach ($results as $result) {
9 $node = Node::load($result);
10 $node->delete();
11 }
12 }
13 $render_array['content'] = [
14 '#type' => 'item',
15 '#markup' => t("10 nodes deleted."),
16 ];
17
18 return $render_array;
19 }
Queries 403

24.1.7: Slice up entityQuery results into batches of 100 nodes


This is often used for batch API operations.

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 }

24.1.8: Query the creation date (among other things) using


entityQuery
Note. The created (and changed) field uses a unix timestamp. This is an int 11 field in the db with a
value like 1525302749 If you add a Drupal date field, its data looks like 2019-05-15T21:32:00 (varchar
20)
If you want to query a date field in a content type, you will have to fiddle around with the
setTimezone stuff that is commented out below. The date field referenced below (field_date) is
a standard Drupal date field.
More at 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

1 protected function loadOpinionForAYear($year, $term_id) {


2 $storage = \Drupal::entityTypeManager()->getStorage('node');
3
4 // Get a date string suitable for use with entity query.
5 // $date = new DrupalDateTime(); // now
6 $format = 'Y-m-d H:i';
7 $start_date = DrupalDateTime::createFromFormat($format, $year . "-01-01 00:00");
8 $end_date = DrupalDateTime::createFromFormat($format, $year . "-12-31 23:59");
9
10 $start_date = $start_date->getTimestamp();
Queries 404

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 }

24.1.9: entityQuery frequently used conditions


• Published: ->condition('status', 1)
• Text field not empty: ->condition('field_source_url', '', '<>')
• Field value > 14: ->condition('field_some_field', 14, '>')
• Reference field empty: ->notExists('field_sf_account_ref');
• Null: ->condition(\$field,NULL, \'IS NULL\');
• Not Null: ->condition(\$field,NULL, \'IS NOT NULL\');
Queries 405

Lots more at https://www.drupal.org/docs/8/api/database-api/dynamic-queries/conditions

24.1.10: Update menu items programatically


To update several items in a menu use hook_update.

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 }

24.1.11: Query multi-value fields


When querying multivalue fields, you need to use %delta to specify the position (or delta) for the
value you are looking for. You also have to identify to the query which position (or delta) you want
to query. In the example below, we specify field_srp_voting_status.%delta as 1 - indicating the
second position (0 based always) and field_srp_voting_status.%delta.value for the actual value
we are looking for (either accepted, rejected or incomplete):
Queries 406

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;

24.1.12: Query entity reference fields if they have a value or no


value
To check if there is a value in an entity reference fields, use the following code

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 }

24.1.13: Query entity reference fields


In the following query, we check for a value in the entity that is referenced in the entity reference
field? For example, if you have an entity reference field which references node (entity) 27. This
query can look in node 27 and check a field value in that node. Here we check in field_first_name
for the the value Fred:
Queries 407

1 ->condition('field_tks_pub_expectation.entity.field_first_name', 'Fred', '=')

For querying for a user id, we query the field_voter.entity:user.uid value. See code below:

1 protected function loadErrorFeedbackVotingRecordNode(int $user_id, int $error_feedba\


2 ck_nid, int $vote_number) {
3 $node = [];
4 $query = \Drupal::entityQuery('node')
5 ->condition('type', 'srp_voting_record')
6 ->condition('field_voter.entity:user.uid', $user_id)
7 ->condition('field_ref_error_feedback', $error_feedback_nid)
8 ->condition('field_srp_vote_number', $vote_number)
9 ->accessCheck(FALSE);
10 $nids = $query->execute();
11 if (!empty($nids)) {
12 $nid = reset($nids);
13 $node = Node::load($nid);
14 }
15 return $node;
16 }

24.2: Static and dynamic Queries


Sometimes you will use static or dynamic queries rather than entityQueries. These use actual SQL
versus the entityQuery approach where you build the various parts of the query.

24.2.1: Static Queries


See https://www.drupal.org/docs/drupal-apis/database-api/static-queries

24.2.2: Get a connection object


There are two ways to get a connection object:
Queries 408

1 /** @var \Drupal\Core\Database\Connection $connection */


2 $connection = Database::getConnection();
3
4 //OR
5
6 /** @var \Drupal\Core\Database\Connection $connection */
7 $connection = \Drupal::service('database');

24.2.3: SQL select example


Static query example from a controller. This loads some fields from the donors table and returns a
render array with a count of how many results it found.

1 public function queryBuild1() {


2 $database = \Drupal::database();
3 $query = $database->query("SELECT id, name, amount FROM {donors}");
4 $results = $query->fetchAll();
5
6 $result_count = count($results);
7
8 $str = "Results from db query";
9 $str .= "<br/> Result count = $result_count";
10
11 $render_array['content'] = [
12 '#type' => 'item',
13 '#markup' => $str,
14 ];
15
16 return $render_array;
17 }

24.2.4: Find the biggest value in a field


We make a quick query and retrieve the result. In this case we are finding the highest value for the
id column.
Queries 409

1 public function highestId() {


2 $database = \Drupal::database();
3 $connection = Database::getConnection();
4
5 $query = $connection->select('donors', 'n');
6 $query->addExpression('MAX(id)', 'id');
7 $result = $query->execute();
8 $highest_id = intval($result->fetchField());
9
10 $str = "Highest id = $highest_id";
11 $render_array['content'] = [
12 '#type' => 'item',
13 '#markup' => $str,
14 ];
15
16 return $render_array;
17 }

24.2.5: SQL update query - example 1


This shows how to update a status field to the new value $status when the uuid matches, the event
is either update or add and the status is new.

1 public function setUpdateStatus(string $uuid, string $status) {


2 $db_connection = \Drupal::database();
3 $result = $db_connection->update('nocs_info')
4 ->fields(['status' => $status])
5 ->condition('uuid', $uuid)
6 ->condition('event', ['UPDATE', 'ADD'], 'IN')
7 ->condition('status', 'new')
8 ->execute();
9
10 return $result;
11 }

24.2.6: SQL update query - example 2


Queries 410

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 }

24.2.7: SQL update query - example 3


This will update values in a table and return the number of rows updated.

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

Note. This will be deprecated in Drupal 11. See https://api.drupal.org/api/drupal/core%21lib%


21Drupal%21Core%21Database%21Statement.php/function/Statement%3A%3ArowCount/9.3.x
and
https://git.drupalcode.org/project/drupal/-/blob/9.5.x/core/lib/Drupal/Core/Database/Connection.
php#L968

24.2.8: SQL insert


From https://www.drupal.org/docs/drupal-apis/database-api/insert-queries
Which to use? $connection->insert() or $connection->query() or what are the difference between
insert() and query()?

• 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.

24.2.9: SQL Insert Query


1 /**
2 * @throws \Exception
3 */
4 public function insert() {
5
6 /** @var \Drupal\Core\Database\Connection $connection */
7 $connection = \Drupal::service('database');
8
9 // $query = $connection->insert('donors', $options);
10
11 // single insert.
12 $result = $connection->insert('donors')
13 ->fields([
14 'name' => 'Singleton',
15 'amount' => 1,
Queries 412

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

24.2.10: SQL Delete query


This will return the number of rows affected by the SQL delete query.
Queries 413

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 }

Note. This will be deprecated in Drupal 11. See https://api.drupal.org/api/drupal/core%21lib%


21Drupal%21Core%21Database%21Statement.php/function/Statement%3A%3ArowCount/9.3.x also
https://git.drupalcode.org/project/drupal/-/blob/9.5.x/core/lib/Drupal/Core/Database/Connection.
php#L968.

24.2.11: Paragraph query


In the txg.theme file this code digs into a table for a paragraph field and grabbing the delta field
value.

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 }

24.2.12: Create a custom table for your module


If you need a custom database table (or two) for use in a custom module, you can use hook_schema
in your module.install file. This will cause the table(s) to be created at module install time and
removed at module uninstall time.

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

30 'type' => 'int',


31 'not null' => TRUE,
32 'description' => 'Content creation date.',
33 ],
34 'updated' => [
35 'type' => 'int',
36 'not null' => TRUE,
37 'description' => 'Content update date.',
38 ],
39 'type' => [
40 'type' => 'varchar',
41 'length' => 255,
42 'not null' => TRUE,
43 'description' => 'Type of content created or updated.',
44 ],
45 'version' => [
46 'type' => 'int',
47 'not null' => TRUE,
48 'description' => 'Content version number.',
49 ],
50 'status' => [
51 'type' => 'varchar',
52 'length' => 255,
53 'not null' => TRUE,
54 'default' => 'NEW',
55 'description' => 'Status of event log row - NEW, PROCESSING, COMPLETE, or ER\
56 ROR.',
57 ],
58 ],
59 'primary key' => ['id'],
60 'indexes' => [
61 'uuid' => ['uuid'],
62 'event' => ['event'],
63 'type' => ['type'],
64 ],
65 ];
66
67 return $schema;
68 }
Queries 416

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());

25.2: Redirect in a form


In a form, you can redirect to a route by its name:

1 $form_state->setRedirect('entity.bike_part.canonical', ['bike_part' => $entity->id()\


2 ]);

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

1 $url = Url::fromRoute('user_account.user_register', [], ['query' => ['destination' =\


2 > $shippingUrl]]);
3 $form_state->setRedirectUrl($url);

25.3: Redirect off-site (to a third-party URL)


From https://drupal.stackexchange.com/questions/136641/how-do-i-redirect-to-an-external-url
Redirect to a third party website with Redirect permanent status. The URL must have absolute path
like http://www.google.com

1 return new RedirectResponse('https://google.com');

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);

25.4: Redirect to an existing route with an anchor (or


fragment)
You can specify the route using the route name from your module.routing.yml file.
Note. You can also redirect to a specific id (anchor) on the page by adding the fragment parameter

1 $form_state->setRedirect('tra_teks_admin.timeline_detail',
2 ['node'=>$parent_nid],
3 ['fragment' => 'milestone-' . $nid,]
4 );

25.5: Redirect to a complex route


Here the route requires four arguments so they all need to be passed in.
Redirects 419

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 ]));

25.6: Redirect in a controller


When you need to send the user to a different page on your site based on some logic, you might
use code something like this. Lots of processing happens and if the conditions are met, instead of
returning a render array, we return a RedirectResponse and the browser will load that page.

1 protected function reloadOperations(): ?RedirectResponse {


2 $program_nid = $this->programNode->id();
3 if (substr($this->action, 0, 6) === 'reload') {
4 $new_type = substr($action, 7);
5 $this->votingProcessor->buildVotingPath($this->programNode, $new_type);
6 $valid_voting_path = $this->votingProcessor->loadYourVotingPathFromTempStore($pr\
7 ogram_nid);
8 $empty_voting_path = $this->votingProcessor->isEmptyVotingPath();
9 if ($valid_voting_path && $empty_voting_path) {
10 // Redirect to kss overview.
11 $message = "No eligible items found. Redirected to KSS overview";
12 \Drupal::messenger()->addMessage($message);
13 $url = Url::fromRoute('tea_teks_srp.kss_overview', [
14 'program' => $program_nid,
15 'type' => 'all',
16 'userid' => $this->persona->getUserId(),
17 ]);
18 return new RedirectResponse($url->toString());
19 }
20 if ($valid_voting_path && !$empty_voting_path) {
21 //redirect to the first item in the path.
22 $item = $this->votingProcessor->getFirstVotingPathItem();
23 $vote_type = 'narrative';
24 if (empty($item['narrative'])) {
25 $vote_type = 'activity';
26 }
27 $url = Url::fromRoute('tea_teks_srp.correlation_voting', [
28 'program' => $program_nid,
Redirects 420

29 'expectation' => $item['expectation_nid'],


30 'correlation' => $item['correlation_nid'],
31 'action' => "vote-$new_type",
32 'type' => $vote_type,
33 ]);
34 return new RedirectResponse($url->toString());
35 }
36 }

25.7: Redirect user after login


From: https://www.drupal.org/forum/support/module-development-and-code-questions/2013-08-
18/how-to-redirect-user-after-login-in This example checks both the route and user role to con-
ditionally redirect.

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 }

25.8: Redirect to the 403 or 404 page


Redirects 421

1 // Redirect to the 403 page.


2 throw new \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException();
3
4 // Redirect to the 404 page.
5 throw new \Symfony\Component\HttpKernel\Exception\NotFoundHttpException();

25.9: Redirect to a new page after node operation


This was implemented in a module in a hook_form_alter call. Here we tell the form to call the
special handler: cn_submit_handler() function in the form.

1 function obg_mods_form_alter(array &$form, FormStateInterface $form_state, $form_id)\


2 {
3 $accountProxy = \Drupal::currentUser();
4 $account = $accountProxy->getAccount();
5
6 // Add special validation for anonymous users only.
7 if (($accountProxy->isAnonymous() && ($form_id == 'node_catastrophe_notice_form'))\
8 ) {
9 $form['#validate'][] = 'cn_form_validate';
10
11 // Submit handler to redirect to /thanks-your-submission.
12 foreach (array_keys($form['actions']) as $action) {
13 if ($action != 'preview' && isset($form['actions'][$action]['#type']) && $form\
14 ['actions'][$action]['#type'] === 'submit') {
15 $form['actions'][$action]['#submit'][] = 'cn_submit_handler';
16 }
17 }
18 }

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

25.10: Redirect dynamically to wherever you came


from
In this case, in a form, we grab the referrer url in the buildForm method using the following code:

1 public function buildForm(array $form, FormStateInterface $form_state, int $program_\


2 nid = 0, int $feedback_error_nid = 0, int $citation_nid = 0){
3 $form['#theme'] = 'tea_teks_srp__vote_error';
4
5 // Get the referer.
6 $request = \Drupal::request();
7 $referer = $request->headers->get('referer');
8 //$base_url = Request::createFromGlobals()->getSchemeAndHttpHost();
9 $base_url = \Drupal::request()->getSchemeAndHttpHost();
10 $alias = '';
11 if (!is_null($referer)) {
12 $alias = substr($referer, strlen($base_url));
13 }
14 $form_state->set('referrer_alias', $alias);
15 ...
16 }

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.2: Overview of the Theme system and Render API.


The main purpose of Drupal’s Theme system is to give themes complete control over the appearance
of the site, which includes the markup returned from HTTP requests and the CSS files used to
style that markup. In order to ensure that a theme can completely customize the markup, module
developers should avoid directly writing HTML markup for pages, blocks, and other user-visible
output in their modules, and instead return structured ”render arrays”. Doing this also increases
usability, by ensuring that the markup used for similar functionality on different areas of the site is
the same, which gives users fewer user interface patterns to learn.
From the Render API overview at https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%
21Render%21theme.api.php/group/theme_render/10.0.x
Render Arrays 425

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

At the response level, you’ll see X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags headers.


Reproduced from the Render API overview at https://api.drupal.org/api/drupal/core%21lib%
21Drupal%21Core%21Render%21theme.api.php/group/theme_render/10.0.x

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

1 $variables['content']['field_image']['#suffix'] = "this is a suffix to the image";

Or

1 $variables['content']['custom_field']= ['#type'=>'markup', '#markup'=>'Hello World'];

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 ];

26.6: Simple Text


1 $text_array = [
2 '#markup' => $this->t('Hello world!'),
3 ];

26.7: Text with variable substitution (Placeholders)


1 $render_array = [
2 '#type' => 'markup',
3 '#markup' => $this->t('You are viewing @title. Unfortunately there is no image de\
4 fined for delta: @delta.', ['@title' => $node->getTitle(), '@delta' =>$delta)],
5 ];

And from the Render API Overview at https://api.drupal.org/api/drupal/core%21lib%21Drupal%


21Core%21Render%21theme.api.php/group/theme_render/10.0.x :
Placeholders in render arrays
Render arrays have a placeholder mechanism, which can be used to add data into
the render array late in the rendering process. This works in a similar manner
to \Drupal\Component\Render\FormattableMarkup::placeholderFormat9 (), with the text that
ends up in the #markup property of the element at the end of the rendering process getting
substitutions from placeholders that are stored in the ’placeholders’ element of the #attached
property.
For example, after the rest of the rendering process was done, if your render array contained:
9 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Component%21Render%21FormattableMarkup.php/function/
FormattableMarkup%3A%3AplaceholderFormat/10.0.x
Render Arrays 429

1 $build['my_element'] = [
2 '#markup' => 'Something about @foo',
3 '#attached' => [
4 'placeholders' => [
5 '@foo' => ['#markup' => 'replacement'],
6 ],
7 ];

then #markup would end up containing ’Something about replacement’.


Note that each placeholder value *must* itself be a render array. It will be rendered, and any cache
tags generated during rendering will be added to the cache tags for the markup.

26.8: Wrap an element with a div with a class


1 $ra['list'][$customer]['name'] = [
2 '#prefix' => '<div class="customer-name">',
3 '#suffix' => '</div>',
4 '#type' => 'markup',
5 '#markup' => $customer['name'],
6 ];

26.9: Prefix and suffix


1 $ra['#prefix'] = '<div id="option-landing-block">';
2 $ra['#suffix'] = '</div>';

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

1 public function build() {


2 $date = new \DateTime();
3 return [
4 '#markup' => t('Copyright @year&copy; My Company', [
5 '@year' => $date->format('Y'),
6 ]), ];
7 }

This uses Drupal\Core\Datetime\DrupalDateTime which is just a wrapper for \DateTime.

26.11: Image
Load an image and display it with the alt text

1 public function displayProductImage(NodeInterface $node, $delta) {


2 if (isset($node->field_product_image[$delta])) {
3 $imageData = $node->field_product_image[$delta]->getValue();
4 $file = File::load($imageData['target_id']);
5 $render_array['image_data'] = array(
6 '#theme' => 'image_style',
7 '#uri' => $file->getFileUri(),
8 '#style_name' => 'product_large',
9 '#alt' => $imageData['alt'],
10 );
11 }

26.12: Several Url’s.


This queries for some nodes, generate a list of url’s and returns them as a render array. The ’#list_-
type’ => ’ol’ (or ordered list)
Render Arrays 431

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 }

26.13: Two paragraphs


Render Arrays 432

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.14: A button that opens a modal dialog


1 use Drupal\Core\Url;
2
3 public function build() {
4 $link_url = Url::fromRoute('custom_modal.modal');
5 $link_url->setOptions([
6 'attributes' => [
7 'class' => ['use-ajax', 'button', 'button--small'],
8 'data-dialog-type' => 'modal',
9 'data-dialog-options' => Json::encode(['width' => 400]),
10 ]
11 ]);
12
13 return [
14 '#type' => 'markup',
15 '#markup' => Link::fromTextAndUrl(t('Open modal'), $link_url)->toString(),
16 '#attached' => ['library' => ['core/drupal.dialog.ajax']]
17 ];

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 ];

Other possible urls:

1 '#url' => Url::fromUri('internal:/dashboard'),


2 '#url' => Url::fromUri('internal:/node/360'),
3 '#url' => Url::fromUri('mailto:' . $value),

26.16: A link with a class


Here we add the #attributes to wrap the link in the classes: button, button-action, button--primary,
and button--small:

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 ],

26.17: A link and its TWIG template


Here is a link with the details of what you expect to see in the TWIG template:

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

and in the template you would expect to see something like:


Render Arrays 434

1 {% raw %}{{ content.back_home_link }}{% endraw %}

26.18: A link with parameters and a template file


This path takes a 4 parameters. Here is its path as defined in the routing.yml file:

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;

Then we wrap up all the variables and send them to buildDetails

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 public function buildDetails(array $content, array $breadcrumbs, array $management_l\


2 inks, array $correlation_info, array $citations): array {
3 return [
4 '#theme' => 'team_abc__correctional_voting',
5 '#content' => $content,
6 '#breadcrumbs' => $breadcrumbs,
7 '#management_links' => $management_links,
8 '#correlation' => $correlation_info,
9 '#citations' => $citations,
10 ];
11 }

Then in team-abc—correctional-voting.html.twig the next links are rendered–see {{next_link }}


Render Arrays 436

1 <div class="cell small-12 medium-6">


2 {% raw %}{% if content.next_links %}
3 <ul class="no-bullet nav-links prev">
4 {% for next_link in content.next_links %}
5 Move mouse below for Next invisible links<li>{{ next_link }}</li>
6 {% endfor %}
7 </ul>
8 {% endif %}{% endraw %}
9 </div>

This example may be a little confusing as it loops through an array of links.

26.19: Simple unordered list


From: https://drupal.stackexchange.com/questions/214928/create-unordered-list-in-render-array

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 ];

26.20: Unordered list of links for a menu


Here a list of links is created in a controller:

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

11 '#type' => 'link',


12 '#title' => $this->t('My Enrollment Training'),
13 '#url' => Url::fromRoute('abc_academy.dashboard_tab', ['tab' => 'enrollment'\
14 ])
15 ],
16 [
17 '#type' => 'link',
18 '#title' => $this->t('My Transcript'),
19 '#url' => Url::fromRoute('abc_academy.dashboard_tab', ['tab' => 'transcript'\
20 ])
21 ],
22 [
23 '#type' => 'link',
24 '#title' => $this->t('My Certificates'),/ '#url' => Url::fromRoute(\
25 'abc_academy.dashboard_tab', ['tab' => 'transcript'])
26 '#url' => Url::fromUri('internal:/dashboard/certificates'),
27 ],
28 ]
29 ];
30
31 return $content;

26.21: Nested Unordered List


1 $sidebar = [
2 '#title' => 'My List',
3 '#theme' => 'item_list',
4 '#list_type' => 'ul',
5 '#attributes' => ['class' => 'mylist'],
6 '#wrapper_attributes' => ['class' => 'container'],
7 '#items' => [
8 [
9 '#type' => 'link',
10 '#title' => t('My Online Training'),
11 '#url' => Url::fromUri('internal:/node/1'),
12 ],
13 [
14 '#type' => 'link',
15 '#title' => t('My Instructor-led Training'),
16 '#url' => Url::fromUri('internal:/node/2'),
17 ],
Render Arrays 438

18 ['#markup' => '<ul><li>item1</li><li>item2</li></ul>',],


19 ['#markup' => '<ul><li>item1</li><li>item2</li><li>item3</li></ul>',],
20 ],
21 ];

26.22: Select (dropdown)


To build a select element, fill an array with some keys and text labels. The text labels will appear in
the dropdown.
To set the default value i.e. the value that appears in the dropdown when it is first displayed, specify
the key. For example: If the contents of the dropdown are an array like ['aaa', 'vvv', 'zzz']
then you can specify $default=2 to display zzz as the default.
In the example below, the default is set to /node/364 and the dropdown will display Above ground
pool 1, Above ground pool 2 etc.

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 ];

26.23: Select (dropdown) Ajaxified


Often sites need select elements that do some action e.g. redirect when the user makes a selection
in the dropdown. Here is one example such a select element. I populate the $options with the result
of a database query. When the user changes the selection in the dropdown, it calls the callback
videoSelectChange(). The callback redirects to the URL in question using the $command = new
RedirectComand();
Render Arrays 439

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 }

26.24: Limit allowed tags in markup


Here we allow <i> (italics) tags in the login menu item, and <i> and <sup> (superscript) tags in the
logout menu item
Render Arrays 440

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 }

26.25: Disable an element


In this example the accept button is disabled when $my_current_vote is accepted.

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

27.1.3: Connecting to a twig template


Most often, you will have a twig template connected to your controller. You do this by a combination
of a #theme element in the render array and a hook_theme function in a .module file.
In the example below, the controller returns a large render array and the theme is identified as
abc_teks_srp__correlation_voting.

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 ],

The template will therefore be abc-teks-srp--correlation.voting.yml


Routes and Controllers 444

27.2: Simple page without arguments


This route is for a page with no arguments/parameters.
In the file page_example.routing.yml (e.g. web/modules/contrib/examples/page_example/page_-
example.routing.yml and the controller is at web/modules/contrib/examples/page_-
example/src/Controller/PageExampleController.php

1 # If the user accesses https://example.com/?q=examples/page-example/simple,


2 # or https://example.com/examples/page-example/simple,
3 # the routing system will look for a route with that path.
4 # In this case it will find a match, and execute the _controller callback.
5 # Access to this path requires "access simple page" permission.
6 page_example_simple:
7 path: 'examples/page-example/simple'
8 defaults:
9 _controller: '\Drupal\page_example\Controller\PageExampleController::simple'
10 _title: 'Simple - no arguments'
11 requirements:
12 _permission: 'access simple page'

27.3: Page with arguments


From web/modules/contrib/examples/page_example/page_example.routing.yml {first}/{second}
are the arguments.

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'

27.4: Simple form


From web/modules/custom/rsvp/rsvp.routing.yml. This route will cause Drupal to load the form:
RSVPForm.php so the user can fill it out.
Routes and Controllers 445

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'

27.5: Admin form (or settings form)


From web/modules/custom/rsvp/rsvp.routing.yml this route loads the admin or settings form
RSVPConfigurationForm.

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

27.6: Routing permissions


These are defined in your module.permissions.yml e.g. rsvp.permissions.yml. If you add this file
to a module, a cache clear will cause the new permissions to appear on the permissions page.
This requires the user to be logged in to access this route:

1 requirements:
2 _user_is_logged_in: 'TRUE'

To skip permissions, set _access to TRUE like this:

1 requirements:
2 _access: 'TRUE'

27.6.1: A specific permission


To specify a particular permission, use the following. Note. Case is critical!
Routes and Controllers 446

1 requirements:
2 _permission: 'administer rsvplist'

27.6.2: Multiple permissions


Drupal allows stacking permissions with the plus(+) sign. Note the + sign means OR. e.g.

1 requirements:
2 _permission: 'vote on own squishy item+manage squishy process'

27.7: Set the page title dynamically


This code shows how to set the title statically in the module.routing.yml file, as well as how to call
a function like getTitle() to return it so it can be dynamically generated:

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 }

27.8: Disable caching on a route


This will cause Drupal to rebuild the page internally on each page load but won’t stop browsers or
CDN’s from caching. The line: no_cache: TRUE is all you need to disable caching for this route.

1 requirements:
2 _permission: 'access content'
3 options:
4 no_cache: TRUE

27.9: Generate route and controller with Drush


Drush has the ability to generate code to start you off. Use drush generate module and or drush
generate controller to get a nice starting point for you to write your own controllers.

For more, on generating controllers see https://www.drush.org/latest/generators/controller/


This is what it looks like to generate a controller:

1 $ drush generate controller


2
3 Welcome to controller generator!
4 ----------------------------------
5
6 Module machine name [web]:
7 � general
8
9 Class [GeneralController]:
10 � ExampleController
11
12 Would you like to inject dependencies? [No]:
13 �
14
Routes and Controllers 448

15 Would you like to create a route for this controller? [Yes]:


16 �
17
18 Route name [general.example]:
19 � general.book_example
20
21 Route path [/general/example]:
22 � /general/book_example
23
24 Route title [Example]:
25 � Book Example
26
27 Route permission [access content]:
28 �
29
30 The following directories and files have been created or updated:
31 -------------------------------------------------------------------
32 • /Users/selwyn/Sites/d9book2/web/modules/custom/general/general.routing.yml
33 • /Users/selwyn/Sites/d9book2/web/modules/custom/general/src/Controller/ExampleCont\
34 roller.php

The file general.routing.yml will then contain:

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'

The ExampleController.php file has these contents:

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

10 class ExampleController extends ControllerBase {


11
12 /**
13 * Builds the response.
14 */
15 public function build() {
16
17 $build['content'] = [
18 '#type' => 'item',
19 '#markup' => $this->t('It works!'),
20 ];
21
22 return $build;
23 }
24
25 }

This is a huge timesaver!

27.10: Finding routes with Drush


Drush lets you figure out the controller associated with a route since version 10.5. Here are some of
the options:

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/

27.10.1: All routes


Output from drush route. It lists the routes by name and the path they apply to.
Routes and Controllers 450

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 ...

27.10.2: Specific path


Output when checking a specific path

1 $ drush route --path /admin/flush


2
3 name: admin_toolbar_tools.flush
4 path: /admin/flush
5 defaults:
6 _controller: '\Drupal\admin_toolbar_tools\Controller\ToolbarController::flushAll'
7 _title: 'Flush all caches'
8 requirements:
9 _permission: 'administer site configuration'
10 _csrf_token: 'TRUE'
11 options:
12 _admin_route: true
13 _access_checks:
14 - access_check.permission
15 - access_check.csrf
Routes and Controllers 451

27.10.3: Specific route name


1 $ drush route --name=admin_toolbar_tools.flush
2
3 name: admin_toolbar_tools.flush
4 path: /admin/flush
5 defaults:
6 _controller: '\Drupal\admin_toolbar_tools\Controller\ToolbarController::flushAll'
7 _title: 'Flush all caches'
8 requirements:
9 _permission: 'administer site configuration'
10 _csrf_token: 'TRUE'
11 options:
12 _admin_route: true
13 _access_checks:
14 - access_check.permission
15 - access_check.csrf

27.11: Getting some help from Chat GPT


At the time of this writing, OpenAI1 has a research release of its ChatGPT available. You could
always ask it to generate a controller for you as I did. I was amazed by the result. I asked it to:
Generate a Drupal 10 controller which displays a table of temperatures from 0 to 100
degrees in both celsius and fahrenheit

To which it replied:

1 Here is a Drupal 10 controller that generates a table of temperatures from 0 to 100 \


2 degrees in both Celsius and Fahrenheit:

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 }

followed by this thoughtful addition:

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:

1 // Access the Drupal JSON serialization service.


2 $this->jsonSerialization = \Drupal::service('serialization.json');
3 $this-jsonSerialization->decode($string);
4
5 // Access your custom service.
6 $this->salutationService = \Drupal::service('hello_world.salutation');
7 $this->salutationService->hello();
8 // Or.
9 $abc_retrieval_service = \Drupal::service('abc.aardvark_retrieval_service');
10 $aardvark_names = $abc_retrieval_service->getAardvarkNames();

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

28.3: Static Shorthand methods


A few popular services also have shorthand methods in the core Drupal.php file for accessing them
faster (and easier for IDE autocompletion), for example, \Drupal::entityTypeManager(). Check it
out for services with shorthand methods:
e.g.

1 public static function routeMatch() {


2 return static::getContainer()->get('current_route_match');
3 }
4
5 public static function currentUser() {
6 return static::getContainer()->get('current_user');
7 }
8
9 public static function entityTypeManager() {
10 return static::getContainer()->get('entity_type.manager');
11 }
12
13 public static function cache($bin = 'default') {
14 return static::getContainer()->get('cache.' . $bin);
15 }

and many more..

28.4: Services in action


Most of these examples show Drupal configuration.
In this example we use the config.factory service (via the ::configFactory() shortcut) to change
the system email plugin to use our mail plugin:
Services and Dependency Injection 456

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 }

Here we use config.factory service to change some config variables:

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');

28.5: ControllerBase shortcuts


ControllerBase.php comes prepackaged with functions to get the following services statically:

1 protected function entityTypeManager() {


2 protected function entityFormBuilder() {
3 protected function cache($bin = 'default') {
4 protected function config($name) {
5 protected function keyValue($collection) {
6 protected function state() {
7 protected function moduleHandler() {
8 protected function formBuilder() {
9 protected function currentUser() {
10 protected function languageManager() {

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

28.6: Injected/Dependency Injection


Using dependency injection is the preferred way to use services although it involves more steps. See
the Dependency Injection section below for more details.
Using dependency injection requires that you create a constructor and a create() function in your
controller class.
The create function gets the service container as a parameter and chooses the services it needs.
The create function calls the constructor and passes the services as arguments and stores them as
properties.
The process for a block (or plugin) is a little different:
Your block must implement ContainerFactoryPluginInterface. Plugins only get access to the service
container if they implement the ContainerFactoryPluginInterface e.g.

1 class TestBlock extends BlockBase implements ContainerFactoryPluginInterface {

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)

Here is an example of a block constructor with the AccountProxyInterface parameter added so we


can inject that service:

1 public function __construct(array $configuration, $plugin_id, $plugin_definition, Ac\


2 countProxyInterface $account) {
3 parent::__construct($configuration, $plugin_id, $plugin_definition);
4 $this->account = $account;
5 }

More about services and dependency injection at https://code.tutsplus.com/tutorials/drupal-8-


properly-injecting-dependencies-using-di--cms-26314
More about using dependency injection for blocks and other plugins https://chromatichq.com/blog/
dependency-injection-drupal-8-plugins

28.6.1: Controller details


Here are the steps for implementing an injected service in a controller.
From: docroot/modules/custom/apitest/src/Controller/ApiTestController.php
1. Your controller must extend ControllerBase
Services and Dependency Injection 459

1 class ApiTestController extends ControllerBase {

2. You need a protected variable to hold the service

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.

1 public static function create(ContainerInterface $container) {


2 return new static(
3 $container->get('cm_api.client')
4 );
5 }

Note, you can pass multiple services by adding additional $container->get() calls like this:

1 public static function create(ContainerInterface $container) {


2 return new static(
3 $container->get('current_user'),
4 $container->get('path.current'),
5 $container->get('path.validator'),
6 );
7 }

4. Your constructor will expect your newly instantiated service(s) as parameters:

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

1 public function __construct(AccountProxyInterface $account, CurrentPathStack $path_s\


2 tack, PathValidatorInterface $path_validator) {
3 $this->account = $account;
4 $this->pathStack = $path_stack;
5 $this->pathValidator = $path_validator;
6 }

5. In your code, use the protected variable ($cmAPIClient) to call functions in the service:

1 $result = $this->cmAPIClient->catchAll('POST', $body);

Rejoice! Note. No need to make any routing changes. Drupal handles all the parameters with the
instructions provided. etc.

28.6.2: Controller Example 1


Here is a complete controller which uses the current_user service:

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

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 }

28.6.3: Controller Example 2


This controller uses 3 different services:

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

65 $message .= "<br> Path: " . $path;


66
67 $test_path = "/vote1";
68 $valid_path = $this->pathValidator->isValid($test_path);
69 $message .= "<br> Check for valid path: " . $test_path . " returned: " . $valid_\
70 path;
71
72
73 $build['content'] = [
74 '#type' => 'item',
75 '#markup' => $this->t($message),
76 ];
77
78 return $build;
79 }
80
81 }

28.7: Finding services


Here is the process to find a commonly used service, the entityTypeManager which is used for
entityQueries.
You can look at https://api.drupal.org/api/drupal/services and search for entity_type. This will
result in:
Services and Dependency Injection 464

This tells you to that the service is in core.services.yml and that it is implemented in the
EntityTypeManager class.

So in Drupal core’s core.services.yml file you will find:

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 }

Looking in Drupal.php, you will also find a shorthand method:

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 }

So to use this statically, you can use the following:

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.

28.8: Creating a custom service


To make a service, you will need two parts: a module.services.yml file and a controller.
In the module.services.yml file. You need a machine name for the service and a class that
implements it. E.g. in kitchen_product.services.yml you might have the following:
Services and Dependency Injection 465

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%"

The %highway.road.user_key_value_cache% value is a special argument which has been defined


above the services key as a parameter. Its value will default to true on production:

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:

1 # Local development services.


2 #
3 # To activate this feature, follow the instructions at the top of the
4 # 'example.settings.local.php' file, which sits next to this file.
5 parameters:
6 highway.road.use_key_value_cache: false

Note you can also pass strings in the form ’blah’ surrounded by single quotes.
Services and Dependency Injection 466

28.8.2: Passing the config factory to our service


As shown above, arguments use the ”arguments” key, which can have an array of services, each
preceeded by an @ symbol. Other values which are not services can also be passed. In the
module.services.yml file below, we pass the config.factory service. You can find it in the
core.services.yml file where you can see it maps to the Drupal\Core\Config\ConfigFactory class.

It looks like this in the core.services.yml:

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']

Here is how it looks in your module.services.yml:

1 services:
2 hello_world.salutation:
3 class: Drupal\hello_world\HelloWorldSalutation
4 arguments: ['@config.factory']

28.8.3: Taxonomy Tree Custom Service


Here is an example where Daniel Sipos of https://www.webomelette.com creates a custom service to
build a taxonomy tree. Read his article at https://www.webomelette.com/loading-taxonomy-terms-
tree-drupal-8 . The repo is at https://github.com/upchuk/taxonomy_tree. His code is reproduced
below:
Here is the taxonomy_tree.services.yml file:

1 services:
2 taxonomy_tree.taxonomy_term_tree:
3 class: Drupal\taxonomy_tree\TaxonomyTermTree
4 arguments: ['@entity_type.manager']

And the controller:


Services and Dependency Injection 467

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 }

28.9: Using your custom service


This is identical to using a Drupal built in service. These are the steps:

1. In your controller, make sure your controller extends ControllerBase.


2. Add protected variable in your class to hold your service.
Services and Dependency Injection 469

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: Dependency Injection

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.

28.10.2: Service Container


The Service container is the PHP object which handles the instantiation of all required services.
When you want to use a service, you ask the service container for one and then you can call methods
on the service. The Drupal Service container is built on top of the Symfony Service container1 .
More at https://www.drupal.org/docs/drupal-apis/services-and-dependency-injection/services-
and-dependency-injection-in-drupal-8.
For example, in core.services.yml, you will find the email.validator service which references
the EmailValidator class. This is what you will see in core.services.yml:

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()

Similarly, in core.services.yml there is a current_route_match service which references the class


CurrentRouteMatch:

1 current_route_match:
2 class: Drupal\Core\Routing\CurrentRouteMatch
3 arguments: ['@request_stack']

Looking in CurrentRouteMatch.php, there is a getRouteName() function which can be used to get


the current route name with a call like:

1 $this->currentRouteMatch->getRouteName()

Dig deeper in core.services.yml file for many more services.

28.10.3: Controller Example 1


Here is a complete controller which uses the current_user service:

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 }

28.10.4: Controller Example 2


This controller uses 3 different services

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

20 public static function create(ContainerInterface $container) {


21 return new static(
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 ->accessCheck(TRUE)
59 ->condition('type', 'article')
60 ->condition('title', $name)
61 ->count();
62 $count_nodes = $query->execute();
Services and Dependency Injection 473

63 $message .= "<br>Retrieved " . $count_nodes . " nodes";


64
65 $path = $this->pathStack->getPath();
66 $message .= "<br> Path: " . $path;
67
68 $test_path = "/vote1";
69 $valid_path = $this->pathValidator->isValid($test_path);
70 $message .= "<br> Check for valid path: " . $test_path . " returned: " . $valid_\
71 path;
72
73
74 $build['content'] = [
75 '#type' => 'item',
76 '#markup' => $this->t($message),
77 ];
78
79 return $build;
80 }
81
82 }

28.10.5: Blocks and other plugins


The process for a block (or plugin) is a little different:
Your block must implement ContainerFactoryPluginInterface. Plugins only get access to the
service container if they implement the ContainerFactoryPluginInterface e.g.

1 class TestBlock extends BlockBase implements


2 ContainerFactoryPluginInterface {

You must also add the extra parameters to the create() and __construct() function i.e. $plugin_id
and $plugin_definition e.g.

1 public static function create(ContainerInterface $container, array $configuration, $\


2 plugin_id, $plugin_definition)

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

1 public function __construct(array $configuration, $plugin_id, $plugin_definition, Ac\


2 countProxyInterface $account) {
3 parent::__construct($configuration, $plugin_id, $plugin_definition);
4 $this->account = $account;
5 }

See more about using dependency injection for blocks and other plugins at https://chromatichq.com/
blog/dependency-injection-drupal-8-plugins

28.11: Procedural to Class-based dependency injection


Here is the overview from the Drupal.php file for Drupal 9.5.0 of when and how to use dependency
injection:

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

28.12: Drush services commands

28.12.1: List all services


drush devel:services or drush dcs or

1 drush eval "print_r(\Drupal::getContainer()->getServiceIds());"

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 …

28.12.2: Generate custom service


Drush provides a great starting point by generating some useful code that you can easily build on.
Consider using this facility as you write your code.
From https://www.drush.org/latest/generators/service_custom
drush generate service:custom. Generates a custom Drupal service

Also there are these gems:


drush generate service:logger. Generates a logger service

drush generate service:breadcrumb-builder. Generates a breadcrumb builder service

drush generate service:event-subscriber. Generates an event subscriber

drush generate service:middleware. Generates a middleware

drush generate service:param-converter. Generates a param converter service

drush generate service:path-processor. Generates a path processor service

drush generate service:request-policy. Generates a request policy service

drush generate service:response-policy. Generates a response policy service

drush generate service:route-subscriber. Generates a route subscriber

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.

29.2: State API


The State API allows you to store small pieces of information specific to a site. This information is
stored permanently in the database.
State API data is stored permanently in the key_value table. There is a set(), get() and delete()
as well as setMultiple() and getMultiple() functions. The convention for the key name is to
State API, TempStore and UserData 479

use periods(. or full-stops) to separate words. E.g. my.state.data or emergency.header.message.


Underscores are also used e.g. system.cron_key or system.cron_last.
State settings are values which should usually not be exported to code, and only make sense in the
context of one site. For example, cron key is a state setting whereas the front page path is a ”config”
variable.
To set a state value:

1 \Drupal::state()->set('system.cron_key', md5("This is an example of a bad cron key")\


2 );

In contrast, this is how you set a config value:

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

29.2.1: Using Drush to read the State API


You can use Drush to set, read or delete values in the State API: e.g. here we read the selwyn1 key
value:

1 $ drush ev 'return \Drupal::state()->get("selwyn1");'


2 abc

29.2.2: Example accessing State API


Here is code that shows examples of setting, getting and deleting state values:
State API, TempStore and UserData 481

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

44 '%selwyn2' => \Drupal::state()->get('selwyn2'),


45 ]),
46 ];
47
48 \Drupal::state()->deleteMultiple(['selwyn1', 'selwyn2']);
49 $build['content'][] = [
50 '#type' => 'item',
51 '#markup' => $this->t('Deleted (multiple) State API data.'),
52 ];
53
54 return $build;
55 }
56
57 }

29.2.3: Long strings broken into paragraphs


If you store long strings, you can explode them into arrays of paragraphs for more control of their
display.

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

1 public function build4() {


2
3 /** @var \Drupal\user\UserDataInterface $userData */
4 $userData = \Drupal::service('user.data');
5 $user_id = 2;
6
7 $userData->set('state_examples', $user_id, 'program.123.vote.0.finalized', 'COMPLE\
8 TED');
9
10 $build['content'][] = [
11 '#type' => 'item',
12 '#markup' => $this->t('Retrieved User Data API data: %value1.', [
13 '%value1' => $userData->get('state_examples', $user_id, 'program.123.vote.0.fi\
14 nalized'),
15 ]),
16 ];
17
18 $userData->delete('state_examples', $user_id, 'program.123.vote.0.finalized');
19
20 return $build;
21 }

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:

1 public function build5() {


2
3 // Private TempStore example.
4
5 // Get the private TempStore for the state_examples module.
6 $tempstore = \Drupal::service('tempstore.private')->get('state_examples');
7 $tempstore->set('selwyn.important.string', 'abcdef');
8
9 $build['content'][] = [
10 '#type' => 'item',
11 '#markup' => $this->t('Retrieved Private TempStore API data: %value1.', [
12 '%value1' => $tempstore->get('selwyn.important.string'),
State API, TempStore and UserData 485

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:

1 public function build6() {


2
3 // Shared TempStore example.
4
5 // Get the shared TempStore for the state_examples module.
6 /** @var \Drupal\Core\TempStore\SharedTempStoreFactory $factory */
7 $factory = \Drupal::service('tempstore.shared');
8 $tempstore = $factory->get('state_examples');
9
10 // Store an array.
11 $agent_array = [
12 'id' => '007',
13 'name' => 'James Bond',
14 ];
15 $tempstore->set('selwyn.important.agent', $agent_array);
16
17 // Retrieve the data.
18 $array = $tempstore->get('selwyn.important.agent');
State API, TempStore and UserData 488

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 here is the data in the database:

And you can see the data owner in the screen shot below:

29.4.2.1: Injecting tempstore.shared

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']

1 /** @var \Drupal\Core\TempStore\SharedTempStore|null */


2 protected ?SharedTempStore $tempStore = NULL;
3
4 public function __construct(EntityTypeManager $entityTypeManager, AccountInterface\
5 $account, SharedTempStoreFactory $sharedTempStoreFactory) {
6 $this->entityTypeManager = $entityTypeManager;
7 $this->account = $account;
8 // Derive the collection.
9 // Note. this will write into the key_value_expire table collection column: temp\
10 store.shared.tea_teks_srp. */
11 $this->tempStore = $sharedTempStoreFactory->get('tea_teks_srp');
12 }
13
14 public function create(ContainerInterface $container) {
15 return new static(
16 $container->get('entity_type.manager'),
17 $container->get('current_user'),
18 $container->get('tempstore.shared')
19 );
20 }

Here is an example of actually writing and then reading the value from the shared tempstore.

1 $now = new DrupalDateTime('now');


2 $votes[12345] = [
3 'vote' => 'a',
4 'reason' => '',
5 'timestamp' => $now->format('Y-m-d H:i:s'),
6 'serialized' => FALSE,
7 ];
8 // Write.
9 $this->tempStore->set($this->tempstoreKey, $votes);
10 // Read.
11 $x = $this->tempStore->get($this->tempstoreKey);

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

36 '#markup' => $this->t('Invalid term: @term', ['@term' => $category])


37 ];
38 return $ra;
39 }
40 // pop off the first one and grab it's term_id
41 $term = reset($terms);
42 $term_id = $term->get('tid')->value;

30.2: Lookup term name using its tid


1 use Drupal\taxonomy\Entity\Term;
2 $term = Term::load($term_id);
3
4 // OR
5
6 $term_name = \Drupal\taxonomy\Entity\Term::load($term_id)->label();
7
8 // Or
9
10 $term_name = \Drupal\taxonomy\Entity\Term::load($term_id)->get('name')->value;
11
12 // Or
13
14 $term = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->load($term_id);
15
16 if (empty($term)) {
17 $render_array = [
18 '#markup' => $this->t('Invalid term id: @termid', ['@termid' => $term_id])
19 ];
20 }
21 else {
22 $term_name = $term->name;
23 $render_array = [
24 '#markup' => $this->t('Term name: @term_name', ['@term_name' => $term_name])
25 ];
26
27 }
28 return $render_array;
29 }
Taxonomy 493

30.3: Lookup term using its uuid


Each taxonomy term has a UUID. See the taxonomy_term_data table uuid field. We can load a
taxonomy term by it’s uuid as shown below:
Here we load a taxonomy term, get it’s name and it’s tid.

1 public function loadByUUID() {


2 $uuid = 'd4a7bbc5-3b1b-46a4-bea4-01255365999f';
3
4 $storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
5 $term_loaded_by_uuid = $storage->loadByProperties(['uuid' => $uuid]);
6 $term = reset($term_loaded_by_uuid);
7 $str = "Term not found";
8 if (!empty($term)) {
9 $term_name = $term->getName();
10 $term_id = $term->id();
11 $str = "Term uuid = $uuid<br>";
12 $str .= "Term name = $term_name<br>";
13 $str .= "Term id = $term_id<br>";
14 }
15
16 $build['content'] = [
17 '#type' => 'item',
18 '#markup' => $str,
19 ];
20
21 return $build;
22 }

30.4: Load terms from a term reference field


Retrieve the values in the field_event_category and display their term name, term id and their uuid.
The call to referencedEntities() returns an array of term objects, so no need to call load() on them
separately.
Taxonomy 494

1 public function loadTermRef() {


2 $nid = 24;
3 $event_node = Node::load($nid);
4 if ($event_node) {
5 $categories = $event_node->get('field_event_category')->referencedEntities();
6 }
7 $str = "Terms:<br>";
8
9 /** @var \Drupal\Core\Entity\EntityInterface $category */
10 foreach ($categories as $category) {
11 $term_id = $category->id();
12 $term_name = $category->getName();
13 $uuid = $category->uuid();
14 $str .= "Term name = $term_name<br>";
15 $str .= "Term id = $term_id<br>";
16 $str .= "Term uuid = $uuid<br>";
17 }
18
19 $build['content'] = [
20 '#type' => 'item',
21 '#markup' => $str,
22 ];
23
24 return $build;
25 }

30.5: Find terms referenced in a paragraph in a term


reference field
Loop thru all the instances of a paragraph reference and grab the term in the paragraph.
use Drupal\taxonomy\Entity\Term;

1 foreach ($node->get('field_my_para')->referencedEntities() as $ent){


2 $term = Term::load($ent->$field_in_paragraph->target_id);
3 $name = $term->getName();
4 print_r($name);
5 }
Taxonomy 495

30.6: Get URL alias from a term ID


This will return something like:

1 URL Alias for tid 3 = https://d9book2.ddev.site/category/rally

1 public function getTaxonomyAlias() {


2 $tid = 3;
3 //return taxonomy alias
4 $options = ['absolute' => TRUE]; //FALSE will return relative path.
5
6 // Build a URL.
7 /** @var \Drupal\Core\Url $url */
8 $url = Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $tid],\
9 $options);
10 $path_string = $url->toString();
11 $str = "URL Alias for tid $tid = $path_string";
12
13 $build['content'] = [
14 '#type' => 'item',
15 '#markup' => $str,
16 ];
17
18 return $build;
19 }

30.7: Load all terms for a vocabulary


This code loads the terms into an array and displays them on screen. Note that you can’t use id()
or getName() on the objects returned from loadTree() as they are standard objects. If you load the
actual term entities using Term::load(), then you can use entity functions like id() and getName(). It
returns this:
Found 5 terms in vocabulary event_category
Term: Hunger strike term_id: 5
Term: Protest term_id: 4
Term: Rally term_id: 3
Term: Training term_id: 2
Term: Webinar term_id: 1
Taxonomy 496

1 public function loadTerms() {


2 $vocabulary_id = 'event_category';
3
4 $terms = \Drupal::entityTypeManager()
5 ->getStorage('taxonomy_term')
6 ->loadTree($vocabulary_id);
7
8 /** @var \Drupal\taxonomy\Entity\Term $term */
9 foreach ($terms as $term) {
10 $terms_array[] = [
11 'id' => $term->tid,
12 'name' => $term->name,
13 'weight' => $term->weight,
14 ];
15 }
16 $length = count($terms_array);
17 $str = "Found $length terms in vocabulary $vocabulary_id<br>";
18
19 foreach ($terms as $term) {
20 $str .= 'Term: ' . $term->name . ' term_id: ' . $term->tid . '<br>';
21 }
22 $build['content'] = [
23 '#type' => 'item',
24 '#markup' => $str,
25 ];
26 return $build;
27 }
28 }

30.8: Load all terms for a vocabulary and put them in a


select (dropdown)
Taxonomy 497

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 ];

30.9: Create taxonomy term programatically


Vid is the vocabulary id e.g. event_category or type

1 $term = Term::create([
2 'name' => 'protest',
3 'vid' => 'event_category',
4 ])->save();

30.10: Find all nodes with a matching term


See the queries chapter for other ways to do this.
Taxonomy 498

1 public function getMatchingNodes() {


2 $term_id = 3;
3
4 $nodes = \Drupal::entityTypeManager()->getStorage('node')->loadByProperties([
5 'field_event_category' => $term_id,
6 ]);
7
8 $length = count($nodes);
9 $str = "Found $length nodes matching term_id $term_id<br>";
10
11 /** @var Node $node */
12 foreach ($nodes as $node) {
13 $str .= "Node ". $node->id() . ": Title: " . $node->getTitle() . "<br>";
14 }
15 $build['content'] = [
16 '#type' => 'item',
17 '#markup' => $str,
18 ];
19
20 return $build;
21 }

30.11: Find nodes with a matching term using


entityQuery
This finds the first 5 nodes that have the matching term.

1 protected function loadFirstOpinion($term_id) {


2 $storage = \Drupal::entityTypeManager()->getStorage('node');
3 $query = \Drupal::entityQuery('node')
4 ->condition('status', 1)
5 ->condition('type', 'opinion')
6 ->condition('field_category', $term_id, '=')
7 ->range(0, 5);
8 $nids = $query->execute();
9 $nodes = $storage->loadMultiple($nids);
10
11 $ra = [];
12 foreach ($nodes as $node) {
13 $ra[] = [
14 '#type' => 'markup',
Taxonomy 499

15 '#markup' => '<p>' . $node->getTitle(),


16 ];
17 }
18 return $ra;
31: TWIG
31.1: Overview
Drupal 10 uses Twig 3. Drupal 9 uses Twig 2. Drupal 8 used Twig 1.

31.1.1: Theme System Overview


Drupal’s theme system allows a theme to have nearly complete control over the appearance of
the site, which includes both the markup and the CSS used to style the markup. For this system
to work, instead of writing HTML markup directly, modules return ”render arrays”, which are
structured hierarchical arrays that include the data to be rendered into HTML, and options that
affect the markup. Render arrays are ultimately rendered into HTML or other output formats
by recursive calls to \Drupal\Core\Render\RendererInterface::render1 (), traversing the depth of the
render array hierarchy. At each level, the theme system is invoked to do the actual rendering. See
the documentation of \Drupal\Core\Render\RendererInterface::render2 () and the Theme system and
Render API topic3 for more information about render arrays and rendering.

31.1.2: Twig Templating Engine


Drupal uses the templating engine Twig. Twig offers developers a fast, secure, and flexible method
for building templates for Drupal 8 sites. Twig does not require front-end developers to know PHP
to build and manipulate Drupal themes.
For more on theming in Drupal see https://www.drupal.org/docs/theming-drupal .
For further Twig documentation see https://twig.symfony.com/doc/2.x4 and https://twig.symfony.
com/doc/3.x
Note. Drupal 10 uses Twig 3, Drupal 9 uses Twig 2 and Drupal 8 used Twig 1.
1 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21RendererInterface.php/function/RendererInterface%3A%
3Arender/10
2 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21RendererInterface.php/function/RendererInterface%3A%
3Arender/10
3 https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21theme.api.php/group/theme_render/10
4 https://twig.symfony.com/doc/2.x%20/
TWIG 501

31.2: Displaying Data

31.2.1: Fields or Logic


Twig can do things that PHP can’t such as whitespacing control, sandboxing, automatic HTML
escaping, manual contextual output escaping, inclusion of custom functions and filters that only
affect templates.
Double curly braces are used to output a variable. E.g.

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 %}

Use brace and pound symbol (hash) for comments e.g.

1 {# this section displays the voting details #}

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)

And even more Twig fun at https://twig.symfony.com/doc/3.x/functions/index.html

31.2.2: Which template, which variables?


There is usually one page.tpl.php and multiple node templates. One node template per content type.
Eg. node-news-story.html.twig, node-event.html.twig. There can also be field specific templates
e.g. web/themes/custom/txg/templates/field/field--field-3-column-links.html.twig
In the page.html.twig, you can refer to variables as page.content or node.label
whereas node templates expect content.field_image or node.field_myfield
Note. If you don’t see a field output for a node, try specifying the preface node. instead of content..
Field specific template are usually very simple and refer to
TWIG 502

1 {{ items }}

and

1 {{ item.content }}

e.g. from txg/web/themes/contrib/zurb_foundation/templates/page.html.twig

1 <section>
2 {{ page.content }}
3 </section>

And from txg/web/themes/custom/txg/templates/content/page--node--event.html.twig I acci-


dentally started implementing this in the page template. See below for the node template.

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:

Note. That node.label becomes label and node.field_for becomes content.field_for.


TWIG 503

1 <h1>{{ label }}</h1>


2 {{ content.field_image }}
3 <div>Node: {{ node.id }}</div>
4 <div>For: {{ content.field_for }}</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 <div>
9 Location:
10 {% if node.field_event_location_link.0.url %}
11 <a href="{{ node.field_event_location_link.0.url }}">{{ node.field_event_locatio\
12 n.0.value }}</a>
13 {% else %}
14 {{ node.field_event_location.0.value }}
15 {% endif %}
16 </div>
17
18 {% if node.field_event_cta_link.0.url %}
19 CTA:<div class="button"> <a href="{{ node.field_event_cta_link.0.url }}">{{ node.f\
20 ield_event_cta_link.0.title }}</a></div>
21 {% endif %}

31.2.3: Display fields or variables


Using node.field_myfield will bypass the rendering and display any markup in the field. Using
content.field_myfield uses the rendering system and is the preferred way to display your content.

This will display all the content rendered

1 {{ content }}

31.2.4: Node Title with and without a link


Render node title (or label) (with markup–so it may include <span> tags)

1 {{ label }}

Render node label (without markup–no html in this version)


TWIG 504

1 {{ node.label }}

Render link to node

1 <a href="{{ url }}">{{ label }}</a>

// Or a little more complex..

1 <div class="title"><a href="{{ url }}">{{ label }}</a> | <span>{{ content.field_vend\


2 or_ref }}</span></div>

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 }}

Any field–just jam content. in front of it

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 }}

31.2.6: Paragraph field


These still work fine: content.field_abc or node.field_ref_topic but instead of node, you preface
fields with paragraph like this:
TWIG 505

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>

31.2.7: Loop thru paragraph reference fields


Here we go looping thru all the values in a multi-value reference field.

1 {% for tax in paragraph.field_ref_tax %}


2 <div>target_id: {{ tax.target_id }}</div>
3 {% endfor %}

It’s the same as outputting these guys:

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

1 {# Figure out parameters to pass to view for news items #}


2 {% set params = '' %}
3 {% for item in paragraph.field_ref_tax_two %}
4 {% set params = params ~ item.target_id %}
5 {% if not loop.last %}
6 {% set params = params ~ '+' %}
7 {% endif %}
8 {% endfor %}
9 params: {{ params }}

This will output something like: 5+6+19

31.2.8: Body
1 {{ content.body }}

Or

1 {{ node.body.value }}

And for summary

1 {{ node.body.summary | raw }}

31.2.9: Multi-value fields


Fields that you preface with node. can also handle an index (the 0 below) i.e. to indicate the first
value in a multi-value field, 1 to indicate the second etc.

1 {{ node.field_iso_n3_country_code.0.value }}

31.2.10: Fields with HTML


If a field has html that you want rendered, use the keyword raw. Be aware this has security
considerations which you can mitigate using striptags5 filters:

5 https://twig.symfony.com/doc/3.x/filters/striptags.html
TWIG 507

1 <div>How to order: {{ how_to_order|raw }}</div>

And maybe you want to only allow <b> tags

1 {{ word|striptags('<b>')|raw }}

Or several tags. In this case <b><a><pre>

1 {{ word|striptags('<b>,<a>,<pre>')|raw }}

31.2.11: The date/time a node is published, updated or created


Each of these calls return a datetime value in string form which can be massaged by the twig date()
function for formatting.

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 {% set post_date = node.changedtime %}

Created (same as authored on date on node edit form):

1 {{ node.createdtime }}

And pretty formatted like Sep 2, 2023

1 {{ node.createdtime\|date('M d, Y') }}

Also

1 <div class="date">Date posted: {{ node.getCreatedTime|date('m/d/Y') }}</div>


2 <div class="date">Date posted: {{ node.getChangedTime|date('m/d/Y') }}</div>

Node published date:

1 Date published: {{ _context.node.published_at.value }}


2 Date published: {{ node.published_at.value }}

31.2.12: Format a date field


Use the field’s format settings; include wrappers. This example includes wrappers.

1 {{ content.field_blog_date }}

The examples below do not include wrappers.


Use the field’s format settings. This will use the format defined in Content type » Manage Displays
»Your View Mode.
TWIG 509

1 {{ content.field_blog_date.0 }}

Using Twig date filter and a defined Drupal date format

1 {{ node.field_blog_date.value|date('U')|format_date('short_mdyyyy') }}

Use Twig date filter

1 {{ node.field_blog_date.value|date('n/j/Y') }}

31.2.13: Smart date field formatting


When using the smart date6 module, dates are stored as timestamps so you have to use the twig date
function to format them. If you just put this in your template:

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') }}

Day of the week

1 {{ node.field_when.0.value|date('l') }} {# day of week #}

Hide the end date if it is the same as the start date

1 {% set start = node.field_when.0.value|date('l F j, Y') %}


2 {% set end = node.field_when.0.end_value|date('l F j, Y') %}
3 <p class="date"> {{ start }}</p>
4 {% if not start is same as(end) %}
5 <p class="date"> {{ end }}</p>
6 {% endif %}

31.2.14: Entity Reference field


If you have an entity reference field such as field_ref_topic (entity reference to topic content) you
have to specify the target_id like this. If you have only 1 reference, use the .0, for the second one
use .1 and so on.

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.

31.2.15: Entity reference destination content


If you have an entity reference and you want to display the content from the node that is referenced
i.e. if you have a contract with a reference to the vendor node and you want to display information
from the vendor node on the contract you can dereference fields in the entity destination:
From dirt/web/themes/custom/dirt_bootstrap/templates/content/node--contract--vendor-list.html.twig:

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>

31.2.16: Taxonomy term


Here is an example of displaying a taxonomy term.

1 <pre>
2 Dump category: {{ dump(node.field_ref_tax.entity.label) }}
3 </pre>

31.2.17: Render a block


Example block with a machine name of block---system-powered-by-block.html.twig from a
custom theme

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>

31.2.18: Render a list created in the template_preprocess_node()


Here we create a list in the function:
TWIG 512

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 }

and render it in the node--article--full.html.twig

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

And output the link without a label.

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.

1 <p><a class="btn secondary navy centered" href="{{ node.field_suggest_button.0.url }\


2 }">{{ node.field_suggest_button.0.title }}</a></p>

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 <p><a class="btn secondary navy centered" href="{{ node.field_suggest_button.uri }}"\


2 >{{ node.field_suggest_button.0.title }}</a></p>

Don’t try this as it won’t work:

1 //bad
2 {{ node.field_suggest_button.url }}.
3 //bad

Want to use the text from a different field? No problem.

1 <div class="title"><a href="{{ node.field_link.uri }}">{{ node.field_contract_number\


2 .value }}</a></div>

31.2.20: Links to other pages on site


Absolute link:

1 <a href="{{ url('entity.node.canonical', {node: 3223}) }}">Link to Weather Balloon n\


2 ode 3223 </a>

Relative link
See path vs url:

1 <a href="{{ path('entity.node.canonical', {node: 3223}) }}">Link to Weather Balloon \


2 node 3223 </a>

31.2.21: Link to a user using user id


You can link to users using the following:

1 <a href="{{ url('entity.user.canonical', {user: 1}) }}">Link to user 1 </a>

31.2.22: External link in a field via an entity reference


Here we have a node with an entity reference field (field_sf_contract_ref) to another entity.
In a preprocess function, you can grab the link. Note, you can just grab the first() one. Later on
you can see that in the twig template, you can specify the first one with .0
From dirt/web/themes/custom/dirt_bootstrap/dirt_bootstrap.theme
TWIG 514

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 }

And in the template we retrieve the URI with .uri:

1 <p><a class="styled-link ext" href="{{ node.field_sf_contract_ref.entity.field_vendo\


2 r_url.uri }}">Vendor Website</a></p>

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

1 <a href="https://www.duckduckgo.com">Vendor Website</a></p>

From inside-marthe/themes/custom/dp/templates/paragraph/paragraph--sidebar-product-card.html.twig
we wrap some stuff in a link:

1 <a href="{{content.field_link.0['#url']}}" {% if content.field_link.0['#options']['a\


2 ttributes']['target'] %} target="{{content.field_link.0['#options']['attributes']['t
3 arget']}}" {% endif %} class="button">{{content.field_link.0['#title']}}
4 {{ content.field_image }}
5 <h2 class="module-header">{{content.field_text}}</h2>
6 {{content.field_text2}}
7 </a>

And from txg/web/themes/custom/txg/templates/content/node--event--card.html.twig if


there is a url, display the link with the url, otherwise just display the title for the link. I’m not 100%
sure this is really valid. Can you put in a title and no 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 %}

31.2.23: Render an internal link programatically


Here we want to render an internal link to a page on our Drupal site (as opposed to a link to another
site.) We grab the link in a preprocess function. Extract out the title and the URI.
TWIG 515

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 }

We can put the pieces in the twig template like this

1 <a href="{{ order_type_link }}">{{ order_link_title }}</a>

31.2.24: Render an image with an image style


From inside-marthe/themes/custom/dp/templates/paragraph/paragraph--sidebar-resource.html.twig
Here we use sidebar_standard image style

1 <aside class="module module--featured" data-interchange="[{{ content.field_image.0['\


2 #item'].entity.uri.value | image_style('sidebar_standard') }}, small]">

Or for a media field, set the image style on the display mode and use this:

1 {{ content.field_banner_image.0 }}

31.2.25: Hide if there is no content in a field or image


From inside-marthe/themes/custom/dp/templates/content/node--video-detail.html.twig I check to
see if there are any values in this array related_lessons_nids and display the view.
TWIG 516

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_myfield is not empty %}


2 {# Do something here #}
3 {% endif %}

31.2.26: Hide if there is no image present


If there is an image (and it is renderable) display the image

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

1 <article{{ attributes.addClass(classes).setAttribute('my-name', 'Selwyn') }}>

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>

More useful examples at https://www.drupal.org/docs/8/theming-drupal-8/using-attributes-in-


templates such as:

1 {% set classes = ['red', 'green', 'blue'] %}


2 {% set my_id = 'specific-id' %}
3 {% set image_src = 'https://www.drupal.org/files/powered-blue-135x42.png' %}
4
5 <img{{ attributes.addClass(classes).removeClass('green').setAttribute('id', my_id).s\
6 etAttribute('src', image_src) }}>

Which outputs the following:

1 <img
2 id="specific-id"
3 class="red blue"
4 src="https://www.drupal.org/files/powered-blue-135x42.png"
5 />

Check if an attribute has a class

1 {{ attributes.hasClass($class) }}

Remove an attribute
TWIG 518

1 {{ attributes.removeAttribute() }}

Convert attributes to array

1 {{ attributes.toArray () }}

31.2.28: Output the content but leave off the field_image


From very/web/themes/very/templates/node--teaser.html.twig:

1 <div{{ content_attributes.addClass('content') }}>


2 {{ content|without('field_image')|render|striptags }}
3 </div>

31.2.29: Add a class


1 <div{{ content_attributes.addClass('node__content') }}>

31.2.30: Add a class conditionally


From very/web/themes/very/templates/node--teaser.html.twig
For an unpublished node, wrap this class around the word unpublished

1 {% if not node.published %}
2 <p class="node--unpublished">{{ 'Unpublished'|t }}</p>
3 {% endif %}

31.2.31: Links to other pages on site


Absolute:

1 <a href="{{ url('entity.node.canonical', {node: 3223}) }}">Link to WEA node 3223 </a>

Relative (see path vs url):


TWIG 519

1 <a href="{{ path('entity.node.canonical', {node: 3223}) }}">Link to WEA node 3223 </\
2 a>

Could also link to users using

1 <a href="{{ url('entity.user.canonical', {user: 1}) }}">Link to user 1 </a>

31.2.32: Loop.index in a paragraph twig template


From: web/themes/custom/dprime/templates/field/field--paragraph--field-links--sidebar-cta.html.twig
Notice the use of loop.index to only output this for the first item

1 {% for item in items %}


2 {% if loop.index == 1 %}
3 <div class="cell medium-6">
4 <a href="{{item.content['#url']}}" class="button {% if loop.index == 2 %}hollo\
5 w {% endif %}button--light m-b-0"{% if item.content['#options']['attributes']['targe
6 t'] %} target="{{item.content['#options']['attributes']['target']}}" {% endif %}>{{i
7 tem.content['#title']}}</a>
8 </div>
9 {% endif %}
10 {% endfor %}

31.2.33: Loop thru an array of items with a separator


This loads all the authors and adds and between them except for the last one:

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>

This version inserts commas:


From org/docroot/themes/custom/org/templates/field/field--node--field-categories--opinion.html.twig
TWIG 520

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.3: Add Javascript into a twig template


1 <script>
2 function hidePRSnippet(config, data) {
3 if (data.average_rating < 3.5 && data.average_rating !== 0) {
4 document.getElementById('pr-reviewsnippet').remove();
5 }
6 }
7
8 var powerReviewsConfig = {{ accPowerreviews | raw}};
9 for (var i = 0; i < powerReviewsConfig.length; i++) {
10 if (powerReviewsConfig[i].hasOwnProperty('components') && powerReviewsConfig[i].\
11 components.hasOwnProperty('ReviewSnippet')) {
12 powerReviewsConfig[i]['on_render'] = hidePRSnippet;
13 }
14 }
15 POWERREVIEWS.display.render(powerReviewsConfig);
16 </script>

31.4: Control/Logic

31.4.1: Concatenate values into a string with join


This would typically be used when passing a series of node id’s to a view to filter its output.
TWIG 521

1 {% set blah = [node.field_ref_unit.0.target_id,node.field_ref_unit.1.target_id,node.\


2 field_ref_unit.2.target_id,node.field_ref_unit.3.target_id]|join('+') %}

This produces 1+2+3+4

31.4.2: Include partial templates


1 {% include '@txg/partials/searchfilterform.html.twig' %}

31.4.3: Loop through entity reference items


In txg/web/themes/custom/txg/templates/content/node--news-story.html.twig I need to loop
through a bunch of entity reference values and build a string of id+id+id… (with an undefined
number) so

1 {% set blah = '' %}


2 {% for item in node.field_ref_unit %}
3 {% set blah = blah ~ item.target_id %}
4 {% if not loop.last %}
5 {% set blah = blah ~ '+' %}
6 {% endif %}
7 {% endfor %}
8
9 <div>blah:{{ blah }}</div>
10 <div>node id: {{ node.id }}</div>
11 {{ drupal_view('related_news_for_news_story', 'block_unit', node.id, blah) }}

31.4.4: IF OR
If there is a value in field_event_date or field_display_date, then display it/them.

1 <div{{ content_attributes.addClass('teaser__content') }}>


2 {% if content.field_event_date or content.field_display_date %}
3 <div class="teaser__date">
4 {{ content.field_event_date|render|striptags }}
5 {{ content.field_display_date|render|striptags }}
6 </div>
7 {% endif %}

31.4.5: Test if a formatted text field is empty


To check a body field or other formatted text field, use |render to render it first.
TWIG 522

1 {% if content.body|render %}
2 <li><a class="scroll" href="#section-overview">Overview</a></li>
3 {% endif %}

31.4.6: Test empty variable


This code checks if a variable is empty using empty10 test if the attributes variable is not set. From
https://www.drupal.org/project/drupal/issues/2558079:

1 {% if attributes is empty %}
2 {{ link(item.title, item.url) }}
3 {% else %}
4 {{ link(item.title, item.url, attributes) }}
5 {% endif %}

You can also use:

1 {% if blah is not empty %}


2 {{content.name}}
3 {% endif %}

31.4.7: Conditionals (empty, defined, even)


1 {% if rows %}
2 {{rows}}
3 {% elseif empty %}
4 {{ empty }}
5 {% endif %}
6
7 {% if var is defined %}
8 {{ content.name}}
9 {% endif %}
10
11 {%if var is even %}
12 {{ content.name}}
13 {% endif %}

e.g. from inside-marthe/themes/custom/dp/templates/paragraph/paragraph--highlight-card.html.twig

10 https://twig.symfony.com/doc/3.x/tests/empty.html
TWIG 523

1 {% set showCat = TRUE %}


2 {% if view_mode == 'overview' or view_mode == 'home' %}
3 {% set showCat = FALSE %}
4 {% endif %}
5 <div class="cell medium-4 home-highlight-card m-b-3" data-equalizer-watch>
6 <a href="{% spaceless %}{{ content.field_link.0 }}{% endspaceless %}" class="card \
7 h-100" data-interchange="[{{ content.field_image.0['#item'].entity.uri.value | image
8 _style('highlight_card_standard') }}, small]" {% if content.field_link.0['#options']
9 ['attributes']['target'] %} target="{{content.field_link.0['#options']['attributes']
10 ['target']}}"{% endif %}>
11 <div class="card-content">
12 {% if showCat %}
13 <span class="card-label">{{content.field_text2}}</span>
14 {% endif %}
15 <h3 class="card-header">{{content.field_text}}</h3>
16 {{content.field_description}}
17 {% if content.field_button_text|render %}
18 <span class="button full-width show-on-hover">{{ content.field_button_text }\
19 }</span>
20 {% else %}
21 <span class="button full-width show-on-hover">{{ content.field_text|render }\
22 }</span>
23 {% endif %}
24 </div>
25 </a>
26 </div>

31.4.8: Test if a paragraph is empty using striptags


From /inside-marthe/themes/custom/dp/templates/content/node--video-collection.html.twig:
Normally you wouldn’t need the striptags, but when twig debugging is enabled, the render infor-
mation includes debug tags. See https://www.drupal.org/project/drupal/issues/2547559#comment-
12103048

1 {% if content.field_related_lessons|render|striptags|trim is not empty %}


2 {{ content.field_related_lessons}}
3 {% endif %}

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 %}

31.4.9: Comparing strings


For complicated strings, you have to use the Twig same as11 function because using if x == y
doesn’t work. See the commented out part where I tried ==:

1 {% set start = node.field_when.0.value|date('l F j, Y') %}


2 {% set end = node.field_when.0.end_value|date('l F j, Y') %}
3 <p class="date"> {{ start }}</p>
4 {#{% if not start == end %}#}
5 {% if not start is same as(end) %}
6 <p class="date"> {{ end }}</p>
7 {% endif %}

31.4.10: Include other templates as partials


In very/web/themes/very/templates/node--featured.html.twig
You can re-use templates. Just put them in the partials directory (you don’t have to but it is a good
convention) and include them.

1 {{ include('node--teaser.html.twig') }}

31.4.11: Check if an attribute has a class


1 {{ attributes.hasClass($class) }}

31.4.12: Remove an attribute


1 {{ attributes.removeAttribute() }}

31.4.13: Convert attributes to array

11 https://twig.symfony.com/doc/3.x/tests/sameas.html
TWIG 525

1 {{ attributes.toArray () }}

31.5: Views

31.5.1: Render a view with contextual filter


Pro tip: Create embed displays (rather than blocks or pages) so users don’t see these
blocks appearing in the block management page. see https://drupal.stackexchange.com/
questions/287209/what-does-the-embed-display-type-do

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.

1 {{ drupal_view('map_data_for_a_country', 'block_stats', node.field_iso_n3_country_co\


2 de.0.value) }}

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

1 {{ drupal_view('resellers_for_this_vendor', 'embed_1', node.field_vendor_id.value ) \


2 }}

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.

31.5.2: Count how many rows returned from a view


https://www.drupal.org/docs/8/modules/twig-tweak/twig-tweak-and-views
Check if View has Results
12 https://www.drupal.org/project/twig_tweak
TWIG 526

1 {% set view = drupal_view_result('related', 'block_1')|length %}


2 {% if view > 0 %}
3 {{ drupal_view('related', 'block_1') }}
4 {% endif %}

31.5.3: If view results empty, show a different view


In txg/web/themes/custom/txg/templates/content/node--news-story.html.twig we show units
(the first view) but if there aren’t any, show aofs (the second view.)

1 {% if drupal_view_result('related_news_for_news_story', 'block_unit', node.id, unit_\


2 ids) %}
3 {{ drupal_view('related_news_for_news_story', 'block_unit', node.id, unit_ids) }}
4 {% elseif drupal_view_result('related_news_aof', 'block_aof', node.id, aof_ids) %}
5 {{ drupal_view('related_news_aof', 'block_aof', node.id, aof_ids) }}
6 {% endif %}

31.5.4: Selectively pass 1 termid or 2 to a view as the contextual


filter
In the view, you can allow multiple terms for a contextual filter
From: https://drupal.stackexchange.com/questions/78701/views-multiple-contextual-filters-
taxonomy:
Instead of Content: The name of Taxonomy (taxonomy_vocabulary_#) you need to select Content:
Has taxonomy term ID Contextual filter and enable Allow multiple values to able to use multiple
values in the form of 1+2+3 (for OR) or 1,2,3 (for AND).
Then in the template, check if there is a second value and build the arguments in the form id+id (e.g.
“13+16”) In this example, I have to assume the setup allows only 2 taxonomy terms to be entered.
See below for an unlimited amount of terms.
From /Users/selwyn/Sites/dirt/web/themes/custom/dirt_bootstrap/templates/paragraphs/paragraph--upcom
TWIG 527

1 {# if there is a second category, pass it separated by + #}


2 {% if paragraph.field_ref_tax.1.target_id %}
3 {% set args = paragraph.field_ref_tax.1.target_id~'+'~paragraph.field_ref_tax.1.ta\
4 rget_id %}
5 args: {{ args }}
6 {{ drupal_view('events', 'embed_2', paragraph.field_ref_tax.0.target_id~'+'~paragr\
7 aph.field_ref_tax.1.target_id) }}
8 {% else %}
9 {{ drupal_view('events', 'embed_2', paragraph.field_ref_tax.0.target_id) }}
10 {% endif %}

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 {# Figure out parameters to pass to view for news items #}


2 {% set params = '' %}
3 {% for item in paragraph.field_ref_tax_two %}
4 {% set params = params ~ item.target_id %}
5 {% if not loop.last %}
6 {% set params = params ~ '+' %}
7 {% endif %}
8 {% endfor %}
9 params: {{ params }}

This will output something like: 5+6+19


And pass the output to a view like this:

1 {{ drupal_view('news', 'embed_2', params) }}

31.5.5: Views templates


Using machine names for the view, you can copy the base view templates (just like in Drupal 7) to
make specific templates.
From https://www.drupal.org/docs/theming-drupal/twig-in-drupal/twig-template-naming-
conventions:
Using a View named foobar with its style: unformatted and row style: Fields, and using display:Page.
TWIG 528

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

or for views-view-list.html.twig, you could use views-view-list---foobar.html.twig


e.g. /Users/selwyn/Sites/dirt/web/themes/custom/dirt_bootstrap/templates/views/views-view-list--resou

31.5.6: Inject variables


You can inject variables into a view using hook_preprocess_views_view() eg. from
txg/web/themes/custom/txg/txg.theme. The code below used to was load up various items
to populate the select dropdown controls in the view:

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

1 <!-- BEGIN OUTPUT from 'core/themes/classy/templates/views/views-view.html.twig' -->

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 }

31.5.6.1: Same field used twice

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 }

31.5.7: Concatenate values into a string with join


This would typically be used when passing a series of node id’s to a view to filter its output.

1 {% set blah = [node.field_ref_unit.0.target_id,node.field_ref_unit.1.target_id,node.\


2 field_ref_unit.2.target_id,node.field_ref_unit.3.target_id]|join('+') %}

This produces 1+2+3+4

31.5.8: Loop through entity reference items


In txg/web/themes/custom/txg/templates/content/node--news-story.html.twig I need to loop
through a bunch of entity reference values and build a string of id+id+id… (with an undefined
number) so:
TWIG 535

1 {% set blah = '' %}


2 {% for item in node.field_ref_unit %}
3 {% set blah = blah ~ item.target_id %}
4 {% if not loop.last %}
5 {% set blah = blah ~ '+' %}
6 {% endif %}
7 {% endfor %}
8
9 <div>blah:{{ blah }}</div>
10 <div>node id: {{ node.id }}</div>
11 {{ drupal_view('related_news_for_news_story', 'block_unit', node.id, blah) }}

31.6: Twig filters and functions


Use these to do almost anything with variables in your twig templates.
See cheat sheet at https://www.drupal.org/docs/contributed-modules/twig-tweak/cheat-sheet
-view-filter15 also https://twig.symfony.com/doc/3.x/ for filters and functions
Here are some examples. A complete list is included below:

1 {{ <span>Hello I am an html twig, but my html will be stripped</span> | striptags }}


2
3 {{'welcome' | upper }}
4
5 {{ data | json_encode() }}
6
7 {%filter upper %}
8 This test becomes uppercase
9 {% endfilter %}

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: Twig Tweak


This is an essential module to add to all projects.

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

31.7.2: Display a block with twig_tweak


Here is a simple example:

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()));"

It outputs something like:

92 https://git.drupalcode.org/project/twig_tweak/-/blob/3.x/src/TwigTweakExtension.php
TWIG 540

31.7.3: Display filter form block


You can then use this to display your ajax exposed filter form block

1 {{ drupal_block('views_exposed_filter_block:news_listing_for_news_landing-page_1\
2 ') }}

31.7.4: Embed view in twig template


In inside-marthe/themes/custom/dprime/templates/content/node-overview.html.twig there is
a view rendered in the twig template. This requires the twig tweak93 module:

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.

1 {{ drupal_view('who_s_new', 'block_1', arg_1, arg_2, arg_3) }}

31.7.5: Some tricky quotes magic


Here I am trying to create a string type="aof" so I had to escape at least one of the quotes like this
\” (backslash and double quote)

1 {% set office_type = 'type=\"' ~ item.type ~ '"' %}

The entire piece of debug code is reproduced below:

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>

The real implementation is shown below:


From txg/web/themes/custom/txg/templates/partials/searchfilterform.html.twig
See the line below that sets office_type = …

1 {% for item in filter.info %}


2 {% set selected = '' %}
3 {% if item.selected is defined and item.selected %}
4 {% set selected = 'selected' %}
5 {% endif %}
6 {% set officetype = '' %}
7 {% if item.type is defined and item.type %}
8 {% set office_type = 'type=\"' ~ item.type ~ '"' %}
9 {% endif %}
10 <option value="/search-news?{{ filter.type }}={{ item.value }} {{ office_type }}" \
11 {{ selected }}>{{ item.title }}</option>
12 {% endfor %}

31.8: Troubleshooting

31.8.1: Enable Twig debugging and disable caches


This will cause twig debugging information to be displayed in the HTML code like the following:
TWIG 542

1 <!-- THEME DEBUG -->


2 <!-- THEME HOOK: 'toolbar' -->
3 <!-- BEGIN OUTPUT from 'core/themes/stable/templates/navigation/toolbar.html.twig' -\
4 ->

and

1 <!-- THEME DEBUG -->


2 <!-- THEME HOOK: 'page' -->
3 <!-- FILE NAME SUGGESTIONS:
4 * page--teks--admin--srp--program--expectation--correlation--vote-all.html.twig
5 * page--teks--admin--srp--program--expectation--correlation--852136.html.twig
6 * page--teks--admin--srp--program--expectation--correlation--%.html.twig
7 * page--teks--admin--srp--program--expectation--correlation.html.twig
8 * page--teks--admin--srp--program--expectation--852131.html.twig
9 * page--teks--admin--srp--program--expectation--%.html.twig
10 * page--teks--admin--srp--program--expectation.html.twig
11 * page--teks--admin--srp--program--852061.html.twig
12 * page--teks--admin--srp--program--%.html.twig
13 * page--teks--admin--srp--program.html.twig
14 * page--teks--admin--srp.html.twig
15 x page--teks--admin.html.twig
16 * page--teks.html.twig
17 * page.html.twig
18 -->

In sites/default/development.services.yml in the parameters, twig.config, set debug:true. See


core.services.yml for lots of other items to change for development.

1 # Local development services.


2 #
3 parameters:
4 http.response.debug_cacheability_headers: true
5 twig.config:
6 debug: true
7 auto_reload: true
8 cache: false
9
10 # To disable caching, you need this and a few other items
11 services:
12 cache.backend.null:
13 class: Drupal\Core\Cache\NullBackendFactory

You also need this in settings.local.php:


TWIG 543

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';

31.8.2: Debugging - Dump a variable


When troubleshooting or trying to make sense of what is being output, use dump.

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

31.8.3: Dump taxonomy reference field


Here we dump a taxonomy reference field which is useful for debugging purposes. The pre tags
format it a little nicer than if we don’t have them.
TWIG 544

1 <pre>
2 {{ dump(paragraph.field_ref_tax.value) }}
3 </pre>

And get ouput:

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 }

31.8.4: Using kint or dump to display variable in a template


With devel and devel: kint enabled, you can display variables in templates. Here we show the
content variable from the above block template. Note. There is also a built in dump() function which
is super useful.

1 {{ kint(content) }}

You can also

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>

Or the body field:

1 {{ kint(content['body']) }}

Or the tags field content[‘field_tags’]

1 {{ kint(content['field_tags']) }}

31.8.5: 502 bad gateway error


While working on Twig changes, if you ever see a 502 bad gateway error, try commenting out the
twig template code you just added and see if it displays. I know, it’s not a friendly error at all!
TWIG 546

31.8.6: Views error


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.

31.8.7: Striptags (when twig debug info causes if to fail)


When you care about the output being affected by twig debugging, you need to use striptags. In
this case, because I enabled twig debugging, the content.field_landing_opinion_page_type was not
ever 'ORD’
So here I compare a field value so I have to use striptags to remove all html. I ended up using the
combination of render|striptags|trim:

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

You might also like