《Yii Rapid Application Development Hotshot》

Chapter 5. Service Please – Integrating Service Data

This chapter is about using third-party web service APIs in your Yii application. Usually, these interfaces are implemented as RESTful web services that use JSON for data interchange. Commonly, clients that wrap these APIs are provided in a number of programming languages. Using an API in PHP is often as simple as registering with the organization as a developer and then downloading and installing the client for PHP. As Yii provides excellent support for using third-party PHP libraries, using these services with your Yii application is easy and straightforward.

Mission Briefing

We will integrate the Google OAuth2 authentication API into Yii and then set up Google authentication. Then, we will build a comic book news stream on our site that is created from the user's Google+ stream. Then, we will set up rich data interaction via Comic Vine (http://www.comicvine.com).

Why Is It Awesome?

An increasingly important component of modern web development is the ability to integrate third-party web services and APIs into your application. Amazon, Facebook, Flickr, Google, Groupon, NASA, Photobucket, Reddit, Salesforce.com, Twitter , the US Postal Service, the Weather Underground , and many other organizations now provide APIs to access their data. No matter what subject matter your website spans, you will probably want to use some of the functionalities and data provided and managed by these organizations. You can use some of them for authentication, offloading the work of building and securing storage of usernames and passwords. You can integrate with popular social networking websites, so members of those sites can fully participate in interactions and ratings on your site.

Your Hotshot Objectives

  • Google Me – Getting Started
  • Google Me – Putting the Rubber to the Road
  • Google Me – The Yii Way
  • Integrating with Comic Vine – The Search, Part 1
  • Integrating with Comic Vine – The Search, Part 2
  • Integrating with Comic Vine – The Details
  • Putting It All Together

Mission Checklist

This project assumes that you have a web development environment prepared. If you do not have one, the tasks in Project 1, Develop a Comic Book Database, will guide you through setting one up. In order to work this project, you will need to set up the project files that have been provided with the book. Refer to the Preface of the book for instructions on downloading these files. The files for this project include a Yii project directory with a database schema. To prepare for the project, follow these steps, replacing the username james with your own username.

  1. Copy the project files into your working directory.
cpr ~/Downloads/project_files/Chapter\ 5/project_files ~/projects/ch5
  1. Make the directories that Yii uses web writeable.
cd ~/projects/ch5/
sudo chown -R james:www-data protected/runtime assets protected/models protected/controllers protected/views
  1. Create a link in the webroot directory to the copied directory.
