Search code examples
mysqllaraveleloquent-relationshiplaravel-9

laravel 9 route model binding with multiple optional parameters


I have this route defined:

Route::get('/categories/{category}/{country}/{state?}/{county?}/{city?}', ['App\Http\Controllers\LocationController', 'show'])->withScopedBindings();

public function show(Category $category, Country $country, State $state = null, County $county = null, City $city = null) {
   echo 'ok';
}

It's fine, automatically checks for relationships, works with 1, 2 or 3 of optional parameters. But... I want to extend it so it that the COUNTY is not always mandatory. Cause there are some cities that have direct relationship to state without county_id in the middle. Cities table has county_id and also state_id and always only one of them is present. If I add:

Route::get('/categories/{category}/{country}/{state?}/{city?}', ['App\Http\Controllers\LocationController', 'show'])->withScopedBindings();

Only one of the routes are working.

How can I fix this? Thanks.


Solution

  • So, following Donkarnash answer I finally have a solution.

    Route::get('/categories/{category}/{country}/{state?}/{county_slug?}/{city_slug?}',
        ['App\Http\Controllers\LocationController', 'show'])->scopeBindings();
    
    public function show(
        Category $category,
        Country $country,
        State $state = null,
        $county_slug = null,
        $city_slug = null
    ) {
        if ($county_slug && $city_slug)
        {
            // two parameters present, that means the chain is state -> county -> city
            $county = County::where('state_id', $state->id)
                ->where('slug', $county_slug)
                ->firstOrFail();
            $city = City::where('county_id', $county->id)
                ->where('slug', $city_slug)
                ->firstOrFail();
        } else {
            if ($county_slug) {
                // one parameter present, that means the chain is state -> county OR state -> city
                $county = County::where('state_id', $state->id)
                    ->where('slug', $county_slug)
                    ->first();
                if (!$county) {
                    $city_slug = $county_slug;
                    $city = City::where('state_id', $state->id)
                        ->where('slug', $city_slug)
                        ->first();
                }
                if (!$county && !$city) {
                    abort(404);
                }
            }
        }
    }
    

    And then in migrations states table:

    $table->unique(['slug', 'country_id']);
    

    Counties table:

    $table->unique(['slug', 'state_id']);
    

    Cities table:

    $table->unique(['slug', 'state_id']);
    $table->unique(['slug', 'county_id']);
    

    And it works. Only drawback is that if there is a county and a city with the same slug that belong to the same state. For example, county with slug "test" and state_id "15" and city with slug "test" and state_id "15". Then it won't work correctly and result as county. But usually ALL cities for a country have a chain of country -> state -> county -> city OR country -> state -> city, so this minor drawback won't affect final results for the website. Nevertheless, this also can be fixed by adjusting request validation rules.