Convert duplicate controller code to clean code in Laravel
Content Index
- Understanding when it's time to refactor code in Laravel
- Signs that your code needs cleaning
- Risks of not refactoring on time
- My experience refactoring payment controllers in Laravel
- The problem: two almost identical controllers
- The solution: a BasePaymentController to centralize the logic
- Why I didn't use helpers (and what I learned from it)
- Inheritance pattern and abstract classes to avoid duplication
- How to design a reusable base controller
- Implementing an abstract class AbstractProduct
- Typing and validations: avoiding errors when passing entities
- Good practices for refactoring code in Laravel
- Applying the DRY and SOLID principles
- Separation of responsibilities with Service Layer and Repositories
- Complete step-by-step refactoring example
- Benefits of refactored and clean code
- Scalability and maintenance
- Error reduction and flow clarity
- Preparing the foundation for testing and new features
- Common mistakes when refactoring in Laravel (and how to avoid them)
- Overloading the base controller
- Not taking advantage of typing
- Using helpers instead of classes
- Conclusion: refactoring is not redoing, it's evolving your code
- Frequently Asked Questions
Refactoring code in Laravel doesn't mean rewriting everything from scratch, but rather **improving its structure, clarity, and reusability**. Above all, the topic of modularization is key in any application and is the foundation for our application to be scalable.
Understanding when it's time to refactor code in Laravel
Signs that your code needs cleaning
There are several symptoms that indicate the code in Laravel needs a refactoring:
- Controllers with overly long or repeated methods.
- Business logic mixed with presentation.
- Difficulty adding new entities without duplicating code.
- Tests that become fragile or hard to maintain.
If your application shows any of these signs, the code is likely asking for a reorganization.
Risks of not refactoring on time
Postponing a refactoring can be costly. Maintenance becomes complicated, errors increase, and every new feature requires more effort. Laravel offers a flexible structure, but without architectural discipline, it can lead to a chaos of misused controllers and helpers.
My experience refactoring payment controllers in Laravel
The problem: two almost identical controllers
For this example, we have the typical scenario where we have two practically identical controllers, one for processing product payments and one for books. Both shared the same flow; the only thing that changed was the payment registration:
$payment = Payment::create([
'paymentable_id' => $product->id,
'paymentable_type' => Product::class,
***
]);
The solution: a BasePaymentController
to centralize the logic
I decided to extract the common logic to a **base controller**. I created a BasePaymentController
with all the shared steps of the payment process, and let BookPaymentController
and ProductPaymentController
inherit from it. This way, each child controller only defined what was specific to its entity, while the general logic remained in one place.
Why I didn't use helpers (and what I learned from it)
I considered creating a helper to reuse the code, but I discarded it. In my case, the logic involved handling requests
and responses
, which belongs to the controller layer, not to global functions. This was a key point: **using helpers for business logic can generate coupling and loss of context.**
Inheritance pattern and abstract classes to avoid duplication
How to design a reusable base controller
The BasePaymentController
became the heart of the payment system. It defines generic methods for validating the request, processing the order, and returning the corresponding view. The child controllers only indicate which entity to use; for this, we use the signature of each method along with Laravel's dependency injection to get an instance of each object.
Implementing an abstract class AbstractProduct
Furthermore, I defined an AbstractProduct
class to ensure that only “purchasable” entities were passed to the controller. The concrete models (Book
, Product
, etc.) inherit from this class and guarantee typed compatibility.
Typing and validations: avoiding errors when passing entities
The use of abstract classes and strong typing reduces the risk of passing incorrect objects. If someone tries to process an invalid entity, PHP and Laravel detect it before executing the flow, improving robustness.
Good practices for refactoring code in Laravel
Applying the DRY and SOLID principles
The **DRY (Don’t Repeat Yourself)** principle was the basis of this refactoring. Instead of repeating the same logic in multiple controllers, I centralized everything in a single place. Additionally, I applied the SOLID principles to separate responsibilities and maintain independence between layers.
Separation of responsibilities with Service Layer and Repositories
In some cases, you can move the payment logic to a dedicated **service** or **repository**. This keeps controllers thin and facilitates testing. The idea is that the controller only orchestrates, it doesn't process.
Complete step-by-step refactoring example
- Identify duplicated code between controllers.
- Create a base controller with the common logic.
- Define abstract methods for the specific behaviors.
- Have the concrete controllers inherit from the base one.
- Reinforce the structure with abstract classes and typing.
- Test the refactored flows.
Benefits of refactored and clean code
Scalability and maintenance
Adding a new purchasable entity (for example, “Shoe”) became a matter of minutes. I only had to create a new model that inherits from AbstractProduct
, without touching the payment logic, understanding that this new entity is a purchasable product like the products and books shown previously.
Error reduction and flow clarity
The payment flow is now clearer and more predictable. Each class has a unique function, which makes it easier to understand and modify the code.
Preparing the foundation for testing and new features
The modular structure allows each part of the flow to be tested separately. By having well-defined methods, unit tests become simple and reliable.
Common mistakes when refactoring in Laravel (and how to avoid them)
Overloading the base controller
Don't turn the BaseController
into a “god controller”. Keep it generic, but lightweight. If it starts to grow too much, extract parts to services.
Not taking advantage of typing
Laravel allows combining modern PHP with strong typing. Ignoring this possibility can nullify the benefits of refactoring.
Using helpers instead of classes
Helpers are useful for very specific functions, but overusing them leads to disorder. Prefer base controllers or services to maintain cohesion.
Conclusion: refactoring is not redoing, it's evolving your code
Refactoring code in Laravel is a continuous process that improves the health of your application. It's not about writing more code, but about **writing it better**. In my case, moving from several duplicated controllers to a centralized flow with inheritance completely changed the project's maintainability.
Refactoring doesn't break your application; it strengthens it. It allows you to grow with confidence, add new entities, and maintain clean, predictable, and scalable code.
Frequently Asked Questions
When is it better to create a Service than to inherit from a base controller?
When the logic stops belonging to the HTTP flow and becomes a business operation (for example, processing payments, generating reports, or sending notifications).
How to test refactored code in Laravel?
Use unit tests for the base controller methods and functional tests to verify the complete flow. PHPUnit and Pest work perfectly for this.
What benefits are there to using abstract classes in Laravel?
They allow for defining clear contracts between entities, ensuring that each one implements the necessary methods without duplicating code.
I agree to receive announcements of interest about this Blog.
I'll show you a demo of how we can convert duplicate code in Laravel corresponding to a controller to reuse it through inheritance.