Как сущности используешь для создания endpoint на Laravel?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Отличный вопрос, который затрагивает ключевые принципы архитектуры современного Laravel-приложения. Моя стратегия основана на разделении ответственности, где сущности (Entities) — это ядро бизнес-логики, а Laravel (с его MVC и Eloquent) — инфраструктура для доставки этой логики пользователю. Я следую принципу "Толстые модели, тонкие контроллеры", но с важным уточнением: "толщина" — это не в Eloquent-моделях, а в слое доменных сущностей и сервисов.
Вот мой поэтапный подход к созданию endpoint с использованием сущностей:
1. Определение доменной сущности (Domain Entity)
Это чистый PHP-класс, представляющий ключевой бизнес-объект (Заказ, Пользователь, Инвойс). Он не наследует Illuminate\Database\Eloquent\Model. Его задача — инкапсулировать данные и бизнес-правила (валидации, состояния, вычисления).
<?php
namespace Domain\Entities;
class Order
{
private string $id;
private string $status;
private float $totalAmount;
private array $lineItems = [];
public function __construct(string $id, float $totalAmount, string $status = 'new')
{
$this->id = $id;
$this->setTotalAmount($totalAmount); // Используем сеттер для валидации
$this->status = $status;
}
// Бизнес-логика внутри сущности
public function markAsPaid(): void
{
if (!in_array($this->status, ['new', 'pending'])) {
throw new \DomainException('Only new or pending orders can be marked as paid.');
}
$this->status = 'paid';
}
public function addLineItem(LineItem $item): void
{
$this->lineItems[] = $item;
$this->recalculateTotal();
}
private function recalculateTotal(): void
{
$this->totalAmount = array_reduce(
$this->lineItems,
fn(float $sum, LineItem $item) => $sum + $item->getPrice(),
0.0
);
}
// Геттеры для доступа к данным
public function getId(): string { return $this->id; }
public function getStatus(): string { return $this->status; }
public function getTotalAmount(): float { return $this->totalAmount; }
private function setTotalAmount(float $amount): void
{
if ($amount < 0) {
throw new \InvalidArgumentException('Total amount cannot be negative.');
}
$this->totalAmount = $amount;
}
}
2. Создание Eloquent-модели как инфраструктурного слоя
Eloquent-модель — это реализация деталей персистентности (хранилища) для моей доменной сущности. Она наследует Model и занимается маппингом полей сущности в базу данных.
<?php
namespace Infrastructure\Eloquent\Models;
use Illuminate\Database\Eloquent\Model;
class OrderModel extends Model
{
protected $table = 'orders';
protected $keyType = 'string';
public $incrementing = false;
// Маппинг данных из Entity в массив для БД
public static function fromEntity(Order $order): self
{
return new self([
'id' => $order->getId(),
'status' => $order->getStatus(),
'total_amount' => $order->getTotalAmount(),
]);
}
// Преобразование данных из БД в Entity
public function toEntity(): Order
{
return new Order(
id: $this->id,
totalAmount: $this->total_amount,
status: $this->status
);
}
}
3. Реализация Репозитория (Repository)
Репозиторий — это абстракция для доступа к коллекции сущностей. Он скрывает детали хранения (Eloquent, база данных) от бизнес-логики. Я определяю интерфейс в доменном слое и реализую его в инфраструктурном.
Интерфейс в Domain Layer:
namespace Domain\Repositories;
use Domain\Entities\Order;
interface OrderRepositoryInterface
{
public function findById(string $id): ?Order;
public function save(Order $order): void;
}
Реализация в Infrastructure Layer:
namespace Infrastructure\Repositories;
use Domain\Entities\Order;
use Domain\Repositories\OrderRepositoryInterface;
use Infrastructure\Eloquent\Models\OrderModel;
class OrderRepository implements OrderRepositoryInterface
{
public function findById(string $id): ?Order
{
$model = OrderModel::find($id);
return $model ? $model->toEntity() : null;
}
public function save(Order $order): void
{
OrderModel::fromEntity($order)->save();
}
}
4. Создание Сервиса (Application Service)
Сервис — это координатор, который оркестрирует сущности, репозитории и другие зависимости для выполнения конкретной use case (сценария использования) приложения. В нем жижит основная workflow-логика.
namespace Application\Services;
use Domain\Entities\Order;
use Domain\Repositories\OrderRepositoryInterface;
class OrderService
{
public function __construct(private OrderRepositoryInterface $repository) {}
public function createOrder(float $totalAmount): Order
{
$order = new Order(
id: \Str::uuid(), // Генерация ID - инфраструктурная деталь
totalAmount: $totalAmount
);
$this->repository->save($order);
return $order;
}
public function processPayment(string $orderId): Order
{
$order = $this->repository->findById($orderId);
if (!$order) {
throw new \Exception('Order not found.');
}
$order->markAsPaid(); // Вызов бизнес-логики из сущности!
$this->repository->save($order);
return $order;
}
}
5. Финальный endpoint в Контроллере (Controller)
Контроллер становится тонким. Его задачи: получить входные данные (HTTP Request), вызвать соответствующий сервис и вернуть ответ (HTTP Response). Вся бизнес-логика уже инкапсулирована в сервисах и сущностях.
namespace App\Http\Controllers\Api\V1;
use Application\Services\OrderService;
use App\Http\Requests\CreateOrderRequest;
use App\Http\Resources\OrderResource;
class OrderController extends Controller
{
public function __construct(private OrderService $orderService) {}
public function store(CreateOrderRequest $request)
{
// 1. Валидация входных данных (в Form Request)
$validated = $request->validated();
// 2. Вызов сервиса (Application Layer)
$order = $this->orderService->createOrder($validated['total_amount']);
// 3. Преобразование сущности в API-ресурс
return new OrderResource($order);
}
public function pay(string $orderId)
{
$order = $this->orderService->processPayment($orderId);
return new OrderResource($order);
}
}
Ключевые преимущества такого подхода:
- Тестируемость: Сущности и сервисы можно тестировать юнит-тестами без базы данных и фреймворка. Eloquent-модели и контроллеры тестируются интеграционными тестами.
- Следование принципу SOLID: Четкое разделение ответственности (SRP). Изменения в бизнес-правилах (
Order::markAsPaid) не затрагивают код базы данных, и наоборот. - Гибкость и поддерживаемость: Завтра можно заменить Eloquent на другую ORM или даже внешний API, изменив только реализации репозиториев в инфраструктурном слое. Доменная логика останется нетронутой.
- Защита инвариантов: Бизнес-правила (например, "оплатить можно только новый заказ") защищены внутри сущности и не могут быть нарушены при вызове из любого места приложения.
- Ясность кода: Контроллер сразу показывает что делается (создается заказ), а сервис и сущность детализируют как это делается, с четким разделением уровней абстракции.
Таким образом, сущности становятся независимым ядром приложения, а Laravel endpoint — лишь одним из возможных способов взаимодействия с этим ядром, наряду с консольными командами, задачами очередей или даже другим API-клиентом.