Customizing The "Not Found" Behavior of Laravel's Routing "Explicit Binding" - php

Here's the documentation: https://laravel.com/docs/5.2/routing#route-model-binding
The routes:
Route::group(['prefix' => 'u'], function () {
Route::post('create', ['as' => 'createUser', 'uses' => 'UserController#create']);
Route::get('{uuid}', ['as' => 'userDashboard', 'uses' => 'UserController#dashboard']);
});
The UserController.php:
public function dashboard(User $uuid)
{
return View::make('user.dashboard');
}
Whenever the User isn't found in the database it throws these two exceptions:
2/2
NotFoundHttpException in Handler.php line 103:
No query results for model [App\User].
1/2
ModelNotFoundException in Builder.php line 303:
No query results for model [App\User].
How do I customize the error? I want to redirect to the createUser route. The documentation instructs to pass a Closure as a third argument but I don't know how to do that with my current code.
EDIT 1
This is what I've tried so far without success:
Route::model('{uuid}', ['as' => 'userDashboard', 'uses' => 'UserController#dashboard'], function () {
App::abort(403, 'Test.');
});
Route::get('{uuid}', ['as' => 'userDashboard', 'uses' => 'UserController#dashboard'], function () {
App::abort(403, 'Test.');
});

This is actually very simple. As none of the answers really give a definite answer I am answering it myself.
In the file RouteServiceController.php's boot function add the following:
$router->model('advertiser', 'App\Advertiser', function () {
throw new AdvertiserNotFoundException;
});
Then create a new empty class in App\Exceptions called (in this case) AdvertiserNotFoundException.php:
<?php
namespace App\Exceptions;
use Exception;
class AdvertiserNotFoundException extends Exception
{
}
The last thing to do is to catch the exception in the Handler.php's render function (App\Exception) like so:
public function render($request, Exception $e)
{
switch ($e)
{
case ($e instanceof AdvertiserNotFoundException):
//Implement your behavior here (redirect, etc...)
}
return parent::render($request, $e);
}
That's it! :)

for a similar case i did,
I took the parent Illuminate\Foundation\Exceptions\Handler isHttpException function and copied it to app/Exceptions/Handler.php and changed it's name to my isUserNotFoundException.
protected function isUserNotFoundException(Exception $e)
{
return $e instanceof UserNotFoundException;
}
and than in the render function add
if ($this->isUserNotFoundException($e))
return redirect('path')->with('error',"Your error message goes here");
Following code must be placed in your RouteServiceProvider::boot method
$router->model('uuid', 'App\User', function () {
throw new UserNotFoundException;
});
and make sure to include this in your view file
and this forum post might help you
https://scotch.io/tutorials/creating-a-laravel-404-page-using-custom-exception-handlers

To do so you need to check if the id exist in the model like so:
public function dashboard(User $uuid)
{
if(User::find($uuid))
{
return View::make('user.dashboard');
} else {
redirect('xyz');
}
}

I think this tutorial will be helpful for you Laravel Model Binding

You could add the exception and treat in in app/Exceptions/Handler.php
/**
* 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 (!env('APP_DEBUG')) {
if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
//treat error
return response()->view('errors.404');
}
return parent::render($request, $e);
}
Edit 1:
This piece of code is from a working project so if this doesn't work it must have an error somewhere else:
if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
$data= \App\Data::orderBy('order', 'asc')->get();
return response()->view('errors.404', [
'data' => $data
], 404);
}
Edit 2:
You can use the above code and this tutorial in order to create a new Exception type in order to get your desired behavior. To my understanding at least. :)

Related

Laravel throwing "Function () does not exist" with method ->controller() on routes

I have this piece of routing here:
<?php
use App\Http\Controllers\SurveyController;
Route::middleware(['auth:api'])->controller(SurveyController::class)->group(function ($route) {
$route->prefix('survey')->group(function ($route) {
$route->post('show', ['show'])->name('survey_show');
$route->post('answer', ['answer'])->name('survey_answer');
});
$route->prefix('member/survey')->group(function ($route) {
$route->post('list', ['list'])->name('member_survey_list');
});
});
The problem is that the route is unable to "find" the controller, I've tried many different approaches to this issue, I found a lot of info about this issue, but none of them could help me solve the problem, and either I get a "Function () does not exist" or a "Target class [App\\Http\\Controllers\\App\\Http\\Controllers\\SurveyController] does not exist.". I didn't want to declare the controller on each route as $route->request('path', [Controller::class, 'function']) since it will have an impact in future maintenance as the routes number grows bigger.
Am I using the method Route::controller()? Should I just write the controller on each route?
I'm using Laravel 8.83.5 and php 8.1.13.
UPDATED
my SurveyController
<?php
namespace App\Http\Controllers;
use App\Http\Resources\SurveyResource;
use App\Services\SurveyService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Exception;
class SurveyController extends Controller
{
private SurveyService $service;
public function __construct(SurveyService $surveyService)
{
$this->service = $surveyService;
}
/**
* php doc here
*/
public function list(Request $request): array
{
$data = $request->only(['keys']);
$validator = Validator::make(
$data,
[
'keys' => 'string'
],
$this->customMessages
);
if ($validator->fails()) {
throw new Exception($validator->errors(), 1);
}
return SurveyResource::method(
$this->service->method(
$data['keys']
)
);
}
/**
* php doc here
*/
public function show(Request $request): array
{
$data = $request->only(['keys']);
$validator = Validator::make(
$data,
[
'keys' => 'string'
],
$this->customMessages
);
if ($validator->fails()) {
throw new Exception($validator->errors(), 2);
}
return SurveyResource::show(
$this->service->show(
$data['keys']
)
);
}
/**
* php doc here
*/
public function answer(Request $request): array
{
$data = $request->only(['keys']);
$validator = Validator::make(
$data,
[
'keys' => 'string'
],
$this->customMessages
);
if ($validator->fails()) {
throw new Exception($validator->errors(), 3);
}
return SurveyResource::answer(
$this->service->answer(
$data['keys']
)
);
}
}
on routes, I've tried calling the route with the method controller() as the first, this way I get a "Function () does not exist" error, the same error pops up when I use it just before the group() method
Route::controller(SurveyController::class)->middleware(['auth:api'])->group(function ($route) {
$route->prefix('member/survey')->group(function ($route) {
$route->post('list', ['list'])->name('member_survey_list');
});
});
also tried calling the route with SurveyController's method without an array, this way I get an "Target class [App\\Http\\Controllers\\App\\Http\\Controllers\\SurveyController] does not exist." error
Route::controller(SurveyController::class)->middleware(['auth:api'])->group(function ($route) {
$route->prefix('member/survey')->group(function ($route) {
$route->post('list', 'list')->name('member_survey_list');
});
});
Also saw some old threads saying that using an array inside the group() method with a namespace key and the namespace value could help, so I tried, but got a "Array to string conversion" error
Route::middleware(['auth:api'])->group(['namespace' => 'App\Http\Controllers\SurveyController'],function ($route) {
$route->prefix('member/survey')->group(function ($route) {
$route->post('list', 'list')->name('member_survey_list');
});
});
SOLUTION
Following the solution given by #matiaslauriti, first you need to remove the declaration of the namespace in the route file (you can keep the declaration if you remove it from the RouteServiceProvider.php), then you can either call it as Route::controller(ControllerName::class) or literally as Route::controller('ControllerName'). Here's my working code:
<?php
Route::controller(SurveyController::class)->middleware(['auth:api'])->group(function ($route) {
$route->prefix('survey')->group(function ($route) {
$route->post('show', 'show')->name('survey_show');
$route->post('answer', 'answer')->name('survey_answer');
});
$route->prefix('member/survey')->group(function ($route) {
$route->post('list', 'list')->name('member_survey_list');
});
});
If you are getting App\\Http\\Controllers\\App\\Http\\Controllers\\SurveyController error, that means you only have to pass SurveyController literally:
Route::controller('SurveyController')->middleware(['auth:api'])->group(function ($route) {
$route->prefix('member/survey')->group(function ($route) {
$route->post('list', ['list'])->name('member_survey_list');
});
});
I would recommend to switch to the new routing system, delete $namespace like this file and from everywhere on that file.
Then, you can do controller(SurveyController::class)

MethodNotAllowedHttpException, redirect to 404 Laravel 9

So when a user randomly types a URL on a route that exists, they get an error message:
Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
The GET method is not supported for this route. Supported methods: POST.
After doing some searching, all the posts I can find suggest to change the render function inside of App\Exceptions\Handler and change it to this:
public function render($request, Exception $exception)
{
if($exception instanceof \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException){
return abort('404');
}
return parent::render($request, $exception);
}
However, with the newer version of Laravel this no longer exists. One post mentioned to add this in routes\web.php:
Route::fallback( function () {
abort( 404 );
} );
This works fine but I'm not sure if this is the best approach/right place to have it? Is there are any other alternative way?
I have also attempted to change the register function inside of App\Exceptions\Handler to this per the Laravel Doc (https://laravel.com/docs/9.x/errors#rendering-exceptions):
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
public function register()
{
$this->renderable(function (NotFoundHttpException $e, $request) {
if ($request->is('api/*')) {
return response()->json([
'message' => 'Record not found.'
], 404);
}
});
}
but it does not work
In a newer version on Laravel, you can add
$this->renderable(function (Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException $e) {
// do something
});
this line inside a register method on a class \App\Exceptions\Handler
If you want to handle NotFoundException you should use
$this->renderable(function (Symfony\Component\HttpKernel\Exception\NotFoundHttpException $e) {
// do something
});
You can find more detailed answer on Laravel documentation here:
https://laravel.com/docs/9.x/errors#rendering-exceptions

"Route [login] not defined." Laravel 5.6 [duplicate]

Trying to play with Laravel today for the first time. I am getting the following error when I attempt to visit localhost/project/public:
InvalidArgumentException
Route [login] not defined.
app/routes.php:
<?php
Route::get('/', 'HomeController#redirect');
Route::get('login', 'LoginController#show');
Route::post('login', 'LoginController#do');
Route::get('dashboard', 'DashboardController#show');
app/controllers/HomeController.php:
<?php
class HomeController extends Controller {
public function redirect()
{
if (Auth::check())
return Redirect::route('dashboard');
return Redirect::route('login');
}
}
app/controllers/LoginContoller.php:
<?php
class LoginController extends Controller {
public function show()
{
if (Auth::check())
return Redirect::route('dashboard');
return View::make('login');
}
public function do()
{
// do login
}
}
app/controllers/DashboardController.php:
<?php
class DashboardController extends Controller {
public function show()
{
if (Auth::guest())
return Redirect::route('login');
return View::make('dashboard');
}
}
Why am I getting this error?
Try to add this at Header of your request: Accept=application/json postman or insomnia add header
You're trying to redirect to a named route whose name is login, but you have no routes with that name:
Route::post('login', [ 'as' => 'login', 'uses' => 'LoginController#do']);
The 'as' portion of the second parameter defines the name of the route. The first string parameter defines its route.
In app\Exceptions\Handler.php
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->expectsJson()) {
return response()->json(['error' => 'Unauthenticated.'], 401);
}
return redirect()->guest(route('auth.login'));
}
Laravel has introduced Named Routes in Laravel 4.2.
WHAT IS NAMED ROUTES?
Named Routes allows you to give names to your router path. Hence using the name we can call the routes in required file.
HOW TO CREATE NAMED ROUTES?
Named Routes created in two different way : as and name()
METHOD 1:
Route::get('about',array('as'=>'about-as',function()
{
return view('about');
}
));
METHOD 2:
Route::get('about',function()
{
return view('about');
})->name('about-as');
How we use in views?
about-as
Hence laravel 'middleware'=>'auth' has already predefined for redirect as login page if user has not yet logged in.Hence we should use as keyword
Route::get('login',array('as'=>'login',function(){
return view('login');
}));
You need to add the following line to your web.php routes file:
Auth::routes();
In case you have custom auth routes, make sure you /login route has 'as' => 'login'
In case of API , or let say while implementing JWT . JWT middleware throws this exception when it couldn't find the token and will try to redirect to the log in route.
Since it couldn't find any log in route specified it throws this exception .
You can change the route in "app\Exceptions\Handler.php"
use Illuminate\Auth\AuthenticationException;
protected function unauthenticated($request, AuthenticationException $exception){
return $request->expectsJson()
? response()->json(['message' => $exception->getMessage()], 401)
: redirect()->guest(route('ROUTENAME'));
}
Am late to the party. if your expectation is some sort of json returned other than being redirected, then edit the exception handler so do just that.
Go to go to App\Exceptions\Handler.php
Then edit this code:
public function render($request, Exception $exception)
{
return parent::render($request, $exception);
}
to
public function render($request, Exception $exception)
{
return response()->json(
[
'errors' => [
'status' => 401,
'message' => 'Unauthenticated',
]
], 401
);
}
Try this method:
look for this file
"RedirectifAuthenticated.php"
update the following as you would prefer
if (Auth::guard($guard)->check()) {
return redirect('/');
}
$guard as an arg will take in the name of the custom guard you have set eg. "admin" then it should be like this.
if (Auth::guard('admin')->check()) {
return redirect('/admin/dashboard');
}else{
return redirect('/admin/login');
}
I ran into this error recently after using Laravel's built-in authentication routing using php artisan make:auth. When you run that command, these new routes are added to your web.php file:
Auth::routes();
Route::get('/home', 'HomeController#index')->name('home');
I must have accidentally deleted these routes. Running php artisan make:auth again restored the routes and solved the problem. I'm running Laravel 5.5.28.
Route::get('/login', function () {
return view('login');})->name('login');
Name your login route if using your own custom auth system.
Route::post('login', 'LoginController#login')->name('login')
works very well and it is clean and self-explanatory
Laravel ^5.7
The Authenticate Middleware
Laravel ^5.7 includes new middleware to handle and redirect unauthenticated users.
It works well with "web" guard... of course the "login" route (or whatever you name your login route) should be defined in web.php.
the problem is when your are using custom guard. Different guard would redirect unauthenticated users to different route.
here's a quick workaround based on John's response (it works for me).
app/Http/Middleware/Authenticate.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class Authenticate extends Middleware
{
/**
* #var array
*/
protected $guards = [];
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #param string[] ...$guards
* #return mixed
*
* #throws \Illuminate\Auth\AuthenticationException
*/
public function handle($request, Closure $next, ...$guards)
{
$this->guards = $guards;
return parent::handle($request, $next, ...$guards);
}
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* #param \Illuminate\Http\Request $request
* #return string
*/
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
if (in_array('admin', $this->guards)) {
return route('admin.login');
}
return route('login');
}
}
}
Source : Issue #26292
Replace in your views (blade files) all
{{route('/')}} ----- by ----> {{url('/')}}
If someone getting this from a rest client (ex. Postman) - You need to set the Header to Accept application/json.
To do this on postman, click on the Headers tab, and add a new key 'Accept' and type the value 'application/json'.
**Adding this for the future me.**
I encountered this because I was reusing Laravel's "HomeController", and adding my custom functions to it. Note that this controller calls the auth middleware in its __construct() method as shown below, which means that all functions must be authenticated. No wonder it tries to take you to login page first. So, if you are not using Laravel's authentication scafffolding, you will be in a mess. Disable the constructor, or do as you seem fit, now that you know what is happening.
public function __construct()
{
$this->middleware('auth');
}
//In Laravel 8
Route::post('login', [LoginController::class, 'do'])->name('login');
If using passport as api do:
routes/web.php:
Route::get('login', function() {
return response()->json(['message' => 'Unauthorized.'], 401);
});
Route::post('login', [ 'as' => 'login']);
For Laravel 8 I face the problem while access home url after creating login register system
First there is no route exists with the name login
Just add a route name like this
for single method like
Route::post('put url here',[Your Controller::class,'method name'])->name('login');
for multiple method like
Route::match(['get','post'],'put url here',[Your Controller::class,'method name'])->name('login');
Route must be valid, should give route name.
Route::group([
'prefix' => 'v1'
], function () {
Route::post('/login', [userController::class, 'loginAction'])->name('login');
});

How to return 403 response in JSON format in Laravel 5.2?

I am trying to develop a RESTful API with Laravel 5.2. I am stumbled on how to return failed authorization in JSON format. Currently, it is throwing the 403 page error instead of JSON.
Controller: TenantController.php
class TenantController extends Controller
{
public function show($id)
{
$tenant = Tenant::find($id);
if($tenant == null) return response()->json(['error' => "Invalid tenant ID."],400);
$this->authorize('show',$tenant);
return $tenant;
}
}
Policy: TenantPolicy.php
class TenantPolicy
{
use HandlesAuthorization;
public function show(User $user, Tenant $tenant)
{
$users = $tenant->users();
return $tenant->users->contains($user->id);
}
}
The authorization is currently working fine but it is showing up a 403 forbidden page instead of returning json error. Is it possible to return it as JSON for the 403? And, is it possible to make it global for all failed authorizations (not just in this controller)?
We managed to resolve this by modifying the exceptions handler found in App\Exceptions\Handler.php adding it in the render function.
public function render($request, Exception $e)
{
if ($e instanceof AuthorizationException)
{
return response()->json(['error' => 'Not authorized.'],403);
}
return parent::render($request, $e);
}
Yes, make a simple before method in your policy which will be executed prior to all other authorization checks,
public function before($user, $ability,Request $request)
{
if (!yourconditiontrue) {
if ($request->ajax()) {
return response('Unauthorized.', 401);
} else {
return abort('403');
}
}
}
You can intercept the exception
try {
$this->authorize('update', $data);
} catch (\Exception $e)
{
return response()->json(null, 403);
}
As for the latest version of Laravel, as of now version >=7.x,
Generally setting request headers 'Accept' => 'application/json' will tell Laravel that you expect a json response back.
For errors you need to also turn off debugging by setting the APP_DEBUG=false on your .env file, which will make sure the response is json and no stacktrace is provided.
The accepted answer works, but if you don't want to return json for every route you can handle this with middleware.
A brief outline of how to do this:
Create an ApiAuthorization class and extend your main auth class.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Auth\Middleware\Authorize;
use Illuminate\Auth\Access\AuthorizationException;
class ApiAuthorization extends Authorize
{
public function handle($request, Closure $next, $ability, ...$models)
{
try {
$this->auth->authenticate();
$this->gate->authorize($ability, $this->getGateArguments($request, $models));
} catch (AuthorizationException $e) {
return response()->json(['error' => 'Not authorized.'],403);
}
return $next($request);
}
}
Add the middleware to $routeMiddleware in App\Http\Kernel.php
'api.can' => \App\Http\Middleware\ApiAuthorization::class,
Update your route. You can now use your new api auth middleware by calling api.can similar to the example in the docs
Route::get('tenant', [
'as' => 'api.tenant',
'uses' => 'TenantController#show'
])->middleware('api.can:show,tenant');
This method allows you to return json for specific routes without modifying the global exception handler.
I have also face the same issue in Laravel version 7.3 where the AuthorizationException is not caught. What I come to know that we have to include AuthorizationException in the Handler.php like
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Auth\Access\AuthorizationException;
use Throwable;
use Exception;
use Request;
use Response;
class Handler extends ExceptionHandler
{
// ...
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Throwable $exception
* #return \Symfony\Component\HttpFoundation\Response
*
* #throws \Throwable
*/
public function render($request, Throwable $exception)
{
if ($exception instanceof AuthorizationException)
{
return response()->json(['message' => 'Forbidden'], 403);
}
if ($exception instanceof ModelNotFoundException && $request->wantsJson()) {
return response()->json(['message' => 'resource not found')], 404);
}
return parent::render($request, $exception);
}
// ...
}
FYI if you just add the AuthorizationException by using the following statement
use AuthorizationException;
It still not working. So we have to specify the fully qualified namespace path.

