Search code examples
laravellaravel-8

Laravel route model binding without global scope


I have following route group in my laravel 8.0 app:

Route::prefix('offline_transaction')->name('offline_transaction.')->group(function () {
    Route::post('/approve/{transaction:uuid}', [OfflineTransactionController::class, 'approve'])
        ->name('approve');

    Route::post('/reject/{transaction:uuid}', [OfflineTransactionController::class, 'reject'])
        ->name('reject');
});

And Transaction model is:


class Transaction extends Model implements CreditBlocker
{
    //....
    protected static function boot()
    {
        parent::boot();
        static::addGlobalScope(new AuthUserScope());
    }
    //....
}

And this is my AuthUserScope:

class AuthUserScope implements Scope
{
    private string $fieldName;

    public function __construct($fieldName = 'user_id')
    {
        $this->fieldName = $fieldName;
    }

    public function apply(Builder $builder, Model $model)
    {
        $user = Auth::user();
        if ($user) {
            $builder->where($this->fieldName, $user->id);
        }
    }
}

Now the problem is when an admin wants to approve or reject a transaction, 404 Not found error will throws. How can I pass this?


Solution

  • Customizing The Resolution Logic

    If you wish to define your own model binding resolution logic, you may use the Route::bind method. The closure you pass to the bind method will receive the value of the URI segment and should return the instance of the class that should be injected into the route. Again, this customization should take place in the boot method of your application's RouteServiceProvider:

    Solution

    What you can do is change the parameter name(s) in your routes/web.php file for the specific route(s).

    Route::prefix('offline_transaction')->name('offline_transaction.')->group(function () {
        Route::post('/approve/{any_transaction}', [OfflineTransactionController::class, 'approve'])
            ->name('approve');
    
        Route::post('/reject/{any_transaction}', [OfflineTransactionController::class, 'reject'])
            ->name('reject');
    

    Note the any_transaction. Change that to whatever naming convention you find most convenient.

    Then, in your app/Providers/RouteServiceProvider.php file, change your boot(...) method to something like this:

    use App\Models\Transaction;
    use Illuminate\Support\Facades\Route;
    // ...
        public function boot()
        {
           // ...
    
            Route::bind('any_transaction', function($uuid) {
                return Transaction::withoutGlobalScopes()->where('uuid', $uuid)->firstOrFail();
            });
    
           // ...
        }
    // ...
    

    Then in your controller app/Http/Controllers/OfflineTransactionController.php file, access the injected model:

    use App\Models\Transaction;
    // ...
    
    public function approve(Transaction $any_transaction) {
    
    // ...
    
    }
    
    // ...
    

    Credits: Using Route Model Binding without Global Scope @thomaskim

    Addendum

    If you would like to remove a specific global scope from the route model bound query, you may use withoutGlobalScope(AuthUserScope::class) in the boot(...) method of the app/Providers/RouteServiceProvider.php file.