1. 程式人生 > >Create a personal video watchlist in the cloud with PHP and the Movie Database API Part 2

Create a personal video watchlist in the cloud with PHP and the Movie Database API Part 2

If you have been following along with Part 1, you are half-way through building a web-based PHP application to store your personal watchlist of movies and TV shows. In the first part, you created the application skeleton, added a page template, and integrated search results from The Movie Database (TMDb) API. In this concluding part, you launch a Cloudant database instance on IBM Cloud, connect the application with the Cloudant database instance, and deploy the result on IBM Cloud.

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:

  • Start a Cloudant database in the cloud using the Cloudant service on IBM Cloud
    .
  • Create and interact with the Cloudant database.
  • Deploy your final application code on IBM Cloud, and debug any runtime errors.

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:

  • A TMDb account and API key (request one here)
  • A basic familiarity with Bootstrap and PHP
  • Composer, the PHP dependency manager
  • The RESTer extension for Firefox or Chrome
  • (Optional) A local or hosted Apache and PHP7 development environment
  • A text editor or integrated development environment (IDE)

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 final part of the two-part tutorial series, you complete the following steps:

  1. Start a Cloudant database instance on IBM Cloud.
  2. Add and delete documents using the Cloudant API.
  3. Add items to the watch list.
  4. Retrieve and display items in the watch list.
  5. Delete and update items in the watch list.
  6. Find similar items.
  7. Deploy the application on IBM Cloud.

Step 1: Start a Cloudant database instance on IBM Cloud

The search results page template already includes an Add button, hyperlinked to the /list/save URL route. This button requires a persistent data store – in this case, a Cloudant database.

To create a Cloudant database, log in to your IBM Cloud account and, from the catalog, under Databases, select the Cloudant service. Review the description of the service and click Create to start it. Select IAM as the authentication method and the free Lite plan.

The service information page opens. Click Launch on this page to open the Cloudant dashboard.

You are redirected to the management page for your Cloudant instance. To create a database, click the Add New Database button in the top menu bar. Then enter the name of the new database as watchlist. Click Create.

Under Service credentials, click the New credential button and click the View credentials button to view the necessary details.

Then, copy the URL from the JSON credentials block into your application’s configuration file at $APP_ROOT/config.php, as shown in the following example:

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

Step 2: Add and delete documents using the Cloudant API

Cloudant is a document-oriented database that offers a REST API to create, modify, and delete databases and documents. By convention, you use a POST request to create a new document, a PUT request to update a document, and a DELETE request to delete a document. Retrieving a document or a collection of documents requires a GET request.

Begin learning the Cloudant API by creating a document in the Cloudant instance. Open RESTer in your browser and send a POST request to the http://CLOUDANT-HOST/watchlist URL. Replace CLOUDANT-HOST with the host name of your Cloudant instance, as shown in the IBM Cloud dashboard. Then add the Cloudant database user name and password in the Authorization section. Within the POST request body, enclose a JSON document like the following example:

{
    "id": 1,
    "title": "Stranger Things",
    "status": 1
}

In response to the POST request, Cloudant creates a document with the specified data and returns a Created (HTTP 201) response code. The newly-created document is visible in the Cloudant dashboard. The following example shows the request and response that you should see:

Retrieve the document by sending a GET request to the http://CLOUDANT-HOST/watchlist/_all_docs?include_docs=true URL from within RESTer, as in the following screen capture:

Finally, delete the document by sending a DELETE request to the http://CLOUDANT-HOST/watchlist/DOC-ID?rev=REVISION-ID URL from RESTer. Replace the placeholders with the document ID and revision ID, which you find in the previous GET response. Successful deletion produces an OK (HTTP 200) response code, as shown in the following screen capture:

Step 3: Add items to the watch list

The next step is to integrate the Cloudant database into the application. To start, create a separate instance of the Guzzle HTTP client in the Slim dependency injection container to handle your requests to the Cloudant API.

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

// configure and add Cloudant client to DI container
$container['cloudant'] = function ($container) use ($config) {
  return new Client([
    'base_uri' => $config['settings']['db']['uri'] . '/',
    'timeout'  => 6000,
    'verify' => false,    // set to true in production
  ]);
};

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