cd /opt/lampp/htdocs
sudo ln -s ~/projects/ch5 cbdb
  1. Import the project into NetBeans (remember to set the project URL to http://localhost/cbdb).
  2. Create a database named cbdb and load the database schema (~/projects/ch5/protected/data/schema.sql) into it.
  3. If you are not using the XAMPP stack or if your access to MySQL is password protected, you should review and update the Yii configuration file (in NetBeans it is ch5 | Source Files | protected | config | main.php).

Note that the admin login is admin/test. Also, in order to work through the first three tasks, you will need a valid Google account.

Google Me – Getting Started

We need to set up the Google OAuth 2 API and integrate it with our installation of Yii, and we need to use the Google API console to set up access to the services we want to use.

Engage Thrusters

The PHP implementation of the Google OAuth 2 API requires curl for PHP. If you are using XAMPP on Linux, it should already be enabled. You can verify this by going to http://localhost/xampp/phpinfo.php and looking for the section labeled curl or the option --with-curl=/opt/lampp.

  1. Find google-api-php-client and download the latest version from the downloads page. Extract it to protected/vendors in your Yii directory (if the vendors subdirectory does not exist, create it in protected first).
  2. Go to the Google API console at https://code.google.com/apis/console/ (you will need to log in to a valid Google account) and click on Create project.
  1. Put in the project name. Then, click on API Access and you should see something like this:
  1. Click on the Create an OAuth 2.0 client ID… button and fill out the form.
  1. For now select Web application as the Application type, and then put in localhost for the hostname.
  1. Once you have all that set up, go to the Services tab and turn on access to Google+ API.
  1. Now we will make a couple of quick changes to our project so we can test to see if everything is set up properly.
  2. Create a new controller (in protected/controllers) for your Google+ feed, and name it GpfController.php.
<?php
class GpfController extends Controller
{
    public $layout='//layouts/column2';
 
    public function actionIndex()
    {
        $this->render('index', array());
    }
}
  1. Modify views/layouts/main.php to look like this:
array(
    'label'=>'Comic Books', 
    'url'=>array('/book/index'),
    'items' => array(
        array('label'=>'Publishers', 'url'=>array('/publisher/index')),
        array('label'=>'WishList', 'url'=>array('/wish/index')),
    )
),
array('label'=>'Users', 'url'=>array('/user/index')),
array('label'=>'Google+ Feed', 'url'=>array('/gpf/index')),
array('label'=>'Login', 'url'=>array('/site/login'), 'visible'=>Yii::app()->user->isGuest),
array('label'=>'Logout ('.Yii::app()->user->name.')', 'url'=>array('/site/logout'), 'visible'=>!Yii::app()->user->isGuest),
  1. Now, add a gpf directory to views, and put a new index.php inside:
<?php
  $this->breadcrumbs=array(
    'gpf',
  );
  echo "Testing, part 1.\n";
?>
  1. Set the permissions chown –R james:www-data views/gpf. When you go to http://localhost/cbdb you should be able to click on Google+ Feed, and then see the testing text.
  2. We are going to incorporate the Google API PHP library and see if it is installed correctly. We will call Yii::import() to add the vendors directory to the include path.
  3. Change index.php to look like the following:
<?php
  Yii::import('application.vendors.*');
  require_once 'google-api-php-client/src/Google_Client.php';
  require_once 'google-api-php-client/src/contrib/Google_PlusService.php';
  $this->breadcrumbs=array(
    'gpf',
  );
  echo "Testing, part 2.\n";
?>
  1. Go to http://localhost/cbdb and click on Google+ Feed. If you don't see an error and you see the testing text, you've installed the Google Auth PHP API client correctly, and it is now working properly in your Yii environment.

Objective Complete - Mini Debriefing

We installed the Google API client for PHP. We created a Client ID with Google to allow us to access their APIs. We turned on access to the Google+ API, so that we can see our stream. We put in a mostly empty controller class, GpfController, with a stub for the index and we added a menu item for Google+ Feed. We created a small view, and put some simple smoke tests in there to see if we can use the API client in Yii.

Google Me – Putting the Rubber to the Road

Now, we will put some simple client code using the client API in the project. Everything should be in place to fetch information from our Google+ stream, so we just have to put some code in place and provide the correct values to the API for authentication.

Engage Thrusters

For this task, we'll need SSL so that we (and Google) can access our application with HTTPS. By default, it is usually enabled for XAMPP on Linux. Verify that it is working by going to https://localhost/cbdb. If it is not enabled, go ahead and enable it (you can find a large number of "how-to" manuals online).

  1. Copy the file index.php in prepared_files over the index file in protected/views/gpf:
cd ~/projects/ch5/
cp prepared_files/gplus/index.php protected/views/gpf
  1. Open the Google API console, and click on API Access. Then click on Edit Settings to the right of Client ID for web applications.
  1. Then set the Redirect URI to http://localhost/cbdb/index.php/gpf/index:

Your final configuration will look like this:

  1. Open protected/views/gpf/index.php.
$client->setClientId('CLIENT_ID');
$client->setClientSecret('CLIENT_SECRET');
$client->setRedirectUri('http://localhost/cbdb/index.php/gpf/index');
$client->setDeveloperKey('DEVELOPER_KEY');
  1. Replace CLIENT_ID, CLIENT_SECRET, and DEVELOPER_KEY with the values from the Google API console in the API Settings section (DEVELOPER_KEY refers to the API Key under Simple API Access).
  2. Now you can go to http://localhost/cbdb and when you click on Google+ Feed, it should have you connect to Google+.
  1. If you are not logged in to Google, it will ask you to authenticate:
  1. Go ahead and agree to have the mini-app we just wrote access your info.
  1. Then you will see a little summary of the first few entries you would see on Google+ if you searched for #comicbooks.

Objective Complete - Mini Debriefing

We put some simple code for fetching from our Google+ stream entirely in the view for Gpf (views/gpf/index.php). This code takes care of all the states for logging in to Google+ by saving the status of progression through the stages in the session. In order for this code to work, we had to set the redirect URI in the code and in the Google API console to https://localhost/cbdb/index.php/gpf/index (the URI of the page for viewing the Google+ feed). If these URIs do not match, you will get an error as shown in the following screenshot:

Classified Intel

The current code basically works to accomplish what we want, but we haven't implemented this using an MVC design pattern. All our logic and code is simply sitting in our view. While this was a quick and dirty way to use the API, and it hints at the power we can gain by incorporating third-party web services into our application, we haven't yet done it right. In the next task, we will do it right.

Google Me – The Yii Way

This task is about dividing the functionality implemented into the previous task into a view and a controller.

Engage Thrusters

  1. Move the PHP code from the beginning of protected/views/gpf/index.php, going down to <h1>Google+ Comic Book News Feed</h1> (leave the breadcrumbs where they are) into the indexAction() function in protected/controllers/GpfController.php. Move the line initializing $authUrl to the top of the function. Then make some changes in the loop to store a list of entities to pass to the view, as follows (remember to replace CLIENT_ID, CLIENT_SECRET, and DEVELOPER_KEY with the correct values):
<?php
Yii::import('application.vendors.*');
require_once 'google-api-php-client/src/apiClient.php';
require_once 'google-api-php-client/src/contrib/apiPlusService.php';
 
class GpfController extends Controller
{
    public $layout='//layouts/column2';
 
    public function actionIndex()
    {
        $authUrl = '';
        $session = Yii::app()->session;
 
        $client = new apiClient();
        $client->setApplicationName("Google+ Comic Book News Feed");
        // Visit https://code.google.com/apis/console to 
        //generate your oauth2_client_id, oauth2_client_secret, 
        //and to register your oauth2_redirect_uri.
        client->setClientId('CLIENT_ID');
        $client->setClientSecret('CLIENT_SECRET');
        $client->setRedirectUri('http://localhost/cbdb/index.php/gpf/index');
        $client->setDeveloperKey('DEVELOPER_KEY');
        $plus = new apiPlusService($client);
 
        if (isset($_REQUEST['logout'])) {
                unset($session['access_token']);
        }
 
 
        if (isset($_GET['code'])) {
                $client->authenticate();
                $session['access_token'] = $client->getAccessToken();
                header('Location: http://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF']);
        }
 
        if (isset($session['access_token'])) {
                $client->setAccessToken($session['access_token']);
        }
        $activityList = array();
        if ($client->getAccessToken()) {
                $optParams = array('maxResults' => 100);
                $activities = $plus->activities->search('#comicbooks');
                foreach($activities['items'] as $activity) {
                        $activityListItem = array();
                        $activityListItem['url'] = filter_var($activity['url'], FILTER_VALIDATE_URL);
                        $activityListItem['title'] = filter_var($activity['title'], 
                            FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_HIGH);
                        $activityListItem['content'] = $activity['object']['content'];
                        $activityListItem['images'] = array();
                                if (isset($activity['object']['attachments'])) {
                                        foreach($activity['object']['attachments'] as $attachment) {
                                                if ($attachment['objectType'] === 'photo') {
                                                        $activityListItem['images'][] = 
                                                                $attachment['image']['url'];
                                                } 
                                        }
                                }
                                $activityList[] = $activityListItem;
                        }
                        $session['access_token'] = $client->getAccessToken();
                } 
                else {
                        $authUrl = $client->createAuthUrl();
                }
                $this->render('index', array('activityList' => $activityList, 'authUrl' => $authUrl));
        }
}
  1. Change the view to reflect the changes:
<?php 
    $this->breadcrumbs=array(
        'gpf',
    );
?>
<h1>Google+ Comic Book News Feed</h1>
<div class="box">
    <?php if(count($activityList)): ?>
    <div class="activities"><h2>Your personal comic book news feed: </h2>
    <?php
        foreach($activityList as $activityListItem) {
            echo("<div class='activity'><a href='" . $activityListItem['url'] . "'>" . 
                $activityListItem['title'] . '</a><div>' .
                $activityListItem['content'] . "</div>\n");
            foreach($activityListItem['images'] as $imageUrl) {
                echo('<img src="' . $imageUrl . '">');
                echo("</div><div><center><img src='" .
                    Yii::app()->request->baseUrl .
                    "/images/hdiv.png' /></center></div>\n");
            }
        }
    ?>
    </div>
    <?php endif;
    if($authUrl) {
        print "<a class='login' href='$authUrl'>Connect Me!</a>";
    } 
    else {
        print "<a class='logout' href='?logout'>Logout</a>";
    }
    ?>
</div>
  1. Now the Google+ feed in your web app should work exactly the same way it did before we started moving the code around.

Objective Complete - Mini Debriefing

The logic responsible for communicating with the model, in this case the Google web services, now sits in the controller for our Google+ feed. The code responsible for marking up and displaying the retrieved data now sits in our view. Separating this functionality is always important when using an MVC paradigm. It makes our code more maintainable, and keeps each part of our application responsible for its own concern.

Integrating with Comic Vine – The Search, Part 1

Comic Vine's mission statement is "to be the most useful and easy to use comic book website in the world". They provide a nice API with JSON or XML formats for integration. Without special arrangement, their API is only for personal use. If you want to take your website to the masses while using their stuff, be sure to give them a call first. Comic Vine does not provide a way to look up comic books by ISSN, so we are going to implement a search by title.

