SeanKimball wrote a reply+100 XP
5mos ago
This is my middleware
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Middleware to enforce parent-child model relationships in nested routes.
* Extracts route parameters in sequence and builds Eloquent queries to validate relationship chains.
*/
class EnforcesModelRelations
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next): Response
{
$parameters = $request->route()->parameters();
$models = array_filter($parameters, fn($p) => $p instanceof Model);
// Walk through models and validate each belongs to previous
$previous = null;
foreach ($models as $paramName => $current) {
if ($previous && method_exists($previous, 'resolveChildRouteBinding')) {
$resolved = $previous->resolveChildRouteBinding(
class_basename($current),
$current->getKey(),
$current->getKeyName()
);
if (!$resolved) {
throw (new ModelNotFoundException)->setModel(get_class($current), [$current->getKey()]);
}
}
$previous = $current;
}
return $next($request);
}
}
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 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 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:
- My route definitions feel sloppy and repetitive - is there a better (dynamic) way to define the note methods on different models?
- 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.