В Laravel 13.27 добавлена настройка для отдельных подключений, которая скрывает значения привязок запроса из сообщений об исключениях, семейство методов whereBinary() для побайтового сравнения в MySQL и MariaDB, метод refreshForUpdate() , который перезагружает модель Eloquent с пессимистической блокировкой, и многое другое:
-
mask_bindings_in_exception_messagesоставляет заполнители?в сообщенияхQueryException -
whereBinary(),orWhereBinary(),whereNotBinary()иorWhereNotBinary() -
refreshForUpdate()обновляет модель с применённымlockForUpdate() - Фасад
Cloudдля проверок Laravel Cloud и управляемого подключения очередей - Методы очереди
totalPendingSize(),totalDelayedSize()иtotalReservedSize() - Запросы векторного расстояния теперь работают в MariaDB
- Опции DSN для Postgres keepalive и общий кэш учётных данных AWS для SQS
- Исправления для обработки ввода и усиления валидации
#Что нового
#Маскирование привязок запроса в сообщениях об исключениях
QueryException подставляет привязки (bindings) прямо в своё сообщение, поэтому при сбойном insert каждое привязанное значение попадает в getMessage() . Затем это сообщение уходит везде, куда попадают исключения: в таблицу failed_jobs , лог-файлы и спаны APM. В итоге логируются email-адреса, имена и другие персональные данные.
Новый ключ конфигурации для конкретного подключения останавливает интерполяцию и оставляет заполнители ? на своих местах:
'mysql' => [
'driver' => 'mysql',
// ...
'mask_bindings_in_exception_messages' => env('DB_MASK_BINDINGS', false),
],
// default
SQLSTATE[23000]: ... SQL: insert into `users` (`email`) values (foo@example.com))
// masked
SQLSTATE[23000]: ... SQL: insert into `users` (`email`) values (?))
Этот ключ добавлен в config/database.php , поэтому приложения, которые никогда не публиковали этот файл конфигурации, могут включить его с помощью DB_MASK_BINDINGS=true . По умолчанию установлено значение false , при этом меняется только текст сообщения; метод getBindings() по-прежнему возвращает значения. Подробнее о маскировании привязок запроса в сообщениях об исключениях Laravel можно прочитать здесь.
Внесено разработчиком @LauJosefsen в PR #61326.
# whereBinary() для сравнений с учётом регистра
Сопоставления (collations) в MySQL по умолчанию сравнивают строки без учёта регистра, поэтому where('name', 'John') также совпадёт с john и JOHN . Чтобы получить точное побайтовое сравнение, приходилось использовать сырой SQL:
DB::table('queues')->whereRaw('name = BINARY ?', [$queueName])->first();
whereBinary() делает то же самое через конструктор запросов (query builder), наряду с orWhereBinary() , whereNotBinary() и orWhereNotBinary() :
DB::table('queues')->whereBinary('name', $queueName)->first();
// select * from `queues` where `name` = binary ?
DB::table('queues')->whereNotBinary('name', $queueName)->get();
// select * from `queues` where `name` != binary ?
MariaDB наследует грамматику MySQL, поэтому там это тоже работает. Postgres, SQLite и SQL Server выбрасывают RuntimeException , что соответствует тому, как whereLike() обрабатывает чувствительные к регистру поиски в СУБД, которые и так сравнивают с учётом регистра.
Внесено разработчиком @xiCO2k в PR #61261.
# refreshForUpdate()
Модели обычно извлекаются до начала транзакции — через связывание моделей с маршрутами (route model binding) или из полезной нагрузки задачи (job payload). Установка пессимистической блокировки на такую модель означала отбрасывание этого экземпляра и повторный запрос по первичному ключу:
DB::transaction(function () use ($product) {
$product = Product::query()
->lockForUpdate()
->findOrFail($product->getKey());
$product->decrement('stock');
});
refreshForUpdate работает аналогично refresh() , но применяет lockForUpdate() к повторному запросу, поэтому уже имеющийся экземпляр обновляется прямо на месте и остаётся пригодным для использования до конца транзакции:
DB::transaction(function () use ($product) {
$product->refreshForUpdate();
if ($product->stock === 0) {
throw new RuntimeException('The product is out of stock.');
}
$product->decrement('stock');
});
Внесено разработчиком @stevebauman в PR #61247. Блокировка удерживается только на время транзакции, поэтому вызов должен находиться внутри неё; это устраняет состояние гонки данных между чтением модели и записью в неё обратно.
#Фасад Cloud
Чтобы ответить на вопросы «находимся ли мы в Laravel Cloud и используем ли управляемые очереди?», ранее приходилось комбинировать хелпер laravel_cloud() с проверками драйверов. Фасад Cloud объединяет их в три метода:
use Illuminate\Support\Facades\Cloud;
Cloud::hosted(); // running on Laravel Cloud?
Cloud::usesManagedQueues(); // is the cloud queue connection configured?
Cloud::queue(); // the managed queue connection itself
Cloud::queue() выбрасывает RuntimeException , когда управляемые очереди не настроены, поэтому он используется в паре с usesManagedQueues() , а не заменяет его. Фасад не зарегистрирован в псевдонимах по умолчанию, поэтому он не будет конфликтовать с существующим классом Cloud в приложении. Существовавший ранее класс загрузки Cloud в рамках этого изменения был переименован в CloudBootstrapper .
Внесено разработчиком @jackbayliss в PR #61275. Ранее в этом году для Laravel Cloud также появился CLI-инструмент.
#Общие итоги по всем очередям
Существующие методы pendingSize() , delayedSize() и reservedSize() принимают одно имя очереди, поэтому для панели управления, охватывающей несколько очередей, приходилось суммировать их вручную. Три аналогичных метода с префиксом total делают это за один вызов:
Queue::totalPendingSize();
Queue::totalDelayedSize();
Queue::totalReservedSize();
Они суммируют значения по всем очередям, о которых известно подключению, и, в отличие от методов allXJobs() , выполняют только подсчёт, поэтому полезная нагрузка задач не декодируется. Реализовано для драйверов Redis, database, failover и fake.
Внесено разработчиком @jackbayliss в PR #61231.
#Запросы векторного расстояния в MariaDB
Ранее методы whereVectorSimilarTo() , whereVectorDistanceLessThan() , orderByVectorDistance() и selectVectorDistance() поддерживали только Postgres, так как проверка драйвера выполнялась через instanceof PostgresConnection , а оператор pgvector <=> был прописан непосредственно в конструкторе запросов.
Оба моменты перенесены в грамматику, следуя паттерну, который уже используется для compileRandom() и supportsSavepoints() . В MariaDB 11.7+ есть нативный тип колонок VECTOR и функции расстояния, поэтому её грамматика компилируется в vec_distance_cosine() , и поддержка typeVector() со стороны схемы наконец получила соответствующую реализацию со стороны запросов. Обычный MySQL по-прежнему выбрасывает исключение, так как VECTOR_DISTANCE() существует только в MySQL HeatWave.
Внесено разработчиком @Rhaima96 в PR #61250.
#Улучшения обработки ввода и усиление валидации
@KIKOmanasijev внёс ряд исправлений, связанных с тем, как Laravel читает ключи и сравнивает значения.
Вызов Request::merge(['*' => 226]) раньше очищал весь массив входных данных вместо добавления ключа * , поскольку редюсер прогонял ключи через data_set() , который трактует * как wildcard. В Uri::withQuery() была та же ошибка уровнем выше, превращавшая ?role=user&tenant=10 в ?role=admin&tenant=admin . Теперь оба используют Arr::set() , что сохраняет работу точечной нотации для настоящих вложенных слияний, но сохраняет * буквально (#61309, #61312).
Отклонение неизвестных полей, добавленное в Laravel 13, разворачивало входной массив с помощью Arr::dot() , что создаёт одинаковый ключ для вложенного поля profile[name] и буквального ключа "profile.name" . Валидатор экранирует буквальные точки как profile\.name , из-за чего они сравнивали ключи в разных нотациях: буквальный ключ с точкой проходил проверку на неизвестное поле, затем для него не запускалось ни одно правило, и validated() возвращался пустым, пока сырое значение находилось в $request->all() . Теперь ключи ввода разворачиваются с тем же экранированием, что и ключи правил (#61313).
Два правила валидации всё ещё использовали нестрогие сравнения. in_array сопоставлял "1e0" с "1" и "0e123456789" с "0" , а doesnt_contain браковал массив, содержащий "0e123" , как будто тот содержал запрещённый "0" . Оба правила теперь используют строгое сравнение (#61319, #61318), вслед за аналогичным исправлением, ранее применённым к правилу in . Эквивалентное изменение для contains было откатано до релиза (#61330).
Наконец, MaintenanceModeBypassCookie::isValid() проверял, что mac куки установлен, но не проверял, что это строка, поэтому cookie laravel_maintenance , содержащая массив, передавала этот массив в hash_equals() и превращала ожидаемую ошибку 503 в неаутентифицированную 500. Проверка isset() была заменена на is_string() (#61314).
#Другие исправления и улучшения
- Подключения Postgres принимают ключи конфигурации
keepalives,keepalives_idle,keepalives_intervalиkeepalives_count, которые попадают в DSN вместо того, чтобы отбрасываться, благодаря чему долгоживущее подключение воркера не завершается тихо из-за таймаута неактивности фаервола (#61307) - Подключения SQS могут делиться закэшированными учётными данными AWS между процессами через опцию
credentials_cache, заменяя получение учётных данных на каждый воркер PHP-FPM одним получением на ротацию. Laravel Cloud включает её по умолчанию для управляемых очередей (#61321) - Вернулись методы
orWhereKey()иorWhereKeyNot(). Первая попытка вышла в 13.26 и была отозвана в 13.26.1 из-за изменения сигнатурwhereKey(), что критически ломало подклассы Builder, переопределяющие их. Эта версия делегирует вызовы через вложенную группу where и оставляет существующие методы нетронутыми (#61242) - Методы
move()иcopy()на сквозном диске (read-through disk) теперь потоково читают из резервного хранилища, когда основное ещё не переместило файл, вместо возвратаfalseдля файла, которыйexists()определяет как существующий (#61272) - Задержанная (debounced) задача больше не оставляет за собой метку времени
maxWaitпосле выполнения, из-за чего первый запуск следующей пачки полностью пропускал задержку (#61281) - Запушенная внутри транзакции задача очереди при откате транзакции больше не освобождает принудительно уникальную блокировку, которую она никогда не захватывала (#61234)
- Замыкания ограничений жадной загрузки (eager-load) снова стали
static, разрушая циклическую ссылку наBuilder, которая удерживала билдеры и их деревья загруженных отношений в памяти до запуска сборщика циклических ссылок, что наиболее критично для воркеров очередей и Octane (#61264) - Вызов
Process::quietly()->run(...)->throw()выбрасываетProcessFailedExceptionвместоLogicExceptionоб отключённом выводе (#61227), аFakeInvokedProcessполучил методыstop()иensureNotTimedOut(), чтобы фейковые процессы можно было останавливать в тестах (#61266), оба изменения от @xurshudyan - Трейт
InteractsWithTestCaseLifecycle::flushState()даёт приложениям документированный хук для сброса собственного статического состояния между тестами вместо переопределения внутреннего методаtearDownTheTestEnvironment()(#61288) - Изображения, созданные из потока, больше не падают при повторном чтении, так как ленивый загрузчик (lazy loader) теперь разрешается один раз и делится результатом с клонами.
fromUrl()по той же причине перестаёт отправлять новый HTTP-запрос на каждое чтение (#61305), а такжеfromUrl()выбрасывает исключение при неуспешных ответах вместо того, чтобы обрабатывать тело 404-й ошибки как данные изображения (#61254) - Методы
PhpRedisConnection::mget()иhmget()защищены от возвратаfalse, который phpredis выдаёт при отклонённой команде, что ранее приводило кTypeErrorизarray_map()при ошибке CROSSSLOT (#61267) - Исключение из
Brick\Mathбольше не выходит за пределы правил числового сравнения. Защитные блоки вgtиgteперехватывали не тот классMathException, а вltиlteзащиты не было вовсе (#61332) -
Container::scoped()больше не записывает один и тот же абстракт дважды в$scopedInstances(#61251),Route::name('x')->post(...)сохраняет свое имя для экшенов контроллера в виде массива (#61285), аmaxRelationshipDepth(0)отбрасывает вложенные включения (includes) вместо того, чтобы брать сегменты с конца массива (#61297)
Ссылки
- Официальный чейнжлог
- Сравнение v13.26.1...v13.27.0
- PR: #61326 (маскирование привязок запроса)
- PR: #61261 (
whereBinary()) - PR: #61247 (
refreshForUpdate()) - PR: #61275 (фасад
Cloud)
Комментарии (0)
Пока нет комментариев — будьте первым.