Permalink • April 27, 2016
For those times when you need to branch out behavior based on dynamic information.
Maybe you offer multiple payment providers, and let the user pick one - or maybe you just have a bunch of alternate logic paths depending on some system status. Once in a while, you run into a situation where you need to defer logic into one of a selection of classes, based on something determined at runtime.
And, of course, your code still needs to look “clean” and all that jazz, right?
Let’s work with a real-world scenario.
”I offer Stripe and PayPal payment gateways on my Invoicing solution. Users can pick which gateway they want to use when they pay for an invoice - each option leads to completely separate logic, redirecting to different pages and issuing different API requests.”
This is a question I get regularly on Laravel’s IRC and Slack support rooms. And by regularly, I mean “every few days”.
The first step seems obvious: make some base gateway classes, one for each gateway/payment provider, and an interface to match.
interface PaymentGateway {
function pay (User $payee, Order $order);
}
class PaypalPaymentGateway implements PaymentGateway {
// use the constructor to provide general information
function __construct ($apiKey) {
$this->apiKey = $apiKey;
}
function pay (User $payee, Order $order) {
// do stuff here using the $order information and the $payee
return new Redirect("/pay/paypal");
}
}
What alarms me is that the go-to solution often involves wacky IoC container magic - sometimes even making bindings from within a controller.
(And it’s doubly-terrifying because you can wrangle the IoC container to do this, but it’d be like coaxing a clotheshanger to fill the role of a fishing pole.)
The dead-simple solution is, well, dead-simple.
class PaymentController extends Controller {
// Please don’t actually do this, unless you’re
// REALLY short on time
function pay (Request $request, Order $order) {
switch ($request->get('gateway')) {
case ("paypal") : {
return (new PaypalPaymentGateway(Config::get('paypal.api_key')))
->pay(Auth::user(), $order);
break;
}
default : {
throw new Exception("Invalid payment gateway");
}
}
}
}
This will work. Of course, it also means you have this jumble of code sitting around in a controller, waiting to terrorize the innocent citizens of your codebase. Plus, you have stuff like Config::get()
in there, which really needs to be banished elsewhere in the code.
So where do we go?
Many a developer decides to exile this code into a trait
, but really, that’s just pushing your dirty sweaters under your bed.
Say hello to a registry class!
class PaymentGatewayRegistry {
protected $gateways = [];
function register ($name, PaymentGateway $instance) {
$this->gateways[$name] = $instance;
return $this;
}
function get($name) {
if (in_array($name, $this->gateways)) {
return $this->gateways[$name];
} else {
throw new Exception("Invalid gateway");
}
}
}
At this point I could point you at the Registry Pattern over at the great book of truth - but come on, that looks pretty self-explanatory to me. You first give the class a bunch of gateways, then when you want a gateway you just tell it a name and it gives you a proper instance.
(You might be asking yourself: isn’t that basically an associative array? Yes, yes it is, but that’s not OOP, kids. (It’s just harder to handle a floating array than a specific class, and that’s why the registry is just a wrapper over a single array. One day PHP will have object-oriented arrays and we can do class Registry extends Array
. One day.))
So now we need to give the registry some gateways. Where do we do that, you ask? Where we do anything else that needs to be executed on initialize - a Service Provider.
class PaymentServiceProvider extends ServiceProvider {
function register () {
$this->app->singleton(PaymentGatewayRegistry::class);
}
function boot () {
$this->app->make(PaymentGatewayRegistry::class)
->register("paypal", new PaypalPaymentGateway(Config::get('paypal.api_key')));
}
}
We first set the PaymentGatewayRegistry
class as a singleton - meaning we always get the same instance whenever we App::make()
(or app()
or dependency-inject) the class. This means we can then conjure it up in the boot()
method, attach a Gateway to it, and it’ll be available everywhere. This also means we can attach more Gateways from other Service Providers.
Note that the Config::get()
invocation also moved to the service provider. As a general rule, access to Config values should be limited to service providers, and given to other classes like this. It’s just more testable, yo.
And now we just need to grab the registry from within a controller. Behold, dependency injection:
class PaymentController extends Controller {
function __construct (PaymentGatewayRegistry $registry) {
$this->gatewayRegistry = $registry;
}
function pay (Request $request, Order $order) {
return $this->gatewayRegistry->get($request->get('gateway'))
->pay(Auth::user(), $order);
}
}
And there you have it. Everything is nice and tucked away in their own files - logic in the respective Gateways, registration and setup in a Service Provider, and the controller looks nice and clean.
… Remember that something doesn’t have to be a “pattern” to deserve to exist. You’re free to make classes to hold logic as you see fit, regardless of if some smart dudes somewhere have “formalized” the pattern.
The Registry class above isn’t really any particular pattern - it’s a pragmatic implementation that has a lot of useful traits - being fully testable and expandable being the tip of the iceberg. Don’t be afraid to tinker with your own patterns and classes!