
はじめに
皆様、 PayPay は使われてますでしょうか?
2018年10月から始まったQRコード決済サービスですが、QRコード決済の先駆けだった Origami Pay(オリガミペイ) をソフトバンクグループの資本力により退け、業界トップシェアとなっており、PayPayのロゴとQRコードがいたるところで目に入ります。
となるとPayPayの決済機能を実装したいという要望があるわけで、少し前に PayPay API を使った決済機能を実装したので、記憶を辿りながら記事を書いていこうと思います。
前提条件
Laravelの初期構築手順は省きます。バージョンは以下の通りです。
- PHP 8.2
- Laravel 10.10
PayPay for Developers
まずは PayPay for Developers に登録します。
登録が完了すると、クライアントID(APIキー)、加盟店ID、シークレットを確認できます。
また、テストユーザーも用意されています。

機能実装1
では、実装を始めていきます。必要なパッケージ・ファイルを用意します。
パッケージのインストール
PayPay API を実装するためのPHP用SDKを使います。
GitHub - paypay/paypayopa-sdk-php: With PayPay's Payment SDK, you can build a custom Payment checkout process to suit your unique business needs and branding guidelines
With PayPay's Payment SDK, you can build a custom Payment checkout process to suit your unique business needs and branding guidelines - paypay/paypayopa-sdk-php
https://github.com/paypay/paypayopa-sdk-php
以下のコマンドを実行してパッケージをインストールします。
$ composer require paypayopa/php-sdk
.env
にクライアントID(APIキー)、加盟店ID、シークレットを記載します。
PAYPAY_API_KEY=クライアントID(APIキー)
PAYPAY_API_SECRET=シークレット
PAYPAY_MERCHANT_ID=加盟店ID
config/services.php
から、 .env
の設定値を読みます。
'paypay' => [
 'api_key' => env('PAYPAY_API_KEY'),
 'api_secret' => env('PAYPAY_API_SECRET'),
 'merchant_id' => env('PAYPAY_MERCHANT_ID'),
],
Controller
以下のコマンドを実行してコントローラーを作成します。
$ php artisan make:controller PayPayController
payment()
関数を用意します。処理は後ほど記載します。
public function payment(Request $request)
{
 // 後で記載
}
View
決済送信と決済完了のビューを用意します。
resources/views/paypay/payment.blade.php
を作成。決済金額を入力して送信をするだけの必要最低限のものです。
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">

<head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>Laravel PayPay API</title>
</head>

<body>
 <form method="POST" action="{{ route('paypay.payment') }}">
 @csrf

 <input type="number" name="price">

 <button type="submit">決済</button>
 </form>
</body>

</html>
resources/views/paypay/payment.blade.php
resources/views/paypay/complete.blade.php
を作成。決済完了時に表示します。
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">

<head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>Laravel PayPay API</title>
</head>

<body>
 <p>決済完了</p>
</body>

</html>
resources/views/paypay/complete.blade.php
Route
routes/web.php
を以下のように変更します。
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PayPayController;

Route::prefix('paypay')->as('paypay.')->group(function () {
 Route::view('/', 'paypay.payment')->name('index');
 Route::view('/complete', 'paypay.complete')->name('complete');
 Route::post('/payment', [PayPayController::class, 'payment'])->name('payment');
});
migration
決済情報を保存するためのテーブルを作成します。コマンドを実行して、マイグレーションファイルを生成します。
php artisan make:migration CreateOrdersTable
以下のようなカラムを用意します。こちらも必要最低限です。
public function up(): void
{
 Schema::create('orders', function (Blueprint $table) {
 $table->id();
 $table->integer('price')->comment('料金');
 $table->boolean('is_payment')->default(false)->comment('決済判定');
 $table->string('paypay_merchant_payment_id')->nullable()->comment('PayPay 決済ID');
 $table->timestamps();
 });
}
マイグレーションファイルを実行します。
php artisan migrate
Model
Orderモデル
を作成します。
php artisan make:model Order
モデルファイルを生成したら、以下のように追記してください。
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
 use HasFactory;

 /**
 * The attributes that are mass assignable.
 *
 * @var array<int, string>
 */
 protected $fillable = [
 'price',
 'is_payment',
 'paypay_merchant_payment_id',
 ];


 /**
 * The attributes that should be cast.
 *
 * @var array<string, string>
 */
 protected $casts = [
 'is_payment' => 'boolean',
 ];
}
機能実装2
必要なファイルやパッケージの準備ができたので、PayPay決済のための処理を実装していきます。
app/Http/Controllers/PayPayController.php
に以下を追記してください。
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use PayPay\OpenPaymentAPI\Client;
use PayPay\OpenPaymentAPI\Models\OrderItem;
use PayPay\OpenPaymentAPI\Models\CreateQrCodePayload;
use App\Models\Order;
app/Http/Controllers/PayPayController.php
payment(Request $request)
にPayPay決済用QRコードのリンクを生成する処理を実装します。
処理についてコード内のコメントで補足しておくので、ご確認ください。
public function payment(Request $request)
{
 DB::beginTransaction();
 try {
 $isProduction = config('app.env') === 'production' ? true : false;
 $client = new Client([
 'API_KEY' => config('services.paypay.api_key'),
 'API_SECRET' => config('services.paypay.api_secret'),
 'MERCHANT_ID' => config('services.paypay.merchant_id')
 ], $isProduction);

 $orderName = 'テスト'; // 商品名等
 $price = $request->price; // 決済金額
 $items = (new OrderItem())->setName($orderName)
 ->setQuantity(1)
 ->setUnitPrice(['amount' => $price, 'currency' => 'JPY']);

 $paypayMerchantPaymentId = 'mpid_' . rand() . time(); // PayPay決済成功時のWebhookに含めるユニークとなる決済ID
 $redirectUrl = route('paypay.complete'); // PayPay決済成功後のリダイレクト先URL

 $CQPayload = new CreateQrCodePayload();
 $CQPayload->setOrderItems($items);
 $CQPayload->setMerchantPaymentId($paypayMerchantPaymentId);
 $CQPayload->setCodeType('ORDER_QR');
 $CQPayload->setAmount(['amount' => $price, 'currency' => 'JPY']);
 $CQPayload->setIsAuthorization(false);
 $CQPayload->setUserAgent($_SERVER['HTTP_USER_AGENT']);
 $CQPayload->setRedirectType('WEB_LINK');
 $CQPayload->setRedirectUrl($redirectUrl);

 $QRCodeResponse = $client->code->createQRCode($CQPayload);

 if ($QRCodeResponse['resultInfo']['code'] !== 'SUCCESS') {
 throw new \Exception('決済用QRコードが生成できませんでした');
 }

 Order::create([
 'price' => $price,
 'is_payment' => false,
 'paypay_merchant_payment_id' => $paypayMerchantPaymentId
 ]);

 DB::commit();

 return redirect()->to($QRCodeResponse['data']['url']); // PayPayの決済画面に遷移

 } catch (\Exception $e) {
 DB::rollback();
 Log::error($e->getMessage());
 }
}
app/Http/Controllers/PayPayController.php
ここまでの処理で、PayPayの決済が可能になります。
http://localhost/paypay にアクセスして、金額を入力し、決済ボタンを押すことで、PayPayの決済用ページに遷移します。

テストユーザーのユーザーネーム(電話番号)とパスワードを入力して決済を済ませてください。
※ テストユーザーの認証情報は PayPay for Developers のダッシュボードで確認できます
PayPay決済が完了し、機能実装1で作成した決済完了ページに遷移すれば問題なく実装できています。
機能実装3
PayPayの決済ページに遷移後、ユーザーが決済したか判定する必要があるため、決済成功時のWebhookのエンドポイントを準備します。
NGROK準備
Webhookのリクエストをローカル環境で受け取るため、 ngrok
を使用します。
インストールや設定方法は以下の記事でわかりやすくまとめていただいているので、参考にしてください。
ngrok
を実行すると、 https://35b8-153-221-229-228.ngrok-free.app
を通じて、ローカル環境と接続ができます。
※ ドメインは起動するたびに変わります

Webhookのリクエストを受けた際の処理
app/Http/Controllers/PayPayController.php
に webhook(Request $request)
関数を追加します。
public function webhook(Request $request)
{
 DB::beginTransaction();
 try {
 $state = $request->state;
 $paypayMerchantPaymentId = $request->merchant_order_id;

 if ($state === 'FAILED') {
 throw new \Exception('オーダーステータス: ' . $state);
 }

 $order = Order::where('paypay_merchant_payment_id', $paypayMerchantPaymentId)->first();
 if (empty($order)) {
 throw new \Exception('注文情報が存在しません');
 }

 $order->is_payment = true;
 $order->save();

 DB::commit();

 return response()->json(['message' => 'success'], 200);

 } catch (\Exception $e) {
 DB::rollback();
 Log::error($e->getMessage());
 }
}
app/Http/Controllers/PayPayController.php
routes/api.php
にルーティングを追加します。
Webhookで叩かれるリクエストのHTTPメソッドは POST
です。
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PayPayController;

Route::prefix('paypay')->group(function () {
 Route::post('/webhook', [PayPayController::class, 'webhook']);
});
routes/api.php
最終的にエンドポイントは以下のようになります。ドメイン部分は ngrok
を起動するたびに変わるので注意ください。
https://35b8-153-221-229-228.ngrok-free.app/api/paypay/webhook
※ api.php
にルーティングを記載しているので、パスに /api
が入ります
エンドポイント設定
PayPay for Developers でWebhookのURLを設定します。
https://developer.paypay.ne.jp/settings
決済トランザクション通知Webhook
にURLを入力して、保存ボタンを押してください。

これで実装完了です。
動作確認
機能実装2と同じ手順で、 http://localhost/paypay から決済処理を行います。
orders
テーブルを確認して、 is_payment
が true
になっていれば正しく処理できています。
処理がうまくいかない場合
まとめ
最後までお読みいただきありがとうございます。
PayPay決済の実装いかがったでしょうか?SDKもあるので、比較的簡単に実装できるかと思います。
私が一番苦戦したのは、実装ではなく、本番環境で使うための加盟店申請でした。加盟店申請から10日後に返信が来て、申請は却下されました笑
そして、一度却下されたアカウント(メールアドレス)での再申請はできないという…
二度目の申請で加盟店登録できましたが、再申請の承認も10日ほど要しました。
また、本番環境用のWebhookはダッシュボードから設定できず、登録申請を出さなければなりません。
そのため、本番公開前に慌てないよう、加盟店登録や本番環境の設定は早めに済ませることをおすすめします。
では、また次回の記事で。