Skip to content

Instantly share code, notes, and snippets.

@cebe
Created May 18, 2013 11:18
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save cebe/5604091 to your computer and use it in GitHub Desktop.
Save cebe/5604091 to your computer and use it in GitHub Desktop.
REST Action classes for Yii 1.1
<?php
/**
* @todo: add documentation!
*
* @property string|array $returnUrl
*/
abstract class BaseRESTAction extends CAction
{
const HTML = 'html';
const JSON = 'json';
const CSV = 'csv';
const ICAL = 'ical';
/**
* @var string classname of the Model which will be handled by this action
*/
public $modelClass = null;
/**
* @var CActiveRecord AR model instance of this action
*/
public $model = null;
/**
* @var bool set this to false to avoid setting this action as return url
*/
public $rememberReturnUrl = true;
private $_returnUrl = null;
public function getReturnUrl()
{
if ($this->_returnUrl !== null) {
return $this->_returnUrl;
}
if (isset($_POST['returnUrl'])) {
// @todo secure this by not allowing external addresses
return $_POST['returnUrl'];
}
if (isset(Yii::app()->user->returnUrl)) {
return Yii::app()->user->returnUrl;
}
return null;
}
public function setReturnUrl($returnUrl)
{
return $this->_returnUrl = $returnUrl;
}
/**
* Must be called in run() on startup
*/
public function init()
{
if ($this->modelClass === null) {
throw new Exception('modelClass attribute of REST action must be specified!');
}
$this->controller->rememberReturnUrl = $this->controller->rememberReturnUrl && $this->rememberReturnUrl;
}
/**
* http://tools.ietf.org/html/rfc2616#section-10.4.7
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
*
* @return string
*/
public function getResponseType($validRequestTypes=array('GET'), $allowAjax=true)
{
$this->init();
$requestType = Yii::app()->request->requestType;
// on ajax request when ajax is not allowed
if (Yii::app()->request->isAjaxRequest && !$allowAjax)
{
header('Allow: ' . implode(', ', $validRequestTypes));
throw new CHttpException(400, 'Invalid request. Please do not repeat this request again. This URL can not handle ajax request.');
}
else if (!in_array($requestType, $validRequestTypes))
{
header('Allow: ' . implode(', ', $validRequestTypes));
throw new CHttpException(405, 'Method Not Allowed. This url can only handle the following request types: ' . implode(', ', $validRequestTypes));
}
$types = Yii::app()->request->acceptTypes;
if (isset($_GET['accept'])) {
$types = $_GET['accept'].'|'.urldecode($_GET['accept']); // support double encoded slashes
}
$type = self::HTML;
if (Yii::app()->request->isAjaxRequest && !isset($_GET['ajax']) || strpos($types, 'application/json') !== false) {
$type = self::JSON;
} elseif (strpos($types, 'text/csv') !== false) {
$type = self::CSV;
} elseif (strpos($types, 'text/calendar') !== false) {
$type = self::ICAL;
}
return $type;
}
/**
* Performs the AJAX validation.
* @param CModel the model to be validated
*/
protected function performAjaxValidation($model)
{
if (isset($_POST['ajax']) && $_POST['ajax']===strtolower($this->modelClass).'-form')
{
echo CActiveForm::validate($model);
Yii::app()->end();
}
}
/**
* Returns the data model based on the primary key given in the GET variable.
* If the data model is not found, an HTTP exception will be raised.
* @param mixed $id the ID of the model to be loaded
*/
public function loadModel($id, $refresh=false)
{
if ($this->model === null || $refresh)
{
$this->model = CActiveRecord::model($this->modelClass)->findByPk($id);
$this->onLoadModel($id);
if($this->model === null) {
throw new CHttpException(404, 'The requested page does not exist.');
}
}
return $this->model;
}
/**
* @param mixed $id the ID of the model to be loaded
*/
public function onLoadModel($id=null)
{
$event = new CEvent($this);
if ($id !== null) { // $id is null on create action
$event->params['id'] = $id;
}
$this->raiseEvent('onLoadModel', $event);
}
}
<?php
class MyRESTController extends Controller
{
public $defaultAction='admin';
public function actions()
{
return array(
'view' => array(
'class' => 'application.controllers.actions.RESTViewAction',
'modelClass' => 'MyModel',
),
'create' => array(
'class' => 'application.controllers.actions.RESTCreateAction',
'modelClass' => 'MyModel',
'onLoadModel' => function($event)
{ /** @var CEvent $event */
// prepare model
$event->sender->model->active = 1;
},
'onBeforeSave' => function($event)
{ /** @var CEvent $event */
// save projects
$event->sender->model->projects = Project::model()->findAllByPk(
isset($_POST['projects']) ? $_POST['projects'] : array()
);
$event->sender->model->creator = Yii::app()->user->model;
},
'onAfterSave' => function($event)
{ /** @var CEvent $event */
/** @var MyModel $task */
$model = $event->sender->model;
$model->refresh();
},
),
'update' => array(
'class' => 'application.controllers.actions.RESTUpdateAction',
'modelClass' => 'MyModel',
'onLoadModel' => function($event)
{ /** @var CEvent $event */
// ...
},
'onBeforeSave' => function($event)
{ /** @var CEvent $event */
// save projects
$event->sender->model->projects = Project::model()->findAllByPk(
isset($_POST['projects']) ? $_POST['projects'] : array()
);
},
),
'delete' => array(
'class' => 'application.controllers.actions.RESTDeleteAction',
'modelClass' => 'MyModel',
),
'admin' => array(
'class' => 'application.controllers.actions.RESTListAction',
'modelClass' => 'MyModel',
'view' => 'admin',
'gridColumns' => array(
'id',
'type.name',
'status.name',
'title',
'created',
'updated',
),
'onLoad' => function($event)
{ /** @var CEvent $event */
$model=new Task('search');
$model->unsetAttributes(); // clear any default values
if(isset($_GET['Task']))
$model->attributes=$_GET['Task'];
$event->sender->viewData['model'] = $model;
$event->sender->dataProvider = $model->search();
}
),
);
}
}
<?php
Yii::import('application.controllers.actions.BaseRESTAction');
/**
* Creates a new model.
* If creation is successful, the browser will be redirected to the 'view' page.
*/
class RESTCreateAction extends BaseRESTAction
{
public $view = 'create';
public $redirectAction = 'view';
public $redirectActionPk = 'id';
/**
* @var bool set this to false to avoid setting this action as return url
*/
public $rememberReturnUrl = false;
/**
* @var string|null text to display after successfull action.
* defaults to null, meaning no flash message.
* {link} will be replaced with a link.
*/
public $successFlashMessage = null;
/**
* @var bool run save in a transaction
*/
public $transactional = true;
/**
* @var CDbTransaction|null used transaction
*/
protected $transaction;
/**
* Creates a new model.
* If creation is successful, the browser will be redirected to the 'view' page.
* @raises CEvent onBeforeSave with parameter valid, set to false to cancel
* @raises CEvent onAfterSave
*/
public function run()
{
$type = $this->getResponseType(array('GET', 'PUT', 'POST'), true);
/** @var $model CActiveRecord */
$this->model = new $this->modelClass;
$this->onLoadModel();
// check if AJAX validation is requested
$this->performAjaxValidation($this->model);
$put = Yii::app()->request->isPutRequest;
$post = Yii::app()->request->isPostRequest;
$ajax = Yii::app()->request->isAjaxRequest;
if ($put || isset($_POST[$this->modelClass]))
{
$this->model->attributes = $put ? Yii::app()->request->getPut() : $_POST[$this->modelClass];
if ($this->transactional && $this->model->dbConnection->currentTransaction === null) {
$this->transaction = $this->model->dbConnection->beginTransaction();
}
try {
if ($this->onBeforeSave() && $this->model->save()) {
$this->onAfterSave();
if ($this->transaction !== null) {
$this->transaction->commit();
}
$pk = $this->model->getPrimaryKey();
if (is_array($pk)) {
$params = $pk; // @todo find a smarter way to support composite pks
} else {
$params = array($this->redirectActionPk => $pk);
}
switch(true) {
case $ajax:
header('Content-type: application/json');
echo CJSON::encode($this->model->attributes);
Yii::app()->end();
break;
case $put:
header('HTTP/1.0 201 Created ' . $this->modelClass);
header('Location: ' . $this->controller->createUrl($this->redirectAction, $params));
Yii::app()->end();
break;
default:
case $post:
if ($this->successFlashMessage !== null) {
Yii::app()->user->setFlash(
'success',
Yii::t('flash', $this->successFlashMessage, array(
'{link}' => CHtml::link($pk, $this->controller->createUrl($this->redirectAction, $params))
))
);
}
array_unshift($params, $this->redirectAction);
$this->controller->redirect($params);
break;
}
} else {
$this->onSaveFailed();
if ($this->transaction !== null) {
$this->transaction->rollback();
}
switch(true) {
case $ajax:
header('Content-type: application/json');
echo CJSON::encode($this->model->getErrors());
Yii::app()->end();
break;
case $put:
header('HTTP/1.0 409 Conflict. Unable to create ' . $this->modelClass);
header('Content-type: application/json');
echo CJSON::encode($this->model->getErrors());
Yii::app()->end();
break;
}
}
} catch(Exception $e) {
if ($this->transaction !== null) {
$this->transaction->rollback();
}
throw $e;
}
} elseif (($post || $ajax) && !isset($_POST[$this->modelClass])) {
throw new CHttpException(500, 'No Post Data found.'); // @todo consider invalid request instead of 500
}
$this->controller->render($this->view, array(
'model'=>$this->model,
));
}
public function onBeforeSave()
{
$this->raiseEvent('onBeforeSave', $event = new CEvent($this, array('valid'=>true)));
return $event->params['valid'];
}
public function onAfterSave()
{
$this->raiseEvent('onAfterSave', new CEvent($this));
}
public function onSaveFailed()
{
$this->raiseEvent('onSaveFailed', new CEvent($this));
}
}
<?php
Yii::import('application.controllers.actions.BaseRESTAction');
/**
* Deletes a particular model.
* If deletion is successful, the browser will be redirected to the 'index' page.
*/
class RESTDeleteAction extends BaseRESTAction
{
/**
* @var bool set this to false to avoid setting this action as return url
*/
public $rememberReturnUrl = false;
/**
* @var bool run save in a transaction
*/
public $transactional = true;
/**
* @var string|null text to display after successfull action.
* defaults to null, meaning no flash message.
* {id} will be replaced with the primary key.
*/
public $successFlashMessage = null;
/**
* @var CDbTransaction|null used transaction
*/
protected $transaction;
/**
* Deletes a particular model.
* If deletion is successful, the browser will be redirected to the 'index' page.
* @param integer $id the ID of the model to be deleted
*/
public function run($id)
{
$type = $this->getResponseType(array('DELETE', 'POST'), true);
$delete = Yii::app()->request->isDeleteRequest;
$post = Yii::app()->request->isPostRequest;
$ajax = Yii::app()->request->isAjaxRequest;
if ($delete || $post || $ajax)
{
$this->loadModel($id);
if ($this->transactional && $this->model->dbConnection->currentTransaction === null) {
$this->transaction = $this->model->dbConnection->beginTransaction();
}
try {
if ($this->onBeforeDelete() && $this->model->delete()) {
$this->onAfterDelete();
if ($this->transaction !== null) {
$this->transaction->commit();
}
switch(true) {
case $ajax:
header('Content-type: application/json');
echo CJSON::encode(array('status' => 'success'));
Yii::app()->end();
break;
case $delete:
header('HTTP/1.0 204 No Content. Deleted ' . $this->modelClass);
Yii::app()->end();
case $post:
if ($this->successFlashMessage !== null) {
Yii::app()->user->setFlash(
'success',
Yii::t('flash', $this->successFlashMessage, array(
'{id}' => $this->model->primaryKey
))
);
}
// @todo: if returnUrl leads to the view page, go one step back further, returnUrl needs to be a stack then
$this->controller->redirect(/*isset($this->returnUrl) ? $this->returnUrl : */array('admin'));
break;
}
}
else
{
if ($this->transaction !== null) {
$this->transaction->rollback();
}
switch(true) {
case $ajax:
case $delete:
header('HTTP/1.0 409 Conflict. Unable to delete ' . $this->modelClass);
header('Content-type: application/json');
echo CJSON::encode(array('status' => 'failed', 'errors'=>array())); // @todo: add errors here
Yii::app()->end();
break;
default:
$message = Yii::t('flash', 'Failed to delete '.get_class($this->model).' {link}.', array(
'{link}' => CHtml::link($this->model->primaryKey, $this->controller->createUrl('view',array('id'=>$this->model->id)))
));
if ($this->model->hasErrors()) {
$message .= "<br />\n<ul>\n";
foreach($this->model->getErrors() as $errors) {
foreach($errors as $error) {
$message .= "<li>$error</li>\n";
}
}
$message .= '</ul>';
}
Yii::app()->user->setFlash('error', $message);
$this->controller->redirect(isset($this->returnUrl) ? $this->returnUrl : array('view','id'=>$this->model->id));
break;
}
}
} catch(Exception $e) {
if ($this->transaction !== null) {
$this->transaction->rollback();
}
throw $e;
}
}
throw new CHttpException(401, 'Invalid Request. Has to be POST or DELETE.');
}
public function onBeforeDelete()
{
$this->raiseEvent('onBeforeDelete', $event = new CEvent($this, array('valid'=>true)));
return $event->params['valid'];
}
public function onAfterDelete()
{
$this->raiseEvent('onAfterDelete', new CEvent($this));
}
}
<?php
Yii::import('application.controllers.actions.BaseRESTAction');
/**
*
*/
class RESTListAction extends BaseRESTAction
{
public $view = 'list';
public $viewData = array();
/**
* @var array column definitions for {@see CGridView}
*/
public $gridColumns = array();
/**
* @var array
*/
public $dataProviderConfig = array();
/**
* @var CDataProvider|IDataProvider
*/
public $dataProvider;
/**
* Displays a particular model.
* @param mixed $id the ID of the model to be displayed
*/
public function run()
{
$type = $this->getResponseType();
$this->onLoad();
if ($this->dataProvider === null) {
if ($this->modelClass === null) {
throw new CException('RESTListAction needs modelClass property configured.');
}
$this->dataProvider = new CActiveDataProvider($this->modelClass, $this->dataProviderConfig);
}
switch($type)
{
case BaseRESTAction::CSV:
Yii::import('application.widgets.grid.CSVGridView');
if ($this->dataProvider->canSetProperty('pagination')) {
$this->dataProvider->pagination = false;
}
$grid = new CSVGridView();
$grid->dataProvider = $this->dataProvider;
$grid->columns = array_merge(array('id'), $this->gridColumns);
$grid->init();
Yii::app()->request->sendFile($this->modelClass.'_'.date('Y-m-d').'.csv', $grid->renderItems(true), 'text/csv'); // will exit application
break;
case BaseRESTAction::ICAL:
if ($this->dataProvider->canSetProperty('pagination')) {
$this->dataProvider->pagination = false;
}
$dates = array();
if ($this->dataProvider instanceof CActiveDataProvider && isset($this->dataProvider->model->metaData->relations['dates'])) {
foreach($this->dataProvider->getData() as $data) {
$dates = array_merge($dates, $data->dates);
}
} elseif ($this->dataProvider instanceof CActiveDataProvider && $this->dataProvider->model instanceof Date) {
$dates = $this->dataProvider->getData();
} else {
throw new CHttpException(401, 'Bad Request, content is not available as ical(text/calendar).');
}
$ical = new ICalCalendar('layer5.de-cal'); // @todo create unique id per calendar
$ical->dates = $dates;
Yii::app()->request->sendFile($this->modelClass.'_'.date('Y-m-d').'.ics', $ical->render(), 'text/calendar'); // will exit application
//echo nl2br($ical->render());
exit;
break;
case BaseRESTAction::JSON:
if ($this->dataProvider->canSetProperty('pagination')) {
$this->dataProvider->pagination = false;
}
header('Content-type: application/json');
echo CJSON::encode($this->dataProvider->getData());
break;
case BaseRESTAction::HTML:
default:
$this->viewData['dataProvider'] = $this->dataProvider;
$this->controller->render($this->view, $this->viewData);
}
}
public function onLoad()
{
$this->raiseEvent('onLoad', new CEvent($this));
}
}
<?php
Yii::import('application.controllers.actions.BaseRESTAction');
/**
* Updates a particular model.
* If update is successful, the browser will be redirected to the 'view' page.
*/
class RESTUpdateAction extends BaseRESTAction
{
public $view = 'update';
/**
* @var bool set this to false to avoid setting this action as return url
*/
public $rememberReturnUrl = false;
/**
* @var string|null text to display after successfull action.
* defaults to null, meaning no flash message.
* {link} will be replaced with a link.
*/
public $successFlashMessage = null;
/**
* @var bool run save in a transaction
*/
public $transactional = true;
/**
* @var CDbTransaction|null used transaction
*/
protected $transaction;
/**
* Updates a particular model.
* If update is successful, the browser will be redirected to the 'view' page.
* @param integer $id the ID of the model to be updated
*/
public function run($id)
{
$type = $this->getResponseType(array('GET', 'PUT', 'POST'), true);
$this->model = $this->loadModel($id);
// Uncomment the following line if AJAX validation is needed
$this->performAjaxValidation($this->model);
$put = Yii::app()->request->isPutRequest;
$post = Yii::app()->request->isPostRequest;
$ajax = Yii::app()->request->isAjaxRequest;
if ($put || (($post || $ajax) && isset($_POST[$this->modelClass])))
{
$this->model->attributes = $put ? Yii::app()->request->getPut() : $_POST[$this->modelClass];
if ($this->transactional && $this->model->dbConnection->currentTransaction === null) {
$this->transaction = $this->model->dbConnection->beginTransaction();
}
try {
if ($this->onBeforeSave() && $this->model->save()) {
$this->onAfterSave();
if ($this->transaction !== null) {
$this->transaction->commit();
}
switch(true) {
case $ajax:
header('Content-type: application/json');
echo CJSON::encode($this->model->attributes);
Yii::app()->end();
break;
case $put:
header('HTTP/1.0 204 No Content. Updated ' . $this->modelClass);
header('Location: ' . $this->controller->createUrl('view', array('id'=>$this->model->id)));
Yii::app()->end();
case $post:
if ($this->successFlashMessage !== null) {
Yii::app()->user->setFlash(
'success',
Yii::t('flash', $this->successFlashMessage, array(
'{link}' => CHtml::link($this->model->primaryKey, $this->controller->createUrl('view',array('id'=>$this->model->id)))
))
);
}
$this->controller->redirect(isset($this->returnUrl) ? $this->returnUrl : array('view','id'=>$this->model->id));
break;
}
} else {
$this->onSaveFailed();
if ($this->transaction !== null) {
$this->transaction->rollback();
}
switch(true) {
case $ajax:
header('Content-type: application/json');
echo CJSON::encode($this->model->getErrors());
Yii::app()->end();
break;
case $put:
header('HTTP/1.0 409 Conflict. Unable to update ' . $this->modelClass);
header('Content-type: application/json');
echo CJSON::encode($this->model->getErrors());
Yii::app()->end();
break;
}
}
} catch(Exception $e) {
if ($this->transaction !== null) {
$this->transaction->rollback();
}
throw $e;
}
} elseif ($ajax && !isset($_POST[$this->modelClass])) {
throw new CHttpException(500, 'No Post Data for Ajax request.');
}
$this->controller->render($this->view, array(
'model'=>$this->model,
));
}
public function onBeforeSave()
{
$this->raiseEvent('onBeforeSave', $event = new CEvent($this, array('valid'=>true)));
return $event->params['valid'];
}
public function onAfterSave()
{
$this->raiseEvent('onAfterSave', new CEvent($this));
}
public function onSaveFailed()
{
$this->raiseEvent('onSaveFailed', new CEvent($this));
}
}
<?php
Yii::import('application.controllers.actions.BaseRESTAction');
/**
* Displays a particular model.
*/
class RESTViewAction extends BaseRESTAction
{
public $view = 'view';
public $viewData = array();
/**
* Displays a particular model.
* @param mixed $id the ID of the model to be displayed
*/
public function run($id)
{
$type = $this->getResponseType();
$model = $this->loadModel($id);
$this->onAfterLoadModel();
switch($type)
{
case BaseRESTAction::JSON:
header('Content-type: application/json');
echo CJSON::encode($model->attributes);
break;
case BaseRESTAction::HTML:
default:
$this->viewData['model'] = $model;
$this->controller->render($this->view, $this->viewData);
}
}
public function onAfterLoadModel()
{
$this->raiseEvent('onAfterLoadModel', new CEvent($this));
}
}
@yjeroen
Copy link

yjeroen commented Jun 21, 2013

You're turning this into an extension, cebe? :3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment