- The problem: admin routes with route bindings
- Option 1: Introduce logic into your global scope
- Option 2: Named route bindings
- Other options
In Laravel global scopes are extremely useful. Global scopes allow you to add segments to every query that is run on a model.
Let's take a look at the following example:
class Post extends Model
{
protected static function booted()
{
static::addGlobalScope('published', function (Builder $builder) {
$builder->where('published_at', '<=', now());
});
}
}
If this isn't obvious it adds a global scope that ensures all posts have a published_at
datetime/timestamp in the past. This means you don't have to worry about checking this date all over your app when pulling posts.
Global scopes like this are very popular in Laravel, and they're very scalable as you can easily add functionality such as the published
flag above without having to refactor lots of parts of your app.
The problem: admin routes with route bindings
Keeping the same example of posts, you probably have an area of your application where you're able to add, edit and delete posts as an admin.
If you're using route bindings then this causes some problems.
What are route bindings?
Route bindings are a feature of Laravel that allows the framework to look up a model and 404 if it can't be found. You can specify the model and the field to lookup as part of the URI in the route.
Same example flow as above, here's how an admin route binding might look:
Route::middleware(['auth', 'admin'])->name('admin.')->prefix('admin')->group(function () {
Route::get('/blogs/{post:id}/edit', [AdminBlogsController::class, 'edit'])->name('blogs.edit');
Route::post('/blogs/{post:id}/update', [AdminBlogsController::class, 'update'])->name('blogs.update');
});
This defines the following URIs:
/admin/blogs/1/edit
/admin/blogs/1/update
Where 1
is the ID of the blog. The problem is that as soon as you introduce the global scope then any unpublished posts won't be editable by the admin. This obviously won't work for us, so how do we handle it?
Option 1: Introduce logic into your global scope
In our above example we don't want admins to have the global scope to be applied. One fix for this is to still apply the global scope, but add some logic to global scope itself to check if its an admin.
// Post.php
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
// [...]
protected static function booted(): void
{
static::addGlobalScope('published', function (Builder $builder) {
if(!Auth::check() || (Auth::check() && !Auth::user()->isSuperAdmin()) ) {
$builder->where('published_at', '<=', now());
}
});
}
Here we've added an if-statement that only applies the Builder
rule/query if the user isn't logged in or they are logged in but aren't an admin.
This approach has a few advantages. Because it globally removes the rule for admins (eg not just a specific route) then it opens up other routes for access, such as the ability to view posts that haven't yet been published on the frontend of our app. In our use case that's a definite bonus, but you should understand the implications of this in your app.
Option 2: Named route bindings
Route binding happens based on the name of the field and model that you pass through. For example, the following route definition:
Route::get('/blogs/{post:id}/edit', [AdminBlogsController::class, 'edit'])->name('blogs.edit');
Is looking at the Post
model on the id
column/field. Laravel's under-the-hood magic takes care of this for us, however it's possible for us to define our own route bindings.
Take the following route definition:
Route::get('/blogs/{admin_blog}/edit', [AdminBlogsController::class, 'edit'])->name('blogs.edit');
This allows use to attach a rule to admin_blog
routes anywhere they're used.
Add the following to your RouteServiceProvider.php
file:
public function boot()
{
parent::boot();
Route::bind('admin_blog', function($id) {
return \App\Model\Post::withoutGlobalScopes()->findOrFail($id);
});
}
This will intercept any route with an admin_blog
segment and run that query for it. This gives much more flexibility than option #1, but if this is an existing app then you'll need to make changes to anywhere you link to the route already.
route('admin.blogs.edit', ['blog' => $blog]);
will need to become:
route('admin.blogs.edit', ['admin_blog' => $blog]);
It's a small change, but if you have a large app this could become problematic.
Other options
You do have other options but they depend heavily on how your app has been put together. Other options include removing the global scope in middleware, or holding the logic in a service, or inside your controllers (although we wouldn't recommend this).
Interested in proving your knowledge of this topic? Take the PHP Fundamentals certification.
PHP Fundamentals
Covering the required knowledge to create and build web applications in PHP.
$99
Related articles
Tutorials PHP Database Design Tooling
When and how to squash migrations
Learn about squashing migrations in Laravel, a pivotal technique for optimizing your application's efficiency and maintainability. This guide covers the why behind migration squashing and provides a tutorial on implementing it.