この連載記事では、Laravel を使用した Web アプリケーションの開発方法を紹介します。実際に(お決まりの?)ToDo アプリを開発する手順を通して Web 開発のエッセンスを学んでいただけるように書いていきます。取り扱う Laravel のバージョンは現時点で最新の 5.7 です。
第6章では、タスクの新規作成機能を実装します。
ルーティング
まずはルーティングの設定を行います。タスク作成機能の URL 設計は以下の通りでした。
URL | メソッド | 処理 |
---|---|---|
/folders/{フォルダID}/tasks/create | GET | タスク作成ページを表示する。 |
/folders/{フォルダID}/tasks/create | POST | タスク作成処理を実行する。 |
routes/web.php
に以下の2行を追記します。
Route::get('/folders/{id}/tasks/create', 'TaskController@showCreateForm')->name('tasks.create');
Route::post('/folders/{id}/tasks/create', 'TaskController@create');
パターンは前章で実装したフォルダ作成機能と一緒ですね。
フォームを表示する
入力フォームを表示するルートを実装します。
コントローラー
app/Http/Controllers/TaskController.php
に showCreateForm
メソッドを追加します。
/**
* GET /folders/{id}/tasks/create
*/
public function showCreateForm(int $id)
{
return view('tasks/create', [
'folder_id' => $id
]);
}
テンプレートで form 要素の action 属性としてタスク作成 URL(/folders/{id}/tasks/create
)を作るためにフォルダの ID が必要なので、コントローラーメソッドの引数で受け取って view
関数でテンプレートに渡しています。
テンプレート
resources/views/tasks/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="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<link rel="stylesheet" href="https://npmcdn.com/flatpickr/dist/themes/material_blue.css">
<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">
@foreach($errors->all() as $message)
<p>{{ $message }}</p>
@endforeach
</div>
@endif
<form action="{{ route('tasks.create', ['id' => $folder_id]) }}" method="POST">
@csrf
<div class="form-group">
<label for="title">タイトル</label>
<input type="text" class="form-control" name="title" id="title" value="{{ old('title') }}" />
</div>
<div class="form-group">
<label for="due_date">期限</label>
<input type="text" class="form-control" name="due_date" id="due_date" value="{{ old('due_date') }}" />
</div>
<div class="text-right">
<button type="submit" class="btn btn-primary">送信</button>
</div>
</form>
</div>
</nav>
</div>
</div>
</div>
</main>
<script src="https://npmcdn.com/flatpickr/dist/flatpickr.min.js"></script>
<script src="https://npmcdn.com/flatpickr/dist/l10n/ja.js"></script>
<script>
flatpickr(document.getElementById('due_date'), {
locale: 'ja',
dateFormat: "Y/m/d",
minDate: new Date()
});
</script>
</body>
</html>
HTML の部分は新しい知識はありません。前章までに扱った知識で理解できるでしょう。
- form の
action
属性にはroute
関数 @csrf
- 入力エラーメッセージの表示欄
- 入力エラーで戻ってきたときのために
value
属性にold
関数
JavaScript ライブラリ
この章で初めて登場するのが JavaScript です。日付(期限日)の入力欄で flatpickr というライブラリを使います。このライブラリによって以下のようなキレイな日付選択機能が実装できます。
flatpickr を使用するために、4つのファイルを読み込んでいます。
<!-- デフォルトのスタイルシート -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<!-- ブルーテーマの追加スタイルシート -->
<link rel="stylesheet" href="https://npmcdn.com/flatpickr/dist/themes/material_blue.css">
<!-- flatpickrスクリプト -->
<script src="https://npmcdn.com/flatpickr/dist/flatpickr.min.js"></script>
<!-- 日本語化のための追加スクリプト -->
<script src="https://npmcdn.com/flatpickr/dist/l10n/ja.js"></script>
スタイルシートは head 内で、スクリプトは body の一番最後で読み込みます。さらに flatpickr の機能を有効化します。第一引数に flatpickr で日付選択を行わせたい要素を指定し、第二引数にオプションを渡します。
flatpickr(document.getElementById('due_date'), {
locale: 'ja',
dateFormat: "Y/m/d",
minDate: new Date()
});
ここでは言語設定(曜日を月火水…と日本語表記するため)と日付表記のフォーマット、また本日日付よろ若い日付(過去)を入力できないようにオプションで指定しました。
ここまでできたらブラウザでタスク作成ページ(/folders/1/tasks/create
)を確認してみましょう。フォームは表示されているでしょうか。日付選択はできていますか。
レイアウトでテンプレートを整理する
ここでテンプレートを整理するために、Blade のレイアウト機能を紹介します。今までタスク一覧ページ、フォルダ作成ページ、タスク作成ページと3枚のテンプレートファイルを作成しましたが、重複する箇所がいくつかありましたね。head 要素や header 要素です。このような重複はレイアウトファイルにまとめることができます。
レイアウトファイル
まずはレイアウト resources/views/layout.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>
@yield('styles')
<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>
@yield('content')
</main>
@yield('scripts')
</body>
</html>
レイアウトとは枠組み、骨組みのような意味合いでしょうか。ページごとに変わらない部分だけを記述します。ページごとに変化する部分は @yield
で穴埋めにしています。いろいろ説明するよりページのテンプレートを見たほうが分かりやすいと思いますので、まずは先ほど作成したタスク作成ページのテンプレート resources/views/tasks/create.blade.php
を以下の通りに編集してください。
タスク作成ページテンプレート
@extends('layout')
@section('styles')
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<link rel="stylesheet" href="https://npmcdn.com/flatpickr/dist/themes/material_blue.css">
@endsection
@section('content')
<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">
@foreach($errors->all() as $message)
<p>{{ $message }}</p>
@endforeach
</div>
@endif
<form action="{{ route('tasks.create', ['id' => $folder_id]) }}" method="POST">
@csrf
<div class="form-group">
<label for="title">タイトル</label>
<input type="text" class="form-control" name="title" id="title" value="{{ old('title') }}" />
</div>
<div class="form-group">
<label for="due_date">期限</label>
<input type="text" class="form-control" name="due_date" id="due_date" value="{{ old('due_date') }}" />
</div>
<div class="text-right">
<button type="submit" class="btn btn-primary">送信</button>
</div>
</form>
</div>
</nav>
</div>
</div>
</div>
@endsection
@section('scripts')
<script src="https://npmcdn.com/flatpickr/dist/flatpickr.min.js"></script>
<script src="https://npmcdn.com/flatpickr/dist/l10n/ja.js"></script>
<script>
flatpickr(document.getElementById('due_date'), {
locale: 'ja',
dateFormat: "Y/m/d",
minDate: new Date()
});
</script>
@endsection
このテンプレートファイルは以下の構造から成り立っています。
@extends('layout')
@section('styles')
<!-- ... -->
@endsection
@section('content')
<!-- ... -->
@endsection
@section('scripts')
<!-- ... -->
@endsection
冒頭の @extends('layout')
で先ほど作成した resources/views/layout.blade.php
をレイアウトファイルとして使用することを宣言しています。@extends
の引数には resources/views
からの相対パスでファイル名(.blade.php
は除く)を記述します。
続いて @section
〜 @endsection
のブロックが3つあります。それぞれのブロックには名前がついていますね。styles
、content
、scripts
です。これはレイアウトファイルの @yield
の名前に対応しています。つまり @yield('abc')
の部分が @section('abc')
〜 @endsection
で置き換わって HTML が作成されるということです。
タスク一覧ページとフォルダ作成ページのテンプレートもレイアウトを使う形に書き換えます。
タスク一覧ページテンプレート
@extends('layout')
@section('content')
<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="{{ route('folders.create') }}" 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' : '' }}"
>
{{ $folder->title }}
</a>
@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_class }}">{{ $task->status_label }}</span>
</td>
<td>{{ $task->formatted_due_date }}</td>
<td><a href="#">編集</a></td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
@endsection
フォルダ作成ページテンプレート
@extends('layout')
@section('content')
<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">
@foreach($errors->all() as $message)
<p>{{ $message }}</p>
@endforeach
</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" value="{{ old('title') }}" />
</div>
<div class="text-right">
<button type="submit" class="btn btn-primary">送信</button>
</div>
</form>
</div>
</nav>
</div>
</div>
</div>
@endsection
タスク一覧ページとフォルダ作成ページには追加のスタイルシートやスクリプトはありません。そういう場合は @section
自体を省略すれば OK です。
ここまでできたらブラウザで見た目が変わっていないことを確認しましょう。
タスクを保存する
タスク作成機能を実装します。
バリデーション
まずはバリデーションのための FormRequest クラスから作成します。
$ php artisan make:request CreateTask
雛形 app/Http/Requests/CreateTask.php
が作成されるので、以下の内容に編集してください。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CreateTask 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|max:100',
'due_date' => 'required|date|after_or_equal:today',
];
}
public function attributes()
{
return [
'title' => 'タイトル',
'due_date' => '期限日',
];
}
public function messages()
{
return [
'due_date.after_or_equal' => ':attribute には今日以降の日付を入力してください。',
];
}
}
title
のバリデーションルールはフォルダ作成のときと同様ですね。due_date
のルールには date
(日付を表す値であること)と after_or_equal
(特定の日付と同じまたはそれ以降の日付であること)を使用しています。after_or_equal
の引数として today
を指定することにより今日を含んだ未来日だけを許容します(タスクの期限日が過去だとおかしいですよね)。
また CreateTask
では messages
メソッドも実装しています。このメソッドは FormRequest クラス単位でエラーメッセージするために定義します。
[
// キーでメッセージが表示されるべきルールを指定する。
// ドット区切りで左側が項目、右側がルールを意味する。
'due_date.after_or_equal' => ':attribute には今日以降の日付を入力してください。',
]
due_date
の after_or_equal
ルールに違反した場合は、値に指定されたメッセージを出力するという意味です。一般的なルールについては validation.php
に記述しますが、messages
メソッドでは個別の FormRequest クラスの内部でのみ有効なメッセージを定義できます。
ちなみにここで after_or_equal
ルールのメッセージ定義に validation.php
ではなく messages
メソッドを使用した理由は...
'after_or_equal' => 'The :attribute must be a date after or equal to :date.',
上記のルールのうち :date
には after_or_equal
の引数、つまり today
が入るのですが、これをうまく「今日」などの日本語にできなかったからです。
jp/validation.php
には date
ルールの翻訳を追加します。
'date' => ':attribute には日付を入力してください。',
コントローラー
コントローラーを実装します。
まずは TaskController
の冒頭で CreateTask
コントローラーをインポートします。
use App\Http\Requests\CreateTask;
次に create
メソッドを追加してください。
public function create(int $id, CreateTask $request)
{
$current_folder = Folder::find($id);
$task = new Task();
$task->title = $request->title;
$task->due_date = $request->due_date;
$current_folder->tasks()->save($task);
return redirect()->route('tasks.index', [
'id' => $current_folder->id,
]);
}
ここでのポイントは、リレーションを活かしたデータの保存方法です。
$current_folder->tasks()->save($task);
上の記述により、$current_folder
に紐づくタスクを作成しています。
テンプレート
最後にタスク一覧ページ resources/views/tasks/index.blade.php
のリンクを編集します。
<a href="{{ route('tasks.create', ['id' => $current_folder_id]) }}" class="btn btn-default btn-block">
タスクを追加する
</a>
ここまでできたらブラウザから確認しましょう。うまくタスクデータを作成できましたか?
期限日のバリデーションをテストする
タスクの作成機能は実装できました!ただ、実装したものの動作を確認できていない箇所があります。それは期限日のバリデーションです。期限日の入力欄は JavaScript で制御しているため、自由なデータを入力できないので、不正なデータをきちんと弾けているかの確認もできません。
そこで、画面から確認できない機能についてはテストコードで正しさを確かめます。
準備
まずはテストを書くまえの準備として、テストのときは開発用のデータベースとは別に、メモリ上の(つまりテストコードが実行し終わると消える)データベースを使う設定を行います。これによりテストを実行する間だけ存在すればいいデータが開発の邪魔になりません。
config/database.php
の connections
に以下の設定を追加してください。
'connections' => [
// ★ この設定を追加
'sqlite_testing' => [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
],
// 中略
]
Laravel ではテスティングライブラリとして PHPUnit をサポートしています。設定ファイル phpunit.xml
に DB_CONNECTION
の設定を追加します。
<php>
<env name="APP_ENV" value="testing"/>
<env name="DB_CONNECTION" value="sqlite_testing"/> <!-- ★ この行を追加-->
<!-- 中略 -->
</php>
これで準備完了です。コマンドラインからテストコードの雛形を生成します。
$ php artisan make:test TaskTest
tests/Feature/TaskTest.php
が作成されたはずです。
テストケースを記述する
TaskTest.php
にバリデーションエラーが発生するケースについてのテストを記述します。
<?php
namespace Tests\Feature;
use App\Http\Requests\CreateTask;
use Carbon\Carbon;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
class TaskTest extends TestCase
{
// テストケースごとにデータベースをリフレッシュしてマイグレーションを再実行する
use RefreshDatabase;
/**
* 各テストメソッドの実行前に呼ばれる
*/
public function setUp()
{
parent::setUp();
// テストケース実行前にフォルダデータを作成する
$this->seed('FoldersTableSeeder');
}
/**
* 期限日が日付ではない場合はバリデーションエラー
* @test
*/
public function due_date_should_be_date()
{
$response = $this->post('/folders/1/tasks/create', [
'title' => 'Sample task',
'due_date' => 123, // 不正なデータ(数値)
]);
$response->assertSessionHasErrors([
'due_date' => '期限日 には日付を入力してください。',
]);
}
/**
* 期限日が過去日付の場合はバリデーションエラー
* @test
*/
public function due_date_should_not_be_past()
{
$response = $this->post('/folders/1/tasks/create', [
'title' => 'Sample task',
'due_date' => Carbon::yesterday()->format('Y/m/d'), // 不正なデータ(昨日の日付)
]);
$response->assertSessionHasErrors([
'due_date' => '期限日 には今日以降の日付を入力してください。',
]);
}
}
タスクの作成にはそもそもフォルダデータが必要なので、各テストメソッドの実行前に呼ばれる setUp
メソッドで、3章で作成したシーダーを実行しています。
due_date_should_be_date
メソッドは due_date
が日付ではないときに入力エラーとなることを確かめるメソッドです。due_date_should_not_be_past
メソッドは due_date
が過去日付であるときに入力エラーとなることを確かめるメソッドです。
それぞれのテストメソッドには冒頭のコメントで @test
と記述しています。テストメソッドとして認識されるために必要なので注意しましょう。
テストメソッドの内容を簡単に説明すると、まず TestCase クラスに用意されている post
メソッドでタスク作成ルートにアクセスしています。
// 第一引数 … アクセスする URL
// 第二引数 … 入力値
$response = $this->post('/folders/1/tasks/create', [
'title' => 'Sample task',
'due_date' => 123, // 不正なデータ(数値)
]);
このように、TestCase クラスの機能を使えば画面へのアクセスをコードで再現できます。入力値にはそれぞれ数値と昨日日付という、エラーになるべきデータを指定しています。そしてレスポンスを受け取った $response
変数に対して、assertSessionHasErrors
メソッドでエラーメッセージがあることを確かめています。
$response->assertSessionHasErrors([
'due_date' => '期限日 には日付を入力してください。',
]);
ではコマンドラインでテストを実行してみましょう。
$ ./vendor/bin/phpunit ./tests/Feature/TaskTest.php --testdox
このように、どちらのテストにもチェックマークが表示されれば OK です。
これで不正な値を送信したときにバリデーションがうまくエラーを返すことが確認できました
第6章はこれでおしまいです。
ここまでのソースコードはリポジトリ(chapter06 ブランチ)を参照してください。
次の章ではタスクを編集する機能を実装します。
連載記事
- 入門Laravelチュートリアル (1) イントロダクション
- 入門Laravelチュートリアル (2) ToDoアプリケーションの設計
- 入門Laravelチュートリアル (3) ToDoアプリのフォルダ一覧表示機能を作る
- 入門Laravelチュートリアル (4) ToDoアプリのタスク一覧表示機能を作る
- 入門Laravelチュートリアル (5) ToDoアプリのフォルダ作成機能を作る
- 入門Laravelチュートリアル (6) ToDoアプリのタスク作成機能を作る
- 入門Laravelチュートリアル (7) ToDoアプリのタスク編集機能を作る
- 入門Laravelチュートリアル (8) ToDoアプリの認証機能を作る
- 入門Laravelチュートリアル (9) ToDoアプリの認証機能を作る パート2
- 入門Laravelチュートリアル (10) エラーハンドリング
- 入門Laravelチュートリアル (11) ToDoアプリをHerokuにデプロイする