2018.12.10

入門Laravelチュートリアル (3) ToDoアプリのフォルダ一覧表示機能を作る


この連載記事では、Laravel を使用した Web アプリケーションの開発方法を紹介します。実際に(お決まりの?)ToDo アプリを開発する手順を通して Web 開発のエッセンスを学んでいただけるように書いていきます。取り扱う Laravel のバージョンは現時点で最新の 5.7 です。

第3章では、フォルダの一覧表示機能を作っていきます。

まずは環境構築ができていて Laravel の初期画面が表示されている前提で進めます。環境構築については下記の記事を参照してください。

ルーティング

ルーティングの設定

さて、まずはルーティングの設定から始めます。ここで作成するページは、タスク一覧ページです。フォルダの一覧表示と言ってもタスク一覧の左側のパネルがフォルダ一覧なのでしたね。この章ではまずこの左側のパネルだけを作ります。

トップページ

URL 設計を見返していただくと、タスク一覧ページの設計は以下の通りです。

URL メソッド 処理
/folders/{フォルダID}/tasks GET タスク一覧ページを表示する。

この設計を踏まえ、routes/web.php に下記の記述をしてください。あらかじめ記載されているサンプルのルーティングは削除して構いません。

web.php
<?php

Route::get('/folders/{id}/tasks', 'TaskController@index')->name('tasks.index');

Route クラスがルーティングの設定をしています。コードの意味はほとんど左から読んだままで、get/folders/{id}/tasks にリクエストが来たら TaskController コントローラーの index メソッドを呼びだす、という記述です。また、最後にこのルートに名前をつけています。アプリケーションの中で URL を参照する際にはこの名前を使います。

ポイントは URL 中の {id} でしょう。タスク一覧ページはフォルダごとのタスクを表示するので、/folders/123/tasks/folders/999/tasks など、どのフォルダのタスクを表示したいかによって URL が変わります。その変わる部分を波括弧の箇所で表現します。波括弧の間の名前(今回は「id」)はどんな値でも構いません。

次にその TaskController クラスおよび index メソッドを作成しましょう。

コントローラークラス

コントローラークラスはコマンドラインから雛形を作成します。

$ php artisan make:controller TaskController

これで app/Http/Controllers ディレクトリに TaskController.php が作成されたはずです。以下の通り index メソッドを追加してください。

TaskController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TaskController extends Controller
{
    public function index()
    {
        return "Hello world";
    }
}

とりあえず "Hello world" と文字列を返却しています。試しに http://todos.test/folders/1/tasks にアクセスしてみましょう。

ちなみに URL のドメインは各自の設定値に読み替えてください。私は Valet を使って todos というディレクトリで開発しているので todos.test というドメインになっています。

さていかがでしょうか。ブラウザに「Hello world」と表示されていれば OK です。ひとまずリクエストとコントローラーメソッドを紐づけられたことが確認できましたね。

メソッドのロジックはあとで書くことにして、データベース周りを先に作っていきます。

マイグレーションとモデルクラス

データベースの接続設定

まずは接続設定を行います。todo というデータベースを作成している前提です。環境構築に Homestead を使ったか Valet を使ったかで DB_USERNAMEDB_PASSWORD が異なります。

Homestead

.env
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=todo
DB_USERNAME=homestead
DB_PASSWORD=secret

Valet

.env
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=todo
DB_USERNAME=postgres
DB_PASSWORD=postgres

マイグレーションファイルの作成

ではマイグレーションファイルを作成します。マイグレーションとは何かについてはこちらの記事にまとめていますので先に進む前に読んでおいてください。

マイグレーションファイルもコマンドラインから作成できます。

$ php artisan make:migration create_folders_table --create=folders

database/migrations ディレクトリに 2018_11_25_121216_create_folders_table.php というような名前のマイグレーションファイルの雛形が作成されたはずです。2018_11_25_121216 の部分はファイルが作成された年月日時分秒ですので、各々で異なります。

フォルダーテーブルのテーブル定義をおさらいしましょう。

