Search code examples
phplaraveleloquentsoft-delete

Allow Laravel routes to specify `withTrashed` despite explicit model bindings


Background

In my Laravel application, I have models Organization, Region, and Location, which all have a polymorphic relationship to the User model via an Assignment model. (An Assignment is a three-way relationship between User, Role, and one of the other three entities, but that is not strictly relevant to the problem here.)

I wanted to add a showUsers method to each of the controllers for the other three models that gets all of the users associated with that entity. To avoid copy-pasting the same code in all three controllers, I created a trait like this:

use App\Http\Resources\UserSimpleWithRoleResource;
use App\Models\Assignment;
use Illuminate\Database\Eloquent\Model;

trait GetsRelatedUsers
{
    function listUsers(Model $model)
    {
        // Thanks to explicit model binding,
        // `$model` can be any supported model.

        $assignments = Assignment::with(['user:id,given_name,surname', 'role:id,name,display_order'])
            ->has('user') // prevents assignments for soft-deleted users from showing up
            ->atPlace($model) // a scope that also adds other conditions based on the provided model
            ->get();

        return UserSimpleWithRoleResource::collection($assignments);
    }
}

In order to resolve the Model $model value from the route, I need to have explicit model bindings in my RouteServiceProvider's boot method, like this:

Route::model('location', Location::class);
Route::model('region', Region::class);
Route::model('organization', Organization::class);

Problem

In my routes file, I have the three controllers for Organization, Region, and Location set up to allow viewing and editing soft-deleted models like this:

Route::apiResource('organizations', \App\Http\Api\Organizations\Controller::class)->withTrashed(['show', 'update']);
Route::apiResource('regions', \App\Http\Api\Regions\Controller::class)->withTrashed(['show', 'update']);
Route::apiResource('locations', \App\Http\Api\Locations\Controller::class)->withTrashed(['show', 'update']);

Before I added the explicit model binding, the built-in implicit model binding would check the route for the presence of the withTrashed option, and would change the model resolver to include trashed items. However, explicit bindings don't do this check.

Short of completely reimplementing the check for $route->allowsTrashedBindings() in my explicit model bindings, is there a nice way to implement this so that some routes can include trashed items and others cannot?


Solution

  • Here's the best I've been able to come up with so far, though I'm not really happy with it:

    Route::bind('location', function ($id, RouteDefintion $route) {
        return Location::query()
            ->when($route->allowsTrashedBindings(), function ($query) {
                $query->withTrashed();
            })
            ->findOrFail($id);
    });
    
    Route::bind('region', function ($id, RouteDefintion $route) {
        return Region::query()
            ->when($route->allowsTrashedBindings(), function ($query) {
                $query->withTrashed();
            })
            ->findOrFail($id);
    });
    
    Route::bind('organization', function ($id, RouteDefintion $route) {
        return Organization::query()
            ->when($route->allowsTrashedBindings(), function ($query) {
                $query->withTrashed();
            })
            ->findOrFail($id);
    });
    

    Notes:

    • RouteDefinition is use Illuminate\Routing\Route as RouteDefintion; because Route is already used by the router facade.
    • The $route parameter in the callback function is undocumented; I found it by looking through the framework source.
    • I could probably simplify this by making a function that takes a model class so that I don't have to repeat it three times, but I really want to see if there's a more elegant answer first.