I'm building RESTful API with Laravel. My API always returns JSON. What I would like to do, is keeping response logic at one place. Here's how I do it right now in API controller, which is pointed to by Route::controller(). Funny and ultra-useful example coming:
public function getDouble($number) {
try {
if (!is_numeric($number)) {
throw new HttpException(400, 'Invalid number.');
}
$response = $number * 2;
$status = 200;
}
catch (HttpException $exception) {
$response = $exception->getMessage();
$status = $exception->getStatusCode();
}
return response()->json($response, $status);
}
In this example, my API route would be for example /double/13 accessed by GET method. The problem is that I repeat this try ... catch block in each method. I would like my API methods to be like:
public function getDouble($number) {
if (!is_numeric($number)) {
throw new HttpException(400, 'Invalid number.');
}
return $number;
}
And then, catch those exceptions and form JSON in another place. What is the best approach here in terms of good application architecture?
Response on Exception
You could do this by handling the exception in App\Exceptions\Handler.
You could do it in the render method, likeso :
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $e
* #return \Illuminate\Http\Response
*/
public function render($request, Exception $e)
{
if($e instanceof HttpException) {
return response()->json($e->getMessage(), $e->getStatusCode());
}
return parent::render($request, $e);
}
Success Response
There are several ways to do this but I guess Middleware would be the best suited one right.
Create a middleware (say, ApiResponseFormatterMiddleware)
In your 'App\Http\Kernel', add it to $routeMiddleware array.
Apply it to the api routes, response to which you want to parse.
You could do something in the lines of :
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
$response = $next($request);
return response()->json($response->getOriginalContent());
}
Ofcourse, you need to change a bit of logic to parse the content the way you want it, but skeleton remains the same.
Related
I am beginner to laravel. I came across one laravel application. In that I need to handle all types of exceptions/ errors that are getting. Exceptions like ViewExceptions, ErrorExceptions etc. I need to show one view page(site under maintenance) for all those system exceptions, errors and for all database or coding exceptions and errors.
I have checked Laravel Error handling and also googled for solutions. But more I searched I am getting confused for solution. As the application is already on production, I can't make changes to each controller to handle the exceptions. I am guessing, I need to make changes in App/Exception/Handler class only but not sure how that will work.
Form search I got that I have to make changes like in Handler class:
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Throwable $exception
* #return \Illuminate\Http\Response
*/
public function render($request, Throwable $exception)
{
if ($exception instanceof CustomException) {
return response()->view('errors.site_down', [], 500);
}
return parent::render($request, $exception);
}
Above code not showing if there is ViewException.
I have observed that in .env APP_DEBUG is true and in config/app it's false. Does that affect?
How all exceptions or errors will redirect to site_down page? also please guide me exception and error handling in laravel. I am getting more confused.
Thanks in advance.
Just get rid of the if statement:
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Throwable $exception
* #return \Illuminate\Http\Response
*/
public function render($request, Throwable $exception)
{
return response()->view('errors.site_down', [], 503);
}
You will also probably want to return 503, if you are trying to claim the site is down for maintenance.
In critique of this approach, I think it is dishonest and transparent to your users to claim the site is in maintenance for your errors, and this will not pay itself off in the long run.
Add a blade page on resources/views/errors/503.blade.php
You may publish Laravel's error page templates using the vendor:publish Artisan command. Once the templates have been published, you may customize them to your liking :
php artisan vendor:publish --tag=laravel-errors
This command will create all your custom error page on resources/views/errors/ directory. You can customize as you want.
See official documentation here
For custom exceptions first you have to make a custom exception file preferably in the exceptions folder App\Exceptions\CustomException.php
<?php
namespace App\Exceptions;
use Exception;
class CustomException extends Exception
{
//
}
Then in your exceptions handler file App\Exceptions\Handler.php
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use App\Exceptions\CustomException as CustomException;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
*
* #var array
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* #var array
*/
protected $dontFlash = [
'password',
'password_confirmation',
];
/**
* Report or log an exception.
*
* #param \Throwable $exception
* #return void
*/
public function report(Throwable $exception)
{
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Throwable $exception
* #return \Illuminate\Http\Response
*/
public function render($request, Throwable $exception)
{
// Thrown when a custom exception occurs.
if ($exception instanceof CustomException) {
return response()->view('error.page.path', [], 500);
}
// Thrown when an exception occurs.
if ($exception instanceof Exception) {
response()->view('errors.page.path', [], 500);
}
return parent::render($request, $exception);
}
}
Remember to use App\Exceptions\CustomException; custom exceptions file where ever you need to throw a custom exception like so:
use App\Exceptions\CustomException;
function test(){
throw new CustomException('This is an error');
}
src/Controller/DataTableController.php
<?php
use DataTables\DataTablesInterface;
/**
* Symfony 3.4 and above
*
* #Route("/users", name="users")
*
* #param Request $request
* #param DataTablesInterface $datatables
* #return JsonResponse
*/
public function usersAction(Request $request, DataTablesInterface $datatables): JsonResponse
{
try {
// Tell the DataTables service to process the request,
// specifying ID of the required handler.
$results = $datatables->handle($request, 'users');
return $this->json($results);
}
catch (HttpException $e) {
// In fact the line below returns 400 HTTP status code.
// The message contains the error description.
return $this->json($e->getMessage(), $e->getStatusCode());
}
}
/**
* Symfony 3.3 and below
*
* #Route("/users", name="users")
*
* #param Request $request
* #return JsonResponse
*/
public function usersAction(Request $request): JsonResponse
{
try {
/** #var \DataTables\DataTablesInterface $datatables */
$datatables = $this->get('datatables');
// Tell the DataTables service to process the request,
// specifying ID of the required handler.
$results = $datatables->handle($request, 'users');
return $this->json($results);
}
catch (HttpException $e) {
// In fact the line below returns 400 HTTP status code.
// The message contains the error description.
return $this->json($e->getMessage(), $e->getStatusCode());
}
}
I get the error message:
The structure for a class, (not related to Symfony) is
class MyClassController() {
public function myMethodAction() {
}
}
There is not way to avoid a parse error because this is just not valid syntax of PHP.
I have a Like button that fires an Ajax Post, this route is protected by auth:api middleware:
myproject/routes/api.php
Route::group(['middleware' => ['auth:api']], function () {
Route::post('/v1/like', 'APIController#set_like');
});
When an authenticated user clicks the like button, no problem at all, everything works smoothly. But when guests click the button, I redirected them to login page via Javascript and after authentication they are redirected to the page specified in RedirectIfAuthenticated middleware, so usually to /home.
I modified that middleware as follows:
myproject/app/Http/Middleware/RedirectIfAuthenticated.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #param string|null $guard
* #return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
return redirect()->intended('/home');
}
return $next($request);
}
}
My Ajax call is this:
var toggleLike = function(){
var token = USER_TOKEN; //javascript variable
var current_type = $(thisLikeable).attr("data-type");
var current_id = $(thisLikeable).attr("data-id");
var jqxhr = $.post( APP_URL + "/api/v1/like", {api_token: token, type: current_type, id: current_id}, function(data) {
setLikeAppearance(data.message);
})
.fail(function(xhr, status, error){
if (xhr.status == 401) {
window.location = APP_URL + "/login" ;
}
});
};
The problem here's the intended() function, which for Ajax calls is not storing the correct session variable and I am not figuring out how to set it properly.
I am clearly missing something obvious, can anyone help?
Cheers!
EDIT
What I want to achieve is this:
GUEST is in //mysite/blabla
clicks Like button
gets redirected to login
logs in (or register)
gets redirected to //mysite/blabla with the Like already triggered on
What's happening is that in APIs sessions are not managed or in other words it's stateless. So the session middleware is not implemented on Laravel framework for API requests. Though you can manually add, it's not idle to use. So if the API does not use sessions and uses the redirect, fronted does not know about it, as API and frontend work as two separate apps. SO you need to send the frontend the status of the response and let the frontend handle the redirect as you have done with ajax. Just remove the redirect if unauthenticated and let the API throw unauthorized exception. Then, from the handler, handle the unauthorized exception.
Here is how to do it.
Add this to app/Exceptions/Handler.php
/**
* Convert an authentication exception into an unauthenticated response.
*
* #param \Illuminate\Http\Request $request
* #param \Illuminate\Auth\AuthenticationException $exception
* #return \Illuminate\Http\Response
*/
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->expectsJson()) {
return response()->json(['error' => 'Unauthenticated.'], 401);
}
return redirect()->guest('login');
}
this will send the user a 401 with message Unauthenticated if the request was json(api request), else(if web request) will redirect to login
check the render method below or check it from source to understand what's happening. when an unauthorized exception is thrown we are telling to check the request type and if it's from an API request, we are sending a json response with 401 status code. So know from frontend we could redirect the user to login page after seeing the 401 status code.
From source
/**
* Render an exception into a response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $e
* #return \Symfony\Component\HttpFoundation\Response
*/
public function render($request, Exception $e)
{
if (method_exists($e, 'render') && $response = $e->render($request)) {
return Router::toResponse($request, $response);
} elseif ($e instanceof Responsable) {
return $e->toResponse($request);
}
$e = $this->prepareException($e);
if ($e instanceof HttpResponseException) {
return $e->getResponse();
} elseif ($e instanceof AuthenticationException) {
return $this->unauthenticated($request, $e);
} elseif ($e instanceof ValidationException) {
return $this->convertValidationExceptionToResponse($e, $request);
}
return $request->expectsJson()
? $this->prepareJsonResponse($request, $e)
: $this->prepareResponse($request, $e);
}
AFTER EDIT
The intended method() is only for web routes as it uses session to extract the intended route or manually passed value. Here is the intended() method.
/**
* Create a new redirect response to the previously intended location.
*
* #param string $default
* #param int $status
* #param array $headers
* #param bool $secure
* #return \Illuminate\Http\RedirectResponse
*/
public function intended($default = '/', $status = 302, $headers = [], $secure = null)
{
$path = $this->session->pull('url.intended', $default);
return $this->to($path, $status, $headers, $secure);
}
To achive the redirect to the page the user is comming from you can
1 - Manually pass some queries with url like
/login?redirect=like(not the url, just a mapping for /comments/like url)&value=true(true is liked, false is unliked)
and handle it manually.
2 - Get and check the query parameters from url
3 - Use to() method to pass the intended url instead of using intended(). here is the to() method. (see 4 to see the recommended way)
/**
* Create a new redirect response to the given path.
*
* #param string $path
* #param int $status
* #param array $headers
* #param bool $secure
* #return \Illuminate\Http\RedirectResponse
*/
public function to($path, $status = 302, $headers = [], $secure = null)
{
return $this->createRedirect($this->generator->to($path, [], $secure), $status, $headers);
}
4 - But, I would recommend sending redirect url (I mean the mapping ex: like) as a response to frontend and let the frontend handle the redirecting. As API redirecting will work if the API is used by websites only. Suppose if you are using this same api for a mobile app, wonder how API redirect will work. It's not a work of API to redirect, unless if it's for things like OAuth Authentication, which would have a redirect url specified.
Remember to sanitize the url params to block XSS like stuff. Better
Send some values and map it to the urls. Like
[
//like is mapping
//comments/like is the actual url
'like' => 'comments/like'
]
Then, get the mapping url from array or use frontend mappings.
You can make changes in your RedirectIfAuthenticated.php to distinguish between Ajax call & normal login like this:
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #param string|null $guard
* #return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
if ($request->has('api_token')) {
// do whatever you want
return response()->json('some response');
}
return redirect()->intended('/home');
}
return $next($request);
}
}
Update:
Another solution is to remove Auth middleware from your route. In APIController#set_like function manually login user, trigger like & return json response.
If a button is not for a guest, then you shouldn't render it on page.
Instead, you should render a link to login, then if the user logs in you will redirect him back to where he was before. Now, user can see and click the button.
I use laravel5.5 for a web program. But when the program is exception, the middleware don't work.
what should i do for it?
my middleware code is list:
namespace App\Http\Middleware;
use Closure;
class ApiException
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #param string|null $guard
* #return mixed
*/
public function handle($request, Closure $next)
{
$response = $next($request);
$env = config('app.env');
$statusCode = $response->getSatusCode();
if ($statusCode >= 500 && $env != 'dev') {
$response->setContent(['error' => 'Internal Server Error']);
}
return $next($request);
}
}
thank you help me !
There is no way you can do that in Middlewares. You can control exceptions in your app\Exceptions\Handler.php instead.
In middleware can't but in Exception/Handler can..
anytime the exception occurred.. it not entered the middleware, it entered Exception/Handler.php..
in render method
public function render($request, Exception $exception)
{
if ($exception->getCode() >= 500 ||) {
//return redirect()->route(); do something here
}
return parent::render($request, $exception);
}
Hello I have an issue with laravel localization
I've made language switcher and currently selected language doesn't work on 404 pages (it works if I return abort(404) in controller manualy), it always shows content on default locale defined in config/app.php
My middleware code
<?php
namespace App\Http\Middleware;
use Closure;
class SetLanguageCookie
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if($request->hasCookie('language')) {
$cookie = $request->cookie('language');
app()->setLocale($cookie);
return $next($request);
} else {
$response = $next($request);
$response->withCookie(cookie()->forever('language', 'en'));
return $response;
}
}
}
Any ideas how can I make this working? So all automaticaly shown 404 pages show content in currently selected language?
If you have default error handling, look for this file:
app\Exceptions\Handler.php
Change the render method to something like this:
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $e
* #return \Illuminate\Http\Response
*/
public function render($request, Exception $e)
{
if($e instanceof NotFoundHttpException)
{
if(\Request::hasCookie('language')) {
$cookie = \Request::cookie('language');
app()->setLocale($cookie);
//.... etc
}
}
return parent::render($request, $e);
}
Fixed it with help of #ArthurSamarcos
app/Exceptions/Handler.php
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $e
* #return \Illuminate\Http\Response
*/
public function render($request, Exception $e)
{
if($request->hasCookie('language')) {
// Get cookie
$cookie = $request->cookie('language');
// Check if cookie is already decrypted if not decrypt
$cookie = strlen($cookie) > 2 ? decrypt($cookie) : $cookie;
// Set locale
app()->setLocale($cookie);
}
if($e instanceof NotFoundHttpException) {
return response()->view('errors.404', [], 404);
}
return parent::render($request, $e);
}