この連載記事では、Laravel を使用した Web アプリケーションの開発方法を紹介します。実際に(お決まりの?)ToDo アプリを開発する手順を通して Web 開発のエッセンスを学んでいただけるように書いていきます。取り扱う Laravel のバージョンは現時点で最新の 5.7 です。
第4章では、タスクの一覧表示機能を作っていきます。
前回の第3章でタスク一覧ページのルーティングは設定済みなので、今回はマイグレーションからスタートします。
マイグレーションとモデルクラス
マイグレーションファイルの作成と実行
まずはフォルダテーブルと同様にマイグレーションファイルを作成します。
$ php artisan make:migration create_tasks_table --create=tasks
タスクテーブルのテーブル定義をおさらいしましょう。
カラム論理名 | カラム物理名 | 型 | 型の意味 |
---|---|---|---|
ID | id | SERIAL | 連番(自動採番) |
フォルダID | folder_id | INTEGER | 数値 |
タイトル | title | VARCHAR(100) | 100文字までの文字列 |
状態 | status | INTEGER | 数値 |
期限日 | due_date | DATE | 日付 |
作成日 | created_at | TIMESTAMP | 日付と時刻 |
更新日 | updated_at | TIMESTAMP | 日付と時刻 |
こちらのテーブル定義をもとにマイグレーションファイルを記述します。
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateTasksTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('tasks', function (Blueprint $table) {
$table->increments('id');
$table->integer('folder_id')->unsigned();
$table->string('title', 100);
$table->date('due_date');
$table->integer('status')->default(1);
$table->timestamps();
// 外部キーを設定する
$table->foreign('folder_id')->references('id')->on('folders');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('tasks');
}
}
カラムの型に合わせて定義を記述していく点はフォルダテーブルと一緒ですね。どの型にどのメソッドを使うかなどマイグレーションファイルの記述方法について詳しくはマニュアル( 公式 / 日本語)を参照してください。
ここでのポイントは2点あります。まず状態カラムにデフォルト値を設定している点です。
$table->integer('status')->default(1);
タスクを作成した最初の状態は必ず「未着手」状態でしょうから、何も指定しないときは「1」が入るようにしておきます。
次に外部キー制約を設定している点に注目してください。
// 外部キー制約を設定する
$table->foreign('folder_id')->references('id')->on('folders');
外部キー制約は他のテーブルとの結びつきを表現するためのカラムに設定します。外部キー制約が設定されたカラムには、好き勝手な値は入れられなくなります。今回の例で言うと、タスクテーブルのフォルダID列には実際に存在するフォルダIDの値しか入れることができなくなります。これによりデータの不整合を防ぐわけです。
さてマイグレーションファイルが書けたらコマンドラインからマイグレーションを実行しましょう。
$ php artisan migrate
データベースクライアントでテーブルが作成できているか確認しておいてください。
モデルクラスの作成
続いてタスクテーブルに対応するモデルクラスを作成します。
$ php artisan make:model Task
タスククラスはとりあえずできたままの状態で OK です。
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Task extends Model
{
//
}
テストデータの作成と確認
シーダーを作成する
テストデータを挿入するためにシーダーを作成します。
$ php artisan make:seeder TasksTableSeeder
database/seeds/TasksTableSeeder.php
を以下の通り記述してください。
<?php
use Carbon\Carbon;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
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(),
]);
}
}
}
ID = 1 のフォルダに対して3つのタスクを登録します。ちなみに due_date
には Carbon ライブラリの addDay
メソッドを利用してそれぞれ現在時間から1〜3日加算した日付を指定しています。Carbon は非常に便利な日付操作ライブラリなので覚えておくとよいですね。
さて、書けたら実行してください。
$ php artisan db:seed --class=TasksTableSeeder
上記のコマンドがうまくいかない場合は composer dump-autoload
コマンドを打ってから実行し直してください。
tinker を使ってみよう
ここでテストデータの確認のために、Laravel の便利なコマンド tinker を使います。まずは以下のコマンドを実行してください。
$ php artisan tinker
tinker を使うと、コマンドラインからアプリケーションの機能を確かめることができます。Web アプリなので本来であれば画面を通してしか確認できない機能をコマンドラインで確かめられるのが便利なポイントです。
さて上記のコマンドを実行すると、以下の出力が返ってきます。>>>
はユーザーからの入力を待ち受けているという意味です。
Psy Shell v0.9.9 (PHP 7.2.10 — cli) by Justin Hileman
>>>
フォルダクラスで、ID に合致するデータを取得する find
メソッドを実行してみましょう。
>>> $folder = \App\Folder::find(1);
このように、find
の結果が出力されます。
=> App\Folder {#2899
id: 1,
title: "プライベート",
created_at: "2018-11-25 13:35:48",
updated_at: "2018-11-25 13:35:48",
}
ではタスクのデータを確認しましょう。
>>> \App\Task::where('folder_id', $folder->id)->get();
このあとのコントローラーの項でも説明しますが、取得条件を指定するメソッド where
を使って、フォルダIDカラムの値が先ほど取得したフォルダのID(=1)に合致するタスクを取得しています。
先ほどシーダーで挿入したデータが取得できていますね!
=> Illuminate\Database\Eloquent\Collection {#2900
all: [
App\Task {#2901
id: 1,
folder_id: 1,
title: "サンプルタスク 1",
due_date: "2018-11-26",
status: 1,
created_at: "2018-11-25 16:04:59",
updated_at: "2018-11-25 16:04:59",
},
App\Task {#2905
id: 2,
folder_id: 1,
title: "サンプルタスク 2",
due_date: "2018-11-27",
status: 2,
created_at: "2018-11-25 16:05:00",
updated_at: "2018-11-25 16:05:00",
},
App\Task {#2906
id: 3,
folder_id: 1,
title: "サンプルタスク 3",
due_date: "2018-11-28",
status: 3,
created_at: "2018-11-25 16:05:00",
updated_at: "2018-11-25 16:05:00",
},
],
}
コントローラー
ではコントローラーを書いていきましょう。
まず TaskController.php
の冒頭で Task
モデルを読み込みます。
use App\Folder;
use App\Task; // ★ 追加
use Illuminate\Http\Request;
次に index
メソッドを以下のように編集します。
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,
]);
}
find メソッド
まず find
メソッドは、プライマリキーのカラムを条件として一行分のデータを取得してきます。
Folder::find(1);
上の例でいうと、フォルダテーブルから ID カラム(プライマリキー)が 1 である行のデータを検索して返します。
クエリビルダ
where メソッド
次に where
メソッドに注目してください。先ほど少し触れましたが、Laravel が提供するクエリビルダの機能を使っています。クエリとは、SQL クエリのことです。ビルダとは構築者という意味ですね。クエリビルダの機能によって SQL を書かなくても PHP 風な記述でデータ操作を表現できます。SQL は裏側で生成されデータベースに発行されます。
Tasks::where('folder_id', $current_folder->id)->get();
where
メソッドはデータの取得条件を表し、SQL の WHERE 句にあたります。第一引数がカラム名で第二引数が比較する値です。ただ厳密にいうと、上記の記述は以下の記述の省略形です。
Tasks::where('folder_id', '=', $current_folder->id)->get();
引数を三つ与えると、イコール以外の比較も可能だということです。
注意していただきたいのは最後の get
メソッドです。この get
メソッドで構築された SQL をデータベースに発行して結果を取得しています。ですので、
Tasks::where('folder_id', $current_folder->id);
ここまでではまだ SQL 作っている段階なので値は取れません。get
メソッドを忘れて「値が取れないぞ??」となりがちなので気をつけましょう。
(少し突っ込んでいうと where
からは QueryBuilder クラスのインスタンスが返ってきます。QueryBuilder の get
メソッドを呼ぶことで結果の詰まった Collection クラスのインスタンスが返ってきます。)
クエリビルダには他にも SQL を表現するたくさんの機能があります。詳しくはマニュアル( 公式 / 日本語)を参照してください。
クエリビルダで SQL を書かなくてよくなると言ってもこの機能は SQL の代替ではありません。あくまで SQL を組み立てる機能ですので、やはり SQL の知識は必要になります。
またクエリビルダの利点は SQL を書かないことではなく(SQL を書かないのが良いとか悪いという話ではない)、SQL を PHP コードによって表現することによって条件の部品化や使い回しが効いてプログラムの生産性や可読性が向上することにあります。
tinker で SQL を確認する
またまた tinker を使って、今度はクエリビルダでどんな SQL が作られているのか覗いてみましょう。get
メソッドの代わりに toSql
メソッドを呼び出すと SQL を発行するのではなく、作った SQL を文字列で出力します。
$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.10 — cli) by Justin Hileman
>>> \App\Task::where('folder_id', 1)->toSql();
=> "select * from "tasks" where "folder_id" = ?"
クエリビルダによって SQL が作成されていることが確認できますね。
テンプレート
さて今度はテンプレートを書きましょう。前回のタスク一覧ページのテンプレートファイル resources/views/tasks/index.blade.php
で <!-- ここにタスクが表示される -->
とコメントが書かれていた箇所に以下のコードを記述してください。
<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>
コントローラーから渡された tasks
をループして一つ一つのタスクデータを出力しています。
ここまでできたら一度ブラウザで確認しましょう。/folders/1/tasks にアクセスしてみてください。
...いかがでしょうか?タスクは3つ表示されているはずです。ただいくつか課題があります。
- 状態が数値で表示されている。
1 →「未着手」、2 →「着手中」、3 →「完了」と表示したい。 - 状態ラベルを色分けできていない。
「未着手」は赤、「着手中」は緑、「完了」は灰色にしたい。 - 日付の形式はハイフン区切りではなくスラッシュ区切りにしたい。
次の項ではモデルクラスのアクセサという機能を使ってこれらの課題を改善します。
Task モデルにアクセサを追加する
アクセサとは
アクセサとは、モデルクラスが本来持つデータを加工した値を、さもモデルクラスのプロパティであるかのように参照できる Laravel の機能です。
まずモデルクラスに get○○○Attribute
という決まったフォーマットのメソッドを用意します。以下の例では性別の文字列表現を取得するための getGenderTextAttribute
メソッドを記述しました。persons テーブルに gender(性別)カラムがあるイメージです。
class Person extends Model
{
public function getGenderTextAttribute()
{
switch($this->attributes['gender']) {
case 1:
return 'male';
case 2;
return 'female';
default:
return '??';
}
}
}
まず $this->attributes['gender']
に注目してください。これで gender カラムの値を取得しています。実は Laravel ではモデルクラスの属性データ(テーブルで言うところの各カラムの値)はそれぞれがクラスのプロパティで管理されているのではなく、$attributes
という一つのプロパティで配列として管理されています。
前回の章でも $folder->title
とクラスのプロパティのようにタイトルを取得していましたが、これは PHP のマジックメソッド __get()
を利用しています。本当は Folder クラスには title というプロパティは存在しません(定義しなかったですよね)。モデルクラスは、存在しないプロパティを参照されると $attributes
からプロパティ名と一致するキーの値が返すように実装されているのです。
さて getGenderTextAttribute
メソッドでは gender カラムの値に応じて文字列を返していますね。そしてこれをどう使うかというと...
// コントローラーやテンプレートなどで...
$person->gender_text; //=> 'male'
上記のように、ここでもクラスのプロパティかのように getGenderTextAttribute
メソッドの出力値を参照できています。get○○○Attribute
の ○○○
の部分が(擬似的な)プロパティになっていますね。ただしアクセサメソッドはキャメルケース(文字の区切りが大文字)ですがプロパティとして参照するときはスネークケース(文字の区切りがアンダースコア)になります。
ルールが分かってしまえば難しくはないですね。「アクセサを使わずに普通にモデルにメソッドを用意して普通にそれを呼び出しても同じでは?」と考える方もいらっしゃるかもしれません。まぁその通りです。ただアクセサを利用したほうがちょっとコードがキレイになります。公式サイトのトップページに "Love beautiful code? We do too." とあるように、コードをキレイに見せることは Laravel のコンセプトの一つであるようです。
アクセサの使いどころは、モデルが持つ属性データ(テーブルで言うところの各カラムの値)を加工した値を取得したいときです。実際に今回の ToDo アプリでも使ってみましょう。
状態の名前を表示する
まずは状態を数値ではなく文字列で表示する機能です。
モデル
タスクモデルに定数 STATUS
と getStatusLabelAttribute
メソッドを追加してください。
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'];
}
}
$this->attributes['status']
で状態カラムの値を取得している以外は、getStatusLabelAttribute
メソッドの中身は普通の PHP です。STATUS
配列から状態値をキーに文字列表現を探して返しています。
テンプレート
テンプレートは、{{ $task->status }}
と記述していた箇所を書き換えてください。
<span class="label">{{ $task->status_label }}</span>
これでもう一度ブラウザで確認してみましょう。状態が文字列で表示されたのではないでしょうか。
状態を色分けする
次に状態のラベルを色分けする機能を追加します。
状態の文字列表現を表示したのとほとんど同じ要領です。
モデル
STATUS
配列に class
を追加して、getStatusClassAttribute
メソッドを追記します。
/**
* 状態定義
*/
const STATUS = [
1 => [ 'label' => '未着手', 'class' => 'label-danger' ],
2 => [ 'label' => '着手中', 'class' => 'label-info' ],
3 => [ 'label' => '完了', 'class' => '' ],
];
/**
* 状態を表すHTMLクラス
* @return string
*/
public function getStatusClassAttribute()
{
// 状態値
$status = $this->attributes['status'];
// 定義されていなければ空文字を返す
if (!isset(self::STATUS[$status])) {
return '';
}
return self::STATUS[$status]['class'];
}
中身は getStatusLabelAttribute
メソッドとほとんど一緒ですね。label
の代わりに class
を返しています。
テンプレート
テンプレートでは、ラベルの部分のクラス属性に $task->status_class
を追加します。
<span class="label {{ $task->status_class }}">{{ $task->status_label }}</span>
これで状態に応じた HTML クラスが出力され、結果的に CSS でそれぞれの色が描画されます。
日付の表示形式を変更する
最後に日付の表示形式を変更します。データベースに入ったままだと 2019-01-01
というハイフン区切りの形式なので、より一般的な 2019/01/01
というスラッシュ区切りの形式で表示しましょう。
モデル
モデルクラスでは、getFormattedDueDateAttribute
メソッドを追加します。
// この行を追加
use Carbon\Carbon;
class Task extends Model
{
/* 中略 */
/**
* 整形した期限日
* @return string
*/
public function getFormattedDueDateAttribute()
{
return Carbon::createFromFormat('Y-m-d', $this->attributes['due_date'])
->format('Y/m/d');
}
}
Carbon ライブラリを使って期限日の値の形式を変更して返却しています。
テンプレート
もうなんとなく分かってきたかもしれませんが、テンプレートでは $task->due_date
だった箇所を書き換えます。
<td>{{ $task->formatted_due_date }}</td>
ブラウザで表示形式が変わっているか確認しましょう。
タスク一覧まできちんと表示できましたので、ここまでで見た目の上ではこの章で作りたい段階は達成しています。ただ、内部のコードはもう少しキレイにすることができます。そこでもう一つだけ Laravel の強力な機能、「テーブル間の関連性をモデルクラスで表現する方法」を紹介してこの章を終わりたいと思います。
モデルクラスにおけるリレーション
タスクコントローラーではタスクの一覧を以下のコードで取得しました。
$tasks = Tasks::where('folder_id', $current_folder->id)->get();
ここで紹介する機能を使うとこのように書き直すことができます。
$tasks = $current_folder->tasks()->get();
特定のフォルダに紐づくタスクの一覧を取得する処理が、ほとんど左から英語で読んだ通りの直感的な表現で記述されています。では実装方法を説明していきます。
hasMany
フォルダクラスを編集します。以下の通り tasks
メソッドを追加してください。
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Folder extends Model
{
public function tasks()
{
return $this->hasMany('App\Task');
}
}
tasks
メソッドの中で hasMany
メソッドを呼び出していますが、これにより Laravel はフォルダテーブルとタスクテーブルの関連性(リレーション)を「たどって」、フォルダクラスのインスタンスから紐づくタスククラスのリストを取得します。
hasMany('App\Task')
は「タスクをたくさん持っている」と読めますね。文字通りフォルダテーブルとタスクテーブルの一対多の関連性を利用するように Laravel に指示しているわけです。
ちなみに上記のhasMany
メソッドは引数を省略しています。省略せずに書くと以下の通りです。
$this->hasMany('App\Task', 'folder_id', 'id');
第一引数が関連するモデル名(名前空間も含む)、第二引数が関連するテーブルが持つ外部キーカラム名、第三引数はモデルに hasMany
が定義されている側のテーブルが持つ、外部キーに紐づけられたカラムの名前です。ただ第二引数と第三引数は今回のようにある決まりに沿っている場合は省略できます。その決まりとは、第二引数が テーブル名単数形_id
で第三引数が id
であることです。
つまり、Laravel はフォルダテーブルとタスクテーブルを結びつけるためのカラムまで知っているのでフォルダさえわかればタスクも分かってしまうのです。
モデルで定義できるリレーションの機能について詳しくはマニュアル( 公式 / 日本語)も参照してください。
ちなみに tinker で SQL を確認すると以下のような出力が得られます。
>>> $folder = \App\Folder::find(1);
>>> $folder->tasks()->toSql();
=> "select * from "tasks" where "tasks"."folder_id" = ? and "tasks"."folder_id" is not null"
folder_id カラムを条件にタスクデータを取得する SQL が作られていますね。
index メソッドを編集する
ではリレーションを使うようにタスクコントローラーの index メソッドを編集しましょう。
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,
]);
}
★印をつけた行を書き換えました。
できたら見た目が変わっていないことをブラウザで確認しましょう。
第4章はこれでおしまいです。
ここまでのソースコードはリポジトリ(chapter04 ブランチ)を参照してください。
次の章ではフォルダを作成する機能を実装します。
連載記事
- 入門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にデプロイする