0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

完成イメージ

今回のサンプルは商品情報をStripeとLaravel経由でPostgreSQLに登録する方法です。

商品登録画面でデータを入力して「Register」ボタンをクリックします。

image.png

正常に登録出来れば、Stripeのダッシュボードに商品が登録されます。

image.png

Stripへ商品を登録する際の画像の扱い方について

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

PostgreSQLのProductsテーブルにもデータが登録されてます。

image.png

ソフトウェアのバージョン

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

フロントエンドの実装

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

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

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?