Репозиторий (Repository Pattern) в Laravel, почему не достаточно писать код в модели
Отличный вопрос — многие разработчики в Laravel на начальном этапе задаются этим. Действительно, модель Eloquent уже предоставляет богатый функционал: find()
, where()
, save()
, with()
и т.д. Поэтому возникает вопрос: зачем использовать репозиторий, если и так всё работает?
Причины использовать паттерн Repository в Laravel:
- Разделение ответственности (SRP — Single Responsibility Principle)
Модель Eloquent — это ActiveRecord. Она уже объединяет:
- доступ к базе,
- бизнес-логику (если вы добавляете методы в модель),
- и иногда — представление (через аксессоры и мутаторы).
Репозиторий позволяет отделить бизнес-логику от модели и контроллера. Модель отвечает только за маппинг с таблицей, а репозиторий — за получение/модификацию данных. - Упрощённое тестирование
Если вы используете репозиторий как интерфейс:interface UserRepositoryInterface { public function getActiveUsers(): Collection; }
...то в тестах легко подменить реализацию:
$this->app->bind(UserRepositoryInterface::class, FakeUserRepository::class);
Это значительно упрощает юнит-тестирование, в отличие от использования
User::where()
напрямую. - Слой абстракции от Eloquent
Если вы решите заменить Eloquent на другую ORM или использовать SQL напрямую, не придется переписывать контроллеры и сервисы — достаточно поменять реализацию репозитория. - Централизация логики доступа к данным
Когда вы обращаетесь к данным из разных частей приложения (например,User::with('roles')->active()->paginate()
), без репозитория вы дублируете логику.
С репозиторием:$users = $this->userRepository->getActiveUsersWithRoles();
Более читаемо, централизовано, легче изменять.
- Читабельность и автодополнение
Вместо сложных цепочек методов Eloquent в контроллерах:User::where('status', 'active')->whereHas('roles', fn($q) => $q->where('name', 'admin'))->get();
...можно написать:
$userRepository->getActiveAdmins();
- Инкапсуляция сложных запросов
Если у вас запрос в 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?
- Сделать модели “тупыми”: вынеси всё в сервисы и репозитории, не храни бизнес-логику в Eloquent-моделях.
- Не вызывать
$model->save()
напрямую — делегируй это в репозиторий. - Не использовать Eloquent-фичи вроде аксессоров, событий и скоупов в бизнес-логике.
- Отделить модели для чтения (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.
Лучший стример года
Анатолий Борисов
Лучший стример года
Алексей
Декабрьское обновление Rust
Вавада актуальное зеркало + на сегодня
Декабрьское обновление Rust
Анатолий Борисов