この連載記事では、Laravel を使用した Web アプリケーションの開発方法を紹介します。実際に(お決まりの?)ToDo アプリを開発する手順を通して Web 開発のエッセンスを学んでいただけるように書いていきます。取り扱う Laravel のバージョンは現時点で最新の 5.7 です。
第7章では、タスクの編集機能を実装します。
ルーティング
まずはルーティングの設定を行います。タスク編集機能の URL 設計は以下の通りでした。
URL | メソッド | 処理 |
---|---|---|
/folders/{フォルダID}/tasks/{タスクID}/edit | GET | タスク編集ページを表示する。 |
/folders/{フォルダID}/tasks/{タスクID}/edit | POST | タスク編集処理を実行する。 |
routes/web.php
に以下の2行を追記します。
Route::get('/folders/{id}/tasks/{task_id}/edit', 'TaskController@showEditForm')->name('tasks.edit');
Route::post('/folders/{id}/tasks/{task_id}/edit', 'TaskController@edit');
もうおなじみのパターンですね。
フォームを表示する
入力フォームを表示するルートを実装します。
コントローラー
app/Http/Controllers/TaskController.php
に showEditForm
を追加します。
/**
* GET /folders/{id}/tasks/{task_id}/edit
*/
public function showEditForm(int $id, int $task_id)
{
$task = Task::find($task_id);
return view('tasks/edit', [
'task' => $task,
]);
}
編集対象のタスクデータを取得してテンプレートに渡しています。編集画面では、画面が表示された時にその時点でのタスクの各項目の値が入力欄にすでに入っているべきでしょう。テンプレートでフォームを作成するときに各 input 要素の value に値を入れるためにタスクを渡します。
テンプレート
resources/views/tasks/edit.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.edit', ['id' => $task->folder_id, 'task_id' => $task->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') ?? $task->title }}" />
</div>
<div class="form-group">
<label for="status">状態</label>
<select name="status" id="status" class="form-control">
@foreach(\App\Task::STATUS as $key => $val)
<option
value="{{ $key }}"
{{ $key == old('status', $task->status) ? 'selected' : '' }}
>
{{ $val['label'] }}
</option>
@endforeach
</select>
</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') ?? $task->formatted_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
ここでのポイントは各入力項目の組み立て箇所でしょう。
タイトル
<input type="text" class="form-control" name="title" id="title"
value="{{ old('title', $task->title) }}" />
入力欄の value に old('title', $task->title)
を指定しています。old
関数は直前の入力値を取得すると説明しましたが、第二引数を指定するとそれがデフォルト値になります。つまり「直前の入力値」がない場合は $task->title
が出力されます。「直前の入力値」がない場合というのは、ページを最初に表示したときですね。
このようにして、編集ページを開いたときはタスクを作成したときのタイトルが入力欄に入っていて、値を変更して送信したが入力エラーになって戻ってきたときは変更後の値が入っているという挙動を実現しています。
状態
<select name="status" id="status" class="form-control">
@foreach(\App\Task::STATUS as $key => $val)
<option
value="{{ $key }}"
{{ $key == old('status', $task->status) ? 'selected' : '' }}
>
{{ $val['label'] }}
</option>
@endforeach
</select>
状態の入力欄はセレクトボックスにするためタイトルとは異なった記述になりますが、実現したい挙動は上で説明した内容と同じです。
まず、Task モデルで定義した配列定数 STATUS
を @foreach
でループして option 要素を出力しています。option 要素の value に配列のキー(1, 2, 3)を、タグで囲んだ表示文字列には 'label'
の値を出力します。
const STATUS = [
1 => [ 'label' => '未着手', 'class' => 'label-danger' ],
2 => [ 'label' => '着手中', 'class' => 'label-info' ],
3 => [ 'label' => '完了', 'class' => '' ],
];
そして選択状態を実現するのが以下のコードです。
{{ $key == old('status', $task->status) ? 'selected' : '' }}
セレクトボックスは、selected 属性の置かれた option 要素が初期表示で選択状態となります。そこでループしたキーと old('status', $task->status)
(直前の入力値またはデータベースに登録済みの値)を比べて、一致する場合に option タグの中に 'selected'
を出力しています。
これによってセレクトボックスでも以下の挙動を実現しています。
編集ページを開いたときはタスクを作成したときのタイトルが入力欄に入っていて、値を変更して送信したが入力エラーになって戻ってきたときは変更後の値が入っているという挙動
期限日
<input type="text" class="form-control" name="due_date" id="due_date"
value="{{ old('due_date', $task->formatted_due_date) }}" />
期限日はタイトル入力欄と同じ記述内容です。
ここまでできたらブラウザでフォームがきちんと表示されているか一度確認してみましょう。
テンプレートを部品化する
タスク作成ページとタスク編集ページは似ていますが、明らかに重複している箇所があります。flatpickr のスタイルとスクリプトを読み込む箇所です。その部分を別のテンプレートとして部品化して重複を排除します。
share
ディレクトリに共有パーツを入れることにします。share
の下にさらに flatpickr
ディレクトリを作成し、styles.blade.php
と scripts.blade.php
を作成します。
$ mkdir -p ./resources/views/share/flatpickr
$ touch ./resources/views/share/flatpickr/styles.blade.php
$ touch ./resources/views/share/flatpickr/scripts.blade.php
styles.blade.php
の内容は以下の通りです。
<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">
scripts.blade.php
の内容は以下の通りです。
<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>
作成した共有部品を読み込むように edit.blade.php
を編集します。
@extends('layout')
@section('styles')
@include('share.flatpickr.styles')
@endsection
@section('content')
<!-- 変更なし -->
@endsection
@section('scripts')
@include('share.flatpickr.scripts')
@endsection
@include
で共通部品を読み込んでいます。
タスク作成ページの create.blade.php
も同様に編集しましょう。
タスクを編集する
タスクの編集処理を実装します。
バリデーション
まずはバリデーションのための FormRequest クラスから作成します。
$ php artisan make:request EditTask
雛形 app/Http/Requests/EditTask.php
が作成されるので、以下の内容に編集してください。
<?php
namespace App\Http\Requests;
use App\Task;
use Illuminate\Validation\Rule;
class EditTask extends CreateTask
{
public function rules()
{
$rule = parent::rules();
$status_rule = Rule::in(array_keys(Task::STATUS));
return $rule + [
'status' => 'required|' . $status_rule,
];
}
public function attributes()
{
$attributes = parent::attributes();
return $attributes + [
'status' => '状態',
];
}
public function messages()
{
$messages = parent::messages();
$status_labels = array_map(function($item) {
return $item['label'];
}, Task::STATUS);
$status_labels = implode('、', $status_labels);
return $messages + [
'status.in' => ':attribute には ' . $status_labels. ' のいずれかを指定してください。',
];
}
}
EditTask
クラスは CreateTask
クラスを継承しています。タスクの作成と編集では状態欄の有無が異なるだけでタイトルと期限日は同一なので重複を避けるために継承を用いました。
rules
状態欄には入力値が許可リストに含まれているか検証する in ルールを使用します。
許可リストは array_keys(Task::STATUS)
で配列として取得できるので、Rule クラスの in
メソッドを使ってルールの文字列を作成しています。
$status_rule = Rule::in(array_keys(Task::STATUS));
// -> 'in(1, 2, 3)' を出力する
結果として出力されるルールは以下のようになります。
'status' => 'required|in(1, 2, 3)',
親クラス CreateTask
の rules
メソッドの結果と合体したルールリストを返却します。
attributes
親クラス CreateTask
の attributes
メソッドの結果と合体した属性名リストを返却します。
messages
ここでは Task::STATUS
から status.in
ルールのメッセージを作成しています。Task::STATUS
の各要素から label
キーの値のみ取り出して作った配列をさらに句読点でくっつけて文字列を作成しています。最終的に「状態 には 未着手、着手中、完了 のいずれかを指定してください。」というメッセージが出来上がります。
コントローラー
TaskController
に edit
メソッドを追加します。
public function edit(int $id, int $task_id, EditTask $request)
{
// 1
$task = Task::find($task_id);
// 2
$task->title = $request->title;
$task->status = $request->status;
$task->due_date = $request->due_date;
$task->save();
// 3
return redirect()->route('tasks.index', [
'id' => $task->folder_id,
]);
}
- まずリクエストされた ID でタスクデータを取得します。これが編集対象となります。
- 編集対象のタスクデータにん入力値を詰めて
save
します。 - 最後に編集対象のタスクが属するタスク一覧画面へリダイレクトしています。
テンプレート
最後にタスク一覧テーブルの編集リンクの href を記述します。
<a href="{{ route('tasks.edit', ['id' => $task->folder_id, 'task_id' => $task->id]) }}">
編集
</a>
これでタスクの編集機能は完成です!
ブラウザからタスクを編集できることを確認してみてください。
状態のバリデーションをテストする
さて前章と同様、この章でも画面からは確認できない機能があります。状態セレクトボックスの値が不正だった場合のバリデーションチェックです。テストコードを書いて確認しましょう。
tests/Feature/TaskTest.php
に以下のメソッドを追加します。
/**
* 状態が定義された値ではない場合はバリデーションエラー
* @test
*/
public function status_should_be_within_defined_numbers()
{
$this->seed('TasksTableSeeder');
$response = $this->post('/folders/1/tasks/1/edit', [
'title' => 'Sample task',
'due_date' => Carbon::today()->format('Y/m/d'),
'status' => 999,
]);
$response->assertSessionHasErrors([
'status' => '状態 には 未着手、着手中、完了 のいずれかを指定してください。',
]);
}
テストコードを実行してバリデーションが正しく動作することを確かめましょう。
$ ./vendor/bin/phpunit ./tests/Feature/TaskTest.php --testdox
第7章はこれでおしまいです。
ここまでのソースコードはリポジトリ(chapter07 ブランチ)を参照してください。
次の章では認証機能を実装します。会員登録とログイン機能に加えて、ログインしたユーザーは自分のフォルダのタスクだけを閲覧できる機能も実装します。
連載記事
- 入門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) エラーハンドリング
- 入門Laravelチュートリアル (10) ToDoアプリをHerokuにデプロイする