Table of Contents for
Drupal 8 Module Development

Version ebook / Retour

Cover image for bash Cookbook, 2nd Edition Drupal 8 Module Development by Daniel Sipos Published by Packt Publishing, 2017
  1. Drupal 8 Module Development
  2. Title Page
  3. Copyright
  4. Drupal 8 Module Development
  5. Credits
  6. About the Author
  7. About the Reviewers
  8. www.PacktPub.com
  9. Why subscribe?
  10. Customer Feedback
  11. Table of Contents
  12. Preface
  13. What this book covers
  14. What you need for this book
  15. Who this book is for
  16. Conventions
  17. Reader feedback
  18. Customer support
  19. Downloading the example code
  20. Downloading the color images of this book 
  21. Errata
  22. Piracy
  23. Questions
  24. Developing for Drupal 8
  25. Introducing Drupal (for developers)
  26. Developing for Drupal 8
  27. Technologies that drive Drupal
  28. PHP
  29. Databases and MySQL
  30. The web server
  31. HTML, CSS, and JavaScript
  32. Drupal architecture
  33. Drupal core, modules, and themes
  34. Hooks, plugins, and events
  35. Services and the dependency injection container
  36. From request to response
  37. Drupal's major subsystems
  38. Routing
  39. Entities
  40. Fields
  41. Menus
  42. Views
  43. Forms
  44. Configuration
  45. Plugins
  46. The theme system
  47. Caching
  48. Other subsystems
  49. Tools for developing in Drupal
  50. Version control
  51. Composer
  52. The API site and coding standards
  53. The developer (Devel) module
  54. Drush (the Drupal shell)
  55. Drupal Console
  56. Developer settings
  57. Summary
  58. Creating Your First Module
  59. Creating a module
  60. Your first hook implementation
  61. Route and controller
  62. The route
  63. Route variables
  64. Namespaces
  65. The Controller
  66. Services
  67. What is a service?
  68. The HelloWorldSalutation service
  69. Tagged services
  70. Using services in Drupal 8
  71. Injecting the service into our Controller
  72. The form
  73. Altering forms
  74. Custom submit handlers
  75. Rendering forms
  76. Service dependencies
  77. Blocks
  78. Our first block plugin
  79. Block configuration
  80. Working with links
  81. The URL
  82. The link
  83. Which way to link?
  84. Event Dispatcher and redirects
  85. Redirecting from a Controller
  86. Redirecting from a subscriber
  87. Dispatching events
  88. Summary
  89. Logging and Mailing
  90. Logging
  91. The Drupal 8 logging theory
  92. Our own logger channel
  93. Our own logger
  94. Logging for Hello World
  95. Logging summary
  96. Mail API
  97. The theory of the Mail API
  98. Implementing hook_mail()
  99. Sending emails
  100. Altering someone else's emails
  101. Custom mail plugins
  102. The mail plugin
  103. Using mail plugins
  104. Tokens
  105. The Token API
  106. Using tokens
  107. Defining new tokens
  108. Token summary
  109. Summary
  110. Theming
  111. Business logic versus presentation logic
  112. Twig
  113. Theme hooks
  114. Theme hook suggestions
  115. Render arrays
  116. The structure of a render array
  117. #type
  118. #theme
  119. #markup
  120. The render pipeline
  121. Assets and libraries
  122. Libraries
  123. Attaching libraries
  124. Common theme hooks
  125. Lists
  126. Links
  127. Tables
  128. Attributes
  129. Theming our Hello World module
  130. Summary
  131. Menus and Menu Links
  132. The menu system
  133. Menus
  134. Menu links
  135. Multiple types of menu links
  136. Local tasks
  137. Local actions
  138. Contextual links
  139. MenuLink trees
  140. Menu link tree manipulators
  141. Menu active trail
  142. Rendering menus
  143. Working with menu links
  144. Defining menu links
  145. Working with menu links
  146. Defining local tasks
  147. Defining local actions
  148. Defining contextual links
  149. Summary
  150. Data Modeling and Storage
  151. Different types of data storage
  152. State API
  153. Tempstore
  154. PrivateTempStore
  155. A note about anonymous users
  156. SharedTempStore
  157. Tempstore conclusion
  158. UserData
  159. Configuration
  160. Introduction
  161. What is configuration used for?
  162. Managing configuration
  163. Different types of configuration
  164. Configuration storage
  165. Schema
  166. Overrides
  167. Global overrides
  168. Module overrides
  169. Language overrides
  170. Priority
  171. Interacting with simple configuration
  172. Entities
  173. Content versus configuration entity types
  174. Entity type plugins
  175. Identifiers
  176. Bundles
  177. Database tables
  178. Entity keys
  179. Links
  180. Entity translation
  181. Entity revisions
  182. Configuration export
  183. Handlers
  184. Fields
  185. Configuration entity fields
  186. Content entity fields
  187. Base fields
  188. Configurable fields
  189. Field storage
  190. Entity types summary
  191. TypedData
  192. Why?
  193. What?
  194. The low-level API
  195. DataType plugins
  196. Data definitions
  197. Content entities
  198. TypedData summary
  199. Interacting with the Entity API
  200. Querying and loading entities
  201. Building queries
  202. Loading entities
  203. Reading entities
  204. Manipulating entities
  205. Creating entities
  206. Rendering content entities
  207. Pseudo-fields
  208. Content entity validation
  209. Validation summary
  210. Summary
  211. Your Own Custom Entity and Plugin Types
  212. Custom content entity type
  213. Custom plugin type
  214. Custom configuration entity type
  215. The Importer plugin
  216. Content entity bundles
  217. Drush command
  218. Summary
  219. The Database API
  220. The Schema API
  221. Running queries
  222. Select queries
  223. Handling the result
  224. More complex select queries
  225. Range queries
  226. Pagers
  227. Insert queries
  228. Update queries
  229. Delete queries
  230. Transactions
  231. Query alters
  232. Update hooks
  233. Summary
  234. Custom Fields
  235. Field type
  236. Field widget
  237. Field formatter
  238. Field settings
  239. Using as a base field
  240. Summary
  241. Access Control
  242. Introduction to the Drupal access system
  243. Roles and permissions under the hood
  244. Defining permissions
  245. Checking the user credentials
  246. Route access
  247. Custom route access
  248. Static approach
  249. Service approach
  250. Programmatically checking access on routes
  251. Bonus - dynamic route options for access control
  252. CSRF protection on routes
  253. Altering routes
  254. Entity access
  255. Injecting services into Entity handlers
  256. Entity access hooks
  257. Field access
  258. Entity access in routes
  259. Node access grants
  260. Block access
  261. Summary
  262. Caching
  263. Introduction
  264. Cacheability metadata
  265. Cache tags
  266. Cache contexts
  267. Max-age
  268. Using the cache metadata
  269. Caching in block plugins
  270. Caching access results
  271. Placeholders and lazy building
  272. Lazy builders
  273. Using the Cache API
  274. Creating our own cache bin
  275. Summary
  276. JavaScript and the Ajax API
  277. JavaScript in Drupal
  278. Drupal behaviors
  279. Our library
  280. The JavaScript
  281. Drupal settings
  282. Ajax API
  283. Ajax links
  284. Ajax in forms
  285. States (Form) system
  286. Summary
  287. Internationalization and Languages
  288. Introduction
  289. Language
  290. Content Translation
  291. Configuration Translation
  292. Interface Translation
  293. Internationalization
  294. Content entities and the Translation API
  295. Summary
  296. Batches, Queues, and Cron
  297. Batch powered update hooks
  298. Batch operations
  299. Creating the batch
  300. Batch operations
  301. Cron
  302. Queues
  303. Introduction to the Queue API
  304. Cron based queue
  305. Processing a queue programmatically
  306. Lock API
  307. Summary
  308. Views
  309. Entities in Views
  310. Exposing custom data to Views
  311. Views data
  312. Views fields
  313. Views relationships
  314. Views sorts and filters
  315. Views arguments
  316. Altering Views data
  317. Custom Views field
  318. Field configuration
  319. Custom Views filter
  320. Custom Views argument
  321. Views theming
  322. Views hooks
  323. Summary
  324. Working with Files and Images
  325. The filesystem
  326. Stream wrappers
  327. Managed versus unmanaged files
  328. Using the File and Image fields
  329. Working with managed files
  330. Attaching managed files to entities
  331. Helpful functions for dealing with managed files
  332. Managed file uploads
  333. Managed file form element
  334. Entity CRUD hooks
  335. Managed file usage service
  336. Processing the CSV file
  337. Our own stream wrapper
  338. Working with unmanaged files
  339. Private file system
  340. Images
  341. Image toolkits
  342. Image styles
  343. Rendering images
  344. Summary
  345. Automated Testing
  346. Testing methodologies in Drupal 8
  347. PHPUnit
  348. Registering tests
  349. Unit tests
  350. Mocked dependencies
  351. Kernel tests
  352. TeamCleaner test
  353. CsvImporter test
  354. Functional tests
  355. Configuration for functional tests
  356. Hello World page test
  357. Hello World form test
  358. Functional JavaScript tests
  359. Time test
  360. CsvImporter test
  361. Summary
  362. Drupal 8 Security
  363. Cross-Site Scripting (XSS)
  364. Sanitization methods in Drupal 8
  365. Double escaping
  366. SQL Injection
  367. Cross-Site Request Forgery (CSRF)
  368. Summary

