DEV Community

Cover image for Simplifying Service Selection in Laravel Using Resolvers
Naveen Kola
Naveen Kola

Posted on

Simplifying Service Selection in Laravel Using Resolvers

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);
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Create Service Implementations

Stripe Service

class StripePaymentService implements PaymentServiceInterface
{
    public function charge(array $data): bool
    {
        // Stripe payment logic
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

PayPal Service

class PaypalPaymentService implements PaymentServiceInterface
{
    public function charge(array $data): bool
    {
        // PayPal payment logic
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

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'),
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

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'
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

✨ Clean, readable, and easy to extend.

Adding a New Service Is Easy

Want to add Razorpay later?

  • Create RazorpayPaymentService

  • Implement PaymentServiceInterface

  • Update resolver

'razorpay' => $this->razorpayPaymentService,
Enter fullscreen mode Exit fullscreen mode

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,
];
Enter fullscreen mode Exit fullscreen mode

Resolver

class PaymentServiceResolver
{
    public function resolve(string $type): PaymentServiceInterface
    {
        $service = config("payment.$type");

        if (! $service) {
            throw new InvalidArgumentException('Unsupported payment type');
        }

        return app($service);
    }
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
xwero profile image
david duymelinck • Edited

A resolver class feels like an old pattern with the current PHP features.

enum PaymentGateway: string
{
  case Stripe = 'stripe';
  case Paypal = 'paypal';

  public function getService()
  {
     return match($this) {
        PaymentGateway::Stripe => StripeService::class,
        PaymentGateway::Paypal => PaypalService::class,
     };
  }
}
Enter fullscreen mode Exit fullscreen mode

While it does less than the resolver in the example, it is more powerful.

  • With the backed enum from and tryFrom methods it is possible to check if the application supports the method from the input. No need for a default option in the match call.
  • Because the service classes are not tied to a container of a specific framework, the enum doesn't require changes when used in other projects.

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.

// config/paymentgateways.php
return [
   PaymentGateway::Stripe => [
       'token' => env('STRIPE_TOKEN'),
   ],
   PaymentGateway::Paypal =>  [
      'token' => env('PAYPAL_TOKEN'),
   ]
];

// app/Payment/functions.php

function createGatewayService(string $gateway)
{
    $appGateway = PaymentGateway::tryFrom($gateway);

    if($appGateway === null) {
       return null;
   }

   try {
     $config = config('paymentgateways');
     $appGatewayConfig = $config[$appGateway];

     return new $appGateway->getService()(...$appGatewayConfig);
   }catch(Exception $e) {
     // logging
    return null;
   }
}
Enter fullscreen mode Exit fullscreen mode

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.