Skip to content

evan108108/RESTFullYii

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 

Repository files navigation

Starship / RestfullYii

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 or cccssw's amazing 5.3 port: https://github.com/cccssw/RESTFullYii

Installation

Installing Manually

  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'),
        .. .
	),
  1. 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'
			),
		),
	)

Installing With Composer

{
    "require": {
        "starship/restfullyii": "dev-master"
    }
}
  1. In config/main.php you will need to add the RestfullYii alias. This allows for flexability in where you place the extension.
	'aliases' => array(
		.. .
		//Path to your Composer vendor dir plus starship/restfullyii path
		'RestfullYii' =>realpath(__DIR__ . '/../../../vendor/starship/restfullyii/starship/RestfullYii'),
        .. .
	),
  1. 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__).'/../../../vendor/starship/restfullyii/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(
				'RestfullYii.filters.ERestFilter + 
			 	REST.GET, REST.PUT, REST.POST, REST.DELETE, REST.OPTIONS'
			),
		);
}
  1. Add the ERestActionProvider to your controllers actions method.
public function actions()
{
		return array(
			'REST.'=>'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', 'REST.OPTIONS'),
			'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:

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

CURL:

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:

{
	"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:

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

CURL:

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:

{
	"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:

 $.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:

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:

{
	"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:

 $.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:

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=%5B%7B%22property%22%3A%22title%22%2C+%22direction%22%3A%22DESC%22%7D%2C+%7B%22property%22%3A%22create_time%22%2C+%22direction%22%3A%22ASC%22%7D%5D

Response:

{
	"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:

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:

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:

{
	"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:

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:

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:

{
	"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:

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

CURL:

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:

{
	"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.PUT':
					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.<verb>.<route_name>.render'). Lets take a look at some examples.

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

  1. [GET] /api/category/active

  2. [GET] /api/work/special/<param1>

  3. [PUT] /api/work/special/<param1>/<param2>

  4. [POST] /api/work/special/<param1>

  5. [DELETE] /api/work/special/<param1>/<param2>

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]);
		});
	}

##Overriding & Adding Default Model attributes We can change the attributes outputted by our model(s) in any given request. We can do this using the "model.YOUR_MODEL_NAME_HERE.override.attributes" event. Lets say we have a model named "Work" and we would like to add the non-AR property "url" to our JSON output every time that model is referenced.

$this->onRest('model.work.override.attributes', function($model) {
   return array_merge($model->attributes, ['url'=>"http://www.mysite.com/media/{$model->id}.jpg"]);
});
  • You could also use this same event to remove or override default AR attributes

##Post Filtering Render Events It is possible to post filter the output of the "req.[get,put,post,delete].[resource, resources].render" events. This will allow you to completely change the output as you see fit.

$this->onRest('post.filter.req.get.resources.render', function($json) {
   $j = CJSON::decode($json);
   $j['data'] = array_map(function($work_data){
      $work = Work::model();
      $work->setAttributes($work_data);
      $work_data['url'] = $work->url;
      return $work_data;
   }, $j['data'])
   return $j;
});

##CORS Requests (Cross-Origin Resource Sharing)

Making cross origin requests from Javascript is now possible with RESTFullYii! RESTFullYii has several CORS specific events that help making CORS requests easy.

Lets suppose you have the following setup; An API server with the domain http://rest-api-server.back and a client application at the domain http://my-js-client-app.front. Obviously you would like to make requests from your front end client application (my-js-client-app.front) to your backend API server (rest-api-server.back).

#####1) The first thing you will need to do is let RESTFullYii know that my-js-client-app.front is allowed to make requests to a given resource or resources. Lets say we want to allow our app to make requests to the 'Artist' resource only. So lets add an event hook in our ArtistController

class ArtistController extends Controller
{
	[...]
	
	public function restEvents()
	{
		$this->onRest('req.cors.access.control.allow.origin', function() {
			return ['http://my-js-client-app.front']; //List of sites allowed to make CORS requests 
		});
	}
}

If you would like to allow my-js-client-app.front to access other resource you simply repeat this process in the appropriate controller(s) or apply it globally in your main config params (see above examples).

#####2) We need to determine is the types of requests we would like to allow. By default RESTFullYii will allow GET & POST but for our application we would like PUT & DELETE as well.

class ArtistController extends Controller
{
	[...]
	
	public function restEvents()
	{
		$this->onRest('req.cors.access.control.allow.origin', function() {
			return ['http://my-js-client-app.front']; //List of sites allowed to make CORS requests 
		});
		
		$this->onRest('req.cors.access.control.allow.methods', function() {
			return ['GET', 'POST', 'PUT', 'DELETE']; //List of allowed http methods (verbs) 
		});
	}
}

This is the minimum configuration but there are more available; See (req.cors.access.control.allow.headers, req.cors.access.control.max.age, req.auth.cors)

#####3) Now that our server is set up to allow CORS requests we are ready to send requests from our client @http://my-js-client-app.front

$.ajax({
	type: "GET",
	url: "http://rest-api-server.back/api/artist",
	headers: {
		'X_REST_CORS': 'Yes',
	},
	success: function( response ){
		console.log(response);
	},
	error: function( error ){
		console.log( "ERROR:", error );
	}
});

######*Notice the headers object in the jQuery request above. It is required that you send X_REST_CORS: 'Yes'

####That's it! You are making sweet CORS!

Events

List of all events and their default event handlers.

Event Pre-Filter Post-Filter Description
Configuration Events
config.application.id Yes Yes Returns the app id that is applied to header vars (username, password)
config.dev.flag Yes Yes Return true to set develop mode; False to turn of develop mode
Request Events
req.event.logger No No Logs events
req.disable.cweblogroute Yes Yes Disable CWebLogRoute (True by default)
req.auth.https.only Yes Yes Return true to restrict to https; false to allow http or https
req.auth.username Yes Yes This is the username used to grant access to non-ajax users. At a minimum you should change this value
req.auth.password Yes Yes This is the password use to grant access to non-ajax users. At a minimum you should change this value
req.auth.user Yes Yes Used to validate a non-ajax user; return true to allow; false to deny
req.auth.ajax.user Yes Yes Used to validate a an ajax user; return true to allow; false to deny
req.auth.type Yes Yes returns the authorization type (1=CORS, 2=USER_PASS, 3=AJAX)
req.cors.access.control.allow.origin Yes Yes returns the allowed remote origins durring a CORS request
req.cors.access.control.allow.methods Yes Yes returns the allowed http methods/verbs for a CORS request
req.cors.access.control.allow.headers Yes Yes returns the allowed headers for a CORS request
req.cors.access.control.max.age Yes Yes Used in a CORS request to indicate how long the response can be cached (seconds)
req.auth.cors Yes Yes returns the authorization true if the CORS request is authorized and false if not
req.auth.uri Yes Yes grant / deny access based on the URI and or HTTP verb
req.after.action Yes Yes Called after the request has been fulfilled. By default it has no behavior
req.param.is.pk Yes Yes Called when attempting to validate a resources primary key. The default is an integer. Return true to confirm Primary Key; False to deny primary key.
req.is.subresource Yes Yes Called when trying to determain if the request is for a subresource
req.data.read Yes Yes Called when reading data on POST & PUT requests
req.get.resource.render Yes Yes Called when a GET request for a single resource is to be rendered
req.get.resources.render Yes Yes Called when a GET request for when a list resources is to be rendered
req.put.resource.render Yes Yes Called when a PUT request for a single resource is to be rendered
req.post.resource.render Yes Yes Called when a POST request is to be rendered
req.delete.resource.render Yes Yes Called when a DELETE request is to be rendered
req.get.subresource.render Yes Yes Called when a GET request for a single sub-resource is to be rendered
req.get.subresources.render Yes Yes Called when a GET request for a list of sub-resources is to be rendered
req.put.subresource.render Yes Yes Called when a PUT request for a single sub-resource is to be rendered
req.delete.subresource.render Yes Yes Called when a DELETE request on a sub-resource is to be rendered
req.render.json Yes No NOT CALLED INTERNALLY. The event exists to allow users the ability to easily render arbitrary JSON.
req.exception Yes No Error handler called when an Exception is thrown
Model Events
model.instance Yes Yes Called when an instance of the model representing the resource(s) is requested. By default this is your controller class name minus the 'Controller'
model.attach.behaviors Yes Yes Attaches helper behaviors to model. Called on all requests (Other then custom) to add some magic to your models.
model.with.relations Yes Yes Called when trying to determine which relations to include in a requests render. The default is all relations not starting with an underscore. You should most likely customize this per resource controller as some resources may have relations that return large number of records
model.lazy.load.relations Yes Yes Called when determining if relations should be lazy or eager loaded. The default is to lazy load. In most cases this is sufficient. Under certain conditions eager loading my be more appropriate
model.limit Yes Yes Called when applying a limit to the resources returned in a GET request. The default is 100 or the value of the _GET param 'limit'
model.offset Yes Yes Called when applying an offset to the records returned in a GET request. The default is 0 or the value of the _GET param 'offset'
model.scenario Yes Yes Called before a resource(s) is found. This is the scenario to apply to a resource pre-find. The default is 'restfullyii' or the value of the _GET param 'scenario'. At this point setting the scenario does very little, but is included here so that you may create custom functionality with little modification.
model.filter Yes Yes Called when attempting to apply a filter to apply to the records in a GET request. The default is 'NULL' or the value of the _GET param 'filter'. The format is JSON: '[{"property":"SOME_PROPERTY", "value":"SOME_VALUE", "operator": =""}]'
model.sort Yes Yes Called when attempting to sort records returned in a GET request. The default is 'NULL' or the value of the _GET param 'sort'. Rhe format is JSON:[{"property":"SOME_PROPERTY", "direction":"DESC"}]
model.find Yes Yes Called when attempting to find a single model
model.find.all Yes Yes Called when attempting to find a list of models
model.count Yes Yes Called when the count of model(s) is needed
model.subresource.find Yes Yes Called when attempting to find a subresource
model.subresource.find.all Yes Yes Called when attempting to find all subresources of a resource
model.subresource.count Yes Yes Called when the count of sub-resources is needed
model.apply.post.data Yes Yes Called on POST requests when attempting to apply posted data
model.apply.put.data Yes Yes Called on PUT requests when attempting to apply PUT data
model.save Yes Yes Called whenever a model resource is saved
model.subresources.save Yes Yes Called whenever a sub-resource is saved
model.delete Yes Yes Called whenever a model resource needs deleting
model.subresource.delete Yes Yes Called whenever a subresource needs deleting
model.restricted.properties Yes Yes Called when determining which properties if any should be considered restricted. The default is [] (no restricted properties)
model.visible.properties Yes Yes Called when determining which properties if any should be visible. The default is [] (no hidden properties)
model.hidden.properties Yes Yes Called when determining which properties if any should be hidden. The default is [] (no hidden properties)

###config.application.id

/**
 * config.application.id
 *
 * returns the app id that is applied to header vars (username, password)
 *
 * @return (String) default is 'REST'
 */
$this->onRest('config.application.id', function() {
	return 'REST';
});

####pre.filter.config.application.id

$this->onRest('pre.filter.config.application.id', function() {
	//no return
});

####post.filter.config.application.id

$this->onRest('post.filter.config.application.id', function($app_id) {
	return $app_id; //String
});

###config.dev.flag

/**
 * config.dev.flag
 *
 * return true to set develop mode; false to turn of develop mode
 *
 * @return (bool) true by default
 */
$this->onRest('config.dev.flag', function() {
	return true;
});

####pre.filter.config.dev.flag

$this->onRest('pre.filter.config.dev.flag', function() {
	//no return
});

####post.filter.config.dev.flag

$this->onRest('post.filter.config.dev.flag', function($flag) {
	return $flag; //Bool
});

###req.event.logger

/**
* req.event.logger
*
* @param (String) (event) the event to log
* @param (String) (category) the log category
* @param (Array) (ignore) Events to ignore logging
*
* @return (Array) the params sent into the event
*/
$this->onRest('req.event.logger', function($event, $category='application',$ignore=[]) {
	if(!isset($ignore[$event])) {
		Yii::trace($event, $category);
	}
	return [$event, $category, $ignore];
});

###req.disable.cweblogroute

/**
 * req.disable.cweblogroute
 *
 * this is only relevant if you have enabled CWebLogRoute in your main config
 *
 * @return (Bool) true (default) to disable CWebLogRoute, false to allow
 */ 
$this->onRest('req.disable.cweblogroute', function(){
	return true;
});

####pre.filter.req.disable.cweblogroute

$this->onRest('pre.filter.req.disable.cweblogroute', function() {
	//no return
});

####post.filter.req.disable.cweblogroute

$this->onRest('post.filter.req.disable.cweblogroute', function($disable) {
	return $disable; //Bool
});

###req.auth.https.only

/**
 * req.auth.https.only
 *
 * return true to restrict to https;
 * false to allow http or https
 *
 * @return (bool) default is false
 */ 
$this->onRest('req.auth.https.only', function() {
	return false;
});

####pre.filter.req.auth.https.only

$this->onRest('pre.filter.req.auth.https.only', function() {
	//no return
});

####post.filter.req.auth.https.only

$this->onRest('post.filter.req.auth.https.only', function($https_only) {
	return $https_only; //Bool
});

###req.auth.username

/**
 * req.auth.username
 *
 * This is the username used to grant access to non-ajax users
 * At a minimum you should change this value
 *
 * @return (String) the username
 */ 
$this->onRest('req.auth.username', function(){
	return 'admin@restuser';
});

####pre.filter.req.auth.username

$this->onRest('pre.filter.req.auth.username', function() {
	//no return
});

####post.filter.req.auth.username

$this->onRest('post.filter.req.auth.username', function($username) {
	return $username; //String
});

###req.auth.password

/**
 * req.auth.password
 *
 * This is the password use to grant access to non-ajax users
 * At a minimum you should change this value
 *
 * @return (String) the password
 */ 
$this->onRest('req.auth.password', function(){
	return 'admin@Access';
});

####pre.filter.req.auth.password

$this->onRest('pre.filter.req.auth.password', function() {
	//no return
});

####post.filter.req.auth.password

$this->onRest('post.filter.req.auth.password', function($password) {
	return $password; //String
});

###req.auth.user

/**
 * req.auth.user
 *
 * Used to validate a non-ajax user
 *
 * @param (String) (application_id) the application_id defined in config.application.id
 * @param (String) (username) the username defined in req.auth.username
 * @param (String) (password) the password defined in req.auth.password
 *
 * @return (Bool) true to grant access; false to deny access
 */ 
$this->onRest('req.auth.user', function($application_id, $username, $password) {
	if(!isset($_SERVER['HTTP_X_'.$application_id.'_USERNAME']) || !isset($_SERVER['HTTP_X_'.$application_id.'_PASSWORD'])) {
				return false;
			}
	if( $username != $_SERVER['HTTP_X_'.$application_id.'_USERNAME'] ) {
		return false;
	}
	if( $password != $_SERVER['HTTP_X_'.$application_id.'_PASSWORD'] ) {
		return false;
	}
	return true;
});

####pre.filter.req.auth.user

$this->onRest('pre.filter.req.auth.user', function($application_id, $username, $password) {
	return [$application_id, $username, $password]; //Array [String, String, String]
});

####post.filter.req.auth.user

$this->onRest('post.filter.req.auth.user', function($validation) {
	return $validation; //Bool
});

###req.auth.ajax.user

/**
 * req.auth.ajax.user
 *
 * Used to validate an ajax user
 * That is requests originating from JavaScript
 * By default all logged-in users will be granted access
 * everyone else will be denied
 * You should most likely over ride this
 *
 * @return (Bool) return true to grant access; false to deny access
 */ 
$this->onRest('req.auth.ajax.user', function() {
	if(Yii::app()->user->isGuest) {
		return false;
	}
	return true;
});

####pre.filter.req.auth.ajax.user

$this->onRest('pre.filter.req.auth.ajax.user', function() {
	//no return
});

####post.filter.req.auth.ajax.user

$this->onRest('post.filter.req.auth.ajax.user', function($validation) {
	return $validation; //Bool
});

###req.auth.type

/**
 * req.auth.type
 *
 * @return (Int) The request authentication type which may be 'USERPASS' (2), 'AJAX' (3) or 'CORS' (1)
 */
$this->onRest('req.auth.type', function($application_id) {
	if(isset($_SERVER['HTTP_X_'.$application_id.'_CORS']) || (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] == 'OPTIONS')) {
		return ERestEventListenerRegistry::REQ_TYPE_CORS;
	} else if(isset($_SERVER['HTTP_X_'.$application_id.'_USERNAME']) && isset($_SERVER['HTTP_X_'.$application_id.'_PASSWORD'])) {
		return ERestEventListenerRegistry::REQ_TYPE_USERPASS;
	} else {
		return ERestEventListenerRegistry::REQ_TYPE_AJAX;
	}
});

####pre.filter.req.auth.type

$this->onRest('pre.filter.req.auth.type', function($application_id) {
	return $application_id; //String
});

####post.filter.req.auth.type

$this->onRest('post.filter.config.application.id', function($auth_type) {
	return $auth_type; //Int
});

###req.cors.access.control.allow.origin

/**
 * req.cors.access.control.allow.origin
 *
 * Used to validate a CORS request
 *
 * @return (Array) return a list of domains (origin) allowed access
 */
$this->onRest('req.cors.access.control.allow.origin', function() {
	return []; //Array
});

####pre.filter.req.cors.access.control.allow.origin

$this->onRest('pre.filter.req.cors.access.control.allow.origin', function() {
	//No Return
});

####post.filter.req.cors.access.control.allow.origin

$this->onRest('post.filter.req.cors.access.control.allow.origin', function($allowed_origins) {
	return $allowed_origins; //Array
});

###req.cors.access.control.allow.methods

/**
 * req.cors.access.control.allow.methods
 *
 * Used by CORS request to indicate the http methods (verbs) 
 * that can be used in the actual request
 *
 * @return (Array) List of http methods allowed via CORS
 */
$this->onRest('req.cors.access.control.allow.methods', function() {
	return ['GET', 'POST'];
});

####pre.filter.req.cors.access.control.allow.methods

$this->onRest('pre.filter.req.cors.access.control.allow.methods', function() {
	//No Return
});

####post.filter.req.cors.access.control.allow.methods

$this->onRest('post.filter.req.cors.access.control.allow.methods', function($allowed_methods) {
	return $allowed_methods; //Array
});

###req.cors.access.control.allow.headers

/**
 * req.cors.access.control.allow.headers
 *
 * Used by CORS request to indicate which custom headers are allowed in a request
 *
 * @return (Array) List of allowed headers
 */ 
$this->onRest('req.cors.access.control.allow.headers', function($application_id) {
	return ["X_{$application_id}_CORS"];
});

####pre.filter.req.cors.access.control.allow.headers

$this->onRest('pre.filter.req.cors.access.control.allow.headers', function() {
	//No Return
});

####post.filter.req.cors.access.control.allow.headers

$this->onRest('post.filter.req.cors.access.control.allow.headers', function($allowed_headers) {
	return $allowed_headers; //Array
});

###req.cors.access.control.max.age

/**
 * req.cors.access.control.max.age
 *
 * Used in a CORS request to indicate how long the response can be cached, 
 * so that for subsequent requests, within the specified time, no preflight request has to be made
 *
 * @return (Int) time in seconds
 */
$this->onRest('req.cors.access.control.max.age', function() {
	return 3628800; //Int
});

####pre.filter.req.cors.access.control.max.age

$this->onRest('pre.filter.req.cors.access.control.max.age', function() {
	//No Return
});

####post.filter.req.cors.access.control.max.age

$this->onRest('post.filter.req.cors.access.control.max.age', function($max_age_in_seconds) {
	return $max_age_in_seconds; //int
});

###req.auth.cors

/**
 * req.auth.cors
 *
 * Used to authorize a given CORS request
 *
 * @param (Array) (allowed_origins) list of allowed remote origins
 *
 * @return (Bool) true to allow access and false to deny access
 */
$this->onRest('req.auth.cors', function ($allowed_origins) {
	if((isset($_SERVER['HTTP_ORIGIN'])) && (( array_search($_SERVER['HTTP_ORIGIN'], $allowed_origins)) !== false )) {
		return true;
	}
	return false;	
});

####pre.filter.req.auth.cors

$this->onRest('pre.filter.req.auth.cors', function($allowed_origins) {
	return $allowed_origins; //Array
});

####post.filter.req.auth.cors

$this->onRest('post.filter.config.application.id', function($auth_cors) {
	return $auth_cors; //Bool
});

###req.auth.uri

/**
 * req.auth.uri
 *
 * return true to allow access to a given uri / http verb;
 * false to deny access to a given uri / http verb;
 *
 * @return (bool) default is true
 */ 
$this->onRest(req.auth.uri, function($uri, $verb) {
	return true;
});

####pre.filter.req.auth.uri

$this->onRest('pre.filter.req.auth.uri', function($uri, $verb) {
	return [$uri, $verb]; //array[string, string]
});

####post.filter.req.auth.uri

$this->onRest('post.filter.req.auth.uri, function($validation) {
	return $validation; //bool
});

####req.after.action

/**
 * req.after.action
 *
 * Called after the request has been fulfilled
 * By default it has no behavior
 */ 
$this->onRest('req.after.action', function($filterChain) {
	//Logic being applied after the action is executed
});

####pre.filter.req.after.action

$this->onRest('pre.filter.req.after.action', function($filterChain) {
	return $filterChain; //Object
});

####post.filter.req.after.action

$this->onRest('post.filter.after.action', function($result=null) {
	return $result; //Mixed
});

###req.param.is.pk

/**
 * req.param.is.pk
 *
 * Called when attempting to validate a resources primary key
 * The default is an integer
 *
 * @return (bool) true to confirm primary key; false to deny
 */
$this->onRest('req.param.is.pk', function($pk) {
	return $pk === '0' || preg_match('/^-?[1-9][0-9]*$/', $pk) === 1;
});

####pre.filter.req.param.is.pk

$this->onRest('pre.filter.req.param.is.pk', function($pk) {
	return $pk; //Mixed
});

####post.filter.req.param.is.pk

$this->onRest('post.filter.req.param.is.pk', function($isPk) {
	return $isPk; //Bool
});

###req.is.subresource

/**
 * req.is.subresource
 * 
 * Called when trying to determain if the request is for a subresource
 * WARNING!!!: ONLY CHANGE THIS EVENTS BEHAVIOR IF YOU REALLY KNOW WHAT YOUR DOING!!!
 * WARNING!!!: CHANGING THIS MAY LEAD TO INCONSISTENT AND OR INCORRECT BEHAVIOR
 *
 * @param (Object) (model) model instance to evaluate
 * @param (String) (subresource_name) potentially the name of the subresource
 * @param (String) (http_verb) the http verb used to make the request
 *
 * @return (Bool) True if this is a subresouce request and false if not
 */ 
$onRest('req.is.subresource', function($model, $subresource_name, $http_verb) {
	if(!array_key_exists($subresource_name, $model->relations())) {
		return false;
	}
	if($model->relations()[$subresource_name][0] != CActiveRecord::MANY_MANY) {
		return false;
	}
	return true;
});

####pre.filter.req.is.subresource

$this->onRest('pre.filter.req.param.is.pk', function($model, $subresource_name, $http_verb) {
	return [$model, $subresource_name, $http_verb]; //Array
});

####post.filter.req.is.subresource

$this->onRest('post.filter.req.is.subresource', function($is_subresource) {
	return $is_subresource; //Bool
});

###req.data.read

/**
 * req.data.read
 *
 * Called when reading data on POST & PUT requests
 *
 * @param (String) this can be either a stream wrapper of a file path
 *
 * @return (Array) the JSON decoded array of data
 */ 
$this->onRest('req.data.read', function($stream='php://input') {
	$reader = new ERestRequestReader($stream);
	return CJSON::decode($reader->getContents());
});

####pre.filter.req.data.read

$this->onRest('pre.filter.req.data.read, function($stream='php://input') {
	return $stream; //Mixed
});

