In real-world Laravel applications, we often work with multiple service classes that do the same job in different ways.
For example, multiple payment gateways, external API providers, or notification services.
A common challenge is deciding which service to use at runtime without filling our code with if-else or switch statements.
In this blog, I’ll explain how to use a Resolver pattern in Laravel to solve this problem in a clean and scalable way with a real-world example.
The Problem: Too Many if-else Conditions
Let’s say we support multiple payment gateways in our application: Stripe and PayPal.
A common approach looks like this:
if ($paymentType === 'stripe') {
$service = new StripePaymentService();
} elseif ($paymentType === 'paypal') {
$service = new PaypalPaymentService();
} else {
throw new Exception('Invalid payment type');
}
$service->charge($data);
What’s wrong with this approach?
Business logic is tightly coupled
Adding a new payment gateway means modifying existing code
Hard to test and maintain
Violates the Open/Closed Principle
This code works, but it doesn’t scale well.
What Is a Resolver?
A Resolver is a class responsible for deciding which service implementation should be used based on runtime data.
In simple terms:
A resolver returns the correct service class for you, so your business logic stays clean.
Real-World Example: Payment Service Resolver
Let’s refactor the above example using a Resolver pattern.
Step 1: Create a Common Interface
All payment services should follow the same contract.
interface PaymentServiceInterface
{
public function charge(array $data): bool;
}
Step 2: Create Service Implementations
Stripe Service
class StripePaymentService implements PaymentServiceInterface
{
public function charge(array $data): bool
{
// Stripe payment logic
return true;
}
}
PayPal Service
class PaypalPaymentService implements PaymentServiceInterface
{
public function charge(array $data): bool
{
// PayPal payment logic
return true;
}
}
Each service handles its own logic, but they all follow the same interface.
Step 3: Create the Resolver Class
class PaymentServiceResolver
{
public function __construct(
public readonly StripePaymentService $stripePaymentService,
public readonly PaypalPaymentService $paypalPaymentService
) {}
public function resolve(string $type): PaymentServiceInterface
{
return match ($type) {
'stripe' => $this->stripePaymentService,
'paypal' => $this->paypalPaymentService,
default => throw new InvalidArgumentException('Unsupported payment type'),
};
}
}
Why use constructor injection?
Makes dependencies explicit and easier to understand
Improves unit testability (easy to mock services)
Keeps the resolver clean and focused
Fully leverages Laravel’s dependency injection container
💡 Note :
You could also resolve services using app() inside the resolver, but constructor injection is generally preferred for cleaner architecture and better testing.
Step 4: Use the Resolver in Your Business Logic
class PaymentController extends Controller
{
public function store(
Request $request,
PaymentServiceResolver $resolver
) {
$paymentType = $request->get('payment_type');
$service = $resolver->resolve($paymentType);
$service->charge($request->all());
return response()->json([
'message' => 'Payment processed successfully'
]);
}
}
✨ Clean, readable, and easy to extend.
Adding a New Service Is Easy
Want to add Razorpay later?
Create
RazorpayPaymentServiceImplement
PaymentServiceInterfaceUpdate resolver
'razorpay' => $this->razorpayPaymentService,
No changes needed in controllers or business logic.
Benefits of Using Resolvers
✅ Cleaner and readable code
✅ Easy to add new services
✅ Follows SOLID principles
✅ Centralized service selection logic
✅ Easy to unit test
Bonus: Making the Resolver More Flexible
Instead of hardcoding values, you can move mapping to a config file:
// config/payment.php
return [
'stripe' => StripePaymentService::class,
'paypal' => PaypalPaymentService::class,
];
Resolver
class PaymentServiceResolver
{
public function resolve(string $type): PaymentServiceInterface
{
$service = config("payment.$type");
if (! $service) {
throw new InvalidArgumentException('Unsupported payment type');
}
return app($service);
}
}
This makes your system even more configurable.
Final Thoughts
Resolvers are a simple yet powerful pattern for managing multiple service implementations in Laravel.
They help you:
Avoid messy conditionals
Keep business logic clean
Build systems that scale gracefully
If you work with multiple APIs, payment gateways, or providers, Resolvers can significantly improve your code quality.
Top comments (1)
A resolver class feels like an old pattern with the current PHP features.
While it does less than the resolver in the example, it is more powerful.
fromandtryFrommethods it is possible to check if the application supports the method from the input. No need for a default option in thematchcall.Using configuration is a good tip, but just using it for a list of payment gateways is not really useful. It isn't that often that a gateway is added or removed.
A better use is to add the authorization and/or authentication data, because that can (and should) change per environment.
By using an enum, it is possible to make the config more robust.
Instead of using the application container the service is instantiated using the config. This is only possible when the class doesn't rely on other instantiated classes.
Resolvers are a good pattern, but using a class is ignoring what PHP offers you to write more reusable code.