Menu Search
Jump to the content X X
Smashing Conf San Francisco

We use ad-blockers as well, you know. We gotta keep those servers running though. Did you know that we publish useful books and run friendly conferences — crafted for pros like yourself? E.g. upcoming SmashingConf San Francisco, dedicated to smart front-end techniques and design patterns.

Making A WordPress Plugin That Uses Service APIs, “From Soup To Nuts”

An increasingly large number of publicly available APIs provide powerful services to expand the functionality of our applications. WordPress is an incredibly dynamic and flexible CMS that powers everything from small personal blogs to major e-commerce websites and everything in between. Part of what makes WordPress so versatile is its powerful plugin system, which makes it incredibly easy to add functionality.

We will walk through how I made GitHub Pipeline1, a plugin that allows you to display data from the GitHub API on WordPress pages using shortcodes. I’ll give specific examples and code snippets, but consider the technique described here a blueprint for how to consume any service API with a plugin. We’ll start from the beginning, but a degree of familiarity with WordPress and plugin development is assumed, and we won’t spend time on beginner topics, like installing WordPress or Composer.

You’ll need:

Choosing An API Link

The first step in writing this kind of plugin is to choose an API. In this tutorial, we’ll use the GitHub API. If you’re thinking of using another API, consider a few important factors that can affect how rewarding your project will be.

One of the first things I look at is the thoroughness and quality of the API’s documentation. If it’s sparse or outdated, be prepared to spend time sifting through the source code to answer your own questions. Also, consider how mature the API is and how responsibly the provider has versioned it. Nothing is worse than investing time in creating something great, only for it to break because of changes to the upstream API. Look for a versioning system in the endpoint URLs.

// good
http://api.stable.com/v1/user/

// danger
http://api.dodgy.com/user/

At the risk of stating the obvious, programming against a third-party API involves a trust relationship, and not all APIs are created equal.

Shopping for a Library Link

Established brands often distribute libraries or SDKs to make it easier to work with their API. If this is not the case, remember to research whether someone else has written a library already before you go and reinvent the wheel. Google and GitHub are two great places to start your research. The number of stars or forks on GitHub is a good indication of how effective a library is. The age of the most recent commits and/or the number of open issues are an indication of how actively it is maintained. In my case, I was lucky to find the lovely PHP GitHub API125, by KNP Labs.

Writing Your Own With Guzzle Link

If there is no satisfactory library for your provider, you still shouldn’t start from scratch because tools like Guzzle6 make working with HTTP requests much easier. Guzzle wraps PHP’s cURL library7 and removes many of the headaches typically associated with configuring and making requests manually. Even if you’re making only one or two requests, I’d still recommend using it because it will make your code more robust, and installing it with Composer is a piece of cake.

Setting Up The Plugin Link

We will begin with the basic skeleton of a minimal WordPress plugin, a directory with two files. Choosing a descriptive and unique folder name is important to avoid conflicts with other plugins. If the name of your plugin is somewhat generic, consider adding a unique prefix.

github-api/
   readme.txt
   github-api.php 

The readme.txt contains the meta data for your plugin that will appear on wordpress.org if you decide to publish it there. Read about publishing WordPress plugins in the documentation8, or check out the comprehensive sample readme.txt9.

The PHP file also contains some meta data in the header that will be used to display info about your plugin in the dashboard. Start with a header that looks like this:

<?php
/**
Plugin Name: GitHub API
Description: Add GitHub project information using shortcode
Version: 1.0
Author: Your Name
License: GPLv2 or later
Text Domain: github-api
*/

For security reasons, it’s also a good idea to deny direct access to the file, like this:

defined( 'ABSPATH' ) or die( 'No script kiddies please!' );

At this point, the plugin won’t do anything, but when you copy the files to wp-content/plugins, it should appear in the plugins list and you should be able to activate it.

WordPress dashboard plugins page10
WordPress dashboard plugins page. (View large version11)

Next, we’ll want to include the library that will handle the API requests. In the following code example, we’re including KNP Labs’ PHP GitHub API125, but you can include any dependency by substituting knplabs/github-api with the package you’re using.

$ cd wp-content/plugins/github-api
$ composer require knplabs/github-api

Now, your file structure should look as follows:

github-api/
   composer.json
   composer.lock
   github-api.php 
   readme.txt
   vendor/

Now that the files are in place, we need to require the vendor/autoload.php that was created when you installed it.

require_once 'vendor/autoload.php';

If everything was set up correctly, you should be able instantiate a class from the library without a fatal error being thrown.

$testing = new \Github\Client();

