Building a Drupal Service - Part 1
Building a module that creates an Address Search Service
In this series, we will look into how to create a Drupal service and utilise it across the CMS. The example will be to create an Address search service.
Before we begin, let's take a moment to plan this out.
- Choose an API service that will let us search for addresses.
- Create a custom module.
- Let CMS users with specific permissions to configure the settings for this API.
- Set a configuration schema.
- Build a Configuration form.
- Create a Drupal Service using the API.
- Creating routes.
- Add a menu link in Admin menu bar under; Configuration > Web services.
- Add a module help page and show link to the module configuration section from Admin modules page.
- Integrate this Drupal service in a form field.
1) Choosing an API for address search
For the API, you can choose whatever suits your preference. However, the rest of the build will depend on the API you choose.
In this case I have found a free service that lets us search Australian addresses.
https://rapidapi.com/addressr-addressr-default/api/addressr/
It has a minimal amount of configurations involved, and the API seems to be simple for the task at hand.
We only require RapidAPI-Key
and RapidAPI-Host
Following is the example on their website for PHP cURL
<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://addressr.p.rapidapi.com/addresses?q=%3CREQUIRED%3E",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"X-RapidAPI-Host: addressr.p.rapidapi.com",
"X-RapidAPI-Key: SIGN-UP-FOR-KEY"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
2) Create a custom module.
For full discloser, I have read the code from the core jsonapi module (web/core/modules/jsonapi) and core ban module (web/core/modules/ban) to learn on how to build this entire custom module. It is a very powerful resource to use as one of my mentor’s Lee Rowlands always say.
Our custom module will be named as playground_address_search
therefore the info.yml will be playground_address_search.info.yml
type: module
core_version_requirement: ^9 || ^10
name: Playground Address Search
description: This module provides a service, field widget to search for addresses.
package: Playground modules
3) Creating Permissions
From our API of choosing, we know it requires 2 parameters for us to make a request. To make it configurable for the authorised editors, we need to create a custom permission and also a config form. We should also define the schema of the configuration and also let the configurations be installed when the module is installed into the CMS.
Let’s create the permissions first.
Create playground_address_search.permissions.yml
file and add below;
administer playground address search settings:
title: 'Administer Playground Address Search Settings'
description: 'Administer Address Search API settings.'
restrict access: TRUE
This will show up in the People > Permissions page as follows when the module is enabled;
4) Setting a configuration schema.
Now let’s see how to set a configuration schema.
Create a folder structure config/schema
inside the module and place a file named playground_search_address.schema.yml
playground_address_search.settings:
type: config_object
label: 'Address Search Service API Settings'
mapping:
rapid_api_key:
type: string
label: 'RapidAPI Key'
description: 'This is used for the request X-RapidAPI-Key header.'
rapid_api_host:
type: string
label: 'RapidAPI Host'
description: 'This is used for the request X-RapidAPI-Host header.'
The schema file defines a medium of validation and restricts the users from adding inputs other than strings. Also, this can be used later in the config form.
For the module to install a default set of configurations alongside module installation, we need to add a config install file. Create a folder structure config/install
and place playground_address_search.settings.yml
rapid_api_key: ''
rapid_api_host: 'addressr.p.rapidapi.com'
Now, if we navigate to the modules page in Drupal, we could see something like this;
Note that it is missing a help and config links, unlike with some other modules. For example, Ban module in Core.
5) Building a Configuration form.
For a CMS editor to make changes to the above configurations, we need to add a custom config form.
Let’s follow the core folder structure to place a config form. Create the following folder structure src/Form
and place ConfigForm.php
file.
You can check the complete file content from my GitHub repository https://github.com/pasankg/d10-playground/blob/develop/web/modules/custom/playground_address_search/src/Form/ConfigForm.php but for the moment let’s break it down in to smaller chunks.
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['playground_address_search.settings'];
}
getEditableConfigNames()
needs to know the configuration setting name we defined in our earlier steps.
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'playground_address_search_settings_form';
}
getFormId()
can be any name that you think is suitable.
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('playground_address_search.settings');
$form['rapid_api_key'] = [
'#title' => $this->t('RapidAPI Key'),
'#type' => 'textfield',
'#required' => TRUE,
'#description' => $this->t('This is used for the request X-RapidAPI-Key header.'),
'#default_value' => $config->get('rapid_api_key'),
];
$form['rapid_api_host'] = [
'#title' => $this->t('RapidAPI Host'),
'#type' => 'textfield',
'#required' => TRUE,
'#description' => $this->t('This is used for the request X-RapidAPI-Host header.'),
'#default_value' => $config->get('rapid_api_host'),
];
return parent::buildForm($form, $form_state);
}
buildForm()
is the primary function in charge of creating the form for the users to edit. Here we can add the fields according to our task. We need two fields. And note the $config
variable that is used to call ConfigFactoryInterface
and the setting for our module. These settings will be used as default values for the fields.
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('playground_address_search.settings')
->set('rapid_api_key', $form_state->getValue('rapid_api_key'))
->set('rapid_api_host', $form_state->getValue('rapid_api_host'))
->save();
parent::submitForm($form, $form_state);
}
submitForm()
is responsible to update the configurations according to the user input.
6) Creating a Drupal Service using the address search API.
This is the most important part of the build.
Looking at other core modules like ban and jsonapi we can notice that a file called *.services.yml
is used as an entry point for the service. Let’s create that.
playground_address_search.services.yml
services:
playground_address_search.connection:
class: Drupal\playground_address_search\Services\Connection
arguments: [ '@config.factory' ]
For our API service to be useful, and to perform an address search, we need to add logic. And this logic can be placed in the Connection.php
file. And as an argument we can use config.factory
although it is not necessary, it will help us to reduce some code as we can use this in the constructor to set the rapid_api_host
and rapid_api_key
settings directly.
Checkout the Drupal Core services to find out other possible arguments to use.
web/core/core.services.yml
and also checkout https://json.schemastore.org/drupal-services.json for the full list of available properties in aservice.yml
file.
The Connection.php
will take care of opening a site wide service that will be callable using \Drupal::service(‘playground_address_search.connection’);
The playground_address_search.connection
string is from the service.yml file we created earlier.
We want our service to be able to take in a query parameter and then make a request to the Addressr API, and let’s imagine once this is done the service call will be like so;
\Drupal::service(‘playground_address_search.connection’)->getAddresses($input)
To make this happen, let’s go back to the Connection.php
file and add this function. The complete file will look like this;
Notice the use of Drupal logger service to log any errors in the error
7) Creating routes
To make the module more user-friendly for the Admin UI users we can add some extra nice to haves, for this we need to make the most of the Drupal routing system.
Specifically, defining a route file will open up the options such as;
- Let users accessible to the config form created earlier.
- Add menu links in the admin menu.
- Help to add a configure link in the module install page.
8) Adding a menu link in Admin menu
Let’s try to add an Admin UI menu link under Configurations > Web services.
For this we need 2 files.
*.routing.yml file
*.links.menu.yml
Let’s create a route.yml
file.
playground_address_search.settings_link:
path: '/admin/config/services/address_search'
defaults:
_form: '\Drupal\playground_address_search\Form\ConfigForm'
_title: 'Address Search Service Configs'
requirements:
_permission: 'administer playground address search settings'
path
any suitable path for the form to be accessible. Because we plan on placing this form under Web services menu, we keep the Drupal default path to that /admin/config/services/
and append our custom route.
defaults
The default behaviour for when hitting this route.
requirements
Here we can make use of the custom permission we created earlier and make sure only user roles with administer playground address search settings
can view or edit the form.
Now if you clear cache and try to append the path
value from the route.yml file<domain>/admin/config/services/address_search
you will see that this will navigate us to the config form we created earlier.
However, if you navigate to /admin/config/services/
you will notice we do not see an option to come to the above form.
For that, we need to create the *.links.menu.yml
file.
playground_address_search.settings_menu:
title: 'Playground Address Search API Settings'
parent: system.admin_config_services
description: "Configure Rapid API settings."
route_name: playground_address_search.settings_link
9) Add a module help page.
This is an extra bit of nice to have. In the module listing page, we can add a help icon that will take the user to a custom help page.
We need to create a *.module file and use hook_help()
/**
* Implements hook_help().
*/
function playground_address_search_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.playground_address_search':
$variables = [
':api' => 'https://github.com/mountain-pass/addressr',
':config' => Url::fromRoute('playground_address_search.settings_link')
->toString(),
];
$output = '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('Provides an Address search service.') . '</p>';
$output .= '<p>' . t('API documentation can be found <a href=":api">here</a>', $variables) . '</p>';
return $output;
}
}
And to add a Configure link, we simply need to update our .info.yml
file with configure: playground_address_search.settings_link
where playground_address_search.settings_link
is the route given in the .route.yml
file. The final result is as below.
type: module
core_version_requirement: ^9 || ^10
name: Playground Address Search
description: This module provides a service, field widget to search for addresses.
package: Playground modules
configure: playground_address_search.settings_link
I will be covering the last point; 10) Integrate this Drupal service in a form field in a separate article.