Back to Blog
LaravelPHPJune 21, 20264 min read

Laravel refactoring: Move business logic into action classes

Laravel refactoring into action classes helps you shrink fat controllers. Learn to build a clean service layer that makes your PHP code easier to test.

LaravelPHPRefactoringClean CodeArchitectureTutorial
Close-up of AI-assisted coding with menu options for debugging and problem-solving.

I remember staring at a UserController@store method that was nearly 150 lines long. It was handling validation, database inserts, sending welcome emails, and even triggering a Slack notification. It was a nightmare to debug, and adding a simple feature usually meant breaking something else.

If your controllers are doing more than receiving a request and returning a response, you’re likely dealing with "fat controller" syndrome. Let's look at how Laravel refactoring using action classes can bring sanity back to your codebase.

Why Controllers Get Fat

Early in my career, I thought putting logic in controllers was just "how Laravel worked." After all, the framework makes it so easy to write everything inside a single method. But as soon as you need that same registration logic in a CLI command or a background job, you’re stuck. You end up copy-pasting code or—worse—manually triggering controller methods, which is a massive anti-pattern.

When you start designing a clean service layer in Laravel without over-abstraction, you realize that the controller's only job is to be an entry point. It should validate the request, call the right service or action, and return a response.

Moving to Action Classes

Group of karate students practicing high kicks outdoors in Buenos Aires.

An "Action" class is essentially a single-purpose class that performs one specific business process. I prefer the "Single Action" pattern, where each class has an __invoke method. It keeps the syntax clean and makes dependency injection a breeze.

Here is what a typical fat controller looks like before we touch it:

PHP
public function store(Request $request)
{
    $validated = $request->validate([...]);
    $user = User::create($validated);
    #6A9955">// ... 50 lines of logic ...
    Mail::to($user)->send(new WelcomeEmail($user));
    return response()->json($user, 201);
}

To clean this up, we create an action class: app/Actions/CreateNewUser.php.

PHP
namespace App\Actions;

use App\Models\User;

class CreateNewUser
{
    public function __invoke(array $data): User
    {
        $user = User::create($data);
        #6A9955">// Business logic goes here
        return $user;
    }
}

Now, your controller becomes remarkably slim:

PHP
public function store(Request $request, CreateNewUser $createUser)
{
    $user = $createUser($request->validated());
    return response()->json($user, 201);
}

The Benefits of Clean Code PHP

This approach isn't just about aesthetics. By pulling logic into action classes, you gain several advantages:

  1. Testability: You can write a dedicated test for CreateNewUser without needing to mock an entire Request object or hit your routing layer.
  2. Reusability: Need to create a user from an Artisan command? Just inject CreateNewUser into your command's constructor.
  3. Readability: Your controller now acts as a table of contents for your route, making it clear exactly what happens when a user hits that endpoint.

I’ve found that this Laravel service pattern (or action pattern) works best when you keep the classes focused. If your action class starts requiring five or six dependencies, it’s a sign that you might need to break it down further or use Laravel events and listeners to decouple the side effects, like sending emails.

Avoiding Over-Engineering

I once spent about two days building a complex interface-based service layer, only to realize I was making the code harder to navigate. Don't fall into the trap of abstracting everything behind interfaces just because you think it’s "professional."

If you're just starting, keep it simple. If you find yourself needing to swap implementations, then introduce an interface. Until then, a plain PHP class is perfectly fine.

When you combine these actions with mastering Laravel traits for cleaner Eloquent models, you’ll find that your models stay lean, your controllers stay focused, and your business logic lives in a predictable, easy-to-find location.

FAQ

Q: Should I use Service Classes or Action Classes? A: They are similar. I prefer Action classes for single-purpose operations (like "ProcessPayment") and Service classes for grouping related business logic (like "PaymentGatewayService"). Use what feels natural for your team.

Q: Where should I put these files? A: I usually create an app/Actions or app/Services directory. Just make sure to register them in your PSR-4 autoloading in composer.json if you're using a custom namespace.

Q: Does this hurt performance? A: The overhead of an extra class instantiation is roughly 0.001ms in modern PHP. You won't notice it. The maintenance cost of a messy controller, however, is massive.

Final Thoughts

Colorful confetti scattered over the word 'Finally' symbolizing celebration or achievement.

Refactoring is an ongoing process. You don't have to rewrite your entire application in an afternoon. Start by picking one bloated controller and moving its logic into an action class. You’ll feel the difference immediately when you go to write the tests.

I’m still experimenting with how to best handle complex transactions within these actions—sometimes I lean on the Transactional Outbox pattern in Laravel to keep things consistent—but the core principle remains: keep your code decoupled and keep your controllers thin.

Similar Posts