Tutorial for Converting a Module from Drupal 7 to Drupal 8

With Drupal 8 now in Beta (as of this blog post), it’s time to start digging in and getting familiar with the new structure. As a way to start teaching myself D8, I decided to convert a pretty simple module I created in D7 into a D8 module and I was surprised at how many basic D8 concepts it touched on. I wanted to share my work with everyone so that we can all get up to speed together.

Above is the directory structure of my Twitter Pull module. The module uses the Twitter API PHP library (https://github.com/J7mbo/twitter-api-php) as a way of connecting to Twitter and creating a tweet feed. It will contain a tweet block, a page, and an admin page for setting all the Twitter settings.

As you can see, it is not a flat module directory and is broken into many files. We will take a look at the code for each file to see how it all works.

twitter_pull.info.yml

name: Twitter pull description: Pulls twitter tweets and display them in a feed type: module core: 8.x

Drupal 8 now makes use of yml syntax for many of its files. As we can see, the syntax is different than the Drupal 7 .info file, but shouldn’t take too long to get used to.

twitter_pull.module

<?php /** * Implements hook_theme(). */ function twitter_pull_theme($existing, $type, $theme, $path) { return array( 'twitter_pull_tweet_listing' = array( 'variables' => array( 'params' => null, ), 'template' => 'twitter_pull_tweet_listing', ), ); } }

In my .module file, I specify my theme which will be used to display my tweets. One difference I found between Drupal 7 and Drupal 8 here: In Drupal 7, since I always stored my themes in moduleName/templates/template.name.tpl.php, I needed to specify this path in the template ‘template’ => ‘templates/twitter_pull_tweet_listing’.

In Drupal 8, you shouldn’t add in the folder name as it looks for it automatically, and if you do, it won’t be able to pick it up.

twitter_pull.credentials.yml

oauth_access_token: "" oauth_access_token_secret: "" consumer_key: "" consumer_secret: "" screen_name: "" tweet_count: ""

In Drupal 8, we no longer store site variables in the variables database table. Instead, they are stored in a config file, in [module-name]/config/install. The file name should be [module-name].someUsefulDescription.yml. Drupal 8 no longer uses variable_get() and variable_set() for retrieving and setting the variable values, and we’ll see the new way in the code following.

One thing to note here: These configuration variables are tracked in Drupal from the time the module is installed. Once installed, adding new variables will not be picked up. You’ll either have to uninstall and reinstall (which obviously won’t be practical in all cases) or run a module update, similar when you run a module update when updating a schema (I haven’t actually tried this yet, so I don’t know how different it is to Drupal 7).

twitter_pull.routing.tml

twitter_pull.tweets: path: '/tweets' defaults: _controller: '\Drupal\twitter_pull\Controller\Twitter_PullController::content' _title: 'Tweets' requirements: _permission: 'access content' twitter_pull.settings: path: 'admin/config/services/twitter-settings' defaults: _form: '\Drupal\twitter_pull\Form\TwitterSettingsForm' requirements: _permission: 'administer site configuration'

Jumping down to the last file in the list, the routing.yml file. Drupal 8 now uses Symfony’s Routing component. This replaces Drupal 7’s hook_menu(). In my file, am registering a regular page with the path /tweets whose callback is a controller. I also registered an admin path so that I could create an admin form for setting the Twitter settings. The appropriate permission settings are set here as well.

twitter_pull.links.menu.yml

twitter_pull.settings: title: 'Twitter settings' description: 'Twitter settings for your site' parent: system.admin_config_services route_name: twitter_pull.settings weight: 100

While in Drupal 7, the menu path and menu links were specified in the same hook_menu(), in Drupal 8, they are split up. If you want your callback to be added to a menu, you need to create a file [module-name].links.menu.yml. Here you set the parent (in this case, I added it to the system configuration menu path – see core/modules/system/system.links.menu.yml) and reference my route name, twitter_pull.settings, which I specified in the file above.

Twitter_PullController.php

<?php namespace Drupal\twitter_pull\Controller; class Twitter_PullController { public function content() { return array('#markup'= 'hello world!'); } }

This is the controller which was referenced in the routing.yml file. As you can see, it defines a content function which is called to create the output of the page. As I am more interested in creating a Twitter block instead of a Twitter page, I left any real content out of my controller and only left it here to show how to create a page.

TweetsBlock.php

<?php namespace Drupal\twitter_pull\Plugin\Block; use Drupal\Core\Block\BlockBase; use Drupal\twitter_pull\twitter_api_php\TwitterAPIExchange; /** * Provides a block for executing PHP code. * * @Block( * id = "twitter_pull_tweets_block", * admin_label = @Translation("Twitter Tweets") * ) */ class TweetsBlock extends BlockBase { /** * Builds and returns the renderable array for this block plugin. * * @return array * A renderable array representing the content of the block. * * @see \Drupal\block\BlockViewBuilder */ public function build() { $config = \Drupal::config('twitter_pull.credentials'); $settings = array(); $settings['oauth_access_token'] = $config->get('oauth_access_token'); $settings['oauth_access_token_secret'] = $config->get('oauth_access_token_secret'); $settings['consumer_key'] = $config->get('consumer_key'); $settings['consumer_secret'] = $config->get('consumer_secret'); $screen_name = $config->get('screen_name'); $tweet_count = $config->get('tweet_count'); $url = 'https://api.twitter.com/1.1/statuses/user_timeline.json'; $getfield = '?screen_name='.$screen_name.'&count=' . $tweet_count; $requestMethod = 'GET'; $twitter = new TwitterAPIExchange($settings); $tweets = $twitter ->setGetfield($getfield ) ->buildOauth($url, $requestMethod) ->performRequest(); $tweets = json_decode($tweets); foreach($tweets as $tweet) { $tweet->text = check_markup($tweet->text, 'full_html'); $cleanTweets[] = $tweet; } $params = array('tweets' => $cleanTweets); $tweet_template = array('#theme' => 'twitter_pull_tweet_listing', '#params' => $params); return $tweet_template; } }

