作成機能 #WebアプリケーショのUIパターン #Laravelの教科書
この記事はWebアプリケーションのUIパターンについて解説しています。その他のUIパターンについてはこちらのマガジンからどうぞ。
# はじめに
UIパターンの一つ、作成機能のパターンについて解説します。
「レシピ情報」を登録する機能を例に作成機能のパターンの解説を行います。
作成機能を作る時、ほとんどの場合Modelの作成から行います。
1. マイグレーションの作成
2. モデルの作成
3. マイグレーションの実行
という準備を行った後
1. 作成機能のControllerの作成
2. 作成機能のViewの作成
3. ルーティングの設定
を行うという流れで開発を行います。
開発の流れはこの順で行いますが、一番最初に「レシピ情報」をどのように作るかを設計するのが作成パターンの一番重要なポイントです。
# 「レシピ情報」の設計
テーブル・モデルの設計は
1. どんな情報を保存する必要があるか
2. どのような形で保存する必要があるか
という質問・回答を繰り返すことでおこないます。
Laravelはマイグレーションを使ってカラムを追加することは比較的簡単に出来るとはいえ、最初の段階で必要な情報は揃えておきたいものです。
まずは次の3個を決めます。
・テーブル名は?
・モデルのクラス名は?
・他のテーブルとの関係性は?
今回作成するレシピ情報は次のように定めました。
・テーブル名は → user_recipe
・モデルのクラス名は → Recipe
・他のテーブルとの関係性は → usersと1対多の関係(user_idカラム)
テーブル名、モデル名が決まれば次にカラムについてまとめます。
カラム毎に次の情報をまとめます。
・カラム名は何か?
・どのように保存するか?(文字列、数値など)
・必須かオプション項目か
今回のレシピ情報は次のように定めました。
・レシピ名:name, 文字列, 必須
・URL:url, 文字列, オプション
・説明:description, テキスト, オプション
・ユーザーID: user_id, usersテーブルとの連携用, 必須
# マイグレーションの作成と実行
それではマイグレーションを作成します。マイグレーションの作成はmake:migrationコマンドです。
php artisan make:migration create_recipe_table --create=user_recipe
マイグレーションファイルの作成先は「database/migrations/」の中に作成されます。
作成されたマイグレーションファイルを開き、設計どおりに記述していきます。
database/migrations/2020_09_13_094222_create_recipe_table.php
class CreateRecipeTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('user_recipe', function (Blueprint $table) {
$table->id();
//レシピ名
$table->string('name');
//URL
$table->string('url')->nullable();
//説明
$table->text('description')->nullable();
//user_id: usersとの連携用
$table->foreignId('user_id')->constrained("users");
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('user_recipe');
}
}
マイグレーションの各カラムには最初に定めた設計情報をコメントで残しておくと良いです。
$table->string('url')->nullable();
$table->text('description')->nullable();
オプション項目は後ろに「nullable()」を入れます。また、string()とtext()の使い分けですが「長文になりそうであればtext()」を使います。レシピの説明は長くなく可能性があるのでtext()を使っています。
usersテーブルと連携するには次のように書きます。
//user_id: usersとの連携用
$table->foreignId('user_id')->constrained("users");
マイグレーションを実行し、テーブルの作成を行います。
php artisan migrate
# モデルの作成とテスト
マイグレーションが問題なく実行できたら、モデルの作成を行います。モデルの作成は「make:model」コマンドです。
php artisan make:model Recipe
Laravel7.xではapp以下にRecipe.phpが作成されていましたが、Laravle8.xから作成場所がModelsディレクトリに変更されました。コマンドが正しく実行されたら「app/Models/Recipe.php」が作成されます。
作成されたRecipe.phpを開いて、テーブルの情報を追加します。
app/Models/Recipe.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Recipe extends Model
{
use HasFactory;
protected $table = "user_recipe";
protected $fillable = ['name', 'url', 'description', 'user_id'];
}
テーブル名を指定するには「protected $table」を使います。
protected $table = "user_recipe";
どんなカラムがあるかを記述するには「protected $fillable」を使います。
protected $fillable = ['name', 'url', 'description', 'user_id'];
モデルにはいくつかの機能がありますが、最低限使うのはこの2個です。
モデルが正しく設定できているかのテストはいくつかの手法があります。今回はTinkerを使うこともできます。Tinkerはターミナルからlaravelのコードを実行することが出来る便利な機能です。Tinkerについては別記事で詳しく説明します。
まずTinkerを起動します。次のコマンドをターミナルで実行することでTinkerが起動します。
php artisan tinker
実行すると、次のように出力されtinkerにコマンドを入力することができます。
Psy Shell v0.10.4 (PHP 7.4.2 — cli) by Justin Hileman
>>>
「exit」と入力してEnterを押すとTinkerを終了することができます。
Psy Shell v0.10.4 (PHP 7.4.2 — cli) by Justin Hileman
>>> exit
Exit: Goodbye
TinkerにPHPのコードを入力すると実行して結果を表示してくれます。
今回はRecipeモデルが正しく実行できているか確認するため、Recipeモデルから値を取り出す処理を入力します。まだデータは空なので何も表示されませんが、テーブル名の設定などが正しいかを確認できます。
Tinkerに「App\Models\Recipe::all()」を入力後Enterを押すと次の表示になっていればOKです。
Psy Shell v0.10.4 (PHP 7.4.2 — cli) by Justin Hileman
>>> App\Models\Recipe::all()
=> Illuminate\Database\Eloquent\Collection {#3371
all: [],
}
テーブル名が違う、クラス名が違うなどの場合はエラーがでます。
# Controllerの作成
モデルの作成が完了したので、Recipeモデルを登録するためのControllerを作成します。
パターンに限らず、Controllerは次のことを開発前に決める必要があります。
・クラス名
・関数名
作成のパターンでは、関数が2個必要です。必要な関数は次の2個です。
1. フォームを表示する関数
2. フォームからの遷移先(作成を実行する)の関数
名前は自由に決めることができますが、作成パターンであることがわかりやすい方が望ましいです。今回は次のように定めました。
・クラス名 = CreateRecipeController
・関数名
・フォーム表示 = create
・遷移先 = store
make:controllerコマンドを使って「CreateRecipeController」を作成します。
php artisan make:controller CreateRecipeController
作成されたCreateRecipeController.phpを開き、次のように書き換えます。
app/Http/Controllers/CreateRecipeController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
/* 必要なモデルをuseする */
use App\Models\Recipe;
use App\Models\User;
class CreateRecipeController extends Controller
{
/**
* バリデーションのルール
*/
protected $validationRules = [
"name" => ["required", "string"],
"url" => ["nullable", "url"],
"description" => ["nullable","string"]
];
function __construct(){
$this->middleware('auth');
}
/**
* レシピ登録フォームを表示
*/
function create(){
return view("recipe.recipe_create_form");
}
/**
* レシピ登録フォームからの遷移先
*/
function store(Request $request){
//入力値の受け取り
$validatedData = $request->validate($this->validationRules);
//作成するユーザーIDを設定\
$validatedData["user_id"] = \Auth::id();
//レシピの保存
$new = Recipe::create($validatedData);
//登録後はダッシュボードに遷移
return redirect()->route("dashboard");
}
}
◆ ポイント ◆
use App\Models\Recipe;
use App\Models\User;
必要なクラスをuseする箇所を必ず記述しましょう。
/**
* バリデーションのルール
*/
protected $validationRules = [
"name" => ["required", "string"],
"url" => ["nullable", "url"],
"description" => ["nullable","string"]
];
バリデーションのルールはクラスのプロパティに保持する形がわかりやすいです。コードが長くなっても探せます。一つのControllerに複数のバリデーションが発生する場合は機能を見直して、別のControllerにするのが良いです。
function __construct(){
$this->middleware('auth');
}
このコントローラーはログイン後にのみ使えることを明記するため「$this->middleware('auth');」を設定しています。ミドルウェアの使用はルーティング内で設定することもできますが、このようにControllerに書いておくと後で見た時にログイン中に使うControllerだな、とすぐわかるメリットがあります。
/**
* レシピ登録フォームを表示
*/
function create(){
return view("recipe.recipe_create_form");
}
レシピ登録フォームを表示するcreate関数はview()を使って画面を返すだけです。
/**
* レシピ登録フォームからの遷移先
*/
function store(Request $request){
//入力値の受け取り
$validatedData = $request->validate($this->validationRules);
//作成するユーザーIDを設定\
$validatedData["user_id"] = \Auth::id();
//レシピの保存
$new = Recipe::create($validatedData);
//登録後は登録フォームを表示
return redirect()->route("create_recipe")
->withStatus("レシピ: {$new->name}を作成しました");
}
レシピの登録処理を行うstore()関数は次の3つの処理を行います。
1. フォームの入力値の受け取り
2. モデルの保存
3. 登録後にリダイレクト
1. フォームの入力値の受け取り
$validatedData = $request->validate($this->validationRules);
$request->validate()関数は「バリデーション実行」「エラーが発生したら前の画面を表示」という処理を自動で行ってくれる関数です。
返却された「$validatedData」にはバリデーションを通った正しいデータが取得されます。
$validatedData["user_id"] = \Auth::id();
また、ログインしているユーザーのIDをuser_idに入れる必要があるため「\Auth::id()」を使ってユーザーIDを取得しています。
Authを使うのはここだけなので「use Auth;」を省略できるように先頭に「バックスラッシュ」が入っています。
2. モデルの保存
$new = Recipe::create($validatedData);
Modelクラス::create()関数を使ってモデルを保存します。
3. 登録後にリダイレクト
return redirect()->route("create_recipe")
->withStatus("レシピ: {$new->name}を作成しました");
一般的に作成パターンは一覧や詳細に遷移することが多いですが、未作成なため、同じ作成フォームにメッセージ付きでリダイレクトしています。
今後一覧、詳細が作成された時に書き換えが簡単なようにroute()を使ってリダイレクトしています。
# Viewの作成
Controllerで指定しているView「recipe.recipe_create_form」を作成していきます。
作成パターンに限らず、フォーム画面は必ず次の要素を持ちます。
・<form>〜</form>でフォーム全体を囲む
・<input type="submit">の保存・作成ボタン
・フォームの各要素
・ラベル
・フォームの要素(input, textarea, selectなど)
・エラー表示
今回作成するViewは「レシピ名」「URL」「説明」の項目を設置します。また、作成後にリダイレクト表示を行うため、メッセージの表示欄を置きます。
設置場所は「resources/views/recipe/recipe_create_form.blade.php」となります。
注意点として、Laravel7から8でCSSがbootstrapからtailwindcssベースに変わりました。また、それ以外にもレイアウトの設定の方法が@extendsを使った指定から<x-app-layout>を使った書き方に変わっています。
大きな変更はありませんがレイアウトに使うclass名に大きな変更があるなど、戸惑うポイントがいくつかあるかもしれません。
◆
recipe_create_form.blade.phpはJetstreamの初期化で作成されたdashboard.blade.phpをベースに次のように作成しました。
resources/views/recipe/recipe_create_form.blade.php
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
レシピ登録
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
@if (session('status'))
<div class="success mt-5 px-4 text-green-900">
{{ session('status') }}
</div>
@endif
<!-- ページ固有要素 -->
<div class="mt-5 px-4 py-5">
<!-- エラー表示 -->
<form method="post" action="{{ route('store_recipe') }}">
@csrf
<div class="mb-4">
<label for="recipe_name" class="block mb-2">レシピ名</label>
<input type="text"
id="recipe_name"
class="form-input w-full"
name="name"
value="{{ old('name') }}"
placeholder="レシピ名" />
@if ($errors->has('name'))
<span class="error mb-4 text-red-900">{{ $errors->first('name') }}</span>
@endif
</div>
<div class="mb-4">
<label for="recipe_url" class="block mb-2">URL</label>
<input type="text"
id="recipe_url"
class="form-input w-full"
name="url"
value="{{ old('url') }}"
placeholder="https://xxxxx" />
@if ($errors->has('url'))
<span class="error mb-4 text-red-900">{{ $errors->first('url') }}</span>
@endif
</div>
<div class="mb-4">
<label for="recipe_description" class="block mb-2">説明</label>
<textarea type="text"
id="recipe_description"
class="form-input w-full"
name="description"
rows="5"
>{{ old('description') }}</textarea>
@if ($errors->has('description'))
<span class="error mb-4 text-red-900">{{ $errors->first('description') }}</span>
@endif
</div>
<div class="mb-4 flex items-center">
<input type="submit" value="作成" class="bg-blue-500 text-white font-bold py-2 px-4 rounded" />
</div>
</form>
</div>
<!-- /ページ固有要素 ここまで -->
</div>
</div>
</div>
</x-app-layout>
◆ ポイント ◆
@if (session('status'))
<div class="success mt-5 px-4 text-green-900">
{{ session('status') }}
</div>
@endif
作成後にメッセージ(レシピ: レシピ名を作成しました)を表示するためのコードです。
<div class="mb-4">
<label for="recipe_name" class="block mb-2">レシピ名</label>
<input type="text"
id="recipe_name"
class="form-input w-full"
name="name"
value="{{ old('name') }}"
placeholder="レシピ名" />
@if ($errors->has('name'))
<span class="error mb-4 text-red-900">{{ $errors->first('name') }}</span>
@endif
</div>
「ラベル(<label>)」「フォームの要素(<input>)」「エラー表示」を順に並べています。この3点セットがフォームの部品の基本要素となります。
@if ($errors->has('name'))
<span class="error mb-4 text-red-900">{{ $errors->first('name') }}</span>
@endif
"name"で指定したバリデーションのエラーを表示しています。3個の要素があり、3個のバリデーションを行うため、それぞれのnameにそろえてエラーを表示しています。
# ルーティングの設定
ルーティングを設定するファイルはroutes/web.phpです。
作成パターンではルーティングは次の2個を設定します。
・create_xxx: フォームの表示(getで指定)
・store_xxx: フォームの遷移先(postで指定)
今回はRecipeを作成するため「create_recipe」「store_recipe」の2個のルーティングを設定します。
routes/web.php
Route::get('/recipe/create', [App\Http\Controllers\CreateRecipeController::class, "create"])
->name("create_recipe");
Route::post('/recipe/create', [App\Http\Controllers\CreateRecipeController::class, "store"])
->name("store_recipe");
各ルーティングに「name()」を使って名前を指定しておきます。ルーティングはこのようにすべて名前をつけておくと後々の開発が楽になります。
Laravel7より以前を使っていた人が見ると、見慣れない書き方になっているように感じると思います。Laravel7 -> 8でControllerの指定方法が変更されました。
Route::get('URL', "クラス名@関数名");
↓
Route::get('URL', [App\Http\Controllers\クラス名::class, "関数名"]);
App\Http\Controllersを追加すると長くなってしまうため、use句を使うやり方もできます。
//先頭に書く
use App\Http\Controllers\CreateRecipeController;
use句はコードの先頭に書く必要があります。use句を書いた場合は「クラス名::class」で指定できるようになります。
Route::get('/recipe/create', [CreateRecipeController::class, "create"]);
この変更はControllerクラスの設置場所を自由にするために行われたものですが、初学者からすると少し難しくなったように思えます。
# 動作確認
ブラウザで「会員登録」「ログイン」をすませた状態で「/recipe/create」にアクセスしてフォームの表示とフォームが遷移できるか確認してください。
レシピ名をわざと空にする、URLに不正な文字(http://〜始まらない文字)などを入れるなどを行い、エラーメッセージを確認する必要もあります。
詳細画面、一覧画面が無いため実際にデータが保存されているかをブラウザで確認することができません。
phpmyadminを使ってレコードを直接見に行くこともできますが、ここではTinkerを使って動作を確認するやり方を説明します。
次のコマンドをターミナルに入力し、Tinkerを起動します。
php artisan tinker
実行すると、次のような表示となり、tinkerにコマンドを入力することができます。
Psy Shell v0.10.4 (PHP 7.4.2 — cli) by Justin Hileman
>>>
最後に追加したレシピを表示するために「App\Models\Recipe::latest()->first()」と入力してみましょう。
Psy Shell v0.10.4 (PHP 7.4.2 — cli) by Justin Hileman
>>> App\Models\Recipe::latest()->first()
=> App\Models\Recipe {#3389
id: 12,
name: "目玉焼き",
url: "https://www.google.com/?q=目玉焼き",
description: """
1. フライパンを温めます\r\n
2. 卵を割り入れます\r\n
3. 塩コショウを振ります\r\n
\r\n
できあがり!
""",
user_id: 1,
created_at: "2020-09-13 13:52:25",
updated_at: "2020-09-13 13:52:25",
}
最後に追加した値が入っていれば成功です。
# まとめ
作成パターンの要点をまとめます。
・モデル、テーブルの設計を行う
・マイグレーションを実行する
・Controllerは「フォーム表示」「フォームの遷移先」の2個を作成
・Viewは「フォーム」の1個を作成
・ルーティングは「フォーム表示」「フォームの遷移先」の2個を設定する
作成パターンで作ったViewは詳細・編集パターンでも再利用することができます。またフォームの入力要素など、ピンポイントで再利用できます。
Controllerのバリデーションも詳細・編集パターンで使うことができます。
別のモデルを作成する時も同じControllerをそのままコピーし、バリデーションとモデル名だけ書き換えたら他のモデルを保存する機能にそのまま利用することができます。
作成パターンはこのようにWebアプリケーションにおいて再利用性が高いため、まずはじめに着手する重要な機能です。
次回は一覧・検索のパターンを解説します。