SeanKimball's avatar

Nested scoped routes

I have an application that I would like to be able to attach notes to various types of models through nested scoped bindings.

/**
 * Note Routes - Using scoped bindings for polymorphic relationships
 */
Route::scopeBindings()->group(function () {
Route::post('projects/{project}/notes', [NoteController::class, 'store']);
Route::put('projects/{project}/notes/{note}', [NoteController::class, 'update']);
Route::delete('projects/{project}/notes/{note}', [NoteController::class, 'destroy']);

Route::post('clients/{client}/notes', [NoteController::class, 'store']);
Route::put('clients/{client}/notes/{note}', [NoteController::class, 'update']);
Route::delete('clients/{client}/notes/{note}', [NoteController::class, 'destroy']);

Route::post('users/{user}/notes', [NoteController::class, 'store']);
Route::put('users/{user}/notes/{note}', [NoteController::class, 'update']);
Route::delete('users/{user}/notes/{note}', [NoteController::class, 'destroy']);

Route::post('projects/{project}/trial-balances/{trialBalance}/notes', [NoteController::class, 'store']);
Route::put('projects/{project}/trial-balances/{trialBalance}/notes/{note}', [NoteController::class, 'update']);
Route::delete('projects/{project}/trial-balances/{trialBalance}/notes/{note}', [NoteController::class, 'destroy']);

Route::post('projects/{project}/trial-balances/{trialBalance}/accounts/{account}/notes', [NoteController::class, 'store']);
Route::put('projects/{project}/trial-balances/{trialBalance}/accounts/{account}/notes/{note}', [NoteController::class, 'update']);
Route::delete('projects/{project}/trial-balances/{trialBalance}/accounts/{account}/notes/{note}', [NoteController::class, 'destroy']);

Route::post('projects/{project}/trial-balances/{trialBalance}/journal-entries/{journalEntry}/notes', [NoteController::class, 'store']);
Route::put('projects/{project}/trial-balances/{trialBalance}/journal-entries/{journalEntry}/notes/{note}', [NoteController::class, 'update']);
Route::delete('projects/{project}/trial-balances/{trialBalance}/journal-entries/{journalEntry}/notes/{note}', [NoteController::class, 'destroy']);
});

I thought the scoped bindings were working, but apparently not, no errors thye would just work anyway - I found that I was not adding the expected models to the controller method

Before:

public function store(StoreNoteRequest $request): NoteResource
    {
        // Get the parent model that should receive the note
        $parent = $this->getParentFromRoute($request);

        // $data = $request->validated();
        $data['created_by'] = auth()->user()->id;

        $note = $parent->notes()->create($data);

        return new NoteResource($note);
    }

after

public function store(StoreNoteRequest $request, Project $project, User $user, TrialBalance $trialBalance, Account $account, Resource $resource): NoteResource
    {
        // Get the parent model that should receive the note
        $parent = $this->getParentFromRoute($request);
        // $data = $request->validated();
        $data['created_by'] = auth()->user()->id;
        $note = $parent->notes()->create($data);
        return new NoteResource($note);
    }

The "after" example works even if the route is not providing some of the models AND one of the method parameters can't actually get passed in the request. The way I see it this should not be working at all!

So:

  1. My route definitions feel sloppy and repetitive - is there a better (dynamic) way to define the note methods on different models?
  2. How is the method working at all?? Does not seem to matter what order they (method parameters) are in or if only some of the models are passed. Testing with several of the routes that would exist show that Laravel is actually resolving the bindings! I don't understand.
0 likes
3 replies
SeanKimball's avatar

OK - so after reading a WHOLE bunch of docs and examples, route/model binding only works when the methods are hinted with the possible model types:

public function store(
	StoreNoteRequest $request, 
	Project $project, 
	User $user, 
	TrialBalance $trialBalance, 
	Account $account, 
	Resource $resource
): NoteResource

ok - fine. however this gets incredibly verbose the more models I add that are "notable" and require updating each method in the controller.

Is there a more elegant way?

Glukinho's avatar

How about having routes for POST /notes with body like this:

{
    "entity_name": "App\Models\Project",
    "entity_id": 123,
    ... other fields
}

Inside controller or form request use validation which still is going to be complex (entity_name should be in a list or enum, entity_id must exist in a table defined by entity_name, etc).

Other routes (PUT and DELETE) are pretty much the same.

I would say that in PUT and DELETE routes a note should be referred uniquely by its id, not involving other models. So maybe your solution would be to have many POST routes (one for each entity a note is added to) and only one route PUT /note/{note} and one DELETE /note/{note}

Glukinho's avatar

And keep in mind you can define routes fully dynamically, I mean you can iterate over your models right inside routes/web.php:

$models = [
    'project',
    'client',
    'user',
];

foreach ($models as $model) {
    Route::post(Str::plural($model) . '/{' . $model . '}/notes', [NoteController::class, 'store']);
    // Route::put...
    // Route::delete...
}

It's a way to get rid of repetitive code.

Just don't forget to route:cache so your loops are not iterated on each request.

Please or to participate in this conversation.