Laravel Core

Laravel Routes with Optional Parameters

Tuesday, October 29, 2024

// 2 min read

Laravel 11 Routing
Laravel Optional Route

This article focuses on optional parameters for Laravel Routes.

If you're looking for a general explanation of how Laravel routing works, read my previous article.

Define Route parameters

To add a parameter in a route definition, you can use curly braces, like this:

Route::get('/post/{id}', [PostController::class, 'show']);

However, this parameter will be required.

Syntax for Optional Parameters

To indicate that a parameter is optional, you can use a question mark (?) after the parameter name.

Let's imagine a scenario in which we need to create a route to list products for an e-commerce store. We also want to allow users to filter products by category. The category will be an optional parameter. Here’s how the route can be defined:

Route::get('/products/{category?}', [ProductController::class, 'index']);

In the controller, you can set the category parameter optional. When defined, the products can be filtered by category.

<?php

namespace App\Http\Controllers;

use App\Models\Product;

class ProductController extends Controller
{
    public function index(?string $category = null)
    {
        $products = Product::query()
            ->when($category, function ($query) use ($category) {
                return $query->whereHas('category', function ($q) use ($category) {
                    return $q->where('slug', $category);
                });
            })
            ->get();

        return view('products.index', compact('products'));
    }
}

When the user loads the /products page, all products are displayed.
However, when the user navigates to the /products/shoes page, only the products with the shoes category are shown.

Default value

You can also set a default value for an optional parameter when it is not specified in the URL. For instance, you can create routes for paginated post pages where the page parameter is optional:

Route::get('/posts/{page?}', [PostController::class, 'index']);

In the controller, you can set a default value (1)for the page parameter. This way, it will always query the first page if no page is specified (/posts).

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index(int $page = 1)
    {
        $posts = Post::paginate(perPage: 5, page: $page);

        return view('post.index', [
            'posts' => $posts,
        ]);
    }
}

Rules for Ordering Optional and Required Parameters

All parameters after the first optional parameter must be optional. You cannot place a required parameter after an optional parameter, if you do, the parameter before the required one also becomes required.

For instance, if you have a route for calendar events that accepts a year and a month parameter, you can choose to make either both parameters optional or both required. You can make the year required and the month optional. However, you cannot make the year optional while requiring the month.

The following route definition won't work:

Route::get('/events/{year?}/{month}', [EventController::class, 'index']);

If you change both parameters to optional in the route definition but still keep the month parameter required in your controller method, you will receive an error.

public function index(int $year = null, int $month)

Route optional error

The correct route definition for this is:

Route::get('/events/{year?}/{month?}', [EventController::class, 'index']);

This is how I handle it in the controller method:

<?php

namespace App\Http\Controllers;

use App\Models\Event;

class EventController extends Controller
{
    public function index(?int $year = null, ?int $month = null)
    {
        $events = Event::when($year, function ($query, $year) {
            return $query->where(function ($q) use ($year) {
                return $q->whereYear('start_date', $year)->orWhereYear('end_date', $year);
            });
        })->when($month, function ($query, $month) {
            return $query->where(function ($q) use ($month) {
                return $q->whereMonth('start_date', $month)->orWhereMonth('end_date', $month);
            });
        })->get();

        return view('event.index', compact('events'));
    }
}

It's important to understand that you cannot place an optional parameter between required route segments.

For instance, in the following URL pattern for events, the year parameter must be required, otherwise, the route won't match for either events or events/2024.

This won't work:

Route::get('/events/{year?}/month/{month?}', [EventController::class, 'index']);

But this will for the following events/2024/month:

Route::get('/events/{year}/month/{month?}', [EventController::class, 'index']);

Here are the most important things you need to know about optional routes. For more information, read my article about the basics of routing in Laravel and refer to the routing section of the official Laravel documentation.