We all know how route model binding works in Laravel. For instance, if you want to display a single post, you would write something like this in your code:

// Route file
Route::get('posts/{post}', [PostController::class, 'show'])->name('posts.show');

// Controller
class PostController
{
    public function show(Post $post): View
    {
        return view('posts.show', compact('post'));
    }
}

In this example, the route model binding retrieves an instance of the post model based on the post ID. If a visitor wants to view the post with an ID of 1, they can access it using the posts.show route.

However, what if you need to add custom logic? For example, if you don't want visitors to see an unpublished post, you can add a check in the show method like this:

public function show(Post $post): View
{
    if (! $post->is_published) {
        abort(404);
    }

    return view('posts.show', compact('post'));
}

But this solution only takes care of the problem within the show view. To guarantee that visitors cannot access an unpublished post, you need to add the same check in every method that depends on a post being published.

A better solution is to customize the binding logic. Here's how you can do that:

// Route file
Route::get('posts/{published_post}', [PostController::class, 'show'])->name('posts.show');
Route::bind('published_post', function ($value) {
    return Post::query()
        ->where('id', $value)
        ->published()
        ->firstOrFail();
});

// Controller
class PostController
{
    public function show(Post $post): View
    {
        return view('posts.show', compact('post'));
    }
}

The bind method returns a 404 error for any unpublished post. Now, wherever you need to use the post parameter in the visitors' domain, you can use the published_post parameter to guarantee that visitors cannot access an unpublished post.

Alternatively, the bind logic can be moved to the boot method of the route service provider if desired, rather than being placed in the route file.

Happy coding!

Note: The last code snippet assumes that you have a published scope in the Post model.