Language:

Search

Service Layer in Laravel and PHP

  • Share this:
Service Layer in Laravel and PHP

In the world of modern web development, building scalable and maintainable applications is a paramount goal. One approach that has gained significant popularity is the use of layered services, a design pattern that provides structure, separation of concerns, and a foundation for extensibility. In this blog post, we'll dive into the concept of layered services and explore how they can be effectively implemented in PHP and the Laravel framework.

Understanding Layered Services:

Layered services, often referred to as service layers or service classes, are an architectural concept that divides an application into discrete layers, each with a specific responsibility. This architectural pattern encourages a separation of concerns, making it easier to manage and scale your application as it grows.

Here are the common layers in a typical layered service architecture:

  1. Presentation Layer: This layer handles user interfaces, such as web pages or APIs. In Laravel, this would include controllers and routes.
  2. Service Layer: The service layer is where the business logic resides. It abstracts the core functionality of the application, making it reusable across different parts of the system.
  3. Data Access Layer: Responsible for database interactions and data storage. In Laravel, this is often represented by Eloquent models and database migrations.
     

Benefits of Layered Services:

  1. Code Reusability: By encapsulating your business logic in service classes, you can reuse the same services in various parts of your application, reducing code duplication.
  2. Testability: Each layer can be tested in isolation, making it easier to write unit tests. You can mock the dependencies in the service layer, simplifying the testing process.
  3. Maintenance: Layered services provide a clear structure, making it easier for developers to maintain and understand the codebase. Changes in one layer typically have minimal impact on the others.

Implementing Layered Services in Laravel:

Let's take a closer look at how to implement layered services in a Laravel application.

  1. Service Classes: Create dedicated service classes for different business operations. For example, if you have an e-commerce platform, you might have services for cart management, order processing, and user authentication.
  2. Dependency Injection: Utilize Laravel's built-in dependency injection to inject these services into your controllers or other service classes.
  3. Keep Controllers Thin: Controllers should primarily be responsible for handling HTTP requests, validating input, and returning responses. Delegate the actual business logic to your service classes.
  4. Service Providers: Use service providers to bind your service classes into the application's service container, making them accessible throughout your application.
  5. Repository Pattern: Implement the repository pattern to separate the data access logic from the service layer, allowing for easier changes in your data storage methods.

Example

First of all we should describe contract (Interface) for UserService:

Location: app/Contracts/Services/UserService/UserServiceInterface.php 

<?php

declare(strict_types=1);

namespace App\Contracts\Services\UserService;

use App\Models\User;
use App\ValueObjects\Phone;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use LogicException;

interface UserServiceInterface
{
    /**
     * @param Phone $phone
     * @return User
     * @throws ModelNotFoundException
     */
    public function findByPhone(Phone $phone): User;

    /**
     * @throws LogicException
     */
    public function create(string $name, Phone $phone): User;

    /**
     * @param int $id
     * @return void
     * @throws ModelNotFoundException
     */
    public function deleteById(int $id): void;
}


Now we can write a class called UserService.php

Location: app/Services/UserService/UserService.php 

<?php

declare(strict_types=1);

namespace App\Services\UserService;

use App\Contracts\Services\UserService\UserServiceInterface;
use App\Models\User;
use App\ValueObjects\Phone;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use LogicException;

class UserService implements UserServiceInterface
{
    public function __construct(private readonly User $user)
    {
    }

    /**
     * @param int $userId
     * @return User
     * @throws ModelNotFoundException
     */
    public function findById(int $userId): User
    {
        //TODO: I prefer to use the Repository pattern for this, but that's a topic for a separate article
        /** @noinspection PhpIncompatibleReturnTypeInspection */
        return $this->user->newQuery()
            ->findOrFail($userId);
    }