####post.filter.req.data.read

$this->onRest('post.filter.req.data.read', function($data) {
	return [$data]; //Array [Array]
});

###req.get.resource.render

/**
 * req.get.resource.render
 *
 * Called when a GET request for a single resource is to be rendered
 * @param (Object) (data) this is the resources model
 * @param (String) (model_name) the name of the resources model
 * @param (Array) (relations) the list of relations to include with the data
 * @param (Int) (count) the count of records to return (will be either 1 or 0)
 */
$this->onRest('req.get.resource.render', function($data, $model_name, $relations, $count, $visibleProperties=[], $hiddenProperties=[]) {
	//Handler for GET (single resource) request
	$this->setHttpStatus((($count > 0)? 200: 204));
	$this->renderJSON([
		'type'				=> 'rest',
		'success'			=> (($count > 0)? true: false),
		'message'			=> (($count > 0)? "Record Found": "No Record Found"),
		'totalCount'		=> $count,
		'modelName'			=> $model_name,
		'relations'			=> $relations,
		'visibleProperties'	=> $visibleProperties,
		'hiddenProperties'	=> $hiddenProperties,
		'data'				=> $data,
	]);
});

####pre.filter.req.get.resource.render

$this->onRest('pre.filter.req.get.resource.render', function($data, $model_name, $relations, $count, $visibleProperties=[], $hiddenProperties=[]) {
	return [$data, $model_name, $relations, $count, $visibleProperties, $hiddenProperties]; //Array [Object, String, Array, Int, Array[String], Array[String]]
});

