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

Node access grants

Earlier I warned about the entity access controls we've been talking about not being taken into account during queries (either written by us or Views). This is something to pay attention to. For example, if you make a listing of entities, you will need to ensure that users have access to these entities before printing the results out. The problem here occurs when using the built-in paging capabilities of either the entity query or database API. That's because the pager information will reflect all the query results. So, if you don't print the inaccessible entities, there will be a mismatch between the pager information and visible results.

If you remember, in Chapter 6, Data Modeling and Storage, I mentioned that when it comes to nodes, the entity query takes access into account. If you want to avoid that, you should use the accessCheck(FALSE) method on the query builder. Let's elaborate a bit on this.

First, this method is available on all entity types, not just nodes. However, it is really useful only for those which have defined a status field to denote that entities can be either published or unpublished (or/off, enabled/disabled however you prefer). The query will simply add a condition to that field and only return the ones with the status that equals 1. Also, passing FALSE to this method simply removes that condition.

Second, the Node entity type has a much more powerful built-in access system called access grants. These have been there from previous versions of Drupal, and this is why we have it available in D8 as well. Unfortunately, it is not there for other entity types. However, if you really need it, you could technically write it yourself now that you know how the entity access system works in general, and can look into how the node access grants are built. But what is this system about?

The node access grants system is a granular way by which we can control access to any of the operations on a node. This is done using a combination of realms and grants. When a node is saved, we have the opportunity to create access records for that node that contain the following information:

  • realm (string): A category for our access records. Typically, this is used to denote specific functionality under which the access control happens.
  • gid (grant ID) (int): The ID of the grant by which we can verify the user trying to access the node. Typically, this will map to either a role or a custom-defined "group" that users belong to. For example, a manager user type (from the earlier example) can map to the grant ID 1. You'll understand this in a moment.
  • grant_view, grant_update, grant_delete (int): Boolean indicating whether this access record is for this operation.
  • langcode (string): The language of the node this access record should apply to.

Then, we can return grant records for a given user when they try to access the node. For a given user, we can return multiple grants as part of multiple realms.

The node access records get stored inside the node_access table, and it's a good idea to keep checking that table while you are developing and preparing your access records. By default, if there are no modules that provide access records, there will be only one row in that table referencing the Node ID "0" and the realm all. This means that basically the node access grants system is not used, and all nodes are accessible for viewing in all realms. That is to say, default access rules apply. Once a module creates records, as we will see, this row is deleted.

To better understand how this system works, let's see a practical code example. For this, we'll get back to our User Types module and create some node access restrictions based on these user types. We'll start with an easy example and then expand on it to make it more complex (and more useful).

To begin with, we want to make sure that Article nodes are only viewable by users of all three types (so there are still some restrictions, as users need to have a type). Page nodes, on the other hand, are restricted to managers and board members. So let's get it done.

All the work we do now takes place inside the .module file of the module. First, let's create a rudimentary mapping function to which we can provide a user type string (as we've seen before) and which returns a corresponding grant ID. We will then use this consistently to get the grant ID of a given user type:

/**
 * Returns the access grant ID for a given user type.
 *
 * @param $type
 *
 * @return int
 */
function user_types_grant_mapping($type) {
  $map = [
    'employee' => 1,
    'manager' => 2,
    'board_member' => 3
  ];

  if (!isset($map[$type])) {
    throw new InvalidArgumentException('Wrong user type provided');
  }

  return $map[$type];
}

It's nothing too complicated. We have our three user types that map to simple integers. Also, we throw an exception if a wrong user type is passed--now comes the fun part.

Working with node access grants restrictions involves the implementation of two hooks--one for creating the access records of the nodes, and the other to provide the grants of the current user. Let's first implement hook_node_access_records():

/**
 * Implements hook_node_access_records().
 */
function user_types_node_access_records(\Drupal\node\NodeInterface $node) {
  $bundles = ['article', 'page'];
  if (!in_array($node->bundle(), $bundles)) {
    return [];
  }

  $map = [
    'article' => [
      'employee',
      'manager',
      'board_member',
    ],
    'page' => [
      'manager',
      'board_member'
    ]
  ];

  $user_types = $map[$node->bundle()];
  $grants = [];

  foreach ($user_types as $user_type) {
    $grants[] = [
      'realm' => 'user_type',
      'gid' => user_types_grant_mapping($user_type),
      'grant_view' => 1,
      'grant_update' => 0,
      'grant_delete' => 0,
    ];
  }

  return $grants;
}

This hook is invoked whenever a node is being saved, and it needs to return an array of access records for that node. As expected, the parameter is the node entity.

The first thing we do is simply return an empty array if the node is not one of the ones we are interested in. If we return no access records, this node will be given one single record for the realm all with the grant ID of 1 for the view operation. This means that it is accessible in accordance to the default node access rules.

Then, we will create a simple map of the user types we want viewing our node bundles. Also, for each user type that corresponds to the current bundle, we create an access record for the user_type realm with the grant ID that maps to that user type, and with permission to view this node.

There are two ways we can trigger this hook and persist the access records. We can edit and save a node, which will create the records for that node, or we can rebuild the permissions that will do so for all the nodes on the site. The link to do so can be found on the status report page:

