6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

LaravelでToDoアプリを作る

Last updated at Posted at 2020-06-13

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

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

##作業環境

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) イントロダクションと環境構築

チュートリアルはこちら
今回はXamppを使い、MySQLで環境構築します。エディタはVSCodeを使用しています。
下記のコマンドを実行すると、Laravelのインストールができます。

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

「todo_app」はフォルダ名ですので、任意のものに書き換えてください。以前の記事にて、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アプリケーションの設計

チュートリアルはこちら
メール認証を実装するので、UserテーブルはLaravel 7にてデフォルトで用意されているものを利用します。

##(3) ToDoアプリのフォルダ一覧表示機能を作る
チュートリアルはこちら
###ルーティングの設定

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
今回はデータベース名は「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アプリのタスク一覧表示機能を作る
チュートリアルはこちら
###マイグレーションとモデルクラス

php artisan make:migration create_tasks_table --create=tasks

公式サイトを見ると、外部キーの書き方が違います。

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/

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

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?