Паттерн "Обработчик" (Handler) с использованием DTO и VO
- Изоляция бизнес-логики: Бизнес-логика изолирована в обработчиках, что позволяет сделать код более организованным и легко поддерживаемым.
- Тестируемость: Обработчики легко тестируются отдельно, так как они не зависят от инфраструктурного кода (например, контроллеров).
- Переиспользование: Обработчики могут быть легко переиспользованы в различных частях приложения.
- Ясность и читаемость кода: Использование DTO и VO позволяет четко определить структуру передаваемых данных, что улучшает читаемость и понимание кода.
- Соблюдение принципов SOLID: Обработчики помогают соблюдать принципы единственной ответственности (SRP) и разделения интерфейсов (ISP).
- Иммутабельность VO: Значения VO не изменяются после создания, что помогает избежать непреднамеренных изменений и улучшает предсказуемость кода.
Рассмотрим на примере API маршрута, который создает товар
- Маршрут (Router) Создадим маршрут для обработки POST-запросов на создание нового товара.
<?php
use App\Http\Controllers\ProductController;
use Illuminate\Support\Facades\Route;
Route::post('products', [ProductController::class, 'store'])
->name('products.store');
- Контроллер (Controller) Контроллер принимает запрос от клиента и вызывает соответствующий обработчик.
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Domains\Product\Core\Handlers\CreateProductHandler;
use App\Http\Requests\CreateProductRequest;
use Illuminate\Http\JsonResponse;
class ProductController extends Controller
{
public function store(CreateProductRequest $request, CreateProductHandler $handler): JsonResponse
{
$product = $handler->handle($request->getDto());
return response()->json([
'message' => 'Product created successfully',
'data' => $product
], 201);
}
}
- Реквест (Request) Реквест используется для валидации входящих данных и создания DTO.
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use App\Domains\Product\Core\DTO\CreateProductDTO;
use Illuminate\Foundation\Http\FormRequest;
class CreateProductRequest extends FormRequest
{
public function authorize(): bool
{
return true; // Здесь можно добавить логику авторизации
}
public function rules(): array
{
return [
'product.name' => 'required|string|max:255',
'product.description' => 'required|string',
'product.price' => 'required|integer|min:0',
'tags' => 'array',
'tags.*' => 'integer|exists:tags,id',
'images' => 'array',
'images.*' => 'string',
];
}
public function getDto(): CreateProductDTO
{
return CreateProductDTO::fromArray($this->validated());
}
}
- DTO (Data Transfer Object) DTO используется для передачи данных запроса между слоями приложения.
<?php
declare(strict_types=1);
namespace App\Domains\Product\Core\DTO;
use App\Domains\Product\Core\ValueObjects\ProductVO;
use Illuminate\Support\Arr;
final class CreateProductDTO
{
public function __construct(
public ProductVO $product
public array $tags,
public array $images
) {
}
public static function fromArray(array $data): self
{
return new self(
ProductVO::fromArray(Arr::get($data, 'product')),
Arr::get($data, 'tags', []),
Arr::get($data, 'images', [])
);
}
}
- VO (Value Object) VO инкапсулирует небольшое количество данных.
<?php
declare(strict_types=1);
namespace App\Domains\Product\Core\ValueObjects;
use Illuminate\Support\Arr;
final class ProductVO
{
public function __construct(
public string $name,
public string $description,
public float $price
) {
}
public static function fromArray(array $data): self
{
return new self(
Arr::get($data, 'name'),
Arr::get($data, 'description'),
null !== Arr::get($data, 'price') ? (float) Arr::get($data, 'price') : 0
);
}
}
- Обработчик (Handler) Обработчик содержит логику создания товара.
<?php
declare(strict_types=1);
namespace App\Domains\Product\Core\Handlers;
use App\Domains\Product\Core\DTO\CreateProductDTO;
use App\Models\Product;
use Illuminate\Support\Facades\DB;
final class CreateProductHandler
{
public function handle(CreateProductDTO $dto): Product
{
return DB::transaction(function () use ($dto) {
// Создание товара
$product = new Product();
$product->name = $dto->product->name;
$product->description = $dto->product->description;
$product->price = $dto->product->price;
$product->save();
// Привязка тегов
$product->tags()->attach($dto->tags);
// Сохранение изображений
foreach ($dto->images as $image) {
$product->images()->create(['path' => $image]);
}
return $product;
});
}
}
Пошаговое объяснение
- Маршрут (Router): В файле маршрутов Laravel определяем маршрут для POST-запросов на создание нового товара. Этот маршрут связывается с методом store контроллера ProductController.
- Контроллер (Controller): Контроллер ProductController принимает HTTP-запрос и вызывает метод getDto() реквеста CreateProductRequest для получения DTO. Затем он передает DTO обработчику CreateProductHandler для создания товара и возвращает JSON-ответ с данными о созданном товаре.
- Реквест (Request): CreateProductRequest используется для валидации входящих данных. Метод getDto() создает DTO из валидированных данных и возвращает его.
- DTO (Data Transfer Object): CreateProductDTO используется для передачи данных запроса. Он инкапсулирует объект ProductVO, массив тегов и массив изображений.
- VO (Value Object): ProductVO инкапсулирует данные о товаре (название, описание и цену). Он используется для структурирования данных внутри DTO.
- Обработчик (Handler): CreateProductHandler содержит логику создания товара. Он принимает DTO и создает товар, привязывает теги и сохраняет изображения в транзакции.
Для успешного использования паттерна “Обработчик” с DTO и VO программист должен иметь уровень подготовки Middle (средний).