За 30 секунд
Laravel — это PHP-фреймворк, в котором из коробки есть роутинг, слой работы с базой данных (Eloquent), валидация, очереди, планировщик и аутентификация. Вы описываете, что должно происходить для каждого URL, а всю обвязку вокруг Laravel берёт на себя. Laravel 13 (март 2026) требует PHP 8.3+ и обновляется с 12-й версии без особой боли.
Почему я это использую
- Я использую его, потому что один фреймворк закрывает весь бэкенд реального продукта: API, админку, очереди, задачи по расписанию, письма и авторизацию — с соглашениями, которые любой новый разработчик уже знает.
- Я использую его, потому что Eloquent и FormRequest позволяют быстро выпускать фичи, не жертвуя структурой, и код остаётся читаемым даже через два года.
- Я использую его, потому что очереди и планировщик встроены, а это важно для тяжёлых частей e-commerce: синхронизации с ERP, писем, сегментов и отчётов.
- Я использую его, потому что инструменты для тестирования (HTTP-тесты, фабрики, фейки) делают бэкенд-тесты настолько дешёвыми, что команда действительно их пишет.
Как это работает
01Жизненный цикл запроса
Каждый запрос входит через public/index.php. Laravel загружает приложение (сервис-провайдеры регистрируют биндинги и конфиг), а затем пропускает запрос через стек middleware — как через цепочку контрольных пунктов в аэропорту. Роутер сопоставляет URL и HTTP-метод с маршрутом, который вызывает метод контроллера. Контроллер что-то возвращает (модель, массив, Resource, view), и Laravel превращает это в HTTP-ответ, который уходит обратно через те же middleware. В Laravel 11+ вся эта связка находится в bootstrap/app.php: маршруты, middleware и обработку исключений регистрируют там, а не в отдельном классе Kernel. Знание этого порядка объясняет большинство багов: если не проходит авторизация — смотрите middleware; если данные неверные — смотрите контроллер и всё, что ниже.
- Сервис-провайдер: класс, который при загрузке регистрирует что-то в сервис-контейнере.
- Сервис-контейнер: система внедрения зависимостей в Laravel; он сам собирает классы и их зависимости.
- Middleware: код, который выполняется до или после контроллера (авторизация, троттлинг, локаль, CORS).
02Роутинг, middleware и тонкие контроллеры
Маршруты связывают URL с кодом. Группируйте их по аудитории и вешайте middleware на группу — тогда каждый маршрут внутри унаследует аутентификацию, лимиты запросов или локаль. Держите контроллеры тонкими: их задача — прочитать провалидированные входные данные, вызвать сервис, который делает реальную работу, и вернуть ответ. Бизнес-правила (как считается цена, когда заказ можно отменить) живут в сервисах или экшенах, а доступ к данным — в моделях. Такое разделение «контроллер → сервис → модель» позволяет вызывать одну и ту же логику из API-эндпоинта, Artisan-команды или задачи в очереди без копирования кода.
// routes/api.php
use App\Http\Controllers\Api\OrderController;
use Illuminate\Support\Facades\Route;
Route::middleware(['auth:sanctum', 'throttle:api'])
->prefix('v1')
->group(function () {
Route::get('orders', [OrderController::class, 'index']);
Route::post('orders', [OrderController::class, 'store']);
Route::get('orders/{order}', [OrderController::class, 'show']);
});
03Валидация через FormRequest, ответ через API Resources
Валидируйте на входе. Класс FormRequest содержит правила и проверку authorize() для одного действия; если валидация не прошла, Laravel вернёт ответ 422 с ошибками по полям ещё до запуска контроллера. Внутри контроллера $request->validated() отдаёт только прошедшие проверку поля, что защищает от mass assignment. На выходе API Resource превращает модель ровно в ту форму JSON, которую ждут клиенты. Это отвязывает колонки базы данных от публичного контракта: можно переименовать колонку и не сломать мобильное приложение. Ещё Resources упрощают условные поля — например, включать связи, только если они были загружены.
class StoreOrderRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create', Order::class);
}
public function rules(): array
{
return [
'items' => ['required', 'array', 'min:1'],
'items.*.product_id' => ['required', 'integer', 'exists:products,id'],
'items.*.qty' => ['required', 'integer', 'between:1,99'],
'note' => ['nullable', 'string', 'max:500'],
];
}
}
04Eloquent, связи и eager loading
Eloquent — это ORM в Laravel: каждый класс модели соответствует таблице, а связи (hasMany, belongsTo, belongsToMany, morphMany) — это методы модели. Классическая ловушка — проблема N+1: вы загружаете 50 заказов, затем в цикле обращаетесь к $order->customer, и Eloquent выполняет 50 лишних запросов. Eager loading через with() подтягивает связь одним дополнительным запросом. В разработке вызывайте Model::preventLazyLoading(), чтобы ленивая загрузка бросала исключение и вы ловили N+1 заранее. Используйте withCount() для подсчётов, выбирайте только нужные колонки для больших списков и пагинируйте всё, что пользователь может листать. Laravel 13 также позволяет настраивать многие свойства модели через PHP-атрибуты, но классические свойства по-прежнему работают.
// AppServiceProvider::boot()
Model::preventLazyLoading(! app()->isProduction());
// One query for orders, one for customers, one for items
$orders = Order::query()
->with(['customer:id,name', 'items.product:id,name,price'])
->withCount('items')
->where('status', OrderStatus::Paid)
->latest()
->paginate(20);
return OrderResource::collection($orders);
05Очереди, планировщик и аутентификация
Всё медленное или ненадёжное (письма, SMS, колбэки платежей, экспорты, синхронизация с ERP) уходит в задачу в очереди: пользователь быстро получает ответ, а воркер выполняет работу в фоне, с повторными попытками. Планировщик заменяет кучу записей в cron одной: cron каждую минуту запускает php artisan schedule:run, а само расписание описывается в routes/console.php. Для аутентификации по умолчанию выбирают Sanctum: сессии на cookie для вашего собственного SPA на том же домене верхнего уровня плюс простые персональные токены доступа для мобильных приложений. Passport — это полноценный OAuth2-сервер; выбирайте его, когда третьим сторонам нужны OAuth-флоу или вы уже завязаны на его модель токенов. Socialite добавляет вход через Google, Apple и другие соцсети поверх любого из них.
// routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('subscriptions:expire')
->dailyAt('02:00')
->withoutOverlapping()
->onOneServer();
Schedule::job(new SyncPricesFromErp)->hourly();
// crontab: * * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1
06Советы для продакшена в эпоху Laravel 13
Работайте на PHP 8.3 или новее и везде используйте enum, readonly-свойства и типизированные сигнатуры. При деплое запускайте php artisan optimize, чтобы закэшировать конфиг, маршруты, события и view, и никогда не вызывайте env() вне конфиг-файлов, потому что закэшированный конфиг его игнорирует. Миграции запускайте отдельным шагом деплоя. Держите воркеры очередей под менеджером процессов и перезапускайте их на каждом деплое через php artisan queue:restart, иначе они продолжат выполнять старый код. Локально для отладки используйте Telescope или Pail, а в продакшене — структурированные логи и трекинг ошибок. В Laravel 13 также появились официальный AI SDK и JSON:API-ресурсы; берите их, когда они решают реальную задачу, а не потому что это новинка.
- Деплой: composer install --no-dev --optimize-autoloader, migrate --force, optimize, queue:restart.
- В продакшене ставьте APP_DEBUG=false: отладочные страницы раскрывают секреты.
- Оборачивайте многошаговые записи в транзакции базы данных и диспатчьте задачи через afterCommit.
Когда применять
- Бизнес-приложения, в центре которых база данных: e-commerce, CRM, бронирования, бэк-офисы.
- Один API, который должен обслуживать сайт, мобильное приложение и админку.
- Команды, которым нужны строгие соглашения, быстрый онбординг и «батарейки в комплекте».
- Продукты с большим объёмом фоновой работы: письма, импорты, экспорты, синхронизации по расписанию.
Когда не стоит
- Тысячи долгоживущих соединений на процесс или тяжёлый стриминг, где естественнее подходят Node или Go (или Reverb рядом с Laravel).
- Крошечные функции с одной задачей, где serverless-обработчик проще целого фреймворка.
- Команда без опыта в PHP, у которой уже есть сильный бэкенд на TypeScript.
Альтернативы
- NestJS
TypeScript, модули и DI на Node; отлично подходит, когда весь стек на TypeScript.
- Symfony
Более явный и гибко настраиваемый PHP-фреймворк; многие компоненты Laravel построены на нём.
- Django
Аналог на Python со встроенной админкой; хорош, когда команда живёт в Python.
- Ruby on Rails
Фреймворк, который во многом вдохновил Laravel; похожая продуктивность, экосистема Ruby.
Лучшие практики
- Держите контроллеры тонкими; бизнес-логику выносите в сервисы или экшен-классы с одной задачей.
- Валидируйте любой ввод через FormRequest и отдавайте данные через API Resources.
- Используйте eager loading для связей и включайте preventLazyLoading вне продакшена.
- Используйте enum для статусов и типов вместо магических строк.
- Оборачивайте многошаговые записи в DB::transaction и диспатчьте задачи после коммита.
- Кэшируйте конфиг и маршруты при деплое и перезапускайте воркеры очередей на каждом релизе.
- Пишите feature-тесты для каждого эндпоинта, который двигает деньги или меняет состояние.
Частые ошибки
- Толстые контроллеры с сотнями строк бизнес-логики, которую нельзя переиспользовать или протестировать.
- N+1-запросы, спрятанные в Blade-шаблонах или Resources.
- Использование $request->all() вместе с create(), что открывает дверь для mass assignment.
- Вызов env() в коде приложения: после кэширования конфига он возвращает null.
- Забытый перезапуск воркеров после деплоя, из-за чего задачи выполняют старый код на новой схеме.
Глубже
07Сервис-контейнер, scope и долгоживущие воркеры
Классический PHP-FPM загружает Laravel на каждый запрос и потом всё выбрасывает, поэтому небрежное состояние редко вредит. Воркеры очередей и Octane (FrankenPHP, Swoole или RoadRunner) загружают приложение один раз и затем обслуживают тысячи запросов или задач из той же памяти. Это меняет правила. Биндинг, зарегистрированный через bind(), даёт новый объект при каждом резолве. singleton() даёт один объект на всё время жизни процесса — это правильно для stateless-клиентов, например HTTP-клиента платёжного шлюза. scoped() даёт один объект на запрос или на задачу и сбрасывается между ними — именно это нужно для всего, что хранит текущего пользователя, тенант или локаль. Положить данные запроса в singleton или статическое свойство — классический баг Octane: один клиент на мгновение видит контекст другого клиента.
// AppServiceProvider::register()
// Stateless client: safe to share for the whole process
$this->app->singleton(PaymentGateway::class, fn () => new GatewayClient(
config('services.gateway.key'),
));
// Holds per-request data: reset for every request and every queued job
$this->app->scoped(CurrentTenant::class, fn () => new CurrentTenant());
// Cheap builder: a new instance on every resolve
$this->app->bind(ReportBuilder::class);
08Eloquent на больших таблицах: chunking, lazy collections и strict mode
Order::all() на таблице с двумя миллионами строк загружает в память два миллиона моделей и убивает процесс. Для пакетной работы используйте chunkById(): он листает по первичному ключу и остаётся корректным, даже когда вы обновляете перебираемые строки (обычный chunk() со смещениями в этом случае пропускает строки). Для экспорта lazyById() возвращает LazyCollection, которая стримит строки почти с постоянным расходом памяти. Осторожнее с whereHas() на больших таблицах: он компилируется в коррелированный подзапрос EXISTS, который может быть медленным без подходящего составного индекса, и иногда join или whereIn с подзапросом намного быстрее. Model::shouldBeStrict() включает сразу три защиты: запрет lazy loading, исключение при установке атрибута, которого нет в fillable, вместо молчаливого игнорирования, и исключение при чтении атрибута, который не был выбран.
// Backfill without loading everything
Order::query()
->where('status', OrderStatus::Paid)
->whereNull('invoice_number')
->chunkById(500, function ($orders) {
foreach ($orders as $order) {
GenerateInvoice::dispatch($order->id);
}
});
// Stream an export with near-constant memory
foreach (Order::query()->select(['id', 'total', 'created_at'])->lazyById(1000) as $order) {
$csv->insertOne([$order->id, $order->total, $order->created_at]);
}
09Гонки: атомарные обновления, блокировки строк и распределённые блокировки
Два покупателя одновременно покупают последний товар. Оба запроса читают stock = 1, оба проходят проверку, оба уменьшают остаток, и stock становится -1. Прочитать, проверить в PHP и потом записать — это никогда не безопасно при конкурентности. Самое дешёвое решение — атомарное условное обновление: пусть база проверит и изменит значение одним запросом, а вы посмотрите на количество затронутых строк. Когда нужно, чтобы несколько чтений и записей оставались согласованными, используйте транзакцию с lockForUpdate(), которая держит блокировку строки до коммита; делайте такие транзакции короткими и всегда блокируйте строки в одном и том же порядке, чтобы избежать deadlock. DB::transaction() принимает число попыток и повторяет при deadlock. Для работы, которая охватывает несколько процессов, например один импорт из ERP одновременно на всех серверах, используйте Cache::lock() с хранилищем Redis или базой данных. Уникальный индекс остаётся последней линией обороны.
DB::transaction(function () use ($productId, $qty) {
$affected = Product::whereKey($productId)
->where('stock', '>=', $qty)
->decrement('stock', $qty);
if ($affected === 0) {
throw new OutOfStockException($productId);
}
}, attempts: 3);
// One import at a time across all servers (needs a shared cache store)
Cache::lock('erp-import', 600)->block(5, function () {
app(ErpImporter::class)->run();
});
10Укрепление Laravel API
Большинство проблем безопасности в Laravel — это конфигурация и привычки, а не баги фреймворка. Задайте именованные rate limiters для каждой аудитории: щедрые для залогиненных пользователей, строгие для анонимного трафика, очень строгие для OTP и сброса пароля, с ключом по пользователю, телефону и IP вместе. Ограничивайте вложенные route bindings через scopeBindings(), чтобы /shops/1/orders/99 падал, если заказ 99 принадлежит другому магазину. Используйте encrypted casts для чувствительных колонок вроде ИНН или токенов и помните, что ротация APP_KEY поддерживается через APP_PREVIOUS_KEYS: старые значения по-прежнему расшифровываются, а новые используют новый ключ. Используйте signed URLs для ссылок в письмах (отписка, скачивание), а не изобретайте свои токены. Держите шорткаты супер-админа в Gate::before узкими, потому что они обходят все политики. И наконец, запускайте composer audit в CI, чтобы ловить пакеты с известными уязвимостями.
// AppServiceProvider::boot()
RateLimiter::for('api', function (Request $request) {
return $request->user()
? Limit::perMinute(120)->by('user:'.$request->user()->id)
: Limit::perMinute(30)->by('ip:'.$request->ip());
});
RateLimiter::for('otp', fn (Request $request) => [
Limit::perMinute(1)->by('otp-phone:'.$request->input('phone')),
Limit::perDay(20)->by('otp-ip:'.$request->ip()),
]);
// routes: ->middleware('throttle:otp')
Реальные ситуации
Список заказов стал медленным после года роста
Проблема
Страница заказов в админке на запуске открывалась за 200 мс, а теперь за 6 секунд. Таблица выросла до нескольких миллионов строк, и клиенты жалуются и на API.
Как решить
Измерьте, прежде чем что-то менять. Локально откройте Telescope или Debugbar и посчитайте запросы; в продакшене включите slow query log базы. Обычно находятся две вещи: N+1 внутри Resource и фильтр без индекса. Добавьте eager loading и preventLazyLoading, затем составной индекс под запрос, например (status, created_at), если фильтруете по статусу и сортируете по дате. Замените offset-пагинацию на глубоких страницах на cursorPaginate(), потому что OFFSET 200000 всё равно сканирует пропущенные строки. Выбирайте только колонки, которые показывает список. Измерьте ещё раз и добавьте feature-тест, проверяющий количество запросов, чтобы проблема не вернулась.
Плановая задача выполняется дважды после добавления второго сервера
Проблема
Команда добавила второй сервер приложения за балансировщиком. С тех пор клиенты получают ежедневное SMS-напоминание дважды, а ночной отчёт генерируется два раза.
Как решить
Оба сервера выполняют одну и ту же запись cron, поэтому оба запускают schedule:run каждую минуту. Добавьте onOneServer() к каждой задаче, которая должна выполняться один раз, и убедитесь, что кэш-хранилище по умолчанию общее — Redis, база данных или Memcached, — потому что блокировка живёт там; файловый кэш или array даёт каждому серверу свою блокировку и ничего не защищает. Для долгих задач добавьте withoutOverlapping(). Как вариант, запускайте планировщик только на одном выделенном воркер-хосте. Затем сделайте сами задачи идемпотентными, например записывайте, что напоминание для этой подписки и даты уже отправлено, чтобы будущая ошибка конфигурации не могла привести к двойной отправке.
Обновление живого продукта с Laravel 12 до 13
Проблема
Нагруженный API интернет-магазина работает на Laravel 12 и PHP 8.2. Бизнес хочет обновиться без заморозки разработки фич и без простоя.
Как решить
Начните с PHP, потому что Laravel 13 требует 8.3 или новее: обновите PHP на staging, прогоните весь набор тестов и PHPStan, затем выкатите новую версию PHP в продакшен, оставаясь пока на Laravel 12. Дальше в отдельной ветке поднимите laravel/framework и first-party пакеты в composer.json, пройдите официальный upgrade guide пункт за пунктом и исправьте депрекации. Прогоните тесты, задеплойте на staging, дайте там поработать воркерам очередей и планировщику сутки и проверьте логи. Выкатывайте обычным деплоем без простоя и перезапустите воркеры. Держите ветку короткоживущей и ребейзьте её каждый день, чтобы работа над фичами не останавливалась.
Шпаргалка
- Показать версии, драйверы и статус кэша
php artisan about- Список роутов с их middleware
php artisan route:list -v --except-vendor- Очистить все кэш-файлы после неудачного деплоя
php artisan optimize:clear- Модель с миграцией, фабрикой и политикой
php artisan make:model Order -mfp- Посмотреть колонки и индексы таблицы
php artisan db:table orders- Смотреть логи приложения в реальном времени
php artisan pail --filter="OrderController"- Плановые задачи и время следующего запуска
php artisan schedule:list- Включить все защиты Eloquent
Model::shouldBeStrict(! app()->isProduction());- Быстрая пагинация для глубоких или бесконечных списков
Order::orderBy('id')->cursorPaginate(50);- Проверить зависимости на известные уязвимости
composer audit
В моих реальных проектах
- Построил платформу заказов для сети ресторанов как единый Laravel API (около 330 строк маршрутов) с контроллерами, разделёнными на Shared, Ui, Mobile и Admin, чтобы веб и приложение переиспользовали одну и ту же базовую логику.
- На платформе зоотоваров (Laravel 12) в бэкенде около 110 сервисных классов поверх доменных моделей и 95 файлов бэкенд-тестов; синхронизация с ERP работает как задачи по расписанию за feature-флагами, с алертами о состоянии.
- Почти в одиночку построил CRM для сети спортзалов на Laravel 13 и PHP 8.3, с упорядоченными, непересекающимися задачами по расписанию (разморозка, затем истечение, затем активация) и Passport для авторизации в приложении.
- Запускаю воркеры очередей Laravel под supervisor, а планировщик — через cron на продакшен-серверах.
Вопросы и ответы
Q1Расскажите о жизненном цикле запроса в Laravel.
Запрос попадает в public/index.php, который подключает автозагрузчик и создаёт приложение. Сервис-провайдеры регистрируются и загружаются, так что контейнер знает, как собрать всё необходимое. Затем запрос проходит через глобальные middleware, роутер находит маршрут, и выполняются middleware маршрута (auth, throttle, bindings). Метод контроллера резолвится через контейнер, поэтому его зависимости внедряются автоматически. Всё, что он возвращает, превращается в ответ, который идёт обратно через middleware, и в конце, уже после отправки ответа, выполняются terminable middleware. В Laravel 11 и новее маршруты, middleware и обработка исключений настраиваются в bootstrap/app.php.
Q2Что такое проблема N+1 и как избежать её в Eloquent?
N+1 возникает, когда вы делаете один запрос за списком, а потом ещё по одному запросу на каждую строку, чтобы получить связь, — например, загружаете 50 заказов, а затем лениво подтягиваете покупателя каждого заказа. Получается 51 запрос вместо двух. В Eloquent это лечится eager loading: Order::with('customer')->get(), или load() на уже загруженной коллекции, и withCount() для подсчётов. Чтобы ловить проблему заранее, я включаю Model::preventLazyLoading() вне продакшена — тогда любая ленивая загрузка бросает исключение. На код-ревью я также смотрю Resources и view, потому что именно там обычно прячется скрытое обращение к связям.
Q3Sanctum или Passport: что выбираете?
Для большинства продуктов — Sanctum. Он обеспечивает сессионную авторизацию на cookie для собственного SPA на том же домене верхнего уровня и простые персональные токены доступа для мобильных приложений, с abilities в роли лёгких scope. Passport — полноценный OAuth2-сервер на базе league/oauth2-server; он нужен, когда внешним разработчикам или партнёрским системам требуются стандартные OAuth-флоу, такие как authorization code с PKCE или client credentials, или когда существующая система уже от него зависит. У Passport больше движущихся частей: ключи, клиенты, таблицы токенов и обработка refresh. Если никому за пределами вашей компании OAuth не нужен, Sanctum проще и его легче защитить.
Q4Где размещать бизнес-логику в Laravel-приложении?
Не в контроллерах и не разбросанной по моделям. Контроллеры читают провалидированный ввод и возвращают Resource. Логика живёт в сервисных классах или экшенах с одной задачей, например PlaceOrder или CalculateDeliveryFee, которые принимают простые данные и модели и могут вызываться из контроллера, задачи или команды. В моделях остаются связи, касты, скоупы и небольшие доменные хелперы. Для сложных правил я использую value objects, например тип Money или Quantity, чтобы правила тестировались изолированно. Так контроллеры остаются короткими, логика переиспользуется между вебом, мобильным приложением и админкой, а unit-тесты пишутся легко.
Q5Как вы запускаете очереди и планировщик в продакшене?
Планировщику нужна одна запись в cron, которая каждую минуту запускает php artisan schedule:run; само расписание живёт в коде, с withoutOverlapping и onOneServer для задач, которые не должны выполниться дважды. Воркеры запускают php artisan queue:work под менеджером процессов вроде supervisor или systemd, который перезапускает их при падении. Для каждой задачи я задаю tries, backoff и таймауты, делаю задачи идемпотентными и слежу за таблицей failed_jobs. На каждом деплое запускаю queue:restart, чтобы воркеры подхватили новый код. Если драйвер — Redis, Horizon даёт дашборд и балансировку между очередями.
Q6Что изменилось в Laravel 13 и стоит ли обновляться?
Laravel 13 вышел в марте 2026 года. Главное практическое изменение — минимальная версия PHP: теперь нужен PHP 8.3 или новее. Добавлена более широкая поддержка PHP-атрибутов для настройки моделей и других классов, официальный AI SDK для генерации текста, эмбеддингов и агентов, а также официальные JSON:API-ресурсы. Официальный гайд по обновлению описывает переход с 12-й версии как относительно небольшой. Для активного проекта я обновляюсь в течение нескольких месяцев: сначала поднимаю PHP, обновляю официальные пакеты, прогоняю тесты и исправляю deprecations. Оставаться на поддерживаемых версиях важно прежде всего ради исправлений безопасности.
Вопросы уровня senior
S1Как бы вы масштабировали приложение на Laravel под большой рост трафика?
Сначала делаю серверы приложения stateless: сессии, кэш и блокировки в Redis, файлы в объектном хранилище, чтобы можно было добавлять серверы за балансировщиком. Кэширую конфиг и роуты, использую OPcache и рассматриваю Octane, когда код готов к долгоживущим процессам: он убирает стоимость загрузки, но наказывает за утёкшее состояние. Медленную работу выношу в очереди и масштабирую воркеры отдельно от веб-узлов. На стороне базы сначала добавляю правильные индексы, затем read-реплики через конфиг read и write соединений Laravel с опцией sticky, чтобы пользователь читал свои же записи. Дорогие чтения кэширую с явной инвалидацией. Статику ставлю за CDN. Компромисс — операционная сложность: каждый шаг добавляет движущиеся части, поэтому прежде чем добавлять следующий, измеряйте, куда уходит время.
S2Что меняется, когда вы переводите приложение на Laravel на Octane?
Приложение загружается один раз на воркер и обслуживает много запросов из той же памяти, поэтому пропускная способность растёт, а задержка падает: нет загрузки на каждый запрос. Цена в том, что теперь состояние утекает. Singletons, захватившие запрос, конфиг или аутентифицированного пользователя, хранят значения первого запроса; статические свойства и массивы растут бесконечно; пакеты, рассчитывающие на свежий процесс, могут ломаться. Я проверяю service providers и заменяю зависящие от запроса singletons на scoped bindings или резолвлю их лениво, избегаю статических кэшей и гоняю нагрузочные тесты, следя за памятью на воркер. Octane может перезапускать воркеры после определённого числа запросов как страховку, но это скрывает утечки, а не исправляет их. Для CRUD-приложений с медленной базой Octane даёт немного; для лёгких по CPU высоконагруженных API выигрыш может быть заметным.
S3Как вы предотвращаете овербукинг или двойное списание при конкурентных запросах?
Никогда не читаю значение, не проверяю его в PHP и не записываю обратно. Для одиночного счётчика вроде остатка или баланса я использую атомарное условное обновление — уменьшить там, где остаток не меньше количества, — и считаю ноль затронутых строк неудачей; это не держит долгих блокировок и хорошо масштабируется. Когда бизнес-операция читает и пишет несколько строк, я использую короткую транзакцию с lockForUpdate на нужных строках, блокируя их в одном порядке, и даю DB::transaction повторить попытку при deadlock. Пессимистичные блокировки просты, но снижают пропускную способность на горячих строках; оптимистичная блокировка с колонкой version подходит для правок с низкой вероятностью конфликта, например форм в админке. Для внешних побочных эффектов вроде платежей idempotency key плюс уникальный индекс гарантируют одно списание, даже если клиент повторит запрос.
S4Как вы сохраняете модульность большой кодовой базы на Laravel?
Laravel не навязывает модули, поэтому уровень выбираете вы. Самый лёгкий — подпапки доменов внутри app, например Orders, Catalog и Loyalty, у каждой свои actions, модели и события, плюс правило, что модули общаются через actions или события, а не через модели друг друга. Следующий уровень — локальные пакеты Composer со своими service providers: они делают границы явными и переиспользуемыми между проектами ценой большей обвязки. Что бы вы ни выбрали, закрепите это инструментами: Deptrac или архитектурные тесты Pest, которые роняют CI, когда модуль импортирует внутренности другого. Я не разделяю на сервисы, пока у домена нет других потребностей в масштабировании или релизах, потому что монолит с хорошими границами намного дешевле в эксплуатации и рефакторинге.
Глоссарий
- Service container
- Система внедрения зависимостей Laravel. Она создаёт классы, резолвит зависимости их конструкторов и решает, переиспользовать ли экземпляры.
- Service provider
- Класс, который во время загрузки регистрирует в приложении биндинги, роуты, события или конфиг.
- Facade
- Статический на вид шорткат вроде Cache::get(), который перенаправляет вызовы реальному объекту из контейнера.
- Scoped binding
- Биндинг контейнера, который живёт один запрос или одну задачу из очереди и затем сбрасывается.
- Octane
- Пакет Laravel, который держит загруженное приложение в памяти между запросами с помощью FrankenPHP, Swoole или RoadRunner.
- Route model binding
- Автоматическое превращение id из URL в загруженную модель с ответом 404, если её не существует.
- Cast
- Правило модели, преобразующее колонку при чтении и записи, например в enum, дату, JSON или зашифрованное значение.
- Policy
- Класс, группирующий правила авторизации для одной модели, например может ли пользователь обновить или удалить заказ.
- Cursor pagination
- Постраничный вывод по последнему увиденному ключу вместо смещения; быстро на огромных таблицах, но перейти сразу на страницу 500 нельзя.
Что изучить дальше
Есть идея продукта?Давайте создадим его.
Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.