Блог
Анатолия Борисова

Как повысить свою долю в экспорте программного
обеспечения из России до 1 000 000 рублей в год?

Подарок

для каждого подписавшегося
на нашу рассылку

Паттерны ORM в Laravel и Sumfony

Репозиторий (Repository Pattern) в Laravel, почему не достаточно писать код в модели

Отличный вопрос — многие разработчики в Laravel на начальном этапе задаются этим. Действительно, модель Eloquent уже предоставляет богатый функционал: find(), where(), save(), with() и т.д. Поэтому возникает вопрос: зачем использовать репозиторий, если и так всё работает?

Причины использовать паттерн Repository в Laravel:

  1. Разделение ответственности (SRP — Single Responsibility Principle)
    Модель Eloquent — это ActiveRecord. Она уже объединяет:
    - доступ к базе,
    - бизнес-логику (если вы добавляете методы в модель),
    - и иногда — представление (через аксессоры и мутаторы).
    Репозиторий позволяет отделить бизнес-логику от модели и контроллера. Модель отвечает только за маппинг с таблицей, а репозиторий — за получение/модификацию данных.
  2. Упрощённое тестирование
    Если вы используете репозиторий как интерфейс:

    interface UserRepositoryInterface {
        public function getActiveUsers(): Collection;
    }

    ...то в тестах легко подменить реализацию:

    $this->app->bind(UserRepositoryInterface::class, FakeUserRepository::class);

    Это значительно упрощает юнит-тестирование, в отличие от использования User::where() напрямую.

  3. Слой абстракции от Eloquent
    Если вы решите заменить Eloquent на другую ORM или использовать SQL напрямую, не придется переписывать контроллеры и сервисы — достаточно поменять реализацию репозитория.
  4. Централизация логики доступа к данным
    Когда вы обращаетесь к данным из разных частей приложения (например, User::with('roles')->active()->paginate()), без репозитория вы дублируете логику.
    С репозиторием:

    $users = $this->userRepository->getActiveUsersWithRoles();

    Более читаемо, централизовано, легче изменять.

  5. Читабельность и автодополнение
    Вместо сложных цепочек методов Eloquent в контроллерах:

    User::where('status', 'active')->whereHas('roles', fn($q) => $q->where('name', 'admin'))->get();

    ...можно написать:

    $userRepository->getActiveAdmins();
  6. Инкапсуляция сложных запросов
    Если у вас запрос в 15 строк с joins, unions, subselects и scopes, вы не хотите таскать это в контроллер.

Почему иногда не нужен репозиторий

  • Если у вас простое CRUD-приложение.
  • Если бизнес-логики почти нет, и вы не планируете менять Eloquent.
  • Если вы используете Laravel только как API-слой над микросервисами.

Не нужно навязывать архитектуру без необходимости. Но в средних и крупных проектах, репозиторий — это хорошая инвестиция.


Eloquent отличие от Doctrine

Отличие между Eloquent (Laravel) и Doctrine ORM (Symfony и другие фреймворки PHP) — это разница в архитектурных подходах к работе с данными. Они оба решают одну задачу (ORM), но делают это по-разному.

Главное отличие: Active Record vs Data Mapper

Особенность Eloquent (Laravel) Doctrine ORM
Подход Active Record Data Mapper
Логика Модель = таблица + логика + доступ к данным Сущность = только данные, доступ через отдельный слой (репозиторий)
Простота Проще для новичков Сложнее, но гибче
Тестируемость Сложнее тестировать из-за тесной связи с БД Легче подменять в тестах

Что такое Active Record?

Модель сама знает, как себя сохранить, найти, удалить и обновить.

$user = new User();
$user->name = 'Alice';
$user->save(); // модель сама себя сохраняет

Что такое Data Mapper?

Сущность — это просто контейнер для данных, и сохранением занимается менеджер (EntityManager).

$user = new User();
$user->setName('Alice');

$entityManager->persist($user); // сохраняет EntityManager
$entityManager->flush();

Технические отличия

Особенность Eloquent Doctrine
Миграции Встроенные (artisan migrate) Использует генерацию из сущностей или отдельные миграции
Жадная загрузка with() join / fetch стратегии (EAGER, LAZY)
Отношения Очень просты: hasOne, belongsTo, hasMany Более сложные, требует аннотаций или YAML/XML
Изменения Сохраняются вручную (save) Doctrine сам трекает изменения (Unit of Work)
Модели/Entity Наследуются от Model Чистые классы без зависимости от ORM
Сложные связи и DDD Ограничено Поддерживает Aggregate Roots, Value Objects, и DDD
Массовое заполнение fillable, guarded Меньше автоматизации, больше контроля
Управление сессией БД Нет (прямые вызовы) Есть EntityManager, Unit of Work, транзакции

Пример работы с отношениями

Eloquent:

$post = Post::with('comments.author')->find(1);

Doctrine:

$post = $entityManager->getRepository(Post::class)->find(1);
$comments = $post->getComments(); // если fetch = EAGER, будут загружены

Подход к бизнес-логике

  • В Eloquent бизнес-логика часто живёт прямо в модели ($user->isAdmin()).
  • В Doctrine модель старается быть "чистой", без логики работы с БД. Вся логика выносится в репозитории или сервисы.

Когда использовать что?

Ситуация Выбор
Быстрый CRUD, Laravel-проект Eloquent
DDD, чистая архитектура, сложные связи, Symfony Doctrine
Хочешь всё делать через одну модель Eloquent
Хочешь полное разделение сущности и БД-логики Doctrine

Вывод

Eloquent = проще, быстрее старт, но менее гибко.
Doctrine = сложнее, но чище архитектура и мощнее для больших и сложных проектов.

 

Добавляя Repository Pattern в Laravel мы превращаем Eloquent в Doctrine?

Отличный и очень глубокий вопрос. Краткий ответ: нет, добавляя Repository Pattern в Laravel, вы не превращаете Eloquent в Doctrine, но делаете архитектурный шаг в сторону более чистого и гибкого подхода, похожего на Data Mapper.

Почему не превращаете?

Eloquent всё ещё остаётся Active Record, даже если вы завернёте его в репозиторий.

$user = new User(); // все еще ActiveRecord
$user->name = 'Alice';
$user->save(); // Active Record сам себя сохраняет

Даже если вы пишете:

$userRepository->save($user);

Внутри всё равно будет:

$user->save();

➡ Eloquent сам знает, как себя сохранять. Это ключевое отличие от Doctrine.

Что изменяется с Repository Pattern?

  • Вы отделяете бизнес-логику от слоя доступа к данным, как это принято в паттерне Data Mapper (Doctrine). То есть:
    • Повышается тестируемость
    • Улучшается расширяемость
    • Код становится ближе к DDD (Domain-Driven Design)

Но это всего лишь обёртка, а не реальная смена архитектуры ORM.

Doctrine требует другого:

  • EntityManager: единый объект, управляющий жизненным циклом всех объектов.
  • Unit of Work: Doctrine сам отслеживает изменения, вам не нужно вручную вызывать save().
  • Entities — просто данные, они не знают, как сохраняться или загружаться.

Пример:

$user = new User();
$user->setName('Alice');

$em->persist($user); // отдельно
$em->flush();        // отдельно

Что можно сделать, чтобы Eloquent вел себя ещё ближе к Doctrine?

  1. Сделать модели “тупыми”: вынеси всё в сервисы и репозитории, не храни бизнес-логику в Eloquent-моделях.
  2. Не вызывать $model->save() напрямую — делегируй это в репозиторий.
  3. Не использовать Eloquent-фичи вроде аксессоров, событий и скоупов в бизнес-логике.
  4. Отделить модели для чтения (DTO, Query Models) и записи.

Вывод

  • Использование Repository Pattern в Laravel — архитектурное улучшение, но не изменяет фундаментальный Active Record-подход Eloquent.
  • Doctrine использует Data Mapper, где ORM и сущность полностью отделены друг от друга.
  • С репозиториями вы получаете лучше организованный код, но не Doctrine.

 

