Originally published on decentphp.dev — part of the Writing decent PHP course. View this lesson on the original site.
Constraints change what "good" looks like
Clean code is not one fixed shape.
What is "best" depends on context: deadlines, team size, risk, and how often the code will change.
We have already alluded to this in earlier lessons, but it is important enough to repeat clearly: context decides the right level of structure.
Before
This version abstracts too early for a simple one-line rule.
<?php
interface PriceRule
{
public function apply(int $totalInPence): int;
}
final class StandardVatRule implements PriceRule
{
private const VAT_RATE = 1.20;
public function apply(int $totalInPence): int
{
return (int) round($totalInPence * self::VAT_RATE);
}
}
final class PricingEngine
{
public function __construct(private PriceRule $rule)
{
}
public function finalTotal(int $totalInPence): int
{
return $this->rule->apply($totalInPence);
}
}
After
In this context, the same behaviour can be expressed more directly.
<?php
const VAT_RATE = 1.20;
function finalTotalWithVat(int $totalInPence): int
{
return (int) round($totalInPence * VAT_RATE);
}
Trade-off to understand
The "before" version is not always wrong.
If you have multiple VAT rules, country-specific logic, or frequent policy changes, abstraction may be the right call.
For a stable single rule, the simple version is easier to read, review, and maintain.
Good PHP is context-aware, not pattern-hungry.
From Writing decent PHP — What decent PHP looks like