Chapter 04

入門04 - [ToDoアプリのタスク一覧表示機能を作る]

eguchi244
eguchi244
2023.12.29に更新

ToDoアプリのタスク一覧表示機能を作る

第4章では、ToDoアプリのタスク一覧表示機能を作成していきます。

今回の目標物

ここで登場するのは、「右側のパネルのタスク一覧」です。

タスク一覧ページの右パネルがタスク一覧パネルをになっています。
今回はこちらを作成していきます。

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

Migrationファイルの不整合の解消

先に「Migrationファイルの不整合」が起きた時の解消法をお伝えしておきます。

例えば、Gitなどを使用していると、Migrationテーブルに載っているMigrationファイル名(前の章で自分で作成したもの)の日付部分が、実物のMigrationファイル(GitからCloneしてきたもの)と異なってしまうことがあります。

不整合が発生する場合に、Migrateを実行すると実行済みの内容が同じものまで実行されてしまいます。当然、既にテーブルなどが存在するのでエラーが発生します。

この不整合を解消するためには Migration と Seeder の再実行を行えばよいのです。
「Migrationファイルの不整合」の場合には下記の手順を実施してみてください。

  1. DatabaseSeeder.phpを編集する
[database/seeders/DatabaseSeeder.php]
class DatabaseSeeder extends Seeder
{
    public function run()
    {
        /* call seeder list */
        $this->call(FoldersTableSeeder::class);
        // 他のシーダーも必要に応じて追加
    }
}
  1. Migrationをやり直して(全てのレコードが破棄される)全てのシーダーを挿入する
Terminal
root~# php artisan migrate:fresh --seed
  1. 全てのシーダーのみ動かしたい場合は下記を実行してください。
Terminal
root~# php artisan db:seed
  1. 個別のシーダーのみ動かしたい場合は下記を実行してください。
Terminal
root~# php artisan db:seed --class=<seederのクラス名>

下記の記事が非常に参考になりますのでご案内しておきます。
https://blog-and-destroy.com/28984

タスクテーブルのテーブル定義を確認する

マイグレーションファイルを作成する前にテーブル定義を確認しておきましょう。

タスクテーブル

カラム論理名 カラム物理名 型の意味
ID id INTEGER 連番(自動採番)
フォルダID folder_id INTEGER 整数
タイトル title VARCHAR(100) 100文字までの文字列
状態 status INTEGER 数値
期限日 due_date DATE 日付
作成日 created_at TIMESTAMP 日付と時刻
更新日 updated_at TIMESTAMP 日付と時刻

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

それではマイグレーションファイルの作成と実行をしていきましょう。

まずは、マイグレーションファイルを作成します。

Terminal
# tasksテーブルのマイグレーションファイルを作成する
root~# php artisan make:migration create_tasks_table --create=tasks

database/migrations ディレクトリに新しくxxxx_xx_xx_xxxx_create_tasks_table.php というような名前のマイグレーションファイルの雛形が作成されたはずです。

次に確認したフォルダーテーブルの定義に沿ってマイグレーションファイルを編集します。

Y_m_d_His_create_tasks_table.php
<?php

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

class CreateTasksTable extends Migration
{
    // テーブル名を明示的に指定する
    protected $table = 'tasks';

    /**
     * Run the migrations.
     * tasksテーブルと各列を作成する
     *
     * @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');
        });
    }
    [中略]
}
Y_m_d_His_create_tasks_table.php の解説

タスクテーブルの定義に沿って下記のようにそれぞれ書き変えています。
コードの意味はコメントで説明しているので確認してみてください。

Y_m_d_His_create_tasks_table.php
<?php

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

class CreateTasksTable extends Migration
{
    // テーブル名を明示的に指定する
    protected $table = 'tasks';

    /**
     * Run the migrations.
     * tasksテーブルと各列を作成する
     *
     * @return void
     */
    public function up()
    {
        /**
         * tasks Table create
         * column1 -> カラム名:id, 型:INTEGER, オプション:AI
         * column2 -> カラム名:folder_id, 型:INTEGER
         * column3 -> カラム名:title, 型:VARCHAR(100)
         * column4 -> カラム名:due_date, 型:DATE
         * column5 -> カラム名:status, 型:INTEGER, デフォルト:1(未着手)
         * column6 -> カラム名:created_at, 型:TIMESTAMP
         * column7 -> カラム名:updated_at, 型:TIMESTAMP
         */
        Schema::create('tasks', function (Blueprint $table) {
            // UNSIGNED INTEGER(主キー)の同等の列を自動インクリメントする
            $table->increments('id');
            // INTEGER相当の列をUNSIGNED(MySQL)として追加する。
            $table->integer('folder_id')->unsigned();
            // オプションの長さのVARCHAR相当の列を追加する
            $table->string('title', 100);
            // DATEに相当する列を追加する
            $table->date('due_date');
            // INTEGER相当の列を追加してデフォルトの値に「1」をセットする
            $table->integer('status')->default(1);
            // created_atとupdated_atのTIMESTAMPに相当する列を追加する
            $table->timestamps();

            /* 外部キーを'folder_id'列に設定する(実在するIDの値以外はDBに入らないようにする) */
            // foreign():外部キーを設定する列を指定する
            // ->references():参照する列名を指定する
            // ->on():参照するテーブル名を指定する
            $table->foreign('folder_id')->references('id')->on('folders');
        });
    }

    /**
     * Reverse the migrations.
     * tasksテーブルを削除する
     *
     * @return void
     */
    public function down()
    {
        // tasksテーブルを削除する
        Schema::dropIfExists('tasks');
    }
}

それではマイグレーションを実行しましょう。

Terminal
root~# php artisan migrate

ブラウザで http://localhost:8080 にアクセスしてテーブルを確認してください。

tasksテーブルが作成されていればOKです。

モデルクラスの作成

次に、タスクテーブルに対応するモデルクラスを作成します。

Terminal
# モデルを作成する
root~# php artisan make:model Task

下記のように タスクモデル が作成されたはずです。
前回と同様にデフォルトのままでOKです。

app/Models/Task.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
    use HasFactory;
}

②テストデータの作成と確認

シーダーを作成する

tasksテーブルのテストデータを挿入するためにシーダーを作成します。

Terminal
# シーダーを作成する
root~# php artisan make:seeder TasksTableSeeder

database/seeds/TasksTableSeeder.php を以下の通り記述してください。

TasksTableSeeder.php
<?php

namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

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

class TasksTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     * tasksTable用テストデータ
     *
     * @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(),
            ]);
        }
    }
}
TasksTableSeeder.php の解説

run メソッドの中にデータを挿入するコードを記述します。
それぞれのコードの解説はコメントに記述しているので確認してください。

TasksTableSeeder.php
<?php

namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

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

class TasksTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     * tasksTable用テストデータ
     *
     * @return void
     */
    public function run()
    {
        // ID = 1 のフォルダに対して3つのタスクを登録(挿入)する
        foreach (range(1, 3) as $num) {
            // tasksテーブルにアクセスして行を挿入する
            DB::table('tasks')->insert([
                // folder_idに 1 を代入する
                'folder_id' => 1,
                // titleに "サンプルタスク {$num}" を代入する
                'title' => "サンプルタスク {$num}",
                // status に $num を代入する
                'status' => $num,
                // 期限日に 現在日時 + addDay($num) の期日を設定して代入する
                // addDay():日単位での加算減算をする関数
                'due_date' => Carbon::now()->addDay($num),
                // 現在の日時を取得してcreated_atに作成日として代入する
                'created_at' => Carbon::now(),
                // 現在の日時を取得してupdated_atに更新日として代入する
                'updated_at' => Carbon::now(),
            ]);
        }
    }
}

Seederを実行する前にすべてのシーダーを実行できるように DatabaseSeeder.php に下記を追加しておきましょう。

DatabaseSeeder.php
// runメソッド内に追加する
$this->call(TasksTableSeeder::class);

それでは Seeder を実行しましょう。

Terminal
# シーダーを実行する
root~# php artisan db:seed --class=TasksTableSeeder

上記のコマンドがうまくいかない場合は composer dump-autoload コマンドを打ってから実行し直してください。

ブラウザで http://localhost:8080 にアクセスしてテーブルを確認してください。

上記のようにデーターが挿入されていればOKです。

tinker を使ってみよう

ここでも前回に引き続き tinker を使ってみましょう。
ここではテストデータの確認を tinker で行っていきます。
下記の手順を実施してください。

  1. tinkerを起動する
Terminal
root~# php artisan tinker
  1. フォルダークラスでの ID に合致するデータを取得してみる
Terminal
> $folder = \App\Models\Folder::find(1);
= App\Models\Folder {#6565
    id: 1,
    title: "プライベート",
    created_at: "2023-09-11 05:25:51",
    updated_at: "2023-09-11 05:25:51",
  }

ここでは、フォルダーテーブルの 1行目を find関数 を使って取得しています。
1行目は id1titleプライベート でしたので正しく取得できていることがわかります。なお、find関数の引数を違う行数番号に変えると異なるレコードを取得できます。

  1. タスクの データ も確認してみましょう。

ここでは、取得条件を指定するメソッド where を使って、フォルダIDカラムの値が先ほど取得したフォルダのID(=1)に合致するタスクを取得しています。

そのため、$folder = \App\Models\Folder::find(1); を実施した直後に下記を実行しないと値を取得できない点に注意してください。

Terminal
> \App\Models\Task::where('folder_id', $folder->id)->get();
= Illuminate\Database\Eloquent\Collection {#6971
    all: [
      App\Models\Task {#6234
        id: 1,
        folder_id: 1,
        title: "サンプルタスク 1",
        due_date: "2023-09-12",
        status: 1,
        created_at: "2023-09-11 06:35:43",
        updated_at: "2023-09-11 06:35:43",
      },
      App\Models\Task {#7187
        id: 2,
        folder_id: 1,
        title: "サンプルタスク 2",
        due_date: "2023-09-13",
        status: 2,
        created_at: "2023-09-11 06:35:43",
        updated_at: "2023-09-11 06:35:43",
      },
      App\Models\Task {#6225
        id: 3,
        folder_id: 1,
        title: "サンプルタスク 3",
        due_date: "2023-09-14",
        status: 3,
        created_at: "2023-09-11 06:35:43",
        updated_at: "2023-09-11 06:35:43",
      },
    ],
  }

# tinkerを終了する
> quit

先ほどシーダーで挿入したtasksテーブルのデータが取得できていればOKです。

③コントローラーの修正

それではコントローラーをTaskモデルに合わせて修正していきましょう。
TaskController.php を下記のように修正してください。

TaskController.php
<?php
namespace App\Http\Controllers;

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

class TaskController extends Controller
{
    /**
     *  【タスク一覧ページの表示機能】
     *
     *  GET /folders/{id}/tasks
     *  @param int $id
     *  @return \Illuminate\View\View
     */
    public function index(int $id)
    {
        $folders = Folder::all();

        $folder = Folder::find($id);

        $tasks = Task::where('folder_id', $folder->id)->get();

        return view('tasks/index', [
            'folders' => $folders,
            'folder_id' => $folder->id,
            'tasks' => $tasks
        ]);
    }
}
TaskController.php の解説

それぞれのコードの解説はコメントに記述しているので確認してください。

TaskController.php
<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Folder;
// タスククラスを名前空間でインポートする
use App\Models\Task;

class TaskController extends Controller
{
    /**
     *  【タスク一覧ページの表示機能】
     *  
     *  GET /folders/{id}/tasks
     *  @param int $id
     *  @return \Illuminate\View\View
     */
    public function index(int $id)
    {
        /* Folderモデルの全てのデータをDBから取得する */
        // all():全てのデータを取得する関数
        $folders = Folder::all();

        /* ユーザーによって選択されたフォルダを取得する */
        // find():一行分のデータを取得する関数
        $folder = Folder::find($id);

        /* ユーザーによって選択されたフォルダに紐づくタスクを取得する */
        // where(カラム名,カラムに対して比較する値):特定の条件を指定する関数 ※一致する場合の条件 `'='` を省略形で記述しています
        // get():値を取得する関数(この場合はwhere関数で生成されたSQL文を発行して値を取得する)
        $tasks = Task::where('folder_id', $folder->id)->get();

        /* DBから取得した情報をViewに渡す */
        // view('遷移先のbladeファイル名', [連想配列:渡したい変数についての情報]);
        // 連想配列:['キー(テンプレート側で参照する際の変数名)' => '渡したい変数']
        return view('tasks/index', [
            'folders' => $folders,
            'folder_id' => $folder->id,
            'tasks' => $tasks
        ]);
    }
}

それでは先ほどのコードを確認していきましょう。

findメソッド

findメソッド

まず find は、プライマリキーのカラムを条件として一行分のデータを取得してきます。上の例でいうと、アクセス直後はフォルダテーブルから ID カラム(プライマリキー)が 1 である行のデータを検索して返します。

findメソッドの例
Folder::find($id);

クエリビルダ

whereメソッド

次に where メソッドに注目してください。先ほど少し触れましたが、Laravel が提供するクエリビルダの機能を使っています。

クエリとは、SQL クエリのことです。ビルダとは構築者という意味です。クエリビルダの機能によって SQL を書かなくても PHP 風な記述でデータ操作を表現できます。

SQL は裏側で生成されデータベースに発行されます。

whereメソッドの例
Tasks::where('folder_id', $folder->id)->get();

where メソッドはデータの取得条件を表し、SQLWHERE 句にあたります。第一引数がカラム名で第二引数が比較する値です。ただ厳密にいうと、上記の記述は以下の記述の省略形です。つまり、引数を三つ与えるとイコール以外の比較も可能だということです。省略しない場合は下記のようになります。

whereメソッドの例
Tasks::where('folder_id', '=', $folder->id)->get();

注意していただきたいのは最後の get メソッドです。この get メソッドで構築された SQL をデータベースに発行して結果を取得しています。

whereメソッドの例
Tasks::where('folder_id', '=', $folder->id)->get();

そのため、get メソッドを付けてない段階では SQL 文を作っているだけなので値は取れません。SQL 文を作っただけの状態です。

get メソッドを忘れて「値が取れないぞ??」となりがちなので気をつけましょう。この辺は本家のPHPが execute しないと SQL 文が実行されないのと似ていますね。

(少し突っ込んでいうと where からは QueryBuilder クラスのインスタンスが返ってきます。QueryBuilderget メソッドを呼ぶことで結果の詰まった Collection クラスのインスタンスが返ってきます。)

クエリビルダで SQL を書かなくてよくなると言ってもこの機能は SQL の代替ではありません。あくまで SQL を組み立てる機能ですので、やはり SQL の知識は必要になります。

また、クエリビルダの利点は SQL を書かないことではなく(SQL を書かないのが良いとか悪いという話ではない)、SQL を PHP コードによって表現することによって条件の部品化や使い回しが効いてプログラムの生産性や可読性が向上することにあります。

tinker で SQL を確認する

tinker を使って クエリビルダ でどんな SQL が作られているのか確認してみましょう。

get メソッドの代わりに toSql メソッドを呼び出すと SQL を発行するのではなく、SQL 文を生成して出力することができます。このtoSql メソッドを使ってどんな SQL 文が作成されているか確認してみましょう。

Terminal
root~# php artisan tinker
# フォルダ選択($id)がデフォルトの状態(1)に指定する
> $id = 1;
= 1
# 選択されたフォルダのレコードを検索するSQL文を発行する
> $folder = \App\Models\Folder::find($id);
= App\Models\Folder {#6571
    id: 1,
    title: "プライベート",
    created_at: "2023-11-20 01:12:33",
    updated_at: "2023-11-20 01:12:33",
  }
# タスクを取得するクエリビルダでどのようなSQL文が記述されているか確認する
> $tasks = \App\Models\Task::where('folder_id', $folder->id)->toSql();
= "select * from `tasks` where `folder_id` = ?"
# tinkerを終了する
> quit

これで クエリビルダによって SQL が作成されていることが確認できました。どのような SQL 文が発行されているか確認したい時にはこのような方法を用いることを覚えておいてください。

④テンプレートの修正

タスククラスに合わせて bladeファイル を修正していきます。

タスク一覧ページのテンプレートファイル resources/views/tasks/index.blade.php<!-- ここにタスクが表示される --> とコメントが書かれていた箇所に以下のコードを記述してください。

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>
                <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>
                    <td><a href="#">削除</a></td>
                </tr>
            @endforeach
        </tbody>
    </table>
</div>
index.blade.php の解説

それぞれのコードの解説はコメントに記述しているので確認してください。

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>
                <th></th>
            </tr>
        </thead>
        <tbody>
            <!--
            * 【task一覧セクション】
            * foreach の中でTaskControllerから渡されたデータ $tasks を参照する
            * $tasks をループして値を全て表示する
            -->
            @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>
                    <td><a href="#">削除</a></td>
                </tr>
            @endforeach
        </tbody>
    </table>
</div>

それではここで表示を確認してみましょう。
ブラウザで http://localhost/folders/1/tasks にアクセスしてください。
タスクが3つ表示されていればOKです。

但し、いくつか課題があります。

  • 状態が数値で表示されている。
    1 →「未着手」、2 →「着手中」、3 →「完了」と表示したい。
  • 状態ラベルを色分けできていない。
    「未着手」は赤、「着手中」は緑、「完了」は灰色にしたい。
  • 日付の形式はハイフン区切りではなくスラッシュ区切りにしたい。

次の項ではモデルクラスのアクセサという機能を使ってこれらの課題を改善します。

⑤Task モデルにアクセサを追加する

Task モデルにアクセサを追加する前にまずは アクセサ とは何かを確認していきましょう。

アクセサとは

アクセサとは、モデルクラスが本来持つデータを加工した値を、さもモデルクラスのプロパティであるかのように参照できる Laravel の機能です。この機能を使用すると少しコードがキレイになります。コードが読みやすいというのは「可読性」と呼ばれ重要なことでもあります。

まずモデルクラスに get○○○Attribute という決まったフォーマットのメソッドを用意します。例えば、性別の文字列表現を取得するための getGenderTextAttribute メソッドがあるとします。persons テーブルに gender(性別)カラムがあるイメージです。

get○○○Attribute の例
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 アプリでも使ってみましょう。

状態の名前を表示する

まずは状態を数値ではなく文字列で表示する機能を実装します。

モデル

タスクモデルに定数 STATUSgetStatusLabelAttribute メソッドを追加してください。

Task.php
class Task extends Model
{
    use HasFactory;

    /**
     * ステータス(状態)定義
     * 
     */
    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'];
    }
}
Task.php の解説

それぞれのコードの解説はコメントに記述しているので確認してください。

Task.php
class Task extends Model
{
    use HasFactory;

    /**
     * ステータス(状態)定義
     * const:定数
     */
    const STATUS = [
        1 => [ 'label' => '未着手' ],
        2 => [ 'label' => '着手中' ],
        3 => [ 'label' => '完了' ],
    ];

    /**
     * ステータス(状態)ラベルのアクセサメソッド
     * 
     * @return string
     */
    public function getStatusLabelAttribute()
    {
        // ステータス(状態)カラムの値を取得する
        $status = $this->attributes['status'];

        // STATUSに定義されていない場合
        if (!isset(self::STATUS[$status])) {
            // 空文字を返す
            return '';
        }
        // STATUSの値(['label'])を返す
        return self::STATUS[$status]['label'];
    }
}

$this->attributes['status'] で状態カラムの値を取得している以外は、getStatusLabelAttribute メソッドの中身は普通の PHP です。STATUS 配列から状態値をキーに文字列表現を探して返しています。

テンプレート

テンプレートは、{{ $task->status }} と記述していた箇所を書き換えてください。

index.blade.php
+ <span class="label">{{ $task->status_label }}</span>
- <span class="label">{{ $task->status }}</span>

それではここで表示を確認してみましょう。
ブラウザで http://localhost/folders/1/tasks にアクセスしてください。

状態が文字列で表示されていればOKです。

状態を色分けする

次に状態のラベルを色分けする機能を追加します。
状態の文字列表現を表示したのとほとんど同じ要領です。

モデル

STATUS 配列に class を追加して、getStatusClassAttribute メソッドを追記します。Task.php を下記のように書き直してください。

Task.php
class Task extends Model
{
    [中略]
    /**
     * ステータス(状態)定義
     * 
     */
    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'];
    }
}
Task.php の解説

それぞれのコードの解説はコメントに記述しているので確認してください。

Task.php
class Task extends Model
{
    [中略]
    /**
     * ステータス(状態)定義
     * 
     */
    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'];

        // STATUSに定義されていない場合
        if (!isset(self::STATUS[$status])) {
            // 空文字を返す
            return '';
        }
        // STATUSの値(['class'])を返す
        return self::STATUS[$status]['class'];
    }
}

コードの中身は getStatusLabelAttribute メソッドとほとんど一緒です。
label の代わりに class を返しています。

テンプレート

テンプレートでは、ラベルの部分のクラス属性に $task->status_class を追加します。

index.blade.php
+ <span class="label {{ $task->status_class }}">{{ $task->status_label }}</span>
- <span class="label">{{ $task->status_label }}</span>

それではここで表示を確認してみましょう。
ブラウザで http://localhost/folders/1/tasks にアクセスしてください。

状態が色分けされて表示されていればOKです。

日付の表示形式を変更する

最後に日付の表示形式を変更します。データベースに入ったままだと 2023-09-12 というハイフン区切りの形式なので、より一般的な 2023/09/12 というスラッシュ区切りの形式で表示していきます。

モデル

モデルクラスでは、getFormattedDueDateAttribute メソッドを追加します。
Task.php に下記を追加してください。

Task.php
// Carbon ライブラリを名前空間でインポートする
use Carbon\Carbon;
[中略]
/**
 * 整形した期限日のアクセサメソッド
 * 
 * @return string
 */
public function getFormattedDueDateAttribute()
{
    return Carbon::createFromFormat('Y-m-d', $this->attributes['due_date'])
        ->format('Y/m/d');
}
Task.php の解説

Carbon ライブラリを使って期限日の値の形式を変更して返却しています。
それぞれのコードの解説はコメントに記述しているので確認してください。

Task.php
// Carbon ライブラリを名前空間でインポートする
use Carbon\Carbon;
[中略]
/**
 * 整形した期限日のアクセサメソッド
 * 
 * @return string
 */
public function getFormattedDueDateAttribute()
{
    /* Carbon ライブラリを使って期限日の値の形式を変更して返す */
    // createFromFormat():datetime で指定した文字列を format で指定した書式に沿って解釈した時刻にする関数
    // format():書式を指定する関数
    return Carbon::createFromFormat('Y-m-d', $this->attributes['due_date'])
        ->format('Y/m/d');
}

テンプレート

テンプレートの $task->due_date だった箇所を書き換えます。

index.blade.php
+ <td>{{ $task->formatted_due_date }}</td>
- <td>{{ $task->due_date }}</td>

それではここで表示を確認してみましょう。
ブラウザで http://localhost/folders/1/tasks にアクセスしてください。
ブラウザで表示形式が変わっていればOKです。

ここまでの内容でタスク一覧ページは見た目の上ではこの章で作りたい段階は達成できました。

⑥モデルクラスにおけるリレーション(関連付け)

ここまで記述した内部のコードはもう少しキレイにすることができます。そこでもう一つだけ Laravel の強力な機能、「テーブル間の関連性をモデルクラスで表現する方法」を紹介してこの章を終わりたいと思います。

タスクコントローラーではタスクの一覧を以下のコードで取得しました。

TaskController.php
$tasks = Task::where('folder_id', $folder->id)->get();

ここで紹介する機能を使うとこのように書き直すことができます。

TaskController.php
 $tasks = $folder->tasks()->get();

「特定のフォルダに紐づくタスクの一覧を取得する処理」が、ほとんど左から英語で読んだ通りの直感的な表現で記述されています。そのため、こちらの方が可読性は上がります。

それでは実際に実装をしてみましょう。

hasMany

フォルダクラスを編集します。以下の通り tasks メソッドを追加してください。

Folder.php
class Folder extends Model
{
    use HasFactory;

    protected $table = 'folders';

    /*
    * フォルダクラスとタスククラスを関連付けするメソッド
    *
    * @return \Illuminate\Database\Eloquent\Relations\HasMany
    */
    public function tasks()
    {
        return $this->hasMany('App\Models\Task');
    }
}
Folder.php の解説

それぞれのコードの解説はコメントに記述しているので確認してください。

Folder.php
class Folder extends Model
{
    use HasFactory;

    // テーブル名を明示的に指定する
    protected $table = 'folders';

    /*
    * フォルダクラスとタスククラスを関連付けするメソッド
    *
    * @return \Illuminate\Database\Eloquent\Relations\HasMany
    */
    public function tasks()
    {
        /* フォルダクラスのタスククラスのリストを取得して返す */
        // hasMany():テーブルの関係性を辿ってインスタンスから紐づく情報を取得する関数
        // hasMany(モデル名, 関連するテーブルが持つ外部キーカラム名, hasManyが定義された外部キーに紐づけられたカラム)
        return $this->hasMany('App\Models\Task');
    }
}

index メソッドを編集する

次に、リレーションを使えるようにタスクコントローラーを書き換えてください。

TaskController.php
+ $tasks = $folder->tasks()->get();
- $tasks = Task::where('folder_id', $folder->id)->get(); 

それではここで表示を確認してみましょう。
ブラウザで http://localhost/folders/1/tasks にアクセスしてください。

元の通りに見た目が変化してなければOKです。

まとめ

ToDoアプリのタスク一覧表示機能を作成はこれで一旦は完了です。

今回の内容は、前回の内容に プラスα したようなものです。
復習と同時に新しいこともいくつか覚えられたのではないでしょうか。

特にアクセサや hasManyなどのリレーション(関連付け)は頻繁に使用することになります。

今回はその一例でしかありませんが、理解の助けにはなると思います。

この調子でLaravelを習得してきましょう。

ここまでのソースコードを下記に掲載しておきます。
確認などにご活用ください。
https://github.com/eguchi244/Laravel9-Tutorial-PJ/tree/Chapter04/ToDoアプリのタスク一覧表示機能を作る