はじめに
個人的にCakePHPでWebアプリを書いていたりしたんですが、最近はLaravelが来てるらしいというので、ぐちゃぐちゃになったアプリをLaravelで書き直してみるか、と思い立ちました。
何から始めればいいか分からないので、
- 初心者のためのLaravel入門(掌田津耶乃氏)
のページを見ながら進めてみました。
しかし、記事が書かれたのが2016年とあって、その後のLaravelのバージョンアップに伴い、そのまま同じことをやっても動かない事態に。
というわけで、上の記事に沿ってLaravel 5.5の操作を進めるうえで、ハマったところをここに記録しておきます。
…というのが元々の動機だったのですが、調べていくとLaravelの流儀に従って実装した方がいいのではと思えるところもあって、後半はほぼそういう感じの話になっています。
なお、以下ではLaravelが新しくなって変わったところと、後述のようにLinuxで動かしているために変わるところが混ざっています。
前提条件
- Laravel 5.5
- OS: Linux (CentOS 7)
- PHP: 対応バージョン (7.0.23) をインストール済みで、パスが通っているとします。
1. Laravelをスタートしよう
Composer
記事は、まずComposerを用意するところから始まります。
Windowsで動かすときはこの通りでOKなのでしょうが、Linuxでphpコマンドを使ったセットアップ(Download Composerの「Command-line installation」参照)を行ったため、実行ファイルの名前がcomposer.phar
になりました。
そのため、ファイル名をcomposer
に変更するか、コマンド実行の説明部分でcomposer
をcomposer.phar
に読み替える必要がありました。
内蔵サーバーで動かす
php artisan serve
コマンドでサーバーを立てて動作を試せます。本番環境だとApache使いたいですが、開発用には良いと思います。
デフォルトだとlocalhostでListen(待ち受け)しますが、LinuxのサーバーにSSHでログインしてプログラムを書くような場合だと、localhostで待ち受けられても困ります。というわけで
$ php artisan serve --host=192.168.123.45
みたいな感じで、--host オプションを付けましょう。
なお、ファイアウォールのチェックもお忘れなく。8000番を開けましょう。
http://192.168.123.45:8000/
にアクセスすると、デモページが出るはずです。
appフォルダ
この「app」フォルダの中には、更にいくつものフォルダが用意されています。以下に整理しましょう。
と書いてあって、フォルダのリストが列挙されていますが、手元を見るとフォルダは「Console」「Exceptions」「Http」「Providers」しかないです。
まあ後で分からなくなったら調べればいいし、一番大事といわれているHttpフォルダは存在するので、とりあえずスルーしまして。
それで、その後を読むと
一番重要な「Http」フォルダの中には、更に複数のフォルダとファイルがあります。これらもざっとまとめておきましょう。
(中略)
とりあえず、「Controllers」フォルダと、routes.phpは、実際のプログラミングに入ったらすぐに使うことになります。この2つについては、頭に入れておきましょう。
とあるんですけど、なんと「routes.php」がありません。これは一大事の予感。
以下のページを見ると、5.3以降はroutes/web.php
に読み替えれば良いようです。
以後、routes.php
と書いてあったらroutes/web.php
だと思って読んでいきます。
2. ルーティングとコントローラー
前章の予告通り、いきなりroutes.phpが出てきましたが、これは先ほど調べたようにroutes/web.php
だと思ってください。
Route::group(○○);
の呼び出しなんて書いてないですが、とりあえず置いておきます。
コントローラが呼び出せない
5ページ目に進むと
Route::controller('helo', 'HeloController');
という記述が登場しますが、これを書くとサーバーを起動するときに
In RouteAction.php line 84:
Invalid route action: [App\Http\Controllers\HeloController].
と言われました。えっなんで??
実はLaravel 5.3でRoute::controller
は削除されたようです。
では5.5時点ではどう書けばいいかというと、以下のようにすればとりあえず動きます。
Route::get('helo', 'HeloController@getIndex');
リソースコントローラ
但し、実際問題で考えると、他にもいろいろな機能を用意したいはずで、それぞれhelo/show
みたいな規則的なURIで呼び出せるようにしたいですね。
あまりweb.php
の記述量を増やしたくないし。
というわけで、「リソースコントローラ」を作ってみます。DBアクセスするアプリに必要になりそうな機能のベースを準備してくれるんだそうで。
最初のHeloController.phpはいったんリネームしておいて
$ php artisan make:controller HeloController --resource
というコマンドを叩くと、app/Http/Controllers/HeloController.php
ができます。
このファイルを開いて、index()
メソッドの中を編集します。
public function index()
{
return view('helo', ['message' => 'Hello!']);
}
この中身は、最初のHeloController.phpでgetIndex()
メソッドに書いてあった中身と同じです。
そのうえで、web.phpの記述は
Route::resource('helo', 'HeloController');
とします。
http://192.168.123.45:8000/helo
にアクセスして、画面に Hello! と出ればOKです。
以下のページを参考にしました。
3. Requestクラスの基本
POSTするとエラーになる?
最初のページでViewにフォームを追加していますが、このままだとPOSTしたときに怒られます。
以下のように、フォームのところでcsrf_field()
の結果を表示しなければいけません。
<!doctype html>
<html>
<head>
<title>Sample</title>
<style>
body { color:gray; }
h1 { font-size:18pt; font-weight:bold; }
</style>
</head>
<body>
<h1>Sample</h1>
<p><?php echo $message; ?></p>
<form method="post" action="/helo">
<?php echo csrf_field(); ?>
<input type="text" name="str">
<input type="submit">
</form>
</body>
但し、検索して調べてみると、多くの資料で「Bladeテンプレート」を使って
{{ csrf_field() }}
と書くように説明されています。
このように書きたい場合は、helo.php
をhelo.blade.php
とリネームすると良いです。これによってhelo
はBladeビューとなり、Bladeテンプレートが解釈されるようになります。
<!doctype html>
<html>
<head>
<title>Sample</title>
<style>
body { color:gray; }
h1 { font-size:18pt; font-weight:bold; }
</style>
</head>
<body>
<h1>Sample</h1>
<p>{{ $message }}</p>
<form method="post" action="/helo">
{{ csrf_field() }}
<input type="text" name="str">
<input type="submit">
</form>
</body>
ついでに $message
の出力部分も書き換えてしまいました(後でも出てきます)。
以下が参考になりました。
- https://laravel.com/docs/5.5/csrf
- https://laravel.com/docs/5.5/blade
- https://stackoverflow.com/questions/46141705/the-page-has-expired-due-to-inactivity-laravel-5-5
postIndex() の代わり
2ページ目に「コントローラにpostIndex()
メソッドを追加する」旨書かれていますが、リソースコントローラとして呼び出すときは、同じことをstore()
メソッドで行います。
public function store(Request $request)
{
$res = "you typed: " . $request->input('str');
return view('helo', ['message' => $res]);
}
これでテキストを入力して「送信」ボタンを押したときに「you typed: ...」の表示が出れば成功。
クエリ文字列の取得
3ページ目のように、クエリで渡された値を取得するにはRequestクラスを使います。index()
メソッドでRequestクラスのインスタンスを受け取るために、最初の行のように引数を追加しておきます。
public function index(Request $request)
{
$res = 'ID : ' . $request->id;
return view('helo', ['message' => $res]);
}
4. データベースアクセスの基本
DB設定が反映されない?
config/database.php
は、5.5にもちゃんとありました。よかった。(笑)
でも、これを説明通りに書き換えても、設定が反映されませんでした。
代わりに、アプリケーションルートにある .env
(Linuxだと ls -a
しないと見えない)ファイルを書き換えると良いです。
DB_CONNECTION=sqlite
#DB_CONNECTION=mysql
#DB_HOST=127.0.0.1
#DB_PORT=3306
#DB_DATABASE=homestead
#DB_USERNAME=homestead
#DB_PASSWORD=secret
後は説明通りで動きます(但しgetIndex()
はindex()
に読み替えてください)。
以下を参考にしました。
Bladeテンプレートを活用しよう(XSS対策にも!)
動くといえば動くんですが、せっかくBladeビューなのに <?php
とか書きたくないですよね。
というわけで、ビューをBladeテンプレートを使って書き直してみます。
<!doctype html>
<html>
<head>
<title>Sample</title>
<style>
body { color:gray; }
h1 { font-size:18pt; font-weight:bold; }
th { color:white; background:#999; }
td { color:black; background:#eee; padding:5px 10px; }
</style>
</head>
<body>
<h1>Sample</h1>
<p>{{ $message }}</p>
<table>
<tr><th>ID</th><th>NAME</th><th>MAIL</th><th>AGE</th></tr>
@foreach ($data as $val)
<tr>
<td>{{ $val->id }}</td>
<td>{{ $val->name }}</td>
<td>{{ $val->mail }}</td>
<td>{{ $val->age }}</td>
</tr>
@endforeach
</table>
</body>
</html>
この方法の良い点は、出力される値が自動的にエスケープされることです。例えば、DBのname
フィールドに<script>
と入力されていたとしても、自動的に<
や>
を<
や>
に置き換えて出力してくれます。クロスサイトスクリプティング(XSS)対策ですね。
PHPで同じことをするには、echo
するときに出力をhtmlspecialchars()
関数で囲む必要があります。いちいち書くの忘れそうだし、何より見た目が長くなってよろしくないですね。
ここでは以下を参考にしました。エスケープされるということも書いてあります。
5. ORM(Eloquent)の利用
ORMの作成方法
$ php artisan make model MyTable
と実行しろとあるんですが、これだとエラーになってしまいました。
以下が正解。「コロン」を忘れずに。
$ php artisan make:model MyTable
ORMが呼べない?
3ページ目を見ながら
public function index(Request $request)
{
$data = MyTable::all();
return view('helo', ['message' => 'MyTable List','data' => $data]);
}
と書いたら、MyTableが見つからないぞ!と怒られてしまいました。
use App\MyTable;
を追加しておきましょう。
6. ModelのCRUD
Bladeビューで作りましょう
2ページ目で new.php
というビューを作成していますが、例によって <?php
書きたくない病なので、代わりに new.blade.php
を以下の内容で作ります。
そうそう、{{ csrf_field() }}
を忘れないでくださいね。
<!doctype html>
<html>
<head>
<title>Sample</title>
<style>
body { color:gray; }
h1 { font-size:18pt; font-weight:bold; }
th { color:white; background:#999; }
td { color:black; background:#eee; padding:5px 10px; }
</style>
</head>
<body>
<h1>Sample</h1>
<p>{{ $message }}</p>
<table>
<form method="post" action="/helo/new">
{{ csrf_field() }}
<tr><td>NAME:</td><td><input type="text" name="name"></td></tr>
<tr><td>MAIL:</td><td><input type="text" name="mail"></td></tr>
<tr><td>AGE:</td><td><input type="text" name="age"></td></tr>
<tr><td></td><td><input type="submit"></td></tr>
</form>
</table>
</body>
</html>
/helo/new の処理はどこに?
さて今までリソースコントローラがどうのと言ってきましたが、/helo/new
はどうするのと。
リソースコントローラでは、アクセスさせるURIと実行される(コントローラ側=PHP側の)メソッドの対応があらかじめ決まっていますので、new
などという知らないものを扱うには工夫が要ります。
第1の方法
routes/web.php
内でRoute::get
やRoute::post
を書くという方法が考えられます。
2ページ目の通りにコントローラにgetNew()
とpostNew()
のメソッドを追加します。1点だけ変わるのは、postNew()
の戻り値が
return redirect()->route('helo.index');
になることです(リソースコントローラのやり方で呼びます)。
routes/web.php
については、こんな感じで。
Route::get('/', function () {
return view('welcome');
});
Route::get('helo/new', 'HeloController@getNew');
Route::post('helo/new', 'HeloController@postNew');
Route::resource('helo', 'HeloController');
第2の方法
この際、URIとコントローラのメソッド名をリソースコントローラの枠組みにすっぽり合わせてしまうというのもアリだと思います(むしろそのためにあるのでは?)。
リソースコントローラの説明を眺めてみると、意味的には
- getNew → create
- postNew → store
に相当すると思われます。
ということで、それぞれのメソッドの中身をごっそり移動してしまいましょう。
(機能に対応するコントローラのメソッド名をカスタマイズすることはできるようですが、特に理由がなければフレームワークに従うのが良いかと…)
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
return view('new', ['message' => 'MyTable Create']);
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$name = $request->input('name');
$mail = $request->input('mail');
$age = $request->input('age');
$data = array(
'name' => $name,
'mail' => $mail,
'age' => $age
);
MyTable::create($data);
return redirect()->route('helo.index');
}
ビュー側は、<form>
メソッドに指定するURIだけが変わっています。
以下のように {{ route('helo.store') }}
とすると、投げるべきURIに自動展開されるのでこの方が良いでしょう。
<body>
<h1>Sample</h1>
<p>{{ $message }}</p>
<table>
<form method="post" action="{{ route('helo.store') }}">
{{ csrf_field() }}
<tr><td>NAME:</td><td><input type="text" name="name"></td></tr>
<tr><td>MAIL:</td><td><input type="text" name="mail"></td></tr>
<tr><td>AGE:</td><td><input type="text" name="age"></td></tr>
<tr><td></td><td><input type="submit"></td></tr>
</form>
</table>
</body>
さらに、登録画面のURIが http://192.168.123.45:8000/helo/create
に変わります。
このURIをわざわざ手入力させることは考えにくいですので、これでもいいのではないでしょうか。
同じくupdateも
4ページ目以降、今度は/helo/update
を追加するとあります。
もちろん、先ほどの「第1の方法」と同じようにルーティングを追加するのもよいですが、せっかくなので「第2の方法」の流れでリソースコントローラの流儀に従って実装していきましょう。
意味を考えると、コントローラのメソッド名は
- getUpdate → edit
- postUpdate → update
でいいですかね。
ビューは例によってBladeビューとして作りましょう。
リソースコントローラでは、更新時のURIにID情報が含まれています。{{ route('helo.update', ['id' => $data->id]) }}
のように、IDを引数に付けて route()
を呼び出すことによって、そのIDを更新するためにアクセスすべきURIに展開されます。URIでIDを書いているので、<input type="hidden">
でIDを指定する必要はなくなっています。
また、この更新時のHTTPメソッドとしては本来PUT
かPATCH
を使うことになっていますが、HTMLフォームからはPOST
しか送れません。フォーム内に {{ method_field('PUT') }}
を書いておくと、LaravelはPUT
メソッドで投げられたものと思って処理してくれます。
<!doctype html>
<html>
<head>
<title>Sample</title>
<style>
body { color:gray; }
h1 { font-size:18pt; font-weight:bold; }
th { color:white; background:#999; }
td { color:black; background:#eee; padding:5px 10px; }
</style>
</head>
<body>
<h1>Sample</h1>
<p>{{ $message }}</p>
<table>
<form method="post" action="{{ route('helo.update', ['id' => $data->id]) }}">
{{ csrf_field() }}
{{ method_field('PUT') }}
<tr><td>NAME:</td><td>
<input type="text" name="name" value="{{ $data->name }}"></td></tr>
<tr><td>MAIL:</td><td>
<input type="text" name="mail" value="{{ $data->mail }}"></td></tr>
<tr><td>AGE:</td><td>
<input type="text" name="age" value="{{ $data->age }}"></td></tr>
<tr><td></td><td><input type="submit"></td></tr>
</form>
</table>
</body>
</html>
さらに、コントローラはこんな感じに。edit()
とupdate()
を実装します。
いずれも、引数としてIDが渡ってくるので、これをそのまま使うように書けばOKです。
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
$data = MyTable::find($id);
$msg = 'MyTable Update [id = ' . $id . ']';
return view('update', ['message' => $msg, 'data' => $data]);
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
$data = MyTable::find($id);
$data->name = $request->input('name');
$data->mail = $request->input('mail');
$data->age = $request->input('age');
$data->save();
return redirect()->route('helo.index');
}
さて、編集画面のURIはどこに行ったでしょうか。
ID: 1を編集するときは http://192.168.123.45:8000/helo/1/edit
になりますよ。
更新がちゃんと効けばOKです。
最後にdelete
この際、deleteもリソースコントローラの流儀で書きますよ。
その場合、さすがにHTTPのGET
で、というわけにいかないので、delete用のフォームを準備します。
新しいビューを作るのも面倒なので、updateの画面を編集します。こんな感じでいいですかね。
updateと同様、deleteの場合も、route()
の引数としてIDを与えるとURIにID情報が反映されます。
<body>
<h1>Sample</h1>
<p>{{ $message }}</p>
<table>
<form method="post" action="{{ route('helo.update', ['id' => $data->id]) }}">
{{ csrf_field() }}
{{ method_field('PUT') }}
<tr><td>NAME:</td><td>
<input type="text" name="name" value="{{ $data->name }}"></td></tr>
<tr><td>MAIL:</td><td>
<input type="text" name="mail" value="{{ $data->mail }}"></td></tr>
<tr><td>AGE:</td><td>
<input type="text" name="age" value="{{ $data->age }}"></td></tr>
<tr><td></td><td><input type="submit"></td></tr>
</form>
<form method="post" action="{{ route('helo.update', ['id' => $data->id]) }}">
{{ csrf_field() }}
{{ method_field('DELETE') }}
<tr><td></td><td><input type="submit" value="削除"></td>
</form>
</table>
</body>
対応するコントローラ側のメソッドはdestroy()
になります。
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
$data = MyTable::find($id);
$data->delete();
return redirect()->route('helo.index');
}
先ほどの http://192.168.123.45:8000/helo/1/edit
にアクセスすると、削除ボタンが追加されています。この削除ボタンを押せば、ID: 1のレコードが消えます。
一覧画面にジャンプしますので、ID: 1が消えていることを確認しましょう。
最後に
なんか意外と長くなってしまったのですが、ここまでできれば何か作れるんじゃないかという気がしますね。まだ使っていない機能が大量にあると思いますが、それはドキュメントなどで調べながらやればいいので。
今回は各所で<form>
タグを直書きしたのですが、URIを直書きするのも何か美しくないなあと思っていたら、やっぱり良さそうな方法がありました。
これを使うと {{ csrf_field() }}
も {{ method_field('PUT') }}
も気にしなくていいみたいですね。詳しくは以下のページなどをご覧ください。
- http://recipes.laravel.jp/recipe/224
- https://laravel10.wordpress.com/2015/03/08/%E5%88%9D%E3%82%81%E3%81%A6%E3%81%AElaravel-5-16-form%E3%81%AE%E4%BD%9C%E6%88%90/
DBの操作(CRUD)に関する所については、リソースコントローラとして書けば、何を実装していて何をまだ実装していないか分かりやすいし、複数人で開発するときにも混乱しなくて良いのではないでしょうか(と書きながら何かを思い出す)。
…と、ここまで書いて気づいた
あっ、まだshow()
を実装してなかった!
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
$data = MyTable::where('id', $id)->get();
return view('helo', ['message' => 'MyTable List', 'data' => $data]);
}
http://192.168.123.45:8000/helo/2
にアクセスしてID: 2のレコードだけが表示されることを確認しましょう。(完)