With regards to architecture, which of the two is a good practice when throwing exceptions from model to controller?
Structure A:
UserController.php
public function updateUserInfo(UserInfoRequest $request, UserModel $userModel)
{
$isError = false;
$message = 'Success';
try {
$message = $userModel->updateUserInfo($request->only(['username', 'password']));
} catch (SomeCustomException $e) {
$isError = true;
$message = $e->getMessage();
}
return json_encode([
'isError' => $isError,
'message' => $message
]);
}
UserModel.php
public function updateUserInfo($request)
{
$isError = false;
$message = 'Success';
$username = $request['username'];
$password = $request['password'];
try {
$this->connect()->beginTransaction();
$this->connect()->table('users')->where('username', $username)->update(['password' => $password]);
$this->connect()->commit();
} catch (\Exception $e) {
$this->connect()->rollback();
$isError = true;
$message = $e->getMessage();
}
return [
'isError' => $isError,
'message' => $message
];
}
Structure B:
UserController.php
public function updateUserInfo(UserInfoRequest $request, UserModel $userModel)
{
$isError = false;
$message = 'Success';
try {
$userModel->updateUserInfo($request->only(['username', 'password']));
} catch (SomeCustomException $e) {
$isError = true;
$message = $e->getMessage();
} catch (QueryException $e) {
$isError = true;
$message = $e->getMessage();
}
return json_encode([
'isError' => $isError,
'message' => $message
]);
}
UserModel.php
public function updateUserInfo($request)
{
$username = $request['username'];
$password = $request['password'];
try {
$this->connect()->beginTransaction();
$this->connect()->table('users')->where('username', $username)->update(['password' => $password]);
$this->connect()->commit();
} catch (\Exception $e) {
$this->connect()->rollback();
throw new QueryException();
}
}
In Structure A, the model catches any exception, rollback the transaction and return if it has an error or none to the controller. The controller then just return whatever is returned from the model.
While in Structure B the model catches any exception, rollback the transaction then throw a QueryException if an exception occurred. The controller then catches the thrown QueryException from the model then the return if it has an error or none.
The reason why Structure B still have a catch is that the model should be the one to do the rollback. If I were to remove the try-catch on the model here and the controller to directly catch an exception, then the rollback will be handled on the controller which I think kind of clutters the functionality of the controller.
Let me know your thoughts.
Thanks!
Why I think the approach from B is better:
Your Model should only include the logical part: This includes the communication with the database (transaction and rollback), not the formatting for the error message you want to print to the user.
Keep your model clean: It's the most important part of the MVC-structure. If you mess it up it will be very difficult to find any errors.
Outsourcing the error-handling: if you put it in the controller you have the choice to handle it there (maybe you want some special formatted output for this method or you need some other functions to call) or you handle it in the App\Exceptions\Handler. In this case you can render this error message here and don't have to do it in the controller.
So if you don't need any special function calls and want to use the full power of Laravel I would suggest you Structure C
UserController.php
public function updateUserInfo(UserInfoRequest $request, UserModel $userModel)
{
$userModel->updateUserInfo($request->only(['username', 'password']));
return response()->json(['message' => 'updated user.']);
}
UserModel.php
public function updateUserInfo($request)
{
$username = $request['username'];
$password = $request['password'];
try {
$this->connect()->beginTransaction();
$this->connect()->table('users')->where('username', $username)->update(['password' => $password]);
$this->connect()->commit();
} catch (\Exception $e) {
$this->connect()->rollback();
throw new QueryException();
}
}
App\Exceptions\Handler
public function render($request, Exception $exception)
{
//catch everything what you want
if ($exception instanceof CustomException) {
return response()->json([
'message' => $exception->getMessage()
], 422);
}
return parent::render($request, $exception);
}
You have a clean separation of the Database stuff (Model), the presentation stuff (Controller) and the error handling (Handler). Structure C allows you to reuse the error-handling in other functions where you have the same situation in another controller function.
This is my opinion but I am open to discuss about any scenario where you think this approach isn't the best solution.
First of all, for your example, you don't even need to use Transaction. You are performing just one query. so why do you need to rollback? Which query you want to rollback? A transaction should be used when you need a set of changes to be processed completely to consider the operation complete and valid. If the first one is successful, but any of the following has any error, you can rollback everything as if nothing was ever done.
Secondly, Lets come to the point good practice or best practice. Laravel suggest Thin controller and Thick model. So all of your business logic should be in model or even better in a repository. Controller will be act as a broker. It will collect data from repository or model and pass it to view.
Alternately, laravel provides some nice and convenient way to organize your codes. You can use Event and Observers for concurrent operation in your model.
Best practice is varies based on the knowledge and experience of the users. So who knows, the best answer for your question is yet to come.
I'd rather keep the controllers and any other part of the system that interacts with the models, as agnostic as possible about the inner workings of the model. So for instance I'd try to avoid being aware of QueryExceptions outside of the model and instead treat it as a plain PHP object whenever possible.
Also I'd avoid the custom JSON response structure and use HTTP statuses. If it makes sense, maybe the route for updating the user info returns the updated resource, or maybe a 200 OK is enough.
// UserModel.php
public function updateUserInfo($request)
{
$username = $request['username'];
$password = $request['password'];
try {
$this->connect()->beginTransaction();
$this->connect()->table('users')->where('username', $username)->update(['password' => $password]);
$this->connect()->commit();
return $this->connect()->table('users')->where('username', $username)->first();
// or just return true;
} catch (\Exception $e) {
$this->connect()->rollback();
return false;
}
}
// UserController.php
public function updateUserInfo(UserInfoRequest $request, UserModel $userModel)
{
$updated = $userModel->updateUserInfo($request->only(['username', 'password']));
if ($updated) {
return response($updated);
// HTTP 200 response. Returns JSON of updated user.
// Alternatively,
// return response('');
// (200 OK response, no content)
} else {
return response('optional message', 422);
// 422 or any other status code that makes more sense in the situation.
}
(Completely off-topic, I guess this is an example, but just in case, a reminder not to store plain text passwords.)
i don't understand, why you not watched Jeffry lesson, but for updating user you don't need try/catch section.
you controller method:
public function update(UpdateUserRequest $request, User $user) : JsonResponse
{
return response()->json($user->update($request->all()))
}
you request rules method:
public function rules(): array
{
return [
'username' => 'required|string',
'password' => 'required|min:6|confirmed',
];
}
And you Exception Handler render method:
public function render($request, Exception $exception)
{
if ($request->ajax() || $request->wantsJson()) {
$exception = $this->prepareException($exception);
if ($exception instanceof \Illuminate\Http\Exception\HttpResponseException) {
return $exception->getResponse();
} elseif ($exception instanceof \Illuminate\Auth\AuthenticationException) {
return $this->unauthenticated($request, $exception);
} elseif ($exception instanceof \Illuminate\Validation\ValidationException) {
return $this->convertValidationExceptionToResponse($exception, $request);
}
// we prepare custom response for other situation such as modelnotfound
$response = [];
$response['error'] = $exception->getMessage();
if (config('app.debug')) {
$response['trace'] = $exception->getTrace();
$response['code'] = $exception->getCode();
}
// we look for assigned status code if there isn't we assign 500
$statusCode = method_exists($exception, 'getStatusCode')
? $exception->getStatusCode()
: 500;
return response()->json($response, $statusCode);
}
return parent::render($request, $exception);
}
Now, if you have Exception, Laravel give you in Json with status code != 200, else give success result!
Related
I'm doing policy authorization in CakePHP. For all CRUD methods, I have to test if the user has the right to execute them. So I create that method to use in all the methods:
Code in SchoolsController
private function authorize(School $s){
try{
$this->Authorization->authorize($s);
} catch(ForbiddenException $e){
$this->Flash->error("You don't have permission.");
return $this->redirect(['controller' => 'Schools', 'action' => 'index']);
}
}
I'm testing the code for a user who wouldn't have permission. This should work but the code after calling this method is still called.
public function delete($id = null) {
$school = $this->Schools->get($id);
$this->authorize($school);
$this->request->allowMethod(['post', 'delete']);
if ($this->Schools->delete($school)) {
$this->Flash->success(__("School has been successfully removed."));
} else {
$this->Flash->error(__("The school could not be deleted. Please try again."));
}
return $this->redirect(['action' => 'index']);
}
I'm redirected and I get the two messages:
"You don't have permission."
"School has been successfully removed."
Here my SchoolPolicy
public function canDelete(IdentityInterface $user, School $school)
{
return $this->isAuthor($user,$school);
}
protected function isAuthor(IdentityInterface $user, School $school)
{
return $school->userId === $user->id;
}
If you catch an exception, then it will of course not halt execution, that's the whole point of catching it. And if you then return a value from your method (Controller::redirect() will return the response object with the Location header configured accordingly), you'll need to do something with that value, otherwise it will just vanish into the void, so for example:
$response = $this->authorize($school);
if ($response) {
return $response;
}
It's a bit hidden in the docs, but the easier approach would be to throw a redirect exception from your authorize() method. Also, if you do not actually make any use of the forbidden exception and the information that it holds, then you could simply use the can() method, which returns a boolean, eg:
if (!$this->Authorization->can($s)) {
$this->Flash->error("You don't have permission.");
throw new \Cake\Http\Exception\RedirectException(
\Cake\Routing\Router::url([
'controller' => 'Schools',
'action' => 'index',
])
);
}
You may also want to consider using a custom unauthorized handler instead.
I am trying to use Service-Repository pattern for the first time in my Laravel app and I have to handle a situation: a record can´t be added to a table (for example because trying to add same unique email....).
I have CustomAuthCOntroller with customRegistration method:
//Validatting request
$request->validated();
//running logic behind this in service
$is_Registered = $this->loginService->register_user(
$request->get('email'),
$request->get('password'),
$request->get('name'),
$request->get('surname')
);
// If everything goes right shol login page
if ($is_Registered) {
return redirect("/login")->withSuccess('Registrácia prebehla úspešne');
} else {
// Else HERE I HAVE TO SHOW what error happened
return redirect("/register")->withError('Registrácia prebehla úspešne');
}
My LoginService->register_user:
public function register_user($email, $password, $firstname, $lastname, $role = 'student') {
$user_attributes = [
'email' => $email,
'password' => $password,
'name' => $firstname,
'surname' => $lastname,
];
$this->userRepositor->create_new_user($user_attributes);
}
And my userRepository->create_new_user:
public function create_new_user(Array $attributes) : bool
{
DB::beginTransaction();
try {
User::create($attributes);
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
}
}
Is this right way how to achieve my goal? How can I show the error in my view if a exception is thrown?
You should not have any problems as you should be validating your input, so if the email/username or whatever that should be unique does already exist, then the validation should fail and return automatically with the errors (no try/catch).
If, you still want to catch any other possible error, let's say the DB timed out because of a connection problem or whatever (something not related to data), then you could either have a general try/catch (as I will show you) or do not catch it at all and let it go to the user, but this last approach depends a lot in your frontend:
If you have an API, then JS should handle this and show the error correctly.
If this is not an API (no AJAX), then the error could be shown to the user as a 500 error (0% descriptive for the user and would be a blank page with a 500 on it).
So, if you want the try/catch approach, you could do this:
//Validatting request
$request->validated();
try {
//running logic behind this in service
$this->loginService->register_user(
$request->get('email'),
$request->get('password'),
$request->get('name'),
$request->get('surname')
);
// If everything goes right shol login page
return redirect("/login")->withSuccess('Registrácia prebehla úspešne');
} catch (Throwable $exception) {
// HERE I HAVE TO SHOW what error happened
// You can even add the exception error with $exception->getMessage()
return redirect("/register")->withError('Registrácia prebehla úspešne');
}
And then, change your create_new_user to either of these:
public function create_new_user(Array $attributes) : bool
{
DB::beginTransaction();
try {
User::create($attributes);
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
Or use DB::transaction(). It will automatically rollback and throw the exception. See the source code here and here.
public function create_new_user(Array $attributes) : bool
{
DB::transaction(function () use ($attributes) {
User::create($attributes);
});
}
One personal recommendatation, do not declare methods as kebab_case like create_new_user, follow the PSR standard and Laravel standard, it should be camelCase: createNewUser.
I'm working on a fairly large JSON API using Slim3. My controllers/actions are currently littered with the following:
return $response->withJson([
'status' => 'error',
'data' => null,
'message' => 'Username or password was incorrect'
]);
At certain points in the application anything can go wrong and the response needs to be appropriate. But one thing that is common is the error responses are always the same. The status is always error, the data is optional (in the case of form validation errors data will contain those) and message is set to indicate to the user or consumer of the API what went wrong.
I smell code duplication. How can I reduce the code duplication?
From the top of my head all I could think of doing was creating a custom exception, something like App\Exceptions\AppException that takes option data and the message will be obtained form $e->getMessage().
<?php
namespace App\Exceptions;
class AppException extends Exception
{
private $data;
public function __construct($message, $data = null, $code = 0, $previous = null)
{
$this->data = $data;
parent::__construct($message, $code, $previous);
}
public function getData()
{
return $this->data;
}
}
Following that create middleware that calls $next wrapped in a try/catch:
$app->add(function($request, $response, $next) {
try {
return $next($request, $response);
}
catch(\App\Exceptions\AppException $e)
{
$container->Logger->addCritical('Application Error: ' . $e->getMessage());
return $response->withJson([
'status' => 'error',
'data' => $e->getData(),
'message' => $e->getMessage()
]);
}
catch(\Exception $e)
{
$container->Logger->addCritical('Unhandled Exception: ' . $e->getMessage());
$container->SMSService->send(getenv('ADMIN_MOBILE'), "Shit has hit the fan! Run to your computer and check the error logs. Beep. Boop.");
return $response->withJson([
'status' => 'error',
'data' => null,
'message' => 'It is not possible to perform this action right now'
]);
}
});
Now all I have to do at points in the code is to throw new \App\Exceptions\AppException("Username or password incorrect", null).
My only issue with this is it feels like I'm using exceptions for the wrong reasons and it may make debugging a little more difficult.
Any suggestions on reducing the duplicates and cleaning up error responses?
You can achieve similar similar results by creating an error handler which outputs JSON.
namespace Slim\Handlers;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
final class ApiError extends \Slim\Handlers\Error
{
public function __invoke(Request $request, Response $response, \Exception $exception)
{
$status = $exception->getCode() ?: 500;
$data = [
"status" => "error",
"message" => $exception->getMessage(),
];
$body = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
return $response
->withStatus($status)
->withHeader("Content-type", "application/json")
->write($body);
}
}
You must also configure Slim to use your custom error handler.
$container = $app->getContainer();
$container["errorHandler"] = function ($container) {
return new Slim\Handlers\ApiError;
};
Check Slim API Skeleton for example implemention.
I just follow some tutorial and so far what I do is :
my App/Exceptions/Handler.php
<?php
...
use Illuminate\Database\Eloquent\ModelNotFoundException;
...
public function render($request, Exception $e)
{
if ($e instanceof ModelNotFoundException){
abort(404);
}
return parent::render($request, $e);
}
and my UsersController looks like this :
...
public function edit($id)
{
$data = User::findOrFail($id);
$roles = Role::where('title', '!=', 'Super Admin')->get();
return View('admin.user.edit', compact(['data', 'roles']));
}
...
with the above code if I visit http://my.url/users/10/edit I get NotFoundHttpException in Application.php line 901:, yes because there is no id 10 in my record, but with User::find($id); I get normal view without data, since no id 10 in my record.
What I want is show default 404 then redirect to somewhere or return something if record not found with User::findOrFail($id); ? How I can do that ?
Thanks, any help appreciated.
ps: .env APP_DEBUG = true
This does what you asked. No need for exceptions.
public function edit($id)
{
$data = User::find($id);
if ($data == null) {
// User not found, show 404 or whatever you want to do
// example:
return View('admin.user.notFound', [], 404);
} else {
$roles = Role::where('title', '!=', 'Super Admin')->get();
return View('admin.user.edit', compact(['data', 'roles']));
}
}
Your exception handler is not necessary as it is. Regarding Illuminate\Database\Eloquent\ModelNotFoundException:
If the exception is not caught, a 404 HTTP response is automatically sent back to the user, so it is not necessary to write explicit checks to return 404 responses when using [findOrFail()].
Also, I'm pretty sure you get the exception page instead of 404 now because you're in debug mode.
public function singleUser($id)
{
try {
$user= User::FindOrFail($id);
return response()->json(['user'=>user], 200);
} catch (\Exception $e) {
return response()->json(['message'=>'user not found!'], 404);
}
}
findOrFail() is alike of find() function with one extra ability - to throws the Not Found Exceptions
Sometimes you may wish to throw an exception if a model is not found. This is particularly useful in routes or controllers. The findOrFail and firstOrFail methods will retrieve the first result of the query; however, if no result is found, a Illuminate\Database\Eloquent\ModelNotFoundException will be thrown:
$model = App\Flight::findOrFail(1);
$model = App\Flight::where('legs', '>', 100)->firstOrFail();
If the exception is not caught, a 404 HTTP response is automatically sent back to the user. It is not necessary to write explicit checks to return 404 responses when using these methods:
Route::get('/api/flights/{id}', function ($id) {
return App\Flight::findOrFail($id);
});
Its not recommended but If still you want to handle this exception globally, following are the changes as per your handle.php
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $exception
* #return \Illuminate\Http\Response
*/
public function render($request, Exception $exception)
{
if ($exception instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
//redirect to errors.custom view page
return response()->view('errors.custom', [], 404);
}
return parent::render($request, $exception);
}
Late addition to above topic: If you want to handle the exception for an API backend and you don't want to make the check for an empty result in each method and return a 400 Bad request error individually like this...
public function open($ingredient_id){
$ingredient = Ingredient::find($ingredient_id);
if(!$ingredient){
return response()->json(['error' => 1, 'message' => 'Unable to find Ingredient with ID '. $ingredient_id], 400);
}
return $ingredient;
}
Instead use findOrFailand catch exception in app/Exceptions/Handler.php.
public function render($request, Exception $exception)
{
if ($exception instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
return response()->json(['error'=>1,'message'=> 'ModelNotFoundException handled for API' ], 400);
}
return parent::render($request, $exception);
}
This will then look like this in your Controllers:
public function open($ingredient_id){
return Ingredient::findOrFail($ingredient_id);
}
which is much cleaner. Consider that you have plenty of Models and plenty of Controllers.
Im just move to laravel 5 and im receiving errors from laravel in HTML page. Something like this:
Sorry, the page you are looking for could not be found.
1/1
NotFoundHttpException in Application.php line 756:
Persona no existe
in Application.php line 756
at Application->abort('404', 'Person doesnt exists', array()) in helpers.php line
When i work with laravel 4 all works fine, the errors are in json format, that way i could parse the error message and show a message to the user. An example of json error:
{"error":{
"type":"Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException",
"message":"Person doesnt exist",
"file":"C:\\xampp\\htdocs\\backend1\\bootstrap\\compiled.php",
"line":768}}
How can i achieve that in laravel 5.
Sorry for my bad english, thanks a lot.
I came here earlier searching for how to throw json exceptions anywhere in Laravel and the answer set me on the correct path. For anyone that finds this searching for a similar solution, here's how I implemented app-wide:
Add this code to the render method of app/Exceptions/Handler.php
if ($request->ajax() || $request->wantsJson()) {
return new JsonResponse($e->getMessage(), 422);
}
Add this to the method to handle objects:
if ($request->ajax() || $request->wantsJson()) {
$message = $e->getMessage();
if (is_object($message)) { $message = $message->toArray(); }
return new JsonResponse($message, 422);
}
And then use this generic bit of code anywhere you want:
throw new \Exception("Custom error message", 422);
And it will convert all errors thrown after an ajax request to Json exceptions ready to be used any which way you want :-)
Laravel 5.1
To keep my HTTP status code on unexpected exceptions, like 404, 500 403...
This is what I use (app/Exceptions/Handler.php):
public function render($request, Exception $e)
{
$error = $this->convertExceptionToResponse($e);
$response = [];
if($error->getStatusCode() == 500) {
$response['error'] = $e->getMessage();
if(Config::get('app.debug')) {
$response['trace'] = $e->getTraceAsString();
$response['code'] = $e->getCode();
}
}
return response()->json($response, $error->getStatusCode());
}
Laravel 5 offers an Exception Handler in app/Exceptions/Handler.php. The render method can be used to render specific exceptions differently, i.e.
public function render($request, Exception $e)
{
if ($e instanceof API\APIError)
return \Response::json(['code' => '...', 'msg' => '...']);
return parent::render($request, $e);
}
Personally, I use App\Exceptions\API\APIError as a general exception to throw when I want to return an API error. Instead, you could just check if the request is AJAX (if ($request->ajax())) but I think explicitly setting an API exception seems cleaner because you can extend the APIError class and add whatever functions you need.
Edit: Laravel 5.6 handles it very well without any change need, just be sure you are sending Accept header as application/json.
If you want to keep status code (it will be useful for front-end side to understand error type) I suggest to use this in your app/Exceptions/Handler.php:
public function render($request, Exception $exception)
{
if ($request->ajax() || $request->wantsJson()) {
// this part is from render function in Illuminate\Foundation\Exceptions\Handler.php
// works well for json
$exception = $this->prepareException($exception);
if ($exception instanceof \Illuminate\Http\Exception\HttpResponseException) {
return $exception->getResponse();
} elseif ($exception instanceof \Illuminate\Auth\AuthenticationException) {
return $this->unauthenticated($request, $exception);
} elseif ($exception instanceof \Illuminate\Validation\ValidationException) {
return $this->convertValidationExceptionToResponse($exception, $request);
}
// we prepare custom response for other situation such as modelnotfound
$response = [];
$response['error'] = $exception->getMessage();
if(config('app.debug')) {
$response['trace'] = $exception->getTrace();
$response['code'] = $exception->getCode();
}
// we look for assigned status code if there isn't we assign 500
$statusCode = method_exists($exception, 'getStatusCode')
? $exception->getStatusCode()
: 500;
return response()->json($response, $statusCode);
}
return parent::render($request, $exception);
}
On Laravel 5.5, you can use prepareJsonResponse method in app/Exceptions/Handler.php that will force response as JSON.
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $exception
* #return \Illuminate\Http\Response
*/
public function render($request, Exception $exception)
{
return $this->prepareJsonResponse($request, $exception);
}
Instead of
if ($request->ajax() || $request->wantsJson()) {...}
use
if ($request->expectsJson()) {...}
vendor\laravel\framework\src\Illuminate\Http\Concerns\InteractsWithContentTypes.php:42
public function expectsJson()
{
return ($this->ajax() && ! $this->pjax()) || $this->wantsJson();
}
I updated my app/Exceptions/Handler.php to catch HTTP Exceptions that were not validation errors:
public function render($request, Exception $exception)
{
// converts errors to JSON when required and when not a validation error
if ($request->expectsJson() && method_exists($exception, 'getStatusCode')) {
$message = $exception->getMessage();
if (is_object($message)) {
$message = $message->toArray();
}
return response()->json([
'errors' => array_wrap($message)
], $exception->getStatusCode());
}
return parent::render($request, $exception);
}
By checking for the method getStatusCode(), you can tell if the exception can successfully be coerced to JSON.
If you want to get Exception errors in json format then
open the Handler class at App\Exceptions\Handler and customize it.
Here's an example for Unauthorized requests and Not found responses
public function render($request, Exception $exception)
{
if ($exception instanceof AuthorizationException) {
return response()->json(['error' => $exception->getMessage()], 403);
}
if ($exception instanceof ModelNotFoundException) {
return response()->json(['error' => $exception->getMessage()], 404);
}
return parent::render($request, $exception);
}