Skip to content

Latest commit

 

History

History
677 lines (508 loc) · 20.4 KB

routing.md

File metadata and controls

677 lines (508 loc) · 20.4 KB

Routing

⬆️ Go to main menu ⬅️ Previous (Views) ➡️ Next (Validation)

Route group within a group

In Routes, you can create a group within a group, assigning a certain middleware only to some URLs in the "parent" group.

Route::group(['prefix' => 'account', 'as' => 'account.'], function() {
    Route::get('login', [AccountController::class, 'login']);
    Route::get('register', [AccountController::class, 'register']);
    Route::group(['middleware' => 'auth'], function() {
        Route::get('edit', [AccountController::class, 'edit']);
    });
});

Declare a resolveRouteBinding method in your Model

Route model binding in Laravel is great, but there are cases where we can't just allow users to easily access resources by ID. We might need to verify their ownership of a resource.

You can declare a resolveRouteBinding method in your Model and add your custom logic there.

public function resolveRouteBinding($value, $field = null)
{
     $user = request()->user();

     return $this->where([
          ['user_id' => $user->id],
          ['id' => $value]
     ])->firstOrFail();
}

Tip given by @notdylanv

assign withTrashed() to Route::resource() method

Before Laravel 9.35 - only for Route::get()

Route::get('/users/{user}', function (User $user) {
     return $user->email;
})->withTrashed();

Since Laravel 9.35 - also for Route::resource()!

Route::resource('users', UserController::class)
     ->withTrashed();

Or, even by method

Route::resource('users', UserController::class)
     ->withTrashed(['show']);

Skip Input Normalization

Laravel automatically trims all incoming string fields on the request. It's called Input Normalization.

Sometimes, you might not want this behavior.

You can use skipWhen method on the TrimStrings middleware and return true to skip it.

public function boot()
{
     TrimStrings::skipWhen(function ($request) {
          return $request->is('admin/*);
     });
}

Tip given by @Laratips1

Wildcard subdomains

You can create route group by dynamic subdomain name, and pass its value to every route.

Route::domain('{username}.workspace.com')->group(function () {
    Route::get('user/{id}', function ($username, $id) {
        //
    });
});

What's behind the routes?

If you use Laravel UI package, you likely want to know what routes are actually behind Auth::routes()?

You can check the file /vendor/laravel/ui/src/AuthRouteMethods.php.

public function auth()
{
    return function ($options = []) {
        // Authentication Routes...
        $this->get('login', 'Auth\LoginController@showLoginForm')->name('login');
        $this->post('login', 'Auth\LoginController@login');
        $this->post('logout', 'Auth\LoginController@logout')->name('logout');
        // Registration Routes...
        if ($options['register'] ?? true) {
            $this->get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
            $this->post('register', 'Auth\RegisterController@register');
        }
        // Password Reset Routes...
        if ($options['reset'] ?? true) {
            $this->resetPassword();
        }
        // Password Confirmation Routes...
        if ($options['confirm'] ?? class_exists($this->prependGroupNamespace('Auth\ConfirmPasswordController'))) {
            $this->confirmPassword();
        }
        // Email Verification Routes...
        if ($options['verify'] ?? false) {
            $this->emailVerification();
        }
    };
}

The default use of that function is simply this:

Auth::routes(); // no parameters

But you can provide parameters to enable or disable certain routes:

Auth::routes([
    'login'    => true,
    'logout'   => true,
    'register' => true,
    'reset'    => true,  // for resetting passwords
    'confirm'  => false, // for additional password confirmations
    'verify'   => false, // for email verification
]);

Tip is based on suggestion by MimisK13

Route Model Binding: You can define a key

You can do Route model binding like Route::get('api/users/{user}', function (User $user) { … } - but not only by ID field. If you want {user} to be a username field, put this in the model:

public function getRouteKeyName() {
    return 'username';
}

Route Fallback: When no Other Route is Matched

If you want to specify additional logic for not-found routes, instead of just throwing default 404 page, you may create a special Route for that, at the very end of your Routes file.

Route::group(['middleware' => ['auth'], 'prefix' => 'admin', 'as' => 'admin.'], function () {
    Route::get('/home', [HomeController::class, 'index']);
    Route::resource('tasks', [Admin\TasksController::class]);
});

// Some more routes....
Route::fallback(function() {
    return 'Hm, why did you land here somehow?';
});

Route Parameters Validation with RegExp

We can validate parameters directly in the route, with “where” parameter. A pretty typical case is to prefix your routes by language locale, like fr/blog and en/article/333. How do we ensure that those two first letters are not used for some other than language?

routes/web.php:

Route::group([
    'prefix' => '{locale}',
    'where' => ['locale' => '[a-zA-Z]{2}']
], function () {
    Route::get('/', [HomeController::class, 'index']);
    Route::get('article/{id}', [ArticleController::class, 'show']);;
});

Rate Limiting: Global and for Guests/Users

You can limit some URL to be called a maximum of 60 times per minute, with throttle:60,1:

Route::middleware('auth:api', 'throttle:60,1')->group(function () {
    Route::get('/user', function () {
        //
    });
});

But also, you can do it separately for public and for logged-in users:

// maximum of 10 requests for guests, 60 for authenticated users
Route::middleware('throttle:10|60,1')->group(function () {
    //
});

Also, you can have a DB field users.rate_limit and limit the amount for specific user:

Route::middleware('auth:api', 'throttle:rate_limit,1')->group(function () {
    Route::get('/user', function () {
        //
    });
});

Query string parameters to Routes

If you pass additional parameters to the route, in the array, those key / value pairs will automatically be added to the generated URL's query string.

Route::get('user/{id}/profile', function ($id) {
    //
})->name('profile');

$url = route('profile', ['id' => 1, 'photos' => 'yes']); // Result: /user/1/profile?photos=yes

Separate Routes by Files

If you have a set of routes related to a certain "section", you may separate them in a special routes/XXXXX.php file, and just include it in routes/web.php

Example with routes/auth.php in Laravel Breeze by Taylor Otwell himself:

Route::get('/', function () {
    return view('welcome');
});

Route::get('/dashboard', function () {
    return view('dashboard');
})->middleware(['auth'])->name('dashboard');

require __DIR__.'/auth.php';

Then, in routes/auth.php:

use App\Http\Controllers\Auth\AuthenticatedSessionController;
use App\Http\Controllers\Auth\RegisteredUserController;
// ... more controllers

use Illuminate\Support\Facades\Route;

Route::get('/register', [RegisteredUserController::class, 'create'])
                ->middleware('guest')
                ->name('register');

Route::post('/register', [RegisteredUserController::class, 'store'])
                ->middleware('guest');

// ... A dozen more routes

But you should use this include() only when that separate route file has the same settings for prefix/middlewares, otherwise it's better to group them in app/Providers/RouteServiceProvider:

public function boot()
{
    $this->configureRateLimiting();

    $this->routes(function () {
        Route::prefix('api')
            ->middleware('api')
            ->namespace($this->namespace)
            ->group(base_path('routes/api.php'));

        Route::middleware('web')
            ->namespace($this->namespace)
            ->group(base_path('routes/web.php'));

        // ... Your routes file listed next here
    });
}

Translate Resource Verbs

If you use resource controllers, but want to change URL verbs to non-English for SEO purposes, so instead of /create you want Spanish /crear, you can configure it by using Route::resourceVerbs() method in App\Providers\RouteServiceProvider:

public function boot()
{
    Route::resourceVerbs([
        'create' => 'crear',
        'edit' => 'editar',
    ]);

    // ...
}

Custom Resource Route Names

When using Resource Controllers, in routes/web.php you can specify ->names() parameter, so the URL prefix in the browser and the route name prefix you use all over Laravel project may be different.

Route::resource('p', ProductController::class)->names('products');

So this code above will generate URLs like /p, /p/{id}, /p/{id}/edit, etc. But you would call them in the code by route('products.index'), route('products.create'), etc.

Eager load relationship

If you use Route Model Binding and think you can't use Eager Loading for relationships, think again.

So you use Route Model Binding

public function show(Product $product) {
    //
}

But you have a belongsTo relationship, and cannot use $product->with('category') eager loading?

You actually can! Load the relationship with ->load()

public function show(Product $product) {
    $product->load('category');
    //
}

Localizing Resource URIs

If you use resource controllers, but want to change URL verbs to non-English, so instead of /create you want Spanish /crear, you can configure it with Route::resourceVerbs() method.

public function boot()
{
    Route::resourceVerbs([
        'create' => 'crear',
        'edit' => 'editar',
    ]);
    //
}

Resource Controllers naming

In Resource Controllers, in routes/web.php you can specify ->names() parameter, so the URL prefix and the route name prefix may be different.

This will generate URLs like /p, /p/{id}, /p/{id}/edit etc. But you would call them:

  • route('products.index)
  • route('products.create)
  • etc
Route::resource('p', \App\Http\Controllers\ProductController::class)->names('products');

Easily highlight your navbar menus

Use Route::is('route-name') to easily highlight your navbar menus

<ul>
    <li @if(Route::is('home')) class="active" @endif>
        <a href="/">Home</a>
    </li>
    <li @if(Route::is('contact-us')) class="active" @endif>
        <a href="/contact-us">Contact us</a>
    </li>
</ul>

Tip given by @anwar_nairi

Generate absolute path using route() helper

route('page.show', $page->id);
// http://laravel.test/pages/1

route('page.show', $page->id, false);
// /pages/1

Tip given by @oliverds_

Override the route binding resolver for each of your models

You can override the route binding resolver for each of your models. In this example, I have no control over the @ sign in the URL, so using the resolveRouteBinding method, I'm able to remove the @ sign and resolve the model.

// Route
Route::get('{product:slug}', Controller::class);

// Request
https://nodejs.pub/@unlock/hello-world

// Product Model
public function resolveRouteBinding($value, $field = null)
{
    $value = str_replace('@', '', $value);

    return parent::resolveRouteBinding($value, $field);
}

Tip given by @Philo01

If you need public URL, but you want them to be secured

If you need public URL but you want them to be secured, use Laravel signed URL

class AccountController extends Controller
{
    public function destroy(Request $request)
    {
        $confirmDeleteUrl = URL::signedRoute('confirm-destroy', [
            $user => $request->user()
        ]);
        // Send link by email...
    }

    public function confirmDestroy(Request $request, User $user)
    {
        if (! $request->hasValidSignature()) {
            abort(403);
        }

        // User confirmed by clicking on the email
        $user->delete();

        return redirect()->route('home');
    }
}

Tip given by @anwar_nairi

Using Gate in middleware method

You can use the gates you specified in App\Providers\AuthServiceProvider in middleware method.

To do this, you just need to put inside the can: and the names of the necessary gates.

Route::put('/post/{post}', function (Post $post) {
    // The current user may update the post...
})->middleware('can:update,post');

Simple route with arrow function

You can use php arrow function in routing, without having to use anonymous function.

To do this, you can use fn() =>, it looks easier.

// Instead of
Route::get('/example', function () {
    return User::all();
});

// You can
Route::get('/example', fn () => User::all());

Route view

You can use Route::view($uri , $bladePage) to return a view directly, without having to use controller function.

//this will return home.blade.php view
Route::view('/home', 'home');

Route directory instead of route file

You can create a /routes/web/ directory and only fill /routes/web.php with:

foreach(glob(dirname(__FILE__).'/web/*', GLOB_NOSORT) as $route_file){
    include $route_file;
}

Now every file inside /routes/web/ act as a web router file and you can organize your routes into different files.

Route resources grouping

If your routes have a lot of resource controllers, you can group them and call one Route::resources() instead of many single Route::resource() statements.

Route::resources([
    'photos' => PhotoController::class,
    'posts' => PostController::class,
]);

Custom route bindings

Did you know you can define custom route bindings in Laravel?

In this example, I need to resolve a portfolio by slug. But the slug is not unique, because multiple users can have a portfolio named 'Foo'

So I define how Laravel should resolve them from a route parameter

class RouteServiceProvider extends ServiceProvider
{
    public const HOME = '/dashboard';

    public function boot()
    {
        Route::bind('portfolio', function (string $slug) {
            return Portfolio::query()
                ->whereBelongsto(request()->user())
                ->whereSlug($slug)
                ->firstOrFail();
        });
    }
}
Route::get('portfolios/{portfolio}', function (Portfolio $portfolio) {
    /*
     * The $portfolio will be the result of the query defined in the RouteServiceProvider
     */
})

Tip given by @mmartin_joo

Two ways to check the route name

Here are two ways to check the route name in Laravel.

// #1
<a
    href="{{ route('home') }}"
    @class="['navbar-link', 'active' => Route::current()->getName() === 'home']"
>
    Home
</a>
// #2
<a
    href="{{ route('home') }}"
    @class="['navbar-link', 'active' => request()->routeIs('home)]"
>
    Home
</a>

Tip given by @AndrewSavetchuk

Route model binding soft-deleted models

By default, when using route model binding will not retrieve models that have been soft-deleted. You can change that behavior by using withTrashed in your route.

Route::get('/posts/{post}', function (Post $post) {
    return $post;
})->withTrashed();

Tip given by @cosmeescobedo

Retrieve the URL without query parameters

If for some reason, your URL is having query parameters, you can retrieve the URL without query parameters using the fullUrlWithoutQuery method of request like so.

// Original URL: https://www.amitmerchant.com?search=laravel&lang=en&sort=desc
$urlWithQueryString = $request->fullUrlWithoutQuery([
    'lang',
    'sort'
]);
echo $urlWithQueryString;
// Outputs: https://www.amitmerchant.com?search=laravel

Tip given by @amit_merchant

Customizing Missing Model Behavior in route model bindings

By default, Laravel throws a 404 error when it can't bind the model, but you can change that behavior by passing a closure to the missing method.

Route::get('/users/{user}', [UsersController::class, 'show'])
    ->missing(function ($parameters) {
        return Redirect::route('users.index');
    });

Tip given by @cosmeescobedo

Exclude middleware from a route

You can exclude middleware at the route level in Laravel using the withoutMiddleware method.

Route::post('/some/route', SomeController::class)
    ->withoutMiddleware([VerifyCsrfToken::class]);

Tip given by @alexjgarrett

Controller groups

Instead of using the controller in each route, consider using a route controller group. Added to Laravel since v8.80

// Before
Route::get('users', [UserController::class, 'index']);
Route::post('users', [UserController::class, 'store']);
Route::get('users/{user}', [UserController::class, 'show']);
Route::get('users/{user}/ban', [UserController::class, 'ban']);
// After
Route::controller(UsersController::class)->group(function () {
    Route::get('users', 'index');
    Route::post('users', 'store');
    Route::get('users/{user}', 'show');
    Route::get('users/{user}/ban', 'ban');
});

Tip given by @justsanjit