33
Designing CakePHP plugins for consuming APIs By @neilcrookes for CakeFest 2010 www.neilcrookes.com github.com/neilcrookes

Designing CakePHP plugins for consuming APIs

Embed Size (px)

DESCRIPTION

Presented at CakeFest 2010. My learnings on developing CakePHP plugins for consuming APIs

Citation preview

Page 1: Designing CakePHP plugins for consuming APIs

Designing CakePHP plugins for consuming APIs

By @neilcrookes for CakeFest 2010www.neilcrookes.com

github.com/neilcrookes

Page 2: Designing CakePHP plugins for consuming APIs

Contents

• Foundations– CakePHP plugins, APIs, REST, HTTP, CakePHP

HttpSocket, OAuth• Design approach– Traditional approach, issues with that, my solution

• Examples

Page 3: Designing CakePHP plugins for consuming APIs

Types of CakePHP plugins

• Mini apps– Provide full functionality you can include in your app

e.g. blog, store locator• Extenders– Extend your app with more functionality e.g.

commentable & taggable• Enhancers– Enhance your apps existing functionality e.g. filter

• Wrappers– Provide functionality to access 3rd party APIs

Page 4: Designing CakePHP plugins for consuming APIs

APIs

Source: http://www.programmableweb.com/apis

• 73% of all APIs (listed on ProgrammableWeb) are RESTful• My work so far has been mainly consuming RESTful APIs so this presentation and examples will focus on REST• But concepts illustrated in the design approach section later on can be applied to any protocol

Page 5: Designing CakePHP plugins for consuming APIs

Quick intro to REST

• REspresentational State Transfer• “The largest known implementation of a

system conforming to the REST architectural style is…” http://en.wikipedia.org/wiki/Representational_State_Transfer

• The World Wide Web• Clients and servers communicating via HTTP• Uses existing HTTP verbs (GET, POST etc)• Acts on a resource (URI)

Page 6: Designing CakePHP plugins for consuming APIs

HTTP

<HTTP verb> <URI> HTTP/1.1<Header 1 name>: <Header 1 value>...

<optional body>

HTTP/1.1 <Status Code> <Status Message><Header 1 name>: <Header 1 value>...

<optional body>

Send request

You’ll get some kind of response (hopefully)

Page 7: Designing CakePHP plugins for consuming APIs

Simple HTTP GET Request & Response

GET http://www.example.com/index.html HTTP/1.1User-Agent: My Web Browser

HTTP/1.1 200 OKContent-Type: text/htmlContent-Length: 70

<html><head><title>My Web Page</title></head><body><h1>My Web Page</h1></body></html>

Request

Response

Page 8: Designing CakePHP plugins for consuming APIs

Simple HTTP POST Request & Response

POST http://www.example.com/login HTTP/1.1Content-Type: application/x-www-form-urlencodedContent-Length: 38

username=neilcrookes&password=abcd1234

HTTP/1.1 301 Moved PermanentlyLocation: http://www.example.com/my_account

Request

Response

Page 9: Designing CakePHP plugins for consuming APIs

CakePHP’s HttpSocket Class

• cake/libs/http_socket.php

• See HttpSocket::request property for defaults

• Usage: App::import(‘Core’, ‘HttpSocket’); $Http = new HttpSocket(); $response = $Http->request(array( ‘method’ => ‘POST’, ‘uri’ => array( ‘host’ => ‘example.com’, ‘path’ => ‘login’), ‘body’ => array( ‘username’ => ‘neilcrookes’, ‘password’ => ‘abcd1234’)));

Page 10: Designing CakePHP plugins for consuming APIs

HttpSocket

• Handles creating, writing to and reading from sockets (because it extends CakeSocket)

• Constructs HTTP escaped, encoded requests from array parameters you send it

• Parses HTTP response from the server into– Status– Body– Cookies

• Can handle Basic Auth (username:password@)

Page 11: Designing CakePHP plugins for consuming APIs

OAuth• In summary, it allows users of a service (e.g. Twitter) to authorize other parties (i.e.

your application) access to their accounts on that service, without sharing their password with the other parties.

• In reality, it means:– a little bit of handshaking between your app and the service provider to get various string

tokens– redirecting the user to the service in order for them to authorize your app to access their

account, so the user only signs in to the service, not your app.– the service provides you with a token you can persist and use to make authorized requests to

their service on behalf of the user• In practice it’s just an extra header line (Authorization header) in the HTTP request

which contains– some arbitrary parameters e.g. timestamp– a token that identifies your application to the API provider– a signature string that signs the request and is a hash of various request parameters and the

secret tokens you retrieved above• Used by e.g. Twitter & Google APIs

Page 12: Designing CakePHP plugins for consuming APIs

HttpSocketOauth

• http://www.neilcrookes.com/2010/04/12/cakephp-oauth-extension-to-httpsocket/• http://github.com/neilcrookes/http_socket_oauth

Usage example to tweet “Hello world!”:

App::import('Vendor', 'HttpSocketOauth');$Http = new HttpSocketOauth();$response = $Http->request(array( 'method' => 'POST', 'uri' => array( 'host' => 'api.twitter.com', 'path' => '1/statuses/update.json'), 'auth' => array( 'method' => 'OAuth', 'oauth_token' => <oauth token>, 'oauth_token_secret' => <oauth token secret>, 'oauth_consumer_key' => <oauth consumer key>, 'oauth_consumer_secret' => <oauth consumer secret>), 'body' => array( 'status' => 'Hello world!')));

Page 13: Designing CakePHP plugins for consuming APIs

Contents

• Foundations– CakePHP plugins, APIs, REST, HTTP, CakePHP

HttpSocket, OAuth• Design approach– Traditional approach, issues with that, my solution

• Examples

Page 14: Designing CakePHP plugins for consuming APIs

Traditional approach: DataSource

• Complex DataSource containing all the logic• Call methods on the DataSource directly from

your models or controllersor as implied by the example Twitter DataSource in the cook book: access DataSource methods through your models but include most of the logic in the DataSource http://book.cakephp.org/view/1077/An-Example

• Works well for simple stuff• This is how I started implementing

Page 15: Designing CakePHP plugins for consuming APIs

However...

• Does not scale well for large APIs– Twitter has ~100 API calls available, all with a wide variety of

options and parameters. The cook book Twitter DataSource partially implements 2 API calls and is 86 lines

• Does not exploit built-in CakePHP goodness– Callbacks– Validation– Pagination

• Does not allow for multiple models (and therefore multiple schemas) to use the same DataSource

• Didn’t feel right to me

Page 16: Designing CakePHP plugins for consuming APIs

So what does feel right?

• What operations are we actually doing?– Reading data– Creating and updating data– Deleting data

• i.e. Find, save & delete• What type of classes in CakePHP provide these

methods?

Page 17: Designing CakePHP plugins for consuming APIs

Models

And what should models be?...

Photo by memoflores, available under creative commonshttp://www.flickr.com/photos/memoflores/

Page 18: Designing CakePHP plugins for consuming APIs

FAT!

Sorry but every other image I found through searching for “fat models” or “fat ladies” was completely inappropriate ;-)

Photo by cstreetus, available under creative commonshttp://www.flickr.com/photos/cstreetus/

Page 19: Designing CakePHP plugins for consuming APIs

So if we move our API calls into Model::find(), Model::save() and Model::delete() methods

• It feels like the right place• We’re more familiar with interacting with these• We can have lots of simple models classes to

achieve scale, separation of concerns and different models can have different validation rules and schemas and we can collect them together in a plugin

• But...

Page 20: Designing CakePHP plugins for consuming APIs

But what about CakePHP goodness?

• Triggering callbacks– beforeFind(), afterFind(), beforeSave(), afterSave(), beforeValidate(),

beforeDelete(), afterDelete()• Triggering validation• Handling custom find types

• If we made the API calls directly in these methods and returned the response, to exploit this excellent built-in additional CakePHP functionality we’d have to trigger/code them manually

• We’d be duplicating loads of code from CakePHP’s core Model class.

• Not very DRY

Page 21: Designing CakePHP plugins for consuming APIs

To understand the solution, we must understand CakePHP Model internals

• Model methods like find(), save() and delete() accept various params such as conditions, data to save etc

• Handle custom find types for find() only• Handle validation for save() only• Trigger the before*() callbacks• Call create(), read(), update() or delete() on that

model’s DataSource• Trigger the after*() callbacks• Return the result

Page 22: Designing CakePHP plugins for consuming APIs

So what’s my solution for designing CakePHP plugins for consuming APIs?

• Plugin containing one model for each type of resource in the API e.g. TwitterStatus or YouTubeVideo

• Models implement find() (or actually more commonly just CakePHP custom find types), save() and delete() methods as appropriate

• These methods set the details of the request, i.e. The array that represents an HTTP request that HttpSocket::request() methods expects (as we saw earlier in this presentation) in a request property of your model, then calls the same method on the parent object i.e. Model.

• Cont...

Page 23: Designing CakePHP plugins for consuming APIs

Solution continued

• CakePHP Model class handles validation and custom find types, triggers callbacks etc then calls create(), read(), update() or delete() on the child model’s (your model’s) DataSource, and passes the model object

• Your model’s useDbConfig property should be set to a custom DataSource that you also include in your plugin

• Your DataSource implements the appropriate CRUD method(s) and issues the API call described in the model’s request property, and returns the results

Page 24: Designing CakePHP plugins for consuming APIs

Hmmm, sounds complicated

• It’s not• I’ve written a REST DataSource you can use

(see later)• All you have to do is create a model that has

find() or save() methods, in which you set a request property to an array expected by HttpSocket::request() and call the same method on the parent.

Page 25: Designing CakePHP plugins for consuming APIs