It's a good idea to rebuild the permissions while developing to make sure that your changes get applied to all the nodes. Once we do this, our nodes now become inaccessible to basically anyone (except the super user with the ID of 1). That's because we now need to specify the grants a given user should have by implementing hook_node_grants():

/**
 * Implements hook_node_grants().
 */
function user_types_node_grants(\Drupal\Core\Session\AccountInterface $account, $op) {
  if ($account->isAnonymous()) {
    return [];
  }

  if ($op !== 'view') {
    return [];
  }

  $user = \Drupal::entityTypeManager()->getStorage('user')->load($account->id());
  $user_type = $user->get('field_user_type')->value;
  if (!$user_type) {
    return [];
  }

  try {
    $gid = user_types_grant_mapping($user_type);
  }
  catch (InvalidArgumentException $e) {
    return [];
  }

  return ['user_type' => [$gid]];
}

This hook is invoked by the node access system every time access is being checked on a given node (for a given operation). Moreover, it is also invoked when running entity queries against the node entity type and the access check has not been disabled. Finally, it is also invoked in database API queries when the node_access tag is used. Remember the query alters based on tags that we talked about in Chapter 8, The Database API?

As an argument, it receives the user account for which access needs to be checked--the grants that it has within the node access grants system of the given operation. So, what we do here is start by returning an empty array (no grants) if the user is anonymous or the operation they are attempting to do is not view--you have not been granted access. The same thing happens if the user entity does not have any value in the field_user_type field. If they do, however, we get the corresponding grant ID and return an array of access grants keyed by the realm. For each realm, we can include more than one grant ID. In this case, though, it is only once since the user can only be of one type. We can also return multiple realms if needed, and, of course, other modules may do so as well, the results being centralized and used in the access logic.

With this in place, all our page nodes are now available for viewing only to board member and manager users, whereas articles are available for viewing to employees as well. If users don't have any type, they don't have access. The great thing is that these restrictions are now being taken into account also when running queries. So, we can automatically exclude from query results the nodes to which users don't have access. This works with Views as well.

Let's now enhance this solution with the following changes:

  • Unpublished article nodes are only available to managers and board members
  • Managers also have access to update and delete articles and pages

The first one is easy. After we define our internal map inside user_types_node_access_records(), we can unset the employee from the array in case the node is unpublished:

if (!$node->isPublished()) {
  unset($map['article'][0]);
}

This was a very simple example, but one meant to draw your attention to an important but often forgotten point. If you create access records for a node, you will need to account for the node status yourself. This means that if you grant access to someone to view a node, they will have access to view that node, regardless of the status (publishing state). Also, more often than not, this is not something you want. So, just make sure that you consider this point when implementing access grants.

Now, let's see how we can alter our logic to allow managers to update and delete nodes (both articles and pages). This is how user_types_node_access_records() looks like now:

$bundles = ['article', 'page'];
if (!in_array($node->bundle(), $bundles)) {
  return [];
}

$view_map = [
  'article' => [
    'employee',
    'manager',
    'board_member',
  ],
  'page' => [
    'manager',
    'board_member'
  ]
];

if (!$node->isPublished()) {
  unset($view_map['article'][0]);
}

$manage_map = [
  'article' => [
    'manager',
  ],
  'page' => [
    'manager',
  ]
];

$user_types = $view_map[$node->bundle()];
$manage_user_types = $manage_map[$node->bundle()];
$grants = [];

foreach ($user_types as $user_type) {
  $grants[] = [
    'realm' => 'user_type',
    'gid' => user_types_grant_mapping($user_type),
    'grant_view' => 1,
    'grant_update' => in_array($user_type, $manage_user_types) ? 1 : 0,
    'grant_delete' => in_array($user_type, $manage_user_types) ? 1 : 0,
  ];
}

return $grants;

What we are doing different is, first, we rename the $map variable to $view_map in order to reflect the actual grant associations. Then, we create a $manage_map to hold the user types that can edit and delete the nodes. Based on this map, we can then set the grant_update and grant_delete values to 1 for the user types that are allowed. Otherwise, they stay as they were.

All we need to do now is go back to the hook_node_grants() implementation and remove the following:

if ($op !== 'view') {
  return [];
}

We are now interested in all operations, so users should be provided all the possible grants. After rebuilding the permissions, manager user types will be able to update and delete articles and pages, while the other user types won't have these permissions. This does have many implications for queries because those use the view operation.

Before closing the topic on the node access grants, you should also know that there is an alter hook available that can be used to modify the access records created by other modules--hook_node_access_records_alter(). This is invoked after all the modules provide their records for a given node, and you can use it to alter whatever they provided before being stored.

The access grants system, as mentioned, is limited to the node entity type. It has been there from previous versions of Drupal, and it didn't quite make it to become standard across the entity system. There is talk, however, of doing this, but it's quite incipient.

To better understand how it works under the hood in case you want to write your own such system, I encourage you to explore the NodeAccessControlHandler. You'll note that its checkAccess() method delegates to the NodeGrantDatabaseStorage service responsible for invoking the grant hooks we've seen before. Moreover, you can also check out the node_query_node_access_alter implementation of hook_query_QUERY_TAG_alter() in which the Node module uses the same grant service to alter the query in order to take into account the access records. It's not the easiest system to dissect, especially if you are a beginner, but well worth going through and to learn more.