restfullyii Adds RESTFul API to your Yii application

Starship / RestfullYii

  1. How it works
  2. Requirements
  3. Additional Documentation Can be found Here: Git Repo
  4. Installation
  5. Controller Setup
  6. Making Requests
  7. Customization & Configuration
  8. Defining Custom Routes
  9. Testing
  10. Contributors
  11. Resources
  12. License

Makes quickly adding a RESTFul API to your Yii project easy. RestfullYii provides full HTTP verb support (GET, PUT, POST, DELETE) for your resources, as well as the ability to offset, limit, sort, filter, etc… . You will also have the ability to read and manipulate related data with ease.

RestfullYii has been lovingly rebuilt from the metal and is now 100% test covered! The new event based architecture allows for clean and unlimited customization.

How it works

RestfullYii adds a new set of RESTFul routes to your standard routes, but prepends '/api' .

So if you apply RestfullYii to the 'WorkController' you will get the following new routes by default.

[GET] http://yoursite.com/api/work (returns all works)
[GET] http://yoursite.com/api/work/1 (returns work with PK=1)
[POST] http://yoursite.com/api/work (create new work)
[PUT] http://yoursite.com/api/work/1 (update work with PK=1)
[DELETE] http://yoursite.com/api/work/1 (delete work with PK=1)

Requirements

  • PHP 5.4.0 (or later)*
  • YiiFramework 1.1.14 (or later)
  • PHPUnit 3.7 (or later) to run tests.

For older versions of PHP (< 5.4) checkout v1.15

Additional Documentation Can be found Here: Git Repo

Installation

  1. Download and place the 'starship' directory in your Yii extension directory.

  2. In config/main.php you will need to add the RestfullYii alias. This allows for flexability in where you place the extension.

'aliases' => array(
		.. .
        'RestfullYii' =>realpath(__DIR__ . '/../extensions/starship/RestfullYii'),
        .. .
	),