E.g. Creating a tweet<?phpclass TwitterStatus extends AppModel { public function save($data = null) { $this->request = array( 'uri' => array( 'host' => 'api.twitter.com', 'path' => '1/statuses/update.json'), 'body' => array( 'status' => $data['TwitterStatus']['text'])); return parent::save($data); }}?>

Page 26: Designing CakePHP plugins for consuming APIs

Which you call like this

ClassRegistry::init('Twitter.TwitterStatus')->save(array( 'TwitterStatus' => array( 'text' => “Hello world!”)));

... from anywhere you like in your CakePHP application, e.g. In your Post model afterSave() method, thus automatically creating a tweet every time you create a new post.

Page 27: Designing CakePHP plugins for consuming APIs

RestSource• http://www.neilcrookes.com/2010/06/01/rest-datasource-plugin-for-cakephp/• http://github.com/neilcrookes/CakePHP-ReST-DataSource-Plugin

• You can set your model’s useDbConfig param to this DataSource, or you can write your own DataSource that extends this one

• E.g. Override RestSource::request() to add in the host key in the $model->request property if it’s the same for all API calls, then call parent::(request)

Page 28: Designing CakePHP plugins for consuming APIs

https://docs.google.com/drawings/edit?id=1Aht7huICl9bhl2hWRdM0VdoaBePpJ0kXkceyQpAR8os&hl=en_GB&authkey=CISSqJkN

This diagram illustrates the flow through the methods an classes involved in creating a tweet

Page 29: Designing CakePHP plugins for consuming APIs

In summary

• By designing plugins like this you’re providing• Simple (1 line) method calls to API functions• That are familiar to all CakePHP bakers• And easy to document• You also get to exploit CakePHP goodness such as

validation and callbacks etc• You can have multiple models, one for each

resource type on the API, each with it’s own schema (which the FormHelper uses) and validation rules

Page 30: Designing CakePHP plugins for consuming APIs

Contents

• Foundations– CakePHP plugins, APIs, REST, HTTP, CakePHP

HttpSocket, OAuth• Design approach– Traditional approach, issues with that, my solution

• Examples

Page 31: Designing CakePHP plugins for consuming APIs

Examples

• YouTube• Twitter

Page 32: Designing CakePHP plugins for consuming APIs

Uploading a YouTube Video – you doClassRegistry::init('Gdata.YouTubeVideo')->save(array( 'YouTubeVideo' => array( 'title' => 'Flying into Chicago Airport', 'description' => 'Filmed through the plane window coming in over the lake', 'category' => 'Travel', 'keywords' => 'Chicago, Plane, Lake, Skyline', 'rate' => 'allowed', 'comment' => 'allowed', 'commentVote' => 'allowed', 'videoRespond' => 'allowed', 'embed' => 'allowed', 'syndicate' => 'allowed', 'private' => 1, 'file' => array( 'name' => 'chicago 1 060.AVI', 'type' => 'video/avi', 'tmp_name' => 'C:\Windows\Temp\php6D66.tmp', 'error' => 0, 'size' => 5863102))));

Page 33: Designing CakePHP plugins for consuming APIs

Uploading a YouTube Video – plugin createsPOST /feeds/api/users/default/uploads HTTP/1.1Host: uploads.gdata.youtube.comConnection: closeUser-Agent: CakePHPContent-Type: multipart/related; boundary="Next_Part_4c801b22-52e8-4c70-961b-0534fba3b5b1“Slug: chicago 1 060.AVIGdata-Version: 2X-Gdata-Key: key=<my developer key>Authorization: OAuth oauth_version="1.0",oauth_signature_method="HMAC-SHA1",oauth_consumer_key="anonymous",oauth_token=“<my oauth

token>",oauth_nonce="fa4b6fc350e19f675f2e5660657e643c",oauth_timestamp="1283463971",oauth_signature="3fIXJ%2BmdV6KLk4zJYszR7M90lIg%3D“Content-Length: 5864289

--Next_Part_4c801b22-52e8-4c70-961b-0534fba3b5b1Content-Type: application/atom+xml; charset=UTF-8

<?xml version="1.0" encoding="utf-8"?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:yt="http://gdata.youtube.com/schemas/2007"> <media:group> <media:title type="plain">Flying into Chicago Airport</media:title> <media:description type="plain">Filmed through the plane window, shows coming in over the lake</media:description> <media:category scheme="http://gdata.youtube.com/schemas/2007/categories.cat">Travel</media:category> <media:keywords>Chicago, Plane, Lake, Skyline</media:keywords> <yt:private/> </media:group> <yt:accessControl action="rate" permission="allowed"/> <yt:accessControl action="comment" permission="allowed"/> <yt:accessControl action="commentVote" permission="allowed"/>< <yt:accessControl action="videoRespond" permission="allowed"/> <yt:accessControl action="embed" permission="allowed"/> <yt:accessControl action="syndicate" permission="allowed"/></entry>

--Next_Part_4c801b22-52e8-4c70-961b-0534fba3b5b1 Content-Type: video/avi Content-Transfer-Encoding: binary

<binary file data>