Tech blog Produced by FOURIER

Laravel10 × PayPay API で決済機能実装

Ichikawa Ichikawa 2024.03.25

はじめに

皆様、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を使います。

    以下のコマンドを実行してパッケージをインストールします。

    $ 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/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>

    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;

    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());
        }
    }

    ここまでの処理で、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.phpwebhook(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());
        }
    }

    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']);
    });

    最終的にエンドポイントは以下のようになります。ドメイン部分は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_paymenttrue になっていれば正しく処理できています。

    処理がうまくいかない場合

    💡
    まずは、Webhookで叩かれたリクエストが届いているか確認してください。ngrokのコンソールにリクエストのログが出ていなければ、WebhookのURL設定が間違っている可能性が高いです。

    まとめ

    最後までお読みいただきありがとうございます。

    PayPay決済の実装いかがったでしょうか?SDKもあるので、比較的簡単に実装できるかと思います。

    私が一番苦戦したのは、実装ではなく、本番環境で使うための加盟店申請でした。加盟店申請から10日後に返信が来て、申請は却下されました笑

    そして、一度却下されたアカウント(メールアドレス)での再申請はできないという…

    二度目の申請で加盟店登録できましたが、再申請の承認も10日ほど要しました。

    また、本番環境用のWebhookはダッシュボードから設定できず、登録申請を出さなければなりません。

    そのため、本番公開前に慌てないよう、加盟店登録や本番環境の設定は早めに済ませることをおすすめします。

    では、また次回の記事で。

    Ichikawa

    Ichikawa / Engineer

    パン屋から転身してエンジニア3年目。主にPHP/Laravelを使っています。最近ではVue.js/Nuxt.jsと人間に興味あり。