Designing CakePHP plugins for consuming APIs

12,874 views

Published on

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

Published in: Technology
1 Comment
8 Likes
Statistics
Notes
No Downloads
Views
Total views
12,874
On SlideShare
0
From Embeds
0
Number of Embeds
18
Actions
Shares
0
Downloads
160
Comments
1
Likes
8
Embeds 0
No embeds

No notes for slide

Designing CakePHP plugins for consuming APIs

  1. 1. Designing CakePHP plugins for consuming APIs<br />By @neilcrookes for CakeFest2010<br />www.neilcrookes.com<br />github.com/neilcrookes<br />
  2. 2. Contents<br />Foundations<br />CakePHP plugins, APIs, REST, HTTP, CakePHP HttpSocket, OAuth<br />Design approach<br />Traditional approach, issues with that, my solution<br />Examples<br />
  3. 3. Types of CakePHP plugins<br />Mini apps<br />Provide full functionality you can include in your app e.g. blog, store locator<br />Extenders<br />Extend your app with more functionality e.g. commentable & taggable<br />Enhancers<br />Enhance your apps existing functionality e.g. filter<br />Wrappers<br />Provide functionality to access 3rd party APIs<br />
  4. 4. APIs<br />Source: http://www.programmableweb.com/apis<br /><ul><li> 73% of all APIs (listed on ProgrammableWeb) are RESTful
  5. 5. My work so far has been mainly consuming RESTful APIs so this presentation and examples will focus on REST
  6. 6. But concepts illustrated in the design approach section later on can be applied to any protocol</li></li></ul><li>Quick intro to REST<br />REspresentational State Transfer<br />“The largest known implementation of a system conforming to the REST architectural style is…” http://en.wikipedia.org/wiki/Representational_State_Transfer<br />The World Wide Web<br />Clients and servers communicating via HTTP<br />Uses existing HTTP verbs (GET, POST etc)<br />Acts on a resource (URI)<br />
  7. 7. HTTP<br />Send request<br /><HTTP verb> <URI> HTTP/1.1<br /><Header 1 name>: <Header 1 value><br />...<br /><optional body><br />You’ll get some kind of response (hopefully)<br />HTTP/1.1 <Status Code> <Status Message><br /><Header 1 name>: <Header 1 value><br />...<br /><optional body><br />
  8. 8. Simple HTTP GET Request & Response<br />Request<br />GET http://www.example.com/index.html HTTP/1.1<br />User-Agent: My Web Browser<br />Response<br />HTTP/1.1 200 OK<br />Content-Type: text/html<br />Content-Length: 70<br /><html><br /><head><br /><title>My Web Page</title><br /></head><br /><body><br /><h1>My Web Page</h1><br /></body><br /></html><br />
  9. 9. Simple HTTP POST Request & Response<br />Request<br />POST http://www.example.com/login HTTP/1.1<br />Content-Type: application/x-www-form-urlencoded<br />Content-Length: 38<br />username=neilcrookes&password=abcd1234<br />Response<br />HTTP/1.1 301 Moved Permanently<br />Location: http://www.example.com/my_account<br />
  10. 10. CakePHP’s HttpSocket Class<br />cake/libs/http_socket.php<br /><ul><li>Usage:</li></ul> App::import(‘Core’, ‘HttpSocket’);<br /> $Http = new HttpSocket();<br /> $response = $Http->request(array(<br /> ‘method’ => ‘POST’,<br /> ‘uri’ => array(<br /> ‘host’ => ‘example.com’,<br /> ‘path’ => ‘login’),<br /> ‘body’ => array(<br /> ‘username’ => ‘neilcrookes’,<br /> ‘password’ => ‘abcd1234’)));<br /><ul><li>See HttpSocket::request property for defaults </li></li></ul><li>HttpSocket<br />Handles creating, writing to and reading from sockets (because it extends CakeSocket)<br />Constructs HTTP escaped, encoded requests from array parameters you send it<br />Parses HTTP response from the server into<br />Status<br />Body<br />Cookies<br />Can handle Basic Auth (username:password@)<br />
  11. 11. OAuth<br />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.<br />In reality, it means:<br />a little bit of handshaking between your app and the service provider to get various string tokens<br />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.<br />the service provides you with a token you can persist and use to make authorized requests to their service on behalf of the user<br />In practice it’s just an extra header line (Authorization header) in the HTTP request which contains<br />some arbitrary parameters e.g. timestamp<br />a token that identifies your application to the API provider<br />a signature string that signs the request and is a hash of various request parameters and the secret tokens you retrieved above<br />Used by e.g. Twitter & Google APIs<br />
  12. 12. HttpSocketOauth<br />Usage example to tweet “Hello world!”:<br />App::import('Vendor', 'HttpSocketOauth');<br />$Http = new HttpSocketOauth();<br />$response = $Http->request(array(<br /> 'method' => 'POST',<br /> 'uri' => array(<br /> 'host' => 'api.twitter.com',<br /> 'path' => '1/statuses/update.json'),<br /> 'auth' => array(<br /> 'method' => 'OAuth',<br /> 'oauth_token' => <oauth token>,<br /> 'oauth_token_secret' => <oauth token secret>,<br /> 'oauth_consumer_key' => <oauth consumer key>,<br /> 'oauth_consumer_secret' => <oauth consumer secret>),<br /> 'body' => array(<br /> 'status' => 'Hello world!')));<br />http://www.neilcrookes.com/2010/04/12/cakephp-oauth-extension-to-httpsocket/<br />http://github.com/neilcrookes/http_socket_oauth<br />
  13. 13. Contents<br />Foundations<br />CakePHP plugins, APIs, REST, HTTP, CakePHP HttpSocket, OAuth<br />Design approach<br />Traditional approach, issues with that, my solution<br />Examples<br />
  14. 14. Traditional approach: DataSource<br />Complex DataSource containing all the logic<br />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 DataSourcehttp://book.cakephp.org/view/1077/An-Example<br />Works well for simple stuff<br />This is how I started implementing<br />
  15. 15. However...<br />Does not scale well for large APIs<br />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<br />Does not exploit built-in CakePHP goodness<br />Callbacks<br />Validation<br />Pagination<br />Does not allow for multiple models (and therefore multiple schemas) to use the same DataSource<br />Didn’t feel right to me<br />
  16. 16. So what does feel right?<br />What operations are we actually doing?<br />Reading data<br />Creating and updating data<br />Deleting data<br />i.e. Find, save & delete<br />What type of classes in CakePHP provide these methods?<br />
  17. 17. Models<br />Photo by memoflores, available under creative commons<br />http://www.flickr.com/photos/memoflores/<br />And what should models be?...<br />
  18. 18. FAT!<br />Photo by cstreetus, available under creative commons<br />http://www.flickr.com/photos/cstreetus/<br />Sorry but every other image I found through searching for “fat models” or “fat ladies” was completely inappropriate ;-)<br />
  19. 19. So if we move our API calls into Model::find(), Model::save() and Model::delete() methods<br />It feels like the right place<br />We’re more familiar with interacting with these<br />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<br />But...<br />
  20. 20. But what about CakePHP goodness?<br />Triggering callbacks<br />beforeFind(), afterFind(), beforeSave(), afterSave(), beforeValidate(), beforeDelete(), afterDelete()<br />Triggering validation<br />Handling custom find types<br />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<br />We’d be duplicating loads of code from CakePHP’s core Model class.<br />Not very DRY<br />
  21. 21. To understand the solution, we must understand CakePHP Model internals<br />Model methods like find(), save() and delete() accept various params such as conditions, data to save etc<br />Handle custom find types for find() only<br />Handle validation for save() only<br />Trigger the before*() callbacks<br />Call create(), read(), update() or delete() on that model’s DataSource<br />Trigger the after*() callbacks<br />Return the result<br />
  22. 22. So what’s my solution for designing CakePHP plugins for consuming APIs?<br />Plugin containing one model for each type of resource in the API e.g. TwitterStatus or YouTubeVideo<br />Models implement find() (or actually more commonly just CakePHP custom find types), save() and delete() methods as appropriate<br />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.<br />Cont...<br />
  23. 23. Solution continued<br />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<br />Your model’s useDbConfig property should be set to a custom DataSource that you also include in your plugin<br />Your DataSource implements the appropriate CRUD method(s) and issues the API call described in the model’s requestproperty, and returns the results<br />
  24. 24. Hmmm, sounds complicated<br />It’s not<br />I’ve written a REST DataSource you can use (see later)<br />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.<br />
  25. 25. E.g. Creating a tweet<br /><?php<br />class TwitterStatus extends AppModel {<br /> public function save($data = null) {<br /> $this->request = array(<br /> 'uri' => array(<br /> 'host' => 'api.twitter.com',<br /> 'path' => '1/statuses/update.json'),<br /> 'body' => array(<br /> 'status' => $data['TwitterStatus']['text']));<br /> return parent::save($data);<br /> }<br />}<br />?><br />
  26. 26. Which you call like this<br />ClassRegistry::init('Twitter.TwitterStatus')->save(array(<br /> 'TwitterStatus' => array(<br /> 'text' => “Hello world!”)<br />));<br />... 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.<br />
  27. 27. RestSource<br />http://www.neilcrookes.com/2010/06/01/rest-datasource-plugin-for-cakephp/<br />http://github.com/neilcrookes/CakePHP-ReST-DataSource-Plugin<br />You can set your model’s useDbConfigparam to this DataSource, or you can write your own DataSource that extends this one<br />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)<br />
  28. 28. This diagram illustrates the flow through the methods an classes involved in creating a tweet<br />https://docs.google.com/drawings/edit?id=1Aht7huICl9bhl2hWRdM0VdoaBePpJ0kXkceyQpAR8os&hl=en_GB&authkey=CISSqJkN<br />
  29. 29. In summary<br />By designing plugins like this you’re providing<br />Simple (1 line) method calls to API functions<br />That are familiar to all CakePHP bakers<br />And easy to document<br />You also get to exploit CakePHP goodness such as validation and callbacks etc<br />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<br />
  30. 30. Contents<br />Foundations<br />CakePHP plugins, APIs, REST, HTTP, CakePHP HttpSocket, OAuth<br />Design approach<br />Traditional approach, issues with that, my solution<br />Examples<br />
  31. 31. Examples<br />YouTube<br />Twitter<br />
  32. 32. Uploading a YouTube Video – you do<br />ClassRegistry::init('Gdata.YouTubeVideo')->save(array(<br /> 'YouTubeVideo' => array(<br /> 'title' => 'Flying into Chicago Airport',<br /> 'description' => 'Filmed through the plane window coming in over the lake',<br /> 'category' => 'Travel',<br /> 'keywords' => 'Chicago, Plane, Lake, Skyline',<br /> 'rate' => 'allowed',<br /> 'comment' => 'allowed',<br /> 'commentVote' => 'allowed',<br /> 'videoRespond' => 'allowed',<br /> 'embed' => 'allowed',<br /> 'syndicate' => 'allowed',<br /> 'private' => 1,<br /> 'file' => array(<br /> 'name' => 'chicago 1 060.AVI',<br /> 'type' => 'video/avi',<br /> 'tmp_name' => 'C:WindowsTempphp6D66.tmp',<br /> 'error' => 0,<br /> 'size' => 5863102))));<br />
  33. 33. Uploading a YouTube Video – plugin creates<br />POST /feeds/api/users/default/uploads HTTP/1.1<br />Host: uploads.gdata.youtube.com<br />Connection: close<br />User-Agent: CakePHP<br />Content-Type: multipart/related; boundary="Next_Part_4c801b22-52e8-4c70-961b-0534fba3b5b1“<br />Slug: chicago 1 060.AVI<br />Gdata-Version: 2<br />X-Gdata-Key: key=<my developer key><br />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“<br />Content-Length: 5864289<br />--Next_Part_4c801b22-52e8-4c70-961b-0534fba3b5b1<br />Content-Type: application/atom+xml; charset=UTF-8<br /><?xml version="1.0" encoding="utf-8"?><br /><entry xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:yt="http://gdata.youtube.com/schemas/2007"><br /> <media:group><br /> <media:title type="plain">Flying into Chicago Airport</media:title><br /> <media:description type="plain">Filmed through the plane window, shows coming in over the lake</media:description><br /> <media:category scheme="http://gdata.youtube.com/schemas/2007/categories.cat">Travel</media:category><br /> <media:keywords>Chicago, Plane, Lake, Skyline</media:keywords><br /> <yt:private/><br /> </media:group><br /> <yt:accessControl action="rate" permission="allowed"/><br /> <yt:accessControl action="comment" permission="allowed"/><br /> <yt:accessControl action="commentVote" permission="allowed"/><<br /> <yt:accessControl action="videoRespond" permission="allowed"/><br /> <yt:accessControl action="embed" permission="allowed"/><br /> <yt:accessControl action="syndicate" permission="allowed"/><br /></entry> <br />--Next_Part_4c801b22-52e8-4c70-961b-0534fba3b5b1 <br />Content-Type: video/avi<br />Content-Transfer-Encoding: binary<br /><binary file data><br />

×