In every developer’s life, there comes a time when you’re faced with a challenge: add another if and worry about it next time around, or go the extra mile and put a more maintainable structure in place. This choice marks the difference between a junior and senior developer.
More so, senior developers know that there’s no need to reinvent the wheel at every step. They know that design patterns exist precisely for this reason: to produce elegant and maintainable solutions. However, there’s a catch. Choose wrong, and you’ll find yourself trapped in a labyrinth even worse than what you had to begin with.
IPC NEWSLETTER
All news about PHP and web development
Strategy and Decorator
In this article, I will discuss the use of two patterns, Strategy and Decorator, which, at first glance, seem to solve the same problem. But if you look closely, you’ll discover that each one has its particular scope. Let’s explore how you can use them to make your code maintainable over the long run through real-world examples and some tips on when to use each one.
What do they have in common?
To begin with, they both belong in the behavioral category. They deal with problems related to the proper distribution of responsibilities among objects. Other patterns deal with different sets of problems, such as those in the creational category, which provide different approaches to how to create new instances of classes, or those in the structural category, which help when the issue is about combining objects into large structures.
Back to our scope, both the Strategy and Decorator patterns address the challenge of determining, at runtime, which logic should be applied to a particular case. This gives you a first clue about when these patterns are useful: whenever multiple approaches are available and the appropriate choice cannot be determined until the application is actually running.
What’s different about them?
Strategy is about choosing one possibility from a pool of interchangeable options, while Decorator is about choosing many composable possibilities. In other words, you can think of Strategy as a big “or” and of Decorator as a big “and”. Sounds confusing? Let’s explain it with some examples.
A real-life example
Say you have a scenario like this: you are developing an application for a wealth management firm. They manage a portfolio of financial assets on behalf of their clients. You have a data model that looks roughly like this:
<?php
abstract readonly class Security
{
public string $isin;
public function __construct(string $isin)
{
$this->isin = $isin;
}
}
<?php
readonly class Stock extends Security
{
public string $ticker;
public function __construct(string $ticker, string $isin)
{
parent::__construct($isin);
$this->ticker = $ticker;
}
}
<?php
readonly class Bond extends Security
{
public string $description;
public function __construct(string $isin, string $description)
{
parent::__construct($isin);
$this->description = $description;
}
}
<?php
readonly class MutualFund extends Security
{
public string $name;
public function __construct(string $isin, string $name)
{
parent::__construct($isin);
$this->name = $name;
}
}
To produce a particular report, your application needs to know the prices the assets held by clients had at random past dates. Sounds pretty simple, doesn’t it? It’s just about iterating over the collection of assets and, for each one, fetching its price at the specific date.
But you know what they say… the devil’s in the details.
Let’s assume there are three APIs you can query to get the information you need, but not everyone will have data for every security. To make things a little more complicated (and realistic), there’s no rule to determine which one will. And, of course, each API has a different contract you need to abide by.
Let’s take a simple approach: query each API until you get a positive result. Since you can’t know in advance which one will be a hit, the order is not really relevant here. A naive first attempt at it could look like this:
<?php
use Mauro\Strategy\Security;
use Mauro\Strategy\SecurityPrice;
function getPriceFor(Security $security, DateTimeInterface $date): ?SecurityPrice
{
// Try to fetch price from first API
if ($price) {
return $price;
}
// Try to fetch price from second API
if ($price) {
return $price;
}
// Try to fetch price from third API
if ($price) {
return $price;
}
return null;
}
And it’ll work. But, as things move forward, it will get messy. For starters, you’ll have too much responsibility buried inside the getPriceFor function, making the code rather difficult to read and reason about. You could work around this by extracting the logic of interacting with each API into its own private method. That would be a step in the right direction, but it won’t make much progress.
More importantly, you need to think about the future: what will happen when a new data source becomes available? Or when an API changes its contract? Or you find out that one of them produces the expected result 85% of the time? In any of these situations, you’ll have to revisit the code you wrote and tested months ago.
And that’s something you definitely don’t want to do. Once something is working, you want to leave it as it is and forget about it. In fact, writing those tests in the first place is not going to be easy (or pleasant). This is the exact scenario where the Strategy pattern comes to the rescue: it provides a generalisation you can extend indefinitely.
IPC NEWSLETTER
All news about PHP and web development
The Strategy Pattern
In practice, this means having a class to interact with each API and giving your function a collection of objects it can use without worrying about the little details.
The interface
Start by defining an interface that all concrete strategies will implement:
<?php
namespace Mauro\Strategy;
use DateTimeInterface;
interface SecurityPriceFetcher
{
function fetch(Security $security, DateTimeInterface $date): float;
}
The strategies
Now each API integration becomes a class of its own, encapsulating its specific interaction logic:
<?php
namespace Mauro\Strategy;
use DateTimeInterface;
class FirstAPIPriceFetcher implements SecurityPriceFetcher
{
function fetch(Security $security, DateTimeInterface $date): ?SecurityPrice
{
// Code to interact with the first API
}
}
<?php
namespace Mauro\Strategy;
use DateTimeInterface;
class SecondAPIPriceFetcher implements SecurityPriceFetcher
{
function fetch(Security $security, DateTimeInterface $date): ?SecurityPrice
{
// Code to interact with the second API
}
}
<?php
namespace Mauro\Strategy;
use DateTimeInterface;
class ThirdAPIPriceFetcher implements SecurityPriceFetcher
{
function fetch(Security $security, DateTimeInterface $date): ?SecurityPrice
{
// Code to interact with the third API
}
}
You could also create these starting with a base abstract class instead of an interface; it doesn’t really matter that much. The important thing is they are basically performing the same action, though with different approaches.
The client code
With these strategies at hand, the orchestration function becomes much cleaner and generic:
<?php
use Mauro\Strategy\Security;
use Mauro\Strategy\SecurityPrice;
function getPriceFor(Security $security, DateTimeInterface $date, array $fetchers): ?SecurityPrice
{
foreach ($fetchers as $fetcher) {
$price = $fetcher->fetch($security, $date);
if ($price !== null) {
return $price;
}
}
return null;
}
And the calling site is explicit and readable:
echo getPriceFor(
new Bond("AA11232H", "Some government-issued security"),
new DateTimeImmutable(),
[
new FirstAPIPriceFetcher(),
new SecondAPIPriceFetcher(),
new ThirdAPIPriceFetcher(),
]
)->value;
And then, when a new API becomes available, all you have to do is:
1. Create the new class
<?php
namespace Mauro\Strategy;
use DateTimeInterface;
class FourthAPIPriceFetcher implements SecurityPriceFetcher
{
function fetch(Security $security, DateTimeInterface $date): ?SecurityPrice
{
// Code to interact with the third API
}
}
2. Add a new instance of such a class to the array passed to the function getPriceFor
echo getPriceFor(
new Bond("AA11232H", "Some government-issued security"),
new DateTimeImmutable(),
[
new FirstAPIPriceFetcher(),
new SecondAPIPriceFetcher(),
new ThirdAPIPriceFetcher(),
new FourthAPIPriceFetcher(),
]
)->value;
Also, should you realize that the calling order is not ideal, it’s just a matter of reorganizing the array and voilà:
echo getPriceFor(
new Bond("AA11232H", "Some government-issued security"),
new DateTimeImmutable(),
[
new ThirdAPIPriceFetcher(),
new FirstAPIPriceFetcher(),
new FourthAPIPriceFetcher(),
new SecondAPIPriceFetcher(),
]
)->value;
Now things look more promising, don’t they? And, on top of that, you get to write separate tests for each strategy and the orchestration function:
<?php
use Mauro\Strategy\MutualFund;
use Mauro\Strategy\SecurityPrice;
use Mauro\Strategy\SecurityPriceFetcher;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
require_once '../vendor/autoload.php';
require_once '../pricer.php';
class PricerTest extends TestCase
{
#[Test]
public function shouldReturnTheFirstPositiveAnswer(): void
{
$firstFetcher = $this->createMock(SecurityPriceFetcher::class );
$secondFetcher = $this->createMock(SecurityPriceFetcher::class );
$security = new MutualFund("123456", "Mutual fund 1");
$date = new DateTimeImmutable();
$expectedPrice = new SecurityPrice($security, $date, 1);
$firstFetcher
->method("fetch")
->willReturn($expectedPrice);
$secondFetcher
->expects($this->never())
->method("fetch");
$actualPrice = getPriceFor(
$security,
$date,
[
$firstFetcher,
$secondFetcher,
]
);
$this->assertEquals($expectedPrice, $actualPrice);
}
}
The Decorator Pattern
Allow me to illustrate it with another example around the same domain. Let’s say that we want to keep a log of every API call we make. We might be tempted to go back to our getPriceFor function and simply add a little line like:
<?php
use Mauro\Strategy\Security;
use Mauro\Strategy\SecurityPrice;
function getPriceFor(Security $security, DateTimeInterface $date, array $fetchers): ?SecurityPrice
{
global $logger;
foreach ($fetchers as $fetcher) {
$logger->log("Trying ".get_class($fetcher));
$price = $fetcher->fetch($security, $date);
if ($price !== null) {
return $price;
}
}
return null;
}
Looks innocent, doesn’t it? It’s just a simple line, what harm could it do? Probably nothing, but we’re changing a perfectly working piece of code for no good reason.
To make my next point more explicit, let’s assume we’re only interested in logging calls to the first and second APIs, but not the third. Suddenly, things got weird. Are you going to add an if on top of the call to the logger? That doesn’t seem like a good idea. Then again, why should you have to modify code that is performing its duty? A better approach is to put together a small Decorator around the classes that deal with the APIs you’re interested in logging.
The logging Decorator
It all starts with this definition:
<?php
use Mauro\Strategy\Security;
use Mauro\Strategy\SecurityPriceFetcher;
class LoggedPriceFetcher implements SecurityPriceFetcher {
private SecurityPriceFetcher $wrapped;
private Logger $logger;
public function __construct(SecurityPriceFetcher $wrapped, Logger $logger) {
$this->wrapped = $wrapped;
}
public function fetch(Security $security, DateTimeInterface $date): ?SecurityPrice
{
$this->logger->log("Trying ".__CLASS__);
return $this->wrapped->fetch($security, $date);
}
}
We simply create a wrapper around the actual worker class and add the new functionality around the original one. In this case, we’re logging before making the call, but in the same fashion, we might have done it afterwards.
Now it’s up to the caller to use the regular SecurityPriceFetcher or the decorated one.
$logger = new Logger();
echo getPriceFor(
new Stock("I12321312W", "GoogStock"),
new DateTimeImmutable()->sub(new DateInterval("P1D")),
[
new FirstAPIPriceFetcher(),
new LoggedPriceFetcher(new SecondAPIPriceFetcher(), $logger),
new ThirdAPIPriceFetcher(),
]
)->value;
The most important detail here is that, for this to work, the decorator must implement the same interface as the decorated class. That is the “trick” to have the client code (getPriceFor in our case) completely ignorant of the fact that it’s talking to an augmented version of the object it expects.
Perhaps logging doesn’t look like such a big deal to you. Let me try to convince you with a more nuanced example. Let’s say that some APIs measure their prices in Euros while others do it in USD, and your application uses Euros all around. The same principle applies. You could implement this conversion logic in every PriceFetcher or even at the getPriceFor level, but that would be a waste, to say the least, and a big problem if things get out of hand. Think about how you’ll keep track of different exchange rates if they’re scattered all over the place.
Now that you know how your Decorators can save the day (and let’s admit it, make you look cool), why not use one of those bad boys? The gist is pretty similar. We start with:
<?php
namespace Mauro\Strategy;
use DateTimeInterface;
class ConvertToEuroPriceFetcher implements SecurityPriceFetcher
{
private SecurityPriceFetcher $wrapped;
private USDToEURConverter $converter;
public function __construct(SecurityPriceFetcher $wrapped, USDToEURConverter $converter)
{
$this->wrapped = $wrapped;
$this->converter = $converter;
}
public function fetch(Security $security, DateTimeInterface $date): ?SecurityPrice
{
return $this
->converter
->convert($this->wrapped->fetch($security, $date));
}
}
And then we can use it as we see fit:
$logger = new Logger();
$converter = new USDToEURConverter();
echo getPriceFor(
new Stock("I12321312W", "GoogStock"),
new DateTimeImmutable()->sub(new DateInterval("P1D")),
[
new FirstAPIPriceFetcher(),
new LoggedPriceFetcher(
new SecondAPIPriceFetcher(),
$logger
),
new ConvertToEuroPriceFetcher(
new ThirdAPIPriceFetcher(),
$converter
),
]
)->value;
Stacking Decorators
A really cool thing about Decorators is that you can combine them however you want. For instance, you may want to log the calls to the API that need currency conversion. You don’t need to go too far to achieve such behavior. Since the Decorator exposes the same interface as the worker, there’s no reason why you can’t use a decorated object as the input to another decorator:
$logger = new Logger();
$converter = new USDToEURConverter();
echo getPriceFor(
new Stock("I12321312W", "GoogStock"),
new DateTimeImmutable()->sub(new DateInterval("P1D")),
[
new FirstAPIPriceFetcher(),
new LoggedPriceFetcher(
new SecondAPIPriceFetcher(),
$logger
),
new ConvertToEuroPriceFetcher(
new LoggedPriceFetcher(
new ThirdAPIPriceFetcher(),
$logger
),
$converter
),
]
)->value;
As you can see, in the case of the third API, I am using both decorators. With the second API, I’m only using one of them. That’s the beauty of this pattern: you can use any conjunction you need to achieve your goals.
Now, there’s a subtle issue I want to clarify. In this example, the decoration order doesn’t change anything. If I wrote:
$logger = new Logger();
$converter = new USDToEURConverter();
echo getPriceFor(
new Stock("I12321312W", "GoogStock"),
new DateTimeImmutable()->sub(new DateInterval("P1D")),
[
new FirstAPIPriceFetcher(),
new LoggedPriceFetcher(
new SecondAPIPriceFetcher(),
$logger
),
new ConvertToEuroPriceFetcher(
new LoggedPriceFetcher(
new ThirdAPIPriceFetcher(),
$logger
),
$converter
),
]
)->value;
The end result would look exactly the same as the former example, but that’s a mere coincidence. In this particular case, the decorations deal with completely different aspects of the problem, so there’s no interference. In many other scenarios where this is not the case, you want to be very mindful of the order in which your decorators are applied.
Consider a hypothetical CachedPriceFetcher Decorator. Should you log before or after checking the cache? That depends entirely on what you’re trying to observe. The ordering becomes a meaningful design decision rather than an afterthought.
What about Traits?
Another point I’d like to explore in this article is the use of Traits instead of Decorators. After all, they seem to offer a pretty similar advantage, don’t they? With Traits, you can have your objects implement extra functionality without re-coding it over and over. So, should you prefer them to old-fashioned Decorators? I’m afraid the answer is most likely “No.”
Here’s why. Traits are, at their core, a form of horizontal inheritance. When a class uses a Trait, the link is static—sealed at compile time. You can’t choose at runtime to give an instance of FirstAPIPriceFetcher the logging trait but not the conversion trait, and another instance the other way around.
What I mean by this is that, unlike Decorators, you can’t dynamically combine them however you might need. Once a class uses a Trait, that’s it; their link is sealed for good. You can always resort to some obscure reflection tricks to break it, but if you have to go that way, you should be having second thoughts already.
Let me show you an example to make the point clearer. Say you implement the logging mechanism as a trait:
<?php
namespace Mauro\Strategy;
use Logger;
trait LoggableFetcherTrait
{
private Logger $logger;
public function setLogger(Logger $logger): void
{
$this->logger = $logger;
}
private function logAttempt(string $className): void
{
if (isset($this->logger)) {
$this->logger->log("Trying " . $className);
}
}
}
That would imply changing the Strategies to something like:
<?php
use Mauro\Strategy\LoggableFetcherTrait;
use Mauro\Strategy\Security;
use Mauro\Strategy\SecurityPrice;
use Mauro\Strategy\SecurityPriceFetcher;
class FirstAPIPriceFetcher implements SecurityPriceFetcher
{
use LoggableFetcherTrait;
public function fetch(Security $security, DateTimeInterface $date): ?SecurityPrice
{
$this->logAttempt(__CLASS__);
// Do the magic
return $price;
}
}
This takes away the whole point of decorators, doesn’t it? Now you need to explicitly call the log method. More so, when the time comes to combine these kinds of traversal features, the headaches grow exponentially as your classes need to go out of their way to keep track of responsibilities they didn’t have before.
Also, their all-or-nothing nature is the definite argument against Traits in this scenario. When you use a Trait, you are saying that all instances of the class will exhibit a specific behavior. With a Decorator, you can decide that on a case-by-case basis, giving you way more flexibility.
Now, I don’t want you to end up with the idea that Traits are somehow evil or that I have something personal against them. As with any other tool, they have their use cases, such as timestamp management, soft-deletes, or serialisation helpers. It’s just that they are not a replacement for Decorators.
As for the testing side, the same principles I discussed for the Strategy case apply here. Unlike the monolithic function version, there’s no need for real (expensive and unpredictable) API calls, elaborate mocks, or reflection gymnastics. Each test stays focused and clean.
The SOLID connection
Though I didn’t mention it explicitly until now, it’s worth noting that, by leveraging these patterns, you’re complying with both Single Responsibility and Open/Close principles (a very important part of SOLID).
SRP
Both the Strategy and the Decorator have a very clear function, one that can be defined independently of the system surrounding them and, most importantly, changed without affecting it.
Should the protocol change for any of the APIs, the update would be circumscribed to a single class, which, once properly tested, can safely replace the pre-existing implementation.
The same is true for the Decorators. Since there is such a clean separation of concerns, moving to a different logging mechanism doesn’t generate any impact on any part of the application whatsoever.
OCP
At this point, it should be clear that new Strategies and Decorators can be added at any time and without significant effort, which effectively makes the application easily extensible to adapt to the constantly evolving business environment. Of course, this is by no means a coincidence. The patterns were designed with these goals in mind.
When not to use these patterns
As with any other tool in your box, there are times when the best course of action is to leave them out of sight, and, while the patterns I’ve shown you through this article are really valuable, there are times when they take more than they provide.
In general, I’d recommend you stay away from Strategy when you only have one implementation and no foreseeable need for others. I want to stress the last part of that sentence: don’t let yourself get trapped in “but what if…?” thoughts. You’ll fix problems when they’re actually there; anything before that is just speculation.
Also, skip the Decorator when the additional behavior is always required. If you never create an undecorated version, there’s no point in the wrapper.
Summary
Here’s what you learned in this article:
- Both Strategy and Decorator are good alternatives to nested if statements.
- The Strategy pattern is useful when there are many ways to achieve the same outcome, and you want to use the most appropriate one for the context.
- The Decorator pattern is useful when there are complementary features you want to add to a core functionality.
- The Strategy pattern and the Decorator pattern are not replacements for each other but rather complements (they make a powerful team).
- The order in which you apply your Decorators can have a significant impact on the end result.
- Both patterns make your code dramatically easier to test.
- Traits offer static composition. Decorators offer dynamic composition.
Here’s a little cheat sheet in case you’re in doubt about which option to use:

