【Next.jsとLaravel連携】決済APIのStripeにLaravelから商品登録する方法

# 完成イメージ
#### 今回のサンプルは商品情報をStripeとLaravel経由でPostgreSQLに登録する方法です。
#### 商品登録画面でデータを入力して「Register」ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/c4c3a114-3056-4706-b57b-993db47be5ba.png)


#### 正常に登録出来れば、Stripeのダッシュボードに商品が登録されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/532e9602-429a-4462-86f7-22c6105da7cb.png)

:::note info
#### Stripへ商品を登録する際の画像の扱い方について
Laravelを経由してStripeへ画像を登録することはできません。(理由：外部アプリから画像データを授受するためのStripeのAPIがないため)
そのため、この記事では商品の画像以外を登録後にStripeのMy Accountでマイページにアクセスして手動で画像を登録しています。
:::

#### PostgreSQLのProductsテーブルにもデータが登録されてます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/e026c928-09e5-44a9-b0b7-31f3e2a5218d.png)

# ソフトウェアのバージョン
|ソフトウェア|バージョン|
|-----------|---------|
|Laravel    |       9*|
|Next.js    |15.2.1   |

# フロントエンドの実装
商品登録フォームには、管理者(Admin)の管理者ID(AdminId)を持たせておきます。
管理者ID(AdminId)は、SesshonStirageから取得することにしました。
Next.jsでは`SessionStorage`を使用するためにはフックである`useEffect`と`useState`を使用することとします。

1.sessionStorage からの値取得を useEffect 内で行い、その値を状態 (adminId と adminUser) に設定します。
2.フォームの input 要素に value プロパティを使って、useState からの値を直接バインドします。defaultValue ではなく、value を使用することで、状態に基づいてフォームの初期値を設定できます。



```frontend/app/products/registerProduct/page.tsx
'use client'
import { useRouter } from "next/navigation"
import { useForm } from "react-hook-form";
import { useState,useEffect } from "react";
import axios from "axios";

export default function RegistrationProduct(){
    //Set routing
    const router = useRouter();
    const [adminId, setAdminId] = useState<string | null>(null);//useState<string | null>(null);
    const [adminUser, setAdminUser] = useState<string | null>(null);//useState<string | null>(null);
    let storedAdminId:any;
    let storedAdminUser:any;
    
    useEffect(() => {
        // This will only run on the client side
        storedAdminId = sessionStorage.getItem("adminUserId");
        storedAdminUser = sessionStorage.getItem('adminUserName');

        if(!storedAdminId || !storedAdminUser){// if(!storedAdminId || !storedAdminUser){
            alert('Woops! adminInformation is not empty.');
            router.push('/');//一旦はトップページにしておく。あとでログイン画面に遷移するようにする。
            return;
        }

        setAdminId(storedAdminId);
        setAdminUser(storedAdminUser);
    }, [router]);

    //Set defaultvalue
    const defaultValues = {
        productName:'',
        productDescription:'',
        productPrice:0,
        productImage:null
    };

    //Initialize form
    const {register,handleSubmit,formState:{errors}} = useForm({
        defaultValues
    });

    const [productName,setProductName] = useState('');
    const [productDescription,setProductDescription] = useState('');
    const [productPrice,setProductPrice] = useState(0);
    const [productImage,setProductImage] = useState<File | null>(null);
    const [error,setError] = useState('');

    const validateImage = (file:File | null)=>{
        if(file){
            const allowedFileextensions = ['image/jpeg', 'image/png'];
            if(!allowedFileextensions.includes(file.type)){
                return "Only JPEG and PNG images are allowed";
            }
        }
        return true;
    }

    //Click register button
    const onsubmit = async(data:any)=>{

        const formData = new FormData();
        if(adminId != null){
            formData.append('adminId',adminId);
        }
        if(adminUser != null){
            formData.append('adminUser',adminUser);
        }
        formData.append('productName',productName);
        formData.append('productDescription',productDescription);
        formData.append('productPrice',productPrice.toString());

        if(productImage){
            formData.append('productImage',productImage);
        }

        try{
            const responseStripe = await axios.post('http://localhost:8000/api/stripe/create-product',formData);

            const response = await axios.post('http://localhost:8000/api/createProducts',formData);

            if(responseStripe.status === 200 && response.status === 200){//responseStripe.status === 200 && response.status === 200
                alert('Product registered successfully');
                router.push('/products/getListProducts');
            }else{
                alert('Failed to register product');
            }
        }catch(error){
            /*
            //Error from server
            if(error.response){
                console.error('Error response:', error.response.data);
            }else if(error.request){
                // Althouth request was sent, there are no response.
                console.error('Error request:', error.request);
            }else{
                console.error('Error:', error.message);
            }
            */
            alert('An error occurred during submission');
            console.error(error);
        }
        
    }

    //Click bakToList button
    const clickBackToProductsList = ()=>{
        router.push('/products/getListProducts');
    }

    if(adminId === null || adminUser === null){
        return <div>Loading...</div>
    }

    return (
        <div className="flex items-center justify-center h-min screen px-8 py-8">
            <form onSubmit={handleSubmit(onsubmit)} className="w-xl px-8 py-8 border border-gray-500 rounded-md shadow-md">
                <div>
                    <input type="hidden" name="adminId" value={adminId} />
                    <input type="hidden" name="adminUser" value={adminUser} />
                </div>
                <div>
                    <label>Name</label>
                    <input
                        type="text"
                        className="w-full text-gray-500 border border-gray-500 rounded-md p-4 mb-4"
                        defaultValue={defaultValues.productName}
                        {...register('productName',{
                            required:'ProductName must be required.'
                        })}
                        onChange={(e)=>setProductName(e.target.value)}
                    />
                    <div className="text-rose-500">{errors.productName?.message}</div>
                </div>
                <div>
                    <label>Description</label>
                    <input
                        type="text"
                        className="w-full text-gray-500 border border-gray-500 rounded-md p-4 mb-4"
                        defaultValue={defaultValues.productDescription}
                        {...register('productDescription',{
                            required:'ProductDescription must be required.'
                        })}
                        onChange={(e)=>setProductDescription(e.target.value)}
                    />
                    <div className="text-rose-500">{errors.productDescription?.message}</div>
                </div>
                <div>
                    <label>ProductImage</label>
                    <input
                        type="file"
                        className="block w-full text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 dark:text-gray-400 focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 p-4 mb-4"
                        {...register('productImage',{
                            validate:validateImage
                        })}
                        onChange={(e)=>{
                            const file = e.target.files ? e.target.files[0]:null;
                            setProductImage(file);
                        }}
                    />
                </div>
                <div>
                    <label>Price</label>
                    <input
                        type="number"
                        className="w-full text-gray-500 border border-gray-500 rounded-md p-4 mb-4"
                        defaultValue={defaultValues.productPrice}
                        {...register('productPrice',{
                            required:'ProductPrice must be required.',
                            min:10
                        })}
                        onChange={(e)=>{setProductPrice(Number(e.target.value))}}
                    />
                    <div className="text-rose-500">{errors.productPrice?.message}</div>
                </div>
                <div className="flex items-center justify-between py-4">
                    <button
                        type="submit"
                        className="bg bg-blue-500 rounded-md text-white px-4 py-4"
                        onClick={onsubmit}>
                        Register
                    </button>
                    <button
                        type="button"
                        className="bg bg-rose-500 rounded-md text-white px-4 py-4"
                        onClick={clickBackToProductsList}>
                        BackToList
                    </button>
                </div>
            </form>
        </div>
    )
}
```

