A Comprehensive Guide To Service Classes In PHP

Last updated 20th October, 2023

Why Do We Use Service Classes?

Service classes are an excellent tool in the developer skillset for building more maintainble, reliable code. It's a tried and tested pattern that builds SOLID applications. There are three main benefits:

Separation of Concerns

We can use service classes to remove business logic from controllers. Meaning that our controllers can stay lean and only deal with HTTP related operations.

Reusability

Extracting business logic from controllers means that it can be reused in other contexts. Your controllers won't be coupled to application logic.

Testing

When code is isolated in service classes, it significantly simplifies the process of writing tests for the logic within them.

Creating A Service Class

A service class is a PHP class that provides a specific service or functionality to other parts of your application. It encapsulates the logic required to perform a specific task, making it easier to reuse and maintain the code.

Defining A Service Interface

The first step is to define the interface for your service class. This should include all of the methods that your service will expose to the rest of the application.

interface ProductServiceInterface
{
    public function get(int $id): Product;
}

Implementing The Interface

Now we have a basic definition of what functions our service class will perform with the interface, we can implement it with a class.

class ProductService implements ProductServiceInterface
{
		public function __construct(
		    private readonly ProductRepository $repository
		)
    {
        //
    }

    public function get(int $id): Product
    {
        return $this->repository->findById($id);
    }
}

We've now implemented the ProductService class, which takes a ProductRepository dependency in its constructor and uses it to fetch the product from the database.

Incorporating It Into Your Code

Let's now work on introducing our service class into a real world scenario. Let's say we have two controllers that need to fetch a product by its ID. Instead of duplicating the code to fetch the product in each controller, we can simply instantiate our ProductService class and call its get method:

class ProductController
{
    public function __construct(
        protected readonly ProductServiceInterface $service
    )
    {
        //
    }

    public function show(int $id)
    {
        $product = $this->service->get($id);

        // Respond to the user's request with the resolved
        // product...
    }
}

You might also use the same service to fetch a product when creating an order for a given product:

class OrderController
{
    public function __construct(
        protected readonly ProductServiceInterface $productService
    )
    {
        //
    }

    public function create(int $productId)
    {
        $product = $this->productService->get($productId);
			
        Order::query()->create([
            'product_id' => $product->id,
        ]);
    }
}

In this example, we've injected our ProductService class into both the ProductController and OrderController classes. This allows us to reuse the logic for fetching a product by its ID in both controllers without duplicating any code.

Advanced Concepts

Exception Handling in Service Classes

It's always important to handle exceptions properly. By throwing an exception, we can signal that something has gone wrong and gracefully handle the error.

Another added benefit of using service classes here is we can wrap up any bulky exception handling code in the service. This means we don't clutter up controllers or any other code with added bloat.

In our service classes, we can also use custom exceptions to provide a better developer experience. This can help make it clear what has gone wrong in the service execution.

class ProductService implements ProductServiceInterface
{
    // ...

    public function get(int $id): Product
    {
        $product = $this->repository->findById($id);
			
        if (! $product) {
            throw ProductServiceException::productNotFound($id);
        }
			
        return $product;
    }
}

In the example above, we fail to fetch a product from our ProductService above because it cannot be found. Instead of throwing a generic error, we can create a custom exception to handle that case. This makes it abundantly clear to the developer what's gone wrong, and how to solve for the problem.

Using Service Classes with APIs

Service classes can be a powerful tool when working with APIs. By encapsulating API calls in a service class, we can simplify our code and make it more modular. Additionally, by abstracting away the details of the API, we can make our code more resilient to changes in the API.

When working with APIs, it's important to handle errors and unexpected situations properly. Most APIs will return error codes or error messages when something goes wrong. Like we went through above in exception handling, you can provide a great DX by creating custom exception handlers to make errors easier to handle.

One common pattern when working with APIs is to use a caching layer. By caching API responses, we can reduce the number of API calls we need to make and improve the performance of our application. In our service classes, we can implement caching by checking if a response has already been cached and returning the cached response if it has.

Overall, by leveraging service classes when working with APIs, we can make our code more modular, resilient, and performant.

Best Practices for Using Service Classes

When using service classes in PHP, there are several best practices that can help you write cleaner, more maintainable code. Here are a few tips to keep in mind:

Keep Services Focused

Each service class should have a single responsibility, and should not try to do too much. This makes it easier to understand and modify the code, and reduces the risk of introducing bugs. If a service class becomes too complex, consider breaking it down into smaller, more focused classes.

Use Dependency Injection

Dependency injection is a technique for passing dependencies into a class rather than creating them inside the class. This makes it easier to test and maintain the code, and allows for greater flexibility in how the code is used. When using service classes, consider using dependency injection to pass in any dependencies they require.

Use Interfaces

Interfaces provide a way to define a contract for a class, specifying what methods it must implement. When using service classes, consider defining interfaces for them, and using these interfaces in other parts of your code. This makes it easier to swap out implementations of the service class, and makes the code more flexible and maintainable.

Use Exceptions

Exceptions provide a way to handle errors and unexpected situations in your code. When using service classes, consider using exceptions to handle errors that might occur during their execution. This makes it easier to handle errors in a consistent way, and helps to ensure that your code is more robust and reliable.

Conclusion

In conclusion, leveraging service classes in PHP can greatly improve the organization and maintainability of your code. By separating business logic from your controllers and models, you can create more modular and reusable code.

Remember that while service classes can be a powerful tool, they are not always necessary. Use your best judgement when deciding whether to implement them in your application.

Overall, we hope this guide has provided you with a solid foundation for leveraging service classes in your PHP projects. With practice and experience, you can become more confident and knowledgeable in creating and using service classes effectively.

No comments yet…

DevInTheWild.

Login To Add Comments.

Want More Like This?

Subscribe to occasional updates.

Related Articles