`3. Include ext.starship.RestfullYii.config.routes in your main config (see below) or copy the routes and paste them in your components->urlManager->rules in same config.

'components'=>array(
		'urlManager'=>array(
			'urlFormat'=>'path',
			'rules'=>require(
				dirname(__FILE__).'/../extensions/starship/restfullyii/config/routes.php'
			),
		),
)

Controller Setup

Adding a set of RESTFul actions to a controller.

  1. Add the ERestFilter to your controllers filter method.
public function filters()
{
		return array(
			'accessControl', // perform access control for CRUD operations
			array(
				'ext.starship.RestfullYii.filters.ERestFilter + 
			 	REST.GET, REST.PUT, REST.POST, REST.DELETE'
			),
		);
}
  1. Add the ERestActionProvider to your controllers actions method.
public function actions()
{
		return array(
			'REST.'=>'ext.starship.RestfullYii.actions.ERestActionProvider',
		);
}	
  1. If you are using the accessControl filter you need to make sure that access is allowed on all RESTFul routes.
public function accessRules()
{
		return array(
			array('allow', 'actions'=>array('REST.GET', 'REST.PUT', 'REST.POST', 'REST.DELETE'),
			'users'=>array('*'),
			),
			array('deny',  // deny all users
				'users'=>array('*'),
			),
		);
}

Making Requests

To understand how to make RestfullYii API requests its best to look at a few examples. Code examples will be shown first in JavaScript as an AJAX user and then using CURL.

* JS examples use jQuery

* Default validation for an AJAX user is !Yii::app()->user->isGuest so the user must be logged in for this type of request.

GET Requests
Getting a list or resources (WorkController)

JavaScript: ~~~ [javascript] $.ajax({

url:'/api/work',
type:"GET",
success:function(data) {
  console.log(data);
},
error:function (xhr, ajaxOptions, thrownError){
  console.log(xhr.responseText);
} 

}); ~~~

CURL: ~~~ [shell] curl -i -H "Accept: application/json" -H "X_REST_USERNAME: admin@restuser" -H "X_REST_PASSWORD: admin@Access"\ http://my-site.com/api/work ~~~

Response: ~~~ [javascript] {

"success":true,
"message":"Record(s) Found",
"data":{
	"totalCount":"30",
	"work":[
		{
			"id": "1",
            "title": "title1",
			"author_id": "1",
            "content": "content1",
            "create_time": "2013-08-07 10:09:41"
		},
		{
			"id": "2",
            "title": "title2",
			"author_id": "2",
            "content": "content2",
            "create_time": "2013-08-08 11:01:11"
		},
		. . .,
	]
}

} ~~~

Getting a single resource (WorkController)

JavaScript: ~~~ [javascript] $.ajax({

url:'/api/work/1',
type:"GET",
success:function(data) {
  console.log(data);
},
error:function (xhr, ajaxOptions, thrownError){
  console.log(xhr.responseText);
} 

}); ~~~

CURL: ~~~ [shell] curl -i -H "Accept: application/json" -H "X_REST_USERNAME: admin@restuser" -H "X_REST_PASSWORD: admin@Access"\ http://my-site.com/api/work/1 ~~~

Response: ~~~ [javascript] {

"success":true,
"message":"Record Found",
"data":{
	"totalCount":"1",
	"work":[
		{
			"id": "1",
            "title": "title1",
			"author_id": "1",
            "content": "content1",
            "create_time": "2013-08-07 10:09:41"
		}
	]
}

} ~~~

GET Request: Limit & Offset (WorkController)

You can limit and paginate through your results by adding the limit and offset variables to the request query string.

JavaScript: ~~~ [javascript] $.ajax({

url:'/api/work?limit=10&offset=30',
type:"GET",
success:function(data) {
  console.log(data);
},
error:function (xhr, ajaxOptions, thrownError){
  console.log(xhr.responseText);
} 

}); ~~~

CURL: ~~~ [shell] curl -i -H "Accept: application/json" -H "X_REST_USERNAME: admin@restuser" -H "X_REST_PASSWORD: admin@Access"\ http://my-site.com/api/work?limit=10&offset=30 ~~~

Response: ~~~ [javascript] {

"success":true,
"message":"Record(s) Found",
"data":{
	"totalCount":"30",
	"work":[
		{
			"id": "11",
            "title": "title11",
			"author_id": "11",
            "content": "content11",
            "create_time": "2013-08-11 11:10:09"
		},
		{
			"id": "12",
            "title": "title12",
			"author_id": "12",
            "content": "content12",
            "create_time": "2013-08-08 12:11:10"
		},
		. . .,
	]
}

}


### GET Request: Sorting results (WorkController)
You can sort your results by any valid param or multiple params as well as provide a sort direction (ASC or DESC). sort=[{"property":"title", "direction":"DESC"}, {"property":"create_time", "direction":"ASC"}]


JavaScript:

[javascript] $.ajax({

url:'/api/work?sort=[{"property":"title", "direction":"DESC"}, {"property":"create_time", "direction":"ASC"}]',
type:"GET",
success:function(data) {
  console.log(data);
},
error:function (xhr, ajaxOptions, thrownError){
  console.log(xhr.responseText);
} 

}); ~~~

CURL: ~~~ [shell] curl -i -H "Accept: application/json" -H "X_REST_USERNAME: admin@restuser" -H "X_REST_PASSWORD: admin@Access"\ http://my-site.com/api/work?sort=[{"property":"title", "direction":"DESC"}, {"property":"create_time", "direction":"ASC"}] ~~~

Response: ~~~ [javascript] {

"success":true,
"message":"Record(s) Found",
"data":{
	"totalCount":"30",
	"work":[
		{
			"id": "29",
            "title": "title30b",
			"author_id": "29",
            "content": "content30b",
            "create_time": "2013-08-07 14:05:01"
		},
		{
			"id": "30",
            "title": "title30",
			"author_id": "30",
            "content": "content30",
            "create_time": "2013-08-08 09:10:09"
		},
		{
			"id": "28",
            "title": "title28",
			"author_id": "28",
            "content": "content28",
            "create_time": "2013-08-09 14:05:01"
		},
		. . .,
	]
}

}


### GET Request: Filtering results (WorkController)
You can filter your results by any valid param or multiple params as well as an operator.

Available filter operators:
* in
* not in
* =
* !=
* >
* >=
* <
* <=
* No operator is "LIKE"

/api/post/?filter = [ {"property": "id", "value" : 50, "operator": ">="} , {"property": "user_id", "value" : [1, 5, 10, 14], "operator": "in"} , {"property": "state", "value" : ["save", "deleted"], "operator": "not in"} , {"property": "date", "value" : "2013-01-01", "operator": ">="} , {"property": "date", "value" : "2013-01-31", "operator": "<="} , {"property": "type", "value" : 2, "operator": "!="} ] ~~~

POST Requests (Creating new resources)

With POST requests we must include the resource data as a JSON object in the request body.

JavaScript: ~~~ [javascript] var postData = {

"title": "title31",
"author_id": "31",
"content": "content31",
"create_time": "2013-08-20 09:23:14"

};

$.ajax({

url:'/api/work',
data:JSON.stringify(postData)
type:"POST",
success:function(data) {
	console.log(data);
},
error:function (xhr, ajaxOptions, thrownError){
	console.log(xhr.responseText);
} 

}); ~~~

CURL: ~~~ [shell] curl -l -H "Accept: application/json" -H "X_REST_USERNAME: admin@restuser" -H "X_REST_PASSWORD: admin@Access"\ -X POST -d '{"title": "title31", "author_id": "31", "content": "content31", "create_time": "2013-08-20 09:23:14"}' http://my-site.com/api/work ~~~

Response: ~~~ [javascript] {

"success":true,
"message":"Record Created",
"data":{
	"totalCount":"1",
	"work":[
		{
			"id": "31",
            "title": "title31",
			"author_id": "31",
            "content": "content31",
            "create_time": "2013-08-20 09:23:14"
		}
	]
}

} ~~~

PUT Requests (Updating existing resources)

With PUT requests like POST requests we must include the resource data as a JSON object in the request body.

JavaScript: ~~~ [javascript] var postData = {

"id": "31",
"title": "title31",
"author_id": "31",
"content": "content31",
"create_time": "2013-08-20 09:23:14"

};

$.ajax({

url:'/api/work/31',
data:JSON.stringify(postData)
type:"PUT",
success:function(data) {
	console.log(data);
},
error:function (xhr, ajaxOptions, thrownError){
	console.log(xhr.responseText);
} 

}); ~~~

CURL: ~~~ [shell] curl -l -H "Accept: application/json" -H "X_REST_USERNAME: admin@restuser" -H "X_REST_PASSWORD: admin@Access"\ -X PUT -d '{"id": "31", "title": "title31", "author_id": "31", "content": "content31", "create_time": "2013-08-20 09:23:14"}' http://my-site.com/api/work/31 ~~~

Response: ~~~ [javascript] {

"success":true,
"message":"Record Updated",
"data":{
	"totalCount":"1",
	"work":[
		{
			"id": "31",
            "title": "title31",
			"author_id": "31",
            "content": "content31",
            "create_time": "2013-08-20 09:23:14"
		}
	]
}

} ~~~

DELETE Requests (Delete a resource)

JavaScript: ~~~ [javascript] $.ajax({

url:'/api/work/1',
type:"DELETE",
success:function(data) {
  console.log(data);
},
error:function (xhr, ajaxOptions, thrownError){
  console.log(xhr.responseText);
} 

}); ~~~

CURL: ~~~ [shell] curl -l -H "Accept: application/json" -H "X_REST_USERNAME: admin@restuser" -H "X_REST_PASSWORD: admin@Access"\ "X-HTTP-Method-Override: DELETE" -X DELETE http://my-site.com/api/work/1


Response:

[javascript] {

"success":true,
"message":"Record Deleted",
"data":{
	"totalCount":"1",
	"work":[
		{
			"id": "1",
            "title": "title1",
			"author_id": "1",
            "content": "content1",
            "create_time": "2013-08-07 10:09:41"
		}
	]
}

} ~~~

Sub-Resources

When working with 'many to many' relations you now have the ability to treat them as sub-resources.

Consider:
~~~ URL Format: http://mysite.com/api/<controller>/<id>/<many_many_relation>/<many_many_relation_id>

Getting player 3 who is on team 1
or simply checking whether player 3 is on that team (200 vs. 404)
GET /api/team/1/players/3

getting player 3 who is also on team 3
GET /api/team/3/players/3

Adding player 3 also to team 2
PUT /api/team/2/players/3

Getting all teams of player 3
GET /api/player/3/teams

Remove player 3 from team 1 (Injury) DELETE /api/team/1/players/3

Team 1 found a replacement, who is not registered in league yet
POST /api/player

From payload you get back the id, now place it officially to team 1
PUT /api/team/1/players/44
~~~

Customization & Configuration

RestfullYii's default behaviors can be easily customized though the built-in event system. Almost all aspects of RestFullYii's request / response handling trigger events. Changing RestfullYii's behaviors is as simple as registering the appropriate event handlers. Event handlers can be registered both globally (in the main config) and locally (at the controller level).

To understand how to do this, lets create a scenario that requires some customization and see how we might accomplish it.

Lets say we have two controllers in our API, WorkController and CategoryController. We would like our API to function in the following ways:

  1. The API should be accessible to JavaScript via AJAX.
  2. The API should not be accessible to any external client.
  3. Only registered users should be allowed to view Work and Category resources.
  4. Only users with the permission REST-UPDATE should be allowed to update works.
  5. Only users with the permission REST-CREATE should be allowed to create works.
  6. Only users with the permission REST-DELETE should be allowed to delete works.
  7. Create, update and delete on categories should be disallowed for all API users.

Now that we know how we would like our API to function, lets take a look at the list above and determine which features can be implemented globally. Since 1, 2 and 3 effect both of our controllers, we can effect these globally by registering a few callbacks in our config/main.php.

To accomplish 1 & 3 we don't have to do anything as this is RestfullYii's default behavior, so that leaves 2. By default RestfullYii does allow access to external clients which is not what we want. Lets change it!

In the /protected/config/main.php params section we will add the following:

$config = array(
	..,
	'params'=>[
		'RestfullYii' => [
			'req.auth.user'=>function($application_id, $username, $password) {
				return false;
			},
		]
	]
);

This tells RestfullYii that when the event 'req.auth.user' is handled it should always return false. Returning false will deny access (true grants it). Similarly validating an AJAX user has it's own event 'req.auth.ajax.user' which (as mentioned earlier) allows access to registered users by default.

That takes care of our global config and we can now focus on features 4-7 while taking care not to break feature 3. Since features 4-6 involve the work controller and user permissions, we can accomplish all of those at the same time. Remember RestfullYii's default behavior is to allow all registered users complete API access and again this is not what we want. Lets change it!

We will now register an event handler locally in the WorkController; To do this we will need to add a special public method in the WorkController called 'restEvents'. Then once we have the 'restEvents' method we can use one other special method 'onRest' to register our event handler. We can call 'onRest' using '$this->onRest()'. 'onRest' takes two params the first is the event name and the second is a Callable that will actually handle the event. This Callable is bound to the controller so the $this context inside the Callable always refers to the current controller. This is true for event handlers registered both globally as well as locally.

Now we are ready modify the output of event handler 'req.auth.ajax.user', but this time instead of overriding the default behavior, we will use the post-filter feature to add our additional user validation. The event name of a post-filter event is always the main event name prefixed with 'post.filter.', thus in our case the event name is 'post.filter.req.auth.ajax.user'. The param(s) passed into a post-filter handler are always the value(s) returned from the main event. Take a look:

class WorkController extends Controller
{
	.. .
	
	public function restEvents()
	{
		$this->onRest('post.filter.req.auth.ajax.user', function($validation) {
			if(!$validation) {
				return false;
			}
			switch ($this->getAction()->getId()) {
				case 'REST.POST':
					return Yii::app()->user->checkAccess('REST-CREATE');
					break;
				case 'REST.POST':
					return Yii::app()->user->checkAccess('REST-UPDATE');
					break;
				case 'REST.DELETE':
					return Yii::app()->user->checkAccess('REST-DELETE');
					break;
				default:
					return false;
					break;
			}
		});
	}
	
	.. .
}

Cool! That just leaves feature 7, disallowing create, update, delete on category. Again we will add this change locally, but this time to the CategoryController. Take a look:

class CategoryController extends Controller
{
	.. .
	
	public function restEvents()
	{
		$this->onRest('post.filter.req.auth.ajax.user', function($validation) {
			if(!$validation) {
				return false;
			}
			return ($this->getAction()->getId() == 'REST.GET');
		});
	}
	
	.. .
}

We now have all features implemented!

Defining Custom Routes

Custom routes are very simple to define as all you really need to do is create an event handler for your route and http verb combination (event name = 'req.

Here is the list of routes we would like to add to our api:

  1. [GET] /api/category/active

  2. [GET] /api/work/special/

  3. [PUT] /api/work/special/

  4. [POST] /api/work/special/

  5. [DELETE] /api/work/special/

Custom Route 1

As you tell from the route the request will be handled by the Category controller. So that is where we will add our event handler.

class CategoryController extends Controller
{
	.. .
	public function restEvents()
    {
    	$this->onRest('req.get.active.render', function() {
    		//Custom logic for this route.
    		//Should output results.
    		$this->emitRest('req.render.json', [
    			[
    				'type'=>'raw',
    				'data'=>['active'=>true]
    			]
    		])
		});
	}
}
Custom Routes 2-5

These routes all involve the Work controller. So that is where we will add our event handlers.

class WorkController extends Controller
{
	.. .
	
	public function restEvents()
	{
		$this->onRest('req.get.special.render', function($param1) {
			echo CJSON::encode(['param1'=>$param1]);
		});
		
		$this->onRest('req.put.special.render', function($data, $param1, $param2) {
			//$data is the data sent in the PUT
			echo CJSON::encode(['data'=>$data, $param1, $param2]);
		});
		
		$this->onRest('req.post.special.render', function($data, $param1) {
			//$data is the data sent in the POST
			echo CJSON::encode(['data'=>$data, 'param1'=>$param1, 'param2'=>$param2]);
		});
		
		$this->onRest('req.delete.special.render', function($param1, $param2) {
			echo CJSON::encode(['param1'=>$param1, 'param2'=>$param2]);
		});
	}

Testing

Running the project's automated tests.

Unit Tests
  1. Make sure you that you have the correct database and database user in the test config (/WEB_ROOT/protected/extensions/starship/RestfullYii/tests/testConfig.php).
  2. % cd /WEB_ROOT/protected/extensions/starship/RestfullYii/tests
  3. % phpunit unit/

Contributors

Resources

License

Starship / RestfullYii is released under the WTFPL license - http://sam.zoy.org/wtfpl/. This means that you can literally do whatever you want with this extension.

25 5
78 followers
8 322 downloads
Yii Version: 1.1
License: BSD-2-Clause
Category: Web Service
Developed by: evan108108
Created on: Mar 19, 2012
Last updated: 7 years ago

Downloads

show all

Related Extensions