LaravelでToDoアプリを作る

下記の「入門Laravelチュートリアル 」を参考に、Laravel 7以上でToDoアプリを作成する方法をご紹介します。

※解説は少なめです。**Laravelのバージョンがチュートリアルとは違うため、コードの記述などが変わっているのでご注意ください。**

https://www.hypertextcandy.com/laravel-tutorial-introduction

##作業環境

OS：Windows 10 HOME Edition（ver.2004）
Laravel：7.15.0
Xampp：7.4.6
Composer：1.10.7
Node.js：13.9.0

## (1) イントロダクションと環境構築
[チュートリアルはこちら](https://www.hypertextcandy.com/laravel-tutorial-introduction)
今回はXamppを使い、MySQLで環境構築します。エディタはVSCodeを使用しています。
下記のコマンドを実行すると、Laravelのインストールができます。

```
cd c:\xampp\htdocs
composer create-project laravel/laravel todo_app --prefer-dist
```


「todo_app」はフォルダ名ですので、任意のものに書き換えてください。[以前の記事](https://qiita.com/neneta0921/items/5f3d95a3d8d15a7947e8)にて、Laravelの初期設定について書いてあるので、今回は変更した箇所のみ記述します。
###Laravelのタイムゾーンと言語設定

```config/app.php
70行目　'timezone' => 'Asia/Tokyo',
83行目　'locale' => 'ja',
```

###データベースの言語設定

```config/database.php
55行目　'charset' => 'utf8',
56行目　'collation' => 'utf8_unicode_ci',
```

###デバックバーのインストール
以下のコマンドを実行して、デバックバーのインストールします。

```
composer require barryvdh/laravel-debugbar
```

## (2) ToDoアプリケーションの設計
[チュートリアルはこちら](https://www.hypertextcandy.com/laravel-tutorial-todo-app-design/)
メール認証を実装するので、UserテーブルはLaravel 7にてデフォルトで用意されているものを利用します。

##(3) ToDoアプリのフォルダ一覧表示機能を作る
[チュートリアルはこちら](https://www.hypertextcandy.com/laravel-tutorial-todo-app-list-folders/)
###ルーティングの設定

```routes/web.php
<?php

use Illuminate\Support\Facades\Route;

Route::get('/folders/{id}/tasks', 'TaskController@index')->name('tasks.index');
```

###コントローラークラス

```
php artisan make:controller TaskController
```

```app/Http/Controllers/TaskController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TaskController extends Controller
{
    public function index()
    {
        return "Hello world";
    }
}
```

http://127.0.0.1:8000/folders/1/tasks にアクセスすると「Hello world」が表示されればOKです。
###データベースの設定
「.env」ファイルを下記を参考に変更します。

```
 9行目 DB_CONNECTION=mysql
10行目 DB_HOST=127.0.0.1
11行目 DB_PORT=3306
12行目 DB_DATABASE=データベース名
13行目 DB_USERNAME=ユーザー名
14行目 DB_PASSWORD=パスワード
```

xamppのMySqlのAdminボタンを押してphpMyAdminを起動し、今回作成したデータベースを作成します。
![202006131056.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643170/37351848-85a3-5074-6ccb-813e86a44661.png)
今回はデータベース名は「todo-app-db」とし、**言語は「utf8_general_ci」を選ぶ点に注意**してください。

###マイグレーションファイルの作成

```
php artisan make:migration create_folders_table --create=folders
```

```database/migrations/2020_06_13_104834_create_folders_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateFoldersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('folders', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('title', 20);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('folders');
    }
}
```

###マイグレーションの実行

```
php artisan migrate
```

###モデルクラス
Modelsフォルダの中にModelを作成します。

```
php artisan make:model Models/Folder
```

###テストデータを挿入する

```
php artisan make:seeder FoldersTableSeeder
```

```database/seeds/FoldersTableSeeder.php
<?php

use Carbon\Carbon;
use Illuminate\Database\Seeder;

class FoldersTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $titles = ['プライベート', '仕事', '旅行'];

        foreach ($titles as $title) {
            DB::table('folders')->insert([
                'title' => $title,
                'created_at' => Carbon::now(),
                'updated_at' => Carbon::now(),
            ]);
        }
    }
}
```

```
composer dump-autoload
php artisan db:seed --class=FoldersTableSeeder
```

###コントローラー

```app/Http/Controllers/TaskController.php
<?php

namespace App\Http\Controllers;

use App\Models\Folder;
use Illuminate\Http\Request;

class TaskController extends Controller
{
    public function index()
    {
        $folders = Folder::all();

        return view('tasks/index', [
            'folders' => $folders,
        ]);
    }
}
```

###Blade テンプレートエンジン

```
mkdir resources/views/tasks
New-Item -Type File resources/views/tasks/index.blade.php
```

```resources/views/tasks/index.blade.php
<!DOCTYPE html>
<html lang="ja">

<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>ToDo App</title>
  <link rel="stylesheet" href="/css/styles.css">
</head>

<body>
  <header>
    <nav class="my-navbar">
      <a class="my-navbar-brand" href="/">ToDo App</a>
    </nav>
  </header>
  <main>
    <div class="container">
      <div class="row">
        <div class="col col-md-4">
          <nav class="panel panel-default">
            <div class="panel-heading">フォルダ</div>
            <div class="panel-body">
              <a href="#" class="btn btn-default btn-block">
                フォルダを追加する
              </a>
            </div>
            <div class="list-group">
              @foreach($folders as $folder)
              <a href="{{ route('tasks.index', ['id' => $folder->id]) }}" class="list-group-item">
                {{ $folder->title }}
              </a>
              @endforeach
            </div>
          </nav>
        </div>
        <div class="column col-md-8">
          <!-- ここにタスクが表示される -->
        </div>
      </div>
    </div>
  </main>
</body>

</html>
```

###スタイルシート

```
mkdir public/css
New-Item -Type File public/css/styles.css
```

```public/css/styles.css
@import url('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css');
@import url('https://cdnjs.cloudflare.com/ajax/libs/bootflat/2.0.4/css/bootflat.min.css');

body {
  background-color: #f4f7f8;
}

.navbar {
  margin: 2rem 0 2.5rem 0;
}

.my-navbar {
  align-items: center;
  background: #333;
  display: flex;
  height: 6rem;
  justify-content: space-between;
  padding: 0 2%;
  margin-bottom: 3rem;
}

.my-navbar-brand {
  font-size: 18px;
}

.my-navbar-brand,
.my-navbar-item {
  color: #8c8c8c;
}

.my-navbar-brand:hover,
a.my-navbar-item:hover {
  color: #ffffff;
}

.table td:nth-child(2),
.table td:nth-child(3),
.table td:nth-child(4) {
  white-space: nowrap;
  width: 1px;
}

.form-control[disabled],
.form-control[readonly] {
  background-color: #fff;
}
```

###フォルダ名を選択表示にする

```app/Http/Controllers/TaskController.php
public function index(int $id)
{
    $folders = Folder::all();

    return view('tasks/index', [
        'folders' => $folders,
        'current_folder_id' => $id,
    ]);
}
```

```resources/views/tasks/index.blade.php
// 29行目
<a href="{{ route('tasks.index', ['id' => $folder->id]) }}" class="list-group-item {{ $current_folder_id === $folder->id ? 'active' : '' }}">
```

##(4) ToDoアプリのタスク一覧表示機能を作る
[チュートリアルはこちら](https://www.hypertextcandy.com/laravel-tutorial-todo-app-list-tasks/)
###マイグレーションとモデルクラス

```
php artisan make:migration create_tasks_table --create=tasks
```

[公式サイト](https://readouble.com/laravel/7.x/ja/migrations.html)を見ると、外部キーの書き方が違います。

```2020_06_13_135200_create_tasks_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTasksTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->foreignId('folder_id')->constrained(); // 外部キーを設定する
            $table->string('title', 100);
            $table->date('due_date');
            $table->integer('status')->default(1);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('tasks');
    }
}
```

```
php artisan migrate
php artisan make:model Models/Task
```

###テストデータの作成と確認

```
php artisan make:seeder TasksTableSeeder
```

```database/seeds/TasksTableSeeder.php
<?php

use Carbon\Carbon;
use Illuminate\Database\Seeder;

class TasksTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        foreach (range(1, 3) as $num) {
            DB::table('tasks')->insert([
                'folder_id' => 1,
                'title' => "サンプルタスク {$num}",
                'status' => $num,
                'due_date' => Carbon::now()->addDay($num),
                'created_at' => Carbon::now(),
                'updated_at' => Carbon::now(),
            ]);
        }
    }
}
```

```
php artisan db:seed --class=TasksTableSeeder
```

###コントローラー

```app/Http/Controllers/TaskController.php
<?php

namespace App\Http\Controllers;

use App\Models\Folder;
use App\Models\Task;
use Illuminate\Http\Request;

class TaskController extends Controller
{
    public function index(int $id)
    {
        // すべてのフォルダを取得する
        $folders = Folder::all();
    
        // 選ばれたフォルダを取得する
        $current_folder = Folder::find($id);
    
        // 選ばれたフォルダに紐づくタスクを取得する
        $tasks = Task::where('folder_id', $current_folder->id)->get();
    
        return view('tasks/index', [
            'folders' => $folders,
            'current_folder_id' => $current_folder->id,
            'tasks' => $tasks,
        ]);
    }
}
```

```resources/views/tasks/index.blade.php
<!DOCTYPE html>
<html lang="ja">

<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>ToDo App</title>
  <link rel="stylesheet" href="/css/styles.css">
</head>

<body>
  <header>
    <nav class="my-navbar">
      <a class="my-navbar-brand" href="/">ToDo App</a>
    </nav>
  </header>
  <main>
    <div class="container">
      <div class="row">
        <div class="col col-md-4">
          <nav class="panel panel-default">
            <div class="panel-heading">フォルダ</div>
            <div class="panel-body">
              <a href="#" class="btn btn-default btn-block">
                フォルダを追加する
              </a>
            </div>
            <div class="list-group">
              @foreach($folders as $folder)
              <a href="{{ route('tasks.index', ['id' => $folder->id]) }}" class="list-group-item {{ $current_folder_id === $folder->id ? 'active' : '' }}">
                @endforeach
            </div>
          </nav>
        </div>
        <div class="column col-md-8">
          <div class="panel panel-default">
            <div class="panel-heading">タスク</div>
            <div class="panel-body">
              <div class="text-right">
                <a href="#" class="btn btn-default btn-block">
                  タスクを追加する
                </a>
              </div>
            </div>
            <table class="table">
              <thead>
                <tr>
                  <th>タイトル</th>
                  <th>状態</th>
                  <th>期限</th>
                  <th></th>
                </tr>
              </thead>
              <tbody>
                @foreach($tasks as $task)
                <tr>
                  <td>{{ $task->title }}</td>
                  <td>
                    <span class="label">{{ $task->status }}</span>
                  </td>
                  <td>{{ $task->due_date }}</td>
                  <td><a href="#">編集</a></td>
                </tr>
                @endforeach
              </tbody>
            </table>
          </div>
        </div>
      </div>
    </div>
  </main>
</body>

</html>
```

###Task モデルにアクセサを追加する

```app/Models/Task.php
class Task extends Model
{
    public function index(int $id)
    {
        // すべてのフォルダを取得する
        $folders = Folder::all();
    
        // 選ばれたフォルダを取得する
        $current_folder = Folder::find($id);
    
        // 選ばれたフォルダに紐づくタスクを取得する
        $tasks = Task::where('folder_id', $current_folder->id)->get();
    
        return view('tasks/index', [
            'folders' => $folders,
            'current_folder_id' => $current_folder->id,
            'tasks' => $tasks,
        ]);
    }

      /**
       * 状態定義
       */
      const STATUS = [
        1 => [ 'label' => '未着手' ],
        2 => [ 'label' => '着手中' ],
        3 => [ 'label' => '完了' ],
    ];
  
    /**
     * 状態のラベル
     * @return string
     */
    public function getStatusLabelAttribute()
    {
        // 状態値
        $status = $this->attributes['status'];
    
        // 定義されていなければ空文字を返す
        if (!isset(self::STATUS[$status])) {
            return '';
        }
      
        return self::STATUS[$status]['label'];
    }
}
```

```resources/views/tasks/index.blade.php
変更前
<span class="label">{{ $task->status }}</span>

変更後
<span class="label {{ $task->status_class }}">{{ $task->status_label }}</span>
```

```app/Models/Task.php
<?php

namespace App\Models;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
    /**
       * 状態定義
       */
      const STATUS = [
        1 => [ 'label' => '未着手' ],
        2 => [ 'label' => '着手中' ],
        3 => [ 'label' => '完了' ],
    ];
  
    /**
     * 状態のラベル
     * @return string
     */
    public function getStatusLabelAttribute()
    {
        // 状態値
        $status = $this->attributes['status'];
    
        // 定義されていなければ空文字を返す
        if (!isset(self::STATUS[$status])) {
            return '';
        }
      
        return self::STATUS[$status]['label'];
    }

    /**
     * 整形した期限日
     * @return string
     */
    public function getFormattedDueDateAttribute()
    {
        return Carbon::createFromFormat('Y-m-d', $this->attributes['due_date'])
            ->format('Y/m/d');
    }
}
```

```resources/views/tasks/index.blade.php
変更前
<td>{{ $task->due_date }}</td>

変更後
<td>{{ $task->formatted_due_date }}</td>
```

###モデルクラスにおけるリレーション

```app/Models/Folder.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Folder extends Model
{
  public function tasks()
  {
      return $this->hasMany('App\Models\Task');
  }
}
```

```app/Http/Controllers/TaskController.php
public function index(int $id)
{
    // すべてのフォルダを取得する
    $folders = Folder::all();

    // 選ばれたフォルダを取得する
    $current_folder = Folder::find($id);

    // 選ばれたフォルダに紐づくタスクを取得する
    $tasks = $current_folder->tasks()->get(); // ★

    return view('tasks/index', [
        'folders' => $folders,
        'current_folder_id' => $current_folder->id,
        'tasks' => $tasks,
    ]);
}
```


##(5) ToDoアプリのフォルダ作成機能を作る

###ルーティング

```routes/web.php
<?php

use Illuminate\Support\Facades\Route;

// タスク一覧ページを表示
Route::get('/folders/{id}/tasks', 'TaskController@index')->name('tasks.index');

// フォルダ作成機能
Route::get('/folders/create', 'FolderController@showCreateForm')->name('folders.create');
Route::post('/folders/create', 'FolderController@create');
```

###フォームを表示する
####コントローラー

```
php artisan make:controller FolderController
```

```app/Http/Controllers/FolderController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class FolderController extends Controller
{
    public function showCreateForm()
    {
        return view('folders/create');
    }
}
```

####テンプレート

```
mkdir ./resources/views/folders
New-Item -Type File ./resources/views/folders/create.blade.php
```

```resources/views/folders/create.blade.php
<!DOCTYPE html>
<html lang="ja">
<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>ToDo App</title>
  <link rel="stylesheet" href="/css/styles.css">
</head>
<body>
  <header>
    <nav class="my-navbar">
      <a class="my-navbar-brand" href="/">ToDo App</a>
    </nav>
  </header>
  <main>
    <div class="container">
      <div class="row">
        <div class="col col-md-offset-3 col-md-6">
          <nav class="panel panel-default">
            <div class="panel-heading">フォルダを追加する</div>
            <div class="panel-body">
              <form action="{{ route('folders.create') }}" method="post">
                @csrf
                <div class="form-group">
                  <label for="title">フォルダ名</label>
                  <input type="text" class="form-control" name="title" id="title" />
                </div>
                <div class="text-right">
                  <button type="submit" class="btn btn-primary">送信</button>
                </div>
              </form>
            </div>
          </nav>
        </div>
      </div>
    </div>
  </main>
</body>
</html>
```

###フォルダを保存する
####コントローラー

```app/Http/Controllers/FolderController.php
<?php

namespace App\Http\Controllers;

use App\Models\Folder;
use Illuminate\Http\Request;

class FolderController extends Controller
{
    public function showCreateForm()
    {
        return view('folders/create');
    }

    public function create(Request $request)
    {
        // フォルダモデルのインスタンスを作成する
        $folder = new Folder();
        // タイトルに入力値を代入する
        $folder->title = $request->title;
        // インスタンスの状態をデータベースに書き込む
        $folder->save();
    
        return redirect()->route('tasks.index', [
            'id' => $folder->id,
        ]);
    }
}
```

###テンプレート

```resources/views/tasks/index.blade.php
変更前
<a href="#" class="btn btn-default btn-block">
　フォルダを追加する
</a>

変更後
<a href="{{ route('folders.create') }}" class="btn btn-default btn-block">
  フォルダを追加する
</a>
```

###入力値バリデーション
####FormRequest クラス

```
php artisan make:request CreateFolder
```

```app/Http?Request/CreateFolder.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class CreateFolder 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', // ★
        ];
    }
}
```

####コントローラー

```app/Http/Controller/FolderController.php
<?php

namespace App\Http\Controllers;

use App\Http\Requests\CreateFolder; // ★ 追加
use App\Models\Folder;
use Illuminate\Http\Request;

class FolderController extends Controller
{
    public function showCreateForm()
    {
        return view('folders/create');
    }

    public function create(CreateFolder $request) // ★ 引数の型を変更
    {
        // フォルダモデルのインスタンスを作成する
        $folder = new Folder();
        // タイトルに入力値を代入する
        $folder->title = $request->title;
        // インスタンスの状態をデータベースに書き込む
        $folder->save();
    
        return redirect()->route('tasks.index', [
            'id' => $folder->id,
        ]);
    }
}
```

####エラーメッセージを表示する

```routes/folders/create.blade.php
<!DOCTYPE html>
<html lang="ja">

<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>ToDo App</title>
  <link rel="stylesheet" href="/css/styles.css">
</head>

<body>
  <header>
    <nav class="my-navbar">
      <a class="my-navbar-brand" href="/">ToDo App</a>
    </nav>
  </header>
  <main>
    <div class="container">
      <div class="row">
        <div class="col col-md-offset-3 col-md-6">
          <nav class="panel panel-default">
            <div class="panel-heading">フォルダを追加する</div>
            <div class="panel-body">
              @if($errors->any())
              <div class="alert alert-danger">
                <ul>
                  @foreach($errors->all() as $message)
                  <li>{{ $message }}</li>
                  @endforeach
                </ul>
              </div>
              @endif
              <form action="{{ route('folders.create') }}" method="post">
                @csrf
                <div class="form-group">
                  <label for="title">フォルダ名</label>
                  <input type="text" class="form-control" name="title" id="title" />
                </div>
                <div class="text-right">
                  <button type="submit" class="btn btn-primary">送信</button>
                </div>
              </form>
            </div>
          </nav>
        </div>
      </div>
    </div>
  </main>
</body>

</html>
```

####エラーメッセージを日本語化する

```
mkdir ./resources/lang/ja
cp ./resources/lang/en/validation.php ./resources/lang/ja/
```

後はチュートリアル通り進めていってください。
今回は息が切れたのでここまでです。