4

Creating a Basic ToDo Application With Laravel 4 – Part 2

Posted December 2nd, 2013 (Updated 14 Mar 2014) in PHP

Welcome back to my simple to-do application tutorial for Laravel 4. This tutorial is relatively long so I’ve broken it up into multiple posts.

  1. Part 1 – Installation, Database and Routes
  2. Part 2 – Listing Projects and Tasks
  3. Part 3 – Create/Edit/Delete
  4. Part 4 – Validation and Slugs

So far we have a working database complete with test data and a bunch of routes for displaying, editing and deleting our projects and tasks. In this second chapter I’ll cover controllers, models (with relationships), views (including the blade templating language and layouts) and route model binding.

 

Controllers and Blade View Layouts

Further details: See Resourceful Controllers Part 1 and Part 2

If you browse to /projects you’ll get an empty page. Why is that? Well let’s find out. Run php artisan routes one more time and look at this line:

+--------+-----------------------------------+------------------+----------------------------+----------------+---------------+
| Domain | URI                               | Name             | Action                     | Before Filters | After Filters |
+--------+-----------------------------------+------------------+----------------------------+----------------+---------------+
|        | GET|HEAD projects                 | projects.index   | ProjectsController@index   |                |               |

Looks like the /projects URL is loading ProjectsController’s index method. So open up /app/controllers/ProjectController.php. It’s currently empty but we want to show the /app/views/projects/index.blade.php file created when we called php artisan generate:resource in Tutorial 1. Change it to the following:

1
2
3
4
public function index()
{	return View::make('projects.index');
}

Do the same to the show, create and edit methods of your ProjectsController and TasksController, replacing ‘index’ with the appropriate view file. Refresh the /projects URL in your browser. You should now see the contents of the file on your screen.

Showing the contents of a view is great, but if we have more than one page on our site we’ll want a consistent template across all pages. In other words, we need the view’s contents to sit inside a basic HTML template. This is done with controller layouts.