Ajax in forms

The most common use of Ajax in Drupal is through the Form API, where we can create dynamic interactions between the server and client with ease. To demonstrate how this works, we will go through an example. This will be a rework of the Importer configuration entity form we created in Chapter 7, Your Own Custom Entity and Plugin Types.

If you remember, we said that tying certain configuration values to the generic entity does not make sense, as importer plugins might be different. The first Importer we wrote loads a JSON file from a remote URL. So, it stands to reason that the configuration value for the URL is tied to the plugin and not the configuration entity (even if the latter actually stores it). Because if we want to create a CSV importer, for example, we don't need the URL. So, let's refactor our work to make this happen.

Here is an outline of the steps we need to do for this refactoring:

  • Importer plugins need to provide their own configuration form elements
  • The Importer configuration form needs to read these elements depending on which plugin is selected (this is where the Ajax API comes into play)
  • We need to alter the storage and configuration schema of the values that are specific to plugins

Let's start by giving the plugin ImporterInterface and new method:

  /**
   * Returns the form array for configuring this plugin.
   *
   * @param \Drupal\products\Entity\ImporterInterface $importer
   *
   * @return array
   */
  public function getConfigurationForm(\Drupal\products\Entity\ImporterInterface $importer);

This is responsible for getting the form elements needed for this plugin. As an argument, it receives the Importer configuration entity, which can be inspected for default values.

