Taxonomy Icon

Cloud

Up until a few years ago, I’d turn on the TV and find myself humming Springsteen’s “57 Channels and Nothin’ On” as I flipped through one boring channel after another. Not anymore! With internet-only streaming services like Netflix, Amazon Video, and Hulu bringing out original content on an almost-weekly basis, there’s never a shortage of something new and interesting to watch. In fact, there are so many new shows releasing across the various streaming services at any given time that it’s actually become a problem to keep track of all of them.

Of course, each service lets you store your own list of must-see shows, but there’s a flaw: you can’t share your watchlist between services. So, I decided to solve this problem by building an IBM Cloud application to store my personal watchlist for movies and TV shows in a service-agnostic fashion. This tutorial shows you the steps I followed to create this app.

Learning objectives

The example application in this tutorial allows users to search for movies and TV shows by name, then add selected items to a personal watchlist. Title information and other metadata is retrieved from The Movie Database (TMDb) API. Behind the scenes, the application uses the IBM Cloud Cloudant service, which provides a Cloudant database in the cloud, together with the Slim PHP micro-framework to process requests and Bootstrap to create a mobile-optimized user experience.

After completing this tutorial, you can complete the following tasks:

  • Develop a PHP-based web application for IBM Cloud.
  • Integrate data from a third-party REST API with your IBM Cloud application.

Although this IBM Cloud application uses PHP with the Cloudant service, similar logic can be applied to other languages and other IBM Cloud services.

The complete source code for this application is available on GitHub.

Prerequisites

Before starting, make sure you have the following environment:

NOTE: Any application that uses the Cloudant service on IBM Cloud must comply with the corresponding Terms of Service. Similarly, any application that uses the TMDb API and IBM Cloud must comply with their respective terms of use, described on the The Movie Database API and IBM Cloud. Before beginning your project, spend a few minutes reading these terms and ensuring that your application complies with them.

Estimated time

This tutorial consists of two parts. It should take you approximately 60 minutes to complete each part.

Steps

In the first part of this tutorial series, you complete the following steps:

  1. Create the application skeleton.
  2. Create an application initialization script.
  3. Integrate the Twig template engine.
  4. Create the application home page.
  5. Connect to The Movie Database API.
  6. Process search queries and display search results.

Step 1: Create the application skeleton

To start, initialize a basic application with the Slim PHP micro-framework. Create the directories $APP_ROOT/public for all web-accessible files and $APP_ROOT/templates for all page templates. Here, $APP_ROOT refers to your local working directory, which is named myapp in this example.

cd myapp
mkdir public templates

If you have an Apache and PHP7 development environment (optional), you should point your Apache Web server’s document root to $APP_ROOT/public.

Next, create the following Composer configuration file, which should be saved to $APP_ROOT/composer.json:

{
 "require": {
 "php": ">=7.0.0",
 "slim/slim": "^3.10",
 "slim/twig-view": "^2.4",
 "guzzlehttp/guzzle": "6.3.3"
 }
}

Use Composer to install Slim and other required components with the following command:

composer install

Finally, create the file $APP_ROOT/config.php file with the following information (you add more parameters to it later in the tutorial):

<?php
// config.php
$config = [
 'settings' => [
 'displayErrorDetails' => true,
 ]
];

Step 2: Create the application initialization script

The next step is to create a script that initializes the PHP application using Slim conventions. This script eventually contains callbacks for all of the application’s routes, with each callback defining the code that is run when the route is matched to an incoming request. Create this script at $APP_ROOT/public/index.php with the following content:

<?php
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;

// autoload files
require '../vendor/autoload.php';
require '../config.php';

// configure Slim application instance
// initialize application
$app = new \Slim\App($config);

// initialize dependency injection container
$container = $app->getContainer();

$app->run();

Add the following Apache configuration as $APP_ROOT/public/.htaccess so that Apache redirects all requests to the previous script:

<IfModule mod_rewrite.c>
 RewriteEngine On
 RewriteCond %{REQUEST_FILENAME} !-f
 RewriteCond %{REQUEST_FILENAME} !-d
 RewriteRule ^ index.php [QSA,L]
</IfModule>

Step 3: Integrate the Twig template engine

The next step is to integrate the Twig templating component with the Slim PHP application. You use Twig to define a common page template that can be reused for all the pages of the application. Integrate Twig into the application code by adding it to the Slim dependency injection (DI) container as shown in the following example:

<?php
// index.php
// ...

// add view renderer to DI container
$container['view'] = function ($container) {
  $view = new \Slim\Views\Twig("../templates/");
  $router = $container->get('router');
  $uri = \Slim\Http\Uri::createFromEnvironment(new \Slim\Http\Environment($_SERVER));
  $view->addExtension(new \Slim\Views\TwigExtension($router, $uri));
  return $view;
};

