Just wanted to add some tips on using exceptions that I’ve found very helpful while developing a REST API for internal use. The primary use case for interacting with the API is through Ajax requests - sending a POST or GET request and returning JSON - and I wanted a streamlined approach to handling and routing errors.
(Note: I get a lot of ideas from the folks at APIgee.)
These exception tips are also useful outside the API context as well for controlling application flow.
Custom CHttpException Base Class
I created a custom HttpException subclass that contains methods specifying the type of information I want available when presenting errors to developers or users, namely:
-
$message: The message for end users
-
$developer: the message for developers (more technical)
-
$statusCode: the relevant Http status code
-
$statusText: the relevant Http status text, e.g. "Forbidden" for 403
-
$infoUrl: a URL the developer can visit for more info about the error
-
$code: the internal classification code for the error
My most common use case for interacting with the API is through AJAX, in which case I usually specify done() and fail() handlers. This info usually contains all I need to easily relay specific information back to both end users and developers.
Common Sub-Classes
-
Http400Exception - For input/parameter errors
-
Http401Exception - for authentication errors
-
Http403Exception - for not-permitted errors
-
Http404Exception - when a specified resource is not found but the request is otherwise valid
-
Http500Exception - for unspecified internal errors
These provider shorthand access to the most common error types and enable me to subclass them for further customization (see below).
Model-Specific Exceptions
-
ValidationException - for when a model fails to validate (extends Http400Exception)
-
SaveException - for when an otherwise valid model fails to save to the database or execute a desired action (extends Http500Exception)
These special exceptions take a CModel instance in their constructor that references the model related to the error, allowing me to use the exception to determine what errors occurred, if any.
Example API Action
With these exceptions, my API Actions end up initializing a model, asking it to do things, and throwing exceptions whenever un-recoverable errors occur. It makes for little reduced visibility at first, but I like that 1) it’s clear once you dive in; 2) it’s easy to debug; and 3) it keeps all but presentation and routing logic out of the controller.
And after much trial and error, I find this easier and more maintainable than using exceptions in Models. I prefer having the controller ask the model to do stuff, see if it worked (true/false), handle that response, and then relay messages to the user (e.g. by asking the model for the errors that occurred).
public function actionUpdate($id, $suppress_status_code=false) {
try {
$model = MyModel::findByPk($id);
if ($model===null)
throw new Http404Exception("Model #{$id} was not found in the database");
elseif (!authManager()->checkAccess('MyModel.update', user()->id, array('modelId' =>$id)))
throw new Http403Exception("You are not authorized to update Model #{$id}");
elseif (!isset($_POST['MyModel']))
throw new Http400Exception("Your request could not be completed because we could not tell what attributes you wanted to update", 'Specify Model attributes using the $_POST["Model"]');
else {
$model->setAttributes($_POST['MyModel']);
if (!$model->validate())
throw new ValidationException($model);
elseif (!$model->save(false))
throw new SaveException($model);
else {
header("HTTP/1.0 200 OK");
header('Content-type: text/json');
echo CJSON::encode(array(
"statusCode" =>200,
"statusText" =>"OK",
"message" =>"Model #{$id} was successfully updated!",
"developer" =>null,
"infoUrl" =>null,
));
Yii::app()->end();
}
}
} catch (HttpException $e) {
$statusHeader = $suppress_status_code
? "HTTP/1.0 200 OK"
: "HTTP/1.0 {$e->statusCode} {$e->getStatusText()}";
header($statusHeader);
header('Content-type: text/json');
echo $e->getJsonMessage();
Yii::app()->end();
}
}