With this Slim dependency injection, the Cloudant API client can be accessed from any handler in the script using $this->cloudant. Notice that this client uses the Cloudant database URL (including credentials) that you added earlier to the application configuration file as its base URL for all operations. The Add button next to each search result passes along the respective item type and TMDb identifier as URL parameters to its target. Refresh your memory to what it looks like:

...
<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>
...

The PHP script handler for the Add button hyperlink must accept the TMDb identifier, query the TMDb API for the corresponding record, and then save the details of the corresponding movie or show to the Cloudant database. Add the necessary code to the $APP_ROOT/public/index.php file, as shown in the following example:

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

$app->get('/list/save/{type}/{id}', function (Request $request, Response $response, $args) {
  $config = $this->get('settings');

  // get item type
  $type = $args['type'];  
  if (!(in_array($type, array('tv', 'movie')))) {
    throw new Exception('ERROR: Type is not valid');
  }

  // get item ID on TMDb
  $id = filter_var($args['id'], FILTER_SANITIZE_NUMBER_INT);

  // get item details from TMDb API
  $apiResponse = $this->tmdb->get("/3/$type/$id", [
    'query' => [
      'api_key' => $config['tmdb']['key'],
    ]
  ]);  

  // create JSON document containing relevant information
  // save document using Cloudant API
  if ($apiResponse->getStatusCode() == 200) {
    $json = (string)$apiResponse->getBody();
    $body = json_decode($json);
    $title = ($type == 'movie') ? $body->title : $body->name;
    $doc = [
      'id' => $body->id,
      'type' => $type,
      'title' => $title,
      'status' => '1',
    ];
    $this->cloudant->post($config['db']['name'], [
      'json' => $doc
    ]);
  }

  return $response->withHeader('Location', $this->router->pathFor('home'));
})->setName('save');

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

This handler performs operations on two different APIs using the following two clients:

  1. It sends a GET request to the /movie/ID or /tv/ID TMDb API endpoint to retrieve details of the specified movie or TV show using the Guzzle TMDb API client.
  2. It sends a POST request to the Cloudant API to save a new document containing the TMDb identifier, type, and title of the selected movie or TV show. It also includes a status field indicating if the user completed watching it or not (more on this later), using the Guzzle Cloudant API client.

Between the two requests, the JSON response from TMDb is decoded, and the necessary metadata (title and identifier) is extracted from it, then converted into a PHP array prior to submission to Cloudant.

To see this action, perform a search in the application, and add an item to your list. You should see the added item as a document in the Cloudant database, as shown in the following screen capture:

Step 4: Retrieve and display the watch list

Next, update the application to show a list of all the items in your watchlist. Modify the existing /home route handler with an additional GET request to Cloudant to retrieve all available records, as in the following example:

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

$app->get('/home', function (Request $request, Response $response) {
  $config = $this->get('settings');

  // get all docs in database
  // include all content
  $dbResponse = $this->cloudant->get($config['db']['name'] . '/_all_docs', [
    'query' => ['include_docs' => 'true']
  ]);
  if($response->getStatusCode() == 200) {
    $json = (string)$dbResponse->getBody();
    $body = json_decode($json);
  }

  // provide results to template
  $response = $this->view->render($response, 'home.twig', [
    'router' => $this->router, 'items' => $body->rows
  ]);
  return $response;
})->setName('home');

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

