Creating Custom Beans in Drupal, Part 2

by Aaron Froehlich

*Feel free to browse the code from this tutorial in my sandbox repository at Drupal.org.

In Part 1 of this tutorial, we created a custom Bean type that provides reusable blocks of tweets from a specified twitter user. In Part 2, we will continue improving our custom bean type by upgrading to the Twitter 1.1 API, which will also give us the opportunity to address some typical decisions that you might face during development of a project, such as:

  • the tradeoffs between custom code and javascript libraries
  • the use of 3rd party php libraries
  • different ways of handling API keys and secrets

The code we wrote in Part 1 was intentionally pretty simple, and Twitter's V1 API supported us in that regard because it allowed non-authenticated access to user timelines. This highlights a situation that is typical with 3rd party integrations: we've created a dependency between our application and Twitter's API. Now that one of our dependencies has changed, it begs the question again: is a custom module the way to go? What are the alternatives?

  1. We could reconsider Twitter Timeline, which also leverages Bean to create reusable twitter timeline blocks. The main advantage to this approach is that it mitigates our application's Twitter dependency, because one could argue that Twitter will likely keep their own widgets up-to-date with their ongoing API changes. However, there are two main disadvantages to this approach. First, there is currently very limited support for theming the Twitter widgets, so we basically choose a light or dark version. Second, relying on Twitter's javascript to handle our timeline block requires that it be loaded from Twitter with every page load. At least some argue that modern-day reliance on 3rd party javascript libraries has a major impact on user experience.
  2. We could ditch our Minimalist Drupal approach and just drush dl twitter && drush en twitter. With this option, we not only get support for tweets on the site, we also get some other cool features, such as logging in via Twitter, posting to Twitter, setting up Rules to auto-post to Twitter. However, if we take a peak at Twitter's info file, we see that it requires Oauth, Views and Entity, the first two which, at this point, we don't actually need on the site except to support our blocks of tweets. This brings into focus one of the key principles of agile development, deferred-decision making. In this case, all we know is that we want to create the ability to add reusable blocks of tweets on the site. Why add additional dependencies that we don't know whether we'll utilize?
  3. We could rework our custom bean to handle Twitter's 1.1 API, and also take a look at some coding best-practices we could follow that will generally make our implementation more flexible moving forward. As you already know, this is the route we're going to take, so let's get started.