Engage Thrusters

The chapter files include a simple PHP wrapper for the Comic Vine functionality that we will use.

We are going to briefly discuss how this wrapper works. Here is the wrapper:

<?php
 
class CbdbComicVine {
  private $apiKey;
  private $baseUrl;
 
  public function __construct($apiKey, $baseUrl = 'http://api.comicvine.com/') {
    $this->setApiKey($apiKey);
    $this->setBaseUrl($baseUrl);
  }
  
  public function setAPiKey($apiKey) {
    $this->apiKey = $apiKey;
  }
 
  public function setBaseUrl($baseUrl) {
    $this->baseUrl = $baseUrl;
  }
 
  public static function buildQueryString($paramArray) {
    $paramString = http_build_query($paramArray);
    ;
    
    if ($paramString) {
      $paramString = '?' . $paramString;
    }
    return $paramString;
  }
 
  public static function makeRequest($url, $paramArray) {
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, $url . CbdbComicVine::buildQueryString($paramArray));
    curl_setopt($curl, CURLOPT_HEADER, false);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
    $response = curl_exec($curl);
    $status = curl_getinfo($curl);
    if ($status['http_code'] == 200) {
      $responseObject = json_decode($response);
      if (is_object($responseObject) && ($responseObject->status_code == 1)) {
        return array('error' => 0, 'content' => $responseObject);
      }
      return array('error' => 1, 'error_type' => 'site', 'content' => $responseObject);
    }
    return array('error' => 1, 'error_type' => 'transfer', 'content' => $status);
  }
 
  private function setInitParams(&$params) {
    $params['api_key'] = $this->apiKey;
    $params['format'] = 'json';
    return $params;
  }
 
  public function getNewParamsArray() {
    $params = array();
    return $this->setInitParams($params);
  }
 
  public function baseRequest($resource, $addlParams) {
    $params = $this->getNewParamsArray();
    $params = array_merge($params, $addlParams);
    return $this->makeRequest($this->baseUrl . $resource . '/', $params);
  }
 
  public function detailRequest($resource, $id, $addlParams) {
    $params = $this->getNewParamsArray();
    $params = array_merge($params, $addlParams);
    return $this->makeRequest($this->baseUrl . $resource . '/' . $id . '/', $params);
  }
 
  public function volumeSearch($query, $params = array(), $offset = 0, $limit = 20) {
    $params['query'] = $query;
    $params['resources'] = 'volume';
    $params['offset'] = $offset;
    $params['limit'] = $limit;
    
    return $this->baseRequest('search', $params);
  }
 
  public function issuesForVolume($volumeId, $params = array()) {
    $params['field_list'] = 'issues';
    return $this->detailRequest('volume', $volumeId, $params);
  }
 
 
  public function volume($id, $params = array()) {
    return $this->baseRequest("volume/$id", $params);
  }
 
  public function issue($id, $params = array()) {
    return $this->baseRequest("issue/$id", $params);
  }
}

The Comic Vine API works like most web services. You create an API key and then use the key to make requests. In the previous object, it is stored in the private data member $apiKey. It is set via the constructor. By default, $baseUrl is set to http://api.comicvine.com, also via the constructor. The member function buildQueryString() takes a parameter array, populated with name-value pairs, and produces a URL query string. The member function makeRequest() accepts a URL and a parameter array, encodes the parameters as a URL query string, makes the queries using curl, and then decodes the JSON reply and returns it in native PHP data constructs. It wraps the results in an associative array containing error information in a standard format. I use getNewParamsArray() to initialize an array to set the API key and to set the format to JSON.

The functions buildQueryString(), makeRequest(), and getNewParamsArray() are not intended to be used directly. They are utility functions used in combination in baseRequest() and detailRequest(), which can be used to implement most, if not all, of the functionality offered by Comic Vine. The function baseRequest() accesses resources the function and detailRequest() accesses resource details. In order to search for particular volumes and issues, we have implemented volumeSearch(), volume(), issue(), and issuesForVolume(). At this point, it is obviously trivial to implement functionality found in the API documentation for Comic Vine. Now that we have discussed the wrapper, we are going to integrate it with our website.

Let's create a controller for our search. Create protected/controller/CvController.php and insert the following code snippet:

<?php
Yii::import('application.vendors.*');
require_once 'comicvine/comicvine.php';
 
class CvController extends Controller
{
  public $layout='//layouts/column2';
  
  public static function newCv()
  {
    return new CbdbComicVine('39aed1911b2cbffd08f19b4bf5922fd96ccf3b4f'); 
    //Replace this with your API key
  }
 
  public function actionIndex() 
  {
    $this->redirect(array('search'));
  }
 
  public function actionSearch() 
  {
    $itemList = array();
    $cv = $this->newCv(); 
    if (isset($_GET['search'])) {
      $offset = 0;
      if (isset($_GET['offset'])) {
        $offset = $_GET['offset'];
      }
      $result = $cv->volumeSearch($_GET['q'], array(), $offset);
      $this->render('search', array('model'=>$model,
        'result' => $result, 'q' => $_GET['q']));
        }
    else {
        $this->render('search',array('model'=>$model));
         }
  }
}

Typically, we would move things like the API key into a config file. For simplicity we will leave it here.

Let's look at what's going on in this controller:

  1. The function actionSearch() takes whatever is passed in the 'q' parameter and conducts a volume search on it. If offset is set in the query string, it is passed along to volumeSearch() as a parameter, otherwise it defaults to 0. Let's make a simple view to test this in protected/views/cv/search.php:
<?php 
 
$this->breadcrumbs=array(
  'Comicvine',
  'Search'
);
 
if (isset($result)) {
  if (isset($result['content'])) {
    if (isset($result['content']->results)) {
      foreach($result['content']->results as $rec){
        echo("<div class = 'result_row'>\n");
        echo("<div class = 'result_name'>\n");  
        echo("$rec->name\n");  
        echo("</div>\n");  
        echo("<div class = 'result_details'>\n");  
        echo("<a href='" . $rec->site_detail_url . "' target='_blank'>". 'View Details' . "</a>\n");  
        echo("</div>\n");  
        echo("</div>\n");
      }
    }
  }
}
 
?>
  1. Let's see where that leaves us. Search for Spiderman by entering the URL http://localhost/cbdb/index.php/cv/search?search&q=Spiderman.

You can click on View Details and it will open a new tab with the Comic Vine details for each result. Our search has a couple of problems. It only shows the first 20 results, and it's really not good. Let's fix the second problem so it looks a little nicer while we work on pagination. Add some styling at the very top of the file and then add a couple of lines.

<style type="text/css">
  .result_header {
    text-align: center;
    font-size: 150%;
    text-decoration: underline;
    width: 350px;
    padding: 4px;
  }
 
  .result_name {
    font-weight:bold;
    float: left;
    width: 250px;
  }
 
  .result_details {
    float: left;
    width: 100px;
  }
 
  .result_row {
    float: left;
    border-width: 1px;
    border-color: #00a;
    border-style: solid;
    padding: 4px;
  }
 
  .clear_left {
    clear: left;
  }
</style>
<?php 
 
$this->breadcrumbs=array(
  'Comicvine',
  'Search'
);
 
 
 
if (isset($result)) {
  if (isset($result['content'])) {
    if (isset($result['content']->results)) {
        echo "<div class = 'result_header'>Results</div>";
      foreach($result['content']->results as $rec){
        echo("<div class = 'result_row'>\n");
        echo("<div class = 'result_name'>\n");  
        echo("$rec->name\n");  
        echo("</div>\n");  
        echo("<div class = 'result_details'>\n");  
        echo("<a href='" . $rec->site_detail_url . "' target='_blank'>". 'View Details' . "</a>\n");  
        echo("</div>\n");  
        echo("</div>\n");
        echo("<div class='clear_left' />");
      }
    }
  }
}
?>

Much better. Typically, we would put the embedded styles in a separate CSS file, but we will put it all in this file for simplicity.

Now let's fix the pagination. This will take a little more work. We will write a function to create a pagination bar, complete with links for the next and previous results. Put this at the top of the view, right below the opening PHP tag.

function printPagination($result, $q) {
  if (isset($result['content']->offset) && isset($result['content']->limit)) {
    $totalResults = isset($result['content']->number_of_total_results)?$result['content']->number_of_total_results:0;
    echo("<div class = 'pagination_row'>\n");  
    echo("<div class = 'left_pagination'>\n");
    if ($result['content']->offset != 0) {  
      $lowerLimit = ($result['content']->offset - $result['content']->limit >= 0) ?
            $result['content']->offset - $result['content']->limit : 0;
      $upperLimit = $result['content']->offset - 1;
      $upperLimit  = ($upperLimit > $totalResults - 1)?$totalResults - 1:$upperLimit;
      echo("<a href='" . Yii::app()->request->getBaseUrl() . '/' . Yii::app()->request->getPathInfo() . 
        "?search=1&offset=$lowerLimit&q=$q'>Prev(" . ($lowerLimit + 1) .  '-' . ($upperLimit + 1) . ")</a>\n");
    }
    echo("</div>");
 
 
    echo("<div class = 'center_pagination'>\n");
    $lowerLimit = $result['content']->offset;
    $upperLimit = $result['content']->offset + $result['content']->limit - 1;
    $upperLimit  = ($upperLimit > $totalResults - 1)?$totalResults - 1:$upperLimit;
    echo('<center>');
    if ($totalResults == 0) {
      echo('Displaying entries 0-0 of 0.');
    }
    else {
      echo('Displaying entries ' . ($lowerLimit + 1) . '-' . ($upperLimit + 1) . " of $totalResults.");
    }
    echo('</center>');
    echo("</div>");
 
    $lowerLimit = $result['content']->offset + $result['content']->limit;
    if ($lowerLimit < $totalResults - 1) {
      $upperLimit = $result['content']->offset + 2 * $result['content']->limit - 1;
      $upperLimit  = ($upperLimit > $totalResults - 1)?$totalResults - 1:$upperLimit;
      echo("<div class = 'right_pagination'>\n");
      echo("<a href='" . Yii::app()->request->getBaseUrl() . '/' . Yii::app()->request->getPathInfo() . 
        "?search=1&offset=$lowerLimit&q=$q'>Next(" . ($lowerLimit + 1) . '-' . ($upperLimit + 1) . ")</a>\n");
      echo("</div>");
    }
    echo("</div>\n");  
  }
}
  1. This function simply takes the information in the result and prints a link to the next and previous results if any. Add these two lines and try it out:
if (isset($result)) {
  if (isset($result['content'])) {
    printPagination($result, $q);
    if (isset($result['content']->results)) {
        echo "<div class = 'result_header'>Results</div>";
      foreach($result['content']->results as $rec){
        echo("<div class = 'result_row'>\n");
        echo("<div class = 'result_name'>\n");  
        echo("$rec->name\n");  
        echo("</div>\n");  
        echo("<div class = 'result_details'>\n");  
        echo("<a href='" . $rec->site_detail_url . "' target='_blank'>". 'View Details' . "</a>\n");  
        echo("</div>\n");  
        echo("</div>\n");
        echo("<div class='clear_left' />");
      }
    }
    printPagination($result, $q);
  }
}
  1. Now add the following to the style section to make it look better:
.left_pagination {float: left; width: 100px;}
.center_pagination {font-weight:bold; float: left; width: 190px;}
.right_pagination {float: left; width: 100px;}
.pagination_row {float: left; padding: 4px;}
  1. Be sure to add a div to clear the left float after the pagination row (add this line near the end of printPagination()).
echo("</div>\n");  
echo("<div class='clear_left' />");
  1. You will wind up with the following result:

The view displays 20 results per page, with the pagination row at the top and bottom of each page.

Objective Complete - Mini Debriefing

We covered a lot of material in this task. If the web service you are trying to use does not provide an API wrapper in a language you can use, you will have to write your own. This task has demonstrated some basic tactics to accomplish this. Then, we integrated a wrapper with a Yii controller. Pagination is often a necessary task, and we see how to deal with it in a relatively standard way when using Comic Vine. Yii has a component for handling pagination information called CPagination, but since we have already handled this on our own as an educational exercise, we won't use it for this task.

In the next task, we will add a search form at the top of the screen, so you don't have to use the URL parameter to search.

Integrating with Comic Vine – The Search, Part 2

We have laid the groundwork for our Comic Vine volume search. Now we will add a search form. We will use the CActiveForm widget.

Engage Thrusters

  1. Create a model for the widget to use. Create the file protected/models/CvSearchForm.php with the following contents:
<?php
class CvSearchForm extends CFormModel {
  public $query;
  
  public function rules()
  {
    return array(
      array('query', 'required'),
    );
  }
 
  public function attributeLabels()
  {
    return array(
      'query'=>'Title Search',
    );
  }
}

Our needs from the model are minimal and this should take care of it.

  1. Now add this to the controller (be sure to change the if condition to an elseif).
$model = new CvSearchForm();
    if (isset($_POST['CvSearchForm']))
    {
      $itemList = array();
           $model->attributes = $_POST['CvSearchForm'];
      if ($model->validate()) {
        $result = $cv->volumeSearch($model->query);
        $this->render('search',array(
          'model' => $model,
          'itemList' => $itemList,  
          'result' => $result,
          'q' => $model->query
        ));
      }
    }
    elseif (isset($_GET['search'])) {
      $offset = 0;
      if (isset($_GET['offset'])) {
        $offset = $_GET['offset'];

CActiveForm is used and explained extensively in Project 3, Access All Areas – Users and Logins. So if you don't know what's going on at this point you might want to refer to it.

  1. In the view, add the following lines of code immediately after the breadcrumbs:
$this->breadcrumbs=array(
  'Comicvine',
  'Search'
);
 
$form=$this->beginWidget('CActiveForm', array(
  'id'=>'search-form',
  'enableClientValidation'=>true,
  'clientOptions'=>array(
    'validateOnSubmit'=>true,
  ),
));
echo $form->errorSummary($model);
?>
<div class="row">
  <?php echo $form->labelEx($model,'query'); ?>
  <?php
  if (isset($q)) {
    echo $form->textField($model,'query',array('size'=>40, 'value' => $q));
  }
  else {
    echo $form->textField($model,'query',array('size'=>40));
  }
  ?>
  <?php echo $form->error($model,'query'); ?>
</div>
<div class="row buttons">
  <?php echo CHtml::submitButton('Submit', array('size'=>40)); ?>
</div>
<?php $this->endWidget();
if (isset($result)) {

Now we have a pretty good interface for our volume search.

Objective Complete - Mini Debriefing

In just a few lines of code, we were able to add our search box. We put the value of $q in the textbox if the q parameter is set, because this is how the link for the next and previous links are constructed, and we still want to show the query for the search in the blank.

Integrating with Comic Vine – The Details

We have a way to search for volumes, which are collections of issues. However, we don't have a way to browse and select individual issues. Let's fix that.

Engage Thrusters

We need to start by making an action for listing issues in CvController. Then, we will make a view. Sounds familiar? Let's go.

  1. Change CvController to look like the following code snippet:
<?php
Yii::import('application.vendors.*');
require_once 'comicvine/comicvine.php';
 
class CvController extends Controller
{
  public $layout='//layouts/column2';
 
  public static function newCv()
  {
    return new  CbdbComicVine('39aed1911b2cbffd08f19b4bf5922fd96ccf3b4f'); 
    //Replace this with your API key
  }
 
  public static function errorHandler($result, $view) {
    if ($result['error']) {
      $this->render($view, array('error'=>
       $result['content']->error));
      return true;
    }
    return false;
  }
 
  public function actionIndex() 
  {
    $this->redirect(array('search'));
  }
 
  public function actionSearch() 
  {
    $itemList = array();
    $cv = $this->newCv(); 
    $model = new CvSearchForm();
    if (isset($_POST['CvSearchForm']))
    {
      $itemList = array();
           $model->attributes = $_POST['CvSearchForm'];
      if ($model->validate()) {
        $result = $cv->volumeSearch($model->query);
        $this->render('search',array(
          'model' => $model,
          'itemList' => $itemList,   
          'result' => $result,
          'q' => $model->query
        ));
      }
    } 
    elseif (isset($_GET['search'])) {
      $offset = 0;
      if (isset($_GET['offset'])) {
        $offset = $_GET['offset'];
      }
      $result = $cv->volumeSearch($_GET['q'], array(), $offset);
      $this->render('search', array('model'=>$model,
        'result' => $result, 'q' => $_GET['q']));
        }
    else {
        $this->render('search',array('model'=>$model));
         }
  }
 
  public function actionIssues() {
    $cv = $this->newCv();
    $title = '';
    if (isset($_GET['title'])) {
      $title = CHtml::encode($_GET['title']);
    }
    if (isset($_GET['volume_id'])) {
      $volumeId = $_GET['volume_id'];
      $result = $cv->issuesForVolume($volumeId);
      if (!$this->errorHandler($result, 'issues')) {
        $issues = $result['content']->results->issues;
          $this->render('issues',array('result' => $issues, 'title' => $title));
      }
    }
    else {
        $this->render('issues',array('error'=>'No volume id or issue id specified', 'result' => null));
    }
  }
}

This gives us a way to fetch the issues associated with a volume ID specified in the query string as volume_id. If title is specified, we pass it through, escaping HTML special characters. We need to see what we are doing, so make a view in the file protected/views/cv/issues.php:

<?php 
$this->breadcrumbs=array(
  'Comicvine',
);
if (isset($error)) {
  echo($error);
}
else {
?>
<style type="text/css">
  .search_row {border-width: 1px; border-color: #0000aa; border-style: solid; padding: 4px;}
  .issue_number {float: left; width: 50px;}
  .issue_name {float: left; width: 250px; font-weight: bold;}
  .issue_detail {float: left; width: 50px;}
</style>
<div class='search_header'>
<?php 
  if(isset($title)) {
    echo("<center><u><h3>$title</h3><u></center>");
  }
?>
 
</div>
<?php
  foreach($result as $issue) {
    echo("<div class='search_row'>\n");
    echo("<div class='issue_number'>\n");
    echo((int) $issue->issue_number);
    echo("</div>");
    echo("<div class='issue_name'>\n");
    echo($issue->name?CHtml::encode($issue->name):'&nbsp;');
    echo("</div>");
    echo("<div class='issue_detail'>\n");
    echo("<a href='" . Yii::app()->request->getBaseUrl() . '/' . Yii::app()->request->getPathInfo() . 
          '?issue_id='  . $issue->id . "' target='_blank'>Details </a>");
    echo("</div>");
    echo("<div style='clear: left;'></div>");
    echo("</div>\n");
  }
}
?>
  1. If you happen to have a valid volume ID for Comic Vine, you can now test with the URL http://localhost/cbdb/index.php/cv/issues?volume_id=2870. It does not sort by issue number, so let's fix that.
  2. Add a sorting function to the controller:
static function sortIssues($a, $b)
{
    $l = $a->issue_number;
    $r = $b->issue_number;
    if ($l == $r) {
        return 0;
    }
    return ($l > $r) ? +1 : -1;
}
  1. Now sort the array in the action:
$result = $cv->issuesForVolume($volumeId);
if (!$this->errorHandler($result, 'issues')) {
    $issues = $result['content']->results->issues;
    usort($issues, array('CvController', 'sortIssues')); 
    $this->render('issues',array('result' => $issues, 'title' => $title));
}

The list is now sorted but the details link does not work.

if (isset($_GET['volume_id'])) {
    $volumeId = $_GET['volume_id'];
    $result = $cv->issuesForVolume($volumeId);
    if (!$this->errorHandler($result, 'issues')) {
        $issues = $result['content']->results->issues;
        usort($issues, array('CvController', 'sortIssues'));  
        $this->render('issues',array('result' => $issues, 'title' => $title));
    }
}
elseif (isset($_GET['issue_id'])) {
    $issueId = $_GET['issue_id'];
    $result = $cv->detailRequest('issue', $issueId, array());
    if (!$this->errorHandler($result, 'issues')) {
        $this->redirect($result['content']->results->site_detail_url);
    }
}
else {
  1. Add the previous code snippet and the site will redirect to the Comic Vine site detail URL, after fetching the detail information for that particular issue.

Objective Complete - Mini Debriefing

Now we have a way to list issues for a particular volume. We will tie this to the volume search in the next task.

Putting It All Together

We will make a link on each volume in the volume search that shows the issues in that volume.

Engage Thrusters

At this point, all we have to do to accomplish this is to make some changes in the view.

  1. Change protected/views/cv/issues.php:
foreach($result['content']->results as $rec){
    echo("<div class = 'result_row'>\n");
    echo("<div class = 'result_name'>\n");  
    echo("$rec->name\n");  
    echo("</div>\n");  
    echo("<div class = 'result_details'>\n");  
    echo("<a href='" . $rec->site_detail_url . 
        "' target='_blank'>". 'View Details' . "</a>\n");  
    echo("</div>\n");  
    echo("<div class = 'result_issues'>\n"); 
    echo("<a href='" . Yii::app()->request->getScriptUrl() .
        '/cv/issues?title=' . urlencode($rec->name) .
        '&volume_id=' . $rec->id .
        "' target='_blank'>Issues(" .
        $rec->count_of_issues . ')</a>');
    echo("</div>\n"); 
    echo("</div>\n");
    echo("<div class='clear_left' />");
}
  1. Fix the styling at the top of the file for the new field.
.result_issues {float: left; width: 100px;}
  1. Change the width of .result_header from 350px to 450px.

Objective Complete - Mini Debriefing

Now the volume search has a column for issues that opens a new tab with a list of issues and the volume title at the top. We have now successfully wrapped a hierarchical collection of resources in Comic Vine and incorporated it into Yii.

Mission Accomplished

We implemented a volume search and an issue browser, both integrated with Comic Vine.

Here is what the search looks like:

You Ready to go Gung HO? A Hotshot Challenge

This project has demonstrated how to incorporate a third party API into your Yii web app. Take this information to the next level by implementing either a way to tie a Comic Vine issue to an issue in our database or a way to import an issue from Comic Vine into our database to complete the integration.

评论 X

      友荐云推荐
      Copyright 2011-2014. YiiBook.com