syedmahamudul / laravel-stripe
Complete Stripe Payment Gateway Integration for ALL Laravel & PHP Versions
Requires
- php: >=7.0
- ext-curl: *
- ext-json: *
- stripe/stripe-php: ^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0|^14.0
Requires (Dev)
- phpunit/phpunit: ^7.0|^8.0|^9.0|^10.0
Suggests
- illuminate/contracts: Required for Laravel integration (any version)
- illuminate/database: Required for Laravel integration (any version)
- illuminate/http: Required for Laravel integration (any version)
- illuminate/routing: Required for Laravel integration (any version)
- illuminate/support: Required for Laravel integration (any version)
- laravel/framework: Required for Laravel integration (any version 5.5 - 11.x)
- orchestra/testbench: Required for testing (any version)
README
This package is built for Stripe online payment gateway. It supports Laravel 5.6+, 6.x, 7.x, 8.x, 9.x, 10.x, and 11.x, 12.x, 13.x and works with PHP 7.4 to 8.6+.
🚀 Features
- ✅ Easy Installation - One command installation
- ✅ Fluent API - Chainable methods for building payment requests
- ✅ Automatic Validation - Built-in payment validation
- ✅ IPN Support - Instant Payment Notification handling
- ✅ Refund Functionality - Process refunds easily
- ✅ Transaction Query - Check transaction status
- ✅ Sandbox/Live Mode - Easy switching between test and production
- ✅ All Laravel Versions - Works with Laravel 5.6 to 13.x
- ✅ PHP 7.4 to 8.6+ - Compatible with all PHP versions
- ✅ No Version Conflicts - Works with any Laravel project
- ✅ Comprehensive Error Handling - Detailed error messages
- ✅ Event-driven Architecture - Events for payment statuses
- ✅ Comprehensive Logging - Debug and track payments
- ✅ EMI Support - Easy EMI payment integration
- ✅ Card BIN Restriction - Restrict payments to specific card BINs
- ✅ Custom Callback URLs - Customize success/failure URLs
- ✅ Multiple Products - Support for multiple products in one transaction
- ✅ Checkout Integration - AJAX/JSON checkout mode support
📋 Table of Contents
- Installation
- Usage
- Available Methods
- Advanced Usage
- Security
- Testing
- Troubleshooting
- Changelog
- License
Installation
Requirements
- PHP 7.4 or higher
- Laravel 5.6 or higher
- Stripe Merchant Account (Sandbox or Live)
Install via Composer
composer require syedmahamudul/laravel-stripe
Publish Configuration
After installing the package, publish the configuration file.
Option 1: Automatic Installation (Recommended)
Run the installer command:
php artisan stripe:install
This command will:
- Publish the configuration file
- Create the required configuration
- Display the next setup instructions
Option 2: Manual Configuration
If you don't want to use the automatic installer, you can publish the configuration file manually.
Run the following command:
php artisan vendor:publish --tag=stripe-config
After running the command, Laravel will publish the package configuration file to your application.
You should see output similar to:
INFO Publishing [stripe-config] assets.
Copied File:
config/stripe.php
The published configuration file will be located at:
config/stripe.php
You can now customize the package settings and configure your Stripe credentials using your .env file.
Note: After publishing the configuration file, clear Laravel's configuration cache to ensure the new settings are loaded.
php artisan config:clear php artisan cache:clear php artisan optimize:clear
Verify the Configuration
After publishing, ensure the configuration file exists:
config/
└── stripe.php
Setup Environment
Update your .env file with Stripe credentials:
Sandbox/Test Mode:
Update your .env file with your sandbox/live credentials:
STRIPE_API_KEY=Stripe_API_Key // Live or Sandbox API Key STRIPE_API_SECRET=Stripe_API_Secret // Live or Sandbox API Secret STRIPE_WEBHOOK_SECRET=stripe_webhook_url STRIPE_CURRENCY=usd STRIPE_SUCCESS_URL=/payment/success STRIPE_CANCEL_URL=/payment/cancel STRIPE_WEBHOOK_URL=/stripe/webhook
Important View File
You must create a view file for the folder payment and view file name form.blade.php, failed.blade.php, success.blade.php, Or use your custom view file name
Create View Controller
/** * Show payment form */ public function index() { return view('payment.index'); }
Create Routes
Create routes for Stripe callbacks in routes/web.php:
<?php use App\Http\Controllers\PaymentController; use App\Http\Controllers\StripeWebhookController; use App\Http\Controllers\SubscriptionController; use Illuminate\Support\Facades\Route; // Payment Routes Route::get('/', [PaymentController::class, 'index'])->name('payment.index'); Route::post('/payment/process', [PaymentController::class, 'process'])->name('payment.process'); Route::get('/payment/success', [PaymentController::class, 'success'])->name('payment.success'); Route::get('/payment/success/{payment_id}', [PaymentController::class, 'showSuccess'])->name('payment.success.show'); Route::get('/payment/cancel', [PaymentController::class, 'cancel'])->name('payment.cancel'); Route::get('/payment/failed', [PaymentController::class, 'failed'])->name('payment.failed'); Route::post('/payment/refund/{paymentIntentId}', [PaymentController::class, 'refund'])->name('payment.refund'); Route::get('/payment/status/{paymentIntentId}', [PaymentController::class, 'status'])->name('payment.status'); // Subscription Routes Route::get('/subscription/plans', [SubscriptionController::class, 'plans'])->name('subscription.plans'); Route::post('/subscription/subscribe', [SubscriptionController::class, 'subscribe'])->name('subscription.subscribe'); Route::delete('/subscription/cancel/{subscriptionId}', [SubscriptionController::class, 'cancel'])->name('subscription.cancel'); Route::post('/subscription/resume/{subscriptionId}', [SubscriptionController::class, 'resume'])->name('subscription.resume'); Route::put('/subscription/update/{subscriptionId}', [SubscriptionController::class, 'update'])->name('subscription.update'); Route::get('/subscription/my', [SubscriptionController::class, 'mySubscriptions'])->name('subscription.my'); Route::get('/subscription/success', [SubscriptionController::class, 'success'])->name('subscription.success'); Route::get('/subscription/failed', [SubscriptionController::class, 'failed'])->name('subscription.failed'); // Webhook Route (No CSRF protection) Route::post('/stripe/webhook', [StripeWebhookController::class, 'handle']) ->name('stripe.webhook') ->withoutMiddleware(['web', 'csrf']);
Create the Payment Form
Create a payment form where customers can enter their order and billing information before initiating the payment.
Create a Blade view at:
resources/views/payment/index.blade.php
<!DOCTYPE html> <html> <head> <title>Make Payment</title> <meta name="csrf-token" content="{{ csrf_token() }}"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <div class="container mt-5"> <div class="row justify-content-center"> <div class="col-md-6"> <div class="card"> <div class="card-header"> <h4>Make Payment</h4> </div> <div class="card-body"> @if(session('error')) <div class="alert alert-danger">{{ session('error') }}</div> @endif <form action="{{ route('payment.process') }}" method="POST" id="payment-form"> @csrf <div class="mb-3"> <label for="amount" class="form-label">Amount (USD)</label> <input type="number" class="form-control @error('amount') is-invalid @enderror" id="amount" name="amount" step="0.01" min="0.5" required> @error('amount') <div class="invalid-feedback">{{ $message }}</div> @enderror </div> <div class="mb-3"> <label for="name" class="form-label">Full Name</label> <input type="text" class="form-control @error('name') is-invalid @enderror" id="name" name="name" required> @error('name') <div class="invalid-feedback">{{ $message }}</div> @enderror </div> <div class="mb-3"> <label for="email" class="form-label">Email</label> <input type="email" class="form-control @error('email') is-invalid @enderror" id="email" name="email" required> @error('email') <div class="invalid-feedback">{{ $message }}</div> @enderror </div> <div class="mb-3"> <label for="product_name" class="form-label">Product Name</label> <input type="text" class="form-control" id="product_name" name="product_name"> </div> <button type="submit" class="btn btn-primary w-100">Pay Now</button> </form> </div> </div> </div> </div> </div> </body> </html>
Create Confirm Payment Form
Create a payment form where customers can enter their order and billing information before initiating the payment.
Create a Blade view at:
resources/views/payment/confirm.blade.php
<!DOCTYPE html> <html> <head> <title>Confirm Payment</title> <meta name="csrf-token" content="{{ csrf_token() }}"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> <script src="https://js.stripe.com/v3/"></script> <style> .payment-card { border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); } .payment-header { background: linear-gradient(135deg, #0d6efd, #0a58ca); color: white; padding: 20px; border-radius: 15px 15px 0 0; } .stripe-element { background: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 12px; } #payment-element { min-height: 100px; } .btn-pay { background: linear-gradient(135deg, #28a745, #20c997); border: none; padding: 12px; font-weight: 600; transition: all 0.3s ease; } .btn-pay:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(40, 167, 69, 0.3); } .btn-pay:disabled { opacity: 0.7; transform: none; } </style> </head> <body> <div class="container mt-5"> <div class="row justify-content-center"> <div class="col-md-6"> <div class="card payment-card"> <div class="payment-header text-center"> <h4><i class="fas fa-lock me-2"></i> Complete Payment</h4> </div> <div class="card-body p-4"> <div class="alert alert-info"> <div class="d-flex justify-content-between"> <span><strong>Amount:</strong></span> <span> @if(function_exists('format_currency')) {{ format_currency($amount, $currency) }} @else {{ number_format($amount, 2) }} {{ strtoupper($currency) }} @endif </span> </div> <div class="d-flex justify-content-between mt-1"> <span><strong>Currency:</strong></span> <span>{{ strtoupper($currency) }}</span> </div> </div> <div class="mb-4"> <label class="form-label fw-bold">Card Details</label> <div class="stripe-element"> <div id="payment-element"></div> </div> </div> <button id="submit-button" class="btn btn-success w-100 btn-pay"> <i class="fas fa-lock me-2"></i>Pay Now </button> <div id="error-message" class="mt-3 text-danger"></div> <div class="mt-3 text-center"> <a href="{{ route('payment.cancel') }}" class="text-decoration-none text-muted"> <i class="fas fa-times me-1"></i> Cancel </a> </div> </div> </div> </div> </div> </div> <script> document.addEventListener('DOMContentLoaded', function() { const stripe = Stripe('{{ config('stripe.api_key') }}'); const clientSecret = '{{ $clientSecret }}'; const elements = stripe.elements({ clientSecret: clientSecret, appearance: { theme: 'stripe', variables: { colorPrimary: '#0d6efd', colorBackground: '#ffffff', colorText: '#212529', fontFamily: 'system-ui, -apple-system, sans-serif', borderRadius: '8px', }, }, }); const paymentElement = elements.create('payment'); paymentElement.mount('#payment-element'); const submitButton = document.getElementById('submit-button'); const errorElement = document.getElementById('error-message'); submitButton.addEventListener('click', async () => { submitButton.disabled = true; submitButton.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Processing...'; errorElement.textContent = ''; try { const { error } = await stripe.confirmPayment({ elements, confirmParams: { return_url: '{{ route('payment.success') }}', }, }); if (error) { errorElement.textContent = error.message; submitButton.disabled = false; submitButton.innerHTML = '<i class="fas fa-lock me-2"></i>Pay Now'; } // If no error, Stripe will redirect to return_url } catch (err) { errorElement.textContent = 'An unexpected error occurred. Please try again.'; submitButton.disabled = false; submitButton.innerHTML = '<i class="fas fa-lock me-2"></i>Pay Now'; } }); }); </script> </body> </html>
Process Payment
Create a controller to handle Stripe payment requests and callback responses.
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Syedmahamudul\LaravelStripe\Services\PaymentService; use Syedmahamudul\LaravelStripe\Exceptions\PaymentFailedException; use Syedmahamudul\LaravelStripe\Models\Payment; use Illuminate\Support\Facades\Log; class PaymentController extends Controller { protected $paymentService; public function __construct(PaymentService $paymentService) { $this->paymentService = $paymentService; } /** * Show payment form */ public function index() { return view('payment.index'); } public function process(Request $request) { $request->validate([ 'amount' => 'required|numeric|min:0.5', 'name' => 'required|string|max:255', 'email' => 'required|email', ]); try { $paymentData = [ 'amount' => $request->amount, 'email' => $request->email, 'name' => $request->name, 'metadata' => [ 'product_name' => $request->product_name ?? 'Payment', 'order_id' => $request->order_id ?? 'ORD-' . time(), ], ]; $payment = $this->paymentService->processPayment($paymentData); return view('payment.confirm', [ 'clientSecret' => $payment['client_secret'], 'paymentIntentId' => $payment['payment_intent_id'], 'amount' => $request->amount, 'currency' => config('stripe.currency', 'usd'), ]); } catch (PaymentFailedException $e) { return back()->with('error', 'Payment failed: ' . $e->getMessage()); } catch (\Exception $e) { return back()->with('error', 'An error occurred: ' . $e->getMessage()); } } }
Success Payment
Create a controller to handle Stripe payment requests and callback responses.
<?php public function success(Request $request) { $paymentIntentId = $request->query('payment_intent'); $redirectStatus = $request->query('redirect_status'); Log::info('Payment success redirect', [ 'payment_intent' => $paymentIntentId, 'redirect_status' => $redirectStatus, 'all_params' => $request->all() ]); // If we have payment_intent from query string if ($paymentIntentId) { try { // First try to get payment from database $payment = Payment::where('payment_intent_id', $paymentIntentId)->first(); if ($payment) { // Update payment status if needed if ($payment->status !== 'completed' && $redirectStatus === 'succeeded') { $payment->update([ 'status' => 'completed', 'paid_at' => now(), ]); } // Redirect to success page with payment details return redirect()->route('payment.success.show', [ 'payment_id' => $payment->id ]); } else { // Payment not found in database, create a record try { // Get payment from Stripe API $stripePayment = $this->paymentService->getPayment($paymentIntentId); if (!$stripePayment) { // Create a new payment record $payment = Payment::create([ 'payment_intent_id' => $paymentIntentId, 'amount' => 0, 'currency' => config('stripe.currency', 'usd'), 'status' => $redirectStatus === 'succeeded' ? 'completed' : 'pending', 'paid_at' => $redirectStatus === 'succeeded' ? now() : null, 'metadata' => ['source' => 'stripe_redirect'], ]); return redirect()->route('payment.success.show', [ 'payment_id' => $payment->id ]); } } catch (\Exception $e) { Log::error('Error creating payment record: ' . $e->getMessage()); } } } catch (\Exception $e) { Log::error('Payment success processing error: ' . $e->getMessage()); } } // If no payment_intent, show generic success return view('payment.success_redirect', [ 'paymentIntentId' => $paymentIntentId, 'status' => $redirectStatus ?? 'succeeded', 'amount' => 0, 'currency' => config('stripe.currency', 'usd'), ]); }
Show Payment Success
Create a controller to handle Stripe payment requests and callback responses.
<?php public function showSuccess($paymentId) { try { $payment = Payment::findOrFail($paymentId); return view('payment.success', [ 'payment' => $payment, 'paymentId' => $payment->id, 'amount' => $payment->amount, 'currency' => $payment->currency, 'paymentIntentId' => $payment->payment_intent_id, 'status' => $payment->status, ]); } catch (\Exception $e) { Log::error('Error showing success page: ' . $e->getMessage()); return view('payment.success_redirect', [ 'paymentIntentId' => null, 'status' => 'succeeded', 'amount' => 0, 'currency' => config('stripe.currency', 'usd'), ]); } }
Cancel Payment
Create a controller to handle Stripe payment requests and callback responses.
<?php /** * Cancel page */ public function cancel(Request $request) { $paymentIntentId = $request->query('payment_intent'); return view('payment.cancel', [ 'paymentIntentId' => $paymentIntentId, 'message' => 'Payment was cancelled' ]); }
Failed Payment
Create a controller to handle Stripe payment requests and callback responses.
<?php /** * Failed page */ public function failed(Request $request) { $paymentIntentId = $request->query('payment_intent'); $errorMessage = session('error') ?? 'Payment failed. Please try again.'; return view('payment.failed', [ 'paymentIntentId' => $paymentIntentId, 'errorMessage' => $errorMessage ]); }
Refund Payment
Create a controller to handle Stripe payment requests and callback responses.
<?php /** * Refund payment */ public function refund(Request $request, $paymentIntentId) { try { $payment = $this->paymentService->refundPayment( $paymentIntentId, $request->amount ?? null ); return response()->json([ 'success' => true, 'payment' => $payment, 'message' => 'Payment refunded successfully' ]); } catch (PaymentFailedException $e) { return response()->json([ 'success' => false, 'message' => $e->getMessage() ], 400); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'An error occurred: ' . $e->getMessage() ], 400); } }
Status Payment
Create a controller to handle Stripe payment requests and callback responses.
<?php /** * Get payment status */ public function status($paymentIntentId) { try { $payment = $this->paymentService->getPayment($paymentIntentId); if (!$payment) { return response()->json([ 'success' => false, 'message' => 'Payment not found' ], 404); } return response()->json([ 'success' => true, 'payment' => $payment, 'status' => $payment->status ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => $e->getMessage() ], 400); } }
Payment Model
Create a Payment model and use Eloquent to temporarily store the order data before redirecting the customer to the Stripe payment gateway.
php artisan make:model Payment -m
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { Schema::create('payments', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('user_id')->nullable(); $table->string('payment_intent_id')->unique(); $table->string('customer_id')->nullable(); $table->decimal('amount', 10, 2); $table->string('currency', 3)->default('usd'); $table->string('status')->default('pending'); $table->json('metadata')->nullable(); $table->json('stripe_data')->nullable(); $table->string('refund_id')->nullable(); $table->decimal('refund_amount', 10, 2)->nullable(); $table->timestamp('refunded_at')->nullable(); $table->timestamp('paid_at')->nullable(); $table->string('checkout_session_id')->nullable(); $table->timestamps(); $table->index('user_id'); $table->index('status'); $table->index('created_at'); }); } public function down(): void { Schema::dropIfExists('payments'); } };
Store the Subscription
Create a Subscription model and use Eloquent to temporarily store the order data before redirecting the customer to the Stripe payment gateway.
php artisan make:model Subscription -m
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { Schema::create('subscriptions', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('user_id')->nullable(); $table->string('customer_id'); $table->string('subscription_id')->unique(); $table->string('price_id'); $table->string('status'); $table->timestamp('trial_ends_at')->nullable(); $table->timestamp('ends_at')->nullable(); $table->json('metadata')->nullable(); $table->json('stripe_data')->nullable(); $table->timestamps(); $table->index('user_id'); $table->index('status'); $table->index('created_at'); }); } public function down(): void { Schema::dropIfExists('subscriptions'); } };
Note: The order data is stored temporarily and should only be persisted to the
orderstable after the payment is completed successfully.
Clear Cache
After configuration:
php artisan config:clear php artisan cache:clear php artisan optimize:clear
Security
- ✅ Callback URLs are automatically excluded from CSRF verification.
- ✅ IPN requests are validated.
- ✅ Amount and Transaction ID are verified.
- ✅ No sensitive data is stored in logs.
- ✅ Sandbox mode supported.
- ✅ SSL/TLS encryption.
- ✅ Validation against Stripe API.
Testing
Run the following command:
php artisan stripe:make-payment 100 --product="Test Product"
This command initiates a sandbox payment and displays the payment URL.
Troubleshooting
1. Could not find a matching version
Cause
Package version not tagged.
Solution
Use dev-main or create a Git tag.
Author
Syed Mahamudul Hassan
- GitHub: syedmahamudul
- Email: syedmahamudhassan@gmail.com
License
This package is open-sourced software licensed under the MIT License.
See the LICENSE file for more information.
Support
If this package helps you, please consider giving it a ⭐ on GitHub.