制作環境
Windows 10
Laravel : 6.18.35
Laravel/ui : 1.0
Laravel-mix : 5.0.1
Bootstrap : 4.0.0
Vue.js : 2.5.17
XAMPP
PHP : 7.4.3
Visual Studio Code
はじめに
この記事はプログラミングをはじめたばかりの素人が、できたことをメモするのに利用しています。
内容には誤りがあるかもしれません。
買い物かごを作成する際にうまくいかなかったので、改善策を見つけるのに作成した小規模プログラムです。
とりあえず完成した物を先に紹介し、後ほどうまくいかず試行錯誤した点を記載したいと思います。
機能実装が目的のため、デザイン(見た目)にはあまりこだわっていません。
また、記述も必要最低限にしています。
一部デザインの整形にBootstrapを使用しております。
Bootstrap、Vue.jsも含め、Laravel-mixを使用して記述してます。
作成要件
- ユーザーの切り替えができる(ログイン機能を付けないので、手動でユーザーを切り替える仕様にします)。
- 商品の情報を取得し、一覧表示する。
- 商品の購入数を指定し、非同期通信でカートに追加できる。
- カートに入っている商品点数を、非同期通信で表示する。
- カートに追加する際はバリデーションを行い、エラーがあればメッセージを表示させる。
- カートへの登録が完了したら、購入数は空に戻す。
- ユーザーを切り替えるとカートに入っている商品点数も、非同期通信で変更される。
イメージ
それでは、作成していきましょう。
マイグレーションファイルの作成
データベースへテーブルを作成したいと思います。
今回作成するのは、商品を管理するproducts_table
とカート用のcarts_table
です。
プロジェクトのディレクトリでターミナルを起動し、以下を実行してください。
php artisan make:migration create_products_table
続けて
php artisan make:migration create_carts_table
作成が完了したら、database>migrations
内にある作成されたファイルを開き、以下のように記述します。
public function up()
{
Schema::create('products', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name')->comment('商品名');
$table->string('price')->comment('価格');
$table->timestamps();
});
}
public function up()
{
Schema::create('carts', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedTinyInteger('user_id')->comment('ユーザーID');
$table->unsignedBigInteger('product_id')->comment('商品ID');
$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
$table->string('quantity')->comment('購入数');
$table->timestamps();
});
}
モデルの作成
次にモデルを作成していきます。
モデルはModels
フォルダの中に作成していきます。
ターミナルを起動し、以下を実行してください。
php artisan make:model Models/Product
続けて
php artisan make:model Models/Cart
作成されたファイルはapp>Models
の中にあります。
各ファイルを以下のように記述してください。
// リレーションのため追記
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Product extends Model
{
// 変更を許可するカラムを指定します
protected $fillable = [
'name', 'price'
];
// リレーションのためのメソッドです
// これでカートの情報がProduct側から取得できます
public function cart()
{
// 紐付けるモデルを指定し返します
return $this->belongsTo('App\Models\Cart');
}
}
// リレーションのため追記
use Illuminate\Database\Eloquent\Relations\HasMany;
class Cart extends Model
{
protected $fillable = [
'user_id', 'product_id', 'quantity'
];
public function product()
{
return $this->hasMany('App\Models\Product');
}
}
ビューの作成
resources>views
内に新しくproduct.blade.php
を作成し、以下のように記述します。
ちなみに、Vueテンプレートは一切使用していません。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="csrf-token" content="{{ csrf_token() }}">
<link rel="stylesheet" href="{{ mix('css/app.css') }}">
<title>商品一覧</title>
<style>
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<div id="app" v-cloak>
<div class="container">
<div class="row mt-2">
<label>ユーザー選択
<select name="user" id="user" v-model="user">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select></label>
<p class="cart_text ml-auto">◆カートの中身(@{{ items }})</p>
</div>
<h1>商品一覧</h1>
<p class="err_msg text-danger">@{{ errors.quantity }}</p>
<div class="row justify-content-center">
@foreach ($productTable as $product)
<div class="product p-3 border border-success col-3">
<h3>商品名</h3>
<p>{{ $product->name }}</p>
<h3>価格</h3>
<p>{{ $product->price }}円</p>
<form id="form{{ $product->id }}">
@csrf
<label>購入数: <input type="text" name="quantity" size="2">個</label>
<br>
<input type="hidden" name="product_id" value="{{ $product->id }}">
<input type="hidden" name="user_id" v-model="user">
<button type="button" @click="addCart({{ $product->id }})">カートに追加</button>
</form>
</div>
@endforeach
</div>
</div>
</div>
<script src="{{ mix('js/app.js') }}"></script>
</body>
</html>
ポイント
{{ $product->name }}
のように表示されているのはblade
側の構文で
@{{ items }}
のように@
が先頭についているのはVue側の構文です。
<style>
[v-cloak] {
display: none;
}
</style>
ここで設定しているスタイルは、Vue側の構文を表示する際に一瞬{{ }}が表示されるのを防ぐためのものです。
フォームリクエストの作成
バリデーションはフォームリクエストで行うようにします。
ターミナルで以下を実行して下さい。
php artisan make:request CartRequest
app>Http>Requests
内にファイルが作成されるので、開いて以下のように記述します。
class CartRequest extends FormRequest
{
public function authorize()
{
// 今回認証は行わないのでtrueにします
return true;
}
// 正規表現で正の整数だけパスするようにしてます
public function rules()
{
return [
'quantity' => 'regex:/^\d+$/'
];
}
public function messages()
{
return [
'quantity.regex' => '購入数は正の整数を入力してください'
];
}
}
ポイント
今回は非同期通信の中でフォームリクエストを使用しバリデーションを行いますが、通常のバリデーション処理と違い注意点があります。
通常であれば自動的に元のページへのリダイレクトレスポンスが作成され、エラーメッセージもフラッシュメッセージとしてセッションに保存されますが、非同期通信の場合はJSONが返されるだけで、レスポンスの作成は行われず、リダイレクトもしません。
コントローラの作成
ターミナルで以下を実行します。
php artisan make:controller ProductController
app>Http>Controllers
内にファイルが作成されるので、開いて以下のように記述します。
// モデル利用のため追記
use App\Models\Product;
use App\Models\Cart;
// フォームリクエスト使用のため追記
use App\Http\Requests\CartRequest;
class ProductController extends Controller
{
public function index()
{
// 商品情報を全て取得します
$productTable = Product::all();
// 取得した内容をビューに渡します
return view('product', compact('productTable'));
}
// カートに商品を追加するメソッドです
public function add_cart(CartRequest $request)
{
// フォームリクエストを通過したリクエストの値を全て$formに代入します
$form = $request->all();
// 不要な項目を削除します
unset($form['_token']);
// Cartモデルをインスタンス化(実体化)します
$cartTable = new Cart;
// 登録する値を各項目に一気に代入します
$cartTable->fill($form);
// Cartテーブルにデータを保存します
$cartTable->save();
// カートからユーザーIDが同じ物だけ抽出して数をカウントします
$cart = $cartTable->where('user_id', $request->user_id)->count();
// カウントした数を返します
return $cart;
}
// カートの商品点数をカウントするメソッドです
public function get_total(Request $request)
{
// カートからユーザーIDが同じ物だけ抽出して数をカウントして返します
$cart = Cart::where('user_id', $request->user_id)->count();
return $cart;
}
}
ルーティングの作成
routes
内のweb.php
を開いて以下のように記述します。
Route::get('/product', 'ProductController@index')->name('product');
Route::post('/ajax/product', 'ProductController@add_cart')->name('add_cart');
Route::get('/ajax/product', 'ProductController@get_total')->name('cart_total');
Vue.jsの作成
resources>js
内のapp.js
を開き、下の方を以下のように記述します。
// Vueをインスタンス化(実体化)しappに代入します
const app = new Vue({
// Vueを使用する範囲(仮想DOM)を指定します
el: '#app',
// 初期値で渡す値を設定します
data() {
return {
// 現在選択されているユーザーです
user: '',
// カートの商品点数です
items: '',
// バリデーションのエラーメッセージです
errors: {},
}
},
methods: {
// カートに商品を非同期通信で追加するメソッドです
addCart(id) {
// アクセス先のURLを作成しurlに代入します
let url = '/ajax/product'
// アクセス先に送信するデータをparamsに代入します
let params = $('#form' + id).serialize()
// thisが使えなくなるのでthatに代入し使えるようにします
let that = this
// エラーメッセージを初期化します
that.errors = {}
// axiosで非同期通信を開始します
axios.post(url, params)
// thenで通信成功時の処理を記載します
// コントローラからの返り値がresに代入されます
.then(res => {
// コントローラからの返り値(商品点数)をitemsに代入します
that.items = res.data
// 購入数の値を空に戻します
$('#form' + that.user)[0].reset()
// catchで通信失敗又はバリデーションエラー時の処理を記載します
// フォームリクエストからの返り値がerrorに代入されます
}).catch(error => {
// ここで使用する変数errorsを定義します
var errors = {}
// for...in分でキーの数だけ処理を繰り返します
for (var key in error.response.data.errors) {
// errorsにキーと値を代入します
errors[key] = error.response.data.errors[key].join()
}
// errorsに抽出したエラーメッセージを代入します
that.errors = errors
})
},
},
// watchで値の変更の監視を行います
watch: {
// userの値が変更された(ユーザーを切り替えた)時の処理です
user: function() {
// アクセス先のURLを作成しurlに代入します
let url = '/ajax/product?user_id=' + this.user
// thisが使えなくなるのでthatに代入し使えるようにします
let that = this
// エラーメッセージを初期化します
that.errors = {}
// axiosで非同期通信を開始します
axios.get(url)
.then(res => {
// resで受け取ったコントローラの返り値(商品点数)をitemsに代入します
that.items = res.data
})
}
}
})
ポイント
errors[key] = error.response.data.errors[key].join()
最後の.join()が抜けると、エラーメッセージの表示が
購入数は正の整数を入力してください
ではなく、
["購入数は正の整数を入力してください"]
と、余計なものが表示されます。
コンパイル
ここまで記述したら、最後にコンパイルを行います。
ターミナルで以下を実行してください。
npm run dev 又は npm run watch-poll
動作確認
/product
にアクセスし、動作を確認してみてください。
要件が全て満たされていたら成功です。
作成時にハマった点
ユーザー切り替え時のカートの点数の取得
最初Vueには以下のように記述を書いていました。
beforeUpdate() {
let url = '/ajax/product?user_id=' + this.user
let that = this
axios.get(url)
.then(res => {
that.items = res.data
})
},
この記述だと、ユーザー変更時にカート内の商品点数を取得してはくれるのですが、同じ処理が2回行われてしまいます。
先ず、ユーザーが切り替わったことで変更とみなされ、処理が走ります(1回目)。
次に、items
に値が入ることで変更とみなされ、処理が走ります(2回目)。
2回目の後に再度items
に値が代入されますが、値が全く同じなので、変更とみなされず処理は走りません。
最終的にwatchを使用し、監視する項目を指定することでうまくいきました。
バリデーションのエラーメッセージの取得
これが一番ハマリました。
最初は@errorディレクティブを使用し、エラーメッセージを表示するようビューに記載していたのですが、非同期通信の場合リダイレクト処理は行われないので、通常フラッシュメッセージとしてセッションに保存されるエラーメッセージが受け取れませんでした。
改善策としてセッションに手動でエラーメッセージを保存しようと試みましたが、まず非同期通信の為ページが更新されないので、セッションに保存したところで反映されませんでした。
一部のDOMだけを更新させることも考えましたが、思う通りにできる記述方法を見つけることができませんでした。
また、色々名称等を試しましたが、セッションにどういうキーで、どうい形で、どんな値が保存されているのかわからず、@errorディレクティブを動かすことができませんでした。
最終的に@errorディレクティブの使用は諦めました。
Vueでのバリデーションのエラーメッセージの取得
最初エラーメッセージを表示させて際、ポイントで記載していますが、余計なものが表示されてしまいました。
["購入数は正の整数を入力してください"]
[""]が不要です。
console.logで受け取ったデータを確認したところ、["購入数は正の整数を入力してください"]この表示の他に、[0]で購入数は正の整数を入力してくださいという値があるのがわかりました。
そこで、以下のように記述を変更しうまくいきました。
errors[key] = error.response.data.errors[key][0]
うまくいきはしたのですが、どうしても[0]がの記述が気になりました。
今回バリデーションルールが1つしかないからいいものの、複数の場合に大変そうで、更に色々探して最終的に掲載しているjoin()
を使うかたちにしました。
カートの件数のカウント
これも結構ハマリました。
以下うまくいかなかったコントローラの記述です。
$cart = $cartTable->find($request->user_id)->count();
カートの点数が常に1になります。
find()
では1件の値しか取れていないようです。
$cart = $cartTable->find($request->user_id)->get()->count();
テーブルのレコード全件がカウントされてしまいます。
find()
で抽出しても、get()
がくると全部抽出されてしまいます。
最終的にwhereで条件を指定しうまくいきました。
個人的にはfind()
でいけると思ったのですが・・・