SeanKimball's avatar

SeanKimball wrote a reply+100 XP

5mos ago

This is my middleware

SeanKimball's avatar

SeanKimball started a new conversation+100 XP

5mos ago

OK - I have an application that has several models that can relate to several other types of models example Resources(files) can be related to an index, an account, a user etc. anything that is "resourceable" That becomes problematic in the respect that adding all the resource crud methods to every controller for a resourceable model is a lot of problematic duplication. So I created a controller trait to handle all the resource methods (great!!) However the problem arises that I need to use route binding to ensure the model relationship chain i.e.:

Route::post('projects/{project}/file-indexes/{fileIndex}/resources', [FileIndexController::class, 'storeResource']);
Route::post('projects/{project}/accounts/{account}/resources', [FileIndexController::class, 'storeResource']);
Route::post('projects/{project}/users/{user}/resources', [FileIndexController::class, 'storeResource']);

We can't pass the type hints on to the Trait (ManagesResourcesTrait) = route/model bindings will not work So, I can pass on the models I want to work with to the ManagesResourcesTrait easily enough:

class FileIndexController extends Controller
{
    use ManagesResources;

    protected ?FileIndex $resourcableModel = null;
    protected ?Resource $resource = null;

    public function __construct(private readonly FileIndexServiceInterface $fileIndexService)
    {
        $this->authorizeResource(FileIndex::class, 'file_index');

        // Resolve the FileIndex model from route parameters for trait access
        $resourcableModelId = request()->route()->parameter('fileIndex');
        $this->resourcableModel = FileIndex::find($resourcableModelId) ?? null;

        $resourceId = request()->route()->parameter('resource');
        $this->resource = Resource::find($resourceId) ?? null;

    }
}

That makes the trait pretty easy to manage:

/**
     * Display a specific resource.
     */
    public function showResource(): ResourceResource
    {
        return new ResourceResource($this->resource);
    }

But I still need to enforce the model relations. resource belongs to an index that belongs to a project.

Is a middleware the best place to do this? I have experimented with route binding in the AppServiceProvider but it seems very messy and unmanageable.

SeanKimball's avatar

SeanKimball wrote a reply+100 XP

5mos ago

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?

SeanKimball's avatar

SeanKimball started a new conversation+100 XP

5mos ago

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.