laravel8とstripeでクレジットカード登録して決済してみた
前回の続き
laravel8でログインしたユーザにのみファイルをダウンロードさせる。ついでに購入処理(NOT決済処理)も入れてみた。
参考URL
https://arrown-blog.com/php-laravel-stripe-creditcard/
前提知識
1, stripeとはクレカ決済会社。決済手数料3.8%(端数切り上げ)
2, クレカ登録を行うと、stripe用の顧客IDが割り振られるのでuserテーブルに登録して、以後の決済はそのidで行う
3, stripeの管理画面では、クレカ情報はブランド名(visaとかmasterとか)・下4桁・有効期限のみ見られる
4, stripe管理画面から、顧客ID(stripe_id)の削除とか返金処理が出来るので、いきなり本番環境で本物のクレカを使っても問題なさげ。
1, stripe用パッケージのインストール
1 |
composer require stripe/stripe-php |
2, usersテーブルにstripe_idカラムを追加(クレカ情報はstripe側が保持)
1 |
$table->string('stripe_id')->unique()->nullable()->comment('クレカ情報はstripe側が保持'); |
3 .envファイルにstripe管理画面から取得したキーを記述(本番用は一度しか表示されないので注意!)
参考サイトは古いせいか微妙に違ってた!
STRIPE_PUBLIC_KEY -> STRIPE_KEY
STRIPE_SECRET_KEY -> STRIPE_SECRET
1 2 |
STRIPE_KEY= STRIPE_SECRET= |
4, config/payment.phpを作っておく。新旧の変数名の変更に注意!
1 2 3 4 5 |
<?php return [ 'stripe_key' => env('STRIPE_KEY'), 'stripe_secret' => env('STRIPE_SECRET'), ]; |
5, payment(クレカ)関係を作っておく
1 |
php artisan make:model Payment --all |
6. routes/web.php
1 2 3 4 5 6 |
use App\Http\Controllers\PaymentController; // ログインした一般ユーザ Route::group(['prefix' => '/user', 'middleware' => ['auth']], function () { // クレカ関連 Route::resource('/payment', PaymentController::class); |
7, クレカ登録画面を作る
App\Http\Controllers\PaymentController;
1 2 3 4 |
public function index() { return view('payment.form'); } |
resources/views/payment/form.blade.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
<x-app-layout> <x-slot name="header"> <h2 class="font-semibold text-xl text-gray-800 leading-tight"> クレジットカード登録 </h2> </x-slot> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg"> <div class="p-6 bg-white border-b border-gray-200"> @if (session('success')) <div class="mt-3 list-disc list-inside text-sm text-green-600"> {{ session('success') }} </div> @else @if(!empty(Auth::User()->stripe_id)) <b> すでにクレジットカードは登録されています。</br> 上書きしたい場合のみ再登録して下さい。</br> </b> @endif @endif @if (count($errors) > 0) <div> <div class="font-medium text-red-600"> {{ __('Whoops! Something went wrong.') }} </div> <ul class="mt-3 list-disc list-inside text-sm text-red-600"> @foreach ($errors->all() as $error) <li>{{ $error }}</li> @endforeach </ul> </div> @endif <form action="{{route('payment.store')}}" class="card-form" id="form_payment" method="POST"> @csrf <label for="name">カード番号</label> <div id="cardNumber"></div> <div class="form-group"> <label for="name">セキュリティコード</label> <div id="securityCode"></div> </div> <div class="form-group"> <label for="name">有効期限</label> <div id="expiration"></div> </div> <div class="form-group"> <label for="name">カード名義</label> <input type="text" name="cardName" id="cardName" class="form-control" value="" placeholder="カード名義を入力"> </div> <div class="form-group"> <input type="submit" id="create_token" value="カードを登録する"></input> </div> </form> </div> </div> </div> </div> </x-app-layout> <script src="https://js.stripe.com/v3/"></script> <script> var stripe_key = '{{ config('payment.stripe_key') }}'; </script> <script src="{{asset('js/payment.js')}}"></script> |
public/js/payment.js
クレカ登録画面でしか使わないから、resources/views/payment/form.blade.phpに直書きでも良いかも。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
/* 基本設定*/ const stripe = Stripe(stripe_key); const elements = stripe.elements(); /* Stripe Elementsを使ったFormの各パーツをどんなデザインにしたいかを定義 */ const style = { base: { fontSize: '12px', color: "#32325d", border: "solid 1px ccc" } }; const classes = { base: "form-control" }; /* フォームでdivタグになっている部分をStripe Elementsを使ってフォームに変換 */ const cardNumber = elements.create('cardNumber', {style:style,classes:classes}); cardNumber.mount('#cardNumber'); const cardCvc = elements.create('cardCvc', {style:style,classes:classes}); cardCvc.mount('#securityCode'); const cardExpiry = elements.create('cardExpiry', {style:style,classes:classes}); cardExpiry.mount('#expiration'); /* id="form_paymentがついたFormのsubmitEvent発生時のプログラム処理を定義"*/ document.querySelector('#form_payment').addEventListener('submit', function(e) { /* 何も処理をかまさないとそのままクレジットカード情報が送信されてしまうので一旦HTMLのFormタグがが従来もっている送信機能を停止させる。 */ e.preventDefault(); /* Stripe.jsを使って、フォームに入力されたコードをStripe側に送信。今回ご紹介している方法の場合、「カード名義」だけはStripe Elementsの仕組みを使っていないため、このままだとカード名義の情報が足りずにカード情報の暗号化ができなくなってしまうので、{name:document.querySelector('#cardName').value}を足すことで、フォームに入力されたカード名義情報も、他の情報と同時にStripeに送ることができるようになる。 */ stripe.createToken(cardNumber,{name: document.querySelector('#cardName').value}).then(function(result) { /* errorが返ってきた場合はその旨を表示 */ if (result.error) { alert("カード登録処理時にエラーが発生しました。カード番号が正しいものかどうかをご確認いただくか、別のクレジットカードで登録してみてください。"); } else { /* 暗号化されたコードが返ってきた場合は以下のStripeTokenHandler関数を実行。その際、引数として暗号化されたコードを渡してあげる。 */ stripeTokenHandler(result.token); } }); /* id="form_payment"が指定されたformの送信ボタン直前に、input type="hidden"のHTMLを挿入し、値にStripeから返ってきた暗号化情報を設定。そして、実際にフォームの内容を送信(事実上、送信されるのは暗号化情報のみとなる) */ function stripeTokenHandler(token) { const form = document.getElementById('form_payment'); const hiddenInput = document.createElement('input'); hiddenInput.setAttribute('type', 'hidden'); hiddenInput.setAttribute('name', 'stripeToken'); hiddenInput.setAttribute('value', token.id); form.appendChild(hiddenInput); form.submit(); } },false); |
8, 購入処理にstripe決済処理を追加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
class PurchaseController extends Controller { // 購入処理 public function purchase($upload_id){ // このユーザが購入済みなら何もしない $user_id = Auth::id(); if(Purchase::where("upload_id", $upload_id)->where("user_id", $user_id)->exists()){ return back(); } // クレジットカードの決済処理 \Stripe\Stripe::setApiKey(\Config::get('payment.stripe_secret')); $upload = Upload::findorFail($upload_id); try { $user = User::find(Auth::id()); $chargeOject = [ 'amount' => 100, 'currency' => 'jpy', // 半角英数字のみ?日本語は駄目? 'description' => "$upload->file_nameの購入料金($user->name)", 'customer' => $user->stripe_id, ]; $charge = \Stripe\Charge::create($chargeOject); } catch (\Stripe\Exception\CardException $e) { $body = $e->getJsonBody(); $errors = $body['error']; return redirect('/user')->with('errors', "決済に失敗しました。しばらく経ってから再度お試しください。"); } // 購入履歴レコード作成 $purchase = new Purchase(); $purchase->upload_id = $upload_id; $purchase->user_id = $user_id; $purchase->save(); return redirect('/user')->with('success', "$upload->file_nameの購入完了。ダウンロードして下さい"); } |
stripeの顧客情報を作成・更新・削除処理を記述
app/Models/Payment
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Auth; class Payment extends Model { use HasFactory; /** * Stripe上に「顧客」を登録するための関数 * * @param String $token・・・・・Stripe上のtoken(フロントエンドで作成) * @param object $user ・・・・・カード登録をするユーザーの情報 * @param object $customer・・・Stripe上に登録する顧客オブジェクト */ public static function setCustomer($token, $user) { \Stripe\Stripe::setApiKey(\Config::get('payment.stripe_secret')); //Stripe上に顧客情報をtokenを使用することで保存 try { $customer = \Stripe\Customer::create([ 'card' => $token, 'name' => $user->name, 'description' => $user->id ]); } catch(\Stripe\Exception\CardException $e) { /* * カード登録失敗時には現段階では一律で別の登録カードを入れていただくように * 促すメッセージで統一。 * カードエラーの類としては以下があるとのこと * 1、カードが決済に失敗しました * 2、セキュリティーコードが間違っています * 3、有効期限が間違っています * 4、処理中にエラーが発生しました * */ return false; } $targetCustomer = null; if (isset($customer->id)) { $targetCustomer = User::find(\Auth::id());//要するに当該顧客のデータをUserテーブルから引っ張りたい $targetCustomer->stripe_id = $customer->id; $targetCustomer->update(); return true; } return false; } /** * Stripe上の「顧客」情報を更新するための関数 * * @param String $token・・・・・Stripe上のtoken(フロントエンドで作成) * @param object $user ・・・・・カード登録をするユーザーの情報 * @param object $customer・・・Stripe上に登録されている顧客オブジェクト * @param object $card・・・・・Stripe上に登録されているクレジットカード情報のオブジェクト */ public static function updateCustomer($token, $user) { \Stripe\Stripe::setApiKey(\Config::get('payment.stripe_secret')); try { $customer = \Stripe\Customer::retrieve($user->stripe_id); $card = $customer->sources->create(['source' => $token]); if (isset($customer)) { $customer->default_source = $card["id"]; $customer->save(); return true; } } catch(\Stripe\Exception\CardException $e) { /* * カード登録失敗時には現段階では一律で別の登録カードを入れていただくように * 促すメッセージで統一。(メッセージ自体はController側で制御しています) * カードエラーの類としては * 1、カードが決済に失敗しました * 2、セキュリティーコードが間違っています * 3、有効期限が間違っています * 4、処理中にエラーが発生しました * */ return false; } return true; } /** * Stripe上に現在登録されている顧客の「使用カード」の情報を取得するための関数 * * @param String $token・・・・・Stripe上のtoken(フロントエンドで作成) * @param object $user ・・・・・カード登録をするユーザーの情報 * @param object $customer・・・Stripe上に登録されている顧客オブジェクト * @param object $default_card・・・・・Stripe上から取得した顧客の「使用カード」オブジェクト */ protected static function getDefaultcard($user) { \Stripe\Stripe::setApiKey(\Config::get('payment.stripe_secret')); $default_card = null; if (!is_null($user->stripe_id)) { $customer = \Stripe\Customer::retrieve($user->stripe_id); if (isset($customer['default_source']) && $customer['default_source']) { $card = $customer->sources->data[0]; $default_card = [ 'number' => str_repeat('*', 8) . $card->last4, 'brand' => $card->brand, 'exp_month' => $card->exp_month, 'exp_year' => $card->exp_year, 'name' => $card->name, 'id' => $card->id, ]; } } return $default_card; } /** * Stripe上に現在登録されている顧客のカード情報を削除するための関数 * * @param object $user ・・・・・カード削除をするユーザーの情報 * @param object $customer・・・Stripe上に登録されている顧客オブジェクト */ protected static function deleteCard($user) { \Stripe\Stripe::setApiKey(\Config::get('payment.stripe_secret')); $customer = \Stripe\Customer::retrieve($user->stripe_id); $card = $customer->sources->data[0]; var_dump($card,"カード"); /* card情報が存在していれば削除 */ if ($card) { \Stripe\Customer::deleteSource( $user->stripe_id, $card->id ); return true; } return false; } } |
実際にクレカ決済が確定する所
App\Http\Controllers\PaymentController;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
public function store(Request $request) { /** * フロントエンドから送信されてきたtokenを取得 * これがないと一切のカード登録が不可 **/ $token = $request->stripeToken; $user = \Auth::user(); //要するにUser情報を取得したい $ret = null; /** * 当該ユーザーがtokenもっていない場合Stripe上でCustomer(顧客)を作る必要がある * これがないと一切のカード登録が不可 **/ if ($token) { /** * Stripe上にCustomer(顧客)が存在しているかどうかによって処理内容が変わる。 * * 「初めての登録」の場合は、Stripe上に「Customer(顧客」と呼ばれる単位の登録をして、その後に * クレジットカードの登録が必要なので、一連の処理を内包しているPaymentモデル内のsetCustomer関数を実行 * * 「2回目以降」の登録(別のカードを登録など)の場合は、「Customer(顧客」を新しく登録してしまうと二重顧客登録になるため、 * 既存のカード情報を取得→削除→新しいカード情報の登録という流れに。 * **/ if (!$user->stripe_id) { $result = Payment::setCustomer($token, $user); /* card error */ if(!$result){ $errors = "カード登録に失敗しました。入力いただいた内容に相違がないかを確認いただき、問題ない場合は別のカードで登録を行ってみてください。"; return redirect('/user/payment/form')->with('errors', $errors); } } else { $defaultCard = Payment::getDefaultcard($user); if (isset($defaultCard['id'])) { Payment::deleteCard($user); } $result = Payment::updateCustomer($token, $user); /* card error */ if(!$result){ $errors = "カード登録に失敗しました。入力いただいた内容に相違がないかを確認いただき、問題ない場合は別のカードで登録を行ってみてください。"; return redirect('/user/payment/form')->with('errors', $errors); } } } else { return redirect('/user/payment')->with('errors', '申し訳ありません、通信状況の良い場所で再度ご登録をしていただくか、しばらく立ってから再度登録を行ってみてください。'); } return redirect('/user/payment')->with("success", "カード情報の登録が完了しました。"); } |
最後の方は参考URLのコピペになっちゃったけど、クレカ決済出来るようにはなった。
決済の実装、めんどい〜!と思いながらも、知らない事・分からない事・出来ない事を、理解して出来るようになってくの、面白い!^_^