How to integrate Instagram Basic Display API using PHP 7

How to integrate Instagram Basic Display API using PHP 7 | Photo Credits Cottonbro on Pexelscom

By on Thu Jul 15 in Instagram, Integration, PHP


1
(1)

Introduction

This article is about the Instagram Graph API, in particular explains how to integrate the Instagram Basic Display API into PHP 7, and offers a solution for integrating API calls to your Instagram profile on your website, retrieving media from it and making it available on your website. We will use PHP as programming language.

It is therefore a small PHP application that you can store in a subfolder of your website. You can modify it at will to save the media in different paths other than the one already encoded in this starting application.

The only prerequisite, apart from knowledge of PHP, is to have Composer already installed, or to install it if necessary, because we will install several PHP packages to make things easier and development faster. Useful for managing dependencies later on.

You can find all the code on Github; is available for updates, modifications and bug fixing:
gmaccario/instagram-basic-display-api-php-integration

Instagram API

An important thing to keep in mind is that there are two types of Instagram APIs:

  • Instagram Graph API: for businesses and Instagram creators who need to fully control all of their social media interactions;
  • Instagram Basic Display API: access any type of Instagram account, but only provides read access to basic data.

In a nutshell: you must use Instagram Graph API if you are building an app that will allow companies or Instagram creators to post media, moderate comments, identify @quoted and #hashtagged media, or get data about other Instagram users, while the Instagram Basic Display API can be used to access any type of Instagram account, but only provides read access to basic data.

This article deals with the second one: Instagram Basic Display API.

The simplicity

The simplicity of uploading a photo on Instagram and finding it shortly afterwards on your own website.

A short time ago (June 2021) I integrated the Instagram Basic Display API on the website of a client who wanted an easy way to add new photos to his website. The client is the owner of a restaurant who uses Instagram quite frequently to share the best photos of the restaurant’s dishes. A CMS such as WordPress for example is certainly a very easy system to use, you don’t need extreme computer skills, on the contrary! However, we must take into account the needs of the customer. For the owner of a restaurant who is already used to share pictures on Instagram very well on a personal level, it is much easier to open the Instagram app and share a photo that has just been taken rather than:

  • opening the browser, log in into your WordPress site
  • click on Media in the sidebar of the administration panel
  • click on Add New
  • search for the image on your device and upload a new photo
  • set title and caption
  • then place it in the correct gallery‚Ķ

I can assure you that uploading a photo to their own WordPress site is not a simple and immediate task for a person who is not technically proficient. I am thinking of people that works away from the digital world on a daily basis, and who probably has very little time to be online, . That’s why I created this PHP/Instagram integration.

I created a script that uses a Long Lived Token to retrieve the media from an Instagram profile. It is very easy to save the files and information on your server: media on a filesystem and the metadata on a database, or on text files such as json.

Now let’s see how to integrate Instagram Basic Display API using PHP 7 and implement the scrip. We will run the script using a cron job.

Composer e autoload

Using Composer we have the possibility to write a single include (or require) and call a single file to make sure to include our class files automatically. The file in question is the autoload.php in the vendor directory.

First of all, we create a file called composer.json inside the project’s main folder and we define the path that will be used to load our classes, in this format:

{
    "autoload": {
        "psr-4": {
            "App\\": "src/app/"
        }
    }
}

App is the namespace of our classes which will be located in the ./src/app/ path.

Open a terminal, move to the folder of your project and run the command:

composer update -vvv 

The -vvv parameter indicates the verbosity of the log in the terminal, used for debugging purposes. It will print out all the running steps on the screen.

Composer will create the vendor folder, and from now on we are able to call classes within the project via their namespaces, without having to include the path to their files with multiple includes or requires.

Finally, we need to include the autoader in our entry point file. Let’s start by creating an index.php file in the project’s root directory and write this code inside:

require __DIR__ . '/vendor/autoload.php';

Il file .htaccess

We are ready to write our classes in ./src/app/ and call them using their namespaces. We will see the specific code later, but first we must secure the main folder using the .htaccess file (for Apache) and send all incoming requests to a single file: the index.php file in the public folder.

Then we create an .htaccess file in the project’s main folder and write this code in it:

