TailTemplate Build stunning websites faster with our pre-designed Tailwind CSS templates

Benefits of using application service classes

In today's tutorial, we share an architect pattern called service class.

Definition of a service class

Writing clean and reusable code is not easy, we learn different patterns to achieve that. The MVC(Model-view-controller) pattern is probably the first architect pattern you got to know.

Stage 1: fat controller

It is very convenient to write all the business logic inside the controller, that seems like the natural choice when we first using MVC. At this stage, our controllers are very big and it consists of most of our domain business.

class OrdersController {
 
    function purchase($ticketId)
    {
        $ticket = Ticket::find($ticketId);
 
        if ($ticket->get('inventory') < 0 ) {
            throw new LogicException('ticket is sold out');
        }
 
        $userId = $this->Auth->user('id');
 
        $order = Order::save([
            'user_id'=>$userId ,
            'ticket_id'=>$ticketId
        ]);
 
        $this->redirect($order->get('id'));
    }
}

Stage 2: fat model

At this stage, we learn about TDD (test-driven development), it is absolutely the way to go. TDD does not only help us think before coding but also improves the overall project quality because we can now do refactoring!

However, it is pain in the ass to write unit tests for controllers. Because controller tests are basic end-to-end tests, we need to prepare data seeds, send HTTP requests and assert against the database results. These unit tests are time-consuming to write and slow to run.

Therefore, we move our business logic to model and start writing fat models.

class OrdersController {
 
    function purchase($ticketId)
    {
        $userId = $this->Auth->user('id');
         
        $order = OrderModel::purchase($userId, $ticketId);
         
        $this->redirect($order->get('id'));
    }
}
 
class OrderModel
{
    public function purchase($userId, $ticketId)
    {
        $ticket = Ticket::find($ticketId);
 
        if ($ticket->get('inventory') < 0 ) {
            throw new LogicException('ticket is sold out');
        }
 
        return Order::save([
            'user_id'=>$userId ,
            'ticket_id'=>$ticketId
        ]);
    }
}

Stage 3: service class

When the business logic only involves one model, it is easy to decide where to place the code. When it involves multiple models, the struggle begins.

For instance, a user purchases a ticket. Shall we place the fat logic inside the user model, the ticket model or the order model? Whenever this happens, we tend to struggle with the best solution.

This is when a service class comes to the play. Service class fits best to how we model the process:

class OrdersController {
 
    function purchase($ticketId)
    {
        $userId = $this->Auth->user('id');
         
        $order = UserPurchaseService::purchase($userId, $ticketId);
         
        $this->redirect($order->get('id'));
    }
}
 
 
class UserPurchaseService
{
    public function purchase($userId, $ticketId)
    {
        $ticket = Ticket::find($ticketId);
 
        if ($ticket->get('inventory') < 0 ) {
            throw new LogicException('ticket is sold out');
        }
 
        return Order::save([
            'user_id'=>$userId ,
            'ticket_id'=>$ticketId
        ]);
    }
}

Why use a service class

So we have seen the benefits of using a service class:

  • Easy to test similar to the fat model
  • Easy to model the business logic