# サーバ(Laravel)の実装

```routes/api.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\StripeController;//Add
use App\Http\Controllers\AdminRegisterController;//Add
use App\Http\Controllers\ProductsController;//Add
use App\Http\Controllers\AdminLoginController;//Add

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

Route::post('/stripe/create-product',[StripeController::class,'createProduct']);
Route::get('/stripe/products',[StripeController::class,'listProducts']);
Route::delete('/stripe/delete-product/{productId}',[StripeController::class,'deleteProduct']);
Route::put('/stripe/update-product/{productId}',[StripeController::class,'updateProduct']);

Route::post('/adminRegistration',[AdminRegisterController::class,'createAdminUser']);
Route::post('/createProducts',[ProductsController::class,'createProducts']);
Route::post('/adminLogin',[AdminLoginController::class,'adminLogin']);
```
## Controllerクラスの実装
```backent/app/Http/Controllers/StripeController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Stripe\Stripe;//Add
use Stripe\Product;//Add
use Stripe\Price;//Add

class StripeController extends Controller
{
    //Method to create product
    public function createProduct(Request $request){
        Stripe::setApiKey(env('STRIPE_SECRET_KEY'));

        //Create Product
        $product = Product::create([
            'name'=>$request->input('productName'),
            'description'=>$request->input('productDescription'),
        ]);

        //Set price
        $price = Price::create([
            'unit_amount'=>$request->input('productPrice')*100,
            'currency'=>'USD',
            'product'=>$product->id,
        ]);

        return response()->json(['product'=>$product,'price'=>$price]);
    }

    //Methos to list all products
    public function listProducts()
    {
        Stripe::setApiKey(env('STRIPE_SECRET_KEY'));
        try{
            //Get list of all products
            $products = Product::all();
            foreach($products->data as &$product){
                if(isset($product->default_price)){
                    $price = Price::retrieve($product->default_price);
                    $product->price = $price->unit_amount / 100;
                    $product->currency = $price->currency;
                }
            }
            return response()->json($products);
        }catch(\Exception $e){
            return response()->json(['error' => $e->getMessage()], 400);
        }
    }
    //Method to delete product
    public function deleteProduct($productId)
    {
        Stripe::setApiKey(env('STRIPE_SECRET_KEY'));
        //Delete product
        $product = Product::retrieve($productId);
        $product->delete();

        return response()->json(['status'=>'success']);
    }

    //Method to update product
    public function updateProduct($productId,Request $request)
    {
        Stripe::setApiKey(env('STRIPE_SECRET_KEY'));
        //Update product
        $product = Product::retrieve($productId);
        $product->name = $request->name;
        $product->description = $request->description;
        $product->save();
        return response()->json($product);
    }
}

```

