С возвращением. Это следующая статья из серии о практическом применении паттернов в Laravel, и в этом руководстве мы поговорим об одном из самых популярных паттернов, который используют многие разработчики — паттерне «Адаптер» (Adapter).
Введение
Начнем с определения Адаптера:
Паттерн «Адаптер» (Adapter) — это structural-паттерн проектирования, который позволяет объектам с несовместимыми интерфейсами работать вместе, преобразуя интерфейс одного класса в интерфейс, ожидаемый клиентом.
Реальный пример из жизни
Представьте, что вы путешествуете из США в Европу. Приехав в отель, вы обнаруживаете, что в Европе используется другой тип розеток для электроприборов. Чтобы решить эту проблему, вы покупаете сетевой адаптер (переходник), который позволяет легко заряжать ваши устройства с американской вилкой.
Плохой пример (антипаттерн)
Сначала давайте посмотрим, как выглядит плохое решение без использования классов-адаптеров и без чистой архитектуры кода:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Stripe\StripeClient;
use Square\SquareClient;
use Square\Models\Money;
use Square\Models\CreatePaymentRequest;
class BadCheckoutController extends Controller
{
public function store(Request $request)
{
// We get a lot of data from $request without any processing, which can create
// a lot of issues in the future for transactions processing
$gateway = $request->string('gateway');
$amount = (float) $request->input('amount');
$currency = $request->input('currency', 'USD');
$pm = $request->string('payment_method');
// Another bad practice - is to use conditions for creating new class objects with hardcoded actions
if ($gateway === 'stripe') {
$stripe = new StripeClient(config('services.stripe.secret'));
$intent = $stripe->paymentIntents->create([
'amount' => (int) round($amount),
'currency' => strtolower($currency),
'payment_method' => $pm,
'confirm' => true,
'description' => $request->input('description', 'Order #'.now()->timestamp),
]);
if ($intent->status !== 'succeeded') {
return back()->withErrors(['payment' => 'Stripe failed: '.$intent->status]);
}
// All this business logic in controller will create another issues
return redirect()->route('thankyou')->with('tx', $intent->id);
} elseif ($gateway === 'square') {
$square = new SquareClient([
'accessToken' => config('services.square.access_token'),
'environment' => config('services.square.environment', 'sandbox'),
]);
$paymentsApi = $square->getPaymentsApi();
$money = new Money();
$money->setAmount((int) ($amount * 100.0));
$money->setCurrency(strtoupper($currency));
$requestObj = new CreatePaymentRequest(
sourceId: $pm,
idempotencyKey: (string) rand(),
amountMoney: $money
);
try {
$response = $paymentsApi->createPayment($requestObj);
if ($response->isSuccess()) {
$payment = $response->getResult()->getPayment();
if ($payment->getStatus() !== 'COMPLETED') {
return back()->withErrors(['payment' => 'Square not completed: '.$payment->getStatus()]);
}
return redirect()->route('thankyou')->with('tx', $payment->getId());
}
$errs = collect($response->getErrors() ?? [])->map(fn($e) => $e->getDetail() ?: 'error')->implode('; ');
return back()->withErrors(['payment' => 'Square failed: '.$errs]);
} catch (\Throwable $e) {
return back()->withErrors(['payment' => 'Square error: '.$e->getMessage()]);
}
return back()->withErrors(['payment' => 'Unknown gateway']);
}
}
}
Выглядит ужасно 0.0 Теперь самое время правильно разделить всю логику, используя лучшие практики, и для решения этой задачи мы воспользуемся паттерном «Адаптер».
Реализация паттерна «Адаптер»
Прежде всего, нам нужно определить интерфейс, который будет использоваться нашими классами-адаптерами для единообразной работы с различными платежными системами через один и тот же метод:
<?php
namespace App\Domains\Payment;
interface PaymentGateawayInterface
{
public function charge(
int $amount,
string $currency,
string $source,
string $description = ''
);
}
Также не забудьте установить библиотеку оплаты через Composer в ваш Laravel-проект:
$ composer require stripe/stripe-php
После установки зависимостей нам также нужно создать класс, в котором мы будем хранить результаты транзакций
<?php
namespace App\Domains\Payment;
class ChargeResult
{
public function __construct(
public bool $success,
public string $transactionId,
public string $message,
) {}
}
Теперь, когда основные компоненты определены, пришло время создать наш первый адаптер для обработки транзакций Stripe:
<?php
namespace App\Domains\Payment;
use Stripe\StripeClient;
use Exception;
class StripeGateawayAdapter implements PaymentGateawayInterface
{
// We use dependency injection to properly handle
// object creation from the framework side
public function __construct(
private StripeClient $client
) {}
public function charge(
int $amount,
string $currency,
string $source,
string $description = ''
) {
try {
$response = $this->client->paymentIntents->create([
'amount' => $amount,
'currency' => $currency,
'payment_method' => $source,
'confirm' => true,
'description' => $description
]);
return new ChargeResult(
success: $response->status === 'succeeded',
transactionId: $response->id,
message: $response->status
);
} catch( Exception $e ) {
return new ChargeResult(
success: false,
transactionId: null,
message: $e->getMessage()
);
}
}
}
Но это не единственный класс-адаптер, который нам нужен; мы также создадим адаптер для обработки транзакций платежной системы Square:
<?php
namespace App\Domains\Payment;
use Exception;
use Square\Payments\Requests\CreatePaymentRequest;
use Square\Legacy\Models\Money;
use Square\SquareClient;
use Illuminate\Support\Str;
class SquareGateawayAdapter implements PaymentGateawayInterface
{
public function __construct(
private SquareClient $client
) {}
public function charge(
int $amount,
string $currency,
string $source,
string $description = ''
) {
$money = new Money();
$money->setAmount($amount);
$money->setCurrency(strtoupper($currency));
$request = new CreatePaymentRequest([
'idempotencyKey' => Str::uuid()->toString(),
'sourceId' => $source,
'amountMoney' => $money
]);
if ($description !== '') {
$request->setNote($description);
}
try {
$response = $this->client->payments->create(
$request
);
if ( $response->getPayment() ) {
// ... return success here
}
} catch ( Exception $e ) {
return new ChargeResult(
success: false,
transactionId: null,
message: $e->getMessage(),
);
}
}
}
Благодаря адаптерам код теперь выглядит намного лучше: он стал чище, читаемее, и мы следуем принципам SOLID и ООП. И, конечно же, если мы используем какой-либо интерфейс во фреймворке Laravel, нам нужно правильно зарегистрировать его связывание в AppServiceProvider.php:
<?php
namespace App\Providers;
use App\Domains\Payment\PaymentGateawayInterface;
use App\Domains\Payment\SquareGateawayAdapter;
use App\Domains\Payment\StripeGateawayAdapter;
use Illuminate\Support\ServiceProvider;
use App\Domains\Report\Builders\IReportBuilder;
use App\Domains\Report\Builders\ReportBuilder;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
$this->app->bind(PaymentGateawayInterface::class, StripeGateawayAdapter::class);
$this->app->bind(PaymentGateawayInterface::class, SquareGateawayAdapter::class);
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}
Отлично, теперь мы можем легко использовать его в наших контроллерах:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Domains\Payment\StripeGateawayAdapter;
use App\Domains\Payment\SquareGateawayAdapter;
use App\Domains\Payment\PaymentGateawayInterface;
use Stripe\StripeClient;
use Square\SquareClient;
class CheckoutController extends Controller
{
private PaymentGateawayInterface $gateway;
public function __construct()
{
// We will have config for all payments
$driver = config('payments.driver', 'stripe');
// Dynamically switch gateaway payment based on config
// we can also move this code to service file
$this->gateway = match ($driver) {
'stripe' => new StripeGateawayAdapter(
new StripeClient(config('services.stripe.secret'))
),
'square' => new SquareGateawayAdapter(
new SquareClient([
'accessToken' => config('services.square.access_token'),
'environment' => config('services.square.environment'),
])
),
default => throw new \RuntimeException("Unknown payment driver [$driver]"),
};
}
public function store(Request $request)
{
$result = $this->gateway->charge(
(int) ($request->input('amount') * 100),
$request->input('currency', 'USD'),
$request->input('payment_method'),
'Order #' . now()->timestamp
);
if (! $result->success) {
return back()->withErrors(['payment' => $result->message]);
}
return redirect()
->route('thankyou')
->with('tx', $result->transactionId);
}
}
Заключение
Теперь наш код стал намного чище и читаемее, и, разумеется, мы соблюдаем принципы SOLID. Помните: когда у вас есть внешняя библиотека или классы, несовместимые с вашим интерфейсом, вы можете использовать паттерн «Адаптер», чтобы легко внедрить их в свой код и правильно изолировать всю логику. Спасибо за чтение!
Комментарии (0)
Пока нет комментариев — будьте первым.