skip to Main Content

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 AppHttpResourcesUserSimpleWithRoleResource;
use AppModelsAssignment;
use IlluminateDatabaseEloquentModel;

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', AppHttpApiOrganizationsController::class)->withTrashed(['show', 'update']);
Route::apiResource('regions', AppHttpApiRegionsController::class)->withTrashed(['show', 'update']);
Route::apiResource('locations', AppHttpApiLocationsController::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?

2

Answers


  1. Chosen as BEST ANSWER

    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 IlluminateRoutingRoute 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.

  2. I think you can do it like this in RouteServiceProvider.
    Where one name for model binding is used for routes where you do want softdeletes in the model binding, and one without softdeletes in the model binding.

    Route::bind('location', function ($value) {
        return Location::where('id', $value)->firstOrFail();
    });
    Route::bind('location-with-trashed', function ($value) {
        return Location::withTrashed()->where('id', $value)->firstOrFail();
    });
    

    Though you need to test if this works.

    See here: https://github.com/laravel/framework/issues/43008#issuecomment-1170673827

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search