下記の「入門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のタイムゾーンと言語設定
70行目 'timezone' => 'Asia/Tokyo',
83行目 'locale' => 'ja',
###データベースの言語設定
55行目 'charset' => 'utf8',
56行目 'collation' => 'utf8_unicode_ci',
###デバックバーのインストール
以下のコマンドを実行して、デバックバーのインストールします。
composer require barryvdh/laravel-debugbar
(2) ToDoアプリケーションの設計
チュートリアルはこちら
メール認証を実装するので、UserテーブルはLaravel 7にてデフォルトで用意されているものを利用します。
##(3) ToDoアプリのフォルダ一覧表示機能を作る
チュートリアルはこちら
###ルーティングの設定
<?php
use Illuminate\Support\Facades\Route;
Route::get('/folders/{id}/tasks', 'TaskController@index')->name('tasks.index');
###コントローラークラス
php artisan make:controller TaskController
<?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を起動し、今回作成したデータベースを作成します。
今回はデータベース名は「todo-app-db」とし、言語は「utf8_general_ci」を選ぶ点に注意してください。
###マイグレーションファイルの作成
php artisan make:migration create_folders_table --create=folders
<?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
<?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
###コントローラー
<?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
<!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
@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;
}
###フォルダ名を選択表示にする
public function index(int $id)
{
$folders = Folder::all();
return view('tasks/index', [
'folders' => $folders,
'current_folder_id' => $id,
]);
}
// 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
公式サイトを見ると、外部キーの書き方が違います。
<?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
<?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
###コントローラー
<?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,
]);
}
}
<!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 モデルにアクセサを追加する
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'];
}
}
変更前
<span class="label">{{ $task->status }}</span>
変更後
<span class="label {{ $task->status_class }}">{{ $task->status_label }}</span>
<?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');
}
}
変更前
<td>{{ $task->due_date }}</td>
変更後
<td>{{ $task->formatted_due_date }}</td>
###モデルクラスにおけるリレーション
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Folder extends Model
{
public function tasks()
{
return $this->hasMany('App\Models\Task');
}
}
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アプリのフォルダ作成機能を作る
###ルーティング
<?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
<?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
<!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>
###フォルダを保存する
####コントローラー
<?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,
]);
}
}
###テンプレート
変更前
<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
<?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', // ★
];
}
}
####コントローラー
<?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,
]);
}
}
####エラーメッセージを表示する
<!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/
後はチュートリアル通り進めていってください。
今回は息が切れたのでここまでです。