Table of Contents
Global query scopes are great for filtering production data โ hiding soft-deleted records, filtering by status, etc. But when you’re building admin tools, those same scopes become obstacles.
Here’s the problem: you’re building a translation editor for your e-commerce dashboard. Your Product model has a global scope that hides discontinued products. But translators need to update ALL products, including discontinued ones, because those translations might be needed for historical orders or future reactivation.
The Pattern: Explicit Scope-Bypassing Methods
Instead of fighting with scopes everywhere, create dedicated methods that explicitly bypass them:
// In your Order model
public function all_items()
{
return $this->hasMany(OrderItem::class)
->withoutGlobalScopes()
->get();
}
// Usage in admin controllers
$order = Order::find($id);
$allItems = $order->all_items(); // Gets even soft-deleted items
Compare this to the default relationship:
// Normal relationship - respects global scopes
$order->items; // Hides discontinued/soft-deleted items
// Admin relationship - explicit about bypassing
$order->all_items(); // Shows everything
Why This Matters
This pattern:
- Makes intent explicit:
all_items()signals “I know what I’m doing, show me everything” - Isolates scope-bypassing: Only admin code uses these methods; production code uses normal relationships
- Prevents bugs: No accidental scope bypassing in customer-facing features
- Self-documenting: Method name explains why scopes are bypassed
When to Use This
- Admin dashboards that need full data visibility
- Translation/content management interfaces
- Data export tools
- Debugging utilities
- Bulk operations that shouldn’t skip “hidden” records
Alternative Approaches
You could use withoutGlobalScopes() inline everywhere:
$order->items()->withoutGlobalScopes()->get();
But this is:
- Verbose and repetitive
- Easy to forget in some places
- Harder to grep for “where are we bypassing scopes?”
A named method is cleaner, easier to test, and more maintainable.
Bonus: Selective Scope Bypassing
You can also bypass specific scopes while keeping others:
public function published_items_including_deleted()
{
return $this->hasMany(OrderItem::class)
->withoutGlobalScope(SoftDeletingScope::class)
->get();
}
This keeps your “published” scope active but removes soft delete filtering โ useful when you want partial bypassing.
Remember: Global scopes exist to protect production users from seeing the wrong data. When you need admin superpowers, make it explicit with dedicated methods. Your future self (and your code reviewers) will thank you.
Leave a Reply