2

Creating a Basic ToDo Application With Laravel 4 – Part 3

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’ve learned how to install and set up Laravel, set up some project and task resources and display them to the user. In this chapter we’ll learn how to use Laravel 4 Generator’s form helpers and set up create, edit and delete pages/actions for our resources.

 

Before you Begin

If you’re a Laracasts member, watch the videos RESTful Forms and Create/Edit Forms. These videos explain far better and in far more detail the concepts below. If you’re serious about learning Laravel I’d highly recommend you sign up.

 

Adding Navigation Links

Before we do anything it would make life a little easier to add create/edit/delete/back links to our projects and tasks pages.

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
<!-- /app/views/projects/index.blade.php -->
@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>
					(						{{ Form::open(array('class' => 'inline', 'method' => 'DELETE', 'route' => array('projects.destroy', $project->slug))) }}							{{ link_to_route('projects.edit', 'Edit', array($project->slug), array('class' => 'btn btn-info')) }}, 							{{ Form::submit('Delete', array('class' => 'btn btn-danger')) }}						{{ Form::close() }}					)				</li>
			@endforeach
		</ul>
	@endif
 
	<p>{{ link_to_route('projects.create', 'Create Project') }}</p>@stop
 
<!-- /app/views/projects/show.blade.php -->
@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>
					(						{{ Form::open(array('class' => 'inline', 'method' => 'DELETE', 'route' => array('projects.tasks.destroy', $project->slug, $task->slug))) }}							{{ link_to_route('projects.tasks.edit', 'Edit', array($project->slug, $task->slug), array('class' => 'btn btn-info')) }}, 							{{ Form::submit('Delete', array('class' => 'btn btn-danger')) }}						{{ Form::close() }}					)				</li>
			@endforeach
		</ul>
	@endif
 
	<p>		{{ link_to_route('projects.index', 'Back to Projects') }} |		{{ link_to_route('projects.tasks.create', 'Create Task', $project->slug) }}	</p>@stop

For the most part this should be pretty self explanatory. The only tricky concept is the delete link. Resource controllers require a HTTP DELETE method to be sent. This can’t be done with a standard link so a form submit to the given route is required. See Actions Handled by Resource Controller in the documentation for more information.

 

Creating the Add and Edit pages

With the listing pages all set up, we need to be able to add and edit projects and tasks. The create and edit forms will be pretty much identical so instead of duplicating them, we will inherit from a single form partial for each model.

I’ll begin with the project create/edit views:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- /app/views/projects/create.blade.php -->
@section('main')
	<h2>Create Project</h2>
 
	{{ Form::model(new Project, ['route' => ['projects.store']]) }}
		@include('projects/partials/_form', ['submit_text' => 'Create Project'])
	{{ Form::close() }}
@stop
 
<!-- /app/views/projects/edit.blade.php -->
@section('main')
	<h2>Edit Project</h2>
 
	{{ Form::model($project, ['method' => 'PATCH', 'route' => ['projects.update', $project->slug]]) }}
		@include('projects/partials/_form', ['submit_text' => 'Edit Project'])
	{{ Form::close() }}
@stop

Do the same for tasks but with updated routes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- /app/views/tasks/create.blade.php -->
@section('main')
	<h2>Create Task for Project "{{ $project->name }}"</h2>
 
	{{ Form::model(new Task, ['route' => ['projects.tasks.store', $project->slug]]) }}
		@include('tasks/partials/_form', ['submit_text' => 'Create Task'])
	{{ Form::close() }}
@stop
 
<!-- /app/views/tasks/edit.blade.php -->
@section('main')
	<h2>Edit Task "{{ $task->name }}"</h2>
 
	{{ Form::model($task, ['method' => 'PATCH', 'route' => ['projects.tasks.update', $project->slug, $task->slug]]) }}
		@include('tasks/partials/_form', ['submit_text' => 'Edit Task'])
	{{ Form::close() }}
@stop

Now the are a few new concepts here:

Including a Partial

Firstly you’ll notice my use of blades @include method. This includes the form view that we will define later (for now, just create the files /app/views/projects/partials/_form.blade.php and /app/views/tasks/partials/_form.blade.php). Because the same form will be used on both create and edit pages we need its submit button to have a ‘Create Form’ and ‘Edit Form’ message appropriately so a submit_text variable is passed to the view.

Form Model Binding

The forms require different HTML <form> tags so rather those were split out from the _form partial and placed directly in the views. The Add form is a simple POST request to the projects.store named route and the Edit form is a PATCH to projects.update. This may seem confusing but it’s just the way RESTful controllers work.

Also notice the use of Form::model(). This is called called form model binding and though this doesn’t do much now, it will be used to automatically populate the edit form later when we add the fields with using Form::input().

CSRF Protection

Form helpers provide alot of functionality for free. If you go to /projects/create and view page source you’ll see something like the following:

1
2
3
<form method="POST" action="http://l4todo.localhost.com/projects" accept-charset="UTF-8">
	<input name="_token" type="hidden" value="Y8uOo7SeD5tQZExezDf5a7UwiYR4P6qIHEUKJNxI">
</form>

See the _token field? This is a CSRF token automatically generated by the {{ Form::model() }} call which prevents cross-site request forgery. See Laravel’s documentation on CSRF for more information. Suffice to say it’s a good thing and we didn’t even have to do anything special to have it!

 

Create the Edit Forms

We need form markup for our projects and tasks. Thanks to form model binding, we can just use Laravel’s Form helpers to output all the fields we need.

