Refactoring Business Logic to Actions in Laravel

Refactoring Business Logic to Actions in Laravel

Laravel is a flexible PHP framework, where we can structure our architecture as per our needs in the project. Based on the project size, we define core components such as Controllers and Models following the MVC architecture. But, for a medium to a large-scale project, considering scalability and maintainability in mind, it is not good to write our business logic, a critical part of our application, in controllers or models. Writing such business logic in a single file like controllers or models, making them a God Object, results in a spaghetti code which makes it harder to cope with the future changes. There are so many ways to resolve this issue by extracting business logic into different single responsible classes in OOP, and the Actions class is the one among them.

Action classes are simple classes without any abstractions (inheritance) or interfaces. It is a class that takes input, does something, and may/may not gives output. That's why an action class usually has only one public method, and sometimes a constructor (for dependency injection). For the convention, we will name this class with the suffix "Action", eg, CreateUserAction. Here are some points to be considered while working on Action classes, though these points are recommended not bind to follow strictly.

  • An action class should have a name that is self-explanatory, such as CreateUserAction.
  • Action classes are responsible for only one action, hence it should have only one public method, usually named as execute() or handle().
  • It should be request and response agnostic because usually HTTP requests and responses are generally responsible to controller classes, and Action classes could be called from controllers as well as other classes like CLI commands, etc.
  • Though it should not be dependable with any external dependencies, it can have its own internal dependencies. It can also have other Actions classes as dependencies.
  • Since action classes follow the "Single Responsibility Principle", they should return only one type of data. In case of errors or other responses, it is best to throw an exception. Also, there should be a single reason to change or update that class in the future.

Let's see an example of implementing an action class. Consider we have a module, which is supposed to save a blog post in the database and then tweet the post's title on Twitter.

The controller would look like this.

class PostController 
{
    public function create(Request $request, TwitterService $twitterService) 
    {
        $post = new Post();
        $post->title = $request->input('title');
        $post->content = $request->input('content');
        $post->is_published = true;
        $post->save();
        $twitterService->tweet($post->title);

        return back()->with(['message' => 'Post has been submitted and published.']);
    }
}

Here, the business logic is defined in the controller's method. As mentioned above, it is not a good idea to write such logic in the controller's method because the same logic can be used in another place (let's say a CLI command) in the future or might need to change this with some other logic.

Let's extract that logic into an Action class.

class CreatePostAction
{
    public function __construct(TwitterService $twitterService)
    {
        $this->twitterService = $twittterService
    }

    public function execute(array $data):Post 
    {
        $post = $this->savePost($data);
        $this->tweet($post->title);

        return $post;
    }

    public function savePost(array $data): Post
    {
        $post = new Post();
        $post->title = $data['title'];
        $post->content = $data['content'];
        $post->is_published = true;
        $post->save();

        return $post;
    }

    public function tweet(string $text): void
    {
        $this->twitterService($text);
    }
}

and refactor controller method as follows.

class PostController 
{
    public function create(Request $request, CreatePostAction $action) 
    {
        $post = $action->execute($request->validated());

        return back()->with(['message' => 'Post has been submitted and published.']);
    }
}

The complete code of implementing the Action class is in the Github repo.

Hope you like the article. We appreciate your comments on this design pattern in Laravel.

References: