Creating and Consuming Web Services in CakePHP 1.3

Exclusive offer: get 50% off this eBook here
CakePHP 1.3 Application Development Cookbook

CakePHP 1.3 Application Development Cookbook — Save 50%

Over 70 great recipes for developing, maintaining, and deploying web applications

£14.99    £7.50
by Mariano Iglesias | March 2011 | Open Source

Web services are essential when looking forward to expose application functionality to third-party applications, or when looking forward to integrate foreign services into our own applications. They offer a broad set of technologies and definitions so that systems written in different programming languages can communicate.

In this article by Mariano Iglesias, author of CakePHP 1.3 Application Development Cookbook, we will cover:

  • Creating an RSS feed
  • Consuming a JSON service

 

CakePHP 1.3 Application Development Cookbook

CakePHP 1.3 Application Development Cookbook

Over 70 great recipes for developing, maintaining, and deploying web applications

        Read more about this book      

(For more resources on CakePHP, see here.)

Creating an RSS feed

RSS feeds are a form of web services, as they provide a service, over the web, using a known format to expose data. Due to their simplicity, they are a great way to introduce us to the world of web services, particularly as CakePHP offers a built in method to create them.

In this recipe, we will produce a feed for our site that can be used by other applications.

Getting ready

To go through this recipe we need a sample table to work with. Create a table named posts, using the following SQL statement:

CREATE TABLE `posts`(posts
`id` INT NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NOT NULL,
`body` TEXT NOT NULL,
`created` DATETIME NOT NULL,
`modified` DATETIME NOT NULL,
PRIMARY KEY(`id`)
);

Add some sample data, using the following SQL statements:

INSERT INTO `posts`(`title`,posts `body`, `created`, `modified`)
VALUES
('Understanding Containable', 'Post body', NOW(), NOW()),
('Creating your first test case', 'Post body', NOW(), NOW()),
('Using bake to start an application', 'Post body', NOW(), NOW()),
('Creating your first helper', 'Post body', NOW(), NOW()),
('Adding indexes', 'Post body', NOW(), NOW());

We proceed now to create the required controller. Create the class PostsController in a file named posts_controller.php and place it in your app/controllers folder, with the following contents:

<?php
class PostsController extends AppController {
public function index() {
$posts = $this->Post->find('all');
$this->set(compact('posts'));
}
}
?>

Create a folder named posts in your app/views folder, and then create the index view in a file named index.ctp and place it in your app/views/posts folder, with the following contents:

<h1>Posts</h1>
<?php if (!empty($posts)) { ?>
<ul>
<?php foreach($posts as $post) { ?>
<li><?php echo $this->Html->link(
$post['Post']['title'],
array(
'action'=>'view',
$post['Post']['id']
)
); ?></li>
<?php } ?>
</ul>
<?php } ?>

How to do it...

  1. Edit your app/config/routes.php file and add the following statement at the end:
    Router::parseExtensions('rss');
  2. Edit your app/controllers/posts_controller.php file and add the following property to the PostsController class:
    public $components = array('RequestHandler');
  3. While still editing PostsController, make the following changes to the index() method:

    public function index() {
    $options = array();
    if ($this->RequestHandler->isRss()) {
    $options = array_merge($options, array(
    'order' => array('Post.created' => 'desc'),
    'limit' => 5
    ));
    }
    $posts = $this->Post->find('all', $options);
    $this->set(compact('posts'));
    }

  4. Create a folder named rss in your app/views/posts folder, and inside the rss folder create a file named index.ctp, with the following contents:

    <?php
    $this->set('channel', array(
    'title' => 'Recent posts',
    'link' => $this->Rss->url('/', true),
    'description' => 'Latest posts in my site'
    ));

    $items = array();
    foreach($posts as $post) {
    $items[] = array(
    'title' => $post['Post']['title'],
    'link' => array('action'=>'view', $post['Post']['id']),
    'description' => array('cdata'=>true, 'value'=>$post['Post']
    ['body']),
    'pubDate' => $post['Post']['created']
    );
    }

    echo $this->Rss->items($items);
    ?>

  5. Edit your app/views/posts/index.ctp file and add the following at the end of the view:

    <?php echo $this->Html->link('Feed', array('action'=>'index',
    'ext'=>'rss')); ?>

If you now browse to http://localhost/posts, you should see a listing of posts with a link entitled Feed. Clicking on this link should produce a valid RSS feed, as shown in the following screenshot:

CakePHP 1.3 Application Development Cookbook

If you view the source of the generated response, you can see that the source for the first item within the RSS document is:

<item>
<title>Understanding Containable</title>
<link>http://rss.cookbook7.kramer/posts/view/1</link>
<description><![CDATA[Post body]]></description>
<pubDate>Fri, 20 Aug 2010 18:55:47 -0300</pubDate>
<guid>http://rss.cookbook7.kramer/posts/view/1</guid>
</item>

How it works...

We started by telling CakePHP that our application accepts the rss extension with a call to Router::parseExtensions(), a method that accepts any number of extensions. Using extensions, we can create different versions of the same view. For example, if we wanted to accept both rss and xml as extensions, we would do:

Router::parseExtensions('rss', 'xml');

In our recipe, we added rss to the list of valid extensions. That way, if an action is accessed using that extension, for example, by using the URL http://localhost/posts.rss, then CakePHP will identify rss as a valid extension, and will execute the ArticlesController::index() action as it normally would, but using the app/views/posts/rss/index.ctp file to render the view. The process also uses the file app/views/layouts/rss/default.ctp as its layout, or CakePHP's default RSS layout if that file is not present.

We then modify how ArticlesController::index() builds the list of posts, and use the RequestHandler component to see if the current request uses the rss extension. If so, we use that knowledge to change the number and order of posts.

In the app/views/posts/rss/index.ctp view, we start by setting some view variables. Because a controller view is always rendered before the layout, we can add or change view variables from the view file, and have them available in the layout. CakePHP's default RSS layout uses a $channel view variable to describe the RSS feed. Using that variable, we set our feed's title, link, and description.

We proceed to output the actual item files. There are different ways to do so, the first one is making a call to the RssHelper::item() method for each item, and the other one requires only a call to RssHelper::items(), passing it an array of items. We chose the latter method due to its simplicity.

While we build the array of items to be included in the feed, we only specify title, link, description, and pubDate. Looking at the generated XML source for the item, we can infer that the RssHelper used our value for the link element as the value for the guid (globally unique identifier) element.

Note that the description field is specified slightly differently than the values for the other fields in our item array. This is because our description may contain HTML code, so we want to make sure that the generated document is still a valid XML document.

By using the array notation for the description field, a notation that uses the value index to specify the actual value on the field, and by setting cdata to true, we are telling the RssHelper (actually the XmlHelper from which RssHelper descends) that the field should be wrapped in a section that should not be parsed as part of the XML document, denoted between a <![CDATA[ prefix and a ]]> postfix.

The final task in this recipe is adding a link to our feed that is shown in the index.ctp view file. While creating this link, we set the special ext URL setting to rss. This sets the extension for the generated link, which ends up being http://localhost/posts.rss.

 

CakePHP 1.3 Application Development Cookbook Over 70 great recipes for developing, maintaining, and deploying web applications
Published: March 2011
eBook Price: £14.99
Book Price: £24.99
See more
Select your format and quantity:

 

        Read more about this book      

(For more resources on CakePHP, see here.)

Adding view caching to an RSS feed

Our feeds may be consumed by feed search crawlers. If we are lucky, we may get tons and tons of requests looking for updates to our blog. It is unlikely that we will update our blog so often that we would have new posts every second, so our server load may force us to add some caching.

When looking to improve performance, some developers are content to only cache their database queries. In our recipe, this would mean caching the results obtained from our $this->Post->find('all') call. Unless we have our database engine on a separate server that suffers from some considerable network latency, chances are this sort of caching will offer little or no benefit.

A much better solution is to use view caching. That is, caching the generated RSS feed, and using that cached document whenever a request is made to our feed, provided we are within the cache time. Fortunately, CakePHP offers us a view-caching implementation right from the dispatcher, speeding up the request considerably. If a cached view file is found, that file is rendered directly to the client, without any intervention by the controller, or the need to load models, components, or helpers.

We want to add caching only when our PostsController::index() action is accessed with the rss extension. That is, we don't want to cache the listing of posts, but its feed. So we will make sure to only specify caching information when a feed is requested. In fact, we are going to cache all actions in our PostsController whenever the rss extension is used.

The first thing we need to do is tell CakePHP to take view caching into account. Edit your app/config/core.php file and uncomment the following line:

Configure::write('Cache.check', true);

Next, edit your app/controllers/posts_controller.php file and add the Cache helper to the PostsController class. Without it, view caching will simply not work:

public $helpers = array('Cache');

While still editing the PostsController class, add the following method:

public function beforeFilter() {
parent::beforeFilter();
if ($this->RequestHandler->isRss()) {
$this->cacheAction = array($this->action => '1 hour');
}
}

In this beforeFilter() implementation, we are checking to see if the current request was made using the rss extension. If so, we add the current action (whatever that may be) to the list of cached actions, and set the cache time to be 1 hour.

If we access the feed multiple times within the hour, we should see the same feed we have been getting so far, but coming from the cache instead of being built in real time.

Consuming a JSON service

JSON (JavaScript Object Notation) is probably one of the best formats available for exposing data, due to its easy-to-read syntax, which greatly simplifies the parsing. In fact, PHP (as of its 5.2.0 release) provides built-in methods to convert data from a JSON-formatted string to a PHP native data type and from PHP types to JSON.

In this recipe, we will learn how to use the HttpSocket class to consume a JSON service from a foreign site. This time, we are going to use the YouTube JSON API to allow our users to search for YouTube videos that match a given search query.

The JSON service we will be consuming from YouTube uses a variant of JSON, called JSON-C. JSON-C is nothing more than JSON, but Google is making a distinction between what YouTube used to provide as JSON, and the new version it is now producing. YouTube's JSON-C-based responses are far simpler than their JSON service. Consequently, Google has decided to deprecate JSON in favor of JSON-C in the near future.

How to do it...

  1. Start by creating the main controller in a file named videos_controller.php and place it in your app/controllers folder, with the following contents:

    <?php
    class VideosController extends AppController {
    public function index() {
    if (!empty($this->data)) {
    $videos = $this->Video->search($this->data);
    $this->set(compact('videos'));
    }
    }
    }
    ?>

  2. Create the required model in a file named video.php and place it in your app/models folder, with the following contents:

    <?php
    App::import('Core', 'HttpSocket');
    class Video extends AppModel {
    public $useTable = false;
    protected $_httpSocket;

    public function __construct($id = false, $table = null, $ds =
    null) {
    parent::__construct($id, $table, $ds);
    $this->_httpSocket = new HttpSocket();
    }
    public function search($data) {
    $query = !empty($data[$this->alias]['q']) ?
    $data[$this->alias]['q'] :
    '';

    $this->_httpSocket->reset();
    $response = $this->_httpSocket->get(
    'http://gdata.youtube.com/feeds/api/videos',
    array(
    'v' => '2',
    'alt' => 'jsonc',
    'q' => $query,
    'orderby' => 'updated'
    )
    );
    $videos = array();
    if (!empty($response)) {
    $response = json_decode($response);
    if (empty($response) || empty($response->data->items)) {
    return $videos;
    }
    foreach($response->data->items as $item) {
    $videos[] = array('Video' => array(
    'url' => $item->player->default,
    'title' => $item->title,
    'uploaded' => strtotime($item->uploaded),
    'category' => $item->category,
    'description' => $item->description,
    'thumbnail' => $item->thumbnail->sqDefault
    ));
    }
    }
    return $videos;
    }
    }
    ?>

  3. Create a view folder named videos in your app/views folder. Then, create a file named index.ctp and place it in your app/views/videos folder, with the following contents:

    <?php
    echo $this->Form->create();
    echo $this->Form->input('q', array('label'=>'Search terms:'));
    echo $this->Form->end('Search');

    if (!empty($videos)) {
    ?>
    <h1>Search results</h1>
    <?php foreach($videos as $video) { ?>
    <div style="float: left; clear: both; margin-bottom: 10px;">
    <h4><?php echo $this->Html->link($video['Video']['title'],
    $video['Video']['url']); ?></h4>
    <?php echo $this->Html->image($video['Video']['thumbnail'],
    array(
    'url' => $video['Video']['url'],
    'align' => 'left',
    'style' => 'margin-right: 10px;'
    )); ?>
    <p><?php echo $video['Video']['description']; ?></p>
    <br />
    <p><small>
    Uploaded on <?php echo date('F d, Y H:i', $video['Video']
    ['uploaded']); ?>
    in <?php echo $video['Video']['category']; ?>
    -
    <strong><?php echo $this->Html->link('PLAY', $video['Video']
    ['url']); ?></strong>
    </small></p>
    </div>
    <?php
    }
    }
    ?>

If you now browse to http://localhost/videos, you will see a search form. Entering CakePHP and clicking the button Search should give you a set of results similar to those shown in the following screenshot:

(Move the mouse over the image to enlarge.)

How it works...

The controller class (ArticlesController) and the view file (index.ctp) have no connection with the underlying web service we are consuming. In fact, if you look closely at their code, they look like a regular controller and a standard view file. This is because we decided to encapsulate the service logic in a model.

Doing so allows us to change how we communicate with the service provider without having to modify neither the controller nor the view. That is one of the many advantages of the MVC (Model View Controller) architecture that is the foundation of CakePHP.

We could have taken a more complex approach, and decided to build a datasource to interact with the server. Instead, we chose a simpler route, by creating a model method that would perform the actual search and return the results in a data format typical of any CakePHP application.

This is what the Video model is there for. As there's no underlying table for our videos, we set the model $useTable property to false. We also import the HttpSocket class, part of CakePHP's core, because it will be the mechanism we will use to communicate with the server.

The search() method is where the magic happens. The first thing we do is extract the search terms out of the submitted data. We then create an instance of HttpSocket, and use its get method to perform the request.

HttpSocket::get() takes three parameters:

  • $uri: The URL to which we are making the request. This can be either a string, or an array that contains the different elements of the URL, such as scheme, host, port, and path.
  • $query: An array of parameters to append to the URL. The indexes in this array are the parameter names and the values their respective values.
  • $request: An array with any additional request information to send to the URL, such as method, header, and body.

In our case we specify the URL to the YouTube video API, and we set the following query parameters:

  • v: The API version to use.
  • alt: The format to get results in.
  • q: The query to use for searching.
  • orderby: The order in which to get the results.

Once we get the response, we decode it using PHP's json_decode() function, which converts a JSON string into a PHP object or to null if it is not a valid JSON string. For example, the following JSON:

{
"name": "Mariano Iglesias",
"profile": {
"url": "http://marianoiglesias.com.ar"
}
}

Would be evaluated to a PHP class with two public attributes: name, and profile. The profile attribute will itself be a class, with one public attribute: url. If we had the above JSON string in a variable called $json, the following code would output Mariano Iglesias has a website in http://marianoiglesias.com.ar:

$user = json_decode($json);
echo $user->name . ' has a website in ' . $user->profile->url;

Back to the Video::search() method. Once we have decoded the JSON response, we check to make sure there are resulting videos available in the $response->data->items property. If so, we iterate through them, and we add elements to our response array, specifying only a subset of the data we obtained.

Once we have the data prepared, we return it back to the controller, which sends it to the view to render the results.

Summary

In this article we saw recipes on how to consume web services and to expose parts of our application as web services.


Further resources on this subject:


CakePHP 1.3 Application Development Cookbook Over 70 great recipes for developing, maintaining, and deploying web applications
Published: March 2011
eBook Price: £14.99
Book Price: £24.99
See more
Select your format and quantity:

About the Author :


Mariano Iglesias

Mariano has been programming since the early age of 11. He quickly became proficient with C and C++, producing his first commercial applications at 15 years of age. He then continued to learn several other programming languages, PHP being one of them. He has contributed to several open source projects, becoming part of the core development team of both CakePHP and Lithium, two of the best PHP frameworks. He currently divides his time between open source programming, his commercial work, his hobbies, and spending time with his family.

Books From Packt


Expert PHP 5 Tools
Expert PHP 5 Tools

PHP jQuery Cookbook
PHP jQuery Cookbook

PHP 5 E-commerce Development
PHP 5 E-commerce Development

PHP 5 Social Networking
PHP 5 Social Networking

AJAX and PHP: Building Modern Web Applications 2nd Edition
AJAX and PHP: Building Modern Web Applications 2nd Edition

PHP 5 CMS Framework Development - 2nd Edition
PHP 5 CMS Framework Development - 2nd Edition

CMS Design Using PHP and jQuery
CMS Design Using PHP and jQuery

CodeIgniter 1.7 Professional Development
CodeIgniter 1.7 Professional Development


Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software