Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel: pass field to resolveRouteBinding from route definition

I have a seemingly stupid question here but I can't find how one is supposed to define explicit model binding resolution logic using resolveRouteBinding method on a Model.

I managed to write down its logic which is based on the value of the field parameter but I can't seem to work out how to pass a value to this parameter when I define the route.

Can anyone please help me out?

EDIT: The proper question is how to tell the framework what value to pass to the field parameter?

EDIT 2: Here's an example of what I'm doing without luck.

Basically I want to implement a custom logic to retrieve a class representing a many-to-many relationship.

These are the master classes.

class Competition extends Model {
    // ...

    /**
     * Get the editions of the competition.
     */
    public function competitionSeasons() {
        return $this->hasMany(CompetitionSeason::class);
    }
}
class Season extends Model {
    // ...

    /**
     * Get the editions of the competitions in this season
     */
    public function competitionsSeasons() {
        return $this->hasMany(CompetitionSeason::class);
    }
}

This is the relationship class.

class CompetitionSeason extends Model {
    // ...

    /**
     * Get the competition of this competition edition.
     */
    public function competition() {
        return $this->belongsTo(Competition::class);
    }

    /**
     * Get the season of this competition edition.
     */
    public function season() {
        return $this->belongsTo(Season::class);
    }

    public function resolveRouteBinding($value, $field = null) {
        error_log("[resolveRouteBinding] Field: " . $field);

        // TODO: make the query
    }

    public function resolveChildRouteBinding($childType, $value, $field = null) {
        error_log("[resolveChildRouteBinding] Field: " . $field);

        // TODO: make the query
    }
}

What I want now is to have a CompetitionSeason injected in the controller as a nested resource under Season (and also under Competition later on).

So this is how the route has been defined.

// api.php

Route::apiResource('seasons.competitionSeasons', MockController::class)->scoped([
    'competitionSeason' => 'competition'
]);

Since I'm using slugs on the routes rather than IDs, competition here would be needed in any of the resolveBinding methods to navigate the relationship and get the slug from the Competition instance.

Basically, the request would be like this:

GET .../api/seasons/19-20/competitionSeasons/premier-league

where premier-league is the slug of the referenced Competition, the other end of the many-to-many relationship basically.

This is the Controller.. completely useless for now.

class MockController extends Controller {

    public function show(Season $season, CompetitionSeason $competitionSeason) {
        error_log($competitionSeason);
    }
}

I know how to implment the missing logic, I have it very clear, what I would like to see is the $field being printed when the framework attempts to resolve the required instance of CompetitionSeason so that I can get the slug of the associated resource as well as to know which relationship to navigate when CompetitionSeason will be a child resource of Competition too.

like image 937
wileecoyote Avatar asked Sep 19 '25 10:09

wileecoyote


2 Answers

That would be when you customize the key used for Implicit Route Model Binding via the route definition:

Route::get('users/{user:name}', ...);

'name' would end up being passed as the $field to that method call.

From Illuminte\Database\Eloquent\Model:

public function resolveRouteBinding($value, $field = null)
{
    return $this->resolveRouteBindingQuery($this, $value, $field)->first();
}

public function resolveRouteBindingQuery($query, $value, $field = null)
{
    return $query->where($field ?? $this->getRouteKeyName(), $value);
}

Laravel 8.x Docs - Routing - Route Model Binding - Implicit Bindings - Customizing the Key

Update:

If you need to create this custom binding with Resource Routing you can use the scoped method on the route definition:

Route::resource('users', Controller::class)
    ->scoped(['user' => 'name']);

You also could use the $options argument (3rd argument) on resource directly:

Route::resource('users', Controller::class, [
    'bindingFields' => [
        'user' => 'name'
    ]
]);
like image 113
lagbox Avatar answered Sep 22 '25 02:09

lagbox


I find out how it is supposed to work even though I don't know why it's like this.. (some considerations below).

Basically the public function resolveChildRouteBinding I should override is the parent, so Season in this case.

So now Season looks like this:

class Season extends Model {
    // ...

    public function competitionsSeasons() {
        return $this->hasMany(CompetitionSeason::class);
    }


    public function resolveChildRouteBinding($childType, $value, $field = null) {
        error_log("[resolveChildRouteBinding] Field: " . $field);

        if ($childType == 'competitionSeason') {
            if ($field == 'competition' {
                // TODO: make the query
            }
        }
    }
}

and here I can see $field being competition with $value equal to premier-league.

Now some considerations: why should the parent class be the one that returns the instance of the child?

Basically, if I go on and implement it like this, the resolveChildRouteBinding will be the one to return the CompetitionSeason while I would have expected the resolution logic on CompetitionSeason itself to be what determines the instance to inject. Am I the only one?

Thank you to @lagbox for pointing out the existence of resolveChildRouteBinding which I completely ignored for whatever reason (maybe deserves a little more space in the docs?)

like image 42
wileecoyote Avatar answered Sep 22 '25 04:09

wileecoyote