As a continuation of the Laravel Basics course here at Treehouse, I want to extend our todo lists example to have todo list items with nested restful routing. Let’s take a look at the URL patterns for our todo lists by looking at the output of Laravel’s artisan routes command.
$ php artisan routes
$ | | GET|HEAD / | | TodoListController@index |
$ | | GET|HEAD todos | todos.index | TodoListController@index |
$ | | GET|HEAD todos/create | todos.create | TodoListController@create | |
$ | | POST todos | todos.store | TodoListController@store |
$ | | GET|HEAD todos/{todos} | todos.show | TodoListController@show |
$ | | GET|HEAD todos/{todos}/edit | todos.edit | TodoListController@edit |
$ | | PUT todos/{todos} | todos.update | TodoListController@update |
$ | | PATCH todos/{todos} | | TodoListController@update |
$ | | DELETE todos/{todos} | todos.destroy | TodoListController@destroy |
From here, we can see all of our routes that currently exist. Take, for instance, our route to edit a todo list. This shows us that we have a URL of ‘todos/{todos}/edit’ which will take a todo list id as a URL argument. Then we see the named route of ‘todos.edit’ and finally the controller name and the corresponding action of ‘TodoListController@edit’.
Using this example as our pattern, we will want to continue this trend and create a route to edit a todo list’s item by its ID as well.Our URL should look as follows:
todos/{todos}/items/{items}/edit
Taking a look at our routes.php file from our project in our app directory we will see our existing route for the todo lists.
Route::resource('todos', 'TodoListController');
If we were just mapping a new resource to a controller for our items, we could map them normally by just adding this line to our routes:
Route::resource('items', 'TodoItemController');
That would give us an URL of ‘/items/{items}/edit’ for out edit route, but that’s not what we want. The way our todo list items work is that they belong to a single list. This is why we need a nested route, and Laravel makes this extremely simple by letting us define our routes like so:
Route::resource('todos.items', 'TodoListController');
Once we make that adjustment and run our ‘artisan routes’ command, we get a much more verbose output that will contain the following two routes as well as others.
1. Our nested route to create a new item for a particular list:
todos/{todos}/items/create | todos.items.create | TodoItemController@create
2. And our nested route to edit that item by list id and item id:
todos/{todos}/items/{items}/edit | todos.items.edit | TodoItemController@edit
Now that we have our routes set up, we need to create our controller for the todo list items by using artisan.
artisan controller:make TodoItemController
Once our controller is set up, let’s create a new item! We will need to jump into our new controller first and modify our “create” action. Here is the code we will need to end up with for that action:
public function create($list_id)
{
$todo_list = TodoList::findOrFail($list_id);
return View::make('items.create')->withTodoList($todo_list);
}
Taking a look at this line by line, our first line will be our method name “create” with a single argument expected, which is our todo list id that will come from the URL.
On the next line, we will need to find our todo list resource by running a query to find or fail by our todo list id.
Once our item is found we will complete this method by returning our view to create the new resource and passing our todo list object into that view.
If you attempt to run this at the moment, you will find that our server will complain about an ‘InvalidArgumentException: View [items.create] not found.’ This is to be expected. We will need to create our folder and the corresponding views for each action. In the views folder create a new folder called ‘items’ and in that folder a file named ‘create.blade.php’ as well as a ‘partials’ folder and a ‘_form.blade.php’ file with the content as follows.
/**
* items/create.blade.php
*/
@extends('layouts.main')
@section('content')
{{ Form::open(['route' => ['todos.items.store', $todo_list->id]]) }}
@include('items.partials._form')
{{ Form::close() }}
@stop
Let’s take a look at the create.blade.php code in our items folder line by line. We are still extending our main layout, so nothing has changed there, nor do we want to change where our code appears on the page. So our first line of changed code compared to our list create page is the form open route.
Here we are using our nested named route from the routes output, which is ‘todos.items.store’. That controller action requires an argument to work correctly so we will pass the ID of the todo list by using $todo_list->id as that route argument. Inside of our form open block we are including our new partial inside of our new folder with the include tag of ‘items.partials._form’. Then to end our file we close out our form block and our section block.
/**
* items/partials/_form.blade.php
*/
{{ Form::label('content', 'List Item') }}
{{ Form::text('content') }}
{{ $errors->first('content', '<small class="error">:message</small>') }}
{{ Form::submit('submit', array('class' => 'button', )) }}
The newly created _form.php partial inside of our items folder has only a few small, but very key changes that differ from our other form partial. First, we are no longer using ‘name’ and have changed it to ‘content’ to match our database column name for the item. We have also changed the label to say ‘List Item’ and the submit button to say ‘submit’ to make a bit more sense for our users.
Once this is saved and we go to the url to create our new item, we should get a view that has a simple form for our Item. If you inspect the form, by looking at the source, you will see a POST action to ‘example.com:8000/todos/1/items’ and according to our artisan routes output that would be going to our TodoItemController@save action.
The look of the save action for the items will be very similar to the save action for the lists.
We will be receiving an method argument this time in the form of a todo list id, so we will add it in our store method and call it ‘$list_id’.
public function store($list_id)
{
Then we will define our rules, which for now is just that the content is required and pass all the input and the rules to a new Validator instance.
$rules = array(
'content' => array('required')
);
// pass input to validator
$validator = Validator::make(Input::all(), $rules);
If our input fails to pass validation, we will need to redirect it back to our form. Notice the change to our route:
// test if input fails
if ($validator->fails()) {
return Redirect::route('todos.items.create', $list_id)->withErrors($validator)->withInput();
}
Next we will need to create a new Item with the input from the form.
$item = new TodoItem();
$item->content = Input::get('content');
A simple save will not work here. We will need to relate the new item to our todo list so instead of ‘$list->save()’ like before we will use the related items method, ‘listItems’, from our model to save. Then redirect to our item’s todo list.
$todo_list->listItems->save($item);
return Redirect::route('todos.show', $todo_list->id)->withMessage('Item Was Added!');
}
This should give you a good base to get started with nested routes in Laravel 4. Make sure to keep your eye on the Laravel homepage for the upcoming release of Laravel 5 which will bring some much anticipated updates to this already easy to use PHP framework.
Got excellent information about Laravel4. Waiting for more informative tutorials like this one.