Laravel7系にPayPal Rest APIの単発決済を導入する

前提条件

既存のプロジェクト(Laravel 5.7)にPayPalを組み込む

※何か改善点などあればご指摘いただければ幸いです

全体像

f:id:makoo5:20210813123758p:plain

  1. Webページに支払いボタンを追加します。
  2. 購入者がボタンをクリックします。
  3. このボタンは、PayPal OrdersAPIを呼び出して商品の登録を行います
  4. ボタンはPayPalチェックアウトエクスペリエンスを起動します。
  5. 買い手は支払いを承認します。
  6. ボタンはPayPalOrders APIを呼び出して、対象の商品の決済を完了します。
  7. 購入者に確認メッセージを表示します。

大変だったこと

  • 決済系のドキュメントが情報量が多くて整理するのが大変
  • 逆に実装例などが少なく、非推奨の情報が大量に出てきた

使用したライブラリ

結論:Checkout-PHP-SDK

他にlaravel-paypalという良さげなライブラリがあったのですが以下の理由で公式を使用しました

  1. ダウンロード数が公式の1/2だった(Checkout-PHP-SDK, laravel-paypal
  2. Laravel 5.7に対応しているのがv2だったがv2はOrderAPI(今回使用するAPI)に対応していなかった(guzzleのバージョンが6系で引っかかっていて、guzzleのバージョンを7.3に上げるとインストールできました)

実装例

だいぶ端折ります。

<?php declare(strict_types = 1);

namespace .../.../...;

use Illuminate\Http\Request;
use PayPalCheckoutSdk\Core\PayPalHttpClient;
use PayPalCheckoutSdk\Orders\OrdersCreateRequest;
use PayPalCheckoutSdk\Orders\OrdersCaptureRequest;

class PaypalPaymentController extends Controller
{
    /** @var PayPalHttpClient */
    private $client;

    public function __construct()
    {
        $this->client = app()->make(PayPalHttpClient::class);
    }

    /**
     * コイン購入決済処理
     * https://developer.paypal.com/docs/api/payments/v2/#authorizations-capture-header-parameters
     *
     * @param Request $request
     */
    public function payment(Request $request)
    {
        $ordersCreateRequest = new OrdersCreateRequest();
        $ordersCreateRequest->prefer('return=representation');
        $ordersCreateRequest->body = [
            "intent" => "CAPTURE",
            "purchase_units" => [[
                'description' => 'xxx',
                "amount" => [
                    "value" => 1100,
                    "currency_code" => "JPY",
                    // ここに税を記述するとtax_totalのvalueがPayPalの領収書の方に税として表示されます。
                    "breakdown" => [
                        "item_total" => [
                            'currency_code' => 'JPY',
                            'value' => 1000,
                        ],
                        "tax_total" => [
                            'currency_code' => 'JPY',
                            'value' => 100,
                        ]
                    ]
                ],
            ]],
            "application_context" => [
                "cancel_url" => route('cancel.payment'),
                "return_url" => route('success.payment'),
            ]
        ];

        try {

            $response = $this->client->execute($ordersCreateRequest);
            return redirect($response->result->links[1]->href);

        } catch (\Exception $e) {

             // Error Handling

        }
    }

    /**
     * コイン決済をユーザーがキャンセルしたときにイベント
     *
     * @param Request $request
     */
    public function cancel(Request $request)
    {
         // キャンセル時の処理
    }

    /**
     * コイン決済をユーザーが承認したときにイベント
     *
     * @param Request $request
     */
    public function success(Request $request)
    {
        $ordersCaptureRequest = new OrdersCaptureRequest($request->token);
        $ordersCaptureRequest->prefer('return=representation');

        try {

            // 決済を実行
            $this->client->execute($ordersCaptureRequest);

        } catch (\Exception $ex) {
            // Error Handling
        }
    }
}

ちなみにコンストラクタでインスタンス化しているClientはServiceProviderでDIコンテナに検証用と本番用のインスタンスを登録しています。

**<?php**

use PayPalCheckoutSdk\Core\PayPalHttpClient;
use PayPalCheckoutSdk\Core\ProductionEnvironment;
use PayPalCheckoutSdk\Core\SandboxEnvironment;

class ClientServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        /**
         * PayPalHttpClientの紐付けをDIコンテナに登録
         * 全体で使用するためパッケージから出して登録
         */
        $this->app->singleton(PayPalHttpClient::class, function ($app) {
            $mode = config('paypal.mode');
            $clientId = config("paypal.${mode}.client_id");
            $clientSecret = config("paypal.${mode}.client_secret");

            switch ($mode) {
                case 'sandbox':
                    return new PayPalHttpClient(new SandboxEnvironment($clientId, $clientSecret));
                case 'live':
                    return new PayPalHttpClient(new ProductionEnvironment($clientId, $clientSecret));
                default:
                    throw new InvalidArgumentException('paypalのmodeに誤りがあります');
            }
        });
    }
}

config/paypal.php

<?php

return [
    'mode'    => env('PAYPAL_MODE', 'sandbox'),
    'sandbox' => [
        'client_id'         => env('PAYPAL_SANDBOX_CLIENT_ID', ''),
        'client_secret'     => env('PAYPAL_SANDBOX_CLIENT_SECRET', ''),
    ],
    'live' => [
        'client_id'         => env('PAYPAL_LIVE_CLIENT_ID', ''),
        'client_secret'     => env('PAYPAL_LIVE_CLIENT_SECRET', ''),
    ],
];

参考

Log in to your PayPal account

Set up standard payments