// ...
$app->run();

This code initializes the Slim Twig extension and configures it to look in the $APP_ROOT/templates directory for template files. With this code in place, the Twig engine can be accessed from any of the Slim callback handlers with $this->view.

Step 4: Create the application home page

Slim works by defining callback functions for HTTP methods and endpoints. It calls the corresponding Slim method – get() for GET requests, post() for POST requests, and so on. It passes the route to be matched as the first argument to the method. The second argument to the method is an anonymous function, which specifies the actions to be taken when the route is matched to an incoming request.

To see this step in action, create a template for the application home page at $APP_ROOT/templates/home.twig with the following content:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
  </head>
  <body style="padding-top: 95px">

    <div class="container">
      <div class="row">
        <nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top justify-content-between">
          <a href="{{ path_for('home') }}" class="btn btn-success">Home</a>
        </nav>
      </div>
    </div>

    <!-- content area -->
    <div class="container" style="text-align: left">    <!-- content area ends-->
    {% block content %}
    {% endblock %}
    </div>
    <!-- content area ends-->

    <!-- footer -->
    <div class="container">
    </div>
    <!-- footer ends -->

  </body>
</html>

This file contains a simple Bootstrap-based user interface with a navigation bar, footer, and content area. In particular, note that you can override the content area from child templates, making it easy to reuse the same HTML skeleton for different pages of the application.

The next step is to add a handler for this page to the Slim PHP script. Add the following handler code to $APP_ROOT/public/index.php:

<?php
// index.php
// ...

// home page controller
$app->get('/', function (Request $request, Response $response) {
  return $response->withHeader('Location', $this->router->pathFor('home'));
});

$app->get('/home', function (Request $request, Response $response) {
  $response = $this->view->render($response, 'home.twig', [
    'router' => $this->router
  ]);
  return $response;
})->setName('home');

// ...
$app->run();

The script in the previous example sets up two handlers. (You add more soon.) The first handler is a simple redirection, which redirects all requests for the / route to the /home route. The second handler is the /home route itself, which renders the content of the $APP_ROOT/templates/home.twig file. If you are using a local Apache and PHP7 development environment, you can browse to http://localhost/home to see the rendered version of the template. Here’s what it looks like:

Step 5: Connect to The Movie Database API

The Movie Database (TMDb) makes an API service available free of charge for development purposes only. Like any other API, it exposes a series of web-accessible endpoints and responds to properly-authenticated API requests with data from the TMDb service. Response data is formatted as JSON.

Access to the TMDb API is controlled through an API key, which you can apply for here. API requests are also rate-limited to a maximum of 40 requests in 10 seconds, and API search results must be accompanied with TMDb attribution. You can find out more about the API in the TMDb API FAQ at www.themoviedb.org/faq/api.

To see the TMDb API in action, fire up RESTer in your browser and send an API request to the URL endpoint https://api.themoviedb.org/3/search/multi?api_key=API-KEY&query=KEYWORD. Replace the API-KEY placeholder with your TMDb API key and the KEYWORD placeholder with a search term. This API endpoint performs a search across the TMDb database and returns a list of movies, TV shows, and personalities to match the specified query term.

Here’s an example of the API request and corresponding response:

For a complete description of the available API methods and responses, see the TMDb API documentation at the Getting Started page for the Movie Database API.

Before proceeding with the next step, update the application configuration file at $APP_ROOT/config.php and add your TMDb API key to it as a configuration parameter, as shown in the following example:

<?php
// config.php
$config = [
 'settings' => [
 'displayErrorDetails' => true,
 ],
 'tmdb' => [
   'key' => 'API-KEY'
 ]
];

Step 6: Process search queries and display search results

Now that you know how to perform a search using the TMDb API, the next step is to integrate it into the application. Begin by creating a search form for your users to input one or more search keywords. Add the following code to the home page template at $APP_ROOT/templates/home.twig:

<form method="post" action="{{ path_for('search') }}" class="form-inline">
  <div class="form-group mx-sm-3">
    <input name="q" type="text" value="{{ q }}" class="form-control" placeholder="Enter search term" />
  </div>
  <button type="submit" name="submit" class="btn btn-primary">Go</button>
</form>

Next, initialize the Guzzle HTTP client and add the client object to application code through the Slim dependency injection container. You use this HTTP client object to send requests to the TMDb API from within the application.

<?php
// index.php
// ...

// configure and add TMDb client to DI container
$container['tmdb'] = function ($container) {
  return new Client([
    'base_uri' => 'https://api.themoviedb.org',
    'timeout'  => 6000,
    'verify' => false,    // set to true in production
  ]);
};

