【AWS】知識ゼロから理解するRDS超入門
AWSのデータベースサービス「Amazon RDS」を初心者にもわかるように解説します。未経験には難しいMultiAZ構成やレプリケーションは、マスター/スレ...
2018-04-11 23:51 2018-04-23 12:09
MIYABI Labでは、技術ブログの編集フォームをLaravel5で製作して利用しています。
今回、WordPressなどではお馴染みのプレビュー機能をつけ忘れていたので早速実装してみたのですが、せっかくなので、プレビュー機能を実装した編集フォームの作り方としてセットで解説します。
全体の処理がどのような流れで進んでいくのかを図を用いてしっかりと理解した上で、Laravel5におけるルーティングやコントローラー設計で押さえておきたいポイント、そしてプレビューボタンを押した際に実行される動的な処理についても、JavascriptとjQueryでの2通りの方法でご紹介します。
まずは完成したページを先に紹介します。
こちらは、実際にMIYABI Labで記事を執筆している編集ページになります。
構造としてはとてもシンプルで、最下部の更新ボタンをクリックするとバリデーションチェックを経てデータベースに保存され、プレビューボタンを押すと別タブでプレビュー画面を表示します。
利用する言語・フレームワーク・ライブラリ等は以下の通りです。
Laravelなどのフレームワークを用いてサイトを製作している場合、機能ごとにファイルを細分化されているため、それぞれのファイルが何の役割を担っているかをしっかりと理解しておくことが大切です。
まずは基本的な処理として、フォームからPOST送信されたデータをバリデーション処理してデータベース(今回はMySQL)に保存するまでの流れを確認しておきます。
実際のものとはファイル名や中身を少し編集しています。
ルーティング
コントローラー
フォームリクエスト
モデル
ビュー
ユーザーがこの技術ブログにアクセスした時、上述したどのファイルを呼び出し、どのような流れで処理が進むのかを図で示すと以下のようになります。
以下でご紹介するプログラム等は、実際に運用しているものよりもかなり簡潔に書いてます。細かい処理は端折っているので、コピペだけでは動きませんが、イメージを掴んで自分なりにアレンジしてみてください。
(※コントローラー名やメソッド名は架空のものに置き換えています)
// この技術ブログ記事へのルート
Route::get('/blog/{id}', 'BlogController@blog')->where('id', '[1-9][0-9]*');
// 記事を保存するコントローラーへのルート
Route::post('/EDIT-PATH/store/{id}', 'EditController@store')->where('id', '[1-9][0-9]*');
※1:
※2:
※3:
class EditController extends Controller
{
public function store($id, EditRequest $request)
{
// ※1
if(is_numeric($id)){
$edit = Edit::find($id);
}else{
return redirect()->back()->withInput($request->input());
}
// ※2
foreach($edit->getAttributes() as $column => $value){
if( isset($request[$column]) ){
// array -> comma implode
if( is_array($request[$column]) ){
$request[$column] = implode(',', $request[$column]);
}
// set
$edit[$column] = $request[$column];
}
}
// ※3
// 保存する
if( $edit->save() ){ // 戻り値はbool(保存の結果)
return redirect()->action("EditController@edit", ['id' => $edit->id])->with('success-msg', 'SUCCESS:データを保存しました!');
}else{
return redirect()->back()->withInput($request->Input())->withErrors(['予期せぬエラーが発生しました!', '保存が失敗した可能性があります。念のためローカルにデータをコピーしておいてください。']);
}
}
}
バリデーションルールはここに記載します。
サポートされている使用可能なバリデーションルールは公式を参照してください。
class EditRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'user_id' => 'required',
'title' => 'required',
/*~~~~~
省略
~~~~~*/
];
}
public function attributes() {
return [
'user_id' => '執筆者',
'title' => 'タイトル',
/*~~~~~
省略
~~~~~*/
];
}
}
Eloquentを利用しているためEditモデルはとても簡潔です。
$guardedや$fillableといったプロパティの仕様については、リンク先の複数代入の項目を参照してください。
class Edit extends Model
{
protected $guarded = [
'id',
'created_at',
'updated_at',
];
public function User()
{
return $this->belongsTo(User::class);
}
}
ここではHTMLフォームの一部のみご紹介します。プレビューの際は、このフォームのaction属性などをJavascriptやjQueryから操作します。
ちなみに、formのaction属性の数値が12になっていますが、この記事のIDです。Laravel側の出力の際はもちろん動的でお願いします。
また、action属性をフルパスで入れている理由としては、Laravelのbladeテンプレートにて {!! Form::open() !!} を利用すると、自動的にhttp(セキュアではない)で始まるURLに変換されてしまうため、わざわざhttpsで書いてます(大した数ではないので関数作ってないです)。
<form method="POST" action="https://miyabi-lab.space/EDIT-PATH/store/12" accept-charset="UTF-8" id="admin-blog-form" class="form-horizontal" enctype="multipart/form-data">
<div class="form-group">
<label for="user_id" class="col-sm-2 control-label">執筆者</label>
<div class="col-sm-2">
<select class="form-control" id="user_id" name="user_id">
<option value="1" selected="selected">加藤雅大</option>
<option value="2">湯座丞太郎</option>
<option value="3">...</option>
</select>
</div>
</div>
<div class="form-group">
<label for="title" class="col-sm-2 control-label">タイトル</label>
<div class="col-sm-8">
<input class="form-control" id="title" name="title" type="text" value="タイトル">
</div>
</div>
<!-- ~~~
省略
~~~ -->
<input name="_token" type="hidden" value="CSRF-TOKEN">
<div class="form-group">
<div class="col-sm-8 col-sm-offset-2 text-center">
<input class="btn btn-primary" type="submit" value="更新する">
</div>
</div>
</form>
ちなみに、Laravel5のフォームファサードを利用して書くと以下のようになります。
CSRFトークンについては、{!! Form::open() !!}を呼び出すことで自動的に挿入されるため、{{ Form::token() }} を敢えて呼び出す必要はありません。
{!! Form::open(['url' => "https://miyabi-lab.space/EDIT-PATH/store/{$edit->id}", 'id'=> 'admin-blog-form', 'class' => 'form-horizontal', 'files' => true]) !!}
<div class="form-group{{ $errors->has('user_id') ? ' has-error' : '' }}">
{!! Form::label('user_id', '執筆者', ['class' => 'col-sm-2 control-label']) !!}
<div class="col-sm-2">
{!! Form::select('user_id',$users, @$edit->user->id, ['class' => 'form-control']) !!}
</div>
</div>
<div class="form-group{{ $errors->has('title') ? ' has-error' : '' }}">
{!! Form::label('title', 'タイトル', ['class' => 'col-sm-2 control-label']) !!}
<div class="col-sm-8">
{!! Form::text('title', $edit->title, ['class' => 'form-control']) !!}
</div>
</div>
<!-- ~~~
省略
~~~ -->
<div class="form-group">
<div class="col-sm-8 col-sm-offset-2 text-center">
{!! Form::submit('更新する', ['class' => 'btn btn-primary']) !!}
{!! Form::submit('プレビュー', ['id' => 'admin-blog-preview-btn', 'class' => 'btn btn-warning']) !!}
</div>
</div>
{!! Form::close() !!}
その他はこのブログのソースコードとほぼ同じです。
また、フォームファサードはデフォルトでは利用できないので、composerで追加してあげる必要があります。初回のみ以下の作業を行いましょう。
Laravelプロジェクトのルートディレクトリにて、コマンドラインから以下のコマンドを叩きます。
$ composer require laravelcollective/html
完了したら、APPPATH/config/app.php の2つの配列に以下の行をそれぞれ追加します。
return [
'providers' => [
// 【追加】
Collective\Html\HtmlServiceProvider::class,
],
'aliases' => [
// 【追加】
'Form' => Collective\Html\FormFacade::class,
'Html' => Collective\Html\HtmlFacade::class,
],
]
これでbladeテンプレート内で{!! Form::open() !!}などが利用できます。
このケースで遭遇することはないかと思いますが、もしクラスを読み込めないなどのエラーが発生した場合には、以下のコマンドを叩いて autoload_classmap.php ファイルを更新してください。
$ composer dump-autoload
次に、完成イメージをコードやプログラムに落とし込むために、プレビューボタンをクリックしてからの処理を時系列順に並べてみます。
プレビューを作成する方法はいくつかありますが、今回はJavascriptやjQueryを用いてフォームタグのアクションを変更し、データのPOST送信先を別のURLにする(厳密には別のコントローラーで処理させる)、というシンプルかつ汎用性の高い手法をとります。
こちらが先ほどの「更新ボタンを押したときの流れ」です。
そしてこちらが「プレビューボタンを押したときの流れ」です。
更新処理の流れと比較すると、更新とプレビューでは異なるコントローラーを利用していることがわかります。
プレビュー用のコントローラーが返すビューテンプレートは、実際のブログで利用しているもの(blog.blade.php)をそのまま利用しています。
また、プレビューでは実際にデータの更新を行うことはないのでモデルは利用しません。モデルから取得するデータの代わりに、POST送信したフォームデータをそのまま変数に格納し、必要であれば名前を揃え、ビューで用意してある変数に当てはめます。
ここまでイメージできれば、あとはこれをプログラムに落とし込むだけです。
今回のケースに限らず、全体の処理をイメージしたり、上手く理解できない場合には絵を描いてみるのも非常に効果的です。
それでは先ほどのファイルにプログラムを追記していきましょう。実際には書くことはほとんどありません。
// この技術ブログ記事へのルート
Route::get('/blog/{id}', 'BlogController@blog')->where('id', '[1-9][0-9]*');
// 記事を保存するコントローラーへのルート
Route::post('/EDIT-PATH/store/{id}', 'EditController@store')->where('id', '[1-9][0-9]*');
// 【追記】 記事をプレビューするコントローラーへのルート
Route::post('/EDIT-PATH/preview/{id}', 'EditController@preview')->where('id', '[1-9][0-9]*');
ビューテンプレートを使い回したことによって、本来であれば埋まるはずの変数が定義されていない、などのエラーが生じる可能性があります。
その際、大抵の場合はあまりプレビューに関係のない変数ばかりなので(必要な情報はPOST送信されているので)、状況に応じて対応する変数に応じてboolや数値、"表示されません"などの文字列を入れてしまえば通ります。
私の場合、著者名を取得する方法をEloquentのリレーションに頼っており、モデルを使用しないプレビューでは Edit::with("user") のようなことができないため、$edit->user->name には"非表示"という文字列を後から与えて調整してます。
どうしても著者名を表示したい場合は、Editコントローラーのプレビュー用メソッドにてリクエストを受け取り、送られてきたユーザーIDを利用して $edit->user->name = User::find($request->user_id) のようなことをしてあげれば取りに行けます。(その際は、$edit->user をnew stdClass() として定義しないと変数入れられない気がします)
ちょっとしたことですが、頼まれごとのシステムの場合はこちらの方が親切ですね。
class EditController extends Controller
{
// 注意!実際のプログラムよりもかなり簡単に書いてます!
// 細かい処理は端折っているので、イメージだけ掴んでください。
public function store($id, EditRequest $request)
{
/*~~~~~
省略
~~~~~*/
}
// 【追記】
public function preview($id, EditRequest $request)
{
$edit = $request;
if( is_null($edit) ) abort(404);
// blog.blade.php ビューテンプレートでエラーが出る部分を簡易的に補填(重要では無いので)
$edit->user = new \stdClass();
$edit->user->name = "非表示";
$edit->user->nickname = "非表示";
// blog.blade.php 使い回し
return view('blog', compact('edit', 'etc..'));
}
}
<form method="POST" action="https://miyabi-lab.space/EDIT-PATH/store/12" accept-charset="UTF-8" id="admin-blog-form" class="form-horizontal" enctype="multipart/form-data">
<!-- ~~~
省略
~~~ -->
<input name="_token" type="hidden" value="CSRF-TOKEN">
<div class="form-group">
<div class="col-sm-8 col-sm-offset-2 text-center">
<input class="btn btn-primary" type="submit" value="更新する">
<!-- ここにプレビューボタンを追記 -->
<input id="admin-blog-preview-btn" class="btn btn-warning" type="submit" value="プレビュー">
</div>
</div>
</form>
ファイルへの追記は以上で終わりです。
実際に自分好みにカスタマイズしていくとどんどんプログラムが増えていくのですが、絶対に外せない部分だけに絞ると非常にシンプルですね。
さあ、プレビューボタンを設置したのでクリックしてみよう!と言いたいところですが、これだけでは思った通りの動作をしてくれません。
現状では input type="submit" ボタンが2つある状態で、どちらをクリックしてもPOSTデータはaction属性に指定されたURLへと送信されます。
ではどうすれば上手く対応できるのでしょうか。もう少し具体的に処理の流れを考えてみましょう。
全ての処理のスタートはプレビューボタンのクリックです。
要となる処理としては、JavascriptやjQueryを利用してクリックイベントを検知し、本来の送信処理を一時的に中断させ、action属性のURLを書き換えた上で再度送信することで、POSTデータをプレビュー用メソッドに渡すことができます。
しかしそれだけだと、今書いている記事ページのタブ内で遷移してしまうため、プレビュー画面は別タブで開く必要があります。
フォーム送信結果を別タブで表示する場合、aタグと同様にformタグに直接 target="_blank" を付け加えるだけでOKです。
また、無事に別タブでプレビュー画面が開いた後にも注意が必要です。動的に変更したactionやtargetはそのまま残るので、今度は更新ボタンを押した際にもプレビューが開いてしまうので、一度開いたら属性を元に戻してあげる必要があります。
これらの流れをまとめると以下のようになります。
イメージできましたでしょうか?それではプログラミングしていきます。
僕はjQueryが好きなのですが、反対派もいると思うので念のため両方書いておきます。
Ajaxなどを利用しない限りそこまで大差ないかなという印象です。
var prevBtn = document.getElementById("admin-blog-preview-btn");
prevBtn.addEventListener("click", function(e){
// stop sending
e.preventDefault();
// set variables
var blogForm = document.getElementById("admin-blog-form");
var defaultAction = blogForm.getAttribute("action");
var previewAction = defaultAction.replace("/store/", "/preview/");
// rewrite action & submit
blogForm.setAttribute("action", previewAction);
blogForm.setAttribute("target", "_blank");
blogForm.submit();
// reset
blogForm.setAttribute("action", defaultAction);
blogForm.removeAttribute("target");
});
この程度ではほとんど違いはありませんが、やっぱりこちらの方が直感的で好きです。チェーンメソッドのあたりが単調にならなくていいですね。
当然ですが、jQueryはライブラリを読み込まないと動きません。
// change temporary action by jQuery
var prevBtn = $("#admin-blog-preview-btn");
prevBtn.on("click", function(e){
// stop sending
e.preventDefault();
// set variables
var blogForm = $("form#admin-blog-form");
var defaultAction = blogForm.attr("action");
var previewAction = defaultAction.replace("/store/", "/preview/");
// rewrite action & submit
blogForm.attr("action", previewAction)
.attr("target", "_blank")
.submit();
// reset
blogForm.attr("action", defaultAction)
.removeAttr("target");
});
記述したスクリプトが正常に動くか試してみましょう。
actionとtargetはすぐに切り替わってしまうため、目視で変化が起こっているか確認するためには、後半の // reset 以下をコメントアウトして実行するとよく分かるかと思います。
ちゃんと表示されました。
投稿時間・更新時間のところがバグっていますが、記事のデータそのものはPOSTデータを利用しており、MySQLを参照していないのでUNIXタイムスタンプ=0の東京時間(+09:00)が表示されています。
プレビューボタンを押した場合でもバリデーションが適用されるため、例えばコンテンツが空っぽのままプレビューを表示しようとすると、バリデーションエラーが表示された同じ編集画面が別タブで表示されてしまいます。
何か問題があるかと言われれば何もないのですが、同じ記事の編集画面が別タブで2ページできてしまいどちらが最新かわかりづらくなってしまったり、CSRFトークンによる送信エラーの懸念などもあるので、対策をしないのであればエラー付きの新しいタブはすぐに消してしまうのが良いかもしれません。
製作時間よりもこの記事執筆に掛けた時間の方が圧倒的に長かったです(笑)
細かく書いているため記事が長くなってしまいましたが、実際にプレビュー機能を実装するのはそこまで難しい課題ではありません。
むしろ、HTMLフォームやPHP(Laravel5)、Javascriptの基礎を応用したちょうどいい勉強材料だと感じました。
これから脱初心者にチャレンジする方にとっては、広い知識を融合しながらWeb製作を行う良い足掛かりになりそうです。
また別の機能を実装したら適宜まとめていきます。
Webエンジニア PHPエンジニア HTML CSS JS jQuery PHP Laravel Python SQL WordPress AWS Linux Apache
【名前】 "みやび"
【関連】 株式会社PLAN / MIYABI Lab / JAPAN MENSA /
【MIYABI Lab】平日オフィスを勉強用に解放中!みんなで楽しくプログラミングを学べる環境を作る!詳しくはコチラ(https://miyabi-lab.space)◆HTML, CSS, JS, PHP, Python, SQL, AWS◆生物学系修士→製薬会社→Webエンジニア(株式会社PLAN)・MENSA会員
【AWS】知識ゼロから理解するRDS超入門
AWSのデータベースサービス「Amazon RDS」を初心者にもわかるように解説します。未経験には難しいMultiAZ構成やレプリケーションは、マスター/スレ...
【AWS】Auto Scalingする前に知っておくべき7つのこと
Amazon EC2 Auto Scaling(オートスケール)を使用すると、CPU使用率等に応じてEC2の台数を自動的に増減できます。ここでは、初心者の持つ疑問を通し...
WordPressの基本構造を理解してオリジナルテーマを作ろう(後半)
WordPressのテーマを自作するために必要なテンプレートファイル(functions.phpやstyle.css)の役割やファイル構造を理解して、どのようにオリジナ...
WordPressの基本構造を理解してオリジナルテーマを作ろう(前半)
WordPressのテーマを自作するために必要なテンプレートファイル(functions.phpやfront-page.php)の役割やファイル構造を理解して、どのようにオ...
【Heroku入門】無料枠サーバーを24時間スリープさせない方法
フリープランのHerokuサーバーでは、30分以上アクセスがないと自動的にスリープしてしまいます。ここでは、Herokuサーバーを寝かせない方法につい...
【入門編】Laravelのディレクトリ構造とMVCの処理の流れを理解する
Laravel初心者が学習する際にわかりにくいLaravelのディレクトリ構造を具体的な例を交えて解説します。MVCの基本であるビュー、モデル、コントロー...
【初心者向け】PythonによるHeroku環境で簡単LINEBot開発
誰でも簡単にLINEBotをpythonを使ってHeroku環境で開発できる方法を解説します。ここでは、LINE Messaging APIを用いることでおうむ返しをするBot...
AWSでWebサーバー構築!踏み台サーバーでセキュアなネットワークを構築する(第5回)
連載の第5回です。メインEC2に対して直接SSH接続できる状態というのは、セキュリティの観点からあまり望ましくありません。MySQLやEBSが紐づいたメ...
AnacondaでのTensorFlow環境構築と基礎的な使い方
Anaconda(アナコンダ)のインストールからJupyter notebook(ジュピターノートブック)とTensorFlow(テンサーフロー)の基本的な使い方を初心者...
脱初心者!MNIST beginnerに隠れ層を加えたニューラルネット解説
TensorFlowのチュートリアルであるMNIST beginnerの応用して、隠れ層と活性化関数を加えたニューラルネットワークで手書き文字識別を解説します。...
AWSでWebサーバー構築!Apache2.4, PHP7, MySQLの導入と初期設定(第4回)
連載の第4回です。今回は作成したEC2インスタンスにWebサーバーとしての機能を持たせるため、Apache2.4のインストールおよびhttpd.conf等の各種設...
AWSでWebサーバー構築!EC2を作成してSSH接続する(第3回)
連載の第3回です。前回作成したVPC・サブネットにおいて、セキュリティーグループに保護されたEC2インスタンスの作成・設定およびSSH接続の確立ま...
初心者必読!MNIST実行環境の準備から手書き文字識別までを徹底解説!
Pythonによる機械学習をプログラミング初心者にもわかりやすいように、TensorFlowチュートリアルのMNIST beginnerを使って、手書き文字(MNIST)識別...
AWSでWebサーバー構築!専門用語の解説とVPC環境を構築する手順(第2回)
連載の第2回です。AWSにてVPCネットワークを構築してWebサーバーを設置・運用するためには、AWS内で利用される重要単語について正しく理解しておく...
知識ゼロで機械学習・AIを理解するために必要なニューラルネットワークの基礎知識
機械学習・AIを理解するために必要なニューラルネットワークの基礎について、これから機械学習を勉強したい人、プログラミング未経験の人にもわか...
Canvaで簡単におしゃれなアイキャッチ画像を作ろう!使い方を徹底解説!
PhotoshopやIllustratorを使えなくてもCanvaなら誰でも簡単におしゃれなアイキャッチ画像が作れます。豊富なデザインテンプレートを組み合わせるだ...
プレビュー機能付きの記事編集画面の作り方(Laravel5)
記事編集フォームにはプレビュー機能の実装が必須です。記事を保存する処理とプレビューを表示する処理を共存させるにあたり、ボタンをクリックし...
AWSでWebサーバー構築!VPC設計に必要なIPアドレスとサブネットの基礎知識(第1回)
連載の第1回です。AWSのVPCネットワークを設計して実際に構築するためには、IPアドレスの基礎を理解することが非常に大切です。EC2 Webサーバーを...
MIYABI Labホームページを製作しました
様々な理由でプログラミングの勉強を困難だと感じてしまっている方のお役に立てれば嬉しいです。これからも小さなWebサービスを作り続けていき、技...
ゆざ、株式会社PLANを卒業します。
2年間インターンとしてお世話になった株式会社PLANを卒業します。AWS,Laravel,Pythonなどの技術的なことだけではなく、エンジニアとして、社会人と...