Introduction
You know that feeling when you open a file and instantly regret it?
That’s your nose telling you: “This code smells.”
A code smell isn’t necessarily a bug — it’s a symptom of deeper design issues.
The code might still work, but it’s fragile, confusing, or hard to extend.
Learning to spot these smells early is one of the key skills that separates a beginner from a seasoned developer.
What Is a Code Smell?
A code smell is any sign that suggests a refactor might be needed.
Martin Fowler (author of Refactoring) called them “surface indicators” of deeper design problems.
They don’t always mean something is broken — they just tell you: “Maybe it could be cleaner.”
The goal isn’t perfection. It’s to make your codebase easy to understand and easy to change.
Common Code Smells (with Simple PHP Examples)
a) Long Function
“If your function doesn’t fit on one screen, it’s probably doing too much.”
A function should do only one thing and do it well.
When it handles multiple responsibilities (validation, calculation, saving, communication), it can become difficult to maintain and test.
Moreover, as the function grows, it becomes harder for developers to understand its intent and structure at a glance.
Before:
function processOrder($order) {
// Validate order
if (empty($order['customer']) || empty($order['items'])) {
throw new Exception('Invalid order');
}
// Calculate total
$total = 0;
foreach ($order['items'] as $item) {
$total += $item['price'] * $item['quantity'];
}
// Apply discount
if (!empty($order['discount'])) {
$total -= $total * $order['discount'];
}
// Save (mock)
echo "Order saved. Total: $total\n";
// Send email (mock)
echo "Email sent to customer.\n";
}
Smell:
One function handles validation, calculation, saving, and communication — too many responsibilities.
After:
function processOrder($order) {
validateOrder($order);
$total = calculateTotal($order);
saveOrder($order, $total);
notifyCustomer($order);
}
function validateOrder($order) { /* ... */ }
function calculateTotal($order) { /* ... */ }
function saveOrder($order, $total) { /* ... */ }
function notifyCustomer($order) { /* ... */ }
Explanation:
Now, each function has a single responsibility. This makes the code cleaner, easier to read, and easier to test.
b) Duplicated Code
“If you copy and paste something once, you’ll paste it again.”
Code duplication is a form of technical debt. If you copy-paste code, you increase the chances of errors and maintenance difficulties.
Any logic that gets repeated should be moved into a separate method or function.
Before:
$price = $product['price'] * $quantity;
$tax = $price * 0.19;
$total = $price + $tax;
$fee = $service['fee'] * $hours;
$tax = $fee * 0.19;
$final = $fee + $tax;
Smell:
Same calculation logic repeated. If tax changes, you must fix it everywhere.
After:
function addTax($amount, $rate = 0.19) {
return $amount * (1 + $rate);
}
$total = addTax($product['price'] * $quantity);
$final = addTax($service['fee'] * $hours);
Explanation:
By creating a reusable function (addTax), we eliminate duplication. Now, if we need to change how the tax is calculated, we only need to modify it in one place.
c) Too Many Conditionals
“When you have a forest of if and switch, consider a better structure.”
Having too many if or switch statements in your code can make it difficult to maintain.
It can also signal that your code is trying to handle too many cases in one place. A better approach is to use polymorphism or strategy patterns.
Before:
function getDiscount($role) {
if ($role === 'admin') {
return 0.3;
} elseif ($role === 'member') {
return 0.1;
} else {
return 0.0;
}
}
Smell:
Hard to extend — if you add a new role, you must edit this function.
After
(using simple OOP idea):
interface DiscountStrategy {
public function getDiscount();
}
class AdminDiscount implements DiscountStrategy {
public function getDiscount() { return 0.3; }
}
class MemberDiscount implements DiscountStrategy {
public function getDiscount() { return 0.1; }
}
class GuestDiscount implements DiscountStrategy {
public function getDiscount() { return 0.0; }
}
function getDiscountForRole($role) {
$strategies = [
'admin' => new AdminDiscount(),
'member' => new MemberDiscount(),
'guest' => new GuestDiscount(),
];
return ($strategies[$role] ?? new GuestDiscount())->getDiscount();
}
Explanation:
Using polymorphism makes it much easier to add new discount types without modifying existing code.
This design follows the Open/Closed Principle — the code is open for extension but closed for modification.
d) Large Class (“God Object”)
“When one class tries to do everything, it ends up doing nothing well.”
Classes that try to do too many things are harder to understand and maintain.
You should break up classes into smaller, more focused units that each have one responsibility.
Before:
class UserManager {
public function registerUser() {}
public function sendEmail() {}
public function resetPassword() {}
public function logUserActivity() {}
public function deleteAccount() {}
}
Smell:
Too many unrelated responsibilities in one place.
After:
class UserRegistration {}
class UserMailer {}
class UserLogger {}
class AccountDeleter {}
Explanation:
Each class now has a single, well-defined responsibility, making the code more modular and easier to test.
e) Primitive Obsession
“When you keep passing around strings and arrays instead of objects.”
Primitive obsession occurs when you use basic data types like strings, integers, or arrays to represent complex concepts.
This can lead to unclear code and make it harder to extend or maintain. Instead, you should create custom types or objects to encapsulate these concepts.
Before:
function createUser($name, $email, $street, $city, $zip) { ... }
Smell:
Too many primitive parameters.
After:
class Address {
public function __construct(public $street, public $city, public $zip) {}
}
function createUser($name, $email, Address $address) { ... }
Explanation:
By creating an Address class, we can group related information together. This improves readability and makes it easier to modify the address structure later if needed.
4. How to Train Your Nose
Like coffee tasting, detecting code smells takes practice.
Here are a few ways to sharpen your sense:
Read old code — especially your own. You’ll quickly see where you overcomplicated things.
Refactor in small steps. Don’t aim for perfect; aim for better.
Ask “Why?” If a piece of code looks weird, ask why it exists — the answer often reveals the smell.
5. The Real Goal: Cleaner, Safer, Happier Code
Refactoring isn’t about rewriting everything.
It’s about making tomorrow’s work easier.
Every smell you clean up today saves you time and frustration in the future.
That’s how healthy, maintainable software grows — one small cleanup at a time.
6. Coming Next: “Simple Refactoring Techniques You Can Apply Today”
In the next article, we’ll take these smells and fix them step by step —
starting with easy refactors like Extract Function, Rename Variable, and Replace Conditional with Polymorphism.
You’ll see how a few small changes can make your PHP code cleaner, safer, and a lot more enjoyable to work with.
Top comments (0)