####post.filter.req.get.resource.render

/*
 * @param (JSON String) $json
 */
$this->onRest('post.filter.req.get.resource.render', function($json) {
	return $json //Mixed[JSON Sting, ARRAY]
});

####req.get.resources.render

/**
 * req.get.resources.render
 *
 * Called when a GET request for when a list resources is to be rendered
 *
 * @param (Array) (data) this is an array of models representing the resources
 * @param (String) (model_name) the name of the resources model
 * @param (Array) (relations) the list of relations to include with the data
 * @param (Int) (count) the count of records to return
 */
$this->onRest('req.get.resources.render', function($data, $model_name, $relations, $count, $visibleProperties=[], $hiddenProperties=[]) {
	//Handler for GET (list resources) request
	$this->setHttpStatus((($count > 0)? 200: 204));
	$this->renderJSON([
		'type'				=> 'rest',
		'success'			=> (($count > 0)? true: false),
		'message'			=> (($count > 0)? "Record(s) Found": "No Record(s) Found"),
		'totalCount'		=> $count,
		'modelName'			=> $model_name,
		'relations'			=> $relations,
		'visibleProperties'	=> $visibleProperties,
		'hiddenProperties'	=> $hiddenProperties,
		'data'				=> $data,
	]);
});

####pre.filter.req.get.resources.render

