Написано уже не мало статей о том как всё же укротить строители запросов в Laravel чтобы вся команда понимала "Что происходит?" на проекте. Но даже в 2026 году я всё ещё встречаю проекты, где программисты продолжают писать запросы везде где только можно, но не в одном обозначенном месте.
Да я раньше тоже писал запросы в Serviсe, Controller, Model - да и вообще, где только приходилось. Но все мы развиваемся и хотим создать вокруг себя сообщество, я бы даже сказал команду, команду которая будет писать код понятный каждому.
Поэтому я решил снова осветить данную тематику. В данной статье я покажу на личном примере, где я храню эти Builder и как использую. Также мы рассмотрим связку с Repository чтобы ваш код стал ещё лучше.
Рассмотрим банальный пример на модели User. Но мы с вами сделаем так чтобы мы могли пользоваться нашими Builder быстро и легко.
Для этого я предлагаю первым шагом реализовать trait. Чтобы не писать каждый раз в модели данный код в будущем.
Сразу хочу сказать эта реализация имеет некоторые ограничения вы можете перенести данные метод в модель для которой пишите Builder.
<?php declare(strict_types=1); namespace App\Models\Traits; use Illuminate\Database\Eloquent\Builder; trait HasCustomEloquentBuilder { /** * Путь к папке с билдерами */ private const string ELOQUENT_BUILDER_PATH = 'EloquentBuilders'; /** * Метод для создания билдера * * @return Builder<static> */ public function newEloquentBuilder($query): Builder { /** @var Builder<static> */ return new (str_replace('Models', self::ELOQUENT_BUILDER_PATH, static::class).'Builder')($query); } }
Далее можем создать папку EloquentBuilders внутри app и далее создать там наш первый кастомный Builder.
<?php declare(strict_types=1); namespace App\EloquentBuilders; use App\Models\User; use Illuminate\Database\Eloquent\Builder; /** * @extends Builder<User> */ final class UserBuilder extends Builder { public function whereActive(bool $isActive = true): self { return $this->where('is_active', $isActive); } }
Далее мы можем создать модель и перейти к использованию нашего trait.
<?php declare(strict_types=1); namespace App\Models; use App\EloquentBuilders\UserBuilder; use App\Traits\HasCustomEloquentBuilder; use Database\Factories\UserFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Laravel\Sanctum\HasApiTokens; /** * User model * * @property int $id * @property string $name * @property bool $is_active * * @method static UserBuilder query() */ class User extends Authenticatable { use HasApiTokens; use HasCustomEloquentBuilder; /** @use HasFactory<UserFactory> */ use HasFactory; protected $fillable = [ 'name', 'is_active', ]; }
Обычно после этого все идут в контроллер и пишут там примерно такое:
<?php declare(strict_types=1); namespace App\Http\Controllers\Api; use App\UseCases\FetchUserList\FetchUseListHandler; use Illuminate\Http\JsonResponse; class UserController { public function index(): JsonResponse { return new JsonResponse(User::query()->whereActive()->get()); } }
Но согласитесь, такой вариант реализации в будущем нас мало устроит, даже если у вас там появится пагинация. Сейчас мы говорим про архитектуру)
Нам надо пойти дальше и реализовать репозиторий, куда мы вынесем с вами наш код.
<?php declare(strict_types=1); namespace App\Repositories; use App\Models\User; use App\Repositories\Contracts\UserRepositoryInterface; use Illuminate\Support\Collection; class UserRepository implements UserRepositoryInterface { /** * @return Collection<int, User> */ public function fetchListActive(): Collection { /** @var Collection<int, User> */ return User::query()->whereActive()->get(); } }
Теперь всё стало намного лучше и контроллер может выглядеть вот так:
<?php declare(strict_types=1); namespace App\Http\Controllers\Api; use App\Repositories\Contracts\UserRepositoryInterface; use Illuminate\Http\JsonResponse; class UserController { public function index(UserRepositoryInterface $repository): JsonResponse { return new JsonResponse($repository->fetchListActive()); } }
Но я прибегну к использованию UseCase и напишу код ещё чище. Реализацию полной версии вы сможете посмотреть в проекте примере: https://github.com/palachX/laravel-usecase
<?php namespace App\Http\Controllers\Api; use App\UseCases\FetchUserList\FetchUseListHandler; use Illuminate\Http\JsonResponse; class UserController { public function index(FetchUseListHandler $fetchUseListHandler): JsonResponse { return new JsonResponse($fetchUseListHandler->handle()); } }
В последствии вы можете добавить разные фильтры в ваш Builder и это всё будет в ручном управлении и в одном месте. Если вдруг вам надо будет написать чистые запросы у вас есть прослойка репозитория. Если вдруг какие-то join с поисками то пишите это в Builder.
Пример метода фильтрации и более сложных запросов.
<?php declare(strict_types=1); namespace App\EloquentBuilders; use App\Models\User; use Illuminate\Database\Eloquent\Builder; /** * @extends Builder<User> */ final class UserBuilder extends Builder { public function whereActive(bool $isActive = true): self { return $this->where('is_active', $isActive); } public function filter(?UserFilterData $filter = null): self { if ($filter === null) { return $this; } if ($filter->name !== null) { $this->where('name', 'like', "%{$filter->name}%"); } if ($filter->isActive !== null) { $this->where('is_active', $filter->isActive); } return $this; } public function withOrdersCount(): self { return $this->withCount('orders'); } public function whereHasActiveSubscription(): self { return $this->whereHas('subscriptions', fn($q) => $q->where('active', true) ); } }
Главное правило - чтобы это всё было в одном месте.
Итог
Со временем я пришёл к тому, что держать запросы где попало - плохая идея. Сначала это кажется удобным, но по мере роста проекта превращается в хаос: одинаковые условия копируются, логика размазывается по коду, а разобраться “что происходит” становится всё сложнее.
Кастомные Query Builders для меня стали точкой, где вся работа с запросами наконец-то собралась в одном месте. В связке с Repository и UseCase это дало понятную структуру: где данные, где запросы, где бизнес-логика.
Мне это дало несколько вещей:
Я всегда знаю, где искать нужный запрос
Могу переиспользовать фильтры без копипаста
Код стал читаться проще и быстрее
Но важно понимать - это не серебряная пуля. Если проект маленький, можно спокойно жить и без этого. Такой подход начинает реально окупаться, когда проект растёт и в нём работает несколько человек.
Для себя я сделал простой вывод: лучше один раз договориться о структуре и придерживаться её, чем потом разбираться в разбросанных по проекту where.
Полезные ссылки:
Проект пример: https://github.com/palachX/laravel-usecase
Статья которую я прочитал в 2024: https://martinjoo.dev/build-your-own-laravel-query-builders
Статья перевод: https://laravel.su/p/kastomnye-query-builders-v-laravel
Статья по UseCase: https://habr.com/ru/articles/1012988/
Комментарии (14)

