Skip to Content
ModulesPayments & Widgets

Payments, Widgets, and Profile

Flute CMS provides a payment system via Omnipay, a flexible widget system, and user profile integration capabilities.

Payment System

Important: Flute CMS automatically handles payments via Omnipay. Creating payments, handling callbacks, verification — all this happens automatically in the core. You only need to:

  1. Register the Omnipay driver in the provider
  2. Add the gateway via the admin panel

Examples below show optional custom logic if you need to extend standard behavior.

Minimal Integration

To add a new payment gateway, it is enough to:

<?php // In module provider protected function registerPaymentGateway(): void { events()->addDeferredListener( \Flute\Core\Modules\Payments\Events\RegisterPaymentFactoriesEvent::NAME, function($event) { $factory = app(\Flute\Core\Modules\Payments\Factories\PaymentDriverFactory::class); $factory->register('mygateway', MyOmnipayGateway::class); } ); }

After that, add the gateway in the admin panel (Settings → Payment Systems) and specify adapter = name of your Omnipay driver.

Everything else (creating payments, callbacks, status checking) is handled automatically by Flute core.


Architecture (for understanding)

The payment system is built on the following components:

ComponentDescription
PaymentGatewayEntity with gateway settings (adapter, enabled, settings)
RegisterPaymentFactoriesEventEvent for registering custom drivers
PaymentDriverFactoryFactory for creating payment drivers
GatewayInitializerInitializes Omnipay instances from enabled gateways
PaymentProcessorAutomatically handles payment scenarios and callbacks
PaymentPromoPromocode validation

Registering Payment Gateway (Detailed)

To add a custom payment gateway, register it in the module provider:

