This article describes a way of implementing Strategy design pattern using a database-based runtime configuration. The approach allows to dynamically (re)configure class behavior without requiring code-based rearrangements.
It comes out from my latest project where I needed to find a way to dinamically extract a subset of a record relations, without hard-coding the pertinent strategy.
Strategy Design Pattern
Many of you are (indeed should!) be familiar with the Strategy design pattern. Strategy is a mean to delegate a configurable partial behavior to an external objects.
For example, consider what you would do to implement an algorithm to determine the best route to a location, and you could choose from a bunch of alternatives:
- Use a remote service like Google Map
- Ask people for indications
- Wander randomly until you eventually reach a destination
These adds to some additional pre- and post-processing static steps.
By implementing Strategy you would code something like this:
RouteGeneratorService.php
1class RouteGeneratorService
2{
3 private $strategy;
4
5 public function __construct(RoutingStrategy $strategy)
6 {
7 $this->strategy = $strategy;
8 }
9
10 public function generate(Destination $destination): Route
11 {
12 $this->initializeSomething();
13
14 $route = $this->strategy->getRouteFor($destination)
15
16 $route = this->refineRoute($route);
17
18 return $route;
19 }
20
21 private function initializeSomething()
22 {
23 ...
24 }
25
26 private function refineRoute(Route $route): Route
27 {
28 ...
29 }
30}
RoutingStrategy is an external interface for implementing the alternative behaviors. It is defined as:
RoutingStrategy.php
1interface RoutingStrategy
2{
3 public function getRouteFor(Destination $destination): Route;
4}
It can be implemented by concrete strategies. For example:
GoogleMapRouting.php
1public class GoogleMapRouting implements RoutingStrategy
2{
3 public function getRouteFor(Destination $destination): Route
4 {
5 // Connect to GoogleMap services to generate a route
6 $route = ...
7
8 return $route;
9 }
10}
AskForIndicationsRouting.php
1public class AskForIndicationsRouting implements RoutingStrategy
2{
3 public function getRouteFor(Destination $destination): Route
4 {
5 // Open a chat channel with other people to ask for indications
6 $route = ...
7
8 return $route;
9 }
10}
RandomWanderingRouting.php
1public class RandomWanderingRouting implements RoutingStrategy
2{
3 public function getRouteFor(Destination $destination): Route
4 {
5 // Generate random route to destination
6 $route = ...
7
8 return $route;
9 }
10}
Then, to use the RouteGenerationService you will do the following:
1...
2$strategy = new RandomWanderingRouting();
3
4$routeGenerator = new RouteGeneratorService($strategy);
5
6$route = $routerGenerator->getRouteFor($destination);
7...
My problem
Simplifying my use case, I have some Events classes, belonging to an EventType. Each EventType relates to some Participants who apply for participating to Event instances, based on the fact that they Candidated to that particular EventType. All of these entities and relations are mapped as follows:

Now, I need to rank potential participants to each event, according to some criteria, related to Participants properties. Additionally, each participant can be excluded from the ranking if it does not meet other criteria.
Important note: both the filtering and ranking criteria are specific for each EventType
I can easily implement this using Strategy:
1class Event
2{
3 public function getRanking()
4 {
5 $filteringStrategy = ...;
6 $rankingStrategy = ...;
7
8 return $this->eventType->candidacies->pluck('participants')
9 ->filter($filteringStrategy)
10 ->sort($rankingStrategy);
11 }
12}
(for who is not familiar with Laravel, I reach the potential participants to an event by traversing the relations through events->event_type->candidacies->participants, and then filter and sort the resulting collection)
In this case $filteringStrategy and $rankingStrategy are defned as instances of Callable classes, i.e. implementing the __invoke() magic method, for example:
Example of FilteringCallable
1public class ExampleFilter
2{
3 public function __invoke(Participant $item)
4 {
5 return $item->hasSomeProperty();
6 }
7}
Example of SortingCallable
1public class ExampleSorter
2{
3 public function __invoke(Participant $item1, Participant $item2)
4 {
5 return $item1->someProperty() > $item2->someProperty();
6 }
7}
Now, the problem is, how should I assign $filteringStrategy and $rankingStrategy?
The 'naive' approach is to assign it according to an EventType property, for example label:
1if ($this->eventType->label == 'contest') {
2 $filteringStrategy = new ContestFiltering();
3}
4elseif ($this->eventType->label == 'poll') {
5 $filteringStrategy = new PollFiltering();
6}
However, this is an evident violation of the Open-closed principle of SOLID: whenever I add a new EventType or simply update the label, I need to update the code.
So, I needed to find an alternative.
The solution is to define both the filtering and sorting strategies as entity fields. So I added two fields in the EventType entity, filter and sorter, each contain a reference to a specific class implementation:

event_types table
| label | filter | sorter |
|---|---|---|
| contest | App\Models\Filters\ContestFilter::class | App\Models\Filters\ContestSorter::class |
| poll | App\Models\Filters\PollFilter::class | App\Models\Filters\PollSorter::class |
then, the getRanking() method of Event class can be implemented as:
Event.php
1class Event
2{
3 public function getRanking()
4 {
5 return $this->eventType->candidacies->pluck('participants')
6 ->filter(new $this->filter)
7 ->sort(new $this->sorter);
8 }
9}
Filtering and sorting is performed by passing a new instance of the specified Filter and Sorter.
The advantages
By using this approach, if the filtering/sorting requirements change, no modifications to the existing code are required. You are required only to:
- Defining a new class implementing the new beahvior
- Change a field in the
event_typesrecord
And everything will continue to work.
The disadvantage
The only drawback of this approach is that you could incur in runtime exceptions if the specified class does not exist or is wrongly implemented. However, this problem will always exist in interpreted languages like PHP.
Conclusion
The article presented a specific solution to a general problem: defining a data-oriented strategy pattern implementation.
Any opinion, suggestion and/or criticism is welcome. Please leave comments in the section below.