FreshRest ¶
- Example yii configurations
- Recommended RESTful reading
- Actions and Verbs
- Offset and Limit
- Timestamp Filter
- User Defined Filters
- Versions
- Authentication
- Controllers
- Models
FreshRest is an elegant Yii extension and module which enables easy RESTful interface development following best practices.
The extension contains three basic building blocks: module based API interfaces, API action controllers, and API resource models.
- Multiple API interfaces can be created as separate modules, allowing for easy versioning maintenance and siloing of access.
- controllers automatically handle CRUD operations on resource models, and easily allow the addition of custom actions.
- API resources is an enhancement to Active Record and contains mapping between active record and the RESTful API.
Project Page: https://bitbucket.org/onebesky/freshrest
Installation ¶
- copy extension files to /extensions/freshRest
- create a module to act as the defenition of your api (i.e. /modules/api1). The included example module can help you get started.
- add the module to your yii configuration file
Example yii configurations ¶
routes for subdomain version accessed as api1.myproject.com:
'components' => array(
'urlManager' => array(
'rules' => array(
// custom actions in resource controller
array('api1/<controller>/<action>', 'pattern' => 'http://api1.*/<controller:\w+>/<action:\w+>/<id:\d+>'),
// crud for resource controller
array('api1/<controller>/<action>', 'pattern' => 'http://api1.*/<controller:\w+>/<action:\w+>'),
// everything else goes to the default controller
array('api1/default/<action>', 'pattern' => 'http://api1.*/<action:\w+>'),
),
),
),
routes for simple path version accessed as myproject.com/api1:
'components' => array(
'urlManager' => array(
'rules' => array(
// custom actions in resource controller
array('api1/<controller>/<action>', 'pattern' => 'api1/<controller:\w+>/<action:\w+>/<id:\d+>'),
// crud for resource controller
array('api1/<controller>/<action>', 'pattern' => 'api1/<controller:\w+>/<action:\w+>'),
// everything else goes to the default controller
array('api1/default/<action>', 'pattern' => 'api1/<action:\w+>'),
),
),
),
enable module:
'modules' => array(
// name and version of the module
'api1' => array(
'class' => 'application.modules.api1.ApiModule',
// optional configuration:
'baseUrl' => 'api.myproject.com', // skip to use path format myproject.com/api1
'lastUpdateAttribute' => 'update_time', // DATETIME field that contains last update time of active record
'format' => 'json', // only json is supported so far
'authModelClass' => 'FrAuthModel', // override this class to change authentication behavior
'myAuthenticatedModelClass' => 'Organization', // active record that used for login
'myAuthenticatedModelPasswordField' => 'api_password',
),
),
How to Use ¶
Recommended RESTful reading ¶
Ebook from Apigee: http://apigee.com/about/api-best-practices.
Actions and Verbs ¶
Resources should be named in plural. If database table name is project
and active record class name is Project
, then name your API resource projects
.
This way we will result in the following urls:
HTTP Method | URL | Description |
---|---|---|
GET | /projects | list of all projects |
GET | /projects/5547 | view one project only |
POST | /projects | create new project |
PUT | /projects/5547 | update one project |
DELETE | /projects/5547 | delete one project |
Offset and Limit ¶
The extension supports offset and limit data selection options passed in via the URL: limit=100&offset=50
will display 100 records starting with record 51.
Timestamp Filter ¶
Timestamp filter is useful for data synchronization. Every response contains a timestamp
field that can be passed into the next request to load only
data changed since the previous request. To enable this behavior all related active records need to have a timestamp column (for example update_time
) that is
updated with each active record change - typically in the beforeSave()
function.
Usage: /api1/items?timestamp=1394048408
User Defined Filters ¶
The filter GET attribute enables search functionality and is applied on top of the default class lookup criteria. This attribute can be a simple array("column"=>"value") column filter or more complex expression. For example, a json representation of the filter GET attribute that one would use to filter for zipcode 93xxx is:
[
{
field: 'zipcode',
operator: '>',
value: 93000
},
{
field: 'zipcode',
operator: '<',
value: 94000
}
]
Versions ¶
The base url can be either subdomain (api1.myproject.com) or path (myproject.com/api1). With any significant changes to the interface.
Authentication ¶
The built-in authentication uses a combination of an authentication token loaded from the url and the ip address to authenticate each request. It is attached to active record through a polymorphic connection. Using the authentication component also gives you access to the authenticated model in any API resource.
Setup ¶
- Create a password field in a table that represents a client (i.e. user or organization). In the module configuration add the following attributes:
// module setup
'api1' => array(
'myAuthenticatedModelClass' => 'Organization', // active record class that will be available in all models after authentication
'myAuthenticatedModelPasswordField' => 'api_password' // table field that contains “secret” password
),
- Enable the Auth Filter by adding it to each controller:
public function filters() {
return array(
array(
'ext.freshRest.FrAuthFilter'
)
);
}
Authentication Process ¶
Get the authentication token
- call authenticate action in the default controller passing password through POST (and preferably ssl too)
FrAuthModel
compares the password to the one stored in the database based on your module configurationFrAuthModel
creates a new record with the authentication token and IP address
Use the token add
&key=randomauthneticationtoken
to the request url
Module Development ¶
Controllers ¶
Controllers inherit from the FrApiBaseController
class.
Each action translates directly into a controller action. The default controller DefaultController.php
handles
authentication and the root index action. It should also have all actions that are not resource related. For example, calculateDistance($latituedA, $longitudeA, $latituedB, $longitudeB)
.
These actions should use verbs.
Any other controller manages one API Resource. For instance, ProjectsController.php would handle all CRUD actions for the Projects api resource.
Index, view, update, create, and delete actions work out of the box, but can be customized by redefining the action within the resource controller.
The controller looks for a model that has the same name, but it can be modified by overriding the resourceClassName
class variable.
This type of controller will also manage any non-CRUD actions that are resource related. For example, /api1/projects/deploy/5562
will trigger actionDeploy($id){...}
.
You can use yii filters in any controller to enforce authentication, disable an action, or accept only post requests.
public function filters() {
return array(
// disable builtin actions
'disabled +delete, update, create',
// action receiveGoods can be submitted only through POST
'postOnly +receiveGoods',
// enable authentication
array(
'ext.freshRest.FrAuthFilter'
)
);
}
Models ¶
Models (API Resources) inherit from the FrApiResource
class. They can be connected to active record or act as standalone form models.
Basic setup requires all the attributes to be defined as a public property and list them all in the rules()
function.
To integrate with active record two functions must be implemented: activeRecordClassName()
, which returns a string name for the active record model, and attributeMap()
,
which returns a mapping array between the active record attributes (key) and the api resource attributes (value). This function maps attributes 1:1 by default.
Scenarios ¶
Scenarios are used to display or accept different attributes in different actions. Also, they can create different lookup criteria for different actions.
Built-in scenarios:
- create - called from create action
- update - called from update action
- list - called from list (index) action
- view - called from view action and as a result of successful create or update action
- setApiParams - used before the model is loaded to pass additional GET attributes to the class
Use Cases
Display extra attributes during the view action
public function rules() {
$rules = parent::rules();
return CMap::mergeArray($rules, array(
array('id, name', 'safe', 'on' => 'view,list'),
array('valueThatIsExpensiveToLoad', 'safe', 'on' => 'view'),
));
}
Disable email attribute update by default, but allow it on create
public function rules() {
$rules = parent::rules();
return CMap::mergeArray($rules, array(
array('id, name, email', 'safe', 'on' => 'view,list'),
array('name, email', 'safe', 'on' => 'create'),
array('name', 'safe', 'on' => 'update'),
));
}
Don't include records with status="new" in the default list view, but include them in newRecords action:
public function scopes(){
return array(
'list' => array(
'condition' => 'status!="new"'
),
'newRecords' => array(
'condition' => 'status="new"'
)
);
}
Scopes ¶
Extra search criteria are useful when the API is supposed to work only with records that
belong to the authenticated user. Default criteria are used for both single and list views.
Additional per-scenario criteria can be specified in scopes()
function.
public function defaultScope() {
// get the user that is currently authenticated
$user = $this->module->getAuthenticatedModel();
return new CDbCriteria(array(
// use "with" to enforce eager loading and speed up the api
'with' => array(
'comments',
),
// newest first
'order' => 't.update_time DESC',
// limit to results for this user only
'condition' => 't.user_id=:userId',
'params' => array(':userId' => $user->id)
));
}
Virtual Getters and Setters ¶
Some fields should be presented in a different way than they are stored within the database. See the following timestamp example:
Translated Active Record Example ¶
class Posts extends FrApiResource {
/**
* Private variable that stores time in unix timestamp format
*/
protected $_updateTime;
public function rules() {
$rules = parent::rules();
return CMap::mergeArray($rules, array(
array('updateTime', 'numerical', 'integerOnly' => true, 'on' => 'view,list,update'),
));
}
/**
* Connects our _updateTime to the active record's update_time field
*/
public function attributeMap() {
return array(
'updateTime' => 'update_time'
);
}
/**
* Getter for updateTime
*/
public function getUpdateTime(){
if ($this->scenario=="update"){
// the input is coming from user and is returned back to active record
return date('Y-m-d H:i', $this->_updateTime);
} else {
// api time is represented as unix timestamp
return $this->_updateTime;
}
}
/**
* Setter for updateTime
*/
public function setUpdateTime($value){
if (is_int($value)){
// Field is already stored as a timestamp
$this->_updateTime = $value;
} else {
// Field is a datetime string, so convert it to a timestamp
$this->_updateTime = strtotime($value);
}
}
}
Notice that there is no updateTime
attribute defined in the class. The user input value is stored into the private _updateTime
variable.
For more details see Yii documentation: http://www.yiiframework.com/wiki/167/understanding-virtual-attributes-and-get-set-methods/.
Nested Data Example ¶
If we want to display a list of all comments in a post resource. We will need two models within our api module: Posts
and Comments
.
class Posts extends FrApiResource {
protected $_comments;
/**
* Get list of all comments in api format.
*/
public function getComments(){
if ($this->_comments == null){
// load comments first
$this->_comments = array();
// use relation from post model to get comments
foreach ($this->model->comments as $model) {
// each api resource needs to be created with the module instance
$_comment = new Comments($this->module, $this->scenario);
// load the comment's model into the object
$_comment->loadFromModel($model);
// add it to our post
$this->_comments[] = $_comment;
}
}
// prepare output as simple array
$output = array();
foreach ($this->_comments as $comment){
$output[] = $comment->getApiOutput();
}
return $output;
}
/**
* Set comments
*/
public function setComments(){
if (!is_array($value)){
return;
}
$this->_comments = array();
foreach ($value as $comment) {
// each api resource needs to be created with the module instance
$_comment = new Comments($this->module, $this->scenario);
// the comment is passed in as an array in API format. If it is active record we could use $_comment->loadFromModel($comment) instead
$_comment->attributes = $comment;
$this->_comments[] = $_comment;
}
}
}
Processing User Input ¶
Use the built-in beforeValidate()
, afterValidate()
, beforeSave()
, and afterSave()
functions to modify the model before it is being saved.
Create and update process is execute in the following order:
- find or create the model
- execute afterFind()
- set attributes on the API Resource - only safe attributes will be applied
- set attributes from API Resource to Active Record
- validate the API Resource (and execute before validate)
- validate the Active Record
- save the Active Record
- change scenario to "view" and render the API Resource
There are a couple of places where additional data processing can happen:
afterFind() to prepare data after the model is loaded from the database
beforeValidate() to process data directly passed to the API Resource and set attributes to Active Record for fields that require additional validation
afterValidate() to update attributes of the Active Record that are not directly mapped using attributeMap() and do not require validation
authentication - what does the organization sample table look like?
I really like the way freshREST is organized. I can get the basic non-authenticated api calls to work, but I'm having trouble implementing the FrAuth. I'm trying to figure it out using the sample but I don't see where the Organization table is defined.
Authentication
The organization table is just an example from my setup. Companies (organizations) are the users of the api. Organization table has a column "api_secret" that is automatically created for every organization in database (32 characters). The extension doesn't create or manage the secret (password). If authentication is used, you can get organization model in any resource:
~~~
$organization = $this->module->getAuthenticatedModel();
~~~
I guess more common use case would be to create this column in user table to authenticate users rather then organizations.
Authentication
Just visited the bitbucket site documentation on that page does show the authenticated call options - thanks.
What I've created is a seperate table for api_keys like this:
CREATE TABLE IF NOT EXISTS `tbl_api_access` ( `api_id` INT NOT NULL AUTO_INCREMENT, `api_key_desc` VARCHAR(45) NOT NULL, `api_key_id` VARCHAR(45) NOT NULL, `api_key_secret` VARCHAR(45) NOT NULL, `update_dt` DATETIME NOT NULL, `responsible_user_id` INT NOT NULL,
responsible_user_id is a FK to "tbl_users". This allows a user to request one or more api keys but I can still tie them back to a requesting user for key life-cycle mgmt.
What would be really nice to add to the documentation would be a sample call w/authentication including the HTTP Headers.
Thanks again
still having trouble calling authenticate
I'm trying to authenticate through the default auth method by calling:
POST: http://localhost/{my application}/api/authenticate&key={my secret key}
I'm getting:
Home » Error Error 404 Unable to resolve the request "api/authenticate".
It looks like my routing rules are not finding authenticate in the default controller.
Here's my "config/main.php"
'modules' => array( // api module implements a RESTFul API into the sommolier system 'api' => array( 'class' => 'application.modules.api.ApiModule', // optional configuration: 'lastUpdateAttribute' => 'update_dt', // DATETIME field that contains last update time of active record 'format' => 'json', // only json is supported so far 'authModelClass' => 'FrAuthModel', // override this class to change authentication behavior 'myAuthenticatedModelClass' => 'ApiAccess', // active record that used for login 'myAuthenticatedModelPasswordField' => 'api_key_secret', ), . . . 'urlManager' => array( 'urlFormat' => 'path', 'rules' => array( // Standard rules for application '<controller:\w+>/<id:\d+>'=>'<controller>/view', '<controller:\w+>/<action:\w+>/<id:\d+>'=>'<controller>/<action>', '<controller:\w+>/<action:\w+>'=>'<controller>/<action>', // custom actions in resource controller array('api/<controller>/<action>', 'pattern' => 'api/<controller:\w+>/<action:\w+>/<id:\d+>'), // crud for resource controller array('api/<controller>/<action>', 'pattern' => 'api/<controller:\w+>/<action:\w+>'), // everything else goes to the default controller array('api/default/<action>', 'pattern' => 'api/<action:\w+>'), ),
And my "api/DefaultController.cfg"
class DefaultController extends FrApiBaseController { /** * Default action - in most cases returns please login or redirects to documentation */ public function actionIndex() { $this->renderOutput("MySommelier"); } /** * @return array action filters */ public function filters() { return array( array( 'ext.freshRest.FrAuthFilter -authenticate -index' ) ); } public function actionAuthenticate() { $data = $this->getData(); if (isset($data['secret'])) { $model = $this->module->getAuthModel(); if ($model->authenticate($data['secret'])) { // return temporary auth token and exit $this->renderOutput(array('token' => $model->token)); } // wrong password provided $this->renderError('403', 'Wrong password provided.'); } // wrong format $this->renderError('403', 'Wrong format, probably missing "secret" key.'); } }
I've set a breakpoint in actionAuthenticate and it's never getting there.
I'm sure I'm missing something obvious in the routing, but can't figure it out. Any help would be greatly appreciated.
Jim
Routing
I think I can see the problem - you have to put API routing rules before general routing rules:
~~~php
'urlManager' => array(
'urlFormat' => 'path', 'rules' => array( // custom actions in resource controller array('api/<controller>/<action>', 'pattern' => 'api/<controller:\w+>/<action:\w+>/<id:\d+>'), // crud for resource controller array('api/<controller>/<action>', 'pattern' => 'api/<controller:\w+>/<action:\w+>'), // everything else goes to the default controller array('api/default/<action>', 'pattern' => 'api/<action:\w+>'), // Standard rules for application '<controller:\w+>/<id:\d+>'=>'<controller>/view', '<controller:\w+>/<action:\w+>/<id:\d+>'=>'<controller>/<action>', '<controller:\w+>/<action:\w+>'=>'<controller>/<action>', ),
~~
Also, the authenticate function accepts HTTP POST data - there is no key in url needed. You could modify it to look for app id and app secret in your tbl_api_secret table. It will return key/token if the authentication process is successful.
Got it working
SOLVED: Turns out I needed to initialize "cache" component in config/main.php. I presume you can use any of the cache implementations, but FileCache worked just fine for my use.
'cache'=>array( 'class'=>'system.caching.CFileCache', ),
@Ondrej: Thanks for the catch on the routing order. I fixed that and added the secret param and it seems to be working until FrAuthModel attempts to set the cache: On this line:
Yii::app()->cache->set("api-auth-token-" . $this->token, $newRecord, $this->module->authCacheDuration);
I'm getting this error:
Fatal error: Call to a member function set() on a non-object in C:\wamp\www\sommelier\protected\extensions\freshRest\FrAuthModel.php on line 204
In debug, I can see that token, newRecord and authCache have values. It's almost like the App()->cache is not instantiated.
Jim
is it possible to please make a sample yii installation with a workable example?
Hi there, I am hopeful about this extension, but its really hard to get started when there is no example.
Can you please provide one with all applicable models? Ie: Organization table etc
I created a model for the api
In step one, you say: "Create a password field in a table that represents a client (i.e. user or organization). In the module configuration add the following attributes"
Is this correct?
CREATE TABLE
api
(id
int(11) NOT NULL,oranization
varchar(255) DEFAULT NULL,api_secret
varchar(255) DEFAULT NULL,PRIMARY KEY (
id
)) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Trouble understanding how the authentication works - can you please provide an example?
Hi there, I', sorry, but I don't understand how the authentication works, could you please provide an example?
authentication - in which model does it store the IP and Token?
In the "Authentication Process" you mention that
"FrAuthModel creates a new record with the authentication token and IP address".
But where and in which model?
fr_api_device table
I noticed that your extension creates a table called fr_api_device,
are there any other tables it creates that the developer should know about?
Hi Onderj
I am chugging along with your extension. I had to hack it abit to get it to work with PUT and POST requests... Seams that the token doesnt get validated properly unless its a GET request sent.
I am wondering, is there a way we can specify that no authentication token is needed for specific actions? Ie: I am using Angular.js as a front end, and I want to do anyonymous json requests to get data, how can I do this without a token?
@Fire: Setting Auth required as filter
@Fire, I'm not seeing Ondrej reply so here's mine:
I'd be very interested in knowing what you had to tweak with PUT/POST. I've only got a read-only API so far, but will be extending it shortly.
I believe the fr_api_device table is the only one created. I think it's supposed to create automatically first time it's used, but went ahead and created it manually by replicating the code from the extension. It seems to work fine.
In this example, I've disabled the auth filter on my apiVersion method. I have a separate controller for each resource so I also have the ability to apply auth filters to each controller-method.
modules/api/controllers/MyController.php
public function filters() { return array( // only list and view actions are allowed 'disabled +update,create,delete', array( // authenticate except simple "version" action 'ext.freshRest.FrAuthFilter -apiVersion' ) ); }
tweaking
@Jim K,
Hi Jim, I ended up not having to tweak anything. There was a bug in Ondrej's code which I reported as an issue (issue #1, #2) on the bitbucket site that prevented the token from authenticating on PUT / POST requests - therefore create and update were not working... but he aptly fixed them the next day. I advise a re-pull of the git code so you can get the latest fixed.
After I applied the fix, GET, PUT, POST and DELETE all work for me.
Items Model
Hi everyone,
The easiest way to get started with Freshrest is to look at the Items Controller, and the Items Model that comes as an example with FreshRest.
It is important to understand (as mentioned in the docs) that freshrest models are supposed to be named in plural - ie: "items" and your models of your yii app should be singular ... therefore the freshrest "Items" model, represents the "Item" model of your yii app.
I don't think Ondrej included the mysql for the actual Item Model, so I'll paste it below.
Anyhow, I hope from the sql below, and from his example of Items model and ItemController you can get started
SET FOREIGN_KEY_CHECKS=0;-- ------------------------------ Table structure for item-- ----------------------------DROP TABLE IF EXISTS
item
;CREATE TABLEitem
(id
int(11) NOT NULL,item_type
varchar(255) DEFAULT NULL,item_name
varchar(255) DEFAULT NULL,update_time
datetime DEFAULT NULL,uid
int(11) DEFAULT NULL, PRIMARY KEY (id
)) ENGINE=InnoDB DEFAULT CHARSET=latin1;Problem in Loading ActiveRecord [solved]
I have ActiveRecord models defined in protected/models/ folder, But freshRest models (of FrApiResource type) in modules/api1/models folder just can't load it's corresponding ActiveRecord Class, which I have defined it in activeRecordClassName() function.
Anyone can help on this?
public function findAll() { $output = array(); if ($this->activeRecordClassName() == null) { return $output; } $className = $this->activeRecordClassName(); $lookupCriteria = $this->getLookupCriteria(); $models = $className::model()->findAll($lookupCriteria); <-----
Sorry, found my problem.
I extends the model from ActiveRecord instead of CActiveRecord. It works fine after I fixed it.
Active record
Hi,
to load corresponding ActiveRecord you have to override activeRecordClassName function:
public function activeRecordClassName(){
return "Address";
}
Also, try to print out $lookupCriteria. The problem might be composite primary key or default scope specified in the model.
@seletar6
@seletar6
Here is what I do in a model called fles.php in /modules/api1/models/Fles.php
...
public function activeRecordClassName() { return 'Fle'; //this tells yii that that associated ActiveRecord is called //"Fle" }
..
Not able to insert new Item by POST [solved]
Hi, I temporarily disabled the authentication for insert and update. But when I tried to do a POST. Following is the error I got. Any idea why?
URL: http://localhost/MySite/index.php/api1/items
method: POST
request body:
{"type": "D","name": "name D"} or {"item_type": "D","item_name": "name D"}
Response I got:
{
"data" : {
"message" : "Array ( [item_type] => Array ( [0] => Item Type cannot be blank. ) [item_name] => Array ( [0] => Item Name cannot be blank. ) )"
},
"code" : 400,
"timestamp" : 1399275935
}
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Update:
Thanks to Ondrej's answer. Adding of the rule solves the problem.
RE: Not able to insert new Item by POST
It seems the problem is a missing validation rule. Make sure there are "type" and "name" in rules function:
~~~
public function rules() {
$rules = parent::rules(); return CMap::mergeArray($rules, array( array('id, name, type', 'safe', 'on' => 'view, list'), array('name, type', 'safe', 'on' => 'create, update, view, list'), ));
}
~~~
also modify attributeMap function to match model attributes:
~~~
public function attributeMap() {
return array( 'id' => 'id', 'name' => 'item_name', 'type' => 'item_type', );
}
~~~
Enable authentication filter on some actions but not on others
I have a question about filters. Is it possible to enable authentication for some actions and not others?
Enable authentication filter on some actions but not on others
It is pretty easy to enable authentication just for certain actions. Just list the actions you want to authenticate using '+' for include or '-' exclude
`
public function filters() {
return array( array( 'ext.freshRest.FrAuthFilter -index,view' ) ); }
How to specify the Method for a new Action?
For a new action in a controller, how do I specify the Method (either GET, POST, PUT, DELETE etc) for this action?
how to use FreshRest Extension
Hi all,
Im trying to use this extension since a long time.. as given zip file item controller not working.
please guide me how to pass token and ipaddress and all in url.
im testing by this...
http://localhost/freshrest/app/items/SayHiToMe?token=sdfjkjk&ipAddress=111
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.