Laravel開発の第2歩目
LaravelはWebアプリの総合環境となっているため、開発環境の構築後どう手を付けていいかわかりにくい状態です。
Laravelの開発環境構築後の、第2歩目となるようなコードを紹介したいと思います。
前置き
先に今回の記事の内容を簡単にまとめておきます。
フォーム画面を作成して、送信して、エラーチェックをして、元のページに戻ってくるところまでを紹介します。
前回の開発環境では、JavaScript環境(Vue.js)の導入も紹介していましたが、複雑になるのでここではJavaScript環境は使用しません。
ただし、ページの装飾においてTailwind CSSを使いたかったので、本来ならJavaScript環境(Webpack)に含めるべきところをページのheadでCDNを読み込んでいます。
Tailwind CSSについては今回の記事では触れませんが、あらかじめ用意されているクラス名を付与するだけで要素を装飾してくれるものです。今回のサンプルのform.blade.php部分ででてくるタグ内のクラスは、すべてTailwind CSS用のものです。
Tailwind CSSはLaravelの認証処理用のコンポーネントであるLaravel Jetstreamで採用されているCSSフレームワークです。今後、認証処理の導入を考えていてJetstreamを使うなら、今のうちからこのフレームワークを使ってページを構成するようにすれば無駄が省けます。そうでない場合は、HTML中のクラスの表記は装飾用とだけ認識してください。
また、フォーム入力についてはデータベース処理がつきものですが、Laravelでのデータベースの設定も項目が多いため今回は範囲にしておりません。リンク先に別記事にしてありますので合わせてお読みいただければ幸いです。
ページの構成
開発環境で紹介していたサンプルコードは、ユーザーがアクセスしたページを「ルーター」が解析し、適切な「コントローラー」に渡します。「コントローラー」はページ作成に必要な処理をして結果を「ビュー」として返すものでした。
ここではそうしてアクセスしたページが入力フォームになっているとし、入力フォームの内容を別アドレスにPOSTします。その内容を「ルーター」が解析し、フォーム用の「コントローラー」に渡します。フォーム用の「コントローラー」はエラーチェックなどをしその処理の結果を元のページに戻します。
フォーム入力から、結果の表示までを図示すると次のようになります。
ビューの記法
まず、ビューを作成したいのですが、その前にLaravelのblade内で使うディレクティブをいくつか紹介しておきます。
- @if(条件)...@endif
@ifの後の条件が成立した場合に、間にあるコードを出力します。
sample.blade.php
@if (count($data) === 0) データが見つかりませんでした。 @elseif (100 < count($data)) データが多すぎます。絞り込んでください。 @else 見つかったデータは次の通りです。 ... @endif
- @for...@endfor
繰り返しです。途中で「@continue」とすればcontinueも利用できます。
また、ループ全般では$loopというループ変数が利用できたとえば、$loop->indexとすることでその時点のインデックスを取得できます。
sample.blade.php
@for ($i = 0; $i > count($messages); $i++)
- {{ $messages[$i] }}
@endfor - @foreach...@endforeach
配列の要素を繰り返します。
sample.blade.php
@foreach ($messages as $msg)
- {{ $msg ] }}
@endforeach - @switch...@endswitch
switchはcaseやbreak、defaultにも@をつけます。
sample.blade.php
@switch($i) @case(1) おめでとうございます。1等当選です @break @case(2) なんと、2等当選です @break @default 残念! ハズレ @endswitch
- @while(条件) ... @endwhile
Laravelではあまり使わないかもしれませんが、whileも存在します。
- @isset(変数)...@endisset
PHPのisset関数が真になる時、間にあるコードが表示されます。同様にPHPのemptyに対応した@emptyディレクティブも存在します。
- @php... @endphp
生のPHPコードを書きたい場合はこの間に記述します。
@を使ったディレクティブの他にもビューにはコンポーネントやスロットなどの機能があります。これについてはLaravelのビューという別記事で紹介しています。合わせてお読みいただければ幸いです。
ビューの記述
それではビューを作成します。resources¥views¥フォルダにform.blade.phpを作成します。
Tailwind CSSによる装飾のため多くのクラスが付与されていますが、それぞれのクラスの役割を知りたいのならTailwind Documentationを参考にしてください。サイズ(Sizing)やスペーシング(Spacing)等、項目に分かれて掲載されています。また、サンプル的なコンポーネントも一部無料で公開されています(鍵印のないものが無料でソースを確認できます)。
基本的には名前とメールアドレスをPOSTするだけのページですが、このコードの要となっている点は次の通りです。
- POSTの宛先
POSTする宛先は「/controller」としています
- old関数
old関数を使って、POSTしてエラーが返ってきた際に前回のフォームの値を復元できるようにしています。
- XSRF対策
XSRF対策としてフォームに「<input type="hidden" name="_token" value="{{ csrf_token() }}" />」という記述を含める必要があります。
{{ csrf_field() }}とすることでinput要素の記述を省くことができます。
こちらの詳細は後で記述します。
- エラーメッセージ
@if ($errors->any()の部分でエラー時のメッセージを表示するようにしています。
- 通常メッセージ
Viewの引数として受け取る通常メッセージを$strMsgとして受け取る前提で設定しています。
- reuqired属性
フォームの各入力要素にはrequired属性を設定でき、今回のケースだとそれをつけるべきですが、Laravel側のバリデーションを例示する為にそれらを除去しています。
(読みづらい場合)
resources¥views¥form.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel サンプルフォーム</title>
<!-- Laravelのデフォルトフォント -->
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
<!-- ブラウザ間のCSS差をなくすためのリセットCSS(省略) -->
<style>
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
</style>
<style>
/* フォントの設定 */
body {
font-family: 'Nunito', sans-serif;
}
</style>
<!-- Tailwind CSS をCDNで取得しています -->
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<!-- Tailwind CSSによる装飾の為にいろいろ書いてありますが、
基本的にはメールアドレスと、ユーザー名をPOSTするだけのHTMLです -->
<div class="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full space-y-8">
<div>
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">Laravel サンプルフォーム</h2>
</div>
<form class="mt-8 space-y-6" action="/controller" method="POST">
<div class="rounded-md shadow-sm -space-y-px">
<div>
<label for="mailaddress" class="sr-only">メールアドレス</label>
<input id="mailaddress" name="mailaddress" type="email" autocomplete="email" class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" placeholder="メールアドレス" value="{{old('mailaddress')}}">
</div>
<div>
<label for="username" class="sr-only">名前</label>
<input id="username" name="username" type="text" autocomplete="name" class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" placeholder="名前" value="{{old('username')}}">
</div>
<input type="hidden" name="_token" value="{{ csrf_token() }}" />
</div>
<div>
<button type="submit" class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<span class="absolute left-0 inset-y-0 flex items-center pl-3">
<!-- Heroicon name: solid/lock-closed -->
<svg class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd" />
</svg>
</span>
登録
</button>
</div>
<div>
@if ($errors->any())
<ul>
@foreach ($errors->all() as $error)
<li>{{$error}}</li>
@endforeach
</ul>
@endif
</div>
<div class="rounded-md shadow-sm bg-green-200">
@isset($strMsg){{$strMsg}}@endisset
</div>
</form>
</div>
</div>
</body>
</html>
コントローラー
POSTされたデータを処理するコントローラーを作成します。名前は「FormController」としました。
作成する場所はapp¥Http¥Controllers¥フォルダです。自分で作成することも可能ですが、次のコマンドを実行するとひな型を自動で生成してくれます。
コントローラーに処理のための関数を記述します。この時Requestを引数に受け取れます。このRequestからPOSTされた値やCookieなどを取り出すことができます。また、Requestの中から直接受け取った値のチェック(バリデーション)も行えます。
app¥Http¥FormController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class FormController extends Controller
{
//
public function receivePost(Request $request) {
// POST(GET)された値をすべて受け取る
$inputs = $request->all();
// チェック
// エラー時はここで中断され、
// エラーメッセージが元のビューに返されます
$request->validate([
'username'=>'required|max:10',
'mailaddress'=>'required|email'
]);
// DBへの登録など
// メッセージを渡して、元の画面に戻る
return view("form",["strMsg"=>"登録しました"]);
/* もしアドレスバーを元のページに戻したい場合はsession変数に
* メッセージをセットしてリダイレクトします。
* return redirect('/')->with("message","登録しました");
*
* この時ルーター側は次のようにします
* Route::get('/', function () {
* return view('form', ["strMsg" => session("message")]);
* });
*/
}
}
この処理ではRequestからデータを取り出しResponseを返しているのですが、LravelのRequestとReseponseについては別記事で詳しく触れていますのでよろしければ参考にしてください。
バリデーションでは、名前の場合は、入力必須(required)と最大文字数(max:10)、メールアドレスの場合は入力必須と既成のメールアドレスチェック(email)を設定しています。複数のバリデーションは|によって連結されていますが、文字列の配列としても渡せます。特にregex:(正規表現)チェックで|の文字を正規表現の構文として利用する場合は、文字列の配列を使わないといけません。
例示の他にも多くのバリデーションのタイプがありますが、それについてはLaravel:「Validation」を参照してください。
これらがエラーとなった際は、エラーメッセージと共に元のページへ戻る仕組みが組み込まれています。そのエラーメッセージを受け取るのform.blade.phpの最後の方にある「@if ($errors->any()) ...@endif」の部分です。
さらに、バリデーションを通すと、前回POSTした値をセッションに保持するようになっています。それらはold関数の引数に名前を渡すことで呼び出せます。フォームの要素のvalueにセットしておくことで、エラーとして戻ってきた際に前の値を復元できます。
バリデーションがエラーになった場合デフォルトだと英語でメッセージが返ります。バリデーションエラーの日本語化については、Laravelにデータベースを設定した記事で紹介していますので、そちらを参考にしてください。
ルーティング
POSTされたアドレスに対してルーティングを設定し、作成したコントローラーへ処理を渡すようにします。
ルーティングの記述はroutes¥web.phpに記述します。
reoutes¥web.php
<?php
use Illuminate¥Support¥Facades¥Route;
use App¥Http¥Controllers¥FormController;
// デフォルトアクセス
Route::get('/', function () {
return view('form');
});
Route::post('/controller', [FormController::class, 'receivePost']);
上記ではRouteファサードからgetやpostメソッドを使用して、postやgetのアクセス時のみに限定させましたがanyとすることですべてのメソッドを対象にしたり、matchを使ってリストで設定することもできます。
...
Route::match(['get', 'post'], '/', function () { ... });
Route::any('/', function () { ... });
419 PAGE EXPIRED
もし先のform.blade.phpで、「<input type="hidden" name="_token" value="{{ csrf_token() }}" />」を含めずに実行すると、POST後に「 419 PAGE EXPIRED 」というエラーになってしまいます。これはミドルウェアとして定義されているXSRFチェックでエラーとなるのが原因で、悪意のあるスクリプトが不正にPOSTした際にプログラムがそれを受け入れてしまわないようになっています。ページ表示時にのレスポンスを受け取った際[XSRF-TOKEN」というキーを持つCookieにセットされますが、csrf_token()関数はそれを読みだしています。
Laravelのミドルウェアに関しては別記事もありますので、合わせてお読みいただければ幸いです。
もし、XSRFチェックを回避したい場合は、ほぼWebエンジニアリング:「Laravel 初心者的 API Routes うまくいかない時に」で紹介されているように、ルーターの記述を「wep.php」ではなく同じ階層にある「api.php」に記述して、POSTの宛先を/api/FormControllerにする方法もあるようです。
これはAPIとして定義することによりXSRFチェックの対象から外す手法です。
reouts¥api.php
<?php
use Illuminate¥Http¥Request;
use Illuminate¥Support¥Facades¥Route;
use App¥Http¥Controllers¥FormController;
// デフォルト値
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
Route::post('/controller', [FormController::class, 'receivePost']);
また、app¥Http¥MiddleWare¥VerifyCsrfToken.phpにXSRFチェックを行わない例外アドレスを設けることでも回避可能です。
app¥Http¥MiddleWare¥VerifyCsrfToken.php
...
class VerifyCsrfToken extends Middleware{
protected $except = [
'controller'
];
}
これらの挙動はLaravelのミドルウェアによるものなのですが、ミドルウェアについては別記事がありますので、リンク先を合わせてお読みいただければ幸いです。
また、とりあえず動くものができたのでLaravelのデプロイの際の作業を確認したいという場合の別記事もございます。