The list of documents is transferred to the home page template in the $items array. Update the home page template at $APP_ROOT/templates/home.twig to iterate over this array and display each item, as shown in the following example:

    <!-- content area -->
    <div class="container" style="text-align: left">
    {% block content %}
      <h3 class="display-6">My List</h3>
      {% if items|length > 0 %}
      <table class="table">
        <thead>
          <tr>
            <th scope="col">ID</th>
            <th scope="col">Title</th>
            <th scope="col">Type</th>
            <th scope="col">Status</th>
            <th scope="col"></th>
            <th scope="col"></th>
            <th scope="col"></th>
          </tr>
        </thead>
        <tbody>
        {% for item in items %}
          <tr>
            <td>{{ item.doc.id }}</td>
            <td>{{ item.doc.title|e }}</td>
            <td>{{ item.doc.type == 'movie' ? 'Movie' : 'TV' }}</td>
            <td>{{ item.doc.status == 1 ? 'Unwatched' : 'Watched' }}</td>
            <td><a href="{{ item.doc.status == 1 ? path_for('update-status', {'id':item.doc._id ~ '.' ~ item.doc._rev, 'status':0}) : path_for('update-status', {'id':item.doc._id ~ '.' ~ item.doc._rev, 'status':1}) }}" class="btn btn-success btn-sm">{{ item.doc.status == 1 ? 'Watched it!' : 'Watch it again!' }}</a></td>
            <td><a href="{{ path_for('search-similar', {'id':item.doc.id, 'type':item.doc.type}) }}" class="btn btn-success btn-sm">Find similar</a></td>
            <td><a href="{{ path_for('delete', {'id':item.doc._id ~ '.' ~ item.doc._rev}) }}" class="btn btn-danger btn-sm">Remove from list</a></td>
          </tr>
        {% endfor %}
        </tbody>
      </table>
      {% else %}
      No items found.
      {% endif %}
    {% endblock %}
    </div>
    <!-- content area ends-->

Here’s an example of what the application home page should look like after this change:

The status field of each document represents the status of the item in the watchlist: 1 for Unwatched, 0 for Watched. Each row also includes three additional buttons, to modify the item’s status, find similar items or remove it from the watchlist.

Step 5: Delete and update items on the watch list

You’ll remember from earlier that documents in Cloudant are deleted by sending a DELETE request to the Cloudant API, including both document identifier and revision identifier. In the previous page template above, the Delete button passes both these parameters to its target handler as URL parameters. Now, add the definition for this handler to $APP_ROOT/public/index.php, as shown in the following example:

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

$app->get('/list/delete/{id}', function (Request $request, Response $response, $args) {
  $config = $this->get('settings');

  // get document ID and revision ID in Cloudant
  $keys = explode('.', filter_var($args['id'], FILTER_SANITIZE_STRING));
  $id = $keys[0];
  $rev = $keys[1];

  // delete document from database using Cloudant API
  $this->cloudant->delete($config['db']['name'] . '/' . $id, [
    "query" => ["rev" => $rev]
  ]);

  return $response->withHeader('Location', $this->router->pathFor('home'));
})->setName('delete');

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

This handler receives the composite document and revision identifier, splits them into their constituent parts and then generates a DELETE request using the Cloudant API client. In response, Cloudant deletes the corresponding document from the database.

In a similar vein, the Watched it/Watch it again button lets you mark items as watched or unwatched. In this case, the corresponding handler, instead of sending a DELETE request, sends a PUT request to Cloudant and submits a revised document changing the status field to 0 or 1. The following handler code is very similar to what you saw in the previous example with the exception of the Cloudant request content:

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

$app->get('/list/update/{id}/{status}', function (Request $request, Response $response, $args) {
  $config = $this->get('settings');

  // get document ID and revision ID in Cloudant
  $keys = explode('.', filter_var($args['id'], FILTER_SANITIZE_STRING));
  $id = $keys[0];
  $rev = $keys[1];

  // retrieve complete document from database using Cloudant API
  $dbResponse = $this->cloudant->get($config['db']['name'] . '/' . $id);
  $json = (string)$dbResponse->getBody();
  $doc = (array)json_decode($json);  

  // update document data
  // save document back to database using Cloudant API
  $doc['status'] = $args['status'];  
  $this->cloudant->put($config['db']['name'] . '/' . $id, [
    "query" => ["rev" => $rev], 'json' => $doc
  ]);

  return $response->withHeader('Location', $this->router->pathFor('home'));
})->setName('update-status');

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

Step 6: Find similar items

An interesting feature of the TMDb API is its ability to perform a similarity search. This feature allows you to find movies or TV shows similar to an existing item, by sending a GET request to the /movie/ID/similar or /tv/ID/similar API endpoint. Use this API call to add suggestions for similar movies to the application, by adding the following handler to $APP_ROOT/public/index.php:

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

