Plugin with HttpSource to provide base class for datasorses with Http protocol. Based on ProLoser implementation. I make some refactoring to make HttpSource more similar to DboSource and removed OAuth component, because i think for login better use Opauth.
For existing plugins check ProLoser readme. But they will not work with HttpSource. I will adapt these plugins later.
HttpSource is an abstract class and must be extended by the Api you wish to support.
Open a bug ticket if you'd like some help making your own or just want me to do it.
It's very easy to add new ones - check out the list
cd my_cake_app/app
git clone git://github.com/imsamurai/cakephp-httpsource-datasource.git Plugin/HttpSource
or if you use git add as submodule:
cd my_cake_app
git submodule add "git://github.com/imsamurai/cakephp-httpsource-datasource.git" "app/Plugin/HttpSource"
then update submodules:
git submodule init
git submodule update
:: database.php ::
var $myapi = array(
'datasource' => 'MyPlugin.Http/MyPlugin', // Example: 'Github.Http/Github'
'host' => 'api.myplugin.com/v1',
'port' => 80,
'persistent' => false,
// These are only required for authenticated requests (write-access)
'login' => '--Your API Key--',
'password' => '--Your API Secret--',
//all other parameters that passed to config of http socket
//...
);
:: MyModel.php ::
public $useDbConfig = 'myapi';
public $useTable = 'myapi_table';
:: bootstrap.php ::
CakePlugin::load('HttpSource', array('bootstrap' => true, 'routes' => false));
CakePlugin::load('MyPlugin');
Best to just give an example. I switch the datasource on the fly because the model is actually a projects table in the
DB. I tend to query from my API and then switch to default and save the results.
class Project extends AppModel {
function findAuthedUserRepos() {
$this->setDataSource('github');
$projects = $this->find('all', array(
//used as repo name if useTable is empty, otherwise used as standart fields parameter
'fields' => 'repos'
));
$this->setDataSource('default'); // if more queries are done later
return $projects;
}
}
[MyPlugin]/Config/[MyPlugin].php
REST paths must be ordered from most specific conditions to least (or none). This is because the map is iterated through until the first path which has all of its required conditions met is found. If a path has no required conditions, it will be used. Optional conditions aren't checked, but are added when building the request.
$config['MyPlugin']['oauth'] = array(
'version' => '1.0', // [Optional] OAuth version (defaults to 1.0): '1.0' or '2.0'
'scheme' => 'https', // [Optional] Values: 'http' or 'https'
'authorize' => 'authorize', // Example URI: api.linkedin.com/uas/oauth/authorize
'request' => 'requestToken',
'access' => 'accessToken',
'login' => 'authenticate', // Like authorize, just auto-redirects
'logout' => 'invalidateToken',
);
$config['MyPlugin']['read'] = array(
// field
'people' => array(
// api url
'people/id=' => array(
// required conditions
'required' => array('id'),
//additionally you can map fields with or without callbacks
//field names are Hash path compatible
'map_fields' => array(
'new_path.name' => 'old_path.name',
'new_path_name' => 'old_path_name',
'new_array_path_name' => array(
'field' => 'old.field_string',
//any callable construction that expect one param with value
'callback' => function($value, Model $model) {
return explode(',', $value);
}
),
),
//additionally you can map conditions with or without callbacks
//field names are *NOT* Hash path compatible
'map_conditions' => array (
'new_param_name' => 'old_param_name',
'new_param_name2' => array(
'condition' => 'old_param_name2',
'callback' => function($value) {
...
return $new_value;
}
)
),
//extract result data from decoded result
//this can be field name or callback
//used only for read method
//for ex:
'map_result' => function ($result) {
return $result;
}
),
'people/url=' => array(
'required' => array('url'),
/**
* default values for required and optional conditions
*/
'defaults' => array(
'url' => 'http://example.com/'
),
/**
* Map parameters like limit, offset, etc to conditions
* by config rules.
*
* For example to map limit to parameter count:
* array(
* 'count' => 'limit+offset'
* );
*/
'map_params' => array(
'count' => 'limit'
)
),
'people/~' => array(),
),
'people-search' => array(
'people-search' => array(
// optional conditions the api call can take
'optional' => array(
'keywords'
),
),
),
);
$config['MyPlugin']['create'] = array(
);
$config['MyPlugin']['update'] = array(
);
//reserved, not used
$config['MyPlugin']['delete'] = array(
);
/**
* Map parameters like limit, offset, etc to conditions
* by config rules.
* Used if map_params not found in url config
*
* For example to map limit to parameter count:
* array(
* 'count' => 'limit+offset'
* );
*/
$config['MyPlugin']['map_read_params'] = array(
);
//extract result data from decoded result
//this can be field name or callback
//applied if callback for url not specified
//used only for read method
$config['MyPlugin']['map_result'] = function ($result) {
return $result;
};
//Cache configuration name or false or null or '' (no cache)
$config['MyPlugin']['cache'] = false;
Format $config['Apis']['MyPlugin'] not supported.
Try browsing the apis datasource and seeing what automagic functionality you can hook into.
[MyPlugin]/Model/Datasource/Http/[MyPlugin].php
App::uses('HttpSource', 'HttpSource.Model/Datasource');
Class MyPlugin extends HttpSource {
// Examples of overriding methods & attributes:
// Enable OAuth for the api
public function __construct($config) {
$config['method'] = 'OAuth'; // or 'OAuthV2'
parent::__construct($config);
}
// Last minute tweaks
public function beforeRequest($request, $request_method) {
$request['header']['x-li-format'] = $this->options['format'];
return $request;
}
}
Lets say you don't feel like bothering to make a new plugin just to support your api, or the existing plugin doesn't cover enough of the features. Good news! The plugin degrades gracefully and allows you to manually manipulate the request (thanks to NeilCrookes' RESTful plugin).
You can use query method (like with DBO) with one argument request array or uri string.
Simply populate Model->request with any request params you wish and then fire off the related action. You can even continue
using the $data & $this->data for save() and update() or pass a 'path' key to find() and it will automagically
be injected into your request object.
I'm eager to hear any recommendations or possible solutions.
- Write better documentation (wiki)
- More automagic
- Better map scanning:
I'm not sure of a good way to add map scanning to
save(),update()anddelete()methods yet since I have little control over the arguments passed to the datasource. It is easy to supplementfind()with information and utilize it for processing. - Complex query-building versatility: Some APIs have multiple different ways of passing query params. Sometimes within the same request! I still need to flesh out param-building functions and options in the driver so that people extending the datasource have less work.