Today I Learnt — How to inject multiple instances of an interface in a service

Recently, I was working on a feature for an application to validate card payments. At the time, we were working with just one service provider with the possibility of adding more in the future. This got me thinking — is there a SOLID way of doing this?
Well, my first thought was to have an interface and then for each service provider, I’d provide an implementation of the interface. In the short run, it solved my problem, I’d interact with the interface wherever I needed to and let Symfony handle the rest (the autowiring subsystem is pretty smart — it’ll inject my implementation without me doing anything else). In the long run however, there were still some hurdles.
With more than one implementation of an interface, I’d have to do some extra work. At the time, I also felt like I’d have to pick one service and bind that to the interface. It didn’t feel very flexible so I headed to Google for some good old investigative programming 😊. I came across a lovely article by Wouter Carabain which perfectly answered my question.
This article follows exactly the same process and will largely feel like a replication of the article, however there are two key differences.
- While following the steps in the articles I encountered some errors — perhaps due to some updates to later versions of Symfony. In that sense, this is kind of an update to Wouter’s article.
- In the comments, there was a suggestion to make the article clearer by providing a real life example. I thought I’d take the opportunity and do that as well.
Alright then let’s get started.
Define your Interface
Here, we’ll define the interface all the payment validation services have to implement.
Because we’ll have multiple implementations, we use the supports
function to determine which implementation to use. The validate
function is where the service does the actual validation of the payment.
The Payment
entity is a PHP object which holds the payment reference and amount.
Create Payment Validation Services
Let’s assume we want our application to handle card payments via Paystack or Flutterwave. Let’s create the services to handle validation for each payment provider.
For the Paystack validation service we have the following.
And for the Flutterwave validation service, we have the following.
Create “Factory” Service
The next thing is to provide a service which “aggregates” the payment validation services and determines which one to use (based on the supports
function). This is what the service looks like.
This is where I had to deviate from Wouter’s solution. When I went with the solution in the article, I kept getting a strange error:
Argument #1 must be of type App\Interfaces\PaymentValidatorInterface, Symfony\Component\DependencyInjection\Argument\RewindableGenerator given
The solution was to rework the constructor — starting with a change to the type hint and constructor argument. It turns out that when injecting tagged services (in this case our payment validation service implementations) to the PaymentValidatorService
, a RewindableGenerator object is passed to the constructor. This class implements the IteratorAggregate
interface which is a grandchild of the iterable
interface. To get our services as an array, we use the iterator_to_array
function.
Once I did that, the rest of Wouter’s solution worked perfectly. In the execute
function, I used a foreach
to iterate the services, determine which service to use and call the validate
function on the service to validate the payment. Because I wanted to throw an exception when none of the services supported the payment type, I used an early return once the validation was complete and threw an exception outside the foreach
loop.
Service Tagging and Autowiring
The last thing we need to make this work is to tag the services that implement PaymentValidatorInterface
and autowire the PaymentValidatorService
so that the tagged services are available in the constructor. We do this in config/services.yaml
as shown below.
And that’s basically it. Use the PaymentValidatorService
anywhere payment validation is required and the relevant implementation will handle the rest. As usual, it was a lot to set up at first but going forward my life would be so easy. If I received a request to take down support for Flutterwave payments, all I need to do is delete the FlutterwavePaymentValidator
and everything else stays the same. What if I need to add support for a new provider? I just create a new class that implements the PaymentValidatorInterface
and we’re good to go - I can already see the future me thanking the past me for this 😊 .
If you get stuck at any point, you can consult this repo. I would also love to hear your thoughts on this approach and even alternatives to this.
Until next time, make peace not war ✌🏾