Введение
Laravel завоевал авторитет у бизнеса и программистов за эффективность решения задач. По данным BuiltWith (данные на ноябрь 2025), Laravel используется на более чем 700 000 сайтах, а основной репозиторий имеет свыше 75 000 звёзд на GitHub — это один из самых популярных PHP-фреймворков в мире. Дружелюбная подача, удобная среда, гибкость, не слишком строгие требования к коду сделали его выбором для стартапов и enterprise-проектов.
Автор не раз встречал суждение среди коллег, что опыт разработки на Symfony и Laravel равнозначны. Оба хороши, все молодцы. На самом деле Laravel ускоряет разработку, но цена скорости — риск архитектурного расползания логики. Если проект живёт больше года, это становится проблемой. Ниже — 7 ловушек Laravel и решений без отказа от фреймворка.

Два пути разработчика
Первый вариант: разработчик имеет опыт на Laravel. Переходя на Symfony, его удивят незнакомые правила, термины и на первый взгляд непонятно зачем ограничивающие подходы:
Разделение кода по бандлам
Явная DI через
services.yamlСлоистая архитектура (Controller → Service → Repository)
Обязательное использование интерфейсов для зависимостей
Строгая типизация и иммутабельность
Тестируемость через изоляцию зависимостей
Отсутствие фасадов
Многое из этого кажется избыточным, хотя всё это имеет смысл.
Второй вариант: разработчик имеет опыт на Symfony и попадает на Laravel-проект. Ему уже знакомы строгие правила.
Более быстрый цикл разработки в Laravel и более низкие требования к коду сразу бросятся ему в глаза. Плюс заключается в творческой свободе, но риск — в меньшей дисциплине на проекте.
Моя ситуация — вторая. Поделюсь, как из неё выходил.
Терминология: В примерах ниже под Command подразумевается объект-запрос (DTO), описывающий намерение пользователя, а под Handler — объект, реализующий соответствующую бизнес-операцию.
Проблема #1: Eloquent как Active Record
В чём проблема?
Laravel Eloquent смешивает три слоя в одном классе:
Доменные данные (свойства модели)
Бизнес-поведение (методы вроде
changeBalance())Инфраструктуру (SQL, timestamps, mass-assignment,
save(),delete())
// Laravel по умолчанию
class User extends Model
{
public function changeBalance(int $amount): void
{
$this->balance -= $amount; // Бизнес-логика
$this->save(); // Инфраструктура (SQL)
}
}
Проблемы:
Бизнес-логика привязана к базе данных
Невозможно протестировать
changeBalance()без реального соединения с БДНарушение Single Responsibility Principle
Атрибуты можно присвоить напрямую (магия Eloquent), инварианты обходятся:
$user->balance = -1000
Как преодолеть в теории?
Разделить слои: доменная модель отдельно, репозиторий отдельно:
Примечание: Примеры рассчитаны на PHP 8.2+.
// Доменная модель (чистый PHP, без Laravel)
final readonly class User
{
public function __construct(
public UserId $id,
public string $email,
public Money $balance,
) {}
public function changeBalance(Money $amount): self
{
if ($this->balance->lessThan($amount)) {
throw new InsufficientFundsException();
}
return new self(
id: $this->id,
email: $this->email,
balance: $this->balance->subtract($amount),
);
}
}
// Репозиторий (инфраструктура)
interface UserRepositoryInterface
{
public function findById(UserId $id): ?User;
public function save(User $user): void;
}
class EloquentUserRepository implements UserRepositoryInterface
{
public function save(User $user): void
{
UserEloquentModel::updateOrCreate(
['id' => $user->id->value()],
['balance' => $user->balance->amount()]
);
// ⚠️ ВАЖНО: Если у User есть сложные связи (hasMany, belongsToMany),
// потребуется ручной менеджмент каждой связи.
}
}
Что это даёт:
Бизнес-логика независима от инфраструктуры и легко тестируется
Инварианты защищены
Практичный компромисс:
Полное разделение домена и инфраструктуры часто слишком дорого. Если остаётесь с Eloquent, держите модели тонкими:
class User extends Model
{
// Ограничьте массовое присваивание
protected $guarded = ['id', 'balance'];
// Value Object через касты (Laravel 11+)
protected function casts(): array
{
return [
'balance' => Money::class, // Защищает инвариант "баланс >= 0"
];
}
// Аксессор/мутатор защищает инвариант
protected function balance(): Attribute
{
return Attribute::make(
set: fn (int $value) => $value >= 0
? $value
: throw new InvalidArgumentException('Balance cannot be negative')
);
}
}
// Создавайте через именованные фабричные методы вместо прямого User::create()
class UserFactory
{
public static function createWithInitialBalance(string $email, Money $balance): User
{
if ($balance->isNegative()) {
throw new InvalidArgumentException('Initial balance must be non-negative');
}
$user = new User();
$user->email = $email;
$user->balance = $balance->amount(); // Мутатор проверит >= 0
$user->save();
return $user;
}
}
Это не заменяет чистую архитектуру, но снижает риски без полной переписки проекта.
Альтернатива — Doctrine ORM?
В типовом стеке Laravel (Nova, Telescope, Horizon, Scout/Scout-Drivers) ожидается Eloquent; при использовании Doctrine часто возникают несовместимости и рост стоимости интеграции. Немало сторонних пакетов придётся адаптировать или искать альтернативы.
Проблема #2: FormRequest как место бизнес-валидации
В чём проблема?
Laravel предлагает помещать правила валидации в FormRequest, который живёт в UI-слое:
class CreateUserRequest extends FormRequest
{
public function rules(): array
{
return [
'email' => 'required|email|unique:users', // в одну строку словно из викторианской эпохи
'age' => 'required|integer|min:18', // Это бизнес-правило!
];
}
}
class UserController extends Controller
{
public function store(CreateUserRequest $request)
{
// Валидация уже прошла
User::create($request->validated());
}
}
Проблемы:
Бизнес-правило "возраст >= 18" живёт рядом с контроллером
UI-разработчик может случайно изменить бизнес-логику
Невозможно переиспользовать правила в CLI-командах или очередях
Нарушение Dependency Rule: UI-слой не должен содержать бизнес-правила
Как преодолеть?
Перенести валидацию в Application Layer. Элегантное решение — Validated Command DTO наследует Command:
// Command (чистый DTO, framework-agnostic)
readonly class CreateUserCommand
{
public function __construct(
public string $email,
public int $age,
) {}
}
// Validated Command DTO наследует Command и добавляет правила
final readonly class CreateUserValidatedDto extends CreateUserCommand implements ValidatableInterface
{
public static function rules(): array
{
return [
'email' => ['required', 'email', 'unique:users,email'],
'age' => ['required', 'integer', 'min:18'], // Бизнес-правило
];
}
}
// DtoFactory валидирует и создаёт DTO
interface DtoFactoryInterface
{
public function validateAndCreate(string $dtoClass, mixed $validatableData): mixed;
public function fromArray(string $dtoClass, array $data): mixed;
}
final class DtoFactory implements DtoFactoryInterface
{
public function validateAndCreate(string $dtoClass, mixed $validatableData): mixed
{
$data = $this->toArray($validatableData); // Command|array → array
$validator = Validator::make($data, $dtoClass::rules());
if ($validator->fails()) {
throw new ValidationException($validator);
}
return $this->fromArray($dtoClass, $data);
}
public function fromArray(string $dtoClass, array $data): mixed
{
return new $dtoClass(...$data);
}
private function toArray(mixed $data): array
{
// Упрощённая реализация без Reflection для демонстрации
if (is_array($data)) {
return $data;
}
// Для readonly DTO с публичными типизированными свойствами
return get_object_vars($data);
}
}
// Handler использует DtoFactory
final class CreateUserHandler
{
public function __construct(
private readonly DtoFactoryInterface $dtoFactory,
private readonly UserRepositoryInterface $repository,
) {}
public function handle(CreateUserCommand $command): Result
{
try {
// Валидация через Validated Command DTO
$validatedDto = $this->dtoFactory->validateAndCreate(
CreateUserValidatedDto::class,
$command // DtoFactory сам преобразует в array через toArray()
);
} catch (ValidationException $e) {
// Преобразуем исключение валидации в Result::failure()
return Result::failure($e->errors());
}
// Создание пользователя с валидированными данными...
return Result::success($user);
}
}
// Controller — тонкий, только маршрутизация
class UserController extends Controller
{
public function __construct(
private readonly MessageBusInterface $bus,
) {}
public function store(Request $request): JsonResponse
{
$command = new CreateUserCommand(
email: $request->input('email'),
age: (int) $request->input('age'),
);
$result = $this->bus->dispatch($command);
return $result->isSuccess()
? response()->json($result->data(), 201)
: response()->json(['errors' => $result->errors()], 422);
}
}
Преимущества Validated Command DTO:
Command остаётся чистым (не знает о валидации)
Rules в Validated Command DTO переиспользуемы в HTTP, CLI, очередях и тестах
Типизация:
CreateUserValidatedDto— это тип-гарантия валидности данныхБизнес-правила живут в Application Layer, где им место
Контроллер тонкий: только создаёт Command и возвращает результат
Используется стандартный Laravel Validator — нет конфликтов с экосистемой
Консистентная модель ошибок: ValidationException перехватывается в Handler и преобразуется в
Result::failure()— единый способ работы с ошибками
Проблема #3: Policy и Gate — смешивание авторизации с бизнес-логикой
В чём проблема?
Laravel Policy часто содержит не только проверку прав, но и бизнес-логику:
class PostPolicy
{
public function update(User $user, Post $post): bool
{
// Это авторизация
if ($user->id !== $post->author_id) {
return false;
}
// А это бизнес-правило!
if ($post->published_at && $post->published_at->diffInHours(now()) > 24) {
return false; // Нельзя редактировать через 24 часа после публикации
}
return true;
}
}
Проблемы:
Бизнес-правило "нельзя редактировать после 24 часов" живёт в UI-инфраструктуре
Нарушение SRP: Policy делает и авторизацию, и бизнес-проверки
Невозможно переиспользовать правило в других контекстах
Как преодолеть?
Разделить авторизацию и бизнес-правила:
// Доменная модель знает свои правила
final readonly class Post
{
public function __construct(
public PostId $id,
public UserId $authorId,
public ?DateTimeImmutable $publishedAt,
) {}
public function canBeEdited(): bool
{
if (!$this->publishedAt) {
return true; // Черновик всегда можно редактировать
}
$hoursSincePublished = (time() - $this->publishedAt->getTimestamp()) / 3600;
return $hoursSincePublished <= 24;
}
public function isAuthoredBy(UserId $userId): bool
{
return $this->authorId->equals($userId);
}
}
// Policy — только авторизация
class PostPolicy
{
public function update(User $user, Post $post): bool
{
return $post->isAuthoredBy(new UserId($user->id));
}
}
// Handler проверяет бизнес-правила
final class UpdatePostHandler
{
public function handle(UpdatePostCommand $command): Result
{
$post = $this->repository->findById($command->postId);
if (!$post->canBeEdited()) {
return Result::failure(['Post cannot be edited after 24 hours']);
}
// Обновление поста...
}
}
Что это даёт:
Policy содержит только авторизацию, бизнес-правило
canBeEdited()— в доменной моделиЛегко тестировать без инфраструктуры
Проблема #4: Jobs с traits — логика смешана с транспортом
В чём проблема?
php artisan make:job создаёт класс с Dispatchable, Queueable traits:
class SendWelcomeEmail implements ShouldQueue
{
use Dispatchable, Queueable;
public function __construct(
public User $user, // Eloquent модель
) {}
public function handle(Mailer $mailer): void
{
$mailer->send(new WelcomeEmail($this->user));
}
}
// Вызов
SendWelcomeEmail::dispatch($user);
Проблемы:
Job — это и Command (данные), и Handler (логика), и конфигурация очереди
Передача Eloquent-модели в конструктор — антипаттерн (сериализация, N+1)
Привязка к Laravel (
Dispatchable,ShouldQueue)Невозможно отделить транспорт от поведения
Как преодолеть?
Разделить Command, Handler и конфигурацию:
// Command — чистый DTO с конфигурацией очереди
final readonly class SendWelcomeEmailCommand implements
QueueNameConfigurable,
QueueConnectionConfigurable,
RetriesConfigurable
{
public function __construct(
public int $userId, // ID, а не модель
) {}
public function getQueueName(): string
{
return 'emails';
}
public function getQueueConnection(): string
{
return 'redis';
}
public function getRetries(): int
{
return 3;
}
}
// Handler — чистая логика
final class SendWelcomeEmailHandler
{
public function __construct(
private readonly UserRepositoryInterface $userRepository,
private readonly MailerInterface $mailer,
) {}
public function handle(SendWelcomeEmailCommand $command): Result
{
$user = $this->userRepository->findById($command->userId);
if (!$user) {
return Result::failure(['User not found']);
}
$this->mailer->send(new WelcomeEmail($user));
return Result::success();
}
}
// Вызов через MessageBus
$this->bus->dispatch(new SendWelcomeEmailCommand(userId: $user->id));
Что это даёт:
Разделение ответственности: Command (данные), Handler (логика), конфигурация (интерфейсы)
Легко тестируется, нет сериализации Eloquent-моделей
Бонус: этот же паттерн решает проблему раздутых контроллеров (см. проблему #5 ниже)
Проблема #5: Фасады и глобальные хелперы упрощают раздувание контроллеров
В чём проблема?
Laravel предоставляет глобальный доступ к инфраструктуре через фасады (Auth, DB, Mail) и хелперы (auth(), request()). Это архитектурная особенность фреймворка, которая ускоряет разработку, но снимает барьеры для раздувания контроллеров:
// Laravel не препятствует этому
class OrderController extends Controller
{
public function store(Request $request)
{
// Всё доступно глобально — можно писать весь код здесь:
$validated = $request->validate([...]);
DB::beginTransaction();
try {
$order = Order::create($validated); // Active Record
foreach ($request->input('items') as $item) {
OrderItem::create([...]);
$product = Product::find($item['product_id']);
$product->decrement('stock', $item['quantity']);
}
Auth::user()->notify(new OrderCreatedNotification($order)); // Фасад
event(new OrderCreated($order)); // Хелпер
PaymentGateway::charge(auth()->user(), $order->total); // Хелпер
DB::commit();
return response()->json($order, 201);
} catch (\Exception $e) {
DB::rollBack();
Log::error($e);
return response()->json(['error' => 'Failed'], 500);
}
}
}
Почему это происходит:
Active Record (Eloquent) позволяет
Model::create()без репозиторияНет необходимости в явных зависимостях через конструктор
Контраст с Symfony:
В Symfony физически невозможно написать такой контроллер — нужны явные зависимости (EntityManager, Security, EventDispatcher), что естественным образом склоняет к выносу логики в сервисы.
Проблемы:
Контроллер знает о БД, аутентификации, уведомлениях, платежах
Невозможно протестировать логику без HTTP-запроса
Нарушение SRP: контроллер делает всё
Junior-разработчики не видят причин для рефакторинга — "и так работает"
Как преодолеть?
Использовать тот же паттерн Commands/Handlers (из решения проблемы #4):
// Controller — тонкий
class OrderController extends Controller
{
public function __construct(
private readonly MessageBusInterface $bus,
) {}
public function store(Request $request): JsonResponse
{
$command = new CreateOrderCommand(
userId: Auth::id(),
items: $request->input('items'),
);
$result = $this->bus->dispatch($command);
return $result->isSuccess()
? response()->json($result->data(), 201)
: response()->json(['errors' => $result->errors()], 422);
}
}
// Handler инкапсулирует логику
final class CreateOrderHandler
{
public function __construct(
private readonly OrderRepositoryInterface $orderRepository,
private readonly ProductRepositoryInterface $productRepository,
private readonly PaymentGatewayInterface $paymentGateway,
private readonly EventDispatcherInterface $eventDispatcher,
) {}
public function handle(CreateOrderCommand $command): Result
{
// Вся логика здесь: валидация, создание заказа, списание, оплата
// 100% тестируемо без HTTP
}
}
Что это даёт:
Тонкий контроллер (10 строк), вся логика в Handler
Легко тестируется и переиспользуется (HTTP, CLI, очереди)
Решается обе проблемы одним паттерном: и Jobs с traits, и раздутые контроллеры
Проблема #6: Laravel склоняет к сериализации Eloquent моделей в события
В чём проблема?
Laravel Event System поощряет передачу Eloquent моделей в конструктор событий. Это архитектурная особенность, удобная для быстрой разработки, но ведущая к проблемам:
class OrderCreated
{
use SerializesModels;
public function __construct(
public Order $order, // Eloquent модель
) {}
}
// Генерируем событие
event(new OrderCreated($order));
// Listener получает модель со всеми связями
class SendOrderConfirmation
{
public function handle(OrderCreated $event): void
{
// Listener может делать всё с моделью:
$event->order->user->notify(new OrderConfirmationNotification()); // N+1 query
$event->order->update(['notified_at' => now()]); // Изменяет БД
PaymentGateway::charge($event->order->user, $event->order->total); // Side-effects
}
}
Почему Laravel склоняет к этому:
Трейт
SerializesModelsавтоматически сериализует Eloquent модели для очередейДокументация Laravel показывает примеры с передачей
$user,$orderв событияУдобно: не нужно вручную мапить данные в DTO
Проблемы:
N+1 запросы:
$event->order->userможет вызвать ленивую загрузкуУстаревание данных: если событие в очереди, модель может быть неактуальной
Нет изоляции: Listener знает об Eloquent, фасадах, инфраструктуре
Сложная отладка: непонятно, кто и когда изменил модель
Как преодолеть?
Использовать доменные события с чистыми данными (DTO):
// Доменное событие (чистый DTO)
final readonly class OrderCreatedEvent
{
public function __construct(
public OrderId $orderId,
public UserId $userId,
public Money $total,
public DateTimeImmutable $createdAt,
) {}
}
// Listener — только делегирует Command
class SendOrderConfirmationListener
{
public function __construct(
private readonly MessageBusInterface $bus,
) {}
public function handle(OrderCreatedEvent $event): void
{
$this->bus->dispatch(new SendOrderConfirmationCommand(
orderId: $event->orderId->value(),
userId: $event->userId->value(),
));
}
}
// Вся логика — в Handler
final class SendOrderConfirmationHandler
{
public function handle(SendOrderConfirmationCommand $command): Result
{
// Изолированная логика: отправка email, обновление статуса
}
}
Что это даёт:
Событие — чистый DTO, Listener тонкий, вся логика в Handler
Полностью тестируемо
Защита через статический анализ:
Чтобы гарантировать, что новый код не использует проблемные трейты, следует запрещать их через кастомное правило PHPStan (реализуется как extension в phpstan.neon):
// Пример: список запрещённых трейтов в кастомном правиле
$forbiddenTraits = [
'Illuminate\Queue\SerializesModels',
'Illuminate\Foundation\Bus\Dispatchable',
'Illuminate\Bus\Queueable',
'Illuminate\Queue\InteractsWithQueue',
'Illuminate\Bus\Batchable',
'Illuminate\Foundation\Events\Dispatchable',
];
Это предотвращает:
Сериализацию Eloquent моделей в события/Commands
Смешивание логики с транспортом (Dispatchable, Queueable)
Прямое знание о Laravel инфраструктуре в доменном коде
Проблема #7: Отсутствие различий между типами сервисов
В чём проблема?
Это типичная проблема PHP-проектов, но Laravel тоже не предоставляет структуру для её решения. Документация Laravel тоже не делает различий между Application Services и Domain Services, что приводит к хаотичной папке App\Services:
// Типичный Laravel проект
namespace App\Services;
class UserService // Что это? Application Service? Domain Service?
{
public function createUser(array $data): User
{
// Валидация? Логика? Сохранение? Всё вместе
$user = User::create($data);
event(new UserCreated($user));
return $user;
}
public function calculateDiscount(User $user, Order $order): float
{
// Это доменная логика! Почему в Service?
return $user->isVip() ? $order->total * 0.1 : 0;
}
}
Почему это происходит:
Laravel не предлагает структуру для разных типов сервисов
Документация использует общий термин "Service" без различий
Нет примеров разделения Application vs Domain Services
Контраст с Symfony/DDD:
В зрелых проектах чётко разделяются:
Application Services (Use Cases, Handlers) — оркестрация
Domain Services — бизнес-логика, не принадлежащая сущности
Infrastructure Services — работа с внешними системами
Проблемы:
Нет различий между Application Service и Domain Service
UserServiceсмешивает оркестрацию (createUser) и доменную логику (calculateDiscount)Непонятно, куда добавлять новую логику
Junior-разработчики кладут всё подряд в Services
Как преодолеть?
Разделить на Application Services (Commands/Handlers) и Domain Services:
// Application Service = Handler
final class CreateUserHandler
{
public function handle(CreateUserCommand $command): Result
{
// Оркестрация: валидация, создание, событие
}
}
// Domain Service (если логика не принадлежит ни одной сущности)
final class DiscountCalculator
{
public function calculate(User $user, Order $order): Money
{
// Чистая доменная логика без знания об инфраструктуре
return $user->isVip()
? $order->total->multiply(0.1)
: Money::zero();
}
}
Что это даёт:
Чёткое разделение ответственности и мест для добавления кода
Нет "божественных" классов
MessageBus: надстройка над Laravel
Во всех примерах используется MessageBusInterface — тонкая надстройка над родным \Illuminate\Bus\Dispatcher. Что она даёт:
Типизирует Commands через
CommandRepresentationОтделяет Commands от транспорта: не важно, синхронно или асинхронно
Изолирует очереди: конфигурация в интерфейсах Command, не в Laravel traits
Упрощённая реализация
use Illuminate\Bus\Dispatcher;
final readonly class MessageBus implements MessageBusInterface
{
public function __construct(private Dispatcher $dispatcher) {}
public function dispatch(CommandRepresentation $command): ResponseRepresentation
{
// Если Command реализует интерфейсы очередей (ShouldQueue, QueueNameConfigurable)
if ($this->shouldQueue($command)) {
// Оборачиваем Command в Job и отправляем в очередь
$job = new QueueableCommandJob($command);
$this->configureQueue($job, $command);
$this->dispatcher->dispatchToQueue($job);
return new NullResponse();
}
// Иначе выполняем синхронно
return $this->dispatcher->dispatchNow($command);
}
public function map(array $map): void
{
$this->dispatcher->map($map);
}
}
Регистрация Handlers
Маппинг Commands на Handlers в AppServiceProvider::boot():
$this->app->make(MessageBusInterface::class)->map([
CreateUserCommand::class => CreateUserHandler::class,
UserBalanceSendCommand::class => UserBalanceSendCommandHandler::class,
// ...
]);
Result-тип: ошибки как данные
Handlers возвращают Result вместо исключений — ошибки становятся явными и предсказуемыми:
// Result — реализация ResponseRepresentation
final readonly class Result implements ResponseRepresentation
{
public function __construct(
public bool $isSuccess,
public array $messages = [],
public mixed $data = null,
) {}
public static function success(mixed $data = null): self
{
return new self(isSuccess: true, data: $data);
}
public static function failure(array $messages): self
{
return new self(isSuccess: false, messages: $messages);
}
public function errors(): array
{
return $this->messages;
}
}
// Использование в Handler
if ($validator->fails()) {
return Result::failure($validator->errors()->all());
}
return Result::success($user);
Преимущество: контроллер явно обрабатывает успех/ошибку без try-catch блоков.
Заключение
Laravel — отличный фреймворк для быстрого старта. Он не запрещает хорошую архитектуру, но и не стимулирует её.
«Равнозначность» опыта в Laravel и Symfony — миф. Его архитектура «по умолчанию» склоняет к плохим практикам:
Eloquent смешивает слои → Разделить на доменные модели + репозитории (с учётом ручного менеджмента сложных связей)
FormRequest в UI‑слое → Валидация в Application Layer (Validated Command DTO + DtoFactory)
Policy с бизнес‑логикой → Авторизация отдельно, бизнес‑правила в домене
Jobs с traits → Commands (DTO) + Handlers + конфигурация отдельно
Фасады упрощают раздувание контроллеров → Тонкие Controllers + Handlers (используя паттерн из #4)
Сериализация Eloquent в события → Доменные события (DTO) + тонкие Listeners + запрет трейтов через PHPStan
Нет различий между типами сервисов → Application Services (Handlers) vs Domain Services (типичная проблема проектов, но Laravel не помогает)
Продемонстрированные решения совместимы с DDD, CQRS и Hexagonal архитектурой — без оверхеда полной их имплементации. Вы получаете:
Тестируемость: логика независима от инфраструктуры
Масштабируемость: чёткие слои и зоны ответственности
Переиспользуемость: Commands работают и в HTTP, и в CLI, и в очередях
Предсказуемость: ошибки — это данные (Result), а не исключения
С Symfony сама среда располагает к строгому коду. Laravel же, выражаясь метафорически, это чёртик над ухом, который постоянно советует «согрешить», поэтому чтобы писать код для долгосрочной поддержки вам понадобится дополнительный уровень профессионализма.
А как вы решаете архитектурные проблемы в Laravel-проектах? Делитесь идеями.
Комментарии (3)

PelmenBlin
26.11.2025 19:43И да вы пишите о высоких материях, а в Request проверка:
'required|email|unique:users', // в одну строку словно из викторианской эпохиРазделите строку на массив построчный и ревью правок человеку легче будет делать при добавлении или изменении 1 условия.

karrakoliko
26.11.2025 19:43Переходя на Symfony, его удивят незнакомые правила <...> Разделение кода по бандлам
ну вот еще. это копролиты, бандлы давно уже (с 5 версии?) не рекомендуются для организации кода.
не позорьтесь
PelmenBlin
Исправленное заключение:
Laravel — отличный фреймворк для быстрого старта. Он не запрещает хорошую архитектуру, остальное зависит от разработчика.
P.S. Laravel поэтому и стал популярными, что не выносит голову "какой же я сервис делаю Domain или Application?".
Как думаете много разработчиков на старте хотят забивать себе голову этими материями?