Nested Resource Controllers and Routes: Laravel CRUD Example

When building CRUD-like projects, sometimes you want some items be accessible only with their parent, for example in countries-cities relationships, you don't want to list all the cities in the world, but only by country, like /countries/123/cities, where 123 is country_id. This article will show you how to do it, using Route::resource() and usual CRUD controllers.

Here's our demo project - CRUD for Countries (mostly generated with our QuickAdminPanel):

You can see Cities column here, and that link on the number will be the only way to access City management. There won't be any specific /cities URL, only the one that contains Country.

That's how you would typically describe two CRUDs in routes/web.php file, right?

Route::resource('countries', 'CountriesController');
Route::resource('cities', 'CitiesController');

Now, let's meet NESTED RESOURCE CONTROLLERS - here's the syntax that you can use, with a dot symbol:

Route::resource('countries.cities', 'CitiesController');

That means that all URLs should contain /countries/[country_id]/ prefix. Also, it means that $country_id should be a parameter in ALL methods of CitiesController.

Here's the full list of how URLs will look for all seven resource methods for Cities CRUD:

  • index($country_id) - GET request to /countries/X/cities
  • create($country_id) - GET request to /countries/X/cities/create
  • store($country_id, Request $request) - POST request to /countries/X/cities
  • show($country_id, City $city) - GET request to /countries/X/cities/Y
  • edit($country_id, City $city) - GET request to /countries/X/cities/Y/edit
  • update($country_id, City $city, Request $request) - PUT request to /countries/X/cities/Y
  • destroy($country_id, City $city) - DELETE request to /countries/X/cities/Y
List of cities by country - CitiesController@index method

As you can see, $country_id is a parameter everywhere now. Here's the full code for our CitiesController:

class CitiesController extends Controller
{
    public function index($country_id)
    {
        $cities = City::where('country_id', $country_id)->get();
        return view('admin.cities.index', compact('cities', 'country_id'));
    }

    public function create($country_id)
    {
        return view('admin.cities.create', compact('country_id'));
    }

    public function store($country_id, Request $request)
    {
        City::create($request->all() + ['country_id' => $country_id]);
        return redirect()->route('countries.cities.index', $country_id);
    }

    public function edit($country_id, City $city)
    {
        return view('admin.cities.edit', compact('country_id', 'city'));
    }

    public function update($country_id, Request $request, City $city)
    {
        $city->update($request->all());
        return redirect()->route('countries.cities.index', $country_id);
    }

    public function show($country_id, City $city)
    {
        return view('admin.cities.show', compact('country_id', 'city'));
    }

    public function destroy($country_id, City $city)
    {
        $city->delete();
        return redirect()->route('countries.cities.index', $country_id);
    }
}

Here are some details to pay attention:

  • We're passing 'country_id' to all the views in compact() method, cause then it is used in Blade to form all the links (will have example below)
  • We're redirecting to route with naming countries.cities.index and passing the parameter of country_id
  • When storing the city record, we don't have country_id in $request->all(), so we merge it from the method parameter: City::create($request->all() + ['country_id' => $country_id]);
Country won't be a choice in city create form, we will take country_id from URL

Now, here's how our Blade files are structured.

First, resources/views/admin/cities/index.blade.php should contain a few URLs:

Link for Adding new city to a particular country:

<a href="{{ route("countries.cities.create", $country_id) }}">Add City</a>

Link to Edit a particular city:

<a href="{{ route('countries.cities.edit', [$city->country_id, $city->id]) }}">Edit</a>

Form to Delete a particular city:

<form action="{{ route('countries.cities.destroy', [$city->country_id, $city->id]) }}" method="POST">
    <input type="hidden" name="_method" value="DELETE">
    <input type="hidden" name="_token" value="{{ csrf_token() }}">
    <input type="submit" class="btn btn-xs btn-danger" value="Delete">
</form>

As you can see, sometimes we have parameter country_id as part of $city object, in other cases - we take it from Controller as $country_id.

A few more last examples.

File resources/views/admin/cities/create.blade.php contains a form to add a city, which looks like this:

<form action="{{ route("countries.cities.store", $country_id) }}" method="POST">

File resources/views/admin/cities/edit.blade.php contains a form to update the city, which looks like this:

<form action="{{ route("countries.cities.update", [$city->country_id, $city->id]) }}" method="POST">

And that's it! This way, we made our cities accessible to manage only by country, and not in one huge list of cities.

Interestingly, in the official Laravel documentation you can find Nested Resources only in 5.1 version docs, in current 5.7 they seem to be removed. So probably this feature was not popular enough? But hey, it's still working!

No comments or questions yet...

Like our articles?

Become a Premium Member for $129/year or $29/month
What else you will get:
  • 57 courses (1055 lessons, total 46 h 42 min)
  • 78 long-form tutorials (one new every week)
  • access to project repositories
  • access to private Discord

Recent Premium Tutorials