// ...
$app->run();

With this code in place, you can access the Guzzle HTTP client from anywhere in the PHP script using $this->tmdb.

Next, add a handler to receive the search terms and invoke the TMDb API method from the previous step, which generates a list of matching results:

<?php
// index.php
// ...
// search page controller
$app->post('/search', function (Request $request, Response $response, $args) {
  $config = $this->get('settings');
  $params = $request->getParams();
  if (!($q = filter_var($params['q'], FILTER_SANITIZE_STRING))) {
    throw new Exception('ERROR: Query is not a valid string');
  }

  // search TMDb API for matches
  $apiResponse = $this->tmdb->get('/3/search/multi', [
    'query' => [
      'api_key' => $config['tmdb']['key'],
      'query' => $q
    ]
  ]);  

  // decode TMDb API response
  // provide results to template
  if ($apiResponse->getStatusCode() == 200) {
    $json = (string)$apiResponse->getBody();
    $body = json_decode($json);
  }  
  $response = $this->view->render($response, 'search.twig', [
    'router' => $this->router,
    'q' => $q,
    'results' => $body->results
  ]);
  return $response;
})->setName('search');

// ...
$app->run();

This handler that you added does the following actions:

  1. It receives POST submissions from the search form and checks and sanitizes the submitted input.
  2. t uses the Guzzle HTTP client object from the Slim dependency injection container to send a GET request to the TMDb API. It incorporates both the search terms and the API key from the application configuration file.
  3. It checks the HTTP client response code and if it receives a successful response code (HTTP 200), it transforms the JSON response packet into a PHP object that contains the search results.
  4. It renders the search results page and provides the page template with the search results as a variable.

Finally, add the following template to display the search results at $APP_ROOT/templates/search.twig:

{% extends 'home.twig' %}

    {% block content %}

    <h3 class="display-6">Search Results</h3>
    {% if results|length > 0 %}
    <table class="table">
      <thead>
        <tr>
          <th scope="col">ID</th>
          <th scope="col">Title</th>
          <th scope="col">Release date</th>
          <th scope="col"></th>
        </tr>
      </thead>
      <tbody>
      {% for result in results %}
        {% if result.media_type != 'person' %}
        <tr>
          <td>{{ result.id }}</td>
          <td>{{ result.title ? result.title|e : result.name|e }}</td>
          <td>{{ result.release_date ? result.release_date|date("M Y") : result.first_air_date|date("M Y") }}</td>
          {% if result.title %}
          <td><a href="{{ path_for('save', {'type':'movie', 'id':result.id}) }}" class="btn btn-success">Add</a></td>
          {% else %}
          <td><a href="{{ path_for('save', {'type':'tv', 'id':result.id}) }}" class="btn btn-success">Add</a></td>
          {% endif %}
        </tr>
        {% endif %}
      {% endfor %}
      </tbody>
    </tbody>
    <strong>This product uses the TMDb API but is not endorsed or certified by TMDb. </strong>
    {% else %}
    No matches found
    {% endif %}

    {% endblock %}

This template uses Twig’s inheritance feature to extend the home page template created earlier and replace the “content” block with a table containing the search results returned by the TMDb API. A loop iterates over the results array and displays each individual result as a row in the rendered table. Within the results array, personality results are filtered out and movie and TV show results are displayed together with their ID and year of release. Each result is accompanied by an Add button. The button doesn’t do anything yet, but you add the necessary functionality for it in the second part of the tutorial.

The search form and results grid looks like the following example:

Summary

This tutorial discussed specific tools, technologies, and APIs for creating a personal video watch list on the cloud using APIs. Although these tools and APIs may change and evolve over time, the general principles in this tutorial remain valid and should serve as guidance for other projects you might undertake. Here are three key takeaways:

  • A micro-framework that supports dependency injection gives you greater control and flexibility in invoking specific libraries as needed. Most micro-frameworks also provide the additional bonus of decoupling application URL routes from the filesystem structure of the code.
  • A template engine that supports inheritance lets you reduce the code you need to write (and also to create a more consistent user experience). It builds a base interface layout that can be reused everywhere but also overridden for specific use cases.
  • A secure, stable and extensible HTTP client library that works independently of system libraries should be a key component of any project that integrates content from third-party APIs.

At the end of this first tutorial in the series, you have a working PHP application that is integrated with The Movie Database’s search API and able to return a list of movies and TV shows that match your keywords. You also have a basic understanding of how routing, page templates, and dependency injection work with Slim PHP applications.

The second part of this tutorial builds on what you learned by adding more functions to the application, integrating it with a Cloudant database running in the IBM Cloud, and then deploying the result to IBM Cloud using a CloudFoundry PHP buildpack.