完成イメージ
今回のサンプルは商品情報をStripeとLaravel経由でPostgreSQLに登録する方法です。
商品登録画面でデータを入力して「Register」ボタンをクリックします。
正常に登録出来れば、Stripeのダッシュボードに商品が登録されます。
PostgreSQLのProductsテーブルにもデータが登録されてます。
ソフトウェアのバージョン
ソフトウェア | バージョン |
---|---|
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;
}
}