<!-- /app/views/projects/partials/_form.blade.php -->
<ul>
	<li>
		{{ Form::label('name', 'Name:') }}
		{{ Form::text('name') }}
	</li>
 
	<li>
		{{ Form::label('slug', 'Slug:') }}
		{{ Form::text('slug') }}
	</li>
 
	<li>
		{{ Form::submit($submit_text) }}
	</li>
</ul>
 
<!-- /app/views/tasks/partials/_form.blade.php -->
<ul>
	<li>
		{{ Form::label('name', 'Name:') }}
		{{ Form::text('name') }}
	</li>
 
	<li>
		{{ Form::label('slug', 'Slug:') }}
		{{ Form::text('slug') }}
	</li>
 
	<li>
		{{ Form::label('completed', 'Completed:') }}
		{{ Form::checkbox('completed') }}
	</li>
 
	<li>
		{{ Form::label('description', 'Description:') }}
		{{ Form::textarea('description') }}
	</li>
 
	<li>
		{{ Form::submit($submit_text) }}
	</li>
</ul>

That’s about it. In your browser you should now be able to browse to your add and edit pages. How easy was that!

 

Making the Forms Work

We have project and task add and edit forms displaying and a pseudo-form for deleting them. Now to make everything work as advertised.

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
// ProjectsController
public function store()
{
	$input = Input::all();
	Project::create( $input );
 
	return Redirect::route('projects.index')->with('message', 'Project created');
}
 
public function update(Project $project)
{
	$input = array_except(Input::all(), '_method');
	$project->update($input);
 
	return Redirect::route('projects.show', $project->slug)->with('message', 'Project updated.');
}
 
public function destroy(Project $project)
{
	$project->delete();
 
	return Redirect::route('projects.index')->with('message', 'Project deleted.');
}
 
// TasksController
public function store(Project $project)
{
	$input = Input::all();
	$input['project_id'] = $project->id;
	Task::create( $input );
 
	return Redirect::route('projects.show', $project->slug)->with('Task created.');
}
 
public function update(Project $project, Task $task)
{
	$input = array_except(Input::all(), '_method');
	$task->update($input);
 
	return Redirect::route('projects.tasks.show', [$project->slug, $task->slug])->with('message', 'Task updated.');
}
 
public function destroy(Project $project, Task $task)
{
	$task->delete();
 
	return Redirect::route('projects.show', $project->slug)->with('message', 'Task deleted.');
}

Again the above is pretty much self explanatory and boilerplate-free thanks to route model binding and Laravel’s beautiful expressive syntax.

If you submitted one of the forms now, you’d likely see an error MassAssignmentException: _token. A mass assignment is where you pass an array of data to Model::create() or Model::update() the way we’re doing in our controllers, instead of setting one field at a time. The problem is occurring because Way Generators 2 adds a protected $fillable = [] line to generated models. Simply change this to $guarded = []; to fix the error. Alternatively you could specify the fillable fields for each model like so:

1
2
3
4
5
6
7
// /app/models/Project.php
class Project extends Eloquent {
	protected $fillable = ['name', 'slug']; 
// /app/models/Task.php
class Task extends Eloquent {
	protected $fillable = ['project_id', 'name', 'slug', 'completed', 'description'];

 

Flash Messages

One thing to note from the above is my use of the with() function. with() passes a flash (one time use) variable to the session which can then be read on the next page load. If you’ll remember back in part 1 of this tutorial our layout (/app/views/layouts/main.blade.php) looks for a message session variable on each page load and prints it out if one exists.

Try creating a project. The message will display as it should however refresh the page and it will be gone.

 

Conclusion

Today things got a little more interesting. We added create and edit forms and delete functionality, we learned about CSRF and Form Model Binding and even a few more blade and Eloquent functions.

With everything now working, have a play around. Create, edit and delete your resources. Remember you can always php artisan migrate:refresh –seed when you’re done and your database will be instantly reset.

In the next and final lesson we’ll put on the finishing touches including form validation and slug management. Stay tuned!

  • http://madnetmedia.com Mike Campbell

    When I run artisan generate:form Task I get an empty form as output… I turned on query log and it is trying to get the columns for the ‘Tasks’ table which doesn’t exist.

    IE:
    —-
    mike@laravel:~/lara$ php artisan generate:form Task
    {{ Form::open(array(‘route’ => ‘Tasks.store’)) }}

    {{ Form::submit() }}

    —-
    Is all that is outputted… But if I rename the model file from Task.php to task.php and run artisan generate:form task I get the proper output.

    The models were named uppercase via generate:resource so I’m not sure where this conflict is coming from. Any idea?

    • flynsarmy

      I tried it out with a fresh install of Laravel:


      $ laravel new l4formtest
      $ composer require "way/generators":"dev-master"
      $ nano app/config/app.php # set generators service provider
      $ nano app/config/database.php # set db credentials
      $ php artisan generate:resource task --fields="project_id:integer, name:string, slug:string, completed:boolean, description:text"
      $ php artisan migrate
      $ php artisan generate:form task

      At this point I was getting an error

      Class ‘Doctrine\DBAL\Driver\PDOMySql\Driver’ not found

      Googled the issue and it looks like generate:form requires doctrine/dbal installed – so I added it.

      $ composer require "doctrine/dbal":"*"

      After that I could generate:form fine. I also notice you’re trying to generate with ‘Task’ instead of ‘task’. It needs to be lowercase.

      Hope this helps.