カラム論理名 カラム物理名 型の意味
ID id SERIAL 連番(自動採番)
タイトル title VARCHAR(20) 20文字までの文字列
作成日 created_at TIMESTAMP 日付と時刻
更新日 updated_at TIMESTAMP 日付と時刻

このテーブル定義を Laravel マイグレーションの PHP コードで表現すると以下のコードになります。

2018_11_25_121216_create_folders_table.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateFoldersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('folders', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title', 20);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('folders');
    }
}

編集しているのは up メソッドですね。テーブル名は folders としました。格納したい物の名前の複数形にするのが一般的です。

$table->increments('id');
$table->string('title', 20);
$table->timestamps();

この部分でカラムの作成を指示しています。自動採番のカラムは increments メソッドで、作成日と更新日はまとめて timestamps メソッドで作成されます。

マイグレーションの実行

マイグレーションの実行もコマンドラインから行います。

$ php artisan migrate

Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table
Migrating: 2018_11_25_121216_create_folders_table
Migrated:  2018_11_25_121216_create_folders_table

create_folders_table マイグレーションが実行されました。create_users_tablecreate_password_resets_table マイグレーションも一緒に実行されていますが、こちらは Laravel で最初から用意されている認証機能のためのマイグレーションです。あとで使うので今は気にしなくて OK です。

データベースクライアントツールからデータベースの中を確認してみましょう。folders テーブルが作成されているでしょうか。

Object-Relational マッピングとは

次にモデルクラスを作成しますが、Object-Relational マッピング

Object-Relational マッピング(ORM)とは、アプリケーションからデータベースの操作をしやすくするためのプログラミング手法です。

オブジェクト指向モデルであるアプリケーション(例:PHP)とリレーショナルデータベース(例:PostgreSQL)では、そもそもデータの持ち方が違います。

オブジェクト指向では、クラスがあってプロパティにデータを持ちます。例えば以下のように。

class User
{
    private $email;
    private $password;
    // 中略
}

一方リレーショナルモデルでは行と列、つまりテーブルでデータを持ちます。

users テーブル
| email           | password |
|-----------------|----------|
| sample@mail.com | test1234 |

例えば users テーブルから取得したデータを、毎回 User クラスに詰めなおすのは面倒ですね。User クラスのデータを user テーブルに挿入したい場合も、いちいちプロパティからデータを引っ張ってきて INSERT 文を作るのは面倒です。そこであらかじめアプリ側のデータとテーブル側のデータの紐づけを定義しておこうというのが ORM です。

言葉だけでは理解が難しいと思いますので、習うより慣れよ、コードを書きながら ORM の働きを体得していってください。

モデルクラス

モデルクラスは PHP 側でデータの入れ物になるクラスです。基本的にはモデルクラスひとつがテーブルひとつに対応するように作ります。

$ php artisan make:model Folder

app ディレクトリに Folder モデルが作成されているでしょう。

Folder.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Folder extends Model
{
    //
}

Folder モデルの記述はこれだけで OK です。クラスの中身は何も書いていませんが、継承元である Model クラスで様々な設定を読み取ってくれます。

例えばこのモデルクラスがどのテーブルに対応しているかはクラス名から自動的に推定されます。つまりモデルクラスのクラス名の複数形のテーブルが対応していると解釈されるのです。今回であれば folders テーブルですね。もちろんこのデフォルトの推定に当てはまらない場合は追加で設定を書けばいいのですが、今回はこの仕組みに合わせてあるのでその必要はありません。

テストデータを挿入する

ここまででデータを扱う準備ができたのですが、テストデータが入っていた方がコントローラーを書きやすいので、データを挿入します。データベースクライアントから直接 SQL を実行してデータを挿入してもよいですが、シーダー(Seeder)を用いた方法を紹介しておきます。

シーダーはテストデータを入れるための仕組みです。以下のコマンドで作成します。

$ php artisan make:seeder FoldersTableSeeder

database/seeds ディレクトリに FoldersTableSeeder.php が出来ているので、以下の通り編集してください。

