Вы написали свой интернет-магазин или CRM на PHP, и теперь нужно принимать платежи. Готовые решения не подходят — то API не те, то негибкие, то слишком громоздкие. Хочется сделать свой модуль, который встроится куда угодно и будет работать именно так, как вам нужно. Это вполне реальная задача, если понимать, как устроены платёжные протоколы и на какие грабли чаще всего наступают.
Ниже — практический путь от идеи до рабочего модуля. Без воды, с конкретными примерами на PHP и объяснением, почему каждый шаг именно такой.
- Что на самом деле делает платёжный модуль
- Выбор подхода: универсальный интерфейс или под конкретный шлюз
- Архитектура модуля: на что разбить код
- Интерфейс шлюза: что он должен уметь
- Пошагово: создаём модуль на примере Stripe
- Шаг 1. Установка SDK и базовая настройка
- Шаг 2. Создание сессии оплаты
- Шаг 3. Обработка webhook — самое важное
- Шаг 4. Возврат средств
- Сравнение популярных шлюзов для PHP-интеграции
- Чего делать в зависимости от вашей ситуации
- Частые ошибки, которые ломают интеграцию
- Практические рекомендации
- Как встроить модуль в любую систему
- Итог
Что на самом деле делает платёжный модуль
Прежде чем писать код, стоит разобраться, что именно вам нужно реализовать. Платёжный модуль — это не один огромный класс. Это набор логических блоков, каждый из которых отвечает за свою часть работы:
- Формирование запроса на оплату — собираете данные о заказе, сумме, валюте и отправляете их в платёжный шлюз.
- Обработка ответа шлюза — получаете результат, проверяете подпись, обновляете статус заказа.
- Приём уведомлений (callback/webhook) — шлюз сообщает о смене статуса платежа асинхронно, без участия пользователя.
- Возвраты и частичные списания — если нужно вернуть деньги клиенту или скорректировать сумму.
- Логирование и обработка ошибок — всё, что поможет разобраться, почему платёж не прошёл.
Хорошая новость: большая часть этого однотипна от шлюза к шлюзу. Плохая: у каждого свои нюансы в формате подписи, полях и поведении webhook’ов.
Выбор подхода: универсальный интерфейс или под конкретный шлюз
Здесь сразу два пути, и выбор зависит от вашей задачи.
Вариант 1: Пишем под конкретный шлюз. Если вы точно знаете, что проект будет работать с одним провайдером (например, ЮKassa, Stripe, CloudPayments), нет смысла абстрагироваться. Вы читаете их документацию, реализуете их протокол — и всё работает. Быстрее, проще, меньше сюрпризов.
Вариант 2: Делаем универсальный модуль с адаптерами. Если продукт продаётся разным клиентам, и каждый хочет свой шлюз — нужна абстракция. Вы определяете общий интерфейс, а под каждый шлюз пишете отдельный адаптер. Это архитектурно правильнее, но требует больше кода и тестирования.
В реальности чаще всего начинают с одного шлюза, а потом выделяют интерфейс, когда появляется второй. Это нормально — не стоит строить «универсальный платёжный фреймворк» заранее.
Архитектура модуля: на что разбить код
Минимальная структура, которая позволяет поддерживать несколько шлюзов и не сойти с ума:
Payment/
Contracts/
PaymentGatewayInterface.php
PaymentRequest.php
PaymentResponse.php
Gateways/
StripeGateway.php
YooKassaGateway.php
CloudPaymentsGateway.php
Services/
PaymentLogger.php
WebhookVerifier.php
Exceptions/
PaymentException.php
InvalidSignatureException.php
PaymentService.php Ключевая идея — PaymentService не знает, какой шлюз используется. Он работает через интерфейс. Это значит, что основная логика приложения (создание заказа, обработка результата, отправка уведомлений клиенту) не меняется при смене провайдера.
Интерфейс шлюза: что он должен уметь
Вот минимальный контракт, которого достаточно для большинства сценариев:
interface PaymentGatewayInterface
{
public function createPayment(PaymentRequest $request): PaymentResponse;
public function capture(string transactionId, intamount): PaymentResponse;
public function refund(string transactionId, intamount): PaymentResponse;
public function handleWebhook(array headers, stringbody): WebhookResult;
} Каждый метод возвращает объект ответа с полями: статус, ID транзакции, сумма, причина отказа (если есть), сырой ответ шлюза. Это удобно — вы всегда можете залогировать оригинальный ответ, если что-то пойдёт не так.
Пошагово: создаём модуль на примере Stripe
Возьмём Stripe как пример — у них понятное API и хорошая документация. Принцип тот же для любого другого шлюза.
Шаг 1. Установка SDK и базовая настройка
composer require stripe/stripe-php В конструктор адаптера передаём секретный ключ и, опционально, webhook-секрет:
class StripeGateway implements PaymentGatewayInterface
{
private \Stripe\Client $client;
private string $webhookSecret;
public function __construct(string secretKey, stringwebhookSecret)
{
this->client = new StripeClient(secretKey);
this->webhookSecret =webhookSecret;
}
} Шаг 2. Создание сессии оплаты
Stripe работает через Checkout Sessions — вы создаёте сессию, получаете URL и перенаправляете туда пользователя:
public function createPayment(PaymentRequest $request): PaymentResponse
{
try {
session =this->client->checkout->sessions->create([
'payment_method_types' => ['card'],
'line_items' => [[
'price_data' => [
'currency' => $request->getCurrency(),
'product_data' => [
'name' => $request->getDescription(),
],
'unit_amount' => $request->getAmountInMinorUnits(),
],
'quantity' => 1,
]],
'mode' => 'payment',
'success_url' => $request->getSuccessUrl() . '?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => $request->getCancelUrl(),
'metadata' => [
'order_id' => $request->getOrderId(),
],
]);
return new PaymentResponse(
status: 'pending',
transactionId: $session->id,
redirectUrl: $session->url,
);
} catch (\Stripe\Exception\ApiErrorException $e) {
throw new PaymentException('Stripe error: ' . $e->getMessage());
}
} Обратите внимание: сумма передаётся в копейках/центах (minor units). Это стандартная практика — никогда не передавайте деньги как числа с плавающей точкой.
Шаг 3. Обработка webhook — самое важное
Пользователь может закрыть вкладку после оплаты. Вы узнаете о результате только через webhook. И здесь главное — проверить подпись, иначе любый желающий сможет «подтвердить» оплату вашего заказа.
public function handleWebhook(array headers, stringbody): WebhookResult
{
sigHeader =headers['Stripe-Signature'] ?? '';
try {
$event = \Stripe\Webhook::constructEvent(
$body,
$sigHeader,
$this->webhookSecret
);
} catch (\UnexpectedValueException $e) {
throw new InvalidSignatureException('Invalid payload');
} catch (\Stripe\Exception\SignatureVerificationException $e) {
throw new InvalidSignatureException('Invalid signature');
}
return match ($event->type) {
'checkout.session.completed' => this->handleSessionCompleted(event->data->object),
'charge.refunded' => this->handleRefund(event->data->object),
default => new WebhookResult(ignored: true),
};
}
private function handleSessionCompleted(\Stripe\CheckoutSession $session): WebhookResult
{
orderId =session->metadata['order_id'];
if ($session->payment_status === 'paid') {
return new WebhookResult(
status: 'completed',
orderId: $orderId,
transactionId: $session->payment_intent,
);
}
return new WebhookResult(status: 'pending', orderId: $orderId);
} Важный момент: webhook может придти несколько раз. Обработчик должен быть идемпотентным — если заказ уже оплачен, повторное уведомление не должно ломать логику.
Шаг 4. Возврат средств
public function refund(string transactionId, intamount): PaymentResponse
{
try {
refund =this->client->refunds->create([
'payment_intent' => $transactionId,
'amount' => $amount,
]);
return new PaymentResponse(
status: $refund->status === 'succeeded' ? 'refunded' : 'refund_pending',
transactionId: $refund->id,
);
} catch (\Stripe\Exception\ApiErrorException $e) {
throw new PaymentException('Refund failed: ' . $e->getMessage());
}
} Сравнение популярных шлюзов для PHP-интеграции
Если вы выбираете, с каким провайдером работать, вот что реально влияет на разработку:
| Параметр | Stripe | ЮKassa | CloudPayments |
|---|---|---|---|
| PHP SDK | Официальный, хорошо документирован | Есть официальный, но менее удобный | Нет официального SDK, работа через HTTP |
| Webhook подпись | HMAC-SHA256, встроенная проверка | MD5-хэш из полей уведомления | HMAC-SHA256 из заголовка |
| Способ оплаты | Checkout Session / Payment Intents | POST-запрос на создание платежа | CloudPayments Checkout / Apple Pay |
| Возвраты | Через API, поддержка частичных | Через API, нужен статус магазина | Через API, частичные через отдельный метод |
| Документация для разработчика | Отличная, с примерами кода | Средняя, много устаревших примеров | Средняя, не хватает примеров на PHP |
| Регион работы | Международный | Россия, СНГ | Россия |
Чего делать в зависимости от вашей ситуации
У вас один проект, один шлюз, нужно запуститься быстро.
Пишите адаптер прямо в проекте, без абстракций. Выделите интерфейс позже, когда появится второй провайдер. Не усложняйте заранее.
Вы делаете платформенное решение (SaaS, конструктор магазинов).
Сразу закладывайте интерфейс и систему адаптеров. Храните настройки шлюза (ключи, идентификаторы) в базе для каждого клиента. Сделайте отдельный класс для маршрутизации — какой клиент каким шлюзом пользуется.
Нужна поддержка Apple Pay / Google Pay.
Stripe и CloudPayments поддерживают это из коробки. Для ЮKassa нужна дополнительная интеграция. Если это критично — учитывайте на этапе выбора шлюза.
Проект работает с российскими картами.
Stripe не работает с картами российских банков. Ваш выбор — ЮKassa, CloudPayments, Робокасса или Тинькофф Эквайринг.
Частые ошибки, которые ломают интеграцию
Не проверяется подпись webhook. Это дыра в безопасности. Любой может отправить POST-запрос на ваш endpoint и подтвердить неоплаченный заказ. Всегда проверяйте подпись перед обработкой.
Сумма передаётся как float.
99.99в PHP может превратиться в99.98999999999999. Передавайте сумму в копейках как целое число:9999. Это касается всех шлюзов без исключения.
Нет идемпотентности при обработке webhook. Шлюз может отправить уведомление дважды. Если ваш код каждый раз начисляет бонус или отправляет письмо — будут дубли. Проверяйте статус заказа перед действиями.
Секретные ключи хранятся в коде или Git. Используйте переменные окружения. Один случайный коммит с ключом в репозитории — и ваши платежи под угрозой.
Нет таймаутов при запросах к API шлюза. Если шлюз «завис», ваш сервер тоже повиснет. Всегда задавайте
timeoutиconnect_timeoutв HTTP-клиенте. Разумное значение — 10–15 секунд.
Практические рекомендации
- Логируйте всё. Каждый запрос к шлюзу и каждый ответ должны записываться в лог. Когда клиент говорит «деньги списаны, но заказ не подтверждён» — логи единственное, что поможет разобраться.
- Используйте уникальные идентификаторы заказов. Передавайте свой ID заказа в
metadata/descriptionплатежа. Это свяжет транзакцию шлюза с вашим заказом. - Обрабатывайте статусы правильно. Не ставьте заказ «оплачен» сразу после создания сессии. Только после подтверждения от webhook с корректной подписью.
- Делайте тестовые платежи. У всех шлюзов есть тестовый режим и тестовые карты. Не депойте в боевую среду, пока не прогнали полный цикл: создание → оплата → webhook → отмена → возврат.
- Обновляйте SDK. Платёжные провайдеры периодически меняют API. Раз в пару месяцев проверяйте релизы и обновляйте зависимости.
Как встроить модуль в любую систему
Если модуль правильно спроектирован, его интеграция в существующий проект сводится к нескольким шагам:
- Подключите модуль через Composer или скопируйте файлы.
- Создайте конфигурационный файл с ключами и настройками шлюза.
- В контроллере создания заказа вызовите
PaymentService::createPayment()и перенаправьте пользователя на полученный URL. - Добавьте публичный endpoint для webhook’ов и пропишите его URL в личном кабинете шлюза.
- В обработчике webhook вызовите
PaymentService::handleWebhook()и обновите статус заказа.
Endpoint для webhook’ов не должен требовать авторизации — шлюз приходит сам. Но он должен быть защищён проверкой подписи. Это единственный способ убедиться, что запрос действительно от вашего провайдера.
Итог
Создать собственный платёжный модуль на PHP — задача не сложная, если разбить её на части: интерфейс, адаптеры под шлюзы, обработка webhook’ов, логирование. Главное — не экономить на проверке подписи, не передавать суммы как float и всегда обрабатывать асинхронные уведомления от шлюза.
Начните с одного провайдера, который закрывает ваши текущие потребности. Напишите рабочий адаптор, протестируйте полный цикл оплаты. Абстракции и поддержку нескольких шлюзов добавляйте по мере необходимости — не раньше.
Информация в статье носит ознакомительный характер. Платёжные интеграции связаны с обработкой финансовых данных и персональных сведений клиентов. Перед запуском в производственную эксплуатацию рекомендуется проконсультироваться с профильным специалистом по информационной безопасности и проверить соответствие требованиям применимого законодательства.