    /**
     * @param Phone $phone
     * @return User
     * @throws ModelNotFoundException
     */
    public function findByPhone(Phone $phone): User
    {
        //TODO: I prefer to use the Repository pattern for this, but that's a topic for a separate article
        /** @noinspection PhpIncompatibleReturnTypeInspection */
        return $this->user->newQuery()
            ->where('phone', '=', $phone->toString())
            ->firstOrFail();
    }

    /**
     * @throws LogicException
     */
    public function create(string $name, Phone $phone): User
    {
        try {
            $this->findByPhone($phone);
            throw new LogicException('User with this phone already exists!');//I recommend to use separate Exception for specific case
        } catch (ModelNotFoundException $e) {
        }

        $user = new User();

        $user->name = $name;
        $user->phone = $phone;
        $user->save();

        //Some mandatory logic when creating a user

        return $user;
    }

    /**
     * @param int $id
     * @return void
     * @throws ModelNotFoundException
     */
    public function deleteById(int $id): void
    {
        //TODO: I prefer to use the Repository pattern for this, but that's a topic for a separate article
        $user = $this->findById($id);
        $user->delete();
    }
}

Now we should register new service, for it we need create ServiceProvider file for our services and describe binding for UserService:

Location: app/Providers/ServiceServiceProvider.php 

<?php

declare(strict_types=1);

namespace App\Providers;

use App\Contracts\Services\UserService\UserServiceInterface;
use App\Services\UserService\UserService;
use Illuminate\Support\ServiceProvider;

class ServiceServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     */
    public function register(): void
    {
        //
    }

    /**
     * Bootstrap services.
     */
    public function boot(): void
    {
        $this->app->bind(
            UserServiceInterface::class,
            UserService::class
        );
    }
}

Add the new ServiceServiceProvider in config/app.php

…
'providers' => [
    /*
     * Laravel Framework Service Providers...
     */
    App\Providers\ServiceServiceProvider::class,
    ...
],
…

So, now we can use our UserService and reuse user creation logic everywhere: In a Controller, for example.

We will use validation for our query, that is, we must create a Request class:

app/Http/Requests/Rest/User/CreateRequest.php 

<?php

declare(strict_types=1);

namespace App\Http\Requests\Rest\User;

use Illuminate\Contracts\Validation\Rule;
use Illuminate\Foundation\Http\FormRequest;

class CreateRequest extends FormRequest
{
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, Rule|array|string>
     */
    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'phone' => ['required', 'string'],//We can create a separate rule for phone validation, but that will be in the next article
        ];
    }
}

And directly we can inject the interface in the controller, that its possible because the binding register in the ServiceProvider help that

app/Http/Controllers/Rest/User/CreateController.php 

<?php

declare(strict_types=1);

namespace App\Http\Controllers\Rest\User;

use App\Http\Controllers\Controller;
use App\Http\Requests\Rest\User\CreateRequest;
use App\Contracts\Services\UserService\UserServiceInterface;
use App\ValueObjects\Phone;
use DateTimeInterface;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;

final class CreateController extends Controller
{
    public function __construct(private readonly UserServiceInterface $userService)
    {
    }

    public function __invoke(CreateRequest $request): JsonResponse
    {
        $name = $request->get('name');
        $phone = Phone::fromString($request->get('phone'));

        $user = $this->userService->create($name, $phone);

        return new JsonResponse([
            'id' => $user->id,
            'name' => $user->name,
            'phone' => $user->phone->toString(),
            'created_at' => $user->created_at->format(DateTimeInterface::ATOM),
        ], Response::HTTP_CREATED);
    }
}


 Conclusion:

Layered services provide a structured and efficient way to manage the complexity of modern PHP and Laravel applications. By separating concerns and organizing your codebase, you can achieve maintainability, scalability, and testability while building robust web applications. Whether you're starting a new project or refactoring an existing one, embracing this architectural pattern can lead to a more manageable and enjoyable development experience.


P.D: the image owner/authot is: Service Layer in Laravel

Tags:
Carlos Santiago

Carlos Santiago

Laravel Developer