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 に登録します。

https://developer.paypay.ne.jp/

登録が完了すると、クライアント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 thumbnail

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
.env

config/services.php から、 .env の設定値を読みます。

'paypay' => [
    'api_key' => env('PAYPAY_API_KEY'),
    'api_secret' => env('PAYPAY_API_SECRET'),
    'merchant_id' => env('PAYPAY_MERCHANT_ID'),
],
config/services.php

Controller

以下のコマンドを実行してコントローラーを作成します。

$ php artisan make:controller PayPayController

payment() 関数を用意します。処理は後ほど記載します。

public function payment(Request $request)
{
   // 後で記載
}
app/Http/Controllers/PayPayController.php

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');
});
routes/web.php

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();
    });
}
src/database/migrations/YYYY_MM_DD_××××××_create_orders_table.php

マイグレーションファイルを実行します。

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',
    ];
}
app/Models/Order.php

機能実装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 を使用します。

インストールや設定方法は以下の記事でわかりやすくまとめていただいているので、参考にしてください。

https://biz.addisteria.com/ngrok-windows/

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());
    }
}
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_paymenttrue になっていれば正しく処理できています。

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

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

まとめ

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

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

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

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

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

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

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

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

Ichikawa

Ichikawa slash forward icon Engineer

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

関連記事