FoldersTableSeeder.php
<?php

use Carbon\Carbon;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

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(),
            ]);
        }
    }
}

run メソッドの中にデータを挿入するコードを記述します。ここでは「プライベート」「仕事」「旅行」という三つのフォルダを作ります。作成日と更新日には Carbon というライブラリを使って現在日時を入れています。

コマンドラインからシーダーを実行します。

$ composer dump-autoload
$ php artisan db:seed --class=FoldersTableSeeder

最初の composer コマンドは、作成したシーダークラスをアプリケーションに認識させるためのものだと思ってください。db:seed コマンドで「Database seeding completed successfully.」と返ってきたら成功です。データベースクライアントでテーブルの中身を確認してみましょう。

folders テーブル

データが挿入されています。

コントローラー

データまで揃ったので、コントローラーにロジックを記述していきます。

TaskController.php
<?php

namespace App\Http\Controllers;

use App\Folder;
use Illuminate\Http\Request;

class TaskController extends Controller
{
    public function index()
    {
        $folders = Folder::all();

        return view('tasks/index', [
            'folders' => $folders,
        ]);
    }
}

まず Folder モデルの all クラスメソッドですべてのフォルダデータをデータベースから取得しています。Laravel の ORM は強力なので、SQL をまったく書かずにデータを取得できています。

次に view 関数でテンプレートに取得したデータを渡した結果を返却しています。view 関数の第一引数がテンプレートファイル名(後ほど作成します)で第二引数がテンプレートに渡すデータです。第二引数には配列を渡しますが、キーがテンプレート側で参照する際の変数名となります。

このように view 関数の結果をコントローラーメソッドから返却すると、テンプレートをレンダリングした結果の HTML がフレームワークによってブラウザにレスポンスされます。

続いてテンプレートを書いていきましょう。

テンプレートの作成

テンプレートとは

テンプレートとはアプリケーションがレスポンスする HTML の雛形で、制御構文(if や foreach など)や変数の展開を記述することができます。ページの枠組みだけ用意して、URL によって変わる箇所だけが穴埋めになっているイメージです。雛形が同じでもデータを変えることで別のページを作り出せる仕組みですね。

テンプレートエンジンとはテンプレートを HTML に変換するライブラリです。テンプレートエンジンが変わればテンプレートの書き方も変わってきます。Laravel のテンプレートエンジンは Blade という名前です。

Blade テンプレートエンジン

テンプレートに関しては作成するための artisan コマンドは用意されていないので、手動でファイルを作成します。resources/views ディレクトリに tasks ディレクトリを作成してください。このディレクトリをタスク関連のテンプレート置き場にします。

$ mkdir resources/views/tasks

tasks ディレクトリに index.blade.php というファイルを作成しましょう。.blade.php という拡張子はテンプレートファイルとして認識されるために決まっています。

$ touch resources/views/tasks/index.blade.php

index.blade.php は以下の通り記述してください。

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>

28行目からがポイントですね。

@foreach($folders as $folder)
  <a href="{{ route('tasks.index', ['id' => $folder->id]) }}" class="list-group-item">
    {{ $folder->title }}
  </a>
@endforeach

テンプレートの中では PHP のように foreach が使えています。ただしテンプレートではアットマーク @ を付けるので注意しましょう。

そして foreach の中でコントローラーから渡されたデータ $folders を参照しています。

[
    'folders' => $folders,
]

キーと値が同じなのでややこしいですが、あくまでテンプレート側ではキー名が変数名になることを覚えておいてください。

変数の値の展開は、{{ $data }} のように波括弧二つで実現します。ここでは二箇所で使われていますね。

まずはタイトルの表示 {{ $folder->title }} です。$folders にすべてのフォルダのデータが入っているので、foreach でループした一つのアイテムである $folder はフォルダテーブルの一行に相当すると考えられます。カラムの値は ->title と、プロパティのように参照することができます。

もう一つはアンカーリンクの href 属性です。

route('tasks.index', ['id' => $folder->id])

Laravel が提供している route 関数の結果を href の値として展開しています。route 関数はルーティングの設定から URL を作り出す関数です。

Route::get('/folders/{id}/tasks', 'TaskController@index')->name('tasks.index');

route 関数の第一引数はルート名です。上記の通り、ルーティングの際に get メソッドに続けて呼び出した name メソッドの引数がそのルートの名前です。route 関数の第二引数として渡している配列は、ルート URL のうち変数になっている部分(ここでは {id})に実際の値を埋める役割です。

なおテンプレートについて詳しくはマニュアル 🇺🇸 公式 / 🇯🇵 日本語を参照してください。

スタイルシート

画面から確認する前に以下のスタイルシートを public/css ディレクトリに作成してください。今回は CSS フレームワーク Bootstrap と Bootstrap にさらにスタイルを足した Bootflat を使って見た目を整えています。

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;
}

ここまでできたら一度ブラウザで確認してみましょう。http://todos.test/folders/1/tasks にアクセスしてください(ドメインは各自の設定によって読み替えてください)。

いかがでしょう?左側のパネルにシーダーで挿入した三つのフォルダが表示されているはずです。またフォルダ名をクリックしてみてください。それぞれの ID に合わせて /folders/1/tasks /folders/2/tasks /folders/3/tasks というリンクが出来ているでしょう。

フォルダ名を選択表示にする

最後にもう一つ機能を追加します。フォルダ名をクリックするとそれぞれのフォルダの URL に遷移することができますが、どのフォルダが選ばれているのかは画面上では分かりません。

そこで、上記のようにアクセスされているフォルダ名だけ選択表示(水色背景)にします。

実装方法としては、URL の変数部分、つまり '/folders/{id}/tasks'{id} の値をコントローラーで受け取ってテンプレートに渡します。テンプレートではループの中でコントローラーから渡された {id} の値に合致する場合だけ CSS で水色背景を表現する HTML クラスを出力します。

コントローラー

ではコントローラーから見ていきましょう。index メソッドを以下のように書き換えてください。

TaskController.php
public function index(int $id)
{
    $folders = Folder::all();

    return view('tasks/index', [
        'folders' => $folders,
        'current_folder_id' => $id,
    ]);
}

まずは URL の変数部分をコントローラーで受け取る方法ですが、以下のようにコントローラーメソッドの引数として受け取ります。この時の引数名はルーティングで定義した波括弧内の値と合致していなければいけません。

public function index(int $id)

今回は {id} と定義したので $id で受け取っています。仮に {sample_value} であれば $sample_value で受け取る必要があるということです。

次に受け取った値をテンプレートに渡しています。

'current_folder_id' => $id,

ただ id では分かりにくいので current_folder_id という名前で参照するように記述しました。

テンプレート

テンプレートは resources/views/tasks/index.blade.php の29行目のリンクを編集します。

index.blade.php
<a
    href="{{ route('tasks.index', ['id' => $folder->id]) }}"
    class="list-group-item {{ $current_folder_id === $folder->id ? 'active' : '' }}"
>

見やすいように折り返していますがコードを足したのは class 属性の部分です。三項演算子を用い、ループしているフォルダデータのうち $current_folder_id つまり閲覧されているフォルダの ID と ID 値が合致する場合のみ 'active' という HTML クラスを出力しています。

ここまでできたら再度ブラウザで確認してみましょう。
フォルダ名をクリックすると選択表示になっていると思います。

最後のおまけ(ログの出力先)

アプリケーションログの出力先を紹介しておきます。

storage/logs/laravel.log

開発中のエラーは画面にも内容が出力されますが、こちらのファイルにも出力されます。

🐤 🐤 🐤

第3章はこれでおしまいです。
ここまでのソースコードはリポジトリ(chapter03 ブランチ)を参照してください。

次の章では同じタスク一覧画面で、タスクの一覧を表示させます。この章と要領は同じですが、ORM でのリレーションの表現が登場します。

連載記事


<!-- Font Awesome Free 5.0.13 by @fontawesome - https://fontawesome.com --><!-- License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) -->