RewriteEngine On 
RewriteRule ^$ public/index.php [L]
RewriteRule ^((?!public/).*)$ public/$1 [L,NC]

Create a ./public folder and move the previous ./index.php into the ./public folder. Finally, we modify the path of the autoloader by adding an additional level. All incoming requests will now be sent to ./public/index.php:

require __DIR__ . '/../vendor/autoload.php';

All incoming requests will now be sent to ./public/index.php.

Entry point

Therefore the entry point of our application is ./public/index.php and, for now, it takes care of calling the autoloader. Later, it will also take care of understanding the user’s input and then perform the correct operation. In this application we will not install a routing system, so we will intercept the user input by native PHP functions, specifically filter_input, sanitised and filtered to launch the correct logic.

The main actions of the application are:

  • log in into Instagram
  • retrieving the OAuth callback code
  • requesting a Long Lived Token and saving it encrypted
  • retrieving the media and saving it.

Configuration

We create a configuration file to be able to manipulate the events. Then we create a folder named ./config in which we’ll create a file named config.php. The file will return a PHP list, here are its contents:

<?php 

return array(
    'secretKey'     => '', // used to encrypt the token
    'appId'         => '', // Instagram App ID
    'appSecret'     => '', // Instagram App Secret
    'redirectUri'   => '', // Valid OAuth Redirect URIs

    'limit_per_page'=> 8,

    'exclude'       => array(
        // list of IG filenames to be excluded, e.g.: '1262329994_225747415234351_9142654959463375561_n.jpg',
    ),

    'debug'         => true,
    'token_file'    => '..' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'token.json', // name of the file where the encrypted token will be stored
);

Espresso Instagram Basic Display API

To get our Instagram account information at the PHP code level, we can write all the necessary calls using the PHP’s cURL library (Client URL Library), or simply use PHP wrapper. In this case we will use a wrapper, check it at this address. We will use composer to install this package in our project. Open a terminal, go to your project folder and run the command:

composer require espresso-dev/instagram-basic-display-php

We will see later how to use the wrapper, but in the meantime, we can open the ./vendor folder to check that the new espresso-dev folder has been installed correctly.

The other libraries

With composer, we also install the rest of the libraries that we will use in the application:

composer require lablnet/encryption
composer require monolog/monolog
composer require nesbot/carbon

The first one helps us for token encryption, the second one provides a simple interface for saving log messages and the last one will be used for working with dates.

Application classes

As we saw earlier in the section on Composer, the classes of our application must be saved in the ./src/app/ folder. The classes we need are:

  • IGBase: parent class that contains some common methods;
  • IGMedia: used to get media from Instagram;
  • IGToken: used for token operations.

IGBase

It contains the methods used by its daughter classes getInstagramBasicDisplayByKeys() and getInstagramBasicDisplayByToken() which return an InstagramBasicDisplay object from the Espresso Instagram Basic Display API wrapper. In addition, the constructor take the configuration array as input, so the configuration values will also be available to classes that extend IGBase.

<?php 

namespace App;

use Lablnet\Encryption;
use EspressoDev\InstagramBasicDisplay\InstagramBasicDisplay;

class IGBase
{
    // Protected because I need it in child classes, same for methods
    protected $config;

    public function __construct(array $config)
    {
        $this->config = $config;
    }

    public function getInstagramBasicDisplayByKeys()
    {
        return new InstagramBasicDisplay([
            'appId'         => $this->config['appId'],
            'appSecret'     => $this->config['appSecret'],
            'redirectUri'   => $this->config['redirectUri'],
        ]);
    }

    protected function getInstagramBasicDisplayByToken()
    {
        if(!\file_exists($this->config['token_file']))
        {
            die('Please, set up your long live token before anything else. Check out the README file.');
        }

        // Get long lived token from config json
        $tokenInfo = json_decode(file_get_contents($this->config['token_file']), true);

        // Decrypt the token
        $encryption = new Encryption($this->config['secretKey']);
        $decryptedToken = $encryption->decrypt($tokenInfo["access_token"]);    

        // Instantiate a new InstagramBasicDisplay object
        $instagram = new InstagramBasicDisplay($decryptedToken);

        // Set user access token
        $instagram->setAccessToken($decryptedToken);

        return $instagram;
    }
}

