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);
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?
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.$route
parameter in the callback function is undocumented; I found it by looking through the framework source.