Permissions in Drupal control access to features and functions. Modules define permissions, which allow site administrators to grant or restrict access based on user roles. As a module developer, you'll create new permissions to restrict access to your module's custom features, independent of existing permissions defined by other modules.
In this tutorial, we'll:
- Explore how and where permissions are defined within a module.
- Discuss the concept of static and dynamic permissions in Drupal.
By the end of this tutorial, you'll have a clear understanding of how permissions function in Drupal and their implementation in modules.
Modules can define custom permissions to restrict access to specific routes or page sections. This control allows module developers to provide granular access while enabling site administrators to manage user privileges. We'll add a view weekly weather
permission via the anytown module to limit access to the weather page.
In this tutorial, we'll:
- Create an MODULE_NAME.permissions.yml file.
- Define a new permission.
- Use the new permission to restrict access to a route.
By the end of this tutorial, you should be able to define a new permission in a module and use it to control access to a route.
Custom services in Drupal modules encapsulate specific business logic or functionality. In our example, we'll demonstrate moving code required to access a weather forecast API from a controller into a service. This will help make our controller thin and our module code more reusable, testable, and maintainable.
In this tutorial, we'll:
- Explore the advantages of custom services for managing business logic.
- Define the components of a custom service.
By the end of this tutorial, you'll understand why creating custom services is a beneficial practice in Drupal module development.
To access services in Drupal through the service container, you'll need to know the unique machine name of the service. We'll use the example of making HTTP requests to a weather forecast API in the anytown module to demonstrate several methods you can use to identify an existing service's ID.
In this tutorial, we'll:
- Discover existing services and their machine names.
- Take a look at an example service definition.
By the end of this tutorial, you should be able to locate and use existing services in your Drupal module.
Concept: Testing
FreeTesting ensures that code remains reliable and functional. This tutorial introduces the primary types of tests in Drupal: Unit, Kernel, Functional, and FunctionalJavascript -- all executed via PHPUnit. We'll clarify the differences between each type of test and appropriate use cases. As module developers, understanding what to test and how to write tests is vital for robust and maintainable code.
In this tutorial, we'll:
- Identify the primary test types in Drupal and their use cases.
- Emphasize the importance of functional tests in custom module development.
- Introduce the basics of authoring tests in a custom module.
By the end of this tutorial, you should recognize the different types of tests Drupal uses and when and how to use each kind.
Before you can run tests, you'll need to configure your local environment. This setup involves Drupal-specific configuration for PHPUnit and ensuring your environment supports Functional JavaScript tests with a WebDriver client and a compatible browser. The setup process varies based on the development environment. In this tutorial, we're using DDEV as the local environment.
In this tutorial, we'll:
- Install all required dependencies.
- Configure PHPUnit specific to our environment.
- Validate the setup by running a Drupal core test.
By the end of this tutorial, you'll be equipped to run Drupal's PHPUnit tests locally using DDEV.
Functional tests simulate user interactions with Drupal applications, which enables us to test user interfaces and complex workflows. This tutorial guides you through writing functional tests for the anytown module, focusing on custom user registration workflow enhancements.
In this tutorial, we'll:
- Examine functional test structure.
- Test
anytown_form_user_register_form_alter()
customizations. - Discuss the functional test execution environment.
By the end of this tutorial, you'll know how to write functional tests that emulate browser interactions with your Drupal application.
Kernel tests in Drupal enable module integration testing with Drupal core systems in a bootstrapped environment. Kernel tests bridge the gap between unit and functional tests. This tutorial focuses on writing kernel tests for the anytown module, specifically to test the ForecastClient
service's caching logic and custom username validation.
In this tutorial, we'll:
- Explore the parts of a kernel test.
- Write kernel tests for anytown module features.
- Use mocks and the Drupal container in kernel tests.
By the end of this tutorial, you should be able to get started writing kernel tests to verify your module's integration with Drupal core.
Unit tests are the simplest among Drupal's test types, ideal for verifying code that performs computations. This tutorial guides through writing unit tests for the anytown module, focusing on the ForecastClient
service, and illustrates how to use mocks for dependencies.
In this tutorial, we'll:
- List potential unit tests for the anytown module.
- Write tests for
ForecastClient
service logic. - Demonstrate how to mock services in unit tests.
By the end of this tutorial you should be able to write PHPUnit Unit tests for logic in the anytown module.
Many of Drupal's APIs that look like a bunch of configuration in a YAML file (migrations, menu links, etc.) are actually plugins in disguise. The YAML from these files is used as arguments to a generic PHP plugin class which then behaves differently depending on the provided values. As a developer, you probably don't need to know that menu links are plugins, but it can be helpful when debugging or just trying to get a better understanding of the big picture.
In this tutorial, we'll:
- Learn about how YAML-based plugins work
- Discuss how to find the implementation details for YAML-based plugins
- Walk through an example of implementing a YAML-based plugin
By the end of this tutorial, you should be able to recognize a YAML-based plugin definition, and author your own.
There just a couple of components that make up the token system in Drupal 7, which is nice because it means fewer new things we have to learn. But, it's important to understand what those components are and some of the fundamentals of how tokens work in order to make the most of Drupal's Token API. In this lesson we'll take a look the use case for tokens, talk about token types, and the concept of global tokens vs. those that require some additional context in order to have their dynamic values calculated.
What are tokens?
Tokens are specially formatted chunks of text that serve as placeholders for a dynamically generated value. Here's a really simple example: You want to display a welcome message to user's of your site, and you want it to be personalized so how about you add their name and instead of just saying "Welcome", you can say "Welcome, Joe". In order to avoid having to hard-code a welcome string for every single user of the site it would be nice to dynamically generate the string. So you use a one like the following, "Welcome, [current-user:name]
".
The [current-user:name]
here is what Drupal refers to as a token. A string of static text that will be located and replaced with a dynamic value. This token is made up of a few parts, inside of the mandatory square brackets that signify that this is a token. The first part current-user:
in this case is the token type.
Token types are used to group like things together into a namespace. User's for example have name, mail, and last-login properties. Nodes have title, nid, and author properties. Token type also plays an important role in determining what tokens are available in what context. User tokens for example might be available when sending an email to a specific user but node type tokens might be irrelevant in this use case. We need to be able to tell end user's what types of tokens can be used in what context so that they don't use a node token (which has no value) in a user context.
The second part, after the colon (:
) is the token itself. This signifies what value will be substituted into the string containing the token. :name
in this case indicates we want the current user's username. This also brings up another important point. Global vs. contextual tokens. Some tokens like this one [system:date]
can be calculated without any additional information. Simply call the PHP date function and you've got a value. This token [current-user:name]
, on the other hand, requires knowledge about the currently logged-in user in order to be able to determine that user's name. This token requires additional context in order to be useful.
Additional resources
Before we can get started creating our own tokens we'll need a basic Drupal site setup and module to start from. This lesson will walk through the prerequisites for the rest of the series including having Drupal installed, downloading and installing the databasics module, creation of some dummy content, and a quick tour of what the module does.
In order to follow along in this series you'll want to have a copy of Drupal 7 installed. If you need a refresher on installing Drupal check out this series on getting up and running with Drupal 7.
Once you've got Drupal 7 installed you'll want to download and install a copy of the databasics module that was created as part of the Drupal 7 module development series. We'll use this as a starting point for adding custom tokens so that we have some new data to play with. You can grab a copy of the files attached to this page.
Finally, we'll create some dummy content on our site and walk through what the databasics module does and talk about the problem we're trying to solve.
The databasics module has a string of text that's displayed at the bottom of each node that displays some dynamic information. We would like this text to be configurable by an administrator, so we'll add some new tokens and then update both the form where text is entered and the code that displays the text to allow for use of the newly created tokens.
Additional resources
The first of two steps required to add new tokens to Drupal 7 is implementing hook_token_info() in order to give Drupal a list of the placeholders your module provides. Placeholders can be grouped together by creating a new token type and placing new tokens under that grouping or by adding new tokens to an existing token type provided by another module like the node module. Token types can be defined as global meaning their value can be calculated without any additional context, or as 'needs-data' tokens that require additional information about the context in which they are being used in order to calculate their value. For example, a [node:title]
token needs to know which node is being referenced, whereas a [system:date]
token doesn't need any additional reference to calculate the current date. In this lesson we will be adding the following token types:
[databasics-totals]
- For tokens that can be calculated without any additional context.[databasics-page]
- For tokens that require additional context in order to be rendered. In this case, they need to know which page is currently being viewed and are thus only useful in the context of viewing a page.
And these tokens:
[databasics-totals:count]
- Total page views for the entire site.[databasics-page:view-count]
- Number of times the current page has been viewed by the current user..[databasics-page:last-viewed]
- Date the current page was last viewed..
We use hook_token_info()
to provide Drupal with information about available placeholder tokens and their types. Here's an example hook_token_info()
implementation:
/**
* Implements hook_token_info().
*/
function databasics_token_info() {
$info = array();
$info['types'] = array(
'databasics-totals' => array(
'name' => t('Databasics totals'),
'description' => t('Global databasics tokens.'),
),
// [databasics-page:]
'databasics-page' => array(
'name' => t('Databasics'),
'description' => t('Tokens for databasics page counts.'),
'needs-data' => array('databasics_record'),
),
);
$info['tokens'] = array(
'databasics-totals' => array(
// [databasics-totals:count]
'count' => array(
'name' => t('Total page views'),
'description' => t('Total page views for entire site.'),
),
),
// Page specific tokens.
'databasics-page' => array(
// Add a token for the view count.
// [databasics-page:view-count]
'view-count' => array(
'name' => t('View count'),
'description' => t('Number of times the page has been viewed by the current user.'),
),
// [databasics-page:last-viewed]
'last-viewed' => array(
'name' => t('Last viewed'),
'description' => t('Date the page was last viewed.'),
),
),
);
// [node:view-count], [node:view-count:last-viewed]
$info['tokens']['node'] = array(
'view-count' => array(
'name' => t('View count'),
'description' => t('Number of times the current node has been viewed by the current user.'),
'type' => 'databasics-page',
),
);
return $info;
}
Additional resources
Note: In the video, around 9:30, we added at the :uid
argument to the query, but did not add the corresponding portion to the WHERE clause of the query. The final query should look like the one in the code sample below.
Now that we've got placeholder tokens that we can enter into a string of text we need to provide the actual values that should be used to replace those placeholders. In this lesson we'll implement hook_tokens() in order to provide the Drupal Token API with the values that correspond to the placeholders our module provides.
Implementations of hook_tokens()
are called once for every token type that's in scope for the current string that's being processed. So, one for all user tokens, once for all node tokens, and once for all global tokens. Each time the hook is passed an array which contains all the tokens of that specific type that where found in the string being processed as well as any additional contextual information such as the current $user
or $node
object.
hook_tokens()
is expected to return an array of values for each of the tokens in question that your module is responsible for. In our case, since we added the [databasics-page:*]
and [databasics-totals:*]
tokens we're responsible for calculating and returning their value whenever they are requested.
We'll also look at using the token_find_with_prefix() function which will allow us to detect and provide values for any chained tokens. Like for example [node:view-count:last-viewed]
.
Example:
/**
* Implements hook_tokens().
*/
function databasics_tokens($type, $tokens, array $data = array(), array $options = array()) {
$replacements = array();
if ($type == 'databasics-totals') {
foreach($tokens as $name => $original) {
switch($name) {
case 'count':
$count = db_query('SELECT SUM(view_count) FROM {databasics}')->fetchField();
$replacements[$original] = $count;
break;
}
}
}
if ($type == 'databasics-page' && !empty($data['databasics_record'])) {
$record = $data['databasics_record'];
foreach ($tokens as $name => $original) {
switch ($name) {
case 'view-count':
$replacements[$original] = $record->view_count;
break;
case 'last-viewed':
$replacements[$original] = $record->last_viewed;
break;
}
}
}
if ($type == 'node' && isset($tokens['view-count']) && !empty($data['node'])) {
$node = $data['node'];
global $user;
$count = db_query('SELECT SUM(view_count) FROM {databasics} WHERE nid = :nid AND uid = :uid', array(':nid' => $node->nid, ':uid' => $user->uid))->fetchField();
$replacements[$tokens['view-count']] = $count;
// [node:view-count:last-viewed]
if ($count_tokens = token_find_with_prefix($tokens, 'view-count')) {
$record = databasics_get_record($node->nid, $user->uid);
$replacements += token_generate('databasics-page', $count_tokens, array('databasics_record' => $record), $options);
}
}
return $replacements;
}
Additional resources
With a list of placeholders and a way to retrieve the values for those placeholders we can now bring it all together and use the token_replace() function to locate placeholders in a string of text and replace them with their dynamically generated counterparts. We'll use token_replace()
to process the $message
variable displayed on nodes by the databasics module so that we can use our new tokens. Then we'll look at passing contextual data like the current $node
to the token_replace()
function so the code that does the actual value calculation can have all the information it needs to do so.
The token_replace()
function will take a string of text like the following: "Welcome, [current-user:name]", and perform the replacement of the token with it's value resulting in something like "Welcome, Joe".
Additional resources
The Token API built into Drupal 7 core provides the ability to add placeholder tokens and replace them with dynamic values. But it's missing some critical features. A UI for allowing end users to browse the list of available tokens for example. And core only provides some very basic tokens, missing support for things like field values. In this lesson we'll look at additional functionality provided by the token module from contrib and how we can make use if it to provide both a UI for browsing available tokens as well as better validation of form elements with tokens in them.
First we'll download and install the token module from contrib and look at some of the features it provides us with without having to write any code.
Then, we'll look at using theme_token_tree_link()
to provide our end users with a user interface for browsing the tokens available in a given context. theme_token_tree_link()
also allows us to specify the token types that should be displayed so we can limit the list to just those that are relevant.
Finally, we'll look at using the token_element_validate()
function as an extra element validator on our Form API element where we allow users to enter strings of text that contain tokens. This validator will provide valuation of the tokens entered into the form element, for example: if the valid token types are 'node' which means they can only use 'node' tokens in that field. This makes sure that the tokens you enter into a field will work in that context.
Additional resources
In this introductory series you will learn how use the Domain Access project to let you manage multiple "sites" with different domain names from just one Drupal installation. Domain Access "multisite" works differently from the core multisite feature in that you truly only have one site to manage. There is just one code base and one database. Domain Access takes advantage of Drupal's node access system to give the illusion of multiple sites. In this series we start off by getting some context through several presentations that explain what Domain Access offers, and why you might use it, how DNS and Apache web servers work, and what you need to understand about the node access system. Once we dive into the hands-on work, you will configure Apache to work with multiple domain names, and get Domain Access installed on your site. Then you will configure a very basic Domain Access site, learning how to share and restrict content, change themes, and set up permissions for fine-grained access control.
Additional resources
Domain Access project (drupal.org)
This series will implement the same example as the Multisite series did, but with Domain Access instead. You can see and compare the two methods. First, let's look at some other examples using Domain Access and see what we get when we download the package from Drupal.org. We'll also talk about the features provided, along with some things to be aware of and consider when choosing Domain Access.
Additional resources
Domain Access project (drupal.org)
Domain Access can do its magic because of the Drupal node access system. In this tutorial we'll walk through the basics of how this system works, highlighting the two main methods, and then explain why this may be important information for you. We won't be diving into the code side of things, but instead outline the basic concepts for anyone who needs to interact with this system. When using a module like Domain Access, you should be aware of the Drupal context in which you are working, even if you hopefully never have to dive into the details.
Additional resources
Controlling Access to Content Overview (drupal.org handbook)
Node access developer documentation (api.drupal.org)
In this tutorial we will get hands-on with Domain Access by getting the module installed. This is a more involved process than a regular module installation, but we just need to make sure we have a few things in place first. We're going to need to make sure we have our domains functioning correctly through Apache, and then add the Domain Access include file to our settings.php. With the configuration and module in place, we'll also verify that it is working properly and take a look at our domain list.
After watching this tutorial you will be able to properly install the Domain Access module, with its additional steps, and then verify that the installation was correct.
Additional resources
Domain Access project (drupal.org)
Domain Access Configuring settings.php (drupal.org handbook)
Installing the Domain Access module (Drush instructions) (drupal.org handbook)