← Назад к вопросам

Как сущности используешь для создания endpoint на Laravel?

2.3 Middle🔥 181 комментариев
#Архитектура и паттерны#Фреймворки

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Отличный вопрос, который затрагивает ключевые принципы архитектуры современного 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-клиентом.