IGMedia

It contains the getMedia() method which returns the json-encoded value of the curl_exec() call of the InstagramBasicDisplay wrapper.

<?php 

namespace App;

class IGMedia extends IGBase
{
    public function __construct(array $config)
    {
        parent::__construct($config);
    }

    public function getMedia()
    {
        $instagram = $this->getInstagramBasicDisplayByToken();

        return $instagram->getUserMedia('me', $this->config['limit_per_page']);
    }
}

This is the response from getMedia(), or rather, the response from $instagram->getUserMedia():

stdClass Object
(
    [data] => Array
        (
            [0] => stdClass Object
                (
                    [caption]
                    [id]
                    [media_type] | es. IMAGE
                    [media_url] 
                    [permalink] 
                    [timestamp] 
                    [username] 
                )

            [1] => stdClass Object
                (
                    [caption]
                    [id]
                    [media_type] | es. IMAGE
                    [media_url] 
                    [permalink] 
                    [timestamp] 
                    [username] 
                )

            [2] => stdClass Object
                (
                    [caption]
                    [id]
                    [media_type] | es. IMAGE
                    [media_url] 
                    [permalink] 
                    [timestamp] 
                    [username] 
                )

        )

    [paging] => stdClass Object
        (
            [cursors] => stdClass Object
                (
                    [before] => 
                    [after] => 
                )

            [next] => 
        )

)

The paging attribute represents the paging returned by Instagram, thanks to which we can move between pages of results.

IGToken

It contains the methods used to save the encrypted Long-Lived Token to a file in the ./config directory and to request a new token when the previous one expires.

<?php 

namespace App;

use Carbon\Carbon;
use Lablnet\Encryption;

class IGToken extends IGBase
{
    private $longLivedToken;

    public function __construct(array $config, string $longLivedToken = '')
    {
        parent::__construct($config);

        $this->longLivedToken = $longLivedToken;
    }

    public function getLongLivedToken()
    {
        return $this->longLivedToken;
    }

    public function setLongLivedToken(string $token)
    {
        return $this->longLivedToken = $token;
    }

    public function encryptNewToken()
    {
        if(!$this->longLivedToken)
        {
            die('Please, send the right value for the long-lived-token parameter.');
        }

        // Encrypt the token
        $encryption = new Encryption($this->config['secretKey']);
        $cryptedToken = $encryption->encrypt($this->longLivedToken);

        // Save on file for later
        return file_put_contents($this->config['token_file'], json_encode(
            array(
                "access_token" => $cryptedToken,
                "token_type" => 'init',
                "expires_in" => Carbon::now()->addMonths(2)->timestamp,
            )
        ));
    }

    public function refreshToken()
    {
        // Get long lived token from config json
        $tokenInfo = json_decode(file_get_contents($this->config['token_file']), true);

        // Decrypt the token
        $encryption = new Encryption($this->config['secretKey']);
        $decryptedToken = $encryption->decrypt($tokenInfo["access_token"]);    

        // Get long lived token from config json
        $tokenInfo = json_decode(file_get_contents($this->config['token_file']), true);

        // Check the expiration date
        $expiresInFinalTimestamp = $tokenInfo['expires_in'] + Carbon::now()->timestamp;

        // One week before the expiration
        $oneWeekBeforeExpirationTimestamp = ($tokenInfo['expires_in'] + Carbon::now()->timestamp) - 604800;

        // IG Token usually expires after 2 months.
        // Refresh the token only if the now is greater than one week before expiration. 
        // Note for thest purpose: Carbon::now()->addMonths(3)->timestamp INSTEAD OF Carbon::now()->timestamp
        if(Carbon::now()->timestamp > $oneWeekBeforeExpirationTimestamp)
        {
            $instagram = $this->getInstagramBasicDisplayByToken();
            $oAuthTokenAndExpiresDate = $instagram->refreshToken($decryptedToken);

            // $oAuthTokenAndExpiresDate->access_token;
            // $oAuthTokenAndExpiresDate->token_type;
            // $oAuthTokenAndExpiresDate->expires_in;

            if(isset($oAuthTokenAndExpiresDate->access_token))
            {
                // Encrypt the token
                $encryption = new Encryption($this->config['secretKey']);
                $cryptedToken = $encryption->encrypt($oAuthTokenAndExpiresDate->access_token);

                // Save on file for later
                file_put_contents($this->config['token_file'], json_encode(
                    array(
                        "access_token" => $cryptedToken,
                        "token_type" => $oAuthTokenAndExpiresDate->token_type,
                        "expires_in" => $oAuthTokenAndExpiresDate->expires_in,
                    )
                ));

                return true;
            }
        }

        return false;
    }
}