Отделить модели для чтения (DTO, Query Models) и записи. Что такое DTO и Query Models

DTO (Data Transfer Object) — это простой контейнер для данных, без бизнес-логики. Используется, чтобы передавать данные между слоями приложения (например, между контроллером и сервисом) или наружу (например, в JSON-ответе).

Признаки DTO:

  • Нет методов кроме конструктора и геттеров (или публичных свойств)
  • Не связан с ORM
  • Используется только для чтения или передачи данных, не для записи в БД

Пример DTO:

class UserDTO {
    public function __construct(
        public readonly int $id,
        public readonly string $name,
        public readonly string $email,
    ) {}
}

Использование:

$user = User::find(1);

return new UserDTO($user->id, $user->name, $user->email);

Query Model (View Model / Read Model) — это отдельная структура, предназначенная исключительно для чтения данных. Часто используется в паттерне CQRS (Command Query Responsibility Segregation).

Признаки Query Model:

  • Создана только для чтения
  • Может включать данные из нескольких таблиц
  • Не содержит бизнес-логики или поведения, как у моделей Eloquent
  • Может быть построена через DB::table, selectRaw, View, или Eloquent, но не содержит save(), update() и т.д.

Пример Query Model:

class UserProfileView {
    public function __construct(
        public readonly string $fullName,
        public readonly string $role,
        public readonly int $postCount
    ) {}
}

Где-то в сервисе:

$data = DB::table('users')
    ->join('roles', 'roles.id', '=', 'users.role_id')
    ->leftJoin('posts', 'posts.user_id', '=', 'users.id')
    ->selectRaw('CONCAT(users.first_name, " ", users.last_name) as full_name, roles.name as role, COUNT(posts.id) as post_count')
    ->groupBy('users.id')
    ->where('users.id', $id)
    ->first();

return new UserProfileView($data->full_name, $data->role, $data->post_count);

DTO vs Query Model: в чём разница?

DTO Query Model
Назначение Перенос данных между слоями Отображение данных, полученных из БД
Источник данных Может быть модель, массив, форма, API Обычно результат SQL или репозитория
Содержит SQL? Нет Иногда — да (или вызывается из места, где SQL)
Логика? Нет Нет
Использование Формы, API-ответы Страницы, отчёты, списки

Зачем их использовать?

  • Явное отделение слоёв приложения
  • Упрощение тестирования
  • Повышение читаемости и предсказуемости
  • Облегчает поддержку при росте проекта
  • Не тянет за собой кучу зависимостей Eloquent

Где это применимо в Laravel?

  • В FormRequest можно преобразовать вход в DTO
  • В репозиториях или сервисах возвращать DTO вместо моделей
  • В ресурсах API или Blade отдавать DTO или Query Model, не давая прямой доступ к Eloquent

 

Пример: FormRequest → DTO → Сервис → Репозиторий → Query Model

Ниже минимальный пример создания пользователя и получения его профиля.

<?php
// UserCreateRequest.php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use App\DTO\UserCreateDTO;

class UserCreateRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name' => 'required|string',
            'email' => 'required|email|unique:users',
            'password' => 'required|string|min:8',
        ];
    }

    public function toDto(): UserCreateDTO
    {
        return new UserCreateDTO(
            $this->input('name'),
            $this->input('email'),
            $this->input('password'),
        );
    }
}
<?php
// UserCreateDTO.php
namespace App\DTO;

class UserCreateDTO
{
    public function __construct(
        public string $name,
        public string $email,
        public string $password,
    ) {}
}
<?php
// UserService.php
namespace App\Services;

use App\DTO\UserCreateDTO;
use App\Repositories\UserRepositoryInterface;

class UserService
{
    public function __construct(protected UserRepositoryInterface $repo) {}

    public function create(UserCreateDTO $dto): int
    {
        return $this->repo->create($dto);
    }

    public function getProfile(int $id)
    {
        return $this->repo->getProfileView($id);
    }
}
<?php
// UserRepositoryInterface.php
namespace App\Repositories;

use App\DTO\UserCreateDTO;
use App\ViewModels\UserProfileView;

interface UserRepositoryInterface
{
    public function create(UserCreateDTO $dto): int;

    public function getProfileView(int $id): UserProfileView;
}
<?php
// EloquentUserRepository.php
namespace App\Repositories;

use App\Models\User;
use Illuminate\Support\Facades\Hash;
use App\DTO\UserCreateDTO;
use App\ViewModels\UserProfileView;
use Illuminate\Support\Facades\DB;

class EloquentUserRepository implements UserRepositoryInterface
{
    public function create(UserCreateDTO $dto): int
    {
        $user = new User();
        $user->name = $dto->name;
        $user->email = $dto->email;
        $user->password = Hash::make($dto->password);
        $user->save();

        return $user->id;
    }

    public function getProfileView(int $id): UserProfileView
    {
        $data = DB::table('users')
            ->select('name', 'email', 'created_at')
            ->where('id', $id)
            ->first();

        return new UserProfileView(
            fullName: $data->name,
            email: $data->email,
            memberSince: $data->created_at,
        );
    }
}
<?php
// UserProfileView.php
namespace App\ViewModels;

class UserProfileView
{
    public function __construct(
        public readonly string $fullName,
        public readonly string $email,
        public readonly string $memberSince,
    ) {}
}
<?php
// UserController.php
namespace App\Http\Controllers;

use App\Http\Requests\UserCreateRequest;
use App\Services\UserService;

class UserController extends Controller
{
    public function __construct(protected UserService $service) {}

    public function store(UserCreateRequest $request)
    {
        $userId = $this->service->create($request->toDto());
        return response()->json(['id' => $userId], 201);
    }

    public function show(int $id)
    {
        $profile = $this->service->getProfile($id);
        return response()->json([
            'name' => $profile->fullName,
            'email' => $profile->email,
            'since' => $profile->memberSince,
        ]);
    }
}

 

Что такое Data Mapper из Symfony

Data Mapper — это архитектурный паттерн, при котором:

  • Сущность (Entity) — это только данные (без знаний о том, как себя сохранить),
  • Сохранением, загрузкой, удалением и изменением сущностей занимается отдельный слой — Data Mapper, чаще всего через EntityManager.

В Symfony этот паттерн реализован в Doctrine ORM.

Сравнение: Active Record vs Data Mapper

Характеристика Active Record (Eloquent) Data Mapper (Doctrine)
Где логика сохранения? Внутри модели ($user->save()) В EntityManager (persist, flush)
Сущность знает о БД? Да Нет
Изменения отслеживаются? Вызываешь save() вручную Doctrine сам трекает изменения
Подход Проще и быстрее начать Чище архитектура, ближе к DDD

Пример Entity

#[ORM\Entity]
class User
{
    #[ORM\Id, ORM\GeneratedValue, ORM\Column]
    private int $id;

    #[ORM\Column(length: 100)]
    private string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function rename(string $newName): void
    {
        $this->name = $newName;
    }
}

Сохранение

$user = new User('Alice');

$entityManager->persist($user);
$entityManager->flush();

Изменение

$user->rename('Bob');

$entityManager->flush();

 

Что такое Active Record в Eloquent

Active Record — это паттерн, где один класс — одна строка таблицы, и этот класс сам умеет сохранять, загружать, обновлять и удалять себя.

В Eloquent модель — это и данные, и методы для доступа к данным.

Пример

$user = new User();
$user->name = 'Alice';
$user->email = 'alice@mail.com';
$user->save();

Признаки Active Record

  • Модель содержит данные и методы сохранения.
  • Таблица соответствует классу.
  • Нет отдельного менеджера для сохранения.

Плюсы

  • Простота и скорость разработки.
  • Всё в одном классе.

Минусы

  • Нарушение принципа единственной ответственности (SRP).
  • Сложно тестировать.
  • Связь с Laravel и Eloquent.

 

Изображение от fullvector на Freepik

Комментариев еще нет.

Оставить комментарий