Next, on the ImporterInterface of the configuration entity, we need to remove the getUrl() method (since that is specific to the JsonImporter plugin) and replace it with a generic method for retrieving all the configuration values pertaining to the plugin selected for the entity:

/**
 * Returns the configuration specific to the chosen plugin.
 *
 * @return array
 */
public function getPluginConfiguration();

And of course, in the Importer entity class, we reflect this change as well:

/**
 * The configuration specific to the plugin.
 *
 * @var array
 */
protected $plugin_configuration;

And the actual getter method with respect to the interface:

/**
 * {@inheritdoc}
 */
public function getPluginConfiguration() {
  return $this->plugin_configuration;
}

So far so good, nothing complicated going on. We are replacing the plugin specific configuration values with a generic one in which values specific to the selected plugin will be stored.

Now, let's turn to the ImporterForm and make all the adjustments there. But before we do that, let's move the form element for the url field into the JsonImporter, where we have to implement the new getConfigurationForm() method:

/**
 * {@inheritdoc}
 */
public function getConfigurationForm(\Drupal\products\Entity\ImporterInterface $importer) {
  $form = [];
  $config = $importer->getPluginConfiguration();
  $form['url'] = [
    '#type' => 'url',
    '#default_value' => isset($config['url']) ? $config['url'] : '',
    '#title' => $this->t('Url'),
    '#description' => $this->t('The URL to the import resource'),
    '#required' => TRUE,
  ];
  return $form;
}

You'll notice some differences in getting the default value. Instead of calling the now removed getUrl() method on the configuration entity, we use the new getPluginConfiguration() method and check inside the resulting array. Also, since we use the $this->t() method to ensure the translation of the strings, we should use the StringTranslationTrait as well (inside the class as it is a Trait):

use StringTranslationTrait;

Let's not forget that we are actually using the URL in the import, so we need to make some adjustments to the getData() method as well:

/**
* Loads the product data from the remote URL.
*
* @return \stdClass
*/
private function getData() {
/** @var ImporterInterface $importer_config */
$importer_config = $this->configuration['config'];
$config = $importer_config->getPluginConfiguration();
$url = isset($config['url']) ? $config['url'] : NULL;
if (!$url) {
return NULL;
}
$request = $this->httpClient->get($url);
$string = $request->getBody();
return json_decode($string);
}