Preparing the entry point file

Now that we have our classes in place, we can see how to integrate the Instagram Basic Display API into PHP 7. Let’s go back to our ./public/index.php and complete the rest of the code.

After the require line, we can add the classes we will be using:

use App\Utility; // dobbiamo ancora esplorarla!
use App\IGMedia;
use App\IGToken;
use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;

Then we will retrieve the config array and display the errors if we are in debug mode:

$config = include '..' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'config.php';

if($config['debug'])
{
    ini_set('display_errors', 1);
    ini_set('display_startup_errors', 1);
    error_reporting(E_ALL);    
}

We can also set up a log manager, which will save the log file with a name that changes according to the day:

// Add log handler
$logger     = new Logger('ig-integration');
$handler    = new RotatingFileHandler('logs/ig-log.log', 0, Logger::INFO, true, 0664);
$handler->setFilenameFormat('{date}-{filename}', 'Y-m-d');
$logger->pushHandler($handler);

The application logic

As I wrote earlier, this application does not have a routing system: all we have to do then is to retrieve the inputs from the query parameters in the request url and then manage the values.

// Get values from query parameters
$code           = filter_input(INPUT_GET, 'code', FILTER_SANITIZE_STRING);
$igAction       = filter_input(INPUT_GET, 'action', FILTER_SANITIZE_STRING);
$longLivedToken = filter_input(INPUT_GET, 'long-lived-token', FILTER_SANITIZE_STRING);

At this point, everything it is much simpler, because we have already prepared what we need. Simply we have to separate the actions and call up the classes involved:

if(isset($config) && $longLivedToken)
{
    // No config, no execution
    if(!$config['debug'])
    {
        $logger->info('Production environment. This action is not supported here.');
        return;
    }

    $logger->info('Start encrypt token');

    // Instagram Token class
    $iGToken = new IGToken($config, $longLivedToken);
    $result = $iGToken->encryptNewToken();

    if($result !== false)
    {
        $logger->info('Success! Please check your new token in ./config/token.json');
    }
    else {
        $logger->info('Error! An error occurred saving the token');
    }

    return;
}
else if($igAction == 'get-media') {

    $logger->info('Start IG get media');

    // Instagram Media class
    $iGMedia = new IGMedia($config, $longLivedToken);
    $media = $iGMedia->getMedia(); // stdClass->data

    $logger->info('Found ' . count($media->data) . ' media.');

    $iGToken = new IGToken($config);
    $result = $iGToken->refreshToken();

    $logger->info('Token ' . ($result ? 'refreshed' : 'not refreshed'));

    // App utilities
    $utility = new Utility();
    $result = $utility->filterMedia($media->data, 'media' . DIRECTORY_SEPARATOR);

    $logger->info('Filtered ' . count($result) . ' media.');

    return;
}
else {

    // Authenticate user (OAuth2)
    if(!empty($code))
    {
        // Ready for the login
        $iGToken = new IGToken($config);
        $instagram = $iGToken->getInstagramBasicDisplayByKeys();

        $logger->info('Code from Instagram: ' . $code);

        // Get the short lived access token (valid for 1 hour)
        $token = $instagram->getOAuthToken($code, true);

        // Exchange this token for a long lived token (valid for 60 days)
        $token = $instagram->getLongLivedToken($token, true);

        // Instagram Token class
        $iGToken->setLongLivedToken($token);

        $result = $iGToken->encryptNewToken();

        $logger->info('Encrypted new token successfully.');

        echo "<a href='?action=get-media'>Get media</a>";

        return;
    }
}


// Ready for the login and initialize the class
$iGToken = new IGToken($config);
$instagram = $iGToken->getInstagramBasicDisplayByKeys();