$this->onRest('pre.filter.req.get.resources.render', function($data, $model_name, $relations, $count, $visibleProperties, $hiddenProperties) {
	return [$data, $model_name, $relations, $count, $visibleProperties, $hiddenProperties]; //Array [Array [Object], String, Array, Int, Array[String], Array[String]]
});

####post.filter.req.get.resources.render

/*
 * @param (JSON String) $json
 */
$this->onRest('post.filter.req.get.resources.render', function($json) {
	return $json //Mixed[JSON Sting, ARRAY]
});

###req.put.resource.render

/**
 * req.put.resource.render
 * 
 * Called when a PUT request (update) is to be rendered
 *
 * @param (Object) (model) the updated model
 * @param (Array) (relations) list of relations to render with model
 */
$this->onRest('req.put.resource.render' function($model, $relations, $visibleProperties=[], $hiddenProperties=[]) {
	$this->renderJSON([
		'type'				=> 'rest',
		'success'			=> 'true',
		'message'			=> "Record Updated",
		'totalCount'	=> "1",
		'modelName'		=> get_class($model),
		'relations'		=> $relations,
		'visibleProperties'	=> $visibleProperties,
		'hiddenProperties'	=> $hiddenProperties,
		'data'				=> $model,
	]);
});

####pre.filter.req.put.resource.render

