Heute werden wir uns in der Drupal 8 Modulentwicklung vertiefen und zeigen, wie man einen eigenen Service in Drupal 8 definieren kann. Das Ziel wird sein, eine (neuen) Controller zu definieren, um eine Route zu registrieren, auf welcher dynamisch die letzten 10 Tweets eines Twitter-Benutzers dargestellt werden. Wir haben bereits in einem unserer vorherigen Beiträge gezeigt, wie man in Drupal 8 eine Controller-Klasse erstelltt, die beim Aufruf einer URL einen einfachen Text ausgibt. Das Beispiel war immerhin nicht ganz trivial, denn der Text war von weiteren Parametern in der URL abhängig. Dieses Beispiel gilt es nun um das Konzepte von Services, ServiceContainer und Dependency Injection zu erweitern, um die Logik für den Abruf der Twitter-Daten in einer Service-Klasse getrennt zu halten und diese gezielt unserem Controller per Dependency Injection zur Verfügung zu stellen.

Voraussetzungen

Um Daten von Twitter holen zu können, muss man eine App bei Twitter registrieren. Das ist notwendig, damit man von der API von Twitter User-spezifische Daten abfragen kann. Wichtig ist es, nach der Erstellung der App einfach sich die Tokens für Consumer Key (API Key) und Consumer Secret (API Secret) zu merken, denn diese werden wir später in dem Code unserer Service-Klasse einsetzen.

Eine neue Route definieren

Dieser Teil dürfte uns schon aus unserem ersten Beitrag bekannt sein (Datei /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'

Wir definieren einen neuen Pfad („/twitter“), der einen Variablen Teil ({username}) erhalten kann. Das neue hier ist, dass wir anstatt von _title den key _title_callback benützen, damit wir den Titel der Seite, entsprechend dem dynamischen Wert in der URL, durch eine Funktion ändern können.

Registrierung unserer Service-Klasse und deren Implementierung

Analog zur Vorgehensweise für die Registrierung der Controller, legen wir im root-Verzeichnis unseres Moduls eine YAML-Datei an, wo wir unsere Service-Klasse bekannt geben. Die Datei muss MODULENAME.services.yml heissen (in unserem Fall /mymodule.services.yml):


services:
  mymodule.twitter_service:
    class: 'Drupal\mymodule\Twitter\TwitterService'

Der Code oben ist ziemlich klar. Der „machine_name“ unserer Klasse ist mit dem Modulnamen prefixed, damit keine Namenskonflikte mit anderen Klassen entstehen. Wir können nun auch die Datei anlegen, die unsere Klasse beinhalten wird. Der Code für diese Datei (/src/Twitter/TwitterService.php) lautet wie folgt:

/** * @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>';

} }

Hier passiert eigentlich nicht viel. In der Klasse gibt es 2 Methoden – getData() und renderData(). getData() beinhaltet einfach den Code, den man braucht, um die Twitter-API abzufragen. Hier müsste man auch die entsprechenden Consumer Key (API Key) und Consumer Secret (API Secret) einsetzen. Die Methode bekommt auch $username als Parameter. Die Zuweisung wird in unserem Controller passieren, den wir noch nicht geschrieben haben. Die $data-Variable, die zurückgegeben wird ist einfach ein Array mit etlichen Infos über die letzten 10 Tweets von einem Benutzer. Um das relativ gut anzeigen zu können existiert die 2. Methode in der Klasse- renderData(). Ja, diese Funktion ist zielmlich einfach, aber für unser Beispiel ausreichend.

Durch die Erstellung der Twitter-Service-Klasse haben wir den Code sauber separiert und anderen Entwicklern die Möglichkeit gegeben, diesen bei Bedarf per Dependency Injection an anderen Stellen einzusetzen. Das ist von Vorteil, denn unsere Twitter-Funktionalität ist nun abgekapselt und könnte sogar in andere PHP-Projekte Verwendung finden. Es ist auch gut, solchen Code, der ganz spezifische Anforderungen erfüllt, getrennt zu haben und nicht in unseren Route-Controller zu finden, welcher eigentlich nur Aufgaben zu Delegierung haben sollte.

Service per Dependency Injection aufrufen

Was uns noch fehlt ist die Implementierung unseres Controllers (siehe Schritt „Eine neue Route definieren“). Schauen wir uns einfach zuerst den Code an (Datei /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; } }

Die wesentliche Änderung zu unserer einfachen HelloWorldController-Klasse aus dem vorherigen Beitrag ist die Benützung von Symfony’s Komponente ContainerInterface. Das ist notwendig, damit wir in der create()-Methode in der Lage sind, Objekte aus anderen Klassen zu instanzieren. So können wir diese Objekte dann in der __construct()-Methode zuweisen und vollständig benützen (siehe die erste Zeile in der feed()-Methode). Ziemlich klar und sauber, oder?

Fazit

Schritt nach Schritt werden die neuen Konzepte in Drupal 8 klarer. Services machen es einem relativ leicht ganze Objekte getrennt zu warten und diese somit für andere Projekte (nicht unbedingt Drupal-Projekte) wieder zu verwenden. Durch deren Registrierung zum Symfony’s ServiceContainer ist es zudem sehr leicht die Eigenschaften und Methoden des geladenen Objekts an beliebigen Stellen einzusetzen.