Creating a small Todos application
The objective of this exercise is to use learning material as read on the French OpenClassrooms tutorial and, too, on Premier projet Laravel 5.4.
Use and practice Laravel’s framework...
Table des matières
1. Put things in place
Create the model, router, controller, views and request. Simple way.
1.1. Create a TODOs app
1.1.1. Create the migration class
php artisan make:migration create_todos_table
1.1.2. Update the migration class
Edit /database/migrations/2018_08_01_000000_create_todos_table.php
and add our fields.
The list of column’s type can be retrieved here
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateTodosTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('todos', function (Blueprint $table) {
$table->increments('id');
$table->timestamps();
$table->string('title', 100);
$table->boolean('completed')->default(0);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('todos');
}
}
1.1.3. Execute and create the table
php artisan migrate
1.1.4. Create a model
php artisan make:model Todo
1.1.5. Add code to the model
Edit /app/Todo.php
.
We don’t need to specify our table’s fields
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Todo extends Model
{
protected $table = 'todos';
// Only if $table->timestamps() was mentioned in the
// CreateTodosTable class; set to False if not mentioned
public $timestamps = true;
}
1.1.6. Create validation rules
php artisan make:request TodoRequest
1.1.7. Add rules
Edit /app/Http/Requests/TodoRequest.php
and add somes rules:
-
the
title
field is mandatory and can’t be longer than 100 characters -
the
completed
field is a boolean.
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class TodoRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true; // allow anyone to submit Todos
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'title' => 'required|string|max:100',
'completed' => 'boolean'
];
}
}
1.1.8. Add routes
Edit /routes/web.php
, add a GET and a POST route and, for the exercise, give a name to the POST one.
Add also a route for showing all todos.
Route::get('form', 'TodoController@getForm');
Route::post('todo', [
'uses' => 'TodoController@postForm',
'as' => 'storeTodo'
]);
Route::get('todos', [
'uses' => 'TodoController@index',
'as' => 'showTodos'
]);
1.1.9. Create a controller
php artisan make:controller TodoController
1.1.10. Add code to the controller
Edit /app/Http/Controllers/TodoController.php
and add this code:
<?php
namespace App\Http\Controllers;
use App\Todo;
use App\Http\Requests\TodoRequest;
class TodoController extends Controller
{
public function index()
{
return Todo::all(); // Return JSON output
}
public function getForm()
{
return view('todo'); // Show the form
}
public function postForm(TodoRequest $request)
{
$todo = new Todo();
$todo->title = $request->input('title');
$todo->completed = $request->input('completed');
$todo->save(); // Save the submitted data
return view('todo_ok'); // And show a "successful" page
}
}
1.1.11. Create the view
Manually create the file /resources/views/form.blade.php
(there is no artisan
command for this)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Some stupid Todos application</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>Some stupid Todos application</h1>
<div class="col-sm-offset-4 col-sm-4">
<div class="panel panel-info">
<div class="panel-heading">Todos</div>
<div class="panel-body">
{!! Form::open(['route' => 'storeTodo']) !!}
<div class="form-group {!! $errors->has('todo') ? 'has-error' : '' !!}">
{!! Form::text('title', null, array('size' => '100', 'class' => 'form-control', 'placeholder' => 'Enter Todo\'s title')) !!}
{!! $errors->first('title', '<div class="alert alert-danger">:message</div>') !!}
</div>
<div class="form-group ">
{!! Form::checkbox('completed', 1, 0) !!}
{!! Form::label('completed', 'Completed'); !!}
</div>
{!! Form::submit('Submit !') !!}
{!! Form::close() !!}
</div>
</div>
</div>
</body>
</html>
1.1.12. Test the view
The route, the controller and the view are now in place, we can therefore access to http://127.0.0.1:8000/todo
and check if our form is well displayed.
1.1.13. Create a successful view
Create /resources/views/todo_ok.blade.php
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Some stupid Todos application</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>Some stupid Todos application</h1>
<div class="panel-body">
Successfully stored in the database
</div>
</body>
</html>
1.1.14. Use the form
Go to http://127.0.0.1:8000/todo
and add a few items. If everything goes fine, each of them will be now immediately stored in your database.
You can open it with, f.i. phpMyAdmin to check how they’re saved.
But remember: we’ve created a route called /todos
so open http://127.0.0.1:8000/todos
and you should see this:
That output is made by the controller, function index():
public function index()
{
return Todo::all(); // Return JSON output
}
(The JSON formatting as displayed on the screen capture has been automatically made thanks the JSON Formatter Chrome extension)
2. Improve the output
Once everything is in place, improve the interface with CSS
2.1. Improving outputs
Our Todos’s application is now up and running (we can add new items and display all of them). We can now improve the look&feel of the interface.
2.1.1. Update the controller
Edit /app/Http/Controllers/TodoController.php
and replace the index function with the one below.
Every record of the Todo
table will be retrieved and pass to the todos
view (will be created below) a variable called $data
.
public function index()
{
$data = Todo::all();
return view('todos', compact('data'));
}
2.1.2. Create a todos view
Create /resources/views/todos.blade.php
for displaying all todos.
Use the Blade language for checking if there are todos to show and if it’s the case, loop and display every title.
Add a link below the list to add new items.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Some stupid Todos application</title>
<link media="screen" rel="stylesheet" type="text/css" href="/css/app.css" />
</head>
<body>
<main role="main">
<div class="jumbotron">
<div class="container">
<h1 class="display-3">Some stupid Todos application</h1>
<p>A simple Laravel application</p>
</div>
</div>
</main>
<div class="container">
@isset($data)
<ol>
@foreach($data as $todo)
<li>{{ $todo->title }}</li>
@endforeach
</ol>
@endisset
<hr/>
<a href="/todo" >Add new item</a>
</div>
</body>
</html>
2.1.3. Edit the todo_ok view
Improve the look and feel of /resources/views/todo_ok.blade.php
, replace the view’s content by:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Some stupid Todos application</title>
<link media="screen" rel="stylesheet" type="text/css" href="/css/app.css" />
</head>
<body>
<main role="main">
<div class="jumbotron">
<div class="container">
<h1 class="display-3">Some stupid Todos application</h1>
<p>A simple Laravel application</p>
</div>
</div>
</main>
<div class="container">
<div class="panel-body">
Successfully stored in the database
</div>
<hr/>
<a href="/todo" >Add new item</a> - <a href="/todos">Show all</a>
</div>
</body>
</html>
2.1.4. Improve the fom
Improve the look and feel of the input form by setting a new content in /resources/views/form.blade.php
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Some stupid Todos application</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link media="screen" rel="stylesheet" type="text/css" href="/css/app.css" />
</head>
<body>
<main role="main">
<div class="jumbotron">
<div class="container">
<h1 class="display-3">Some stupid Todos application</h1>
<p>A simple Laravel application</p>
</div>
</div>
</main>
<div class="container">
<div class="panel-heading">Add a new Todo</div>
<div class="panel-body">
{!! Form::open(['route' => 'storeTodo']) !!}
<div class="form-group {!! $errors->has('todo') ? 'has-error' : '' !!}">
{!! $errors->first('title', '<small class="help-block">:message</small>') !!}
{!! Form::text('title', null, array('size' => '100', 'class' => 'form-control', 'placeholder' => 'Enter Todo\'s title')) !!}
</div>
<div class="form-group ">
{!! Form::checkbox('completed', 1, 0) !!}
{!! Form::label('completed', 'Completed'); !!}
</div>
{!! Form::submit('Submit !') !!}
{!! Form::close() !!}
</div>
<hr/>
<a href="/todos">Show all</a>
</div>
</body>
</html>
2.1.5. Test the application
Go to http://127.0.0.1:8000/todo
and
- You’ll see the input form: you can type a new item and submit or
-
You can click on the
Show all
hyperlink for getting the list of todos.
The list of todos is accessible at http://127.0.0.1:8000/todos
, click on the Add new item
link for getting the input form.
By submitting a new todo, the feedback screen will display the two hyperlinks: Add new item
and Show all
The application is working with a basic Bootstrap theme.
3. Update the structure
Having only a title and a completed flag for a todo is working but too limited.
By adding a comment field and, also, the ID of the author will be useful and will allow to make the tool more useful.
3.1. Follow evolution
Idea: add extra fields to our Todos application, add dummy records by the way of a migration plan and update views to reflect these changes.
3.1.1. Add new fields
Edit the migration /database/migrations/2018_08_01_000000_create_todos_table.php
and add extra columns in the up()
function.
We’ll add a description field (that can stay empty) and the ID of the user who has filled in the Todo. We’ll also add a foreign key to the users
table and, we can too, add a cascade delete trigger.
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateTodosTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('todos', function (Blueprint $table) {
$table->increments('id');
$table->timestamps();
$table->string('title', 100);
$table->boolean('completed');
$table->text('description', 1000)->nullable();
$table->integer('user_id')->unsigned();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('todos');
}
}
Perhaps is it a bad idea to remove all todo’s when the user removes his account. If so, just use $table->foreign('user_id')->references('id')->on('users');
i.e. without the trigger.
3.1.2. Refresh the table’s structure
Run the following instruction for refreshing the structure of all tables. Be careful: tables will be cleared first.
php artisan migrate:refresh
By looking at the table’s structure we’ll see the changes:
3.1.3. Populate the table
To add a few items in our table, we can use a script:
php artisan make:migration PopulateTestingDatas
Just like a table, Laravel will create a php file in the folder /database/migrations/
; here, for this test, the file will be called 2018_08_01_000000_populate_testing_datas.php
.
<?php
use Illuminate\Database\Migrations\Migration;
class PopulateTestingDatas extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// Create user Christophe
DB::table('users')->insert([[
'name' => 'Christophe',
'email' => 'christophe@todos.com',
'password' => bcrypt('admin')
]
]);
// Getting the ID of the user Christophe
$user_id = DB::table('users')->where('name', 'Christophe')->take(1)->value('id');
// Insert a few items for him
for ($i = 0; $i < 20; $i++) {
DB::table('todos')->insert([
[
'title' => 'Todo #' . ($i + 1),
'description' => 'Some important content for the Todo #' . ($i + 1),
'user_id' => $user_id,
'completed' => rand(0, 1),
'created_at' => now(),
'updated_at' => now()
]
]);
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// Getting the ID of the user Christophe
$user_id = DB::table('users')->where('name', 'Christophe')->take(1)->value('id');
// Delete todos added by him
DB::table('todos')->where('user_id', $user_id)->delete();
// And remove the user
DB::table('users')->where('name', '=', 'Christophe')->delete();
}
}
3.1.3.1. Using Faker
Just for the fun, use Faker
(see https://github.com/fzaninotto/Faker). So, we’ll have random dummy text.
First install Faker
composer require fzaninotto/faker
Then edit the up()
function:
public function up()
{
// Create user Christophe
DB::table('users')->insert([[
'name' => 'Christophe',
'email' => 'christophe@todos.com',
'password' => bcrypt('admin')
]]);
// Getting the ID of the user Christophe
$user_id = DB::table('users')->where('name', 'Christophe')->take(1)->value('id');
// Use faker to get french dummy text
// If needed, just run "composer require fzaninotto/faker" in
// a DOS prompt
$faker = Faker\Factory::create('fr_FR');
// Insert a few items for him
for ($i = 0; $i < 20; $i++) {
DB::table('todos')->insert([
[
'title' => $faker->sentence($nbWords = 6, $variableNbWords = true) .
' (todo #' . ($i + 1) . ')',
'description' => $faker->realText($maxNbChars = 1000),
'user_id' => $user_id,
'completed' => $faker->boolean(),
'created_at' => now(),
'updated_at' => now()
]
]);
}
}
3.1.4. Edit the todos view
Now that we’ve a description field and we know the author, we can use these infos in our todos view.
Edit /resources/views/todos.blade.php
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Some stupid Todos application</title>
<link media="screen" rel="stylesheet" type="text/css" href="/css/app.css" />
</head>
<body>
<main role="main">
<div class="jumbotron">
<div class="container">
<h1 class="display-3">Some stupid Todos application</h1>
<p>A simple Laravel application</p>
</div>
</div>
</main>
<div class="container">
@isset($data)
@foreach($data as $post)
<h3><a href="todo/{{ $post->id }}">{{ $post->title }}</a></h3>
<p>{{ $post->description }}</p>
<small>Author: {{ $post->user_id }}</small>
<hr/>
@endforeach
@endisset
<hr/>
<a href="/todo" >Add new item</a>
</div>
</body>
</html>
This will result into this blog view. The author is here displayed with only his ID; we’ll change this.
As you can see, we’ve added an hyperlink for the todo’s title:
<h3><a href="todo/{{ $post->id }}">{{ $post->title }}</a></h3>
So, we need to create a route that will show the selected Todo.
3.1.5. Retrieve user’s information
Edit /app\Todo.php
and since we’ve a foreign key between the user_id
of the todos
table and the field id
of the table users
; it’s really use to retrieve informations.
Just add the user()
function like below:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Todo extends Model
{
protected $table = 'todos';
// Only if $table->timestamps() was mentioned in the
// CreateTodosTable class; set to False if not mentioned
public $timestamps = true;
public function user()
{
return $this->belongsTo('App\User', 'user_id', 'id');
}
}
This works since, in our up()
function for the CreateTodosTable
we’ve:
$table->foreign('user_id')->references('id')->on('users');
So, now, we can use every columns of the users
table by just referencing the ´user()` function.
3.1.6. Edit the todos view once more
Edit /resources/views/todos.blade.php
and replace
<small>Author: {{ $post->user_id }}</small>
by
<small>Author: {{ $post->user->name }}</small>
And now, we’ve the name...
3.1.7. Add a detail todo route
Edit /app/Http/Controllers/TodoController.php
and add this function:
public function show($id)
{
$data = Todo::where('id', $id)->firstOrFail();
return view('show', compact('data'));
}
We’ll use the Todo model (as defined in /app/Todo.php
) and the where method will retrieved the specified item.
3.1.8. Add a detail todo view
Create this file /resources/views/show.blade.php
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Some stupid Todos application</title>
<link media="screen" rel="stylesheet" type="text/css" href="/css/app.css" />
</head>
<body>
<main role="main">
<div class="jumbotron">
<div class="container">
<h1 class="display-3">Some stupid Todos application</h1>
<p>A simple Laravel application</p>
</div>
</div>
</main>
<div class="container">
@isset($data)
<h3>{{ $data->title }}</h3>
<p>{{ $data->description }}</p>
<small>
Created at: {{ $data->created_at }}
<br/>
Last updated: {{ $data->updated_at }}
<br/>
Author: {{ $data->user->name }}
</small>
<hr/>
@endisset
<a href="/todo" >Add new item</a> - <a href="/todos">Show all</a>
</div>
</body>
</html>
3.1.9. Edit the form
We need to add a description field.
Edit /resources/views/form.blade.php
and add the two lines below:
{!! Form::label('description', 'Description (optional)'); !!}
{!! Form::textarea('description', null, array('class' => 'form-control')) !!}
Then form will thus become:
{!! Form::open(['route' => 'storeTodo']) !!}
<div class="form-group {!! $errors->has('todo') ? 'has-error' : '' !!}">
{!! Form::text('title', null, array('size' => '100', 'class' => 'form-control', 'placeholder' => 'Enter Todo\'s title')) !!}
{!! $errors->first('title', '<div class="alert alert-danger">:message</div>') !!}
</div>
<div class="form-group ">
{!! Form::checkbox('completed', 1, 0) !!}
{!! Form::label('completed', 'Completed'); !!}
</div>
<div class="form-group ">
{!! Form::label('description', 'Description (optional)'); !!}
{!! Form::textarea('description', null, array('class' => 'form-control')) !!}
</div>
{!! Form::submit('Submit !') !!}
{!! Form::close() !!}
3.1.10. Store the description
Edit /app/Http/Controllers/TodoController.php
and add in the postForm()
function:
$todo->description = $request->input('description');
The function will become:
public function postForm(TodoRequest $request)
{
$todo = new Todo();
$todo->title = $request->input('title');
$todo->description = $request->input('description');
$todo->completed = $request->input('completed');
$todo->save(); // Save the submitted data
return view('todo_ok'); // And show a "successful" page
}
4. Use Blade and make life easier
Using Blade, we can use views and inject variable contents in them. With a master view f.i., we’ll draw the look & feel of the page only once. We don’t need anymore to repeat again and again the structure of the page but just concentrate on the content.
4.1. DRY thanks to Blade framework
Don’t repeat yourself thanks to Blade framework
So far, we’ve already create a few views and each time we’re coding again and again the same content: the header block f.i. is always the same.
4.1.1. The master template
The look & feel of our pages will be:
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Some stupid Todos application</title>
<link media="screen" rel="stylesheet" type="text/css" href="/css/app.css" />
</head>
<body>
<main role="main">
<div class="jumbotron">
<div class="container">
<h1 class="display-3">Some stupid Todos application</h1>
<p>A simple Laravel application</p>
</div>
</div>
</main>
<div class="container">
@yield('content')
<hr/>
@yield('navigation')
</div>
</body>
</html>
Use app()->getLocale()
to retrieve the langage for the .env
.
We’ll have two variables part: the content of the page and the navigation.
Already add the Laravel token in the template to protect form’s submissions.
4.1.2. The todos view
List all todos (http://127.0.0.1:8000/todos)
Edit /resources/views/todos.blade.php
:
@extends('master')
@section('content')
@isset($data)
@foreach($data as $post)
<h3><a href="todo/{{ $post->id }}">{{ $post->title }}</a></h3>
<p>{{ $post->description }}</p>
<small>Author: {{ $post->user->name }}</small>
<hr/>
@endforeach
@endisset
@endsection
@section('navigation')
<a href="/todo" >Add new item</a>
@endsection
We’ll use the master template then inject our two contents.
The content will be the list of todos, one h3 by Todo followed by his description and the name of the todo’s author.
4.1.3. The detail view
Show the detail of a given todo (http://127.0.0.1:8000/todo/1)
Edit /resources/views/show.blade.php
:
@extends('master')
@section('content')
@isset($data)
<h3>{{ $data->title }}</h3>
<p>{{ $data->description }}</p>
<small>
Created at: {{ $data->created_at }}
<br/>
Last updated: {{ $data->updated_at }}
<br/>
Author: {{ $data->user->name }}
</small>
<hr/>
@endisset
@endsection
@section('navigation')
<a href="/todo" >Add new item</a> - <a href="/todos">Show all</a>
@endsection
4.1.4. The todo_ok view
Displayed after the submission of a new Todo
Edit /resources/views/todo_ok.blade.php
:
@extends('master')
@section('content')
<div class="alert alert-success" role="alert">Successfully stored in the database</div>
@endsection
@section('navigation')
<a href="/todo" >Add new item</a> - <a href="/todos">Show all</a>
@endsection
4.1.5. The form view
Edit /resources/views/form.blade.php
:
@extends('master')
@section('content')
<div class="panel-heading">Hi {{ Auth::user()->name }}, please add your new Todo below</div>
<div class="panel-body">
{!! Form::open(['route' => 'storeTodo']) !!}
<div class="form-group {!! $errors->has('todo') ? 'has-error' : '' !!}">
{!! Form::text('title', null, array('size' => '100', 'class' => 'form-control', 'placeholder' => 'Enter Todo\'s title')) !!}
{!! $errors->first('title', '<div class="alert alert-danger">:message</div>') !!}
</div>
<div class="form-group ">
{!! Form::checkbox('completed', 1, 0) !!}
{!! Form::label('completed', 'Completed'); !!}
</div>
<div class="form-group ">
{!! Form::label('description', 'Description (optional)'); !!}
{!! Form::textarea('description', null, array('class' => 'form-control')) !!}
</div>
{!! Form::submit('Submit !') !!}
{!! Form::close() !!}
</div>
@endsection
@section('navigation')
<a href="/todos">Show all</a>
@endsection
4.1.5.1. Tips:
https://laravel-news.com/five-useful-laravel-blade-directives
We can replace
@if(auth()->user())
// The user is authenticated.
@endif
by the @auth
directive; more powerful.
@auth
// The user is authenticated.
@endauth
Same for @guest
.
4.1.6. Test
Now, you can test each view, no change will be visible by visiting the site.
But views are now easier to manage; we can update the master view and the change will be visible everywhere.
5. Protect our form
Use the auth
middleware of Laravel, display a login form and retrieve the connected user.
5.1. Enable auth middleware
5.1.1. Enable the authentification layer
This is easy, just run artisan:
php artisan make:auth
A new controller will be automatically created: /app/Http/Controllers/HomeController.php
, routes will be also added into /routes/web.php
.
Auth::routes();
Route::get('home', 'HomeController@index')->name('home');
Route::get('logout', ['as' => 'logout', 'uses' => 'Auth\LoginController@logout']);
The logout
route isn’t mandatory but make easy to disconnect by visiting http://127.0.0.1:8000/logout
.
5.1.2. Update routes
We’ll protect our application so, we don’t want to protect the URL /home
but the root /
.
Edit /routes/web.php
and update like this:
<?php
Auth::routes();
Route::get('/', 'HomeController@index')->name('home');
Route::get('form', 'TodoController@getForm');
Route::post('todo', ['uses' => 'TodoController@postForm', 'as' => 'storeTodo']);
Route::get('todos', ['uses' => 'TodoController@index','as' => 'showTodos']);
The last three routes remain, for this moment, unchanged.
If everything is going fine, now, we’ll have an authentication screen when visiting http://127.0.0.1:8000/
.
We’ve already created a dummy user previously:
-
E-Mail Address:
christophe@todos.com
-
Password:
admin
Let’s try
The look&feel of the views and the management of the authentication’s process is entirely managed by Laravel.
Note: by registering a new user, Laravel will redirect to the /home
URL. We can change this easily by updating the file /app/Http/Controllers/Auth/RegisterController.php
and change the $redirectTo
variable to /
:
protected $redirectTo = '/';
5.1.3. Update the HomeController
Once connected, it would be great that http://127.0.0.1:8000/
display our views and not the You are logged in
message so, when connected, the /
route shouldn’t more redirect to the login feature but to one or our page.
Edit the /app/Http/Controllers/HomeController.php
controller and add the following Facades
t the top of the file (below the namespace
).
use Illuminate\Support\Facades\Auth;
Then update the index()
function like this:
/**
* Show the application dashboard.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
if (Auth::check()) {
return view('todos');
} else {
return view('home');
}
}
So, when check()
is true (meaning that the user is connected), the controller will show the todos
view otherwhise the home
view (login form).
5.1.4. Display the logged-in username
Edit file /resources/views/form.blade.php
and replace the line
<div class="panel-heading">Add a new Todo</div>
With this one:
<div class="panel-heading">Hi {{ Auth::user()->name }}, please add your new Todo below</div>
We’ll just show the name of the connected user.
5.1.5. Store the user_id
Edit /app/Http/Controllers/TodoController.php
and add in the postForm()
function:
$todo->user_id = Auth::user()->id;
The function will become:
public function postForm(TodoRequest $request)
{
if (Auth::check()) {
$todo = new Todo();
$todo->title = $request->input('title');
$todo->description = $request->input('description');
$todo->completed = $request->input('completed');
$todo->user_id = Auth::user()->id;
$todo->save(); // Save the submitted data
return view('todo_ok'); // And show a "successful" page
}
return redirect()->back();
}
5.1.6. Add a new view for displaying message
This simple view will be used for, only, displaying a message like an error.
Create file /resources/views/message.blade.php
@extends('master')
@section('content')
@if(!empty($message))
<div class="alert alert-{{ $status }}">{!! $message !!}</div>
@endif
@endsection
Note: use and not
so, we can use HTML output in the message.
This view is using two variables: status
and message
. In the controller, we’ll use:
return view('message')
->with('message', 'The message that needs to be displayed')
->with('status', 'status'); // can be warning, error, success, info, ...
5.1.7. Don’t show the form unless logged-in
Since the /resources/views/form.blade.php
view now show the connected username, we need to block the access to http://127.0.0.1:8000/todo
(the submission form) and show it only if logged-in.
One way is to edit the controller, add a check in the getForm()
function.
public function getForm()
{
if (Auth::check()) {
return view('todo'); // Show the form
} else {
return view('message')
->with('message', 'Session expired, please <a href="/login">reconnect</a>')
->with('status', 'warning');
}
}
But there is a much better way to do this
Because we need to check if the user is authenticated for methods like DELETE
, PUT
or POST
, it’ll be more efficient to use the middleware()
notion:
// Protect these actions, a valid user should be connected
Route::get('form', 'TodoController@getForm')->middleware('auth');
Route::post('todo', ['uses' => 'TodoController@postForm', 'as' => 'storeTodo']);
Laravel will make sure that a user is connected before running the controller. So, prevent to display the form and to post data for a guest.
6. Make the app working
Add actions to our application so, we can edit and delete items.
6.1. Add update and delete verbs
6.1.1. Add a route for edit and delete
Edit `/routes/web.php’ and add the new routes:
Route::get('todo/{id}/edit', 'TodoController@edit')->where('id', '[0-9]+')->middleware('auth');
Route::put('todo', 'TodoController@put')->middleware('auth');
Route::delete('todo/{id}', 'TodoController@delete')->where('id', '[0-9]+')->middleware('auth');
These routes are using HTTP methods and thus use Route::delete()
and Route::put()
to be ready for REST approach.
Route::get('todo/{id}/edit', 'TodoController@edit')->where('id', '[0-9]+')->middleware('auth');
is for displaying the form with, already filled in, the data coming from the todo that should be edited.
So, two actions but three new routes.
6.1.2. Update master view and add script
Edit `/resources/views/master.blade.php’ and add jquery and a yield variable to make possible to inject js statements:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Some stupid Todos application</title>
<link media="screen" rel="stylesheet" type="text/css" href="/css/app.css" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
</head>
<body>
<main role="main">
<div class="jumbotron">
<div class="container">
<h1 class="display-3">Some stupid Todos application</h1>
<p>A simple Laravel application</p>
</div>
</div>
</main>
<div class="container">
@yield('content')
<hr/>
@yield('navigation')
</div>
@yield('script')
</body>
</html>
6.1.3. Add buttons to the detail view
Edit `/resources/views/show.blade.php’ and add jquery and a yield variable.
The HTTP method (PUT
or DELETE
) will be derived from the text of the button.
The Ajax part will send the request to the server then remove the two buttons for displaying a text.
@extends('master')
@section('content')
@isset($data)
<h3>{{ $data->title }}</h3>
<p>{{ $data->description }}</p>
<small>
Created at: {{ $data->created_at }}
<br/>
Last updated: {{ $data->updated_at }}
<br/>
Author: {{ $data->user->name }}
</small>
<hr/>
<span class="buttons">
<input type="button" value="Update" class="edit"/> - <input type="button" value="Delete" class="delete"/>
</span>
@endisset
@endsection
@section('navigation')
<a href="/todo" >Add new item</a> - <a href="/todos">Show all</a>
@endsection
@section('script')
<script defer="defer">
$('.delete, .edit').click(function(){
if (this.value.toUpperCase() === 'DELETE') {
// Add the csrf-token protection but only when the request is
// made on the same site (no cross-domain).
// Don't share the token outside
$.ajaxSetup({
beforeSend: function(xhr, type) {
if (!type.crossDomain) {
xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'));
}
}
});
$.ajax({
url: '/todo/{{ $data->id }}',
type: 'DELETE',
contentType: 'application/json',
success: function (data) {
if (data.hasOwnProperty("message")) {
// Replace buttons and display the feedback message
$('.buttons').html(data.message);
}
},
error: function (data, textStatus, errorThrown) {
console.log(data);
}
});
} else {
window.location.replace('/todo/{{ $data->id }}/edit');
}
});
</script>
@endsection
6.1.4. Add the delete action
Edit /app/Http/Controllers/TodoController.php
and add a delete()
function and add the Response
facade:
use Response;
public function delete($id)
{
Todo::destroy($id);
return Response::json([
'status' => true,
'message' => '<div class="alert alert-success" role="alert">Successfully deleted</div>'
]);
}
So, the delete()
function will return a JSON output and the Ajax success()
anonymous function will use the message
and output it:
6.1.5. Add the put action
Edit /app/Http/Controllers/TodoController.php
and add a put()
function:
public function put(TodoRequest $request)
{
// Retrieve the record
$todo = Todo::where('id', $request->input('id'))->firstOrFail();
// List of fields that we'll update
$todo->update($request->only(['title', 'completed', 'description']));
// Redirect to the edit form
return redirect()->back()
->with('message', '<div class="alert alert-success" role="alert">Successfully updated</div>');
}
We’ll reuse our todo
view i.e. the one that is displaying the form.
6.1.6. Update the Todo model
Since the update()
method requires the use of the $fillable
property, we need to update our model. $fillable
lists the fields that can be updated so not mentioning user_id
f.i. will make the update()
method fails when trying to modifying that specific field.
Edit /App/Todo.php
and add the property:
public $fillable = ['title', 'completed', 'description'];
6.1.7. Edit the form and show the record
Edit /resources/views/form.blade.php
and add $data
variable like below. Add also extra informations like the completed flag and the ID
in a hidden field.
@extends('master')
@section('content')
<div class="panel-heading">Hi {{ Auth::user()->name }}, please add your new Todo below</div>
<div class="panel-body">
@if(Session::has('message'))
{!! Session::get('message') !!}
@endif
{!! Form::open(['route' => 'storeTodo', 'method' => (isset($data) ? 'PUT' : 'POST')]) !!}
{!! Form::hidden('id', $data->id ?? 0) !!}
<div class="form-group {!! $errors->has('todo') ? 'has-error' : '' !!}">
{!! Form::text('title', $data->title ?? '', array('size' => '100', 'class' => 'form-control', 'placeholder' => 'Enter Todo\'s title')) !!}
{!! $errors->first('title', '<div class="alert alert-danger">:message</div>') !!}
</div>
<div class="form-group ">
{!! Form::checkbox('completed', 1, (isset($data) ? ($data->completed==1) : 0)) !!}
{!! Form::label('completed', 'Completed'); !!}
</div>
<div class="form-group">
{!! Form::label('description', 'Description (optional)'); !!}
{!! Form::textarea('description', $data->description ?? '', array('class' => 'form-control')) !!}
</div>
{!! Form::submit('Submit !') !!}
{!! Form::close() !!}
</div>
@endsection
@section('navigation')
<a href="/todos">Show all</a>
@endsection
6.1.7.1. Tips:
We can use the @auth
directive; more powerful.
Note: HTML::Forms doesn’t support the PUT method so the trick is to add a _method
field. This can be done with the line below.
If we’ve a record (so, we’ve an ID), we’ll use the PUT
method, if not, this is a new record so use POST
.
Form::open(['route' => 'storeTodo', 'method' => (isset($data) ? 'PUT' : 'POST')])
Since Laravel 5.6, we can also just use @method('put')
.
@section('content')
<form>
@method('put')
...
The three lines below will allow to output message sent by the controller.
@if(Session::has('message'))
{!! Session::get('message') !!}
@endif
The put()
in the controller indeed redirect with a message. Laravel use the Session object for storing the message.
return redirect()->back()
->with('message', '<div class="alert alert-success" role="alert">Successfully updated</div>');
6.1.8. Test
Everything is now in place.
Go to http://127.0.0.1:8000/todos
, display a Todo by clicking on his title, the detail of the todo will be displayed with two buttons : Update
and Delete
.
By clicking on Update
, the form will be displayed with the todo’s info loaded.
Clicking on the Submit !
button will send a PUT
request to the server and the put()
function of the controller will be called; make the changes and show the form back.
By clicking on Delete
, an Ajax call will be made with a DELETE
request, the delete()
function of the controller will be called and a alert message will then be displayed.
Pay attention, in the database, to the updated_at
column: Laravel will automatically update that column. Just because our model (/app/Todo.php
) has mentionned $timestamp = true;
.
7. Review code organization
Make code quality better.
7.1. Better coding
7.1.1. Context
In our /app/Http/Controllers/TodoController.php
controller, we’ve this:
use App\Todo;
public function postForm(TodoRequest $request)
{
$todo = new Todo();
$todo->title = $request->input('title');
$todo->description = $request->input('description');
$todo->completed = $request->input('completed');
$todo->user_id = Auth::user()->id;
$todo->save();
return view('todo_ok');
}
The problem: what will happen if, f.i., the description
field is removed / renamed in the model /app/Todo.php
? The controller will fail.
An updated version of the code can be the one below where the instantiation of the Todo() class has been removed in the function since the object is now part of the parameters.
use App\Todo;
public function postForm(TodoRequest $request, Todo $todo)
{
$todo->title = $request->input('title');
$todo->description = $request->input('description');
$todo->completed = $request->input('completed');
$todo->user_id = Auth::user()->id;
$todo->save();
return view('todo_ok');
}
But we still have our fields in our controller... The idea is to just call a save()
method and without having a use App\Todo
. Just save()
without knowing what and where.
public function postForm(...)
{
$todo->save();
return view('todo_ok');
}
7.1.2. Using a repository
Put the data logic into the repository and remove it from the controller.
7.1.2.1. Edit the controller
The controller should only know that the method is called save()
and nothing else. The controller will become agnostic: no need to known which fields are in the model, which ones needs to be saved, ... just call save()
and that’s it.
Edit /app/Http/Controllers/TodoController.php
, add a use
statement for the repository and change the postForm()
function:
use App\Repositories\TodoRepository;
public function postForm(
TodoRequest $request,
TodoRepository $todoRepository
) {
$todoRepository->save($request);
return view('todo_ok');
}
7.1.2.2. Create a repository
Create the /App/Repositories/TodoRepository.php
file.
<?php
namespace App\Repositories;
use App\Todo;
use App\Http\Requests\TodoRequest;
use Auth;
class TodoRepository
{
protected $todo;
public function __construct(Todo $todo)
{
$this->todo = $todo;
}
public function save(TodoRequest $todo)
{
$this->todo->title = $todo->input('title');
$this->todo->description = $todo->input('description');
$this->todo->completed = $todo->input('completed');
$this->todo->user_id = Auth::user()->id;
$this->todo->save(); // Save the submitted data
}
}
7.1.2.3. Test the repository
Once the controller has been updated and is referencing the repository, the save()
method will be the one of the repository.
By going to http://127.0.0.1:8000/todo
, if correctly implemented, the form will still work and the record stored in our database;
We can go one step further: by using an interface.
7.1.3. Using an interface
The interface will only inform other classes about the existing methods in the repository.
7.1.3.1. Create the interface
Create the /App/Repositories
folder if needed and create the TodoRepositoryInterface.php
file.
<?php
namespace App\Repositories;
use App\Http\Requests\TodoRequest;
interface TodoRepositoryInterface
{
public function save(TodoRequest $todo);
}
7.1.3.2. Edit the repository
Edit /App/Repositories/TodoRepository.php
and change
class TodoRepository
by
class TodoRepository implements TodoRepositoryInterface
The class extends the interface so, we need to implement the save()
function and, too, we need to strictly respect the function declaration (so if the function in the interface call his parameter $todo
, the repository should call it $todo
too).
7.1.3.3. Edit the controller and use the interface
Edit /app/Http/Controllers/TodoController.php
and change the postForm()
function:
public function postForm(
TodoRequest $request,
TodoRepository $todoRepository
)
to
public function postForm(
TodoRequest $request,
TodoRepositoryInterface $todoRepository
)
So ask to use the interface and not the repository directly.
And ... this won’t work.
If you test to submit a new record, you’ll get this error:
Illuminate\Contracts\Container\BindingResolutionException
Target [App\Repositories\TodoRepositoryInterface] is not instantiable.
This because and interface can’t be instantiating.
Laravel comes with a little trick: bind the interface and the class using it.
Edit /app/Http/Providers/AppServiceProvider.php
and add these lines to the register()
function:
public function register()
{
...
$this->app->bind(
'App\Repositories\TodoRepositoryInterface',
'App\Repositories\TodoRepository'
);
}
Now, when Laravel will see TodoRepositoryInterface
he’ll knows that he should use TodoRepository
.
7.1.3.4. Test the interface
Once the controller has been updated and is referencing the interface, the save()
method will be the one of the repository (which is an extension of the interface).
By going to http://127.0.0.1:8000/todo
, if correctly implemented, the form will still work and the record stored in our database;
8. Final
Final code
8.1. Final code
Here is the final code of this lab: todos.zip
A few changes have been made like showing the list of todos as the homepage. So the code proposed here is a little different (also documented).
8.1.1. Install a fresh copy
To use this code, please follow these steps:
- Start a DOS prompt
- Make sure Laravel is correctly install: type
Laravel -V
, if you see the Laravel Installer’s version number (f.i. 2.0.1), it’s fine. - Install a fresh Laravel website: go to your public_html folder (i.e. where you wish create your website, can be
c:\development\my_sites
too if configured like this on your machine). - At the prompt level, run
laravel new a_folder_name
(f.i.laravel new app_todos
) - Once the installation is done, go in that folder: type
cd app_todos
(i.e. your folder) - Grab a copy of todos.zip and unzip the file (under DOS
"C:\Program Files\7-Zip\7z.exe" x todos.zip -aoa
i.e. extract with full path and overwrite existing files) - Retrieve the name of the database to create:
- Open with a text editor the file called
.env
present in the root ofapp_todos
- Look to the
DB_DATABASE
variable, you’ll find the name of the database for the application (by default, it’ll betodos
)
- Open with a text editor the file called
- Create a database with that name (
todos
) in your mySQL- At the prompt level, type
mysql -u root -p
(where root is the username) - When asking the password, just press enter (there is no password)
- In the MySQL prompt, type
CREATE DATABASE todos;
and press Enter - Type
quit
for leaving MySQL
- At the prompt level, type
- Start the migration:
php artisan migrate:install
- Create tables:
php artisan migrate:fresh
- Add the authentication layer:
php artisan make:auth
- Run
composer require "laravelcollective/html"
- This done, start artisan by starting
php artisan serve
on the DOS prompt. - With your browser go to
http://127.0.0.1:8000/login
and make a login withchristophe@todos.com
for the email whileadmin
is the password. - Then we can use the application:
http://127.0.0.1:8000
8.1.2. Code source
8.1.2.1. routes/web.php
Definition of our routes and how to manage them.
<?php
// Enable authentication routes
Auth::routes();
// Register http://127.0.0.1:8000/login and display the login screen
Route::get('home', 'HomeController@index')->name('home');
// Register the http://127.0.0.1:8000/logout and logout
Route::get('logout', ['as' => 'logout', 'uses' => 'Auth\LoginController@logout']);
// -------------------
// GET {URL} - Default URL : Show the list of todos, as a blog
Route::get('/', [
'uses' => 'TodoController@index',
'as' => 'showTodos'
]);
// GET {URL}/todo/{id} -> show the todo #{id}
Route::get('todo/{id}', 'TodoController@show')->where('id', '[0-9]+');
// GET {URL}/todo -> show a form -> only for logged in users
Route::get('todo', 'TodoController@getForm')->middleware('auth');
// POST {URL}/todo -> the form is being submitted -> only for logged in users
Route::post('todo', [
'uses' => 'TodoController@postForm',
'as' => 'storeTodo'
])->middleware('auth');
// GET {URL}/todo/{id}/edit -> edit an existing item in a form -> only for logged in users
Route::get('todo/{id}/edit', 'TodoController@edit')->where('id', '[0-9]+')->middleware('auth');
// DELETE {URL}/todo/{id} -> delete an existing item -> only for logged in users
Route::delete('todo/{id}', 'TodoController@delete')->where('id', '[0-9]+')->middleware('auth');
// PUT {URL}/todo -> update an existing item -> only for logged in users
Route::put('todo', 'TodoController@put')->middleware('auth');
8.1.2.2. app/Todo.php
Our model, layer for our todos
table.
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Todo extends Model
{
// Name of our table in the database
protected $table = 'todos';
// Only if $table->timestamps() was mentioned in the
// CreateTodosTable class; set to False if not mentioned
public $timestamps = true;
// List of columns that we can update
public $fillable = ['title', 'completed', 'description'];
/**
* Extend the model and offer a convenient way to retrieve the
* user linked to our todo (thanks the user_id foreign key and
* his link with the users table)
*
* @return void
*/
public function user()
{
return $this->belongsTo('App\User', 'user_id', 'id');
}
}
8.1.2.3. database/migrations/create_todos_table.php
Define the structure of the todos
table.
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateTodosTable extends Migration
{
/**
* The migration is running
* Define a foreign key between user_id and the users table
*
* @return void
*/
public function up()
{
Schema::create('todos', function (Blueprint $table) {
// Our primary key
$table->increments('id');
// Allow Eloquent to add two fields and managed them:
// created_at and updated_at
$table->timestamps();
$table->string('title', 100);
$table->boolean('completed')->default(0);
$table->text('description', 1000)->nullable();
// The author of the record
$table->integer('user_id')->unsigned();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('todos');
}
}
8.1.2.4. database/migrations/populate_testing_datas.php
Populate the table with fake records.
<?php
use Illuminate\Database\Migrations\Migration;
class PopulateTestingDatas extends Migration
{
/**
* The migration is running
*
* @return void
*/
public function up()
{
// Create user Christophe
DB::table('users')->insert([[
'name' => 'Christophe',
'email' => 'christophe@todos.com',
'password' => bcrypt('admin')
]]);
// Getting the ID of the user Christophe
$user_id = DB::table('users')->where('name', 'Christophe')->take(1)->value('id');
// Use faker to get french dummy text
// If needed, just run "composer require fzaninotto/faker" in
// a DOS prompt
$faker = Faker\Factory::create('fr_FR');
// Insert a few items for him
for ($i = 0; $i < 20; $i++) {
DB::table('todos')->insert([
[
'title' => $faker->sentence($nbWords = 6, $variableNbWords = true) .
' (todo #' . ($i + 1) . ')',
'description' => $faker->realText($maxNbChars = 1000),
'user_id' => $user_id,
'completed' => $faker->boolean(),
'created_at' => now(),
'updated_at' => now()
]
]);
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// Getting the ID of the user Christophe
$user_id = DB::table('users')->where('name', 'Christophe')->take(1)->value('id');
// Delete todos added by him
DB::table('todos')->where('user_id', $user_id)->delete();
// And remove the user
DB::table('users')->where('name', '=', 'Christophe')->delete();
}
}
8.1.2.5. app/Repositories/TodoRepositoryInterface.php
Defines the functions that are supported by the TodoRepository i.e. the data logic.
<?php
namespace App\Repositories;
use App\Todo;
use Illuminate\Database\Eloquent\Collection;
use App\Http\Requests\TodoRequest;
/**
* Define the function that should be available through the
* TodoRepository
*/
interface TodoRepositoryInterface
{
/**
* Get the list of records of the table
*
* @return Collection
*/
public function index() : Collection;
/**
* Get a specific todo; identified by his ID
*
* @param integer $id
* @return Todo
*/
public function get(int $id) : Todo;
/**
* Save the todo
*
* @param TodoRequest $todo Submitted data
* @return void
*/
public function save(TodoRequest $todo);
/**
* Remove the specified todo
*
* @param integer $id
* @return void
*/
public function delete(int $id);
/**
* Update the specified todo, update columns
*
* @param TodoRequest $request
* @return void
*/
public function put(TodoRequest $request);
}
8.1.2.6. app/Repositories/TodoRepository.php
Implements functions for our data logic.
<?php
namespace App\Repositories;
use App\Http\Requests\TodoRequest;
use Illuminate\Database\Eloquent\Collection;
use App\Todo;
use Auth;
/**
* Implements functions for working with the todos table
*/
class TodoRepository implements TodoRepositoryInterface
{
protected $todo;
public function __construct(Todo $todo)
{
$this->todo = $todo;
}
/**
* Get the list of records of the table
*
* @return Collection
*/
public function index() : Collection
{
return $this->todo->all();
}
/**
* Get a specific todo; identified by his ID
*
* @param integer $id
* @return Todo
*/
public function get(int $id) : Todo
{
return $this->todo->where('id', $id)->firstOrFail();
}
/**
* Save the todo
*
* @param TodoRequest $todo Submitted data
* @return void
*/
public function save(TodoRequest $todo)
{
$this->todo->title = $todo->input('title');
$this->todo->description = $todo->input('description');
$this->todo->completed = false;
$this->todo->user_id = Auth::user()->id;
$this->todo->save(); // Save the submitted data
}
/**
* Remove the specified todo
*
* @param integer $id
* @return void
*/
public function delete(int $id)
{
$this->todo->destroy($id);
}
/**
* Update the specified todo, update columns
*
* @param TodoRequest $request
* @return void
*/
public function put(TodoRequest $request)
{
// Retrieve the record
$data = self::get($request->input('id'));
// List of fields that we'll update
$data->update($request->only(['title', 'completed', 'description']));
}
}
8.1.2.7. app/Http/Controllers/TodoController.php
Implements the code to run for each route.
<?php
namespace App\Http\Controllers;
use App\Http\Requests\TodoRequest;
use App\Repositories\TodoRepositoryInterface;
use Response;
class TodoController extends Controller
{
/**
* Show the list of todos
*
* @param TodoRepositoryInterface $todoRepository
* @return void
*/
public function index(
TodoRepositoryInterface $todoRepository
) {
// Retrieve all todos
$datas = $todoRepository->index();
// call the list.blade.php view and pass the data
return view('list', compact('datas'));
}
/**
* Show the detail of a todo
*
* @param integer $id
* @param TodoRepositoryInterface $todoRepository
* @return void
*/
public function show(int $id, TodoRepositoryInterface $todoRepository)
{
$data = $todoRepository->get($id);
return view('show', compact('data'));
}
/**
* Show the submission form
*
* @return void
*/
public function getForm()
{
return view('form');
}
/**
* The form is being submitted, save the data, create a new todo
* This function answer to the POST method, not PUT
*
* @param TodoRequest $request
* @param TodoRepositoryInterface $todoRepository
* @return void
*/
public function postForm(
TodoRequest $request,
TodoRepositoryInterface $todoRepository
) {
$todoRepository->save($request);
// Show the form back and pass a "Success" alert
return redirect()->back()
->with('message', '<div class="alert alert-success" role="alert">' .
'Successfully created</div>');
}
/**
* Edit an existing record : retrieve the record and show the form
*
* @param integer $id
* @param TodoRepositoryInterface $todoRepository
* @return void
*/
public function edit(int $id, TodoRepositoryInterface $todoRepository)
{
$data = $todoRepository->get($id);
return view('form', compact('data'));
}
/**
* Delete a todo
*
* @param integer $id
* @param TodoRepositoryInterface $todoRepository
* @return void
*/
public function delete(int $id, TodoRepositoryInterface $todoRepository)
{
$todoRepository->delete($id);
// Return a JSON string that will be used in an Ajax request
return Response::json([
'status' => true,
'message' => '<div class="alert alert-success" role="alert">' .
'Successfully deleted</div>'
]);
}
/**
* Update a todo record
* This function answer to the PUT method (updating an existing record)
*
* @param TodoRequest $request
* @param TodoRepositoryInterface $todoRepository
* @return void
*/
public function put(TodoRequest $request, TodoRepositoryInterface $todoRepository)
{
$todoRepository->put($request);
// Redirect to the edit form
return redirect()->back()
->with('message', '<div class="alert alert-success" role="alert">' .
'Successfully updated</div>');
}
}
8.1.2.8. app/Http/Requests/TodoRequest.php
Define who can use our Todo records and specify which rules should be applied to records.
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class TodoRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'title' => 'required|string|max:100',
'completed' => 'boolean'
];
}
}
8.1.2.9. resources/views/form.blade.php
Show the submission form.
@extends('master')
@section('content')
{{-- Display the name of the connected user --}}
<div class="panel-heading">Hi {{ Auth::user()->name }}, please add your new Todo below</div>
<div class="panel-body">
{{--
Messages sent by the controller like in
return view('...')->with('message', '<div class="...">success</div>'))
will be displayed here
--}}
@if(Session::has('message'))
{!! Session::get('message') !!}
@endif
{{--
The method will be 'POST' unless when $data is set.
Not set : we're creating a new record so the HTTP method will be POST
Set : we're editing an existing record so the HTTP method will be PUT
--}}
{!! Form::open(['route' => 'storeTodo', 'method' => (isset($data) ? 'PUT' : 'POST')]) !!}
{{--
If we're editing an existing record, we'll need his ID in our controller.
If we're creating a new record, we don't have yet an ID => 0
--}}
{!! Form::hidden('id', $data->id ?? 0) !!}
<div class="form-group {!! $errors->has('todo') ? 'has-error' : '' !!}">
{{--
Output the "Title" entry.
--}}
{!! Form::text('title', $data->title ?? '', array('size' => '100', 'class' => 'form-control', 'placeholder' => 'Enter Todo\'s title')) !!}
{!! $errors->first('title', '<div class="alert alert-danger">:message</div>') !!}
</div>
<div class="form-group">
{{--
Output the "Completed" flag.
--}}
{!! Form::checkbox('completed', 1, (isset($data) ? ($data->completed==1) : 0)) !!}
{!! Form::label('completed', 'Completed'); !!}
</div>
<div class="form-group {!! $errors->has('todo') ? 'has-error' : '' !!}">
{{--
Output the "Description" textarea.
--}}
{!! Form::label('description', 'Description (optional)'); !!}
{!! Form::textarea('description', $data->description ?? '', array('class' => 'form-control')) !!}
</div>
{{--
Output the submit button
--}}
{!! Form::submit('Submit !') !!}
{!! Form::close() !!}
</div>
@endsection
@section('navigation')
<a href="/">Show all</a>
@endsection
8.1.2.10. resources/views/list.blade.php
Display the list of todos, like in a blog.
@extends('master')
@section('content')
{{--
$data is a collection of records
--}}
@isset($datas)
{{--
For each todo, we'll show his title in a H3
Clicking on the title will display the todo's details
We'll also show: todo's description and his author.
--}}
@foreach($datas as $data)
<h3><a href="todo/{{ $data->id }}">{{ $data->title }}</a></h3>
<p>{{ $data->description }}</p>
<small>Author: {{ $data->user->name }}</small>
<hr/>
@endforeach
@endisset
@endsection
@section('navigation')
<a href="/todo" >Add new item</a>
@endsection
8.1.2.11. resources/views/master.blade.php
Our page’s master. Defines how pages should looks like.
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{{--
Important for our Ajax requests: we need to protect our server's requests
with the generated session token
--}}
<meta name="csrf-token" content="{{ csrf_token() }}">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Some stupid Todos application</title>
<link media="screen" rel="stylesheet" type="text/css" href="/css/app.css" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
</head>
<body>
<main role="main">
<div class="jumbotron">
<div class="container">
<h1 class="display-3">Some stupid Todos application</h1>
<small>A simple Laravel application, learning purposes</small>
</div>
</div>
</main>
<div class="container">
@yield('content')
<hr/>
@yield('navigation')
</div>
@yield('script')
</body>
</html>
8.1.2.12. resources/views/show.blade.php
Show the detail of a todo. If a valid user is logged-in, display action’s buttons.
@extends('master')
@section('content')
{{--
Display the detail of a todo; make sure we've one
--}}
@isset($data)
{{--
Show informations like title, description and timestamps
--}}
<h3>{{ $data->title }}</h3>
<p>{{ $data->description }}</p>
<small>
Created at: {{ $data->created_at }}
<br/>
Last updated: {{ $data->updated_at }}
<br/>
{{--
$data->user isn't a column but, in our model, the user() function
returns an object which represent a record of the users table.
So, through $data->user we can access to the user's name, email, ...
--}}
Author: {{ $data->user->name }}
</small>
{{--
Only for logged-in users, show action's buttons
--}}
@if(Illuminate\Support\Facades\Auth::check())
<hr/>
<span class="buttons">
<input type="button" value="Update" class="edit"/> -
<input type="button" value="Delete" class="delete"/>
</span>
@endif
@endisset
@endsection
@section('navigation')
<a href="/todo" >Add new item</a> - <a href="/">Show all</a>
@endsection
@section('script')
{{--
Add our script for our buttons
--}}
<script defer="defer">
$('.delete, .edit').click(function(){
if (this.value.toUpperCase() === 'DELETE') {
// Add the csrf-token protection but only when the request is
// made on the same site (no cross-domain).
// Don't share the token outside
$.ajaxSetup({
beforeSend: function(xhr, type) {
if (!type.crossDomain) {
xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'));
}
}
});
// By clicking on the delete button, make an Ajax request
$.ajax({
url: '/todo/{{ $data->id }}',
type: 'DELETE',
contentType: 'application/json',
success: function (data) {
if (data.hasOwnProperty("message")) {
// Replace buttons and display the feedback message
// Indeed, when deleted, we can't anymore edit or delete
$('.buttons').html(data.message);
}
},
error: function (data, textStatus, errorThrown) {
console.log(data);
}
});
} else {
// The user has clicked on the edit button, redirect the browser
// to the edit page
window.location.replace('/todo/{{ $data->id }}/edit');
}
});
</script>
@endsection