With this, we can adjust our ImporterForm (where we no longer have the form element for the URL field).

There are two main things we need to do:

  • Expose the plugin selection element to Ajax, that is, trigger an Ajax request when the user makes a selection
  • Add the extra elements to the form depending on the chosen plugin

This is what the new plugin element looks like:

$form['plugin'] = [
  '#type' => 'select',
  '#title' => $this->t('Plugin'),
  '#default_value' => $importer->getPluginId(),
  '#options' => $options,
  '#description' => $this->t('The plugin to be used with this importer.'),
  '#required' => TRUE,
  '#empty_option' => $this->t('Please select a plugin'),
  '#ajax' => array(
    'callback' => [$this, 'pluginConfigAjaxCallback'],
    'wrapper' => 'plugin-configuration-wrapper'
  ),
];

There are two noticeable changes--we've added an #empty_option key (to be used as the option shown if the user has not made any choice) and the #ajax key (which we will discuss in a bit more detail).

What we did is pretty simple--we declared a callback method to be triggered when a user makes a change to this form element, and we declared the HTML ID of the element which should be replaced with the result of the Ajax callback. And in the latter case (which is a simple method on the same class), all we have to do is this:

/**
 * Ajax callback for the plugin configuration form elements.
 *
 * @param $form
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *
 * @return array
 */
public function pluginConfigAjaxCallback($form, FormStateInterface $form_state) {
  return $form['plugin_configuration'];
}

We return a form element (which we still have to define). An important lesson here is that Ajax responses in forms can return content as well (in the form of render arrays or even strings), which will be used to replace the HTML found by the ID specified in the wrapper key of the Ajax declaration. Alternatively, an AjaxResponse full of commands can also be returned to do more complex things, as we saw in the previous section.

Before we look at this new plugin_configuration form element, let's look at some of the other options that can be used inside the #ajax array:

  • method: This indicates the jQuery method to use when interacting with the wrapper element (if specified). The default is replaceWith(), but you can also use append(), html(), and others.
  • event: This shows which event should be used to trigger the Ajax call. By default, the form element in question decides that. For example, when selecting an option in a select element or when typing something into a textfield.
  • progress: This defines the indicator to be used while the Ajax request is taking place.
  • url: A URL to trigger the Ajax request in case the callback was not specified. Typically, using the latter is more powerful as the entire $form and $form_state are passed as parameters and can be used in processing.