$this->onRest('pre.filter.req.req.put.resource.render', function($model, $relations, $visibleProperties=[], $hiddenProperties=[]) {
	return [$model, $relations, $visibleProperties, $hiddenProperties]; //Array [Object, Array, Array[String], Array[String]]
});

####post.filter.req.put.resource.render

/*
 * @param (JSON String) $json
 */
$this->onRest('post.filter.req.put.resource.render', function($json) {
	return $json //Mixed[JSON Sting, ARRAY]
});

###req.post.resource.render

/**
 * req.post.resource.render
 * 
 * Called when a POST request (create) is to be rendered
 *
 * @param (Object) (model) the newly created model
 * @param (Array) (relations) list of relations to render with model
 */
$this->onRest('req.post.resource.render', function($model, $relations=[], $visibleProperties=[], $hiddenProperties=[]) {
	$this->renderJSON([
		'type'				=> 'rest',
		'success'			=> 'true',
		'message'			=> "Record Created",
		'totalCount'	=> "1",
		'modelName'		=> get_class($model),
		'relations'		=> $relations,
		'visibleProperties'	=> $visibleProperties,
		'hiddenProperties'	=> $hiddenProperties,
		'data'				=> $model,
	]);
});

####pre.filter.req.post.resource.render

$this->onRest('pre.filter.req.post.resource.render', function($model, $relations, $visibleProperties=[], $hiddenProperties=[]) {
	return [$model, $relations, $visibleProperties, $hiddenProperties]; //Array [Object, Array, Array[String], Array[String]]
});

####post.filter.req.post.resource.render

/*
 * @param (JSON String) $json
 */
$this->onRest('post.filter.req.post.resource.render', function($json) {
	return $json //Mixed[JSON Sting, ARRAY]
});

###req.delete.resource.render

/**
 * req.delete.resource.render
 *
 * Called when DELETE request is to be rendered
 *
 * @param (Object) (model) this is the deleted model object for the resource
 */
$this->onRest('req.delete.resource.render', function($model, $visibleProperties=[], $hiddenProperties=[]) {
	$this->renderJSON([
		'type'				=> 'rest',
		'success'			=> 'true',
		'message'			=> "Record Deleted",
		'totalCount'		=> "1",
		'modelName'			=> get_class($model),
		'relations'			=> [],
		'visibleProperties'	=> $visibleProperties,
		'hiddenProperties'	=> $hiddenProperties,
		'data'				=> $model,
	]);
});

####req.delete.resource.render

$this->onRest('pre.filter.req.delete.resource.render', function($model, $visibleProperties=[], $hiddenProperties=[]) {
	return[$model, $visibleProperties, $hiddenProperties]; //Array[Object, Array[String], Array[String]]
});

####post.filter.req.get.resource.render

/*
 * @param (JSON String) $json
 */
$this->onRest('post.filter.req.get.resource.render', function($json) {
	return $json //Mixed[JSON Sting, ARRAY]
});

###req.get.subresource.render

/**
 * req.get.subresource.render
 *
 * Called when a GET request for a sub-resource is to be rendered
 *
 * @param (Object) (model) the model representing the sub-resource
 * @param (String) (subresource_name) the name of the sub-resource to render
 * @param (Int) (count) the count of sub-resources to render (will be either 1 or 0)
 */
$this->onRest('req.get.subresource.render', function($model, $subresource_name, $count, $visibleProperties, $hiddenProperties) {
	$this->setHttpStatus((($count > 0)? 200: 204));

	$this->renderJSON([
		'type'				=> 'rest',
		'success'			=> true,
		'message'			=> (($count > 0)? "Record(s) Found": "No Record(s) Found"),
		'totalCount'		=> $count,
		'modelName'			=> $subresource_name,
		'visibleProperties'	=> $visibleProperties,
		'hiddenProperties'	=> $hiddenProperties,
		'data'				=> $model,
	]);
});

####pre.filter.req.delete.subresource.render

$this->onRest('pre.filter.req.delete.subresource.render', function($model, $subresource_name, $count, $visibleProperties=[], $hiddenProperties=[]) {
	return [$model, $subresource_name, $count, $visibleProperties, $hiddenProperties]; //Array [Object, String, Int, Array[String], Array[String]]
});

###req.get.subresources.render

/**
 * req.get.subresources.render
 *
 * Called when a GET request for a list of sub-resources is to be rendered
 *
 * @param (Array) (models) list of sub-resource models
 * @param (String) (subresource_name) the name of the sub-resources to render
 * @param (Int) (count) the count of sub-resources to render
 */
$this->onRest('req.get.subresources.render', function($models, $subresource_name, $count, $visibleProperties=[], $hiddenProperties=[]) {
	$this->setHttpStatus((($count > 0)? 200: 204));
	
	$this->renderJSON([
		'type'				=> 'rest',
		'success'			=> (($count > 0)? true: false),
		'message'			=> (($count > 0)? "Record(s) Found": "No Record(s) Found"),
		'totalCount'		=> $count,
		'modelName'			=> $subresource_name,
		'visibleProperties'	=> $visibleProperties,
		'hiddenProperties'	=> $hiddenProperties,
		'data'				=> $models,
	]);
});

####pre.filter.req.get.subresources.render

$this->onRest('pre.filter.req.get.subresources.render', function($models, $subresource_name, $count, $visibleProperties=[], $hiddenProperties=[]) {
	return [$models, $subresource_name, $count, $visibleProperties, $hiddenProperties]; //Array [Array[Object], String, Int, Array[String], Array[String]]
});

####post.filter.req.get.subresources.render

/*
 * @param (JSON String) $json
 */
$this->onRest('post.filter.req.get.subresources.render', function($json) {
	return $json //Mixed[JSON Sting, ARRAY]
});

###req.put.subresource.render

/**
 * req.put.subresource.render
 *
 * Called when a PUT request to a sub-resource (add a sub-resource) is rendered
 *
 * @param (Object) (model) the model of the resource that owns the subresource
 * @param (String) (subresource_name) the name of the sub-resource
 * @param (Mixed/Int) (subresource_id) the primary key of the sub-resource
 */