<?php namespace Flute\Modules\Shop\Providers; use Flute\Core\Support\ModuleServiceProvider; use Flute\Modules\Shop\Gateways\CustomGateway; class ShopProvider extends ModuleServiceProvider { public function boot(\DI\Container $container): void { $this->registerPaymentGateway(); $this->bootstrapModule(); } protected function registerPaymentGateway(): void { // Registration via event events()->addDeferredListener( \Flute\Core\Modules\Payments\Events\RegisterPaymentFactoriesEvent::NAME, function($event) { $factory = app(\Flute\Core\Modules\Payments\Factories\PaymentDriverFactory::class); // 'custom' — driver alias // CustomGateway::class — class implementing driver $factory->register('custom', CustomGateway::class); } ); } }

After registering the driver, add a record to the payment_gateways table via admin panel with adapter equal to the Omnipay gateway name.

Creating Custom Gateway

Create a gateway class inheriting from AbstractGateway and implementing PaymentDriverInterface:

<?php namespace Flute\Modules\Shop\Gateways; use Flute\Core\Modules\Payments\Contracts\PaymentDriverInterface; use Omnipay\Common\AbstractGateway; class CustomGateway extends AbstractGateway implements PaymentDriverInterface { /** * Gateway name for display */ public function getName(): string { return 'Custom Payment Gateway'; } /** * Default parameters (configured in admin) */ public function getDefaultParameters(): array { return [ 'apiKey' => '', 'secretKey' => '', 'testMode' => false, ]; } /** * Creating payment */ public function purchase(array $parameters = []): \Omnipay\Common\Message\RequestInterface { return $this->createRequest( \Flute\Modules\Shop\Gateways\Message\PurchaseRequest::class, $parameters ); } /** * Completing payment (callback) */ public function completePurchase(array $parameters = []): \Omnipay\Common\Message\RequestInterface { return $this->createRequest( \Flute\Modules\Shop\Gateways\Message\CompletePurchaseRequest::class, $parameters ); } /** * PaymentDriverInterface Implementation */ public function processPayment(array $data): array { $response = $this->purchase($data)->send(); return [ 'success' => $response->isSuccessful(), 'transaction_id' => $response->getTransactionReference(), 'amount' => $data['amount'], 'currency' => $data['currency'], ]; } public function verifyPayment(string $transactionId): array { $response = $this->completePurchase([ 'transactionReference' => $transactionId ])->send(); return [ 'verified' => $response->isSuccessful(), 'amount' => $response->getAmount(), 'currency' => $response->getCurrency(), ]; } }

Working with Payments

Creating Payment

<?php namespace Flute\Modules\Shop\Services; use Flute\Modules\Shop\Database\Entities\Order; class PaymentService { /** * Create payment for order */ public function createPayment(Order $order): array { // Payment data $paymentData = [ 'amount' => $order->total, 'currency' => config('shop.currency', 'RUB'), 'description' => "Order #{$order->id}", 'returnUrl' => route('shop.payment.success', ['order_id' => $order->id]), 'cancelUrl' => route('shop.payment.cancel', ['order_id' => $order->id]), 'notifyUrl' => route('shop.payment.notify', ['order_id' => $order->id]), 'metadata' => [ 'order_id' => $order->id, 'user_id' => $order->user_id, ] ]; // Get available payment gateways $gateways = payments()->getAllGateways(); if (empty($gateways)) { throw new \Exception('No available payment gateways'); } // Use first available gateway $gateway = reset($gateways); // Create payment $response = $gateway->purchase($paymentData)->send(); if ($response->isSuccessful() || $response->isRedirect()) { return [ 'success' => true, 'redirect_url' => $response->getRedirectUrl(), 'transaction_id' => $response->getTransactionReference(), ]; } return [ 'success' => false, 'error' => $response->getMessage(), ]; } }

Processing Callback

<?php public function processCallback(Order $order, array $callbackData): bool { $transactionId = $callbackData['transaction_id'] ?? null; if (!$transactionId) { logs('payments')->error('Missing transaction_id in callback'); return false; } // Get gateway and verify payment $gateways = payments()->getAllGateways(); $gateway = reset($gateways); $verification = $gateway->completePurchase([ 'transactionReference' => $transactionId ])->send(); if ($verification->isSuccessful()) { // Update order status $order->status = 'paid'; $order->paid_at = new \DateTime(); $order->transaction_id = $transactionId; $order->save(); // Send notification $this->sendPaymentSuccessNotification($order); return true; } logs('payments')->error('Payment verification failed', [ 'order_id' => $order->id, 'transaction_id' => $transactionId, 'error' => $verification->getMessage() ]); return false; }

Checkout Page

Full example of checkout page with payment gateway selection:

{{-- Resources/views/pages/checkout.blade.php --}} @extends('shop::layouts.app') @push('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header"> <h1>{{ __('shop.checkout') }}</h1> </div> <div class="card-body"> {{-- Order Summary --}} <div class="order-summary mb-4"> <h3>{{ __('shop.order_summary') }}</h3> <div class="order-items"> @foreach($order->items as $item) <div class="order-item d-flex justify-content-between"> <span>{{ $item->product->name }} x {{ $item->quantity }}</span> <span>{{ number_format($item->total, 2) }} {{ $order->currency }}</span> </div> @endforeach </div> <hr> <div class="order-total d-flex justify-content-between"> <strong>{{ __('shop.total') }}</strong> <strong>{{ number_format($order->total, 2) }} {{ $order->currency }}</strong> </div> </div> {{-- Payment Form --}} <div class="payment-form"> <h3>{{ __('shop.payment_method') }}</h3> @if($gateways = payments()->getAllGateways()) <form action="{{ route('shop.payment.process') }}" method="POST"> @csrf {{-- Payment Gateway Selection --}} <div class="mb-3"> <label for="gateway" class="form-label"> {{ __('shop.select_gateway') }} </label> <select name="gateway" id="gateway" class="form-control" required> @foreach($gateways as $key => $gateway) <option value="{{ $key }}">{{ $gateway->getName() }}</option> @endforeach </select> </div> <input type="hidden" name="order_id" value="{{ $order->id }}"> <button type="submit" class="btn btn-primary btn-lg w-100"> {{ __('shop.pay_now') }} </button> </form> @else <div class="alert alert-warning"> {{ __('shop.no_gateways_available') }} </div> @endif </div> </div> </div> </div> </div> </div> @endpush

Payment Controller

<?php namespace Flute\Modules\Shop\Http\Controllers; use Flute\Core\Support\BaseController; use Flute\Core\Router\Annotations\Route; use Flute\Modules\Shop\Database\Entities\Order; use Flute\Modules\Shop\Services\PaymentService; class PaymentController extends BaseController { protected PaymentService $paymentService; public function __construct(PaymentService $paymentService) { $this->paymentService = $paymentService; } #[Route('/shop/checkout/{orderId}', name: 'shop.checkout', methods: ['GET'])] public function checkout(int $orderId) { $order = Order::findByPK($orderId); if (!$order || $order->user_id !== user()->id) { return $this->error(__('shop.order_not_found'), 404); } if ($order->status !== 'pending') { return redirect(route('shop.order.show', ['id' => $order->id])); } return view('shop::pages.checkout', [ 'order' => $order ]); } #[Route('/shop/payment/process', name: 'shop.payment.process', methods: ['POST'])] public function process() { $orderId = request()->input('order_id'); $gatewayKey = request()->input('gateway'); $order = Order::findByPK($orderId); if (!$order || $order->user_id !== user()->id) { return $this->error(__('shop.order_not_found'), 404); } try { $result = $this->paymentService->createPayment($order, $gatewayKey); if ($result['success'] && isset($result['redirect_url'])) { return redirect($result['redirect_url']); } return $this->error($result['error'] ?? __('shop.payment_error')); } catch (\Exception $e) { logs('payments')->error('Payment error: ' . $e->getMessage()); return $this->error(__('shop.payment_error')); } } #[Route('/shop/payment/success', name: 'shop.payment.success', methods: ['GET'])] public function success() { return view('shop::pages.payment-success'); } #[Route('/shop/payment/cancel', name: 'shop.payment.cancel', methods: ['GET'])] public function cancel() { return view('shop::pages.payment-cancel'); } #[Route('/shop/payment/notify', name: 'shop.payment.notify', methods: ['POST'])] public function notify() { // Webhook from payment system $data = request()->all(); logs('payments')->info('Payment notification received', $data); $orderId = $data['order_id'] ?? $data['metadata']['order_id'] ?? null; if (!$orderId) { return response()->json(['error' => 'Order ID not found'], 400); } $order = Order::findByPK($orderId); if (!$order) { return response()->json(['error' => 'Order not found'], 404); } $success = $this->paymentService->processCallback($order, $data); return response()->json(['success' => $success]); } }

Widget System

Widgets allow adding dynamic content to site pages with customizable parameters.

Creating a Widget

Widget implements WidgetInterface:

<?php namespace Flute\Modules\Blog\Widgets; use Flute\Core\Modules\Page\Widgets\Contracts\WidgetInterface; use Flute\Modules\Blog\Database\Entities\Article; class RecentArticlesWidget implements WidgetInterface { /** * Widget name for admin */ public function getName(): string { return __('blog.widget.recent_articles'); } /** * Widget icon */ public function getIcon(): string { return '<x-icon path="newspaper" />'; } /** * Widget settings with types */ public function getSettings(): array { return [ 'limit' => [ 'type' => 'number', 'label' => __('blog.widget.limit'), 'default' => 5, 'min' => 1, 'max' => 20, ], 'show_excerpt' => [ 'type' => 'boolean', 'label' => __('blog.widget.show_excerpt'), 'default' => true, ], 'show_date' => [ 'type' => 'boolean', 'label' => __('blog.widget.show_date'), 'default' => true, ], 'category_filter' => [ 'type' => 'select', 'label' => __('blog.widget.category'), 'options' => $this->getCategoryOptions(), 'default' => null, ] ]; } /** * Rendering widget */ public function render(array $settings): string|null { $limit = $settings['limit'] ?? 5; $categoryId = $settings['category_filter'] ?? null; // Get articles $query = Article::query() ->where('status', 'published') ->orderBy('publishedAt', 'DESC') ->limit($limit); if ($categoryId) { $query->where('category.id', $categoryId); } $articles = $query->fetchAll(); if (empty($articles)) { return null; // Do not display empty widget } return view('blog::widgets.recent-articles', [ 'articles' => $articles, 'settings' => $settings, ])->render(); } /** * Does the widget have settings */ public function hasSettings(): bool { return true; } /** * Default widget width (1-12) */ public function getDefaultWidth(): int { return 12; } /** * Minimal width */ public function getMinWidth(): int { return 6; } /** * Widget category for grouping */ public function getCategory(): string { return 'blog'; } /** * Settings validation */ public function validateSettings(array $input): true|array { $validator = validator(); $validated = $validator->validate($input, [ 'limit' => 'required|integer|min:1|max:20', 'show_excerpt' => 'boolean', 'show_date' => 'boolean', 'category_filter' => 'nullable|integer', ]); return $validated ? true : $validator->getErrors()->toArray(); } /** * Save settings */ public function saveSettings(array $input): array { return $input; } /** * Settings form (optional, if custom is needed) */ public function renderSettingsForm(array $settings): string|bool { return view('blog::widgets.recent-articles-settings', [ 'settings' => $settings ])->render(); } /** * Widget action buttons */ public function getButtons(): array { return [ [ 'title' => __('blog.widget.refresh_cache'), 'action' => 'refresh_cache', 'icon' => '<x-icon path="sync" />', ] ]; } /** * Action handling */ public function handleAction(string $action, ?string $widgetId = null): array { if ($action === 'refresh_cache') { cache()->delete("widget_recent_articles_{$widgetId}"); return ['success' => true, 'message' => __('blog.cache_cleared')]; } return ['success' => false, 'message' => __('blog.unknown_action')]; } protected function getCategoryOptions(): array { $categories = \Flute\Modules\Blog\Database\Entities\Category::findAll(); $options = ['' => __('blog.all_categories')]; foreach ($categories as $category) { $options[$category->id] = $category->name; } return $options; } }

Widget Template

{{-- Resources/views/widgets/recent-articles.blade.php --}} <div class="widget widget-recent-articles"> <div class="widget-header"> <h3 class="widget-title"> <x-icon path="newspaper" /> {{ __('blog.recent_articles') }} </h3> </div> <div class="widget-body"> <div class="articles-list"> @foreach($articles as $article) <article class="article-item"> <h4 class="article-title"> <a href="{{ route('blog.show', ['slug' => $article->slug]) }}"> {{ $article->title }} </a> </h4> @if($settings['show_date'] ?? true) <time class="article-date"> {{ $article->publishedAt->format('d.m.Y') }} </time> @endif @if(($settings['show_excerpt'] ?? true) && $article->excerpt) <p class="article-excerpt"> {{ Str::limit($article->excerpt, 100) }} </p> @endif @if($settings['show_author'] ?? false) <div class="article-author"> <x-icon path="user" /> {{ $article->author->name }} </div> @endif </article> @endforeach </div> <div class="widget-footer"> <a href="{{ route('blog.index') }}" class="view-all"> {{ __('blog.view_all') }} <x-icon path="arrow-right" /> </a> </div> </div> </div>

Widget Settings Form

Custom form for configuring the widget in admin panel:

{{-- Resources/views/widgets/recent-articles-settings.blade.php --}} <div class="widget-settings"> {{-- Number of articles --}} <div class="mb-3"> <label for="limit" class="form-label">{{ __('blog.widget.limit') }}</label> <input type="number" id="limit" name="limit" class="form-control" value="{{ $settings['limit'] ?? 5 }}" min="1" max="20" required> <small class="form-text text-muted"> {{ __('blog.widget.limit_description') }} </small> </div> {{-- Show excerpt --}} <div class="mb-3"> <div class="form-check"> <input type="checkbox" id="show_excerpt" name="show_excerpt" value="1" class="form-check-input" {{ ($settings['show_excerpt'] ?? true) ? 'checked' : '' }}> <label for="show_excerpt" class="form-check-label"> {{ __('blog.widget.show_excerpt') }} </label> </div> </div> {{-- Show date --}} <div class="mb-3"> <div class="form-check"> <input type="checkbox" id="show_date" name="show_date" value="1" class="form-check-input" {{ ($settings['show_date'] ?? true) ? 'checked' : '' }}> <label for="show_date" class="form-check-label"> {{ __('blog.widget.show_date') }} </label> </div> </div> {{-- Show author --}} <div class="mb-3"> <div class="form-check"> <input type="checkbox" id="show_author" name="show_author" value="1" class="form-check-input" {{ ($settings['show_author'] ?? false) ? 'checked' : '' }}> <label for="show_author" class="form-check-label"> {{ __('blog.widget.show_author') }} </label> </div> </div> {{-- Category filter --}} <div class="mb-3"> <label for="category_filter" class="form-label"> {{ __('blog.widget.category_filter') }} </label> <select id="category_filter" name="category_filter" class="form-control"> <option value="">{{ __('blog.widget.all_categories') }}</option> @php $categories = \Flute\Modules\Blog\Database\Entities\Category::findAll(); @endphp @foreach($categories as $category) <option value="{{ $category->id }}" {{ ($settings['category_filter'] ?? null) == $category->id ? 'selected' : '' }}> {{ $category->name }} </option> @endforeach </select> </div> </div>

Registering Widget

Widgets from the Widgets/ folder are automatically registered when bootstrapModule() is called:

      • RecentArticlesWidget.php
      • CategoriesWidget.php

Profile Integration

Adding Tab to Profile

You can add a tab to user profile via TemplateInitialized event:

<?php namespace Flute\Modules\Blog\Providers; use Flute\Core\Support\ModuleServiceProvider; class BlogProvider extends ModuleServiceProvider { public function boot(\DI\Container $container): void { $this->bootstrapModule(); $this->registerProfileTab(); } protected function registerProfileTab(): void { events()->addDeferredListener( \Flute\Core\Template\Events\TemplateInitialized::NAME, function($event) { $tpl = $event->getTemplate(); // Add tab to profile navigation $tpl->prependTemplateToSection('profile_tabs', 'blog::partials.profile-tab', [ 'user' => user() ]); // Add tab content $tpl->prependTemplateToSection('profile_tab_content', 'blog::partials.profile-content', [ 'user' => user() ]); } ); } }

Tab Template

{{-- Resources/views/partials/profile-tab.blade.php --}} <li class="nav-item"> <a class="nav-link" id="blog-tab" data-bs-toggle="tab" href="#blog" role="tab"> <x-icon path="blog" /> {{ __('blog.my_articles') }} @if($user->articles->count() > 0) <span class="badge bg-primary">{{ $user->articles->count() }}</span> @endif </a> </li>

Tab Content Template

{{-- Resources/views/partials/profile-content.blade.php --}} <div class="tab-pane fade" id="blog" role="tabpanel"> <div class="blog-profile-section"> <div class="d-flex justify-content-between align-items-center mb-4"> <h3>{{ __('blog.my_articles') }}</h3> <a href="{{ route('blog.create') }}" class="btn btn-primary"> <x-icon path="plus" /> {{ __('blog.create_article') }} </a> </div> @if($user->articles->count() > 0) <div class="articles-list"> @foreach($user->articles as $article) <div class="article-item card mb-3"> <div class="card-body"> <h5 class="card-title"> <a href="{{ route('blog.show', ['slug' => $article->slug]) }}"> {{ $article->title }} </a> </h5> <div class="article-meta text-muted small"> <span> <x-icon path="calendar" /> {{ $article->createdAt->format('d.m.Y') }} </span> <span> <x-icon path="eye" /> {{ $article->views }} </span> <span> @if($article->status === 'published') <span class="badge bg-success">{{ __('blog.published') }}</span> @else <span class="badge bg-warning">{{ __('blog.draft') }}</span> @endif </span> </div> <div class="article-actions mt-2"> <a href="{{ route('blog.edit', ['id' => $article->id]) }}" class="btn btn-sm btn-outline-primary"> {{ __('blog.edit') }} </a> </div> </div> </div> @endforeach </div> @else <div class="text-center py-5"> <x-icon path="blog" class="fa-3x text-muted mb-3" /> <h4>{{ __('blog.no_articles') }}</h4> <p class="text-muted">{{ __('blog.create_first_article_description') }}</p> </div> @endif </div> </div>

Adding Statistics to Profile

<?php protected function registerProfileStats(): void { events()->addDeferredListener( \Flute\Core\Template\Events\TemplateInitialized::NAME, function($event) { $tpl = $event->getTemplate(); $tpl->prependTemplateToSection('profile_stats', 'blog::partials.profile-stats', [ 'user' => user() ]); } ); }
{{-- Resources/views/partials/profile-stats.blade.php --}} <div class="col-md-4 mb-4"> <div class="card stat-card"> <div class="card-body text-center"> <x-icon path="blog" class="fa-2x text-primary mb-2" /> <h3>{{ $user->articles->count() }}</h3> <p class="text-muted mb-0">{{ __('blog.articles') }}</p> </div> </div> </div> <div class="col-md-4 mb-4"> <div class="card stat-card"> <div class="card-body text-center"> <x-icon path="eye" class="fa-2x text-info mb-2" /> <h3>{{ $user->articles->sum('views') }}</h3> <p class="text-muted mb-0">{{ __('blog.total_views') }}</p> </div> </div> </div>

Best Practices

Payments

  • Always validate payments on the server
  • Use HTTPS for payment forms
  • Log all transactions
  • Test in sandbox before production

Widgets

  • Cache widget data
  • Optimize DB queries
  • Provide reasonable default values
  • Validate all settings

Profile

  • Check data access permissions
  • Use AJAX for dynamic updates
  • Cache statistics