En el mundo del desarrollo web moderno, crear aplicaciones escalables y mantenibles es un objetivo primordial. Un enfoque que ha ganado una popularidad significativa es el uso de servicios en capas, un patrón de diseño que proporciona estructura, separación de preocupaciones y una base para la extensibilidad. En esta publicación de blog, profundizaremos en el concepto de servicios en capas y exploraremos cómo se pueden implementar de manera efectiva en PHP y el framework Laravel.
Comprensión de los servicios en capas:
Los servicios por capas, a menudo denominados capas de servicios o clases de servicios, son un concepto arquitectónico que divide una aplicación en capas discretas, cada una con una responsabilidad específica. Este patrón arquitectónico fomenta la separación de preocupaciones, lo que facilita la administración y escalamiento de su aplicación a medida que crece.
Estas son las capas comunes en una arquitectura de servicio en capas típica:
- Capa de presentación: esta capa maneja interfaces de usuario, como páginas web o API. En Laravel, esto incluiría controladores y rutas.
- Capa de servicio: la capa de servicio es donde reside la lógica empresarial. Abstrae la funcionalidad principal de la aplicación, haciéndola reutilizable en diferentes partes del sistema.
- Capa de acceso a datos: Responsable de las interacciones con la base de datos y el almacenamiento de datos. En Laravel, esto suele estar representado por modelos Eloquent y migraciones de bases de datos.
Beneficios de los servicios en capas:
- Reutilización de código: al encapsular su lógica empresarial en clases de servicios, puede reutilizar los mismos servicios en varias partes de su aplicación, lo que reduce la duplicación de código.
- Capacidad de prueba: cada capa se puede probar de forma aislada, lo que facilita la escritura de pruebas unitarias. Puede burlarse de las dependencias en la capa de servicio, simplificando el proceso de prueba.
- Mantenimiento: los servicios en capas proporcionan una estructura clara, lo que facilita a los desarrolladores mantener y comprender el código base. Los cambios en una capa suelen tener un impacto mínimo en las demás.
Implementación de servicios en capas en Laravel:
Echemos un vistazo más de cerca a cómo implementar servicios en capas en una aplicación Laravel.
- Clases de servicio: cree clases de servicio dedicadas para diferentes operaciones comerciales. Por ejemplo, si tiene una plataforma de comercio electrónico, es posible que tenga servicios para gestión de carritos, procesamiento de pedidos y autenticación de usuarios.
- Inyección de dependencia: utilice la inyección de dependencia incorporada de Laravel para inyectar estos servicios en sus controladores u otras clases de servicios.
Mantenga los controladores delgados: los controladores deben ser los principales responsables de manejar las solicitudes HTTP, validar las entradas y devolver las respuestas. Delegue la lógica empresarial real a sus clases de servicios. - Proveedores de servicios: utilice proveedores de servicios para vincular sus clases de servicios al contenedor de servicios de la aplicación, haciéndolos accesibles en toda su aplicación.
- Patrón de repositorio: implemente el patrón de repositorio para separar la lógica de acceso a datos de la capa de servicio, lo que permite cambios más fáciles en sus métodos de almacenamiento de datos.
Ejemplo
En primer lugar debemos describir el contrato (Interfaz) para UserService:
Ubicación: 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;
}
Ubicación: 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();
}
}
Ahora debemos registrar un nuevo servicio, para ello necesitamos crear un archivo ServiceProvider para nuestros servicios y describir el enlace para UserService:
Ubicación: 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
);
}
}
Agregue el nuevo ServiceServiceProvider en config/app.php
…
'providers' => [
/*
* Laravel Framework Service Providers...
*/
App\Providers\ServiceServiceProvider::class,
...
],
…
Entonces, ahora podemos usar nuestro Servicio de Usuario y reutilizar la lógica de creación de usuarios en todas partes: en un Controlador, por ejemplo.
Usaremos validación para nuestra consulta, es decir, debemos crear una clase Request:
Ubicación: 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
];
}
}
Y directamente podemos inyectar la interfaz en el controlador, eso es posible porque el registro vinculante en ServiceProvider ayuda a eso.
Ubicación: 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);
}
}
Conclusión:
Los servicios en capas proporcionan una forma estructurada y eficiente de gestionar la complejidad de las aplicaciones PHP y Laravel modernas. Al separar las preocupaciones y organizar su código base, puede lograr mantenibilidad, escalabilidad y capacidad de prueba mientras crea aplicaciones web sólidas. Ya sea que esté iniciando un nuevo proyecto o refactorizando uno existente, adoptar este patrón arquitectónico puede generar una experiencia de desarrollo más manejable y agradable.
P.D: the image owner/authot is: Services in Laravel