$this->onRest('req.put.subresource.render', function($model, $subresource_name, $subresource_id, $visibleProperties=[], $hiddenProperties=[]) {
	$this->renderJSON([
		'type'				=> 'rest',
		'success'			=> 'true',
		'message'			=> "Subresource Added",
		'totalCount'		=> "1",
		'modelName'			=> get_class($model),
		'relations'			=> [$subresource_name],
		'visibleProperties'	=> $visibleProperties,
		'hiddenProperties'	=> $hiddenProperties,
		'data'				=> $model,
	]);
});

####pre.filter.req.put.subresource.render

$this->onRest('pre.filter.req.put.subresource.render', function($model, $subresource_name, $subresource_id, $visibleProperties=[], $hiddenProperties=[]) {
	return [$model, $subresource_name, $subresource_id, $visibleProperties, $hiddenProperties]; //Array [Object, String, Int, Array[String], Array[String]]
});

####post.filter.req.put.subresources.render

/*
 * @param (JSON String) $json
 */
$this->onRest('post.filter.req.put.subresources.render', function($json) {
	return $json //Mixed[JSON Sting, ARRAY]
});

###req.delete.subresource.render

/**
 * req.delete.subresource.render
 *
 * Called when DELETE request on a sub-resource is to be made
 *
 * @param (Object) (model) this is the model object that owns the deleted sub-resource
 * @param (String) (subresource_name) the name of the deleted sub-resource
 * @param (Mixed/Int) (subresource_id) the primary key of the deleted sub-resource
 */
$this->onRest('req.delete.subresource.render', function($model, $subresource_name, $subresource_id, $visibleProperties=[], $hiddenProperties=[]) {
	$this->renderJSON([
		'type'				=> 'rest',
		'success'			=> 'true',
		'message'			=> "Sub-Resource Deleted",
		'totalCount'		=> "1",
		'modelName'			=> get_class($model),
		'relations'			=> [$subresource_name],
		'visibleProperties'	=> $visibleProperties,
		'hiddenProperties'	=> $hiddenProperties,
		'data'				=> $model,
	]);
});

####pre.filter.req.delete.subresource.render

$this->onRest('pre.filter.req.delete.subresource.render', function($model, $subresource_name, $subresource_id, $visibleProperties=[], $hiddenProperties=[]) {
	return [$model, $subresource_name, $subresource_id, $visibleProperties, $hiddenProperties]; //Array [Object, String, Int, Array[String], Array[String]]
});

####post.filter.req.delete.subresources.render

/*
 * @param (JSON String) $json
 */
$this->onRest('post.filter.req.delete.subresources.render', function($json) {
	return $json //Mixed[JSON Sting, ARRAY]
});

###req.render.json

/**
 * req.render.json
 * NOT CALLED internally
 * The handler exists to allow users the ability to easily render arbitrary JSON
 * To do so you must 'emitRest' this event
 *
 * @param (Array) (data) data to be rendered
 */
$this->onRest('req.render.json', function($data) {
	$this->renderJSON([
		'type' => 'raw',
		'data' => $data,
	]);
});

####pre.filter.req.render.json

$this->onRest('pre.filter.req.render.json', function($data) {
	return [$data]; //Array[Array]
});

###req.exception

/**
 * req.exception
 *
 * Error handler called when an Exception is thrown
 * Used to render response to the client
 *
 * @param (Int) (errorCode) the http status code
 * @param (String) the error message
 */
$this->onRest('req.exception', function($errorCode, $message=null) {
	$this->renderJSON([
		'type'			=> 'error',
		'success'		=> false,
		'message'		=> (is_null($message)? $this->getHttpStatus()->message: $message),
		'errorCode' => $errorCode,
	]);
});

####pre.filter.req.exception

$this->onRest('pre.filter.req.exception', function($errorCode, $message=null) {
	return [$errorCode, $message=null]; //Array [Int, String]
});

###model.instance

/**
 * model.instance
 *
 * Called when an instance of the model representing the resource(s) is requested
 * By default this is your controller class name minus the 'Controller'
 *
 * @return (Object) an empty instance of a resources Active Record model
 */
$this->onRest('model.instance', function() {
	$modelName = str_replace('Controller', '', get_class($this));
	return new $modelName();
});

####pre.filter.model.instance

$this->onRest('pre.filter.model.instance', function() {
	//No return
});

####post.filter.model.instance

$this->onRest('post.filter.model.instance', function($result)) {
	return $result; //
});

###model.attach.behaviors

/**
 * model.attach.behaviors
 *
 * Called on all requests (Other then custom) to add some magic to your models
 * Attach helper behaviors to model
 *
 * @param (Object) (model) an instance of an AR Model
 *
 * @return (Object) an instance of an Active Record model
 */
$this->onRest('model.attach.behaviors', function($model) {
	//Attach this behavior to help saving nested models
	if(!array_key_exists('ERestActiveRecordRelationBehavior', $model->behaviors())) {
		$model->attachBehavior('ERestActiveRecordRelationBehavior', new ERestActiveRecordRelationBehavior());
	}
	
	if(!array_key_exists('ERestHelperScopes', $model->behaviors())) {
		$model->attachBehavior('ERestHelperScopes', new ERestHelperScopes());
	}
	return $model;
});

####pre.filter.model.attach.behaviors

$this->onRest('pre.filter.model.attach.behaviors', function($model) {
	return $model //Object
});

####post.filter.model.attach.behaviors

$this->onRest('post.filter.model.attach.behaviors', function($model)) {
	return $model; //Object
});

###model.with.relations

/**
 * model.with.relations
 *
 * Called when trying to determine which relations to include in a requests render
 * The default is all relations not starting with an underscore
 * You should most likely customize this per resource
 * as some resources may have relations that return large number of records
 *
 * @return (Array) list of relations (Strings) to attach to resources output
 */
$this->onRest('model.with.relations', function($model) {
	$nestedRelations = [];
	foreach($model->metadata->relations as $rel=>$val)
	{
		$className = $val->className;
		$rel_model = call_user_func([$className, 'model']);
		if(!is_array($rel_model->tableSchema->primaryKey) && substr($rel, 0, 1) != '_') {
			$nestedRelations[] = $rel;
		}
	}
	return $nestedRelations;
});

####pre.filter.model.with.relations

$this->onRest('pre.filter.model.with.relations', function($model) {
	return $model; //Object
});

####post.filter.model.with.relations

$this->onRest('post.filter.model.with.relations', function($result)) {
	return $result; //Array[String]
});

###model.lazy.load.relations

/**
 * model.lazy.load.relations
 *
 * Called when determining if relations should be lazy or eager loaded
 * The default is to lazy load. In most cases this is sufficient
 * Under certain conditions eager loading my be more appropriate
 *
 * @return (Bool) true to lazy load relations; false to eager load
 */
$this->onRest('model.lazy.load.relations', function() {
	return true;
});

####pre.filter.model.lazy.load.relations

$this->onRest('pre.filter.model.lazy.load.relations', function() {
	//No return
});

####post.filter.model.lazy.load.relations

$this->onRest('post.filter.model.lazy.load.relations', function($result)) {
	return $result; //Bool
});

###model.limit

/**
 * model.limit
 *
 * Called when applying a limit to the resources returned in a GET request
 * The default is 100 or the value of the _GET param 'limit'
 *
 * @return (Int) the number of results to return
 */
$this->onRest('model.limit', function() {
	return isset($_GET['limit'])? $_GET['limit']: 100;
});

####pre.filter.model.limit

$this->onRest('pre.filter.model.limit', function() {
	//No return
});

####post.filter.model.limit

$this->onRest('post.filter.model.limit', function($result)) {
	return $result; //Int
});

###model.offset

/**
 * model.offset
 *
 * Called when applying an offset to the records returned in a GET request
 * The default is 0 or the value of the _GET param 'offset'
 *
 * @return (Int) the offset of results to return
 */
$this->onRest('model.offset', function() {
	return isset($_GET['offset'])? $_GET['offset']: 0;
});

####pre.filter.model.offset

$this->onRest('pre.filter.model.offset', function() {
	//No return
});

####post.filter.model.offset

$this->onRest('post.filter.model.offset', function($result)) {
	return $result; //Int
});

###model.scenario

/**
 * model.scenario
 *
 * Called before a resource is found
 * This is the scenario to apply to a resource pre-find
 * The default is 'restfullyii' or the value of the _GET param 'scenario'
 *
 * @return (String) the scenario name
 */
$this->onRest('model.scenario', function() {
	return isset($_GET['scenario'])? $_GET['scenario']: 'restfullyii';
});

####pre.filter.model.scenario

$this->onRest('pre.filter.model.scenario', function() {
	//No return
});

####post.filter.model.scenario

$this->onRest('post.filter.model.scenario', function($result)) {
	return $result; //String
});

###model.filter

/**
 * model.filter
 *
 * Called when attempting to apply a filter to apply to the records in a GET request
 * The default is 'NULL' or the value of the _GET param 'filter'
 * The format is JSON: 
 * '[{"property":"SOME_PROPERTY", "value":"SOME_VALUE"}]'
 * See documentation for additional options
 *
 * @return (JSON) the filter to apply
 */
$this->onRest('model.filter', function() {
	return isset($_GET['filter'])? $_GET['filter']: null;
});

####pre.filter.model.filter

$this->onRest('pre.filter.model.filter', function() {
	//No return
});

####post.filter.model.filter

$this->onRest('post.filter.model.filter', function($result)) {
	return $result; //Array[Object]
});

###model.sort

/**
 * model.sort
 *
 * Called when attempting to sort records returned in a GET request
 * The default is 'NULL' or the value of the _GET param 'sort'
 * The format is JSON:
 * [{"property":"SOME_PROPERTY", "direction":"DESC"}]
 *
 * @return (JSON) the sort to apply
 */ 
$this->onRest('model.sort', function() {
	return isset($_GET['sort'])? $_GET['sort']: null;
});

####pre.filter.model.sort

$this->onRest('pre.filter.model.sort', function() {
	//No return
});

####post.filter.model.sort

$this->onRest('post.filter.model.sort', function($result)) {
	return $result; //Array[Object]
});

###model.find

/**
 * model.find
 *
 * Called when attempting to find a single model
 *
 * @param (Object) (model) an instance of the resources model
 * @param (Mixed/Int) (id) the resources primary key
 *
 * @return (Object) the found model
 */
$this->onRest('model.find', function($model, $id) {
	return $model->findByPk($id);
});

####pre.filter.model.find

$this->onRest('pre.filter.model.find', function($model, $id) {
	return [$model, $id]; //Array [Object, Int]
});

####post.filter.model.find

$this->onRest('post.filter.model.find', function($result)) {
	return $result; //Object
});

###model.find.all

/**
 * model.find.all
 *
 * Called when attempting to find a list of models
 *
 * @param (Object) (model) an instance of the resources model
 *
 * @return (Array) list of found models
 */
$this->onRest('model.find.all', function($model) {
	return $model->findAll();
});

####pre.filter.model.find.all

$this->onRest('pre.filter.model.find.all', function($model) {
	return $model; //Object
});

####post.filter.model.find.all

$this->onRest('post.filter.model.find.all', function($result)) {
	return $result; //Array[Object]
});

###model.count

/**
 * model.count
 *
 * Called when the count of model(s) is needed
 *
 * @param (Object) (model) the model to apply the count to
 *
 * @return (Int) the count of models
 */
$this->onRest('model.count', function($model) {
	return $model->count();
});

####pre.filter.model.count

$this->onRest('pre.filter.model.count', function($model) {
	return $model; //Object
});

####post.filter.model.count

$this->onRest('post.filter.model.count', function($result)) {
	return $result; //Int
});

###model.subresource.find

/**
 * model.subresource.find
 *
 * Called when attempting to find a subresource
 *
 * @param (Object) (model) the model that represents the owner of the sub-resource
 * @param (String) (subresource_name) the name of the sub-resource
 * @param (Mixed/Int) (subresource_id) the primary key of the sub-resource
 *
 * @return (Object) the sub-resource model
 */
$this->onRest('model.subresource.find', function($model, $subresource_name, $subresource_id) {
	$subresource = @$this->getSubresourceHelper()->getSubresource($model, $subresource_name, $subresource_id);
	if(count($subresource) > 0) {
		return $subresource[0];
	}
	return $subresource; //Object
});

####pre.filter.model.subresource.find

$this->onRest('pre.filter.model.subresource.find', function($model, $subresource_name, $subresource_id) {
	return [$model, $subresource_name, $subresource_id]; //Array [Object, String, Int]
});

####post.filter.model.subresource.find

$this->onRest('post.filter.model.subresource.find', function($result)) {
	return $result; //Object
});

###model.subresource.find.all

/**
 * model.subresource.find.all
 *
 * Called when attempting to find all subresources of a resource
 *
 * @param (Object) (model) the model that represents the owner of the sub-resources
 * @param (String) (subresource_name) the name of the sub-resource
 *
 * @return (Array) list of sub-resource models
 */
$this->onRest(ERestEvent::MODEL_SUBRESOURCES_FIND_ALL, function($model, $subresource_name) {
	return $this->getSubresourceHelper()->getSubresource($model, $subresource_name);
});

####pre.filter.model.subresource.find.all

$this->onRest('pre.filter.model.subresource.find.all', function($model, $subresource_name) {
	return [$model, $subresource_name]; //Array [Object, String]
});

####post.filter.model.subresource.find.all

$this->onRest('post.filter.model.subresource.find.all', function($result)) {
	return $result; //Array[Object]
});

###model.subresource.count

/**
 * model.subresource.count
 *
 * Called when the count of sub-resources is needed
 *
 * @param (Object) (model) the model that represents the owner of the sub-resource
 * @param (String) (subresource_name) the name of the sub-resource
 * @param (Mixed/Int) (subresource_id) the primary key of the sub-resource
 *
 * @return (Int) count of the subresources
 */
$this->onRest('model.subresource.count', function($model, $subresource_name, $subresource_id=null) {
	return $this->getSubresourceHelper()->getSubresourceCount($model, $subresource_name, $subresource_id);
});