Here’s where the twitter block is created and there’s a lot happening here. Starting from the top, we need to use PHP Namespacing to autoload the Twitter API library I will be using to pull in the tweets. One thing to note here: I wasn’t sure where to put my Twitter API library files. Normally I’d put them in sites/all/libraries and then include that file in my code. With PHP Namespacing, I was unclear about how to properly include the files. The way I did it (which I’m sure is not the best way) is by manually adding a namespace at the top of the file I wanted to include. So, on the top of the TwitterAPIExchange.php file, I added:

namespace Drupal\twitter_pull\twitter_api_php;

Next we get to the annotations. Drupal uses the Symphony annotation system to set the block id and admin label. This is required in order to set up your block. Now comes the Drupal Configuration object. As I mentioned earlier, Drupal no longer uses variable_get or variable_set, which stored the global variables in the variable database table. Rather, it uses a configuration object and the configurations are stored in code. In the code above, we pull the required settings from the configuration object to send to our Twitter API library for processing. Finally we get to the theming. In Drupal 8, we no longer use the theming function theme() (it doesn’t actually exist in D8). Rather, we structure our render array, specify our render array and return it. See this post for more details: https://www.drupal.org/node/1920746.

TwitterSettingsForm.php

?php namespace Drupal\twitter_pull\Form; use Drupal\Core\Form\ConfigFormBase; use Drupal\Core\Form\FormStateInterface; class TwitterSettingsForm extends ConfigFormBase { public function getFormID() { return 'twitter_pull_twitter_settings_form'; } /** * Form constructor. * * @param array $form * An associative array containing the structure of the form. * @param array $form_state * An associative array containing the current state of the form. * * @return array * The form structure. */ public function buildForm(array $form, FormStateInterface $form_state) { //$config = \Drupal::config('twitter_pull.credentials'); $config = $this-config('twitter_pull.credentials'); //since we are extending ConfigFormBase instaad of FormBase, it gives use access to the config object $form['oauth_access_token'] = array( '#type' => 'textfield', '#description' => t('Oauth Access Token'), '#title' => t('Oauth Access Token'), '#default_value' => $config->get('oauth_access_token'), ); $form['oauth_access_token_secret'] = array( '#type' => 'textfield', '#description' => t('Oauth Access Token Secret'), '#title' => t('Oauth Access Token Secret'), '#default_value' => $config->get('oauth_access_token_secret'), ); $form['consumer_key'] = array( '#type' => 'textfield', '#description' => t('Consumer Key'), '#title' => t('Consumer Key'), '#default_value' => $config->get('consumer_key'), ); $form['consumer_secret'] = array( '#type' => 'textfield', '#description' => t('Consumer Secret'), '#title' => t('Consumer Secret'), '#default_value' => $config->get('consumer_secret'), ); $form['screen_name'] = array( '#type' => 'textfield', '#description' => t('Screen Name'), '#title' => t('Screen Name'), '#default_value' => $config->get('screen_name'), ); $form['tweet_count'] = array( '#type' => 'textfield', '#description' => t('Tweet Count'), '#title' => t('Tweet Count'), '#default_value' => $config->get('tweet_count'), ); return parent::buildForm($form,$form_state); } /** * Form submission handler. * * @param array $form * An associative array containing the structure of the form. * @param array $form_state * An associative array containing the current state of the form. */ public function submitForm(array &$form, FormStateInterface $form_state) { $this->config('twitter_pull.credentials') ->set('oauth_access_token', $form_state->getValue('oauth_access_token')) ->set('oauth_access_token_secret', $form_state->getValue('oauth_access_token_secret')) ->set('consumer_key', $form_state->getValue('consumer_key')) ->set('consumer_secret', $form_state->getValue('consumer_secret')) ->set('screen_name', $form_state->getValue('screen_name')) ->set('tweet_count', $form_state->getValue('tweet_count')) ->save(); } }

This is my admin form and it is also referenced in the routing.yml file to register a path. I wanted to create an admin form so that users could easily update the twitter settings for their twitter feed. Again we use the Drupal configuration object to get and set configurations. The one difference here is that since we are using the configFormBase namespace instead of the FormBase namespace, we now have direct access to the Config object via $this->config(‘configuationName’). In the form callback, we set the configurations based on the users’ input, using the FormStateInterface object instead of the Drupal 7 $form_state array.

twitter_pull_tweet_listing.html.twig

{% for tweet in params.tweets %} <div class="tweet-wrapper"> <div class="tweet-profile-image"> <img src='{{ tweet.user.profile_image_url }}'/> </div> <div class='tweet-text'> {{ tweet.text }} </div> </div> {% endfor %}

Finally, we have the twig template. There’s a lot of write up about the twig syntax, but the above code shows you can access the tweets array data and output it in the template.

I hope that this Drupal 8 module tutorial has been of some help. While this module is by no means complete, I think that it highlights some essential D8 knowledge for creating a custom module. Here are some snapshots of the module working: