Reputation: 199
I'm building a JSON RESTful API with CakePHP3, but I'm not sure what is the best approach to handle errors and give the client information about the error. My approach so far is to throw a HttpException if (for example) saving an entity fails due to validation errors.
In my controller I have the following:
if (!$this->Categories->save($categoryEntity)) {
throw new InternalErrorException('Saving failed');
}
$this->set('status', 'Everything fine!');
$this->set('_serialize', true);
If the saving fails, the exception is serialized to json and the response looks like this:
{
"message": "Saving failed",
"url": "\/categories\/edit",
"code": 500,
}
Now I want to include some more detailed information about the error. For example something like this:
{
"message": "Saving failed",
"errors": "Validation error: Field id has to be numeric"
"url": "\/categories\/edit",
"code": 500,
}
I've tried it with an extended HttpException that took the errors as an extra parameter but that extra parameter is not serialized. How can I include some extra information into the exception or how can I change the serialization behavior of an exception in CakePHP3?
Upvotes: 2
Views: 1976
Reputation: 60463
The serialized view variables for exceptions are hard coded in the exception renderer, you'd have to create a custom/extended one that handles your custom exception, so that it can pick up the additional data that it provides.
Here's a quick & dirty example, using a custom exception named ValidationErrorException
(InternalErrorException
is already used by the CakePHP core), which extends \Cake\Http\Exception\HttpException
, and implements a getValidationErrors()
method that returns the validation errors:
// in src/Error/Exception/ValidationErrorException.php
namespace App\Error\Exception;
use Cake\Datasource\EntityInterface;
use Cake\Http\Exception\HttpException;
class ValidationErrorException extends HttpException
{
protected $_validationErrors;
public function __construct(EntityInterface $entity, $message = null, $code = 422)
{
$this->_validationErrors = $entity->getErrors();
if ($message === null) {
$message = 'A validation error occurred.';
}
parent::__construct($message, $code);
}
public function getValidationErrors()
{
return $this->_validationErrors;
}
}
Such a HTTP exception will map to a exception renderer class method with a matching name:
// in src/Error/AppExceptionRenderer.php
namespace App\Error;
use App\Error\Exception\ValidationErrorException;
use Cake\Error\ExceptionRenderer;
class AppExceptionRenderer extends ExceptionRenderer
{
// HttpExceptions automatically map to methods matching the inflected variable name
public function validationError(ValidationErrorException $exception)
{
$code = $this->_code($exception);
$method = $this->_method($exception);
$template = $this->_template($exception, $method, $code);
$message = $this->_message($exception, $code);
$url = $this->controller->request->getRequestTarget();
$response = $this->controller->getResponse();
foreach ((array)$exception->responseHeader() as $key => $value) {
$response = $response->withHeader($key, $value);
}
$this->controller->setResponse($response->withStatus($code));
$viewVars = [
'message' => $message,
'url' => h($url),
'error' => $exception,
'code' => $code,
// set the errors as a view variable
'errors' => $exception->getValidationErrors(),
'_serialize' => [
'message',
'url',
'code',
'errors' // mark the variable as to be serialized
]
];
$this->controller->set($viewVars);
return $this->_outputMessage($template);
}
}
In your controller you can then throw it like this, feeding it with the entity that failed validation:
if (!$this->Categories->save($categoryEntity)) {
throw new \App\Error\Exception\ValidationErrorException($categoryEntity);
}
See also
Upvotes: 4