####pre.filter.model.subresource.count

$this->onRest('pre.filter.model.subresource.count', function($model, $subresource_name, $subresource_id=null) {
	return [$model, $subresource_name, $subresource_id=null]; //Array [Object, String, Int]
});

####post.filter.model.subresource.count

$this->onRest('post.filter.model.subresource.count', function($result)) {
	return $result; //Int
});

###model.apply.post.data

/**
 * model.apply.post.data
 *
 * Called on POST requests when attempting to apply posted data
 *
 * @param (Object) (model) the resource model to save data to
 * @param (Array) (data) the data to save to the model
 * @param (Array) (restricted_properties) list of restricted properties
 *
 * @return (Object) the model with posted data applied
 */
$this->onRest('model.apply.post.data', function($model, $data, $restricted_properties) {
	return $this->getResourceHelper()->setModelAttributes($model, $data, $restricted_properties);
});

####pre.filter.model.apply.post.data

$this->onRest('pre.filter.model.apply.post.data', function($model, $data, $restricted_properties) {
	return [$model, $data, $restricted_properties]; //Array []
});

####post.filter.model.apply.post.data

$this->onRest('post.filter.model.apply.post.data', function($result)) {
	return $result; //
});

###model.apply.put.data

/**
 * model.apply.put.data
 *
 * Called on PUT requests when attempting to apply PUT data
 *
 * @param (Object) (model) the resource model to save data to
 * @param (Array) (data) the data to save to the model
 * @param (Array) (restricted_properties) list of restricted properties
 *
 * @return (Object) the model with PUT data applied
 */
$this->onRest('model.apply.put.data', function($model, $data, $restricted_properties) {
	return $this->getResourceHelper()->setModelAttributes($model, $data, $restricted_properties);
});

####pre.filter.model.apply.put.data

$this->onRest('pre.filter.model.apply.put.data', function($model, $data, $restricted_properties) {
	return [$model, $data, $restricted_properties]; //Array [Object, Array, Array]
});

####post.filter.model.apply.put.data

$this->onRest('post.filter.model.apply.put.data', function($result)) {
	return $result; //Object
});

###model.save

/**
 * model.save
 *
 * Called whenever a model resource is saved
 *
 * @param (Object) the resource model to save
 *
 * @return (Object) the saved resource
 */
$this->onRest('model.save', function($model) {
	if(!$model->save()) {
		throw new CHttpException('400', CJSON::encode($model->errors));
	}
	$model->refresh();
	return $model;
});

####pre.filter.model.save

$this->onRest('pre.filter.model.save', function($model) {
	return $model; //Object
});

####post.filter.model.save

$this->onRest('post.filter.model.save', function($result)) {
	return $result; //Object
});

###model.subresources.save

/**
 * model.subresources.save
 *
 * Called whenever a sub-resource is saved
 *
 * @param (Object) (model) the owner of the sub-resource
 * @param (String) (subresource_name) the name of the subresource
 * @param (Mixed/Int) (subresource_id) the primary key of the subresource
 *
 * @return (Object) the updated model representing the owner of the sub-resource
 */
$this->onRest(ERestEvent::MODEL_SUBRESOURCE_SAVE, function($model, $subresource_name, $subresource_id) {
	if(!$this->getSubresourceHelper()->putSubresourceHelper($model, $subresource_name, $subresource_id)) {
		throw new CHttpException('500', 'Could not save Sub-Resource');
	}
	$model->refresh();
	return true;
});

####pre.filter.model.subresources.save

$this->onRest('pre.filter.model.subresources.save', function($model, $subresource_name, $subresource_id) {
	return [$model, $subresource_name, $subresource_id]; //Array [Object, String, Int]
});

####post.filter.model.subresources.save

$this->onRest('post.filter.model.subresources.save', function($result)) {
	return $result; //Object
});

###model.delete

/**
 * model.delete
 *
 * Called whenever a model resource needs deleting
 *
 * @param (Object) (model) the model resource to be deleted
 */
$this->onRest('model.delete', function($model) {
	if(!$model->delete()) {
		throw new CHttpException(500, 'Could not delete model');
	}
	return $model;
});

####pre.filter.model.delete

$this->onRest('pre.filter.model.delete', function($model) {
	return $model; //Object
});

####post.filter.model.delete

$this->onRest('post.filter.model.delete', function($result)) {
	return $result; //Object
});

###model.subresource.delete

/**
 * model.subresource.delete
 *
 * Called whenever a subresource needs deleting
 *
 * @param (Object) (model) the owner of the sub-resource
 * @param (String) (subresource_name) the name of the subresource
 * @param (Mixed/Int) (subresource_id) the primary key of the subresource
 *
 * @return (Object) the updated model representing the owner of the sub-resource
 */
$this->onRest('model.subresource.delete', function($model, $subresource_name, $subresource_id) {
	if(!$this->getSubresourceHelper()->deleteSubResource($model, $subresource_name, $subresource_id)) {
		throw new CHttpException(500, 'Could not delete Sub-Resource');
	}
	$model->refresh();
	return $model;
});

####pre.filter.model.subresource.delete

$this->onRest('pre.filter.model.subresource.delete', function($model, $subresource_name, $subresource_id) {
	return [$model, $subresource_name, $subresource_id]; //Array [Object, String, Int]
});

####post.filter.model.subresource.delete

$this->onRest('post.filter.model.subresource.delete', function($result)) {
	return $result; //Object
});

###model.restricted.properties

/**
 * model.restricted.properties
 *
 * Called when determining which properties if any should be considered restricted
 * The default is [] (no restricted properties)
 *
 * @return (Array) list of restricted properties
 */
$this->onRest('model.restricted.properties', function() {
	return [];
});

####pre.filter.model.restricted.properties

$this->onRest('pre.filter.model.restricted.properties', function() {
	//No return
});

####post.filter.model.restricted.properties

$this->onRest('post.filter.model.restricted.properties', function($result)) {
	return $result; //Array
});

###model.visible.properties

/**
 * model.visible.properties
 *
 * Called when determining which properties if any should be considered visible
 * The default is [] (no hidden properties)
 *
 * @return (Array) list of visible properties
 */
$this->onRest('model.visible.properties', function() {
	return [];
});

####pre.filter.model.visible.properties

$this->onRest('pre.filter.model.visible.properties', function() {
	//No return
});

####post.filter.model.visible.properties

$this->onRest('post.filter.model.visible.properties', function($result) {
	return $result; //Array
});

###model.hidden.properties

/**
 * model.hidden.properties
 *
 * Called when determining which properties if any should be considered hidden
 * The default is [] (no hidden properties)
 *
 * @return (Array) list of hidden properties
 */
$this->onRest('model.hidden.properties', function() {
	return [];
});

####pre.filter.model.hidden.properties

$this->onRest('pre.filter.model.hidden.properties', function() {
	//No return
});

####post.filter.model.hidden.properties

$this->onRest('post.filter.model.hidden.properties', function($result)) {
	return $result; //Array
});

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

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.