```backent/app/Http/Controllers/ProductsController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Products;//Add
use Illuminate\Support\Facades\Storage;//Add
use Illuminate\Support\Facades\DB;//Add
use Illuminate\Support\Facades\Log;//Add
use Illuminate\Support\Str;//Add

class ProductsController extends Controller
{
    public function createProducts(Request $request)
    {
        $request->validate([
            'productName'=>'required|string|max:255',
            'productDescription'=>'required|string|max:255',
            'productImage'=>'required|image|mimes:jpeg,png,jpg,gif|max:2048'
        ]);
        $adminId = $request->input('adminId');
        $productName = $request->input('productName');
        $productDescription = $request->input('productDescription');
        $productPrice = $request->input('productPrice');

        $originalFileName = $request->file('productImage')->getClientOriginalName();
        $fileName = time().'_'.$originalFileName;
        $imagePath = $request->file('productImage')->storeAs('public/uploads',$fileName);
        $imageUrl = Storage::url($imagePath);

        DB::begintransaction();
        try{
            $products = new Products();
            $products->adminId = $adminId;
            $products->productId = Str::uuid();
            $products->productName = $productName;
            $products->productDescription = $productDescription;
            $products->productPrice = $productPrice;
            $products->productImagePath = $imagePath;

            $products->save();
            DB::commit();

            return response()->json([
                'adminId'=> $adminId,
                'productname'=>$productName,
                'productdescription'=>$productDescription,
                'productPrice'=>$productPrice,
                'productImagePath'=>$imagePath
            ]);
            
        }catch(\Throwable $e){
            DB::rollBack();
            Log::error('fail to create product'.$e->getMessage());
            return response()->json([
                'error'=>'Fail to create products.',
                'message'=>$e->getMessage(),
            ]);
            throw $e;
        }
        /*
        return response()->json([
            'adminId'=> $adminId,
            'productname'=>$productName,
            'productdescription'=>$productDescription,
            'productPrice'=>$productPrice,
            'productImagePath'=>$imagePath
        ]);
        */
    }
}

```
## Modelクラスの実装
`Products`テーブルに登録する際の主キーは`id`ではなく、`productId`とするため、`Model`クラスファイルの冒頭に下記のように記載します。
これにより、Eloquentが自動的にidを参照しなくなります。

```backend/app/Models/Products.php
namespace App\Models;

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

class Products extends Model
{
    use HasFactory;

    // 主キーを指定
    protected $primaryKey = 'productId';  // 'id'の代わりに'productId'を主キーとして使用

    // 主キーがUUID型であることをLaravelに伝える
    public $incrementing = false;  // 自動増分を無効化（UUIDを使うため）

    // 主キーの型を指定（UUIDの場合）
    protected $keyType = 'string';  // UUIDは文字列として扱う
}

```
上記の設定を忘れるとエラーが発生して登録できません。↓

```
"SQLSTATE[42703]: Undefined column: 7 ERROR:  列"id"は存在しません
LINE 1: ..._at") values ($1, $2, $3, $4, $5, $6, $7, $8) returning "id"
 (SQL: insert into "products" 
```


いかが全体のコーです。

```backend/app/Models/Products.php
<?php

namespace App\Models;

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

class Products extends Model
{
    use HasFactory;
    protected $table = 'products';//Add
    protected $primaryKey = 'productId';
    public $incrementing = false;
    protected $keyType = 'string';

    public function getAdminId()
    {
        return $this->attributes['adminId'];
    }

    public function setAdminId($adminId)
    {
        $this->attributes['adminId'] = $adminId;
    }

    public function getProductId(){
        return $this->attributes['productId'];
    }

    public function setProductId($productId)
    {
        $this->attributes['productId'] = $productId;
    }

    public function getProductName()
    {
        return $this->attributes['productName'];
    }

    public function setProductName($productName)
    {
        $this->attributes['productName'] = $productName;
    }

    public function getProductDescription()
    {
        return $this->attributes['productDescription'];
    }

    public function setProductDescription($productDescription)
    {
        $this->attributes['productDescription'] = $productDescription;
    }

    public function getProductPrice()
    {
        return $this->attributes['productPrice'];
    }

    public function setProductPrice($productPrice)
    {
        $this->attributes['productPrice'] = $productPrice;
    }

    public function getProductImagePath()
    {
        return $this->attributes['productImagePath'];
    }

    public function setProductImagePath($productImagePath)
    {
        $this->attributes['productImagePath'] = $productImagePath;
    }
}

```