There are a few steps to implementing controller layouts:

  1. Create the your layout file – create the file /app/views/layouts/main.blade.php and drop the following in:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
    <!DOCTYPE html>
    <!--[if lt IE 7]>      <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
    <!--[if IE 7]>         <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
    <!--[if IE 8]>         <html class="no-js lt-ie9"> <![endif]-->
    <!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
    <head>
    	<meta charset="utf-8">
    	<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    	<title>My To-Do App</title>
    	<meta name="viewport" content="width=device-width">
    	<style>
    		#wrapper {width:960px;max-width:100%;margin:auto}
    		.inline {display:inline}
    		.error {color:red}
    	</style>
    </head>
    <body>
    	<div id='wrapper'>
    		<header>
    			<h1>My To-Do App</h1>
    		</header>
    		<div id="content">
    			@if (Session::has('message'))
    				<div class="flash alert">
    					<p>{{ Session::get('message') }}</p>
    				</div>
    			@endif
     
    			@yield('main')
    		</div>
    	</div>
    </body>
    </html>
  2. Add a layout property to your controllers pointing to the new layout file. In our case, we want every controller to use the same layout. So at the top of BaseController (/app/controllers/BaseController.php) add:
    1
    2
    
    class BaseController extends Controller {
    	protected $layout = 'layouts.main';
  3. Apply the following change to all return View::make() lines in each of your controllers:
    1
    2
    
    // return View::make('projects.index');
    $this->layout->content = View::make('projects.index');
  4. Wrap all of your blade templates’ contents like so:
    1
    2
    3
    
    @section('main')	/path/to/app/views/projects/index.blade.php
    @stop

With all of the above in place, refresh the page and view page source. You should now see the HTML skeleton around your view contents.

 

Route Model Binding

Further Details: If you’re a laracasts member see Route Model Binding

By default L4 will provide an ID value to various resourceful controller methods such as show(), edit(), update() and destroy(). This is fine but it adds alot of extra boilerplate we need to write – grabbing the model instance, checking if it exists etc. Thankfully Laravel provides something called route model binding that helps with this issue. Instead of providing an $id variable, the method will be given the $project or $task object instance instead.

Open up /app/routes.php and add the following two lines:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Provide controller methods with object instead of ID
Route::model('tasks', 'Task');Route::model('projects', 'Project'); 
// Use slugs rather than IDs in URLs
Route::bind('tasks', function($value, $route) {
	return Task::whereSlug($value)-&gt;first();
});
Route::bind('projects', function($value, $route) {
	return Project::whereSlug($value)-&gt;first();
});
 
Route::resource('projects', 'ProjectsController');
Route::resource('projects.tasks', 'TasksController');

and in your TasksController and ProjectsController replace every method definition’s $id reference with Task $task and Project $project like so:

// public function edit($id)
public function edit(Project $project)

At this point you can also pass the object to its respective view in each controllers show and edit methods like so as we’ll be using them later:

1
2
3
4
public function edit(Project $project)
{
    $this->layout->content = View::make('projects.show', compact('project'));}

The TasksController will also need some minor modifications. Because we’re using nested resources, php artisan routes will tell us that task routes all include a {projects} mask in addition to the {tasks} mask that some of them receive.. As a result the controller methods will be passed a Project instance as their first argument. So update them accordingly remembering to update method docs and pass the new $project variable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
class TasksController extends BaseController {
 
	/**
	 * Display a listing of the resource.
	 *
	 * @param  Project  $project	 * @return Response
	 */
	public function index(Project $project)	{
		$this->layout->content = View::make('tasks.index', compact('project'));	}
 
	/**
	 * Show the form for creating a new resource.
	 *
	 * @param  Project  $project	 * @return Response
	 */
	public function create(Project $project)	{
		$this->layout->content = View::make('tasks.create', compact('project'));	}
 
	/**
	 * Store a newly created resource in storage.
	 *
	 * @param  Project  $project	 * @return Response
	 */
	public function store(Project $project)	{
		//
	}
 
	/**
	 * Display the specified resource.
	 *
	 * @param  Project  $project	 * @param  Task     $task	 * @return Response
	 */
	public function show(Project $project, Task $task)	{
		$this->layout->content = View::make('tasks.show', compact('project', 'task'));	}
 
	/**
	 * Show the form for editing the specified resource.
	 *
	 * @param  Project  $project	 * @param  Task     $task	 * @return Response
	 */
	public function edit(Project $project, Task $task)	{
		$this->layout->content = View::make('tasks.edit', compact('project', 'task'));	}
 
	/**
	 * Update the specified resource in storage.
	 *
	 * @param  Project  $project	 * @param  Task     $task	 * @return Response
	 */
	public function update(Project $project, Task $task)	{
		//
	}
 
	/**
	 * Remove the specified resource from storage.
	 *
	 * @param  Project  $project	 * @param  Task     $task	 * @return Response
	 */
	public function destroy(Project $project, Task $task)	{
		//
	}
 
}

If you refresh the url /projects/project-1 everything should still be working.

 

Displaying Our Models

Project listing page

It’s time to start listing our projects and tasks. Open /projects in your browser. Based on php artisan routes this is our project listing page. Open /app/views/projects/index.blade.php and set it to the following:

1
2
3
4
5
6
7
8
9
10
11
12
@section('main')
	<h2>Projects</h2>	@if ( !$projects->count() )		You have no projects	@else		<ul>			@foreach( $projects as $project )				<li><a href="{{ route('projects.show', $project->slug) }}">{{ $project->name }}</a></li>			@endforeach		</ul>	@endif@stop

 

There are a few things going on above:

  1. I’m using the blade templating language’s if and foreach control-flow functions as well as its print function (the double curly braces).
  2. I’m checking if there are any projects to show. If not, display a message saying so. If there are, show them all
  3. I’m calling the route() helper with a named route (You can see a list of your named routes with php artisan routes) to link to each projects details page.

You’ll also need to pass the $projects variable to this view or you’ll get an undefined variable error. Open /app/controllers/ProjectsController.php and update the index() method to:

1
2
3
4
5
6
public function index()
{
	$projects = Project::all(); 	$this->layout->content = View::make('projects.index', compact('projects'));}

Refresh and you’ll now see a listing of your projects.

 

Model Relations – The Project Details page

On the project details page we need to display a list of the given projects tasks. To do that we need to define a one-to-many relationship in our Project model allowing it to grab its tasks.

Open /app/models/Project.php and add a tasks() method like so:

1
2
3
4
public function tasks()
{
	return $this->hasMany('Task');
}

Inversely we can also add a many-to-one relationship to our Task model:

1
2
3
4
public function project()
{
	return $this->belongsTo('Project');
}

To make sure it worked:

$ php artisan tinker
>echo Project::whereSlug('project-1')->first()->tasks->count();
3
>echo Project::whereSlug('project-2')->first()->tasks->count();
2
>echo Task::first()->project->name;
Project 1

Perfect! The view can now be updated (/app/views/projects/show.blade.php):

1
2
3
4
5
6
7
8
9
10
11
12
@section('main')
	<h2>{{ $project->name }}</h2>
	@if ( !$project->tasks->count() )
		Your project has no tasks.
	@else
		<ul>
			@foreach( $project->tasks as $task )
				<li><a href="{{ route('projects.tasks.show', [$project->slug, $task->slug]) }}">{{ $task->name }}</a></li>
			@endforeach
		</ul>
	@endif
@stop

Click a project on the project listing page in your browser and your project will now display complete with its task listing.

Finally we have the task show page (/app/views/tasks/show.blade.php). This one is very straightforward:

1
2
3
4
@section('main')
	<h2>{{ $project->name }} - {{ $task->name }}</h2>
	{{ $task->description }}
@stop

Note: Be very careful when using model relationships. It’s easy to generate huge numbers of SQL queries if you’re not careful. This is called the N+1 problem. See the video below for details on what exactly the N+1 problem is, and how to protect against it.

 

Conclusion

Today we covered:

  • Route Model Binding
  • Models (with one-to-many relationships)
  • Controllers (with route model binding)
  • Views (with blade templating language and layouts)

We now have a working listing of projects and tasks. In the next lesson we’ll concentrate on editing, creating and deleting projects and tasks. Stay tuned!

  • http://www.webradish.com Webradish

    Thanks for your last tip – got me through to Part 2: Another challenge for ya! Please drag me to the end of this tute! I’ve tried and failed them all – django, openerp, yii, sails.js, express.js. My stupidity beats all! If you don’t help me, I’ll become an author. Spare the world another writer like me and please assist.

    Part 2 – got up to Route Model Binding, then got a…

    syntax error, unexpected ‘&’

    /var/www/laravel/app/routes.php

    Route::get(‘/’, function()

    {

    //return View::make(‘hello’);

    // Provide controller methods with object instead of ID

    Route::model(‘tasks’, ‘Task’);

    Route::model(‘projects’, ‘Project’);

    // Use slugs rather than IDs in URLs

    Route::bind(‘tasks’, function($value, $route) {

    return Task::whereSlug($value)->first();

    });

    Route::bind(‘projects’, function($value, $route) {

    return Project::whereSlug($value)->first();

    });

    Route::resource(‘projects’, ‘ProjectsController’);

    Route::resource(‘projects.tasks’, ‘TasksController’);

    • flynsarmy

      You’ve opened


      Route::get('/', function()
      {

      but never closed it. Should be


      Route::get('/', function() {
      //return View::make('hello');
      });

      // Provide controller methods with object instead of ID
      Route::model('tasks', 'Task');
      Route::model('projects', 'Project');

      • http://www.webradish.com Webradish

        Thanks mate. Sorry but I failed again – I got stuck and just used your phase 4 github files. On the bright side, I got it to work, which is a first for me. Now I’ll do this tutorial again, properly and without cheating from the beginning. Cheers,

    • flynsarmy

      You can find a working versoin of each part of this tutorial on github. There is a corrosponding branch in the repo for each part of this tutorial.