Immediately, we're faced with yet another decision: how are we going to handle OAuth? In reality, we almost certainly addressed this question when we were choosing from the three options above. There is the OAuth module, which seems to be a good candidate, as it "acts as a support module for other modules that wish to use OAuth." This would allow us to add additional services with OAuth in the future. However, deferred-decision making again encourages us to wait until we know we'll need it. The project may require another OAuth integration at some point, but now is not that point. We discover a second option if we look at Twitter's documentation about integrating OAuth with PHP, where they refer to Abraham William's project twitteroauth. Finally, we could roll our own implementation, but then we are binding our external dependency to our internal code. We mitigate (but don't eliminate) that dependency by using twitteroauth, and with an unofficial endorsement by Twitter and only two files totaling 37k, that's the route we'll take here.

To begin our refactor, let's return to our custom TweetBlockBean class and begin reworking our view method to support the new API. Since we're adding some complexity, I'll begin by creating a new method that handles getting the tweets:

In this refactor, we've added a getTweets() method, which either returns the existing cached data, or initiates the (still incomplete) call to Twitter. We've also added some error handling, so that the user will know if there was an error getting the tweets.

With that, let's turn to our OAuth implementation, which will rely on Abraham William's twitteroauth repo. To accomplish this, begin by cloning the repo somewhere on your system git clone git://github.com/abraham/twitteroauth.git and copying the twitteroauth folder to a newly created /libraries folder within the custom module. In a real project, the sites/all/libraries folder would probably be a better location, but since I'd like the sandbox to include a working version, we can add it right to our module. Next, we'll require the library in our class and add the necessary implementation code to connect and authorize our Twitter credentials:

The main items of interest here are the $connection object we're creating that initializes TwitterOAuth, and the $connection->host that we set to use the new API. As you can see, our connection will require some keys and secrets, which will be the topic of the final section of the tutorial. But before we turn to that, I'd like to take a minute to discuss an additional refactoring that is tempting to consider. Feel free to skip the next paragraph if you'd just like to continue the tutorial.

In object oriented programming, design principles such as the single responsibility principle and separation of concerns invite us to consider some additional refactoring here. In particular, our getTweets() method has poor separation of concerns, and is currently checking the cache, establishing the OAuth connection, reaching out to the Twitter API, and caching the results. If this were a public interface, or if we knew that we would be reusing some of these methods in other places, we would definitely want to refactor them out. However, given that this is a private interface and that we don't yet have a use for more modular code in this tutorial, we will leave the class as is. If you're interested in seeing a refactored version that attempts to separate these overlapping concerns, take a look a this gist.

Our final step in Part 2 is to handle Twitter's authorization, which requires a consumer key and secret, as well as an access token and secret. There are a few things we'll need to cover in order to get this working in our module. First, Twitter needs to know about our application so it can supply us with the keys and secrets. Head over to your Twitter Developer apps page and either access an app you've already created, or create a new one. Once you've done that, you should be able to access the "OAuth" tool tab, which will either give you the pairs of keys and secrets you need, or invite you to create a token for the app, if you haven't done that yet.

With those values in hand, we can head back to our module. There are a few basic approaches we can consider at this point to provide access to these values to our module. To start with, let's take the 'leanest' approach and simply add the following variables to our sites/default/settings.php file:

This approach is better than adding the keys and secrets to the class itself, as the settings.php is typically left out of source control, and therefore isolates the sensitive information to this one file. However, it also has limitations, the primary one being that the token and token secret are values that can be reset if need be, which would require updating the settings.php file on the server if and when that happens. This may be an acceptable tradeoff, but for the purpose of digging a little deeper in this tutorial, we're going to choose a different option. As the last step in Part 2 of this tutorial, we're going to add a configuration interface for a site administrator to access and update those values. This is accomplished in four basic steps:

  1. Add a configuration line to our module.info file
  2. Use hook_menu to define the configuration details of our interface
  3. Implement an admin interface with a form to save the values we need from Twitter
  4. Add a module.install file to clear the values from the database if the module is deleted.

Let's run through those in order. First, we'll add our configuration line to bean_tweets.info:

This new line tells Drupal that our custom module has a configuration form, which becomes accessible to the user from the module administration page. Next, we'll implement hook_menu() in our bean_tweets.module file to define what happens when a user clicks on the "Configure" button:

As you can see, we're going to be using the form api through a new function called bean_tweets_admin(), which will live in a new file that we need to create now (assuming you're in the module file: mkdir includes, followed by touch includes/bean_tweets.admin.inc). Within this new file we'll implement the form:

Notice the system_settings_form wrapper that we're utilizing. That's a handy Drupal helper that keeps us from needing to define a submit handler or set our variables. Now, when an administrator goes to the module page, they will see a "configure" link next to the Bean Tweets listing, where they can add the necessary keys and secrets.

The last bit of housekeeping is to add a module.install file, where we'll add some housekeeping tasks as a Drupal best-practice touch bean_tweets.install, whose current function is simply to remove its own variables in the event that the module is uninstalled:

With that, we've reached the end of Part 2 of our Custom Bean Types tutorial. Again, you can get the code for this tutorial in my sandbox repository. Please let me know if you have questions or suggestions. Otherwise, I'll return in a couple of weeks for Part 3, where we'll be adding a theme layer to our view, as well as re-considering our caching strategy. Thanks for joining me!


Are you passionate about programming and making the world a better place with your code? We want to hear from you!