// similarity search handler
$app->get('/search/similar/{type}/{id}', function (Request $request, Response $response, $args) {
  $config = $this->get('settings');

  // get item type
  $type = $args['type'];  
  if (!(in_array($type, array('tv', 'movie')))) {
    throw new Exception('ERROR: Type is not valid');
  }

  // get item ID on TMDb
  $id = filter_var($args['id'], FILTER_SANITIZE_NUMBER_INT);

  // search TMDb API for similar items
  $apiResponse = $this->tmdb->get("/3/$type/$id/similar", [
    'query' => [
      'api_key' => $config['tmdb']['key']
    ]
  ]);  

  // 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,
    'results' => $body->results
  ]);
  return $response;
})->setName('search-similar');

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

This handler accepts a TMDb record identifier and type and then performs a similarity search by sending a GET request to the corresponding TMDb API endpoint. The JSON results are decoded into a PHP object and interpolated into the search results page template. As these results are rendered using the same page template as keyword searches, they can then be added to the user’s watchlist using the same workflow too, by clicking the Add button next to each item.

The following example shows a similarity search results page:

Step 7: Deploy the application on IBM Cloud

To connect to the Cloudant instance, the PHP application needs the database URL and access credentials. You previously specified this information in the application configuration file. However, as an alternative, you can bind the database instance to the application and import these credentials directly from the IBM Cloud environment (the VCAP_SERVICES variable).

To use this approach, add the following code to the $APP_ROOT/public/index.php script, before the lines that initialize the database connection:

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

// if VCAP_SERVICES environment available
// overwrite local credentials with environment credentials
if ($services = getenv("VCAP_SERVICES")) {
  $services_json = json_decode($services, true);
  $config['settings']['db']['uri'] = $services_json['cloudantNoSQLDB'][0]['credentials']['url'];
}

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

At this point, the application is complete. To deploy it, create the application manifest file. Remember to use a unique host and application name by appending a random string to it (like your initials):

---
applications:
- name: watchlist-[initials]
memory: 256M
instances: 1
host: watchlist-[initials]
buildpack: https://github.com/cloudfoundry/php-buildpack.git
stack: cflinuxfs2

Configure the PHP build pack to use the public directory of the application as the Web server directory.

Create a $APP_ROOT/.bp-config/options.json file with the following content:

{
  "WEB_SERVER": "httpd",
  "COMPOSER_VENDOR_DIR": "vendor",
  "WEBDIR": "public",
  "PHP_VERSION": "{PHP_71_LATEST}"
}

Now, go ahead and push the application to IBM Cloud:

cf api https://api.ng.bluemix.net
cf login
cf push

If you decided to import database credentials from the IBM Cloud environment, use the correct ID for the Cloudant instance and bind it to the application. You can obtain the ID from the IBM Cloud dashboard.

cf bind-service watchlist-[initials] "Cloudant-[id]"
cf restage watchlist-[initials]

You should now be able to browse to the application at http://watchlist-[initials].mybluemix.net and see the home page. If you don’t see it, troubleshoot the problem using the tips I collected here.

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 cloud-based database service (as opposed to an on-premise hosted instance) gives you the dual benefits of economy and scalability. You can scale up (or down) your database CPU, memory and storage requirements in response to application usage trends, thereby ensuring that you only pay for what you use.
  • REST APIs use well-understood HTTP requests and responses and therefore can be integrated into your application using any standards-compliant HTTP client library (which is typically available in every programming language). Using this approach reduces the learning curve and ensures automatic compliance with best practices.
  • Binding IBM Cloud services to your application and sourcing service credentials from the shared environment ensures security, and it gives you the flexibility to adjust application infrastructure without redeploying the application.

Now you know how to build a simple, useful application to store a personal watch list for movies and TV shows in a service-agnostic fashion. The application runs entirely in the cloud with a cloud database, cloud hosting infrastructure, and cloud APIs. The Cloudant database service in IBM Cloud, coupled with the PHP CloudFoundry buildpack, makes it easy to build database-backed web applications that integrate with third-party APIs without worrying about infrastructure security and scalability.

Try this out as an inspiration for working with cloud-based database services and APIs for other applications, or just use it to keep track of the all the shows you want to binge watch!