Shortly: how can you set a specific http error code, instead of a generic 500, when a constraint fails on entity save?
Details
I'm using Symfony custom constraint #UniqueEntity (http://symfony.com/doc/current/reference/constraints/UniqueEntity.html) to assert that some data is not duplicated when saving an entity.
If this constraint check results in a violation, I get a 500 http code, while others may be more appropriate, e.g. 409 - Conflict (https://httpstatuses.com/409).
I can't seem to find any documentation on how to override the validation response.
Thank you in advance for any suggestion.
Maybe you could create a Listener to the event : kernel.exception
And then you will have something like :
<?php
public function onKernelException(GetResponseForExceptionEvent $event)
{
$e = $event->getException();
if ($e instanceof NameOfTheException) {
// logic here
return (new Response())
->setStatusCode(409)
;
}
}
Just catch exception in controller:
public function saveAction()
{
try {
$entity = new Entity('duplicate name');
$this->entityManager->persist($entity);
$this->entityManager->flush();
return new Response();
} catch(UniqueConstraintViolationException $e) {
return new Response('Entity with same name already exists', Response::HTTP_CONFLICT);
} catch (\Exception $e) {
return new Response('Internal error', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
Related
I'm trying to use Laravel API Resource and handle the error message by sending a specific HTTP code
Here is my code :
public function show($id)
{
try {
return FruitResource::make(Fruit::find($id));
}
catch(Exception $e)
{
throw new HttpException(500, 'My custom error message');
}
}
My try/catch is systematically ignored when I try to access the route.
I am voluntarily accessing an object that is not in the database. I have ErrorException with message Trying to get property 'id' of non-object.
I would like to be able to send my own Exception here, in case the user tries to access data that doesn't exist. And return a json error.
Try this (notice the \ before Exception):
public function show($id)
{
try {
return FruitResource::make(Fruit::find($id));
}
catch(\Exception $e)
{
throw new HttpException(500, 'My custom error message');
}
}
I am trying to return a custom exception message using Laravel's ValidationException class. I have it successfully working in the following example:
public function store(Request $request)
{
$this->validate($request, CurrencyValidatorArrays::$store);
try {
$this->currenciesInstance->createOrUpdateCurrency($request->all());
return redirect()->route('currencies.index')
->with('success', 'Successfully created currency');
} catch (Exception $e) {
return redirect()->route('currencies.create')
->with('error', $e->getMessage());
}
}
where the exception is thrown from within createOrUpdateCurrency()
if(Currency::where('position', $data['position'])->where('id', '!=', $id)->exists()) {
throw ValidationException::withMessages([
'error' => ['Position value is already taken']
]);
}
This then passes the exception message back to my view for display.
However, I am trying to implement this somewhere else when trying to delete a company I am checking that no users exist first:
public function destroy($id)
{
try {
$this->companiesInstance->deleteCompany($id);
return redirect()->route('companies.index')
->with('success', 'Successfully deleted company');
} catch (Exception $e) {
return redirect()->route('companies.index')
->with('error', $e->getMessage());
}
}
Inside of deleteCompany():
$company = Company::find($id);
if($company->users()->exists()){
throw ValidationException::withMessages([
'error' => ['Position value is already taken']
]);
}
For some reason this doesn't hit my catch block from the main destroy method,if I change the exception type to ValidationException from Exception I can access and see the exception object but not in the way I can in the Store() methods version. Any ideas what is going on here?
UPDATE:
So I have a some what of a workaround:
To get by the exception type issue I used:
catch (ValidationException | Exception $e) { ...
But that still does not help when accessing the exception messages as when it's of type ValidationException the default constructor validation message is returned from getMessage() and not my specified one.
PHP 7 handles exceptions a bit differently. You can read it here.
Coming back to your code, you have to try and catch object of Throwable instead of Exception.
try
{
// Code that may throw an Exception or Error.
}
catch (Throwable $t)
{
// Code that handles the error
}
Hope this helps.
So I am messing around with symfony router component and I created a small wrapper.
One thing that came up was how do I get a request to throw a 500 in unit tests? The method in question is:
public function processRoutes(Request $request) {
try {
$request->attributes->add($this->_matcher->match($request->getPathInfo()));
return call_user_func_array($request->attributes->get('callback'), array($request));
} catch (ResourceNotFoundException $e) {
return new RedirectResponse('/404', 302);
} catch (Exception $e) {
return new RedirectResponse('/500', 302);
}
}
And the test in question is:
public function testFiveHundred() {
$router = new Router();
$router->get('/foo/{bar}', 'foo', function($request){
return 'hello ' . $request->attributes->get('bar');
});
$response = $router->processRoutes(Request::create('/foo/bar', 'GET'));
$this->assertEquals(500, $response->getStatusCode());
}
Right now the test will fail because we are defined and the status code will be 200. Is there something special I can do to the Request object I create, to make it throw a 500?
I think you got several options here you can play with:
Decide that a specific path will always throw an exception.
This will force you to make some changes in your code.
public function processRoutes(Request $request) {
...
if ($request->getRequestUri() == '/path/that/throws/exception') {
throw Exception('Forced to throw exception by URL');
}
...
}
public function testFiveHundred() {
...
$response = $router->processRoutes(Request::create('/path/that/throws/exception', 'GET'));
...
}
Make a DummyRequest object that will extends your original Request class and make sure this object will raise an Exception (for example - you know for sure that you use the getPathInfo(), so you can use this).
class DummyRequest extends Request {
public function getPathInfo() {
throw new Exception('This dummy request object should only throw an exception so we can test our routes for problems');
}
}
public function testFiveHundred() {
...
$dummyRequest = new DummyRequest();
$response = $router->processRoutes($dummyRequest);
...
}
Since the function getRequestUri of our $dummyRequest throws an exception, your call to $router->processRoutes will have our dummy to throw that exception.
This is a general idea, you would probably need to play a bit with the namespaces and the functions there (I didn't test it, however this should work).
To explicitly bind a parameter to a model and throwing a custom exception I have to (among other things) add the following to the RouteServiceProvider:
$router->model('parameter', 'App\Model', function () {
throw new CustomNotFoundException;
});
To customize the resolution logic of the explicit binding I have to add the following to the RouteServiceProvider:
$router->bind('parameter', function ($parameter) {
return App\Model::where('field', $parameter)->first();
});
My problem is that I need both but apparently can't. If I bind the parameter to the model and also customize the resolution logic it won't throw my CustomNotFoundException, instead it will throw the default ModelNotFoundException.
To clearly summarize my objective: I want to customize the resolution logic and throw a custom exception if it fails to find a record.
EDIT I have tried this as per a suggestion from #Maraboc:
$router->bind('parameter', function ($parameter) {
try {
return App\Model::where('field' => $parameter)->first();
} catch (Exception $e) {
throw new CustomNotFoundException;
}
});
This will still throw a ModelNotFoundException for a reason I'm not aware.
Try it like this :
$router->bind('parameter', function ($parameter) {
try {
return App\Model::where('field' => $parameter)->firstOrFail();
} catch (ModelNotFoundException $e) {
throw new CustomNotFoundException;
}
});
OR in the App\Exceptions\Handler add this to the render method :
if ($e instanceof ModelNotFoundException) {
throw new CustomNotFoundException;
}
And use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;
OR an other workaround is to do like so :
$router->bind('parameter', function ($parameter) {
$model = App\Model::where('field' => $parameter)->first();
if ( ! $model) {
throw new CustomNotFoundException;
}
return $model;
});
I was curious if there is a way we can check if there is a constraint violation error when delete or insert a record into the database.
The exception thrown is called 'QueryException' but this can be a wide range of errors. Would be nice if we can check in the exception what the specific error is.
You are looking for the 23000 Error code (Integrity Constraint Violation). If you take a look at QueryException class, it extends from PDOException, so you can access to $errorInfo variable.
To catch this error, you may try:
try {
// ...
} catch (\Illuminate\Database\QueryException $e) {
var_dump($e->errorInfo);
}
// Example output from MySQL
array (size=3)
0 => string '23000' (length=5)
1 => int 1452
2 => string 'Cannot add or update a child row: a foreign key constraint fails (...)'
To be more specific (Duplicate entry, not null, add/update child row, delete parent row...), it depends on each DBMS:
PostgreSQL and SQL server follow the SQL standard's conventions for SQLSTATE code, so you may return the first value from the array $e->errorInfo[0] or call $e->getCode() directly
MySQL, MariaDB and SQLite do not strictly obey the rules, so you need to return the second value from the array $e->errorInfo[1]
For laravel, handling errors is easy, just add this code in your "app/start/global.php" file ( or create a service provider):
App::error(function(\Illuminate\Database\QueryException $exception)
{
$error = $exception->errorInfo;
// add your business logic
});
first put this in your controller
use Exception;
second handle the error by using try catch like this example
try{ //here trying to update email and phone in db which are unique values
DB::table('users')
->where('role_id',1)
->update($edit);
return redirect("admin/update_profile")
->with('update','update');
}catch(Exception $e){
//if email or phone exist before in db redirect with error messages
return redirect()->back()->with('phone_email','phone_email_exist before');
}
New updates here without need to use try catch you can easily do that in validation rules as the following code blew
public function update(Request $request, $id)
{
$profile = request()->all();
$rules = [
'name' => 'required|unique:users,id,'.$id,
'email' => 'required|email|unique:users,id,'.$id,
'phone' => 'required|unique:users,id,'.$id,
];
$validator = Validator::make($profile,$rules);
if ($validator->fails()){
return redirect()->back()->withInput($profile)->withErrors($validator);
}else{
if(!empty($profile['password'])){
$save['password'] = bcrypt($profile['password']);
}
$save['name'] = $profile['name'];
$save['email'] = $profile['email'];
$save['phone'] = $profile['phone'];
$save['remember_token'] = $profile['_token'];
$save['updated_at'] = Carbon::now();
DB::table('users')->where('id',$id)->update($save);
return redirect()->back()->with('update','update');
}
}
where id related to record which you edit.
You may also try
try {
...
} catch ( \Exception $e) {
var_dump($e->errorInfo );
}
then look for error code.
This catches all exception including QueryException
If you are using Laravel version 5 and want global exception handling of specific cases you should put your code in the report method of the /app/Exception/Handler.php file. Here is an example of how we do it in one of our micro services:
public function render($request, Exception $e)
{
$response = app()->make(\App\Support\Response::class);
$details = $this->details($e);
$shouldRenderHttp = $details['statusCode'] >= 500 && config('app.env') !== 'production';
if($shouldRenderHttp) {
return parent::render($request, $e);
}
return $response->setStatusCode($details['statusCode'])->withMessage($details['message']);
}
protected function details(Exception $e) : array
{
// We will give Error 500 if we cannot detect the error from the exception
$statusCode = 500;
$message = $e->getMessage();
if (method_exists($e, 'getStatusCode')) { // Not all Exceptions have a http status code
$statusCode = $e->getStatusCode();
}
if($e instanceof ModelNotFoundException) {
$statusCode = 404;
}
else if($e instanceof QueryException) {
$statusCode = 406;
$integrityConstraintViolation = 1451;
if ($e->errorInfo[1] == $integrityConstraintViolation) {
$message = "Cannot proceed with query, it is referenced by other records in the database.";
\Log::info($e->errorInfo[2]);
}
else {
$message = 'Could not execute query: ' . $e->errorInfo[2];
\Log::error($message);
}
}
elseif ($e instanceof NotFoundHttpException) {
$message = "Url does not exist.";
}
return compact('statusCode', 'message');
}
The Response class we use is a simple wrapper of Symfony\Component\HttpFoundation\Response as HttpResponse which returns HTTP responses in a way that better suits us.
Have a look at the documentation, it is straightforward.
You can add the following code in app/start/global.php file in order to print the exception
App::error(function(QueryException $exception)
{
print_r($exception->getMessage());
});
check this part in the documentation