Redirection in laravel without return statement

i have this blogsController, the create function is as follows.
public function create() {
if($this->reqLogin()) return $this->reqLogin();
return View::make('blogs.create');
}
In BaseController, i have this function which checks if user is logged in.
public function reqLogin(){
if(!Auth::check()){
Session::flash('message', 'You need to login');
return Redirect::to("login");
}
}
This code is working fine , but it is not what is need i want my create function as follows.
public function create() {
$this->reqLogin();
return View::make('blogs.create');
}
Can i do so?
Apart from that, can i set authantication rules , like we do in Yii framework, at the top of controller.
Beside organizing your code to fit better Laravel's architecture, there's a little trick you can use when returning a response is not possible and a redirect is absolutely needed.
The trick is to call \App::abort() and pass the approriate code and headers. This will work in most of the circumstances (excluding, notably, blade views and __toString() methods.
Here's a simple function that will work everywhere, no matter what, while still keeping your shutdown logic intact.
/**
* Redirect the user no matter what. No need to use a return
* statement. Also avoids the trap put in place by the Blade Compiler.
*
* #param string $url
* #param int $code http code for the redirect (should be 302 or 301)
*/
function redirect_now($url, $code = 302)
{
try {
\App::abort($code, '', ['Location' => $url]);
} catch (\Exception $exception) {
// the blade compiler catches exceptions and rethrows them
// as ErrorExceptions :(
//
// also the __toString() magic method cannot throw exceptions
// in that case also we need to manually call the exception
// handler
$previousErrorHandler = set_exception_handler(function () {
});
restore_error_handler();
call_user_func($previousErrorHandler, $exception);
die;
}
}
Usage in PHP:
redirect_now('/');
Usage in Blade:
{{ redirect_now('/') }}
You should put the check into a filter, then only let the user get to the controller if they are logged in in the first place.
Filter
Route::filter('auth', function($route, $request, $response)
{
if(!Auth::check()) {
Session::flash('message', 'You need to login');
return Redirect::to("login");
}
});
Route
Route::get('blogs/create', array('before' => 'auth', 'uses' => 'BlogsController#create'));
Controller
public function create() {
return View::make('blogs.create');
}
We can do like this,
throw new \Illuminate\Http\Exceptions\HttpResponseException(redirect('/to/another/route/')->with('status', 'An error occurred.'));
It's not a best practice to use this method, but to solve your question, you can use this gist.
Create a helper function like:
if(!function_exists('abortTo')) {
function abortTo($to = '/') {
throw new \Illuminate\Http\Exceptions\HttpResponseException(redirect($to));
}
}
then use it in your code:
public function reqLogin(){
if(!Auth::check()){
abortTo(route('login'));
}
}
public function create() {
$this->reqLogin();
return View::make('blogs.create');
}

Categories