Welcome back. This is the next article in our practical use of patterns in Laravel, and in this guide, we are going to talk about one of the most popular patterns that is used by many developers — the Adapter pattern.
Introduction
Let’s begin with the definition of Adapter:
Adapter pattern is a structural design pattern that allows objects with incompatible interfaces to work together by translating one interface into another that a client expects.
Real-world example
Imagine that you’re traveling from the US to the EU, and when you arrive at your hotel, you find out that in the EU, people use a different type of socket to use their electronic devices. And to solve this problem, you buy a socket adapter to easily charge your devices with your US plug.
Bad example (anti-pattern)
First, let’s see what a bad solution looks like without using any adapter classes and without any clean code architecture:
<?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']);
}
}
}
Looks horrible 0.0 Now it’s time to separate all logic properly with best practices, and to solve it — we are going to use the Adapter pattern.
Adapter pattern implementation
First of all, we need to define an interface that will be used by our adapter classes to properly handle different payment systems with the same function:
<?php
namespace App\Domains\Payment;
interface PaymentGateawayInterface
{
public function charge(
int $amount,
string $currency,
string $source,
string $description = ''
);
}
Also, don’t forget to install the payment library with Composer into the Laravel project:
$ composer require stripe/stripe-php
After dependencies are installed, we also need to define a class where we are going to store transaction results
<?php
namespace App\Domains\Payment;
class ChargeResult
{
public function __construct(
public bool $success,
public string $transactionId,
public string $message,
) {}
}
Now that the main components are defined, it’s time to create our first adapter to handle Stripe transactions:
<?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()
);
}
}
}
But that's not the only adapter class that we need to create; we will also create an adapter to handle Square payment system transactions:
<?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(),
);
}
}
}
That looks much better now with using adapters; our code is now cleaner, readable, and we follow SOLID principles with OOP. And of course, if we are using any interface in the Laravel framework, we also need to bind it properly in the 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
{
//
}
}
Amazing, now we can easily use it in our controllers:
<?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);
}
}
Conclusion
Now our code is much cleaner and readable, and of course, we follow SOLID principles. Remember, in cases when you have any external library or classes that are not compatible with your interface, you can use the Adapter pattern to easily implement them into your code and separate all logic properly. Thanks for reading!
Top comments (0)