zartdinov
27.04.2026 19:06Динамические where... по всем полям и так должны работать, например:
User::whereStatus(‘active’);
User::whereEmail(‘example@example.com’);
Поэтому обычно только специфичные добавляем.
Даже вроде всякие ->whereAgeGreaterThan(10) и тд. можно использовать, но это лучше проверить. Используем стандартный билдер.
KoIIIeY
27.04.2026 19:06Статья про SOLID, а не про билдер :)

zartdinov
27.04.2026 19:06Окей, но она вся построена на добавлении функции whereActive, которая и так по сути есть, просто называется whereIsActive.
Круто, конечно, что вы тут увидели какие-то принципы в коде, который не нужен).

KoIIIeY
27.04.2026 19:06Пример и правда плохой.
Но он не про изЭктив, а про то что сегодня это изЭктив, а завтра изЭктив+нотБаннед, поменяные в одном месте, а не в ста местах

zartdinov
27.04.2026 19:06Да, пример согласен, но статья полезная. Вот тоже хотел обратить внимание на фильтр. У нас они везде разные, например, мы не заносили в билдер. Бывает по одному и тому же полю надо немного по-другому фильтрануть. Это надо или уникальные названия каждый раз придумывать или нарушать ТЗ. Не знаю, может нарушает тот же SOLID.

adobry96 Автор
27.04.2026 19:06Спасибо что прочитали статью!)
В примерах я также описал вариант с фильирами и другими подзрапросами. whereActive только для начального прмера чтобы люди уловили идею. Да магия laravel позволяет это писать и без кастомных билдеров. Но в крупных проектах иногда хочется держать всё в ручном режиме.

zartdinov
27.04.2026 19:06Да, идея понятна, спасибо, не придирался, просто хотел обратить внимание. У нас эти билдеры достаточно толстые сами по себе. Кастомить билдер намного удобнее чем добавлять where... или scope... в саму модель. Есть много других интересных примеров помимо фильтрации. Хотя и с фильтрацией бывает идут дальше, тот же Spatie Query Builder вообще работает от параметров запооса.

Evgvfv
27.04.2026 19:06В ларке и так кругом магия, надо добавить магию над магией))

adobry96 Автор
27.04.2026 19:06Спасибо что прочитали статью!)
Да в laravel много магии, но как раз тут мы и хотим сделать это ручным управлением) А кастомный билдер помогает нам в этом, ведь это единая точка и любой разработчик её видит и может ей управлять)

tkovacs
27.04.2026 19:06А чем не устроили скоупы, что понадобилось создание отдельного QueryBuilder?
Я ещё понимаю если бы расширили его возможности, но добавить туда что то намекающие на бизнес логику, это довольно дико.

adobry96 Автор
27.04.2026 19:06Спасибо что прочитали статью!)
Да scope которые по дефольу есть работают хорошо, но сами понимаете в большое проекте магии laravel хочеться меньше, плюс scope могут работать медленее если посмотрите как они устроены. В примерах я показал как можно добавить фильтрацию и другие более сложные запросы. Все таки хочется больше ручногл управления.
В данных примерах нет бизнес логики, но и да писать там её не следуюет.
whereActiveбыло бы бизнес логикой если бы было вот так:public function whereActive() { return $this ->whereNotNull('email_verified_at') ->where('status', '!=', 'blocked') ->whereHas('subscription', fn ($q) => $q->valid()); }Т.е. само обозначение active. Конечно тогда такое лучше писать в других слоях приложения.
Kot_na_klaviature
Не хватает ещё пары классов со скоупами типа isActive, которые будут прослойкой над методом whereActive. И вообще все части запросов билдера надо обязательно упаковывать в отдельные методы над методами и чем больше методов тем лучше.
adobry96 Автор
Большое спасибо, что прочитали статью!)
Конечно я показал самые базовые примеры, но то как их применить и где остается за вами!