x-laravel/payline
Composer 安装命令:
composer require x-laravel/payline
包简介
Laravel payment gateway abstraction layer
README 文档
README
A modern, DTO-based Laravel payment gateway abstraction layer. Driver packages for individual gateways (hoppa, iyzico, qnb-vpos) extend this core package.
How It Works
- Implement the
Payableinterface on any model (Order, Invoice, etc.) to make it payable - Add the
HasPaylinetrait to get payment relationships and apay()shortcut - Every gateway operation automatically creates a
Payment+Transactionrecord in the database - 3DS callbacks and server-to-server webhooks are handled via built-in routes
- Laravel Events are dispatched on every status change
- Driver packages register themselves via
extend()— one line, no core changes needed - BIN lookup resolves card family and type automatically, enabling commission-based auto-routing
Requirements
- PHP ^8.3
- Laravel ^12.0 | ^13.0
- At least one driver package (
x-laravel/payline-hoppa,x-laravel/payline-iyzico, etc.)
Installation
composer require x-laravel/payline
Run the migrations:
php artisan migrate
Optionally publish the config:
php artisan vendor:publish --tag=payline-config
Setup
1. Implement Payable
Add the Payable interface and HasPayline trait to any model you want to charge for:
use Illuminate\Database\Eloquent\Model; use XLaravel\Payline\Contracts\Payable; use XLaravel\Payline\Traits\HasPayline; class Order extends Model implements Payable { use HasPayline; public function getPayableAmount(): int { return $this->total; } public function getPayableCurrency(): string { return $this->currency; } public function getPayableReference(): string { return $this->order_number; } }
HasPayline provides default implementations for getPayableCurrency() ('TRY'), getPayableCustomerEmail(), getPayableCustomerName(), and getPayableDescription() — override only what you need.
2. Install a Driver
Install and configure at least one gateway driver. Refer to the driver package's README for gateway-specific setup.
composer require x-laravel/payline-iyzico
Set the default gateway in your .env:
PAYLINE_DRIVER=iyzico
Usage
Making a Payment
use XLaravel\Payline\DTOs\Card; use XLaravel\Payline\DTOs\PaymentRequest; $data = PaymentRequest::fromPayable( payable: $order, card: new Card( holderName: 'John Doe', number: '4111111111111111', expiryMonth: '12', expiryYear: '2030', cvv: '123', ), customerIp: $request->ip(), ); $response = $order->pay('iyzico')->charge($data);
Security:
Cardimplements__debugInfo()— CVV and the full card number are automatically masked invar_dump(),dd(), logs, and tools like Telescope. The raw values are never leaked through debug output.
Or using the facade:
use XLaravel\Payline\Facades\Payline; $response = Payline::for($order)->via('iyzico')->charge($data);
Handling the Response
if ($response->isSuccessful()) { // Payment complete $response->gatewayTransactionId; } if ($response->requiresRedirect()) { // 3DS flow return redirect($response->redirectUrl); // or render a POST form: $response->redirectForm } if ($response->isFailure()) { $response->errorCode; $response->errorMessage; }
Authorization & Capture
// 1. Reserve funds without capturing $response = $order->pay()->authorize($data); // 2. Capture later use XLaravel\Payline\DTOs\CaptureData; $response = Payline::via()->capture( new CaptureData(gatewayTransactionId: $transaction->gateway_transaction_id, amount: $transaction->amount), $payment, $transaction, );
Refund & Void
use XLaravel\Payline\DTOs\RefundData; use XLaravel\Payline\DTOs\VoidData; // Partial or full refund Payline::via()->refund( new RefundData(gatewayTransactionId: $transaction->gateway_transaction_id, amount: 5000, reason: 'Customer request'), $payment, $transaction, ); // Void an authorization (before capture) Payline::via()->void( new VoidData(gatewayTransactionId: $transaction->gateway_transaction_id), $payment, $transaction, );
Three Access Levels
Payline::driver('iyzico') // raw Gateway — no DB recording Payline::via('iyzico') // PendingPayment — recording + events Payline::for($order)->via('iyzico') // same, with a Payable bound $order->pay('iyzico') // explicit driver via HasPayline trait $order->pay() // auto-routing: cheapest gateway selected by GatewayRouter
Commission Routing
Automatically route payments to the cheapest gateway based on card family, card type, and installment count. Rates are stored in the database (payline_commission_rates) and can be updated without deployment.
Setup
Seed commission rates for each gateway:
use XLaravel\Payline\Models\CommissionRate; // Wildcard: applies to all card families / types CommissionRate::create(['gateway' => 'hoppa', 'card_family' => null, 'card_type' => null, 'installments' => 1, 'rate' => 2.03]); // Specific card family + type CommissionRate::create(['gateway' => 'qnb', 'card_family' => 'CardFinans', 'card_type' => 'credit', 'installments' => 3, 'rate' => 2.92, 'blocking_days' => 3]); CommissionRate::create(['gateway' => 'hoppa', 'card_family' => 'Bonus', 'card_type' => 'credit', 'installments' => 3, 'rate' => 2.03]);
Soft-delete a rate to deactivate it without losing history:
CommissionRate::find($id)->delete();
Usage
CardProfile can be provided manually or resolved automatically via BIN Lookup.
use XLaravel\Payline\DTOs\Card; use XLaravel\Payline\DTOs\CardProfile; use XLaravel\Payline\Enums\CardType; // Option A: manual profile $data = PaymentRequest::fromPayable( payable: $order, card: new Card( holderName: 'Ali Veli', number: '4111111111111111', expiryMonth: '12', expiryYear: '2030', cvv: '123', profile: new CardProfile('Bonus', CardType::Credit), ), installments: 3, ); // Option B: BIN lookup (see BIN Lookup section) $data = PaymentRequest::fromPayable( payable: $order, card: (new Card(holderName: 'Ali Veli', number: '4111111111111111', ...))->resolveProfile(app(\XLaravel\Payline\BinLookupManager::class)), installments: 3, ); // Auto-route — cheapest gateway is selected automatically $order->pay()->charge($data); // Explicit driver — skip routing $order->pay('iyzico')->charge($data); // Query directly Payline::cheapestFor(new CardProfile('Bonus', CardType::Credit), installments: 3); // → 'hoppa' // Full ranked list app(\XLaravel\Payline\Routing\GatewayRouter::class) ->rankedFor(new CardProfile('Bonus', CardType::Credit), 3); // → ['hoppa' => 2.03, 'qnb' => 2.92]
Matching priority: Rows with exact card_family + card_type take precedence over wildcards (null). If no row matches, the configured default gateway is used.
BIN Lookup
Automatically resolve a card's family and type from its BIN (first 8 digits of the card number), eliminating the need to pass CardProfile manually. BIN lookup drivers are provided by gateway packages — the core package ships with a null driver that always returns null.
Setup
Install a driver package that supports BIN lookup and set the driver in .env:
PAYLINE_BIN_LOOKUP_DRIVER=iyzico
Usage
use XLaravel\Payline\BinLookupManager; use XLaravel\Payline\DTOs\Card; use XLaravel\Payline\DTOs\PaymentRequest; $card = new Card( holderName: 'Ali Veli', number: '4111111111111111', expiryMonth: '12', expiryYear: '2030', cvv: '123', ); $data = PaymentRequest::fromPayable( payable: $order, card: $card->resolveProfile(app(BinLookupManager::class)), installments: 3, ); // CardProfile resolved — GatewayRouter picks the cheapest gateway automatically $order->pay()->charge($data);
resolveProfile() calls the active BIN lookup driver internally and returns the card with its profile set. If the driver returns null (unknown card or no driver configured), the card is returned unchanged and the default gateway is used.
Writing a BIN Lookup Driver
Implement BinLookupProvider and register it in your driver package's ServiceProvider:
use XLaravel\Payline\Contracts\BinLookupProvider; use XLaravel\Payline\DTOs\CardProfile; class MyBinLookupProvider implements BinLookupProvider { public function __construct(private array $config) {} public function lookup(string $bin): ?CardProfile { // Call your BIN lookup API with $bin (first 8 digits, already extracted) $result = $this->callApi($bin); if (! $result) { return null; } return new CardProfile($result['family'], CardType::from($result['type'])); } }
public function boot(): void { $this->app->make('payline.bin_lookup')->extend('my-gateway', function ($app, array $config) { return new MyBinLookupProvider($config); }); }
The $config array is automatically injected from config('payline.bin_lookup.drivers.my-gateway').
HasPayline Trait
Add HasPayline to any model to get relationships and helpers:
$order->payments() // MorphMany — all payments for this model $order->successfulPayments() // only successful ones $order->pendingPayments() // initiated + pending $order->amountPaid() // int — total charged (in kuruş) $order->lastPayment() // latest Payment model, or null $order->pay() // start a payment (auto-route via GatewayRouter) $order->pay('iyzico') // start a payment (explicit driver)
Address & BasketItem
Use typed DTOs instead of plain arrays for billing/shipping addresses and basket items:
use XLaravel\Payline\DTOs\Address; use XLaravel\Payline\DTOs\BasketItem; $data = PaymentRequest::fromPayable( payable: $order, card: $card, billingAddress: new Address( name: 'John Doe', line1: '123 Main St', city: 'Istanbul', country: 'TR', zipCode: '34000', ), shippingAddress: new Address( name: 'John Doe', line1: '456 Other St', city: 'Ankara', country: 'TR', ), basketItems: [ new BasketItem(id: 'SKU-1', name: 'T-Shirt', category: 'Clothing', price: 15000, quantity: 2), new BasketItem(id: 'SKU-2', name: 'Shipping', category: 'Delivery', price: 1000), ], );
PaymentResponse
All gateway operations return a unified PaymentResponse DTO:
| Property | Type | Description |
|---|---|---|
status |
TransactionStatus |
initiated / pending / authorized / successful / failed / refunded / voided / expired |
type |
TransactionType |
payment / authorization / capture / refund / void |
gatewayName |
string |
|
gatewayTransactionId |
?string |
|
gatewayOrderId |
?string |
|
gatewayAuthCode |
?string |
|
gatewayResponseCode |
?string |
Raw response code from the gateway |
gatewayResponseMessage |
?string |
Raw response message from the gateway |
amount |
int |
kuruş |
currency |
string |
|
redirectUrl |
?string |
3DS redirect target |
redirectForm |
?string |
POST form HTML |
errorCode |
?string |
|
errorMessage |
?string |
|
eventType |
?string |
Webhook event type (e.g. payment.captured, refund.created) — set by parseWebhook() |
metadata |
?array |
Gateway-specific extras |
Helper methods: isSuccessful(), isPending(), isFailure(), requiresRedirect().
Events
Most events carry a Payment and Transaction model. Exceptions are noted below.
| Event | Fired when | Extra payload |
|---|---|---|
PaymentInitiated |
Before the gateway call | PaymentRequest |
PaymentSucceeded |
Gateway confirms success | PaymentResponse |
PaymentPending |
Gateway redirects to 3DS (status = pending) | PaymentResponse |
PaymentFailed |
Gateway returns a failure response | PaymentResponse |
PaymentErrored |
Gateway call throws an exception (network, timeout, parse error) | Throwable |
PaymentAuthorized |
Pre-authorization succeeds | PaymentResponse |
PaymentCaptured |
Capture succeeds | PaymentResponse |
PaymentRefunded |
Refund succeeds | PaymentResponse |
PaymentVoided |
Void succeeds | PaymentResponse |
WebhookReceived |
Webhook processed | PaymentResponse + raw payload |
CallbackUnmatched |
Callback received but no matching transaction found | gateway (string) + PaymentResponse |
use XLaravel\Payline\Events\PaymentSucceeded; use XLaravel\Payline\Events\PaymentPending; use XLaravel\Payline\Events\PaymentFailed; use XLaravel\Payline\Events\PaymentErrored; use XLaravel\Payline\Events\CallbackUnmatched; class SendPaymentConfirmation { public function handle(PaymentSucceeded $event): void { $event->payment->payable->sendConfirmationEmail(); } } // Listen for 3DS redirect class HandlePendingPayment { public function handle(PaymentPending $event): void { // $event->response->redirectUrl is ready; store payment ID in session if needed } } // Gateway returned a failure response (e.g. insufficient funds, card declined) class HandlePaymentFailed { public function handle(PaymentFailed $event): void { Log::info('Payment declined', [ 'payment_id' => $event->payment->id, 'error_code' => $event->response->errorCode, 'error_message' => $event->response->errorMessage, ]); } } // Gateway call threw an exception (network error, timeout, parse failure) class HandlePaymentErrored { public function handle(PaymentErrored $event): void { Log::error('Payment gateway exception', [ 'payment_id' => $event->payment->id, 'error' => $event->exception->getMessage(), ]); } } // Alert on unmatched callbacks (e.g. double delivery, wrong gateway config) class AlertUnmatchedCallback { public function handle(CallbackUnmatched $event): void { Log::warning('Unmatched payment callback', [ 'gateway' => $event->gateway, 'gateway_order_id' => $event->response->gatewayOrderId, ]); } }
Webhooks
Payline registers a CSRF-exempt webhook route automatically:
POST /payline/webhooks/{gateway}
Point your gateway's dashboard to this URL. The controller verifies the signature, parses the payload, finds the matching transaction, updates it, and dispatches WebhookReceived. Signature verification is handled per-driver via Gateway::verifyWebhook().
Models
Payment
payline_payments — one record per checkout attempt.
$payment->payable; // polymorphic — Order, Invoice, etc. $payment->owner; // polymorphic — User, etc. $payment->transactions(); // all gateway calls for this payment $payment->latestTransaction; // HasOne — latest transaction (eager-loadable) $payment->successfulTransaction; // HasOne — successful payment tx (eager-loadable) $payment->refunds(); // HasMany — all refund transactions $payment->isSuccessful(); $payment->isPending(); $payment->totalRefunded(); // int, kuruş $payment->remainingRefundable(); // int, kuruş $payment->nextAttemptNumber(); // Eager load to avoid N+1 Payment::with('latestTransaction', 'successfulTransaction')->get();
Transaction
payline_transactions — one row per gateway API call (pay, capture, refund, void, webhook update).
$transaction->payment; // BelongsTo Payment $transaction->parent; // BelongsTo Transaction (refund/capture source) $transaction->children(); // HasMany
Writing a Driver
Implement Gateway and register it in your ServiceProvider:
use XLaravel\Payline\Contracts\Gateway; class MyGatewayDriver implements Gateway { public function __construct(private array $config) {} public function pay(PaymentRequest $data): PaymentResponse { ... } public function authorize(PaymentRequest $data): PaymentResponse { ... } public function capture(CaptureData $data): PaymentResponse { ... } public function refund(RefundData $data): PaymentResponse { ... } public function void(VoidData $data): PaymentResponse { ... } public function handleCallback(CallbackData $data): PaymentResponse { ... } public function verifyWebhook(array $payload, string $signature): bool { ... } public function parseWebhook(array $payload): PaymentResponse { ... } // set eventType (e.g. 'payment.captured') public function supportedMethods(): array { return [PaymentMethod::CreditCard]; } public function getName(): string { return 'my-gateway'; } }
Register in your driver package's ServiceProvider:
public function boot(): void { $this->app->make('payline')->extend('my-gateway', function ($app, array $config) { return new MyGatewayDriver($config); }); // Optional: register a BIN lookup driver $this->app->make('payline.bin_lookup')->extend('my-gateway', function ($app, array $config) { return new MyBinLookupProvider($config); }); }
The $config array for the gateway driver is injected from config('payline.gateways.my-gateway'). For the BIN lookup driver, it comes from config('payline.bin_lookup.drivers.my-gateway').
Configuration
// config/payline.php return [ 'default' => env('PAYLINE_DRIVER'), 'gateways' => [ 'iyzico' => [ 'api_key' => env('IYZICO_API_KEY'), 'secret_key' => env('IYZICO_SECRET_KEY'), 'base_url' => env('IYZICO_BASE_URL', 'https://sandbox-api.iyzipay.com'), ], ], 'bin_lookup' => [ 'default' => env('PAYLINE_BIN_LOOKUP_DRIVER', 'null'), // 'null' = disabled 'drivers' => [ // driver-specific config (injected into BinLookupProvider constructor) // 'my-gateway' => ['api_key' => env('MY_GATEWAY_API_KEY')], ], ], 'routes' => [ 'enabled' => true, 'prefix' => 'payline', 'middleware' => ['web'], // applied to both callback and webhook routes 'webhook_middleware' => [], // applied to webhook route only (e.g. ['throttle:60,1']) ], 'models' => [ 'payment' => XLaravel\Payline\Models\Payment::class, 'transaction' => XLaravel\Payline\Models\Transaction::class, 'webhook_log' => XLaravel\Payline\Models\WebhookLog::class, 'commission_rate' => XLaravel\Payline\Models\CommissionRate::class, ], 'database' => [ 'connection' => env('PAYLINE_DB_CONNECTION', env('DB_CONNECTION', 'sqlite')), ], ];
Dedicated Database Connection
Set PAYLINE_DB_CONNECTION in your .env to store all Payline tables (payments, transactions, webhook logs, commission rates) in a separate database:
PAYLINE_DB_CONNECTION=payments
The named connection must exist in config/database.php. When PAYLINE_DB_CONNECTION is not set, the value falls back to DB_CONNECTION (the application's default connection).
Database
payline_payments
├── id (ulid)
├── payable_type/id (polymorphic — Order, Invoice, etc.)
├── owner_type/id (polymorphic — User, etc.)
├── gateway
├── status (TransactionStatus)
├── amount (int, kuruş)
├── currency (char 3)
├── reference (order number, invoice id, etc.)
├── metadata (json, nullable)
└── timestamps
payline_transactions
├── id (ulid)
├── payment_id (FK → payline_payments, cascadeOnDelete)
├── type (TransactionType)
├── status (TransactionStatus)
├── amount, currency
├── attempt (int — retry counter)
├── gateway_transaction_id (indexed)
├── gateway_order_id (indexed)
├── gateway_auth_code
├── gateway_response_code/message
├── error_code/message
├── redirect_url
├── parent_transaction_id (FK → payline_transactions, for refunds/captures)
├── metadata (json, nullable)
└── timestamps
payline_webhook_logs
├── id (ulid)
├── gateway
├── event_type
├── gateway_event_id (unique per gateway)
├── payload (json)
├── status (WebhookStatus enum: received / processing / processed / failed)
├── exception (text, nullable)
├── processed_at
└── timestamps
payline_commission_rates
├── id (ulid)
├── gateway ('hoppa', 'qnb', 'iyzico'…)
├── card_family (string, nullable — null = wildcard)
├── card_type ('credit' / 'debit' / 'foreign_credit', nullable — null = wildcard)
├── installments (int, default 1)
├── rate (decimal 8,4 — e.g. 2.9200 means 2.92%)
├── blocking_days (int, nullable)
├── deleted_at (soft delete — null = active)
└── timestamps
Testing
# Build first (once per PHP version) DOCKER_BUILDKIT=0 docker compose --profile php82 build # Run tests docker compose --profile php82 up docker compose --profile php83 up docker compose --profile php84 up
License
This package is open-sourced software licensed under the MIT license.
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 2
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-06-19