Dec 17, 2016

Goal

Embed last Instagram Post through a block provided by a custom module, in Drupal 8.

Details

  • Drupal 8

  • Made up theme name is Icecream

Summary

My goal is to use the Instagram API through a custom module so I can add my last Instagram post wherever I like on my site. I will be able to do that if my custom module provides a block. By the end of this post you should be able to have something like this:

Instagram Feed Block

That's how it looks like when I place the created custom module's block in my site's footer.

Getting started

Grab a cup of java and lets get started! I followed a Drupal.org tutorial to create my custom module. If you want more information about custom module creation you should refer to the link.

In the route /httpdocs/modules/ you may or may not have a directory called "custom". If you don't have it, go ahead and create it. This is where you will be storing your custom modules.

You don't actually have to create the /httpdocs/modules/custom directory, you can just as well store your module in /httpdocs/modules , but I thought it would be orderly to keep custom modules under a directory.

cd /httpdcos/modules
​​​​​​​mkdir custom

Go inside the custom directory. We are going to name the module, there are some important rules when selecting the machine name:

  • It must start with a letter

  • It must contain only lower-case letters and underscores

  • It must not contain any spaces

  • It must be unique. Your module may not have the same short name as any other module, theme, or installation profile you will be using on the site

  • It may not be any of the reserved terms: src, lib, vendor, assets, css, files, images, js, misc, templates, includes, fixtures, Drupal

  • DO NOT USE UPPER-CASE LETTERS, since Drupal will not recognize hook implementations.

For my case, I chose instragram_feed, so I'll create a directory with that name. Keep in mind that you don't have to use the same machine name for the folder's name, but I thought it would be a good idea to keep it all cohesive.

mkdir instagram_feed

Go ahead and enter the new directory. Unless specified, the rest of the actions are going to occur inside the instagram_feed directory.

Let Drupal 8 see your module

Announce your module to Drupal 8 through a .info.yml file.

It's necessary to create a .info.yml file so that Drupal 8 can know about the custom module. In the created directory instagram_feed, create the file. The name of the file MUST be the machine_name you chose for your custom module. In my case, that would be instagram_feed.

Whenever you make changes to your module's code (be it PHP, or TWIG or in the .info.yml file) be sure to clear cache for Drupal to assimilate those changes.

touch instagram_feed.info.yml

You'll have to edit the created file with the editor of your choice and populate it with the following.

name: Instagram Feed Module
description: Shows recent instagram media
package: Custom

type: module
core: 8.x

If the module you are creating has dependencies, be sure to check out the link to the official Drupal documentation for a complete example.

The first three lines are primarily used in the administration UI when allowing users to enable/disable the module. The name and description (both required) provide the text that is shown on the module administration page and the package allows you to group similar modules together. Core, for example, uses package: Core.

Type is required and indicates the extension, e.g. module, theme or profile.

For modules hosted on drupal.org, the version number will be filled in by the packaging script, you should not specify it manually, but leave out the version line entirely.

The core key is required and specifies the version of Drupal core that your module is compatible with.

If you save this, you should be able to see your module listed in the URL /admin/modules. Don't see it? Then check out the debugging section of the official Drupal 8 documentation for adding custom modules.

Module is visible

Adding a composer.json file

touch composer.json

Edit the file and add the following.

{
  "name": "drupal/instagram_feed",
  "description": "Shows recent instagram media.",
  "type": "drupal-module",
  "homepage": "https://chayemor.com",
  "authors": [
    {
      "name": "Johanna Mesa Ramos (Chayemor)",
      "homepage": "https://chayemor.com",
      "role": "Maintainer"
    }
  ]
}

You can define external dependencies in the composer.json file, but it won't work out of the box. Check out the official Drupal documentation to learn how it will work.

Adding a .module file

This step is about adding a .module file in your custom block's directory (my case that would be instagram_feed).

pwd
.../httpdcos/modules/custom/instagram_feed
touch instagram_feed.module

Now the editing part.

<?php

// Imeplements hook_theme()
function instagram_feed_theme() {
  return [
    'instagram_feed' => [ //Value to use in #theme for block
      'variables' => ['image' => NULL ] //Variable passed to twig file
    ],
  ];
}

