Today, we will delve into Drupal 8 module development and demonstrate how to define your own service in Drupal 8. The goal will be to define a (new) controller to register a route that dynamically displays the last 10 tweets of a Twitter user. We have already shown in one of our previous posts how to create a controller class in Drupal 8 that outputs a simple text when a URL is called. The example was not entirely trivial, as the text depended on further parameters in the URL. This example will now be extended by the concepts of Services, ServiceContainer, and Dependency Injection to keep the logic for retrieving Twitter data separate in a service class and make it specifically available to our controller via Dependency Injection.
Prerequisites
To retrieve data from Twitter, you must register an app with Twitter. This is necessary to query user-specific data from the Twitter API. It is important to note the tokens for Consumer Key (API Key) and Consumer Secret (API Secret) after creating the app, as we will use these later in the code of our service class.
Defining a new route
This part should already be familiar to us from our first post (file /mymodule.routing.yml):
mymodule.twitter:
path: '/twitter/{username}'
defaults:
_controller: '\Drupal\mymodule\Controller\TwitterController::feed'
_title_callback: '\Drupal\mymodule\Controller\TwitterController::setTitle'
username: 'drupal'
requirements:
_access: 'TRUE'
We define a new path ('/twitter') that can contain a variable part ({username}). The new aspect here is that instead of _title, we use the key _title_callback, allowing us to change the page title dynamically based on the value in the URL, using a function.
Registering our Service Class and its Implementation
Similar to the procedure for registering controllers, we create a YAML file in the root directory of our module to declare our service class. The file must be named MODULENAME.services.yml (in our case /mymodule.services.yml):
services:
mymodule.twitter_service:
class: 'Drupal\mymodule\Twitter\TwitterService'
The code above is quite clear. The "machine_name" of our class is prefixed with the module name to prevent naming conflicts with other classes. We can now also create the file that will contain our class. The code for this file (/src/Twitter/TwitterService.php) is as follows:
/** * @file * Contains Drupal\mymodule\TwitterService. */
namespace Drupal\mymodule\Twitter;
class TwitterService {
public function getData($username) { $api_key = urlencode('YOUR_TWITTER_API_KEY'); $api_secret = urlencode('YOUR_TWITTER_API_SECRET'); $auth_url = 'https://api.twitter.com/oauth2/token';
$data_username = $username;
$data_count = 10;
$data_url = 'https://api.twitter.com/1.1/statuses/user_timeline.json';
$api_credentials = base64_encode($api_key.':'.$api_secret);
$auth_headers = 'Authorization: Basic '.$api_credentials."\r\n".
'Content-Type: application/x-www-form-urlencoded;charset=UTF-8'."\r\n";
$auth_context = stream_context_create(
array(
'http' => array(
'header' => $auth_headers,
'method' => 'POST',
'content'=> http_build_query(array('grant_type' => 'client_credentials', )),
)
)
);
$auth_response = json_decode(file_get_contents($auth_url, 0, $auth_context), true);
$auth_token = $auth_response['access_token'];
$data_context = stream_context_create( array( 'http' => array( 'header' => 'Authorization: Bearer '.$auth_token."\r\n", ) ) );
$data = json_decode(file_get_contents($data_url.'?count='.$data_count.'&screen_name='.urlencode($data_username), 0, $data_context), true);
return $data;
}
public function renderData($username) { $data = $this->getData($username);
$tweets = [];
foreach ($data as $value) {
$tweets[] = $value['text'];
}
return '<div>' . implode('</div><div>', $tweets) . '</div>';
} }
Not much happens here. The class has two methods: getData() and renderData(). getData() simply contains the code needed to query the Twitter API. You would need to insert the corresponding Consumer Key (API Key) and Consumer Secret (API Secret) here. The method also receives $username as a parameter. The assignment will happen in our controller, which we have not yet written. The $data variable that is returned is simply an array with various information about the last 10 tweets of a user. To display this relatively well, the second method in the class, renderData(), exists. Yes, this function is quite simple, but sufficient for our example.
By creating the Twitter service class, we have cleanly separated the code and given other developers the opportunity to use it elsewhere via Dependency Injection if needed. This is advantageous because our Twitter functionality is now encapsulated and could even be used in other PHP projects. It is also good to have such code, which meets very specific requirements, separated and not found in our route controller, which should ideally only have delegation tasks.
Calling a Service via Dependency Injection
What we still need is the implementation of our controller (see step "Defining a new route"). Let us first look at the code (file /src/Controller/TwitterController.php):
namespace Drupal\mymodule\Controller;
use Drupal\Core\Controller\ControllerBase; use Symfony\Component\DependencyInjection\ContainerInterface;
class TwitterController extends ControllerBase {
protected $twitterService;
public function __construct($twitterService) { $this->twitterService = $twitterService; }
public static function create(ContainerInterface $container) { return new static( $container->get('mymodule.twitter_service') ); }
public function feed($username) { $data = $this->twitterService->renderData($username);
$content = array(
'#markup' => $data,
);
return $content;
}
public function setTitle($username) { return 'Latest 10 tweets of ' . $username; } }
The essential change from our simple HelloWorldController class in the previous post is the use of Symfony's ContainerInterface component. This is necessary so that we can instantiate objects from other classes within the create() method. This way, we can then assign and fully utilise these objects in the __construct() method (see the first line in the feed() method). Quite clear and clean, would you not agree?
Conclusion
Step by step, the new concepts in Drupal 8 become clearer. Services make it relatively easy to maintain entire objects separately and thus reuse them for other projects (not necessarily Drupal projects). By registering them with Symfony's ServiceContainer, it is also very easy to use the properties and methods of the loaded object anywhere.

