Commit 7b20d442 authored by Dillenn Terumalai's avatar Dillenn Terumalai
Browse files

Wrote the README and final status of the app

parent 6d7676a2
awesome-laravel
# How To Build a (Basic) TODO list with Laravel
A tutorial by Dillenn TERUMALAI
---
## Server Requirements
- Composer - Depedency manager for PHP
- PHP >= 7.2.0
- BCMath PHP Extension
- Ctype PHP Extension
- JSON PHP Extension
- Mbstring PHP Extension
- OpenSSL PHP Extension
- PDO PHP Extension
- Tokenizer PHP Extension
- XML PHP Extension
## Installing Laravel
First, we need to install Laravel by using Composer:
```php
composer create-project --prefer-dist laravel/laravel awesome-laravel
```
Once the installation is done, you can directly test your application if PHP is installed locally by typing:
```php
php artisan serve
```
You can access the server at `http://localhost:8000`
## Laravel's Directory Structure
Your directory should contain multiple folders and files. Here is a short description for each.
- **app** contains your models, controllers and most of your future code
- **bootstrap** contains the files that the Laravel framework uses to boot every time it runs
- **config** contains all the configuration files
- **database** contains your migrations and seeders
- **public** is the directory the server points to when it's serving the website
- **resources** contains non-PHP files that are needed, views, language files, sass or JS files
- **routes** contains all the route definitions
- **storage** contains the caches, logs, and compiled system files
- **test** contains unit and integration tests
- **vendor** contains all the composer dependencies
- *env* and *env.example* are files that dictate the environment variables
- *artisan* is the file that allows your tu run Artisan commands from the command line
- *.gitignore* and *.gitattributes* are Git config files
- *composer.json* and *composer.lock* are the config files for Composer
- *phpunit.xml* is a configuration file for PHPUnit
- *server.php* is a backup server that tries to allow less-capable servers to still preview the Laravel application
## Working with Docker - Vessel
Vessel is just a small set of files that sets up a local Docker-based dev environment per project. There is nothing to install globally, except Docker itself.
To install Vessel, simply type this command inside your Laravel project:
```php
composer require shipping-docker/vessel
```
Finally, publish the `command` and Docker files:
```bash
php artisan vendor:publish --provider="Vessel\VesselServiceProvider"
```
### Getting started
```bash
# Run this once to initialize project
# Must run with "bash" until initialized
bash vessel init
# Start vessel
./vessel start
# Stop vessel
./vessel stop
```
Once initialized, four containers will be running:
- PHP 7.3
- MySQL 5.7
- Redis
- NodeJS
## Routing and Controllers
### Router
In Laravel, you define your "web" routes in `routes/web.php` and your "API" routes in `routes/api.php`. In this tutorial, we will focus only on the routes in `routes/web.php`.
Let's create our first route. Modify `routes/web.php` closure to this:
```php
Route::get('/', function () {
return 'Awesome Laravel!';
});
```
Then hit `http://localhost`!
You should see the message "Awesome Laravel!".
If we look at the structure of the route:
1) The "verb" is **GET**
2) The path defined is root **"/"**
3) The closure return the string **Awesome Laravel!**
We can use any kind of "verb", for example:
```php
Route::post('/', function () {
return 'This is a post request';
});
```
We can also match any pattern, for example:
```php
Route::get('home', function () {
return 'This is my Home';
});
```
### Controller
The common option when defining a route is to pass a controller name and method as a string in place of the closure.
```php
Route::get('/', 'HomeController@index');
```
Of course, this controller doesn't exist yet, so type the following to create it:
```bash
php artisan make:controller HomeController
```
This will create `App/Http/HomeController.php`. Open the file and add the `index` function:
```php
public function index()
{
return 'Hello World!';
}
```
You should see **"Hello World!"** @`http://localhost`.
Nice right? And this is only the beginning!
## Blade Templating
Laravel offers a custom templating engine called Blade. Let's have a look!
Open `resources/views/welcome.blade.php`. First, notice the extension **blade.php**, it is important to always use this extension for any template we will create.
Inside `welcome.blade.php`, you can already see some **"directives"** prefixed with an `@`.
```html
@if (Route::has('login'))
<div class="top-right links">
@auth
<a href="{{ url('/home') }}">Home</a>
@else
<a href="{{ route('login') }}">Login</a>
@if (Route::has('register'))
<a href="{{ route('register') }}">Register</a>
@endif
@endauth
</div>
@endif
```
Blade's `@if ($condition)` compiles to <?php if ($condition)> (don't forget to always end your `@if` with an `@endif`). There are a lot of directives such as `@auth`, `@for`, `@foreach` ...
To use <?php echo $string>, simply use double brackets:
```php
{{ 'This is a string' }}
```
## Database
### Migrations
Modern frameworks like Laravel make it easy to define your database structure with code-driven migrations. Every new table, column, index, and key can be defined in code.
To build our **TODO** application, let's start by creating two migration files:
```bash
php artisan make:migration create_categories_table
php artisan make:migration create_todos_table
php artisan make:migration create_tags_table
php artisan make:migration create_tag_todo_table
```
We now have three migration files in `database/migrations`:
- ####-##-##-####_create_categories_table.php
- ####-##-##-####_create_todos_table.php
- ####-##-##-####_create_tags_table.php
- ####-##-##-####_create_tag_todo_table.php
Let's modify the content of these files:
```php
# create_categories_table.php
Schema::create('categories', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('title');
$table->timestamps();
});
```
```php
# create_todos_table.php
Schema::create('todos', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('task');
$table->boolean('done')->default(false);
$table->unsignedBigInteger('category_id')->nullable();
$table->foreign('category_id')
->references('id')->on('categories')
->onUpdate('cascade')
->onDelete('cascade');
$table->timestamps();
});
```
```php
# create_tags_table.php
Schema::create('tags', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('title');
$table->timestamps();
});
```
```php
# create_tag_todo_table.php
Schema::create('tag_todo', function (Blueprint $table) {
$table->unsignedBigInteger('tag_id');
$table->foreign('tag_id')
->references('id')->on('tags')
->onUpdate('cascade')
->onDelete('cascade');
$table->unsignedBigInteger('todo_id');
$table->foreign('todo_id')
->references('id')->on('todos')
->onUpdate('cascade')
->onDelete('cascade');
});
```
The table `categories` contains:
- **id**
- **title**
- **updated_at**
- **created_at**
The table `todos` contains:
- **id**
- **task**
- **done**
- **category_id** which is foreign key pointing to the table *"categories"* (1:1)
- **updated_at**
- **created_at**
The table `tags` contains:
- **id**
- **title**
- **updated_at**
- **created_at**
The table `tag_todo` (N:N) contains:
- **tag_id** -> *"tags"* -> id
- **todo_id** -> *"todos"* -> id
#### Running the migrations
To run your migration, you need to type:
```bash
./vessel artisan migrate
```
Because we are using docker and each part of our application is in a container, any artisan command that will interact with the cache or the database needs to be called using `./vessel artisan` instead of `php artisan`.
### Models
For this part, we will use Eloquent ORM (Object-relational mapping) which is a database abstraction layer that provides a single interface to interact with mutliple database types.
Let's create our models. As you may expect, we need three models:
- Category
- Todo
- Tag
Let's create them by typing:
```bash
php artisan make:model Category
php artisan make:model Todo
php artisan make:model Tag
```
We now have our three models in `app`.
Let's start with `Category.php`. By default, Model should be named after the singular of the table name and using SnakeCase. It will then guess the name of the table by adding an **'s'** but for **category** it is a bit more complicated. That is why we can manually define the name of the table.
```php
# Category.php
class Category extends Model
{
protected $table = 'categories';
public function todos()
{
return $this->hasMany('App\Todo');
}
}
```
Also by default, Eloquent will assume that each table has a primary key columns named `id` and expects `created_at` and `updated_at` columns to exist on your table. Of course, this can be manually defined.
Then let's modify `Todo.php`. Here, we need define relationships with both tables **categories**(1:1) and **tags**(1:N).
```php
# Todo.php
class Todo extends Model
{
public function category()
{
return $this->belongsTo('App\Category');
}
public function tags()
{
return $this->belongsToMany('App\Tag');
}
}
```
```php
# Tag.php
class Tag extends Model
{
public function todos()
{
return $this->belongsToMany('App\Todo');
}
}
```
## Controllers
Now we need to define a controller for our **todos** to handle any kind of request (GET,PATCH,DELETE...)
First let's create the controller using the `--reource` option that will automatically create a method for each of the available resource operations (CRUD).
```bash
php artisan make:controller TodoController --resource
```
The structure of the file is the following:
```php
class TodoController extends Controller
{
public function index()
{
//
}
public function create()
{
//
}
public function store(Request $request)
{
//
}
public function show($id)
{
//
}
public function edit($id)
{
//
}
public function update(Request $request, $id)
{
//
}
public function destroy($id)
{
//
}
}
```
Now, we need to fill these functions. To do so, we will need to use Eloquent ORM to work with our `Todo.php` model.
Let's start with `index()`:
Here, the function is supposed to return all the rows of the table **todos**. We can write this by simply typing:
```php
public function index()
{
return Todo::all();
}
```
Don't forget to add `use App\Todo;` at the beginning of the file to be able to directly call `Todo` model.
Let's check the result, but before we need to setup the routes and by using a resourceful route to the controller, it's really easy. In your `routes/web.php` file, just add:
```php
Route::resource('todos', 'TodoController');
```
We can check that our routes have been properly set up by typing:
```bash
php artisan route:list
```
And we can see that we have all the needed routes. Now, let's check in a browser what happens if we go to `http://localhost/todos`.
We see nothing, but this is completely normal as our table doesn't contain any data. So let's add manually some data by using any MySQL application (I use Sequel Pro on MacOS).
If we hit `http://localhost/todos`, we can see the added row(s). Nice!
What if we go to `http://localhost/todos/1`, let's configure the function `show($id)` before:
```php
public function show($id)
{
return Todo::find($id);
}
```
We can see the specific **todo** with the corresponding **id**.
As you can see, Eloquent is really easy to use. But what about relationships? Let's add two categories (*Home*, *Work*) and five tags (*Groceries*, *Admin*, *Family*, *Development*, *Holidays*) with Sequel Pro. Let's modify the **category_id** to match `1` or `2`. And finally, in the table **tag_todo**, let's create 4 rows (`(2,1)`, `(4,1)`, `(3,2)`, `(5,2)`).
But if we hit `http://localhost/todos`, we see the category_id but that is not convenient at all. So, let's modify our `index()` function:
```php
public function index()
{
return Todo::with(['category', 'tags'])->get();
}
public function show($id)
{
return Todo::with(['category', 'tags'])->find($id);
}
```
This returns the category and all the tags linked to each todo, awesome right?
Let's define our functions to:
- create a **todo** (we need the *task*, the *category_id* and the *tag_id*'s) -> `store(Request $request)`
- update a **todo** (we need the *id*, from *undone* to *done* or the opposite) -> `update(Request $request, $id)`
- delete a **todo** (we need the *id*) -> `destroy($id)``
```php
public function store(Request $request)
{
$category = \App\Category::find($request->category); //We retrieve the row of the category
$todo = new Todo; //We create a new model of Todo
$todo->task = $request->task; //We set it's task field to the request's task field
$todo->category()->associate($category); //We associate our Todo with the category
$todo->save(); //We save it in the Database
$todo->tags()->attach($request->tags); //We create links between our Todo and tags
return $todo;
}
public function update(Request $request, $id)
{
$todo = Todo::find($id); //We retrieve the row of the todo
$todo->done = !$todo->done; //We switch its status
$todo->save(); //We update the row
return $todo;
}
public function destroy($id)
{
return Todo::destroy($id); //We destroy our row by using the id
}
```
As you can see, Eloquent allow easy-to-read code and has already all the functions that we need.
By default, Laravel automatically generates a CSRF "token" for each active user session managed by the application. This token is used to verify that the authenticated user is the one actually making the requests to the application.
The `VerifyCsrfToken` **middleware**, which is included in the web middleware group, will automatically verify that the token in the request input matches the token stored in the session.
So we need to specify that we want to exclude URIs From CSRF Protection. To do so, open `App\Http\Middleware\VerifyCsrfToken` and modify the content:
```php
protected $except = [
'http://localhost/*'
];
```
Now, we can use a REST client (Insomnia or Postman) to create a new todo. Don't forget to set your headers properly:
- **Accept**: application/json
- **Content-Type**: application/json
And let's send the following as our request (`POST http://localhost/todos`):
```json
{
"task": "Buy gifts for the kids",
"category": 1,
"tags": [1,3]
}
```
The server send back the created **todo** and we can check in our database (with Sequel) that the row and its relationships has been properly added.
What about `update(Request $request, $id)` and `destroy($id)`. Let's test those with our REST client:
- `PUT http://localhost/todos/1` -> the column **done** changed to 1 (`true`) for the first row
- `DELETE http://localhost/todos/2` -> the second row has been deleted from the table **todos**
It works!
## Views
Now that we set up the server, let's build our view. Views (or templates) are files that describe what some particular output should look like. Of course, we are going to use **Blade templates**.
Let's start by creating our view. Create a file called `todos.blade.php` inside `resources/views`.
Paste this code:
```html
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css?family=Nunito:200,600" rel="stylesheet">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<!-- Styles -->
<style>
html, body {
background-color: #fff;
color: #636b6f;
font-family: 'Nunito', sans-serif;
font-weight: 200;
height: 100vh;
margin: 0;
}
.full-height {
height: 100vh;
}
.flex-center {
align-items: center;
display: flex;
justify-content: center;
}
.position-ref {
position: relative;
}
.top-right {
position: absolute;
right: 10px;
top: 18px;
}
.content {
text-align: center;
width: 50%;
}
.title {
font-size: 84px;
}
.m-b-md {
margin-bottom: 30px;
}
</style>
</head>
<body>
<div class="flex-center position-ref full-height">
@if (Route::has('login'))
<div class="top-right links">
@auth
<a href="{{ url('/home') }}">Home</a>
@else
<a href="{{ route('login') }}">Login</a>
@if (Route::has('register'))
<a href="{{ route('register') }}">Register</a>
@endif
@endauth
</div>
@endif
<div class="content">
<div class="title m-b-md">
{{ "My TODO's" }}
</div>
<div class="container-fluid">
<!-- Our list separated in 3 parts: creation form, categories and todos -->
<ul class="list-group">
<!-- CREATION FORM -->
<li class="list-group-item">
<form action="http://localhost/todos" method="POST">
<div class="form-row">
<div class="col">
<input type="text" class="form-control" id="task" name="task" placeholder="My new todo...">
</div>