Final Thoughts
The next time you are tempted to add a boolean parameter like $withLogging or $convertToEuro to your function, stop for a second and ask yourself, ”Am I choosing a path, or am I adding a layer?” Either way, chances are a Strategy or a Decorator will be a better option in the long run. And you already know how to make the choice between them.
Your future self—the one who will have to maintain this code six months from now when the APIs change and the business rules double—will thank you for choosing the elegance of composition over the immediate convenience of an if statement.
Author
🔍 Frequently Asked Questions (FAQ)
1. What is the difference between Strategy and Decorator in PHP?
The Strategy pattern is used when an application needs to choose one option from several interchangeable implementations. The Decorator pattern is used when an application needs to add one or more complementary behavior layers around an existing object. In the article’s terms, Strategy is like an “or,” while Decorator is like an “and.”
2. When should PHP developers use the Strategy pattern?
PHP developers should use the Strategy pattern when there are multiple ways to achieve the same outcome and the right implementation can only be selected at runtime. In the article’s example, different API price fetchers are modeled as separate strategies that implement the same interface. This makes the code easier to extend, reorder, and test.
3. When should PHP developers use the Decorator pattern?
PHP developers should use the Decorator pattern when they need to add optional behavior to an object without modifying its existing implementation. The article demonstrates this with logging and currency conversion decorators around price fetchers. Because decorators implement the same interface as the wrapped object, the client code does not need to know whether it is using a decorated or undecorated object.
4. Why are Decorators often more flexible than Traits in PHP?
Decorators are more flexible than Traits when behavior needs to be selected or combined at runtime. Traits are a form of static composition because the relationship is fixed when the class is defined. Decorators allow different object instances to receive different combinations of behavior, such as logging, currency conversion, or both.
5. How do Strategy and Decorator support SOLID principles?
Strategy and Decorator support the Single Responsibility Principle because each class has a focused role that can be changed independently. They also support the Open/Closed Principle because new strategies or decorators can be added without changing the existing orchestration code. This helps PHP applications remain extensible as APIs, business rules, or technical requirements change.
6. When should developers avoid using Strategy or Decorator?
Developers should avoid the Strategy pattern when there is only one implementation and no realistic need for alternatives. They should avoid the Decorator pattern when the additional behavior is always required and an undecorated version is never used. In those cases, the extra abstraction may add more complexity than value.





