PHPのフレームワークLaravel で blog アプリを作成しながら、laravelの使い方を説明しています。
Laravelはバージョン7を使います。
仕様と手順は、以下の記事の通りです。
今回は、3. アプリ作成の【7 Blog機能作成】の続きとなります。
今回の学習ポイントは次のとおり。
7.7. 投稿の編集機能の作成 + 自分の投稿かどうかのチェック機能追加
いろんな書き方はあるのですが、基本的であり、応用が利きそうな方法を使って作成していきます。
今回は自分が作成した投稿の編集処理を作ります。今回作成するページは下図のようになります。
見た目はほぼ、新規投稿ページと変わりませんが、現在登録されている投稿データがすでに入力されている状態から編集を開始するところが主な違いです。
なお、前回までで投稿一覧ページ・投稿詳細表示ページ・新規投稿作成ページまで作成しましたが、もしまだの方は、作成してから、またこちらに戻ってきてくださいね。
↓のリンクに一覧を載せてます。
更新:投稿の編集機能の作成
リソースコントローラにおいてCRUDのうち、更新(Update)を実現するのに2つのアクションを使います。edit アクションと update アクションです。edit アクションで、投稿の編集フォームを表示します。編集されたデータは update アクションで処理され、データを更新します。
ルート設定
ルート設定は、すでにリソースコントローラとして一括してやってしまってますが、一応ざっと説明します。
editアクションのURLは、 /posts/投稿ID/edit となっており、GETメソッドでアクセスします。
updateアクションは URLは、 /posts/投稿ID で、PATCHメソッドでアクセスすることになっています。
http://example.com/public がトップページだとすると、
http://example.com/public/posts/投稿ID/edit
などとアクセスします。
editアクションを作成
edit は、投稿編集用のフォームを表示するアクションです。こちらから作っていきましょう。
コントローラにeditメソッドの実装
PostsController の edit()メソッドには、以下のコードを実装します。
1 2 3 4 5 6 7 8 9 |
public function edit($id) { $post = Post::find($id); if (auth()->user()->id != $post->user_id) { return redirect(route('posts.index'))->with('error', '許可されていない操作です'); } return view('posts.edit')->with('post', $post); } |
createメソッドと同様に、すでにログイン認証付きになってます。createメソッドと違うのは、投稿編集の場合、自分の投稿じゃないと操作できないようにする必要があります。
ひとつずつ説明していきます。
投稿データの読み出し
1 |
$post = Post::find($id); |
このコードは、詳細表示でも出てきましたね。$idで指定された投稿IDの投稿をデータベースから読み出して $post 変数に代入しています。
記事の投稿者チェック
1 2 3 |
if (auth()->user()->id != $post->user_id) { return redirect(route('posts.index'))->with('error', '許可されていない操作です'); } |
この部分は、自分の投稿した記事かどうかをチェックしています。
auth()->user()->id がログイン中のユーザのid で、 $post->user_id は投稿レコードに記録された投稿したユーザのidです。
もし、違う場合は、次の行が実行されます。
次の return redirect(route('posts.index')) で、投稿一覧のページにリダイレクトしていますが、そのままだと何が起きたのかユーザに伝わりません。
そこで、 ->with('error', '許可されていない操作です'); とメソッドをつなげて、セッションデータ名「error」に、リダイレクトの原因「許可されていない操作です」を表示するようにしています。
セッションデータをページに表示させる処理は、「リソースコントローラで新規作成」の記事で、テンプレートの共通部分に追加しています。
URLに直接投稿IDを入力して、わざと自分じゃない投稿を編集しようとすると以下のように投稿一覧のページにエラーメッセージが表示されます。
編集用フォームの表示
1 |
return view('posts.edit')->with('post', $post); |
投稿者チェックをパスしたら、編集用のフォームを表示します。現在の投稿内容を表示するため、$post 変数を ビューファイル posts/edit.blade.php に渡します。
ビュー posts.edit の実装
次に、ビューの実装です。
ユーザーに表示される投稿の編集ページを作っていきます。
create.blade.php とよく似ています。views/posts フォルダにあるcreate.blade.php ファイルをコピーしてedit.blade.phpと名前を変更してから編集すると楽です。中身は下リストのように修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@extends('layouts.app') @section('content') <div class="container"> <h1>投稿「{{$post->title}}」の編集</h1> <form action="{{route('posts.update', $post->id)}}" method="post"> @csrf @method('patch') <div class="form-group"> <label for="title">タイトル</label> <input type="text" name="title" id="title" class="form-control" value="{{old('title') ?? $post->title}}" placeholder="タイトルを入力してください"> </div> <div class="form-group"> <label for="body">本文</label> <textarea name="body" id="body" class="form-control" rows="5" placeholder="本文を入力してください">{{old('body') ?? $post->body}}</textarea> </div> <input type="submit" value="決定" class="btn btn-primary"> <input type="reset" value="キャンセル" class="btn btn-secondary" onclick='window.history.back(-1);'> </form> </div> @endsection |
create.blade.php との違いは下図の赤線を引いたところのようになります。
差分のある場所を説明していきます。
タイトル
1 |
<h1>投稿「{{$post->title}}」の編集</h1> |
タイトルには、編集対象の投稿のタイトルを表示するようにしています。
フォームの提出先
1 |
<form action="{{route('posts.update', $post->id)}}" method="post"> |
投稿の編集の送信先は、updateアクションになります。route()関数で posts.update とルート設定で名前を付けたURLを作ります。今回は 編集する投稿の投稿IDも教えています。
通信のメソッド
1 |
@method('patch') |
updateアクション は 通信メソッドとして、PATCHを指定する必要があります。
具体的にはformタグの中はのメソッドの指定はpost を指定して、@method 構文でメソッドを指定します。
テキストボックスの初期値 タイトル
1 |
<input type="text" name="title" id="title" class="form-control" value="{{old('title') ?? $post->title}}" placeholder="タイトルを入力してください"> |
テキストボックスの初期値として、 {{old('title') ?? $post->title}} を渡しています。 old('title') は、バリデーションエラーが発生してこのページに戻ってきたとき、関数old()を使って編集中の値を読み出していました。
新しいのは、 ?? $post->title の部分です。
?? は PHP7以降で使えるようになった演算子で「null合体演算子」といいます。
?? の左側が NULL だった場合、右側に書いた値が使われます。
PHPの公式サイトに以下の説明があります。
この演算子を使うことにより、タイトルを入力するテキストボックスの初期値として、バリデーションエラーで戻ってきたら入力中の値を使い、初めて表示するときは、 $post->title、保存されていたタイトルを テキストボックスの初期値として使用するという意味になります。
テキストボックスの初期値 本文
{{old('body') ?? $post->body}} であり、タイトルと全く同じ考え方です。バリデーションエラーの時は、入力中の値を表示し、初めて表示するときは、 ?? $post->body によりデータベースに保存されている値を表示します。
ボタンのキャプション
最後の違いはボタンについている文字列です。
新規作成では投稿となっていたのを、編集の時は value="決定" と表示するようにしました。
updateアクションを実装
次に、投稿の編集フォームから決定ボタンが押された時のデータ保存処理であるupdateアクションを実装していきます。
PostsControllerにupdateメソッドを実装
まず、コントローラを実装しましょう。PostsController の updateメソッドに以下の処理を実装してください。これも似ているので、storeメソッドの中身をコピーしてきて編集すると楽できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public function update(Request $request, $id) { $request->validate([ 'title' => 'required|max:255', 'body' => 'required' ]); $post = Post::find($id); if (auth()->user()->id != $post->user_id) { return redirect(route('posts.index'))->with('error', '許可されていない操作です'); } $post->title = $request->input('title'); $post->body = $request->input('body'); $post->save(); return redirect(route('posts.index'))->with('success', 'ブログ記事を更新しました'); } |
storeメソッドの処理とupdateメソッドの処理との違いは、下図の背景が黄色の部分になります。
上図にも簡単には説明を書きましたが、PostsControllerのupdate()メソッドのコードについてstore()メソッドと違う部分を説明していきます。
URLに指定した投稿ID
1 |
public function update(Request $request, $id) |
正確には、リソースコントローラを作成したときに自動生成されたコードですが、自分で作成する場合も同じですので説明します。
ルート設定で、URL に指定した投稿IDは、アクションメソッドの引数として受け取ることができます。今回の場合、posts/{投稿ID} のようにして指定されています。
引数名としては $id として受け取っています。
編集する投稿を読み出す
1 |
$post = Post::find($id); |
投稿データを更新するため、データベースから読み出します。またこのデータは投稿者チェックにも使用します。
記事の投稿者チェック
1 2 3 |
if (auth()->user()->id != $post->user_id) { return redirect(route('posts.index'))->with('error', '許可されていない操作です'); } |
この部分は、自分の投稿した記事かどうかをチェックしています。
先ほど、edit()メソッドでも同じ処理が出てきましたね。
updateメソッドにも投稿者チェックは必要なのか?
「editメソッドでチェックしたからupdateメソッドではチェックしなくてもよいのでは?」って思ったかもしれませんね。
普通であればチェックしなくても、editメソッドでチェックしてOKとなってから、投稿の編集ページで編集して、送られてきたデータを処理するので、ここではチェック不要です。
投稿者チェックが必要な例
でも、投稿IDを改ざんして投稿の編集データを送ってくることができてしまうんです。
一例を紹介します。
chrome では ブラウザ側の検証機能が付いています。CSSやHTML、Javascriptなどブラウザで表示されているコードを変更して表示がどう変化するかチェックすることができるんです。
検証機能を使えば、formタグのaction を少し書き換えるだけで投稿IDを変えて編集データを送りつけることができてしまいます。
もし、updateメソッドで投稿IDをチェックしていなかったら、違うユーザの投稿記事の内容を改ざんできてしまいますね。
下図がchromeの検証機能です。右側のウィンドウに表示されているformタグの action に記述されている updateアクションのURLには、編集対象の投稿IDが現在編集中の投稿ID である 4 が指定されています。
これを、chromeの検証画面で変更し、例えば、違うユーザが投稿した投稿IDである3 に変更してしまうと、updateアクションには、他人の投稿記事にを変更してしまいます。
それを防ぐために、updateメソッドでも、投稿者チェックが必要なんです。
今回作成しているようなブログの投稿記事のリソースコントローラでいうと、ユーザIDのチェックが必要な処理は、editアクション、updateアクション、destroyアクションになります。
もし、表示機能においても、投稿者本人以外の本文を表示しないって仕様にすると、詳細表示であるshowメソッドも投稿者本人確認が必要となりますね。
大ざっぱにいうと、好奇心旺盛なユーザがいるので、ユーザから送られてくるデータは改ざんされているかもしれないことを念頭に置くことが大事ですね。
大事なことなので、忘れないようにしましょう。
保存するデータの準備とデータベースへの保存
1 2 3 |
$post->title = $request->input('title'); $post->body = $request->input('body'); $post->save(); |
この部分はほぼstoreと処理は同じです。
唯一違いはuser_id の操作だけです。storeの時は新規作成だったため、$post->user_id に自分のユーザIDを代入していましたが、今回は、読み出した $post レコード の user_id に自分のユーザIDが入っているため代入がありません。
$post->save() メソッドを呼び出すと、$post変数の内容がデータベースへ保存されます。$post->id が設定されている場合は、id指定で上書き保存ですね。
リダイレクト
1 |
return redirect(route('posts.index'))->with('success', 'ブログ記事を更新しました'); |
投稿者チェックでNGになった時と同様、投稿一覧ページにリダイレクトします。
storeの時もリダイレクト先は一緒ですね。メッセージが少し違って、今回は、更新しましたとのメッセージを表示します。成功したと気なので、’sucess’の名称を使ってます。
updateアクションにはビューがありません
storeアクションと同様、データ保存した後は、投稿一覧ページにリダイレクトするため、ビューの実装はありません。
詳細表示に編集ボタンを実装
投稿の編集は、投稿の詳細表示に編集ボタンを配置して、編集ボタンを押したら編集ページへ遷移するようにしましょう。
自分が投稿した記事を表示している時だけ、下図のようなボタンを付けます。
投稿一覧のビューの編集
では、作業の説明です。
投稿一覧ページのビューファイルであるposts/show.blade.php ファイルを修正しましょう。
戻るボタンの下に下リストの3行を追加します。
1 2 3 |
@if (!Auth::guest() && Auth::user()->id == $post->user_id) <a href="{{route('posts.edit', $post->id)}}" class="btn btn-primary">編集</a> @endif |
追加後は下図のようになります。赤枠が追加した部分です。
赤枠内のコードを解説します。
条件分岐
1 |
@if (!Auth::guest() && Auth::user()->id == $post->user_id) |
@if は、blade の省略記号ですが、条件分岐です。条件が成立している時だけ、@endif までのhtml が有効になり表示されます。
まずは条件分岐の中の !Auth::guest() ですが、「ゲストじゃないこと」を表します。
! は条件論理の反転ですので、 Auth::guest() の「ゲストであること」、つまり「ログインしてないこと」の反対という意味です。
そして、 && は、PHPの論理演算子で「かつ、AND」を意味します。ここまでで、「もしゲストじゃなくて、かつ、」までの意味になります。
Auth::user()->id == $post->user_id の部分は、「ログインしているユーザのID が、投稿者のユーザIDであること」を表します。
以上より、この一行は、「もし、ゲストじゃなくて、この記事の投稿者だったら @endif までを表示する」という意味になります。
編集ボタン
1 |
<a href="{{route('posts.edit', $post->id)}}" class="btn btn-primary">編集</a> |
これはそれほど問題ではないと思いますが、編集ボタンに、editアクションへのURLのリンクを付けています。
route()関数の中は、ルート設定で付けた名前 posts.edit を指定し、渡すパラメータとして投稿IDを第2引数で指定します。
/posts/投稿ID/edit のような感じのリンクが設定されますね。下図の赤枠のような感じです。
動作確認
では動作確認しましょう。
正常な投稿編集のパターン
まず、投稿詳細表示のページを表示させます。自分が投稿した記事jには、「編集」アイコンが表示されます。
クリックしましょう。
編集ページが表示されます。
編集して「決定」を押してみましょう。
編集後は投稿一覧ページに遷移します。メッセージが表示されます。
もう一度、詳細表示のページに遷移して、反映されていることを確認します。
バリデーションエラー
バリデーションエラー時のふるまいをチェックしましょう。
下のように、本文だけ書き換えつつ、タイトルを空白にして「決定」を押してみます。
すると、ちゃんとバリデーションエラーが表示され、かつ、本文は編集中の文字列がキープされています。
本文に関しては、空だったため、編集前の状態の文章に戻っています。正常動作です。
updateアクションの投稿者チェック
最後にこちらをチェックしましょう。ブラウザの検証機能を使って、updateアクションへ渡す投稿IDを無理やり他人の投稿を示すように変更します。
下図では、投稿ID = 4 の投稿を編集しているのに、formタグのactionプロパティで update に渡す投稿IDを 3に改ざんしてから、「決定」を押してみます。
すると、投稿一覧ページにリダイレクトして、エラーメッセージが表示されました。これも狙い通りの動作です。
まとめ
今回は、投稿の編集機能を実装しました。
学習した項目としては以下の通りです。
- 投稿者チェック 自分が投稿した記事であることを確認
- 自分が投稿した記事だけ、編集ボタンを表示
- 投稿の編集処理
- PHPの && と || の重要な役割 = 「左の条件の結果次第で右側の条件を実行しない」
今回も、盛りだくさんになりました。CRUDでは、残すところ投稿の削除処理だけになりました。
とりあえずのゴールはあと少しです。引き続き勉強していきましょう。
===目次===
- laravelはPHPで動くフレームワーク ~laravel入門01~
- laravelでログイン機能付きサイトを作ってみよう、作成予定のWebアプリケーションの概要と作業手順の概略~laravel入門~
- 開発環境のインストール
- XAMPP
- VS code
- Git for Windows
- SourceTree
- laravelインストール
- composerインストール
- laravel本体インストール
- laravelのUIインストール
- node.jsインストール
- アプリ作成
- データベース設定
- ログイン機能追加
- npmインストール
- css, jsコンパイル
- Topページ作成
- Aboutページ作成
- 問い合わせページ作成
- Blog機能作成
- Model作成と一緒にmigrationファイルも作る。~Laravel7入門~
- リソース コントローラを自動生成して CRUD のうち一覧表示 indexアクションを実装する ~Laravel7入門~
- リソース コントローラで、詳細表示 showアクション を実装する ~Laravel7入門~
- リソースコントローラで新規作成:create, storeアクション を実装する~Laravel7入門~
- bladeテンプレートエンジンでタグを無視させつつ、改行を表示させる方法 ~Laravel7入門~
- リソースコントローラで更新:edit, updateアクション を実装する~Laravel7入門~
- リソースコントローラで削除:destroyアクション を実装する~Laravel7入門~
- 投稿にカバー画像を追加する ~Laravel7入門~
コメント