But this is just a quick way to test that your dependencies are available. I recommend defining a new class that extends the library.

class MyGithub extends \Github\Client {};

This isn’t critical, but it makes your code more flexible in a couple ways. The most obvious way is that you can alter or add new functionality if you need to. But it also makes life easier should you ever want to switch libraries in the future, because you’ll have to make the changes in only once place, rather than throughout your code.

WordPress Shortcodes Link

Now it’s time to set up our first shortcode, which is a special snippet that allows you to generate content by placing it in a post or page. If you’re completely new to shortcodes or you want a deeper understanding of how they work, check out the official shortcode documentation13. In my example, I’m going to display a list of GitHub issues wherever the author places this shortcode: [github_issues]

As we did before, let’s start with a minimal example to make sure the shortcode is registered correctly before we work on making the API call.

function github_issues_func( $atts ) {
   return "Hello world!";
}
add_shortcode( "github_issues", "github_issues_func" );

Now, if you publish a page containing [github_issues], you should see “Hello world” when you visit the page. Now that this is working, let’s set up the API call.

In the previous section, we set up autoloading for our GitHub library and defined our own class to extend it. Now, we can start using it to make API calls. So, we’ll add it to the shortcode’s callback function. As a proof of concept, let’s fetch all of the issues in the GitHub Pipeline repository. Documentation for this method is available14.

function github_issues_func( $atts ) {

   // Instantiate our class
   $gh = new MyGithub();

   // Make the API call to get issues, passing in the GitHub owner and repository
   $issues = $gh->api('issue')->all('TransitScreen', 'wp-github-pipeline');

   // Handle the case when there are no issues
   if ( empty($issues) )
      return "<strong>" . __("No issues to show", 'githup-api') . "</strong>";
        
   // We're going to return a string. First, we open a list.
   $return = "<ul>";
    
   // Loop over the returned issues
   foreach( $issues as $issue ) {

      // Add a list item for each issue to the string
      // (Feel free to get fancier here)
      // Maybe make each one a link to the issue issuing $issue['url] )
      $return .= "<li>{$issue['title']}</li>";

   }
    
   // Don't forget to close the list
   $return .= "</ul>";
    
   return $return;

}
add_shortcode( 'github_issues', 'github_issues_func' );

Now you should be able to view the same page that we used above to test the shortcode, and this time you should see an unordered list of issues. But wait! Before moving on, let’s make one optimization so that our code is easier to test. (You are testing, right?!) Instead of using new in our function to instantiate the GitHub library class, let’s pass it in as a parameter and instantiate it only if we have to.

function github_issues_func( $atts, $gh=null ) {

   // Conditionally instantiate our class
   $gh = ( $gh ) ? $gh : new MyGithub();

   …

This modification resembles the dependency injection15 pattern, which makes our function much easier to test. WordPress unit testing is beyond the scope of this tutorial, but it’s easy to see how this new version makes it easy for us to pass “fake” API data to the function so that our test’s assertions know exactly what to expect.

What we’ve made so far is well and good for an internal project that will only be used on a single repository, but it would be much more useful if users could configure which repository to fetch issues from. Let’s set that up.

First, we’ll use a WordPress hook to register our new settings page, and the callback function will define the menu’s labels and titles. We’ll use the same incremental approach to get it working.

// Register the menu.
add_action( "admin_menu", "gh_plugin_menu_func" );
function gh_plugin_menu_func() {
   add_submenu_page( "options-general.php",  // Which menu parent
                  "GitHub",            // Page title
                  "GitHub",            // Menu title
                  "manage_options",       // Minimum capability (manage_options is an easy way to target administrators)
                  "github",            // Menu slug
                  "gh_plugin_options"     // Callback that prints the markup
               );
}

// Print the markup for the page
function gh_plugin_options() {
   if ( !current_user_can( "manage_options" ) )  {
      wp_die( __( "You do not have sufficient permissions to access this page." ) );
   }
   echo "Hello world!";
}

Now you should be able to log into the dashboard and see the new GitHub menu under “Settings” and then click it to see a blank settings page with “Hello world!”

Empty WordPress plugins page16
Empty WordPress plugins page. (View large version17)

Next, we will replace Hello word with the form’s actual markup. I’ll give you a barebones example that you can style as little or as much as you like.

?>
<form method="post" action="<?php echo admin_url( 'admin-post.php'); ?>">

   <input type="hidden" name="action" value="update_github_settings" />

   <h3><?php _e("GitHub Repository Info", "github-api"); ?></h3>
   <p>
   <label><?php _e("GitHub Organization:", "github-api"); ?></label>
   <input class="" type="text" name="gh_org" value="<?php echo get_option('gh_org'); ?>" />
   </p>

   <p>
   <label><?php _e("GitHub repository (slug):", "github-api"); ?></label>
   <input class="" type="text" name="gh_repo" value="<?php echo get_option('gh_repo'); ?>" />
   </p>

   <input class="button button-primary" type="submit" value="<?php _e("Save", "github-api"); ?>" />

</form>
<?php

The CSS classes I’ve included match those used by WordPress’ core, which is a nice way to get your custom options page to look decent with no additional effort.

Custom WordPress settings page with basic styles18
Custom WordPress settings page with basic styles. (View large version19)

There are two important things to understand about this form. First, the endpoint it submits to must be /wp-admin/admin-post.php. You could hardcode this path, but if the website is installed in a subdirectory it won’t work. So, we use the built-in admin_url()20 to create it dynamically.

Secondly, notice the hidden input with the name action. This field is how WordPress knows what to do with the post request that gets submitted by the form. The value corresponds to the name of the action hook we will use to set the callback function. The name of the action we’ll hook into will be this value, prefixed with admin_post_. So, in our case, we need to add this:

add_action( 'admin_post_update_github_settings', 'github_handle_save' );

I find this to be one of the quirkier, less intuitive aspects of creating custom administration menus, but once you get used to it, it’s pretty painless. As you may have guessed, the second parameter of add_action() is the name of our callback function that will actually save the values to the database.

function github_handle_save() {

   // Get the options that were sent
   $org = (!empty($_POST["gh_org"])) ? $_POST["gh_org"] : NULL;
   $repo = (!empty($_POST["gh_repo"])) ? $_POST["gh_repo"] : NULL;

   // Validation would go here

   // Update the values
   update_option( "gh_repo", $repo, TRUE );
   update_option("gh_org", $org, TRUE);

   // Redirect back to settings page
   // The ?page=github corresponds to the "slug" 
   // set in the fourth parameter of add_submenu_page() above.
   $redirect_url = get_bloginfo("url") . "/wp-admin/options-general.php?page=github&status=success";
   header("Location: ".$redirect_url);
   exit;
}

The example is also quite minimal. In a production setting, you would probably want to add some validation. Also, in this example, we’re automatically passing back status=success to the URL. The form’s markup doesn’t actually use it yet. To conditionally show a success message, add something like the following above the form in gh_plugin_options():

if ( isset($_GET['status']) && $_GET['status']=='success') { 
?>
   <div id="message" class="updated notice is-dismissible">
      <p><?php _e("Settings updated!", "github-api"); ?></p>
      <button type="button" class="notice-dismiss">
         <span class="screen-reader-text"><?php _e("Dismiss this notice.", "github-api"); ?></span>
      </button>
   </div>
<?php
}

If you add validation, use this same pattern to pass back different statuses and messages so that the user knows if and why their submission of the form failed.

Now you should be able to save new owner and repository values. Test it by entering the owner and repository of any public project on GitHub. The final step is to go back to the shortcode callback function github_issues_func() and replace the hardcoded owner and repository values.

…   
$issues = $gh->api("issue")->all(get_option("gh_org"), get_option("gh_repo"));
…

Once this is in place, revisit the page where you added the shortcode. You should now see issues from whichever project you set.

Bonus Round: OAuth 2.0 Authentication Link

The approach we used above works great for public repositories, but what if we want to use this plugin with a private repository that requires authentication? The GitHub API authenticates using the OAuth 2.0 protocol21, which is what you will encounter working with most popular APIs these days. The basic OAuth 2.0 workflow goes like this:

  1. You register an application (sometimes called a “client”) with the provider and receive a unique ID and secret key.
  2. Your application makes a request to the provider’s authentication endpoint, passing the credentials above as well as a redirect URL that the provider uses to redirect the request back to an endpoint of your application.
  3. The user is then prompted to accept your request for access. If they do, the provider uses the URL you sent with the request to redirect the user back to your application, along with a temporary code.
  4. Your application captures this code and then makes a second request, passing this code back to the provider. The provider responds with an access token, which your application then uses to authenticate with the provider.

We’ll start by registering an application22 with GitHub. As with most popular APIs, this section is located in the developer area of the website.

GitHub new application registration page23
GitHub’s new application registration page. (View large version24)

The most important thing here is the authorization callback URL. The redirect URL passed in step three must match what you enter here, or include it. So, during development, I usually enter the home page of the website. This way, my app can redirect to any path.

Next, we’ll add a second form to our settings page, in gh_plugin_options(), for submitting the application’s client ID and secret.

<form method="post" action="<?php echo admin_url( 'admin-post.php'); ?>">

   <input type="hidden" name="action" value="oauth_submit" />

   <h3>Oauth 2.0</h3>
   
   <p>
      <label><?php _e("GitHub Application Client ID:", "github-api"); ?></label>
      <input class="" type="text" name="client_id" value="<?php echo get_option('client_id')?>" />
   </p>
   <p>
      <label><?php _e("GitHub Application Client Secret:", "github-api"); ?></label>
      <input class="" type="password" name="client_secret" value="<?php echo get_option('client_secret')?>" />
   </p>
      

   <input class="button button-primary" type="submit" value="<?php _e("Authorize", "github-api"); ?>" />

</form>

To make life easier, I’ll use the GitHub Provider for OAuth 2.0 Client25 by The League of Extraordinary Packages. So, first, let’s use Composer again to add the dependency:

composer require league/oauth2-github

Note that the GitHub library from KNP Labs also has OAuth 2.0 support built in. So, in my specific example, this is somewhat redundant. But I wanted to introduce this library because it belongs to a suite of provider-specific OAuth 2.0 client libraries that all extend the same framework maintained by the mighty League of Extraordinary Packages26. You can view a complete list of supported providers or read instructions on how to extend the framework to support a new provider27.

Let’s create a function to request and save the token when the user submits the form. Because GitHub is one of the providers already supported, I can copy the example in its documentation with only a couple modifications.

function handle_oauth() {

   // If the form was just submitted, save the values
   // (Step 1 above)
   if ( isset($_POST["client_id"]) && 
         isset($_POST["client_secret"])
   ) {
        
   update_option( "client_id", $_POST["client_id"], TRUE );
   update_option("client_secret", $_POST["client_secret"], TRUE);

   }

   // Get the saved application info
   $client_id = get_option("client_id");
   $client_secret = get_option("client_secret");

   if ($client_id && $client_secret)   
   {
      $provider = new League\OAuth2\Client\Provider\Github([
         "clientId"          =>  $client_id,
         "clientSecret"      =>  $client_secret,
         "redirectUri"       => admin_url("options-general.php?page=github"),
      ]);

   }   

   // If this is a form submission, start the workflow
   // (Step 2)
   if (!isset($_GET["code"]) && $_SERVER["REQUEST_METHOD"] === "POST") {

      // If we don't have an authorization code, then get one
      $authUrl = $provider->getAuthorizationUrl();
      $_SESSION["oauth2state"] = $provider->getState();
      header("Location: ".$authUrl);
      exit;

   // Check given state against previously stored one to mitigate CSRF attack
   // (Step 3 just happened and the user was redirected back)
   } elseif (empty($_GET["state"]) || ($_GET["state"] !== $_SESSION["oauth2state"])) {

      unset($_SESSION["oauth2state"]);
      exit("Invalid state");

   } else {

      // Try to get an access token (using the authorization code grant)
      // (Step 4)
      $token = $provider->getAccessToken("authorization_code", [
         "code" => $_GET["code"]
      ]);
        
      // Save the token for future use
      update_option( "github_token", $token->getToken(), TRUE );

   }
}

And, just as we did with the other form, we need to add the action hook so that the function is called when the form is saved.

add_action( "admin_post_oauth_submit", "handle_oauth" );

Saving the form should now send you to the authorization page of the API provider. After authorization, your application can use the saved token to request data from private repositories to which you have access. The library I’m using by KNP Labs has a handy method for this.

$gh = new MyGithub();
$gh->authenticate( get_option("github_token"), NULL, Github\Client::AUTH_HTTP_TOKEN);

Libraries will differ on precisely how they handle authentication, but one way or another, you will pass in the token, which will then make authenticated requests on behalf of a user.

Conclusion Link

We’ve covered a lot of ground, and I’ve tried to keep the examples as minimal as possible so that the overall workflow remains clear. The complete source code28 from this tutorial is available. I hope you now have a clear understanding of the moving pieces involved in creating a WordPress plugin that consumes third-party service APIs, and hopefully you are inspired to write your own WordPress API plugin.

Final Notes Link

(dp, jb, ml, al)

Footnotes Link

  1. 1 http://wordpress.org/plugins/wp-github-pipeline
  2. 2 https://codex.wordpress.org/Installing_WordPress
  3. 3 https://github.com/
  4. 4 https://getcomposer.org/
  5. 5 https://github.com/KnpLabs/php-github-api
  6. 6 http://guzzle.readthedocs.org/en/latest/
  7. 7 http://php.net/manual/en/book.curl.php
  8. 8 https://wordpress.org/plugins/about/
  9. 9 https://wordpress.org/plugins/about/readme.txt
  10. 10 https://www.smashingmagazine.com/wp-content/uploads/2015/10/plugins-list-opt.png
  11. 11 https://www.smashingmagazine.com/wp-content/uploads/2015/10/plugins-list-opt.png
  12. 12 https://github.com/KnpLabs/php-github-api
  13. 13 https://codex.wordpress.org/Shortcode_API
  14. 14 https://github.com/KnpLabs/php-github-api/blob/master/doc/issues.md
  15. 15 https://en.wikipedia.org/wiki/Dependency_injection
  16. 16 https://www.smashingmagazine.com/wp-content/uploads/2015/10/empty-settings-page-opt.png
  17. 17 https://www.smashingmagazine.com/wp-content/uploads/2015/10/empty-settings-page-opt.png
  18. 18 https://www.smashingmagazine.com/wp-content/uploads/2015/10/plugin-form-opt.png
  19. 19 https://www.smashingmagazine.com/wp-content/uploads/2015/10/plugin-form-opt.png
  20. 20 https://codex.wordpress.org/Function_Reference/admin_url
  21. 21 http://oauth.net/2/
  22. 22 https://github.com/settings/applications/new
  23. 23 https://www.smashingmagazine.com/wp-content/uploads/2015/10/githubnewapp-opt.png
  24. 24 https://www.smashingmagazine.com/wp-content/uploads/2015/10/githubnewapp-opt.png
  25. 25 https://github.com/thephpleague/oauth2-github
  26. 26 https://thephpleague.com/
  27. 27 https://github.com/thephpleague/oauth2-client/blob/master/README.PROVIDER-GUIDE.md
  28. 28 https://github.com/emersonthis/smashing-mag-wordpress-api-demo
  29. 29 https://codex.wordpress.org/
  30. 30 https://developer.github.com/v3/
  31. 31 http://oauth.net/2/
  32. 32 https://getcomposer.org/
  33. 33 https://thephpleague.com/
  34. 34 http://guzzle.readthedocs.org/en/latest/

↑ Back to top Tweet itShare on Facebook

Advertisement

Emerson is a designer, developer, and problem solver. He has been working on the web since 2006. He is currently the CTO of TransitScreen.

  1. 1

    Two suggestions for anyone following the path of this plugin:

    1. WordPress has a number of HTTP wrappers that use cURL with fallback to fopen(). Instead of adding the Guzzle dependency, see if those functions work with your API client needs.

    2. Consider scalability and cache the response from any relatively slow remote calls by using the transient or caching APIs. This is less important if you use static page caching but it’s easy enough to add for a universal performance increase.

    5
  2. 2

    That’s a good article, but I have to mention it’s a quite strange to your statement that the use of API versioning via URL is “good”, because the whole article is based on the Custom Media Type architecture from GitHub or a Custom Header for a specific version.

    It can be troubling for those who are looking to learning RESTful APIs.

    Even more now that WordPress API are staying put.

    3
    • 3

      That’s a fair point. I wasn’t trying to be dogmatic about API versioning via URL specifically. Just trying to encourage folks to consider the long term stability of the API they choose to build against.

      4
  3. 4

    Howdy,

    some suggestions for your code:

    1. you should escape your output to avoid XSS and malformed HTML:

    // wrong
    <input class="" type="text" name="gh_org" value="<?php echo get_option('gh_org'); ?>" />

    // right
    $gh_org = get_option('gh_org');
    <input class="" type="text" name="gh_org" value="<?php echo esc_attr($gh_org); ?>"/>

    2. Your settings- and auth-page should have at least a nonce to avoid misuse and/or malicious usage of your code.

    3. You should sanitize and validate your options before saving them.

    4. You shouldn’t access the $_SESSION directly without testing if the session is started.

    14
  4. 5

    Marius Vetrici

    March 20, 2016 8:35 am

    Hello and thank you for you article.

    I have a short question, why do you need the line copied below? I think you can completely remove it.

    // Don’t forget to close the list
    $return .= “”;

    1
    • 6

      My guess is maybe the smashing code editor strip down his html tab. It should be

      
      $return .= "";
      
      0
    • 7

      It should be: a closing tag fur an unordered but the HTML tags are getting stripped out. I will talk to the editors about fixing it. Thanks for noticing!

      0

↑ Back to top