Payment Plugin
Development Guide
Build custom payment integrations for the KobiZeka™ Hotel Management System. This guide covers the complete plugin architecture, from creating a simple offline payment method to building a full 3D Secure online payment gateway.
Introduction
The KobiZeka payment plugin system is designed to be modular and self-contained. Each plugin lives in its own directory under app/plugins/Payment/ and is automatically discovered by the PaymentPluginManager.
Plugins can be:
- Offline — Pay at hotel, bank transfer, cash on delivery
- Online — Credit card gateways (iyzico, PayTR, Stripe, etc.)
The system supports:
- Auto-discovery — just drop a folder with
Provider.php - Built-in settings management — define fields, the admin UI is generated automatically
- Multi-language — each plugin ships its own translation files
- 3D Secure callbacks and webhook handling
- Refund support (optional)
Architecture
The payment system consists of three main components:
| Component | Description |
|---|---|
PaymentProviderInterface |
Contract that all plugins must implement |
PaymentPluginManager |
Singleton that discovers, loads, and manages all plugins |
PluginTranslationTrait |
Provides multi-language support via __p() method |
PaymentController |
Admin panel controller for settings & management |
Plugin Lifecycle
- Discovery — Manager scans
app/plugins/Payment/*/Provider.php - Registration — New plugins are auto-registered in the
payment_pluginstable - Configuration — Admin enables plugin and fills in settings (API keys, etc.)
- Checkout — Active plugins appear on the reservation confirmation page
- Payment — Guest selects method →
createPayment()→ redirect/process - Callback — Gateway sends result →
handleCallback()→ transaction saved
Directory Structure
Provider.php file implementing PaymentProviderInterface. The system will automatically detect it on the next page load.
Quick Start
Create Plugin Directory
Create a new folder: app/plugins/Payment/MyGateway/
Create Provider.php
Implement PaymentProviderInterface with all required methods.
Add Language Files
Create lang/tr.php, lang/en.php, lang/de.php for translations.
Activate in Admin Panel
Navigate to Payment → Plugins. Your plugin appears automatically. Enable it and configure settings.
PaymentProviderInterface
Every plugin must implement this interface. Here is the complete contract:
// app/plugins/Payment/PaymentProviderInterface.php
namespace App\Plugins\Payment;
interface PaymentProviderInterface
{
// ─── Identity ───
public function getSlug(): string; // Unique ID: 'my_gateway'
public function getDisplayName(): string; // 'My Gateway'
public function getDescription(): string; // Short description
public function getIcon(): string; // FontAwesome: 'fa-credit-card'
public function getVersion(): string; // '1.0.0'
public function getAuthor(): string; // 'Your Company'
public function getType(): string; // 'online' or 'offline'
// ─── Configuration ───
public function getSettingsFields(): array; // Admin settings form fields
public function isConfigured(): bool; // Are API keys set?
// ─── Checkout ───
public function renderCheckoutInfo(array $reservation): string;
// ─── Payment ───
public function createPayment(array $reservation, float $amount): array;
public function handleCallback(array $data): array;
// ─── Refunds ───
public function supportsRefund(): bool;
public function refund(string $transactionId, float $amount): array;
}
Method Reference
| Method | Return | Description |
|---|---|---|
getSlug() | string | Unique identifier. Use snake_case. Must be unique across all plugins. |
getDisplayName() | string | Human-readable name. Use $this->__p('name') for i18n. |
getDescription() | string | Short description. Use $this->__p('description') for i18n. |
getIcon() | string | FontAwesome icon class without fas prefix. |
getType() | string | 'online' for payment gateways, 'offline' for manual methods. |
getSettingsFields() | array | Array of field definitions for the admin settings form. |
isConfigured() | bool | Return true if all required settings are filled. Offline plugins can always return true. |
createPayment() | array | Initiate payment. Return ['redirect_url' => '...'] for online, [] for offline. |
handleCallback() | array | Process gateway callback. Return ['success' => bool, 'transaction_id' => '...']. |
supportsRefund() | bool | Whether this plugin supports programmatic refunds. |
refund() | array | Process refund. Return ['success' => bool, 'message' => '...']. |
Provider.php
The main plugin class. Must implement PaymentProviderInterface and use PluginTranslationTrait for multi-language support.
YourPlugin/Provider.phpnamespace App\Plugins\Payment\MyGateway;
use App\Plugins\Payment\PaymentProviderInterface;
use App\Plugins\Payment\PluginTranslationTrait;
class Provider implements PaymentProviderInterface
{
use PluginTranslationTrait;
public function getSlug(): string { return 'my_gateway'; }
public function getDisplayName(): string{ return $this->__p('name'); }
public function getDescription(): string{ return $this->__p('description'); }
public function getIcon(): string { return 'fa-credit-card'; }
public function getVersion(): string { return '1.0.0'; }
public function getAuthor(): string { return 'Your Company'; }
public function getType(): string { return 'online'; }
// ... see full examples below
}
Settings Fields
The getSettingsFields() method returns an array of field definitions. The admin panel automatically generates a settings form from these fields.
public function getSettingsFields(): array
{
return [
[
'key' => 'api_key', // Setting key (stored in DB)
'label' => $this->__p('api_key'), // Label (translated)
'type' => 'text', // text | textarea | select | checkbox
'default' => '', // Default value
'required' => true, // Show as required in UI
],
[
'key' => 'secret_key',
'label' => $this->__p('secret_key'),
'type' => 'text',
'default' => '',
'required' => true,
],
[
'key' => 'sandbox',
'label' => 'Sandbox Mode',
'type' => 'checkbox',
'default' => '1',
'required' => false,
],
];
}
Supported Field Types
| Type | Renders As | Notes |
|---|---|---|
text | Text input | For API keys, titles, URLs |
textarea | Multi-line textarea | For descriptions, instructions |
select | Dropdown | Add 'options' => ['val' => 'Label'] |
checkbox | Toggle checkbox | Value is '1' or '0' |
Checkout Rendering
The renderCheckoutInfo() method returns HTML that is displayed on the reservation confirmation page when the guest selects this payment method.
public function renderCheckoutInfo(array $reservation): string
{
$html = '<div style="padding:16px;background:#f8f9fa;border-radius:10px">';
$html .= '<h4>' . e($this->__p('name')) . '</h4>';
$html .= '<p>You will be redirected to our secure payment page.</p>';
$html .= '</div>';
return $html;
}
e() helper to escape user-supplied data in your HTML output. Never trust data from $reservation without escaping.
Payment Flow
Online Plugins
For online payment gateways, the flow is:
public function createPayment(array $reservation, float $amount): array
{
// 1. Read your API settings
$mgr = \App\Plugins\Payment\PaymentPluginManager::getInstance();
$apiKey = $mgr->getPluginSetting($this->getSlug(), 'api_key', '');
// 2. Build payment request to gateway API
$callbackUrl = base_url('payment/callback/' . $this->getSlug());
// 3. Call gateway API...
// $response = $gateway->createPayment(...);
// 4. Return redirect URL
return [
'redirect_url' => $response['payment_page_url'],
'transaction_id' => $response['token'],
];
}
Callback Handling
public function handleCallback(array $data): array
{
// $data contains POST/GET parameters from the gateway
// Verify the payment with gateway API...
return [
'success' => true,
'transaction_id' => $data['token'],
'amount' => 250.00,
'currency' => 'TRY',
'message' => 'Payment successful',
];
}
Offline Plugins
Offline plugins have simpler implementations:
// No redirect needed
public function createPayment(array $reservation, float $amount): array
{
return [];
}
// No callback to handle
public function handleCallback(array $data): array
{
return ['success' => false, 'message' => 'N/A'];
}
Refunds
If your gateway supports programmatic refunds, implement the refund methods:
public function supportsRefund(): bool { return true; }
public function refund(string $transactionId, float $amount): array
{
// Call gateway refund API...
return [
'success' => true,
'message' => 'Refund processed',
'refund_id' => 'ref_abc123',
];
}
PluginTranslationTrait
The translation trait provides each plugin with its own isolated translation system. No need to modify core language files.
How It Works
- Trait detects the current admin locale via
admin_locale() - Loads the matching
lang/{locale}.phpfrom the plugin's own directory - Falls back through: requested locale → en → tr
- Automatically reloads when locale changes (no caching issues)
Language Files
Each language file returns a simple key-value array:
YourPlugin/lang/en.php<?php
return [
'name' => 'My Gateway',
'description' => 'Secure online payments via My Gateway.',
'api_key' => 'API Key',
'secret_key' => 'Secret Key',
'sandbox_note' => 'Use sandbox URL for testing',
];
YourPlugin/lang/tr.php<?php
return [
'name' => 'Ödeme Geçidi',
'description' => 'My Gateway ile güvenli online ödeme.',
'api_key' => 'API Anahtarı',
'secret_key' => 'Gizli Anahtar',
'sandbox_note' => 'Test için sandbox URL kullanın',
];
Using __p()
Call $this->__p('key') anywhere in your Provider class:
// In identity methods
public function getDisplayName(): string { return $this->__p('name'); }
// In settings field labels
['label' => $this->__p('api_key')]
// In checkout rendering
$html .= '<p>' . e($this->__p('checkout_instructions')) . '</p>';
__p() returns the key itself. This prevents broken output and makes it easy to spot missing translations.
Plugin Manager API
Access the plugin manager from anywhere via the singleton:
$mgr = \App\Plugins\Payment\PaymentPluginManager::getInstance();
| Method | Returns | Description |
|---|---|---|
getAllPlugins() | array | All discovered plugins (slug → Provider instance) |
getActivePlugins() | array | Only enabled plugins, sorted by admin-defined order |
getPlugin($slug) | ?Provider | Get a specific plugin by slug |
getPluginDbRecord($slug) | ?array | DB record with live-translated display_name & description |
getPluginSettings($slug) | array | Decoded JSON settings from DB |
getPluginSetting($slug, $key, $default) | string | Single setting value |
isActive($slug) | bool | Whether plugin is enabled |
getCheckoutMethods() | array | Active & configured plugins for frontend checkout |
Database Schema
payment_plugins
| Column | Type | Description |
|---|---|---|
id | INT AUTO_INCREMENT | Primary key |
slug | VARCHAR(50) UNIQUE | Plugin identifier |
display_name | VARCHAR(100) | Cached display name (overridden at runtime by Provider) |
description | TEXT | Cached description (overridden at runtime by Provider) |
icon | VARCHAR(50) | FontAwesome icon class |
type | ENUM('online','offline') | Plugin type |
version | VARCHAR(20) | Plugin version |
author | VARCHAR(100) | Plugin author |
is_active | TINYINT(1) | Enabled/disabled |
is_built_in | TINYINT(1) | System plugin flag |
settings | JSON | Plugin settings as JSON object |
sort_order | INT | Display order in admin & frontend |
payment_transactions
| Column | Type | Description |
|---|---|---|
id | INT AUTO_INCREMENT | Primary key |
reservation_id | INT | FK to reservations table |
plugin_slug | VARCHAR(50) | Which plugin processed this |
transaction_id | VARCHAR(255) | Gateway's transaction reference |
amount | DECIMAL(10,2) | Transaction amount |
currency | VARCHAR(3) | ISO currency code |
status | ENUM('pending','success','failed','refunded') | Transaction status |
raw_response | JSON | Full gateway response for debugging |
created_at | DATETIME | Transaction timestamp |
Helper Functions
These global functions are available in your plugin:
| Function | Description |
|---|---|
e($string) | HTML escape (htmlspecialchars) |
base_url($path) | Full URL: https://hotel.com/path |
admin_url($path) | Admin URL: https://hotel.com/admin/path |
setting($key, $default) | Read site setting from DB |
format_money($amount) | Format as currency: ₺1.250,00 |
admin_locale() | Current admin panel language: 'tr', 'en', 'de' |
get_locale() | Current frontend language |
csrf_field() | CSRF hidden input for forms |
csrf_token() | CSRF token string |
Security Guidelines
- Never log API keys or secrets — Use
getPluginSetting()at runtime, never hardcode - Always verify callbacks — Check signatures, hashes, or call the gateway's verification API
- Escape all output — Use
e()for any data rendered in HTML - Use HTTPS only — Never allow HTTP callback URLs
- Validate amounts — Compare the callback amount with the reservation amount
- Store raw responses — Save gateway responses in
raw_responsefor dispute resolution - Handle errors gracefully — Wrap gateway calls in try/catch, log errors, show user-friendly messages
- Test with sandbox — Always provide a sandbox/test mode toggle in your settings
Example: Offline Plugin
A complete offline payment plugin (e.g., "Pay at Hotel"):
PayAtHotel/Provider.php<?php
namespace App\Plugins\Payment\PayAtHotel;
use App\Plugins\Payment\PaymentProviderInterface;
use App\Plugins\Payment\PluginTranslationTrait;
class Provider implements PaymentProviderInterface
{
use PluginTranslationTrait;
public function getSlug(): string { return 'pay_at_hotel'; }
public function getDisplayName(): string { return $this->__p('name'); }
public function getDescription(): string { return $this->__p('description'); }
public function getIcon(): string { return 'fa-hotel'; }
public function getVersion(): string { return '1.0.0'; }
public function getAuthor(): string { return 'KobiZeka'; }
public function getType(): string { return 'offline'; }
public function getSettingsFields(): array
{
return [
['key' => 'title', 'label' => $this->__p('title_label'),
'type' => 'text', 'default' => 'pay_at_hotel', 'required' => false],
['key' => 'accepted_methods', 'label' => $this->__p('accepted_methods'),
'type' => 'text', 'default' => 'Cash, Credit Card', 'required' => false],
];
}
public function isConfigured(): bool { return true; }
public function renderCheckoutInfo(array $reservation): string
{
return '<div class="checkout-info">'
. '<p>' . e($this->__p('description')) . '</p>'
. '</div>';
}
public function createPayment(array $reservation, float $amount): array
{
return []; // No redirect for offline
}
public function handleCallback(array $data): array
{
return ['success' => false, 'message' => 'N/A'];
}
public function supportsRefund(): bool { return false; }
public function refund(string $tid, float $amt): array { return ['success' => false]; }
}
Full Plugin Template
Copy this template to start building your own plugin:
Create your plugin folder, implement PaymentProviderInterface, add translation files, and your plugin will appear automatically in the admin panel. No core files need to be modified.
KobiZeka™ Hotel Management System
Payment Plugin Development Guide v1.0
Need help? Contact developer@kobizeka.com