I recommend you check out the documentation page (https://api.drupal.org/api/drupal/core!core.api.php/group/ajax/8.3.x) for the Ajax API for more information about these options and the other ones that are available.

With that out of the way, we can go back to our form definition and add our missing parts, right after the plugin element:

$form['plugin_configuration'] = [
  '#type' => 'hidden',
  '#prefix' => '<div id="plugin-configuration-wrapper">',
  '#suffix' => '</div>',
];

$plugin_id = NULL;
if ($importer->getPluginId()) {
  $plugin_id = $importer->getPluginId();
}
if ($form_state->getValue('plugin') && $plugin_id !== $form_state->getValue('plugin')) {
  $plugin_id = $form_state->getValue('plugin');
}

if ($plugin_id) {
  /** @var \Drupal\products\Plugin\ImporterInterface $plugin */
  $plugin = $this->importerManager->createInstance($plugin_id, ['config' => $importer]);
  $form['plugin_configuration']['#type'] = 'details';
  $form['plugin_configuration']['#tree'] = TRUE;
  $form['plugin_configuration']['#open'] = TRUE;
  $form['plugin_configuration']['#title'] = $this->t('Plugin configuration for <em>@plugin</em>', ['@plugin' => $plugin->getPluginDefinition()['label']]);
  $form['plugin_configuration']['plugin'] = $plugin->getConfigurationForm($importer);
}

First, we define the plugin_configuration form element as a hidden type. This means it will not be visible to users when the page loads for the first time. However, we do use the #prefix and #suffix options (common practice with the Drupal Form API) to wrap this element with a DIV that has the ID we indicated as the wrapper of our Ajax declaration. So, the goal is to have this element replaced each time an Ajax request is made, that is, each time a plugin is selected.

Next, we try to get the ID of the chosen plugin. First, we load it from the configuration entity in case we are looking at an edit form. However, we also check in the form state to see if one has been selected (and is different than the one in the entity). And if you are wondering how we can have the plugin in the form state, the answer is that after an Ajax call is made (triggered by the user selecting a plugin), the form gets rebuilt. Now, we can see what's in the form state and retrieve the plugin ID that was chosen.

Even more than that, if we get our hands on a plugin ID, we can completely change the plugin_configuration element, which in turn then gets returned by the Ajax callback to be used to replace our wrapper. So to sum up:

  • Page loads for the first time (on a new form). The element is hidden.
  • User selects a plugin. Ajax request is triggered, which rebuilds the form.
  • As the form is rebuilt, we check for the selected plugin and alter the plugin_configuration element to reflect the selected plugin.
  • The Ajax response replaces the old element with the new potentially changed one.

The new plugin_configuration element becomes a details one (a collapsible container for multiple elements), open by default and which has one key called plugin, onto which we add all the elements coming from the plugin. Moreover, we use the #tree property to indicate that when the form is submitted, the values of the elements are sent and stored in a tree that reflects the form element (a multidimensional array, basically). Otherwise, the form state values that are submitted get flattened and we lose their connection to the plugin_configuration element (which is also the Importer configuration entity field name we want to store the data under).

We are almost there. We can already go create an Importer entity, and when to use the JSON Importer, the new fieldset containing the URL field should show up below. But we still have one problem. If we save the form, the URL value will be stored inside an array keyed by plugin, inside the plugin_configuration field. So, we need to clean things up a bit, and we can do so inside the save() method.

Right before saving the entity, we can do this:

$importer->set('plugin_configuration', $importer->getPluginConfiguration()['plugin']);

So, we basically move the values one array up, removing the superfluous plugin level in the array (which was only needed to neatly organise the form tree).

With this, we are done. Well, not really, as we still need to handle the configuration schema aspect. Yes, remember those from Chapter 6, Data Modeling and Storage, and Chapter 7 , Your Own Custom Entity and Plugin Types? We are now going to see how we can work with our own dynamic configuration schema, similar to how we did with the ones needed for the field plugins in Chapter 9, Custom Fields. But why do we need a dynamic configuration schema?

Before this refactoring, we knew the exact the fields on the Importer configuration entity and we could declare the schema for each easily (as we did). However, now plugins can come with their own individual fields, so we need to make sure they can provide their own schema definitions for the respective data. So how can we do this?

First, inside our importer.schema.yml file, we need to remove the url field schema definition as it no longer exists. We replace it, however, with one for the new field we created, namely the plugin_configuration array of values that came from the plugin:

plugin_configuration:
  type: products.importer.plugin.[%parent.plugin]

Here is where things become interesting. We don't know what fields there will be inside, so we instead reference another type (our own). Moreover, the name of the type is dynamic. We have a prefix (products.importer.plugin.) followed by a variable name given by the value of the plugin field of the parent (the main configuration entity). So basically, if a given configuration entity uses the json plugin, the type of schema definition will be products.importer.plugin.json. So now, it's the responsibility of whoever creates new plugins to also provide their own schema definitions for their own fields (like we did in Chapter 9, Custom Fields, when we defined field plugins).

But before that can happen, we need to define this new type we created:

products.importer.plugin.*:
  type: mapping
  label: 'Plugin configuration'

So essentially, our new type extends from mapping and has a simple label. Of course it applies to all that start with that name (hence the wildcard we encountered before).

Now, we can add the schema definition for our single json Importer plugin:

products.importer.plugin.json:
  type: mapping
  label: Plugin configuration for the Json importer plugin
  mapping:
    url:
      type: uri
      label: Uri

As you can see, we now have our first instance of the products.importer.plugin type, which contains the url field which is inside the plugin_configuration field of the configuration entity--reflecting a simple array hierarchy.

But the point of this dynamic declaration is that other modules that define new plugins can now also define their own instances of the products.importer.plugin.* schema definitions to map their own fields. It is no longer the responsibility of the configuration entity (schema) to "guess" what field types are being used on each plugin.

With this, our refactoring is complete. Drupal is well aware of the type of data the configuration entity is saving, even if it is in part relating to external input (the selected plugin). So that means we can create (if we want) another importer plugin which uses a CSV file for the product data. But we'll see how to do that in a later chapter when we talk about file handling.