echo "<a href='{$instagram->getLoginUrl()}'>Instagram Login</a>";

I kept the Utility class (also under ./src/app) for the last. Its function is to perform operations that are not related to Instagram, but that use its data. For example, I have implemented a method, modifiable at will, to filter and save media. Then, your site will be able to read media and info and upload them to specific pages, or within photo galleries.

<?php 

namespace App;

class Utility
{
    // Filter media type by $filter from IG API
    public function filterMedia(array $media, string $targetDir, array $exclude = array(), $filter = 'IMAGE', $action = 'save-media')
    {
        $mediaOutput = array();
        foreach($media as $m)
        {
            if($m->media_type == $filter)
            {
                $originalFilename = basename(parse_url($m->media_url, PHP_URL_PATH));

                //echo $m->media_url . " ||| " . $originalFilename . "<br />";
                if(in_array($originalFilename, $exclude))
                {
                    continue;
                }

                $ext = pathinfo($originalFilename, PATHINFO_EXTENSION);

                $m->filename = substr($this->slugify($m->caption), 0, 75) . '.' . $ext;

                if($action != 'save-media')
                {
                    // Set output image
                    $mediaOutput[] = $m;
                }
                else {
                    // Save image
                    $result = $this->createFolderIfNotExists($targetDir);
                    $result = $this->convertJpgToWebpORPng($targetDir, $m->filename, $m->media_url);

                    // New webp extension
                    $m->filename = str_replace('jpg', $result['conversion'], $m->filename);

                    if(file_exists($result['path']) !== false)
                    {
                        // Set output image
                        $mediaOutput[] = $m;
                    }
                }
            }
        }

        // Collect info
        if($action == 'save-media')
        {
            file_put_contents($targetDir . 'info.json', json_encode($mediaOutput));
        }


        return $mediaOutput;
    }

    public function slugify(string $text)
    {
        // replace non letter or digits by -
        $text = preg_replace('~[^\pL\d]+~u', '-', $text);

        // transliterate
        $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);

        // remove unwanted characters
        $text = preg_replace('~[^-\w]+~', '', $text);

        // trim
        $text = trim($text, '-');

        // remove duplicate -
        $text = preg_replace('~-+~', '-', $text);

        // lowercase
        $text = strtolower($text);

        if (empty($text)) {
            return 'n-a';
        }

        return $text;
    }

    private function convertJpgToWebpORPng($targetDir, $filename, $fileurl)
    {
        $jpg = \imagecreatefromjpeg($fileurl);

        \imagepalettetotruecolor($jpg);
        \imagealphablending($jpg, true);
        \imagesavealpha($jpg, true);

        $path = '';
        $conversion = '';
        if(function_exists('imagewebp'))
        {
            $conversion = 'webp';

            $path = $targetDir . str_replace('jpg', $conversion, $filename);

            \imagewebp($jpg, $path, 100);
        }
        else {
            $conversion = 'png';

            $path = $targetDir . str_replace('jpg', $conversion, $filename);

            \imagepng($jpg, $path, 7);
        }

        \imagedestroy($jpg);

        return array(
            'path'          => $path,
            'conversion'    => $conversion,
        );
    }

    private function createFolderIfNotExists(string $path)
    {
        if (!file_exists($path)) 
        {
            mkdir($path, 0777, true);
        }
    }
}

Use a cron job

Using a cron job we will be able to launch our application every minute/hour, or whenever you think it is necessary. So, while you are in a park and you take a photo, you share it on Instagram: when the cron job will execute the Instagram/PHP integration, your new photo will appear on your site!

Moreover, by using a cron job, the API calls are decoupled from the execution of the website. The latter only has to read the media information from a file that is always up-to-date and, above all, on the same server as the images.

Conclusion

We have seen how to integrate the Instagram Basic Display API into PHP 7.

The script is editable and can be installed in a subfolder of your site. Modify it according to the needs of your site to read the information obtained. Launch it via a cron job and share your images on Instagram to see them shared on your site.

Don’t hesitate to write to me via the website or on Linkedin, or open an issue on Github.

See you soon!

How useful was this post?

Click on a star to rate it!

Average rating 1 / 5. Vote count: 1

No votes so far! Be the first to rate this post.