The .module stores theming information, learn more about it in the official Drupal documentation. As can be read in the code, instagram_feed is the name of the template (and the value to be used for the key #theme when coding the block). The variables are what the template file will have access to.

Creating a custom block

To create a custom block you'll need to create the following structure inside your instagram_feed directory.

Directory structure

pwd
.../httpdcos/modules/custom/instagram_feed
mkdir src
cd src
mkdir Plugin
cd Plugin
mkdir Block
cd Block

Inside the Block directory you'll need to create the PHP file that will hold the code that interacts with Instagram. The name of the file must match the name you use for the class inside of it.

touch InstagramFeedBlock.php

Edit the created PHP file with the editor of your choice, and populate it with the following.

<?php

namespace Drupal\instagram_feed\Plugin\Block;

use Drupal\Core\Block\BlockBase;

/**
 * Provides recent Instagram feed Block
  * @Block(
 *   id = "instagram_feed_block",
 *   admin_label = @Translation("Instagram Feed Block"),
 *   category = @Translation("Instagram Feed")
 * )
 */
class InstagramFeedBlock extends BlockBase {
  /**
   * {@inheritdoc}
   */
  public function build() {
    $embed = $this->get_instagram_feed_data();
	return [
		'#theme' => 'instagram_feed',
		'#image' => $embed
	];
  }
  
	public function get_instagram_feed_data(){
		//Code that gets the user's URL to their last Instagram post
	}
	
	public function get_instagram_embed($link){
		//Code that gets the embedded Instagram post code
	}	
  
}

To finish the PHP code, first we have to get some information from Instagram.

Getting Instagram Access Token

The proper way to approach the Instagram part of the module would be create a Configuration page and let the web administrator input the Instagram Client's (you'll create one now, in the next step) Client ID, and a button that when clicked retrieves and stores the user's access token. Because I am the user Instagram and web administrator, I bypassed that and do it all in code, but it's not the best approach.

Head over to Instagram Developers Manage Clients. Of course you will need to create an Instagram Developer Account or use your Instagram Account and sign in. After that, go ahead and "Register a New Client". Here's how I populated mine:

Instagram Access Token

Alright! You have a registered Client, next move? Getting that Access Token. If you want to dig into the details, be sure to read Instagram's Authentication section.

Your code should handle the case that either the user revokes access, or Instagram expires the access token. In other words: DO NOT assume your access_token is valid forever.

To get the Access Token I am going to go through the Server-side (Explicit) Flow, for that I will need the Instagram Client I just created (LastPostAccess), specifically I will need its CLIENT ID. You can find that in the general dashboard of your Instagram clients.

Dashboard client ID​​​​​​​

Open a tab in your browser and paste the following link:

https://api.instagram.com/oauth/authorize/?client_id=YOUR_CLIENT_ID&redirect_uri=http://chayemor.com/&response_type=code

Make sure that the redirect_uri that you use in the above URL matches exactly the one that can be seen when you select your Instagram Client (by clicking Manage on the general dashboard ) and then select the Security tab and look for "Valid redirect URIs".

Valid redirect uris​​​​​​​

When you hit enter after entering that URL you will get the following message if you are already signed into Instagram (if not you'll be asked to sign into Instagram and then you'll be shown this message). Remember you must use the Instagram account you want to pull the last post from.

Authorize Instagram Client

In order to allow the Instagram User to authorize the app to access their media and profile info they must be a sandbox user for the Instagram Client you've created.  If you are using the same Instagram account for both developing the Instagram Client and authorizing it, then you'll have no problem, just hit "Authorize", otherwise navigate to your general dashboard, manage the client you've created, go over to the "Sandbox" tab and enter the Instagram user name you want to become a sandbox user. After that you'll be able to use that Instagram user to authorize and let the Instagram Client access their media. Head over to Instagram Sandbox Mode documentation to learn more about it and how to take your Instagram Client live.

After you select Authorize you'll be taken to your redirect_uri and you'll have the data for the next step appended to your URL.

http://chayemor.com/?code=0ec728e8e2f84f4199d60aaa9fe79140

The next step is to request the Access Token (yay!). To that you'll need the following data: Client ID, Client Secret (it's right below the Client ID when you manage your Instagram Client), redirect uri and code (the one you just received on the URL). In order to be able to request the Access Token the petition must be a POST, so you can't just do it over the address bar (that would be a GET). There's a lot of online tools that let you do POST requests, but my go to tool is Postman , I use the Chrome app. You'll arrange the POST so that it looks as follows:

POST URL: https://api.instagram.com/oauth/access_token
PARAMETERS:
   client_secret = CLIENT_SECRET,
   client_id = CLIENT_ID,
   grant_type = authorization_code,
   redirect_uri = http://chayemor.com/,
   code = YOUR_CODE
 

PostMan Set up

The answer after you send that POST will be in JSON format and it will look like this:

{
  "access_token": "1609140958.59250a5.17ec36bd1a7e4f5eb2d89b850d8ac09d",
  "user": {
    "username": "chayemor",
    "bio": "Coding, illustrating, acting, and writing. That's me. Let's meet every Monday!",
    "website": "",
    "profile_picture": "https://scontent.cdninstagram.com/t51.2885-19/s150x150/13422808_82294546_a.jpg",
    "full_name": "JMR",
    "id": "1333147778"
  }
}

There's your Access Token right there, hooray! Make note that the response also includes an important piece of data, the user's ID. You'll need that in the next step

PHP code that interacts with Instagram API

class InstagramFeedBlock extends BlockBase {
  /**
   * {@inheritdoc}
   */
  public function build() {
    $embed = $this->get_instagram_feed_data();
	return [
		'#theme' => 'instagram_feed',
		'#image' => $embed
	];
  }
  
	public function get_instagram_feed_data(){
		$recent_media_link = false;
		
		$client = \Drupal::httpClient();
		$req = $client->get('http://api.instagram.com/v1/users/YOUR_USER_ID/media/recent/?access_token=ACCESS_TOKEN_FROM_LAST_STEP&count=1');	
	    $response = (string) $req->getBody(true);
		$response_json = json_decode($response, true);
		
		if(array_key_exists('data', $response_json))
			$recent_media_link = $response_json['data'][0]['link'];
		
		if(!$recent_media_link) //Couldn't get most recent media link
			return NULL;
			
		return $this->get_instagram_embed($recent_media_link);
	}
	
	public function get_instagram_embed($link){
		$client = \Drupal::httpClient();
		$req = $client->get('https://api.instagram.com/oembed/?url='.$link);	
	    $response = (string) $req->getBody(true);
		$response_json = json_decode($response, true);
		return $response_json['html'];
	}	
  
}

To briefly explain what's going on in the code above. It all start with the build() function. This is in charge of obtaining the data and passing it to the TWIG template (the one defined with the #theme parameter) through the variable #image. The get_instagram_feed_data() function is in charge of getting the URL to the last post the user had done in their Instagram. To do that you need to query a specific URL with the following sensitive data: user's id and your Instagram Client access token. All of these were obtained in previous steps. If the request is successful and you do get a response back, then all that's left to do is acquire the Instagram embed code. That is done in the get_instagram_embed($link) function, using the URL we just got on the previous function. And that's it. The next part is setting the TWIG profile so we can see the outcome.

A brief mention here is that Google's PageSpeed Tools will complain about how little the links that are in the hashtags for mobile users (taking away a couple of points for User Experience). If you want to avoid that (which I did), you can add the parameter hidecaption=true to the URL that fetches the embed code, so it would look like this: $req = $client->get('https://api.instagram.com/oembed/?hidecaption=true&url='.$link);    

Creating TWIG template

Head over to your custom module's root folder. In my case that would be /httpdocs/modules/custom/instagram_feed and do the following.

mkdir templates
cd templates
touch instagram-feed.html.twig

The name of the TWIG template must match that one used in your build() function back in your BlockBase class, specifically the value for #theme. Note that if you include '_' in the name, then when you create the file you have to substitute '_' for '-' , for it to work. 

public function build() {
    $embed = $this->get_instagram_feed_data();
	return [
		'#theme' => 'instagram_feed',
		'#image' => $embed
	];
  }

The TWIG file will contain the HTML you want to output whenever you place the block in a region of your site. Mine is very simple:

{% if image  %}
	{{ image | raw }}
{% endif %}

Keep in mind that the variable image contains the embed code that was obtained in the PHP code. the | raw print is because if not done like so then Twig will print the embed code as a string, and that won't work.

Finishing up

All that's left to do is enable your module (in case you haven't already done so), head over to your site's Block Layout and place your block.

Place block

Well done!

Final Comments

The module is far from perfect, there are still many things one could do to improve it. Like exposing a configuration form so that the application's Client ID can be set and saved and the Access Token can be accessed programmatically rather than manually. This would allow the module to retrieve a new Access Token if it ever does an Instagram Request and gets and error of access denied.