During my coding sessions, I have been confronted with multiple problems of various complexity.
Solving those for the first time was a real challenge since you have to think about the best solution for both :
- short term: Does it really solve it? Does it fit in the time I have for this task?
- long term: Can it be understood by future developers? Can it be enhanced when the original need evolves?
And quite a lot of other questions.
From those developments, I have been able to extract some patterns. Even though it is a bit longer to implement than a more procedural solution, it appears that those patterns are really reusable and are completely extendable.
Composite service
A composite service is a generic service that has “children” services injected. His role is to provide developers a unique service to call. The composed service will then call other services, and it can even do it according to a priority queue.
For this example, I am creating a checker that will check whether a payment is refundable. The payment is the first argument, and the second one contains an array of refund options, such as amount, what service is used to transfer the funds, …
Imagine this is your service interface :
interface EligibilityCheckerInterface {
public function isEligible(Payment $payment, array $options = []): bool;
}
And this is your composite service :
use Zend\Stdlib\SplPriorityQueue;
final class CompositeEligibilityChecker implements EligibilityCheckerInterface
{
/** @var SplPriorityQueue|EligibilityCheckerInterface[] */
private $checkers;
public function __construct()
{
$this->checkers = new SplPriorityQueue();
}
public function addChecker(EligibilityCheckerInterface $checker, int $priority = 0): void
{
$this->checkers->insert($checker, $priority);
}
public function isEligible(Payment $payment, array $options = []): bool
{
/** @var EligibilityCheckerInterface $checker */
foreach ($this->checkers->toArray() as $checker) {
if (!$checker->isEligible($payment, $options)) {
return false;
}
}
return true;
}
}
Notice the `addChecker` method. Using a ZendPriorityQueue, you will be able to add multiple services to the queue with a priority.
Then the `isEligible` method will simply loop through this queue, and to its checks. It is up to you to declare how the check is done and whether stop at first successful, or whatever you need.
And you can instantiate multiple CompositeEligibityChecker with different queues. So each one will perform a different action.
In my case, each real checkers has a unique check to perform. One will be to compare the payment amount with the wanted refund, another one will be to check that Payment has been successful before trying to refund it. And if any checker answers `no` then the payment is considered as not eligible.
Supporting or not
You can also add a concept to this Composite service, the `support` method.
We will add this to our interface :
public function supports(Payment $payment, array $options = []): bool;
And then also to our composite service :
public function supports(Payment $payment, array $options = []): bool
{
/** @var EligibilityCheckerInterface $checker */
foreach ($this->checkers->toArray() as $checker) {
if ($checker->supports($payment, $options)) {
return true;
}
}
return false;
}
Once again, composite is quite simple in implementation. We will simply loop through each service, and as soon as one supports our arguments, the composite will also support them.
And finally, we can change our `isEligible` method like this :
public function isEligible(Payment $payment, array $options = []): bool
{
if (!$this->supports($payment, $options)) {
return false;
}
/** @var EligibilityCheckerInterface $checker */
foreach ($this->checkers->toArray() as $checker) {
if ($checker->supports($payment, $options) && !$checker->isEligible($payment, $options)) {
return false;
}
}
return true;
}
So, from now on, each checker will first have to support the provided arguments before being able to say if it is eligible or not.
Such a pattern is quite often used, like in Sylius for OrderProcessing, or in Symfony for Security Voters.
Service Provider
Another useful pattern is the service provider. The aim is quite simple, according to provided arguments, we will return the user a service.
Imagine we want to provide an Adapter for importing data. But you can have different sources, like XLS, JSON, XML, CSV, … You could do that with a if/else and fetch whatever service you need, but you can also do it with a provider.
Let’s have this interface :
interface AdapterProviderInterface {
public function getAdapter($data, string $sourceFormat, string $targetFormat): ?AdapterInterface;
}
Notice that here, we are taking benefits of PHP loose typing here by not giving information about $data.
And then, here is our provider :
<?php
use Zend\Stdlib\PriorityQueue;
final class AdapterProvider implements AdapterProviderInterface
{
/** @var PriorityQueue|AdapterInterface[] */
private $pool;
public function __construct()
{
$this->pool = new PriorityQueue();
}
public function addAdapter(AdapterInterface $adapter, int $priority = 0): void
{
$this->pool->insert($adapter, $priority);
}
public function getAdapter($data, string $sourceFormat, string $targetFormat): ?AdapterInterface
{
/** @var AdapterInterface $adapter */
foreach ($this->pool->toArray() as $adapter) {
if ($adapter->supports($data, $sourceFormat, $targetFormat)) {
return $adapter;
}
}
return null;
}
}
Once more, we will use ZendPriorityQueue since it is quite a convenient way to offer priority system, but a simple array could also work.
And then, we will fetch the first Adapter that supports our data with the desired formats. In the Provider support method, you can do whatever you want. Test if $data is an array, or if the source and target format match what you expect
Conclusion
Those are two patterns that I often use, which are quite easy to understand, and also very powerful and highly extendable. For example, if you want to add a new supported type for the Import feature we just described, then simply add a new Adapter, work on it without being afraid of breaking the other ones and add it to the queue. The rest of your code will not even have to change a single line to use this new Adapter.
So, what do you think of those two patterns? Are you already using them?
And what about you? Do you have any patterns that you use and want to share?