یکی از نیازهای رایج در پروژههای Laravel، دریافت وجه از کاربران از طریق درگاه پرداخت اینترنتی است. فروشگاههای اینترنتی، سامانههای اشتراکی، پنلهای فروش خدمات و نرمافزارهای SaaS معمولاً به قابلیتی نیاز دارند که کاربر بتواند هزینه سفارش خود را بهصورت آنلاین پرداخت کند.
در Laravel میتوان فرایند پرداخت را بهصورت ساختاریافته پیادهسازی کرد؛ بهگونهای که درخواست پرداخت، انتقال کاربر به درگاه، بازگشت از درگاه و تأیید نهایی تراکنش بهصورت مجزا مدیریت شوند.
در این آموزش با اصول اتصال درگاه پرداخت ایرانی به Laravel آشنا میشویم و یک ساختار قابل توسعه برای پیادهسازی آن طراحی میکنیم.
۱. پیشنیازهای اتصال درگاه پرداخت
قبل از شروع پیادهسازی، باید چند مورد را آماده کنید:
- یک پروژه Laravel فعال
- حساب کاربری و دسترسی لازم برای استفاده از درگاه پرداخت
- شناسه پذیرنده یا کلیدهای موردنیاز درگاه
- آدرس callback معتبر برای دریافت نتیجه پرداخت
- یک مدل یا جدول برای نگهداری سفارشها
- یک مدل یا جدول برای ثبت تراکنشهای پرداخت
- محیطی با HTTPS معتبر برای اجرای پرداخت در محیط عملیاتی
پارامترها و مراحل دقیق اتصال به ارائهدهنده درگاه بستگی دارند. درگاههایی مانند زرینپال، بهپرداخت ملت و درگاههای مبتنی بر قراردادهای دیگر ممکن است ساختار درخواست، روش احراز هویت و پاسخ متفاوتی داشته باشند.
بنابراین، قبل از استفاده عملی باید مستندات رسمی همان درگاه را بررسی کنید.
۲. معماری صحیح پرداخت در Laravel
یک سیستم پرداخت استاندارد معمولاً از چهار بخش اصلی تشکیل میشود:
| بخش | وظیفه |
|---|---|
| Controller | دریافت درخواست کاربر و مدیریت جریان کلی |
| Payment Service | ارتباط با درگاه و اجرای منطق پرداخت |
| Payment Model | نگهداری وضعیت و اطلاعات تراکنش |
| Callback Handler | دریافت نتیجه بازگشت و آغاز فرایند تأیید |
بهتر است منطق اختصاصی هر درگاه را مستقیماً داخل کنترلر قرار ندهیم. با ایجاد یک لایه مستقل برای درگاه، میتوان در آینده ارائهدهنده پرداخت را تغییر داد یا چند درگاه را در یک پروژه پشتیبانی کرد.
جریان کلی پرداخت به این صورت است:
- کاربر روی دکمه پرداخت کلیک میکند.
- سرور سفارش و مبلغ آن را بررسی میکند.
- یک رکورد پرداخت با وضعیت اولیه ایجاد میشود.
- درخواست ایجاد پرداخت به درگاه ارسال میشود.
- شناسه پرداخت در رکورد داخلی ذخیره میشود.
- کاربر به صفحه پرداخت هدایت میشود.
- درگاه کاربر را به callback برمیگرداند.
- سرور پرداخت را از طریق API رسمی درگاه تأیید میکند.
- وضعیت تراکنش و سفارش بهروزرسانی میشود.
نکته مهم: بازگشت کاربر از درگاه به سایت، بهتنهایی به معنی موفقیت پرداخت نیست. تأیید نهایی باید در سمت سرور انجام شود.
۳. نگهداری تنظیمات درگاه در فایل محیطی
اطلاعات محرمانه درگاه را در کد کنترلر یا فایلهای عمومی پروژه قرار ندهید. بهتر است از فایل .env و فایل پیکربندی Laravel استفاده کنید.
برای مثال، تنظیمات فرضی یک درگاه را به این صورت در .env قرار میدهیم:
PAYMENT_MERCHANT_ID=your-merchant-id
PAYMENT_BASE_URL=https://gateway.example
PAYMENT_CALLBACK_URL=https://example.com/payment/callback
آدرس gateway.example صرفاً نمونه است و باید با آدرس رسمی درگاه انتخابشده جایگزین شود.
سپس در فایل config/services.php تنظیمات را اضافه میکنیم:
'payment_gateway' => [
'merchant_id' => env('PAYMENT_MERCHANT_ID'),
'base_url' => env('PAYMENT_BASE_URL'),
'callback_url' => env('PAYMENT_CALLBACK_URL'),
],
اکنون میتوانیم در بخشهای مختلف پروژه از این تنظیمات استفاده کنیم:
$merchantId = config('services.payment_gateway.merchant_id');
$callbackUrl = config('services.payment_gateway.callback_url');
در محیط عملیاتی، دسترسی به فایل .env باید محدود باشد و این فایل نباید وارد مخزن عمومی Git شود.
۴. ایجاد جدول تراکنشهای پرداخت
برای ثبت تلاشهای پرداخت، بهتر است یک جدول مستقل داشته باشیم. اگر از قبل در پروژه جدول payments یا payment_transactions دارید، میتوانید همین ساختار را با مدل موجود تطبیق دهید.
برای ساخت مدل و مایگریشن:
php artisan make:model Payment -m
نمونهای از ساختار جدول:
Schema::create('payments', function (Blueprint $table) {
$table->id();
$table->foreignId('order_id')
->constrained()
->restrictOnDelete();
$table->string('gateway', 50);
$table->string('authority')->nullable()->unique();
$table->string('transaction_id')->nullable()->unique();
$table->unsignedBigInteger('amount');
$table->string('currency', 10)->default('IRR');
$table->string('status', 30)->default('pending');
$table->string('idempotency_key')->unique();
$table->timestamp('verified_at')->nullable();
$table->timestamps();
});
این جدول اطلاعات اصلی تلاش پرداخت را نگهداری میکند. اگر درگاه بهجای authority از شناسه یا توکن دیگری استفاده میکند، نام و ساختار فیلدها را متناسب با API آن تغییر دهید.
پس از تکمیل مایگریشن، اجرا کنید:
php artisan migrate
مبلغ باید با واحد مشخص و یکسان در کل برنامه ذخیره شود. برای مثال، اگر مبلغ سفارش بر اساس ریال است، مقدار ارسالی به درگاه نیز باید مطابق قرارداد همان درگاه تبدیل و ارسال شود. بعضی ارائهدهندگان از واحدهای متفاوت استفاده میکنند؛ بنابراین نباید بدون بررسی فرض کرد همه درگاهها مبلغ را با یک واحد دریافت میکنند.
۵. ایجاد سرویس مستقل برای درگاه پرداخت
برای جداکردن منطق ارتباط با درگاه از کنترلر، یک فایل سرویس ایجاد کنید:
app/Services/PaymentGatewayService.php
در این سرویس میتوانیم متدهایی مانند requestPayment() و verifyPayment() تعریف کنیم.
برای مثال، قرارداد کلی سرویس میتواند چنین باشد:
<?php
namespace App\Services;
interface PaymentGateway
{
public function requestPayment(
int $amount,
string $callbackUrl,
string $orderNumber
): array;
public function verifyPayment(
string $authority,
int $expectedAmount
): array;
}
این رابط مشخص میکند هر پیادهسازی درگاه باید چه عملیاتهایی ارائه دهد. جزئیات درخواست HTTP، ساختار JSON، نام پارامترها و کدهای پاسخ باید در کلاس اختصاصی همان درگاه پیادهسازی شوند.
برای نمونه میتوان کلاسهای جداگانهای برای هر ارائهدهنده ساخت:
app/Services/Payment/
ZarinpalGateway.php
MellatGateway.php
SadadGateway.php
نام فایلها صرفاً یک پیشنهاد معماری است. پیادهسازی واقعی هر کلاس باید مطابق مستندات و نسخه فعلی API همان ارائهدهنده باشد.
این معماری باعث میشود کنترلر اصلی پرداخت، به ساختار پاسخ یک درگاه خاص وابسته نباشد.
۶. ثبت درخواست پرداخت و هدایت کاربر
در زمان شروع پرداخت، ابتدا باید سفارش را از دیتابیس پیدا کنیم و مالکیت آن را بررسی کنیم. مبلغ نیز باید از اطلاعات معتبر سفارش به دست بیاید.
نمونهای از تعریف مسیرها:
use App\Http\Controllers\PaymentController;
use Illuminate\Support\Facades\Route;
Route::middleware('auth')->group(function () {
Route::post(
'/orders/{order}/pay',
[PaymentController::class, 'start']
)->name('payments.start');
});
Route::match(
['get', 'post'],
'/payment/callback',
[PaymentController::class, 'callback']
)->name('payments.callback');
مسیر callback باید با روش HTTP و قرارداد اعلامشده توسط درگاه هماهنگ باشد. اگر درگاه فقط از POST استفاده میکند، ضرورتی ندارد GET را نیز فعال کنید.
در کنترلر شروع پرداخت، منطق کلی به این شکل است:
- سفارش را از دیتابیس پیدا کنید.
- مطمئن شوید سفارش متعلق به کاربر واردشده است.
- بررسی کنید سفارش قبلاً پرداخت نشده باشد.
- مبلغ نهایی را از سمت سرور محاسبه کنید.
- رکورد پرداخت را ایجاد کنید.
- درخواست را با سرویس درگاه ارسال کنید.
- شناسه دریافتی را ذخیره کنید.
- کاربر را به آدرس پرداخت ارائهشده توسط درگاه هدایت کنید.
از دریافت مبلغ نهایی از یک فیلد مخفی HTML یا پارامتر ساده درخواست خودداری کنید. کاربر میتواند مقادیر ارسالی مرورگر را تغییر دهد.
۷. مدیریت callback درگاه پرداخت
پس از پرداخت یا لغو عملیات، درگاه معمولاً کاربر را به آدرس callback بازمیگرداند. اطلاعات دریافتی میتواند شامل شناسه درخواست پرداخت و نتیجه اولیه عملیات باشد.
این اطلاعات باید بهعنوان ورودی بررسی شوند، نه مدرک قطعی موفقیت پرداخت.
در callback لازم است:
- رکورد پرداخت مربوط به شناسه معتبر پیدا شود.
- پرداخت به سفارش درست مرتبط باشد.
- وضعیت فعلی پرداخت بررسی شود.
- شناسه دریافتی با اطلاعات ذخیرهشده در دیتابیس تطبیق داده شود.
- درخواست تأیید رسمی به درگاه ارسال شود.
- مبلغ و نتیجه نهایی بررسی شوند.
- نتیجه در دیتابیس ثبت شود.
اگر callback چند بار ارسال شود، سیستم نباید یک پرداخت را چند بار ثبت کند. برای همین، بررسی وضعیت و تغییر آن باید بهشکل کنترلشده انجام شود.
نکته درباره CSRF
در برخی قراردادها، درگاه callback را با درخواست POST به سایت ارسال میکند. اگر این مسیر زیر محافظت CSRF لاراول باشد، ممکن است درخواست خارجی به دلیل نداشتن توکن CSRF رد شود.
اگر لازم است مسیر callback از بررسی CSRF مستثنا شود، فقط همان مسیر مشخص را مطابق نسخه Laravel و تنظیمات امنیتی پروژه استثنا کنید؛ نه اینکه محافظت CSRF را بهصورت کلی غیرفعال کنید.
این استثنا جایگزین تأیید پرداخت نیست. امنیت callback باید بر پایه اعتبارسنجی شناسهها، تأیید سمت سرور و بررسی مبلغ باشد.
۸. تأیید پرداخت از سمت سرور
پس از دریافت callback، سرور باید از API رسمی درگاه برای تأیید پرداخت استفاده کند.
درگاه ممکن است برای این مرحله به شناسه درخواست پرداخت، مبلغ، توکن یا اطلاعات دیگری نیاز داشته باشد. این موارد باید از مستندات رسمی آن درگاه استخراج شوند.
در فرایند تأیید، موارد زیر اهمیت دارند:
- پاسخ از سرویس معتبر درگاه دریافت شده باشد.
- وضعیت پاسخ نشاندهنده تأیید موفق باشد.
- مبلغ تأییدشده با مبلغ مورد انتظار تطبیق داشته باشد.
- شناسه تراکنش در صورت ارائه، ذخیره و یکتا کنترل شود.
- پرداخت قبلاً نهایی نشده باشد.
اگر ارتباط با درگاه قطع شود یا پاسخ مبهم باشد، نباید بدون استعلام مجدد پرداخت را موفق یا ناموفق قطعی اعلام کرد. بهتر است وضعیت آن تا تعیین تکلیف در حالت انتظار یا بررسی باقی بماند.
۹. جلوگیری از ثبت تکراری پرداخت
فرض کنید کاربر پرداخت موفقی انجام داده، اما به دلیل مشکل اینترنت صفحه نتیجه را دوباره باز میکند. همچنین ممکن است درگاه callback را بیش از یک بار ارسال کند.
اگر سیستم در هر بار بازشدن callback، سفارش را مجدداً پرداختشده اعلام کند یا کیف پول را شارژ کند، ممکن است یک عملیات مالی چند بار اجرا شود.
برای جلوگیری از این مشکل:
- برای هر تلاش پرداخت شناسه داخلی یکتا ایجاد کنید.
- شناسه تراکنش درگاه را در صورت پشتیبانی بهصورت یکتا ذخیره کنید.
- پیش از تغییر وضعیت، وضعیت فعلی پرداخت را بررسی کنید.
- تغییر وضعیت پرداخت و سفارش را در یک تراکنش دیتابیس انجام دهید.
- عملیات جانبی مانند شارژ کیف پول را نیز به شناسه یکتای عملیات متصل کنید.
توجه کنید که بررسی سادهای مانند if ($payment->status !== 'paid') بهتنهایی در برابر دو درخواست همزمان کافی نیست. برای عملیات حساس از تراکنش دیتابیس، قفل مناسب و محدودیتهای یکتای دیتابیس استفاده کنید.
۱۰. مدیریت خطاهای درگاه
یک سیستم پرداخت خوب فقط مسیر موفقیت را مدیریت نمیکند. خطاهای زیر نیز باید در نظر گرفته شوند:
| خطا | رفتار پیشنهادی |
|---|---|
| قطع ارتباط با درگاه | حفظ وضعیت قابل پیگیری و امکان استعلام مجدد |
| رد درخواست پرداخت | ثبت نتیجه ناموفق و ارائه پیام مناسب |
| callback ناقص | رد امن درخواست و ثبت گزارش غیرحساس |
| مغایرت مبلغ | جلوگیری از نهاییکردن پرداخت و بررسی مورد |
| ارسال چندباره callback | بازگرداندن نتیجه قبلی بدون اجرای مجدد عملیات مالی |
| پرداخت موفق با خطای ثبت داخلی | امکان بازیابی و تطبیق مجدد تراکنش |
در زمان ثبت خطا، اطلاعاتی مانند شناسه داخلی پرداخت و سفارش را ثبت کنید تا عیبیابی آسان باشد. کلیدهای محرمانه و اطلاعات حساس نباید در گزارشها قرار گیرند.
۱۱. استفاده از پکیج آماده یا پیادهسازی مستقیم؟
برای اتصال درگاه پرداخت به Laravel دو رویکرد اصلی وجود دارد.
استفاده از پکیج آماده: اگر پکیج معتبر و بهروز برای درگاه موردنظر وجود داشته باشد، میتواند کار توسعه را ساده کند. قبل از استفاده، نسخه Laravel و PHP، وضعیت نگهداری پکیج، مستندات و سازگاری آن با API فعلی درگاه را بررسی کنید.
پیادهسازی مستقیم API: در این روش، درخواستها و پاسخها را با ابزارهایی مانند HTTP Client داخلی Laravel مدیریت میکنید. این رویکرد کنترل بیشتری در اختیار شما قرار میدهد، اما مسئولیت پیادهسازی صحیح قرارداد درگاه، مدیریت خطاها و بهروزرسانی API نیز بر عهده شما خواهد بود.
در هر دو روش، منطق سفارش، کنترل مالکیت، ثبت تاریخچه و جلوگیری از تأیید تکراری باید در برنامه شما بهدرستی پیادهسازی شود. پکیج پرداخت جایگزین طراحی صحیح فرایند مالی نیست.
۱۲. چکلیست قبل از انتشار
قبل از فعالکردن درگاه در محیط عملیاتی، این موارد را بررسی کنید:
- [ ] کلیدهای درگاه در فایل محیطی نگهداری میشوند.
- [ ] callback با آدرس ثبتشده در پنل درگاه هماهنگ است.
- [ ] مبلغ سفارش فقط از سمت سرور محاسبه میشود.
- [ ] واحد پول در کل سیستم مشخص است.
- [ ] سفارش و تراکنش پرداخت جداگانه نگهداری میشوند.
- [ ] نتیجه پرداخت از API رسمی درگاه تأیید میشود.
- [ ] callback تکراری باعث اجرای دوباره عملیات مالی نمیشود.
- [ ] خطاهای شبکه و وضعیتهای نامشخص قابل پیگیری هستند.
- [ ] دسترسی کاربر به سفارش دیگران کنترل میشود.
- [ ] گزارشهای پرداخت شامل اطلاعات محرمانه نیستند.
- [ ] سناریوهای موفق، ناموفق، timeout و callback تکراری تست شدهاند.
سوالات متداول
چگونه درگاه پرداخت را به Laravel متصل کنیم؟
ابتدا دسترسی و تنظیمات درگاه را دریافت کنید، سپس درخواست پرداخت را از سمت سرور ارسال کنید، کاربر را به درگاه هدایت کنید و پس از بازگشت، تراکنش را از طریق API رسمی تأیید کنید.
آیا برای اتصال همه درگاههای ایرانی میتوان از یک کد استفاده کرد؟
معماری کلی میتواند مشترک باشد، اما جزئیات API، شناسهها، واحد مبلغ و روش تأیید هر درگاه متفاوت است. بهتر است برای هر ارائهدهنده یک پیادهسازی مستقل داشته باشید.
چرا callback بهتنهایی برای تأیید پرداخت کافی نیست؟
چون پارامترهای callback ممکن است قابل دستکاری باشند یا فقط نتیجه اولیه عملیات را نشان دهند. تأیید نهایی باید از طریق روش رسمی و معتبر درگاه انجام شود.
آیا استفاده از پکیج پرداخت ضروری است؟
خیر. میتوانید از پکیج معتبر یا API مستقیم درگاه استفاده کنید. انتخاب مناسب به پیچیدگی پروژه، سازگاری پکیج و نیاز به کنترل جزئیات بستگی دارد.
اگر کاربر پرداخت کند ولی نتیجه در سایت ثبت نشود، چه باید کرد؟
باید امکان استعلام مجدد از درگاه و تطبیق وضعیت پرداخت با رکورد داخلی وجود داشته باشد. این وضعیت نباید بدون بررسی بهصورت خودکار موفق یا ناموفق قطعی اعلام شود.
جمعبندی
اتصال درگاه پرداخت ایرانی به Laravel فقط شامل ارسال کاربر به صفحه پرداخت نیست. برای پیادهسازی صحیح، باید درخواست پرداخت، callback، تأیید سمت سرور، مدیریت خطا و ثبت وضعیت تراکنش را بهصورت هماهنگ طراحی کنید.
با تفکیک سرویس درگاه از منطق سفارش و استفاده از کنترلهای مناسب دیتابیس، میتوانید ساختاری قابل توسعه ایجاد کنید که درگاههای مختلف را پشتیبانی کند و در برابر درخواستهای تکراری و خطاهای ارتباطی مقاومتر باشد.





