Welcome back to my simple to-do application tutorial for Laravel 5. This tutorial is relatively long so I’ve broken it up into multiple posts.
- Part 1 – Installation, Database and Routes
- Part 2 – Listing Projects and Tasks
- Part 3 – Create/Edit/Delete
- Part 4 – Validation
The source for each part can also be found on GitHub.
So far we have a working database complete with seed 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.
Laravel Form Helpers
In Laravel 4 we had a HTML package but it was removed for 5 to cut down on cruft. Add it back in by following the instructions in my How to Install Illuminate/HTML In Laravel 5 tutorial. In addition to HTML and Form facades, this package provides some handy helper functions such as link_to_route() which we’ll be using later.
Controllers and Blade View Layouts
If you browse to /projects you’ll get an empty page. Why is that? Well let’s find out. Run php artisan route:list one more time and look at this line:
1 2 3 4 5 | +--------+----------+----------------------------------------+------------------------+-------------------------------------------------+------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+----------+----------------------------------------+------------------------+-------------------------------------------------+------------+
| | GET|HEAD | projects | projects.index | App\Http\Controllers\ProjectsController@index | |+--------+----------+----------------------------------------+------------------------+-------------------------------------------------+------------+ |
Looks like the /projects URL is loading ProjectsController’s index method. So open up /app/Http/controllers/ProjectsController.php and update the method to point to a view we’ll create:
1 2 3 4 | public function index() { return view('projects.index');} |
We’re using Blade Templates in this tutorial so create a /resources/views/projects/index.blade.php file and enter some text in there. Hit /projects in your browser again. If everything is working correctly you should see the text you entered above. Do the same for the create controller method.
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:
- Create a layout view. Laravel actually ships with a pretty decent one called app.blade.php so we’ll save some time and just use that. Notice near the bottom the layout contains a @yield(‘content’) line. That’s the function that will load our actual content.
- Reference your layout in your view using @extends(‘app’) and wrap in a @section(‘content’) block like so:
1 2 3 4 5
@extends('app') @section('content') This is my /resources/views/projects/index.blade.php file! @endsection
In /resources/views/projects folder create a show.blade.php, index.blade.php and create.blade.php view with the above markup replacing the filename as necessary. With these in place, refresh /projects in your browser. You should now see the app.blade.php skeleton around your view contents.
Route Model Binding
By default Laravel 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/Http/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 App\Task::whereSlug($value)->first(); }); Route::bind('projects', function($value, $route) { return App\Project::whereSlug($value)->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) |
Don’t forget to add use App\Task and App\Project at the top of your respective controllers now that we’re referencing those models!
At this point you can also pass the object to its respective view in each controllers show, edit and update methods like so as we’ll be using them later:
1 2 3 4 | public function edit(Project $project) { return view('projects.show', compact('project'));} |
The TasksController will also need some minor modifications. Because we’re using nested resources, php artisan route:list 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.
By this point your controllers should look like so:
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 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | // /app/Http/Controllers/ProjectsController.php <?php namespace App\Http\Controllers; use App\Project; use App\Http\Requests; use App\Http\Controllers\Controller; use Illuminate\Http\Request; class ProjectsController extends Controller { /** * Display a listing of the resource. * * @return Response */ public function index() { $projects = Project::all(); return view('projects.index', compact('projects')); } /** * Show the form for creating a new resource. * * @return Response */ public function create() { return view('projects.create'); } /** * Store a newly created resource in storage. * * @return Response */ public function store() { // } /** * Display the specified resource. * * @param \App\Project $project * @return Response */ public function show(Project $project) { return view('projects.show', compact('project')); } /** * Show the form for editing the specified resource. * * @param \App\Project $project * @return Response */ public function edit(Project $project) { return view('projects.edit', compact('project')); } /** * Update the specified resource in storage. * * @param \App\Project $project * @return Response */ public function update(Project $project) { // } /** * Remove the specified resource from storage. * * @param \App\Project $project * @return Response */ public function destroy(Project $project) { // } } // /app/Http/Controllers/TasksController.php <?php namespace App\Http\Controllers; use App\Project; use App\Task; use App\Http\Requests; use App\Http\Controllers\Controller; use Illuminate\Http\Request; class TasksController extends Controller { /** * Display a listing of the resource. * * @param \App\Project $project * @return Response */ public function index(Project $project) { return view('tasks.index', compact('project')); } /** * Show the form for creating a new resource. * * @param \App\Project $project * @return Response */ public function create(Project $project) { return view('tasks.create', compact('project')); } /** * Store a newly created resource in storage. * * @param \App\Project $project * @return Response */ public function store(Project $project) { // } /** * Display the specified resource. * * @param \App\Project $project * @param \App\Task $task * @return Response */ public function show(Project $project, Task $task) { return view('tasks.show', compact('project', 'task')); } /** * Show the form for editing the specified resource. * * @param \App\Project $project * @param \App\Task $task * @return Response */ public function edit(Project $project, Task $task) { return view('tasks.edit', compact('project', 'task')); } /** * Update the specified resource in storage. * * @param \App\Project $project * @param \App\Task $task * @return Response */ public function update(Project $project, Task $task) { // } /** * Remove the specified resource from storage. * * @param \App\Project $project * @param \App\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 route:list this is our project listing page. Open /resources/views/projects/index.blade.php and set it to the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @extends('app') @section('content') <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@endsection |
There are a few things going on above:
- I’m using the blade templating language’s if and foreach control-flow functions as well as its print function (the double curly braces).
- I’m checking if there are any projects to show. If not, display a message saying so. If there are, list them all
- I’m calling the route() helper with a named route (You can see a list of your named routes with php artisan route:list) 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/Http/controllers/ProjectsController.php and update the index() method to:
1 2 3 4 5 | public function index() { $projects = Project::all(); return view('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/Project.php and add a tasks() method like so:
1 2 3 4 | public function tasks() { return $this->hasMany('App\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('App\Project'); } |
To make sure it worked:
$ php artisan tinker >App\Project::whereSlug('project-1')->first()->tasks->count(); 5 >App\Project::whereSlug('project-2')->first()->tasks->count(); 2 >App\Task::first()->project->name; Project 1 |
Perfect! The view can now be updated (/resources/views/projects/show.blade.php):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @extends('app') @section('content') <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@endsection |
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 (/resources/views/tasks/show.blade.php). This one is very straightforward:
1 2 3 4 5 6 7 8 9 10 | @extends('app') @section('content') <h2> {!! link_to_route('projects.show', $project->name, [$project->slug]) !!} - {{ $task->name }} </h2> {{ $task->description }} @endsection |
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!