Тема
Грабли и антипаттерны
Ошибки, которые чаще всего доезжают до прода. Формат: грабля → симптом → защита. Дополняет чеклисты объяснением «почему».
Конфигурация и окружение
| Грабля | Симптом | Защита |
|---|---|---|
env() вне config/ | Работает в dev, null в prod после config:cache | Только config(), правило студии; composer audit привычки — grep по env( вне config/ |
Забытый php artisan optimize в деплое | Прод медленнее dev | optimize (config+route+view+event cache) — обязательный шаг деплоя |
APP_KEY потерян/перегенерирован | Все шифрованные данные и куки невосстановимы | Ключ в менеджере секретов; никогда key:generate на проде с данными |
APP_DEBUG=true на проде | Stack traces с секретами наружу | Чеклист деплоя; debug гейтится на APP_ENV |
Значение с пробелом в .env без кавычек | Обрезанное значение, тихо | Всегда кавычки: MAIL_FROM_NAME="Мой сайт" |
Изменил .env — «не применилось» | Старые значения | После правки: config:clear (dev) / redeploy c optimize (prod) |
Деньги и числа
| Грабля | Симптом | Защита |
|---|---|---|
float для денег | 19.99 + 0.01 = 20.000000000004, расхождение копеек в отчётах | decimal cast или integer в копейках + brick/money |
| Округление в разных местах по-разному | Сумма позиций ≠ итог заказа | Одна точка округления в домене (Money-объект), не в Blade |
intdiv / проценты «на глаз» | Комиссия площадки расходится с ожиданиями на копейку | Тесты на денежную арифметику с граничными суммами |
Eloquent и БД
| Грабля | Симптом | Защита |
|---|---|---|
| N+1 | Страница делает 200 запросов | ->with() явно; query-detector в dev; N+1 — баг по стандарту |
Model::all() / ->get() на большой таблице | OOM, таймауты | chunkById(), lazy(), пагинация |
firstOrCreate под конкуренцией | Дубли записей при параллельных запросах | Уникальный индекс в БД (истина — в БД) + createOrFirst |
Массовые update()/insert() в обход моделей | Не сработали observers, события, activitylog | Осознанно: либо цикл с моделями, либо явно пересчитать зависимое |
whereIn с 10k+ ID | Медленно, лимиты плейсхолдеров | JOIN на временную таблицу / whereIntegerInRaw / чанки |
paginate() + groupBy | Неверный total | Подзапрос или simplePaginate |
| Обычная пагинация на глубоких страницах | OFFSET 100000 — деградация | cursorPaginate() для лент и API |
| Soft deletes + route binding | 404 на «удалённую» запись в админке | withTrashed() в binding админских роутов |
Забытый withoutGlobalScope в админке | Админ не видит неактивные записи | Не вешать global scope на «активность» — явный scope |
Миграция change()/rename на проде с большой таблицей | Блокировка таблицы на минуты | Смотреть спецификацию миграций: CONCURRENTLY, отдельные шаги |
| Транзакция обняла внешний API-вызов | Долгие блокировки, дедлоки | В транзакции — только БД; API/почта до или после (afterCommit) |
Очереди
| Грабля | Симптом | Защита |
|---|---|---|
Деплой без queue:restart | Воркеры выполняют старый код днями | queue:restart — обязательный шаг деплоя |
dispatch() внутри транзакции | «ModelNotFound» в джобе: воркер быстрее коммита | ->afterCommit() или dispatchAfterResponse |
| Целая коллекция/массив данных в конструкторе джоба | Раздутый payload, устаревшие данные при выполнении | Передавать ID/модель (SerializesModels), данные читать в handle() |
Нет $tries / failed() | Бесконечные ретраи или тихая смерть | По стандарту студии — всегда указывать |
| Один воркер на всё | Тяжёлый импорт блокирует отправку писем | Очереди по типам: notifications, heavy, default |
Cache::flush() в коде | Снесли сессии, throttle, чужие кеши | Только теги: Cache::tags([...])->flush() |
Дата и время
| Грабля | Симптом | Защита |
|---|---|---|
| Мутабельный Carbon | $start->addDays(3) изменил и $start | CarbonImmutable через Date::use() |
Смена APP_TIMEZONE с UTC | Сдвиги в расписаниях, сравнениях, DST-баги | Хранить UTC, конвертировать на выводе |
| Сравнение дат строками | «2026-1-9» > «2026-10-1» | Casts datetime/date, сравнение Carbon-методами |
now() в запросах, кешируемых надолго | «Сегодняшние» данные вчерашние | TTL кеша согласован с семантикой данных |
HTTP, роутинг, безопасность периметра
| Грабля | Симптом | Защита |
|---|---|---|
Нет timeout на HTTP-клиенте | Внешний API завис — ваши воркеры/запросы тоже | timeout()+connectTimeout() всегда |
Забытый trustProxies за реверс-прокси | url() генерит http://, редиректы на http, неправильный IP | Обязательная настройка под Caddy (стандарт студии) |
| Вебхук без проверки подписи | Подделка платёжных событий | Проверка подписи + идемпотентность (уникальный event_id) |
| Вебхук не идемпотентен | Двойное начисление при повторной доставке | Таблица обработанных событий с unique |
Валидация array.* без правила на сам array | Обход валидации нестандартным payload | `'items' => 'required |
| Файл валидируется только по расширению | Залит php-файл с расширением .jpg | mimes:+max: + хранение вне public + medialibrary |
| ID в публичных URL | Перебор объектов, утечка объёмов | slug/UUID по стандарту студии |
back() без fallback | Редирект в никуда с пустым referer | back(fallback: route('home')) |
Кеш
| Грабля | Симптом | Защита |
|---|---|---|
| Кеш без инвалидации при правке в админке | «Я поменял, а на сайте старое» | afterSave → Cache::tags(...)->flush() — стандарт студии |
| Кеш-стампид (одновременный прогрев) | Всплеск нагрузки при истечении популярного ключа | Cache::flexible() (SWR) или lock() вокруг прогрева |
| Кеширование объектов с замыканиями/ресурсами | Ошибки сериализации | Кешировать данные (массивы/DTO), не объекты с состоянием |
| В кеше — модель с загруженными связями | Раздутый Redis, устаревшие связи | Кешировать id/массивы, гидрировать по необходимости |
Livewire / Filament
| Грабля | Симптом | Защита |
|---|---|---|
| Модель целиком в public-свойстве | Скрытые запросы на каждый запрос компонента, утечка скрытых полей в HTML | ID + computed property, или DTO |
->live() на каждом поле | Форма шлёт запрос на каждый символ | live(onBlur: true) / debounce |
| Тяжёлый запрос в методе, вызываемом при каждом рендере | Медленная админка | #[Computed] с кешированием / загрузка в mount |
| Загрузка файлов Livewire на проде за прокси | 422 на upload | Проверить лимиты body в Caddy/nginx и post_max_size PHP |
Тесты
| Грабля | Симптом | Защита |
|---|---|---|
RefreshDatabase на рабочей БД | Потеря всех данных | phpunit.xml с {project}_test — железное правило студии |
| Тесты зависят от порядка | Flaky: поодиночке зелёные, вместе падают | Изоляция; свежие фабрики в каждом тесте |
| Замоканное всё | Тесты зелёные, прод падает | Мокать только внешнее (API, почту); БД — настоящая |
sleep() в тестах | Медленный CI | Sleep::fake(), Queue::fake(), travel() для времени |
Общий принцип
Большинство граблей — это расхождение между dev и prod: кеш конфига, очереди, прокси, объёмы данных. Лечится приближением dev к prod (Docker по стандарту студии) и обязательным чеклистом деплоя → Деплой и эксплуатация.