Команда Laravel выпустила версию 13.38.0, в которой появились middleware  PreventStaleDatabaseReads  для приложений, читающих данные из реплик базы данных, метод  usingWritePdo() , направляющий все операции чтения внутри callback-функции на соединение для записи, маршрут проверки работоспособности (health route), работающий с кэшированными маршрутами на разных машинах, и многое другое.

#Middleware PreventStaleDatabaseReads

Jack Bayliss добавил опциональное middleware для приложений с разделенными соединениями для чтения и записи базы данных. Реплика может отставать от сервера записи, поэтому пользователь, сохранивший изменения и перезагрузивший страницу, может увидеть старые данные.

После того как запрос выполняет запись в соединение базы данных по умолчанию, middleware сохраняет временную метку в сессии. В течение следующих двух секунд операции чтения этого пользователя направляются на соединение для записи. После этого чтение снова переключается на реплики. Вы можете добавить его в группу  web :

use Illuminate\Foundation\Http\Middleware\PreventStaleDatabaseReads;
 
->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [
        PreventStaleDatabaseReads::class,
    ]);
})
 

Или применить его к группе маршрутов с более длительным окном:

Route::middleware(PreventStaleDatabaseReads::for(10))->group(function () {
    Route::get('/posts/{post}', [PostController::class, 'show']);
    Route::put('/posts/{post}', [PostController::class, 'update']);
});
 

Middleware требует наличия сессии и отслеживает только соединение по умолчанию. Оно охватывает только того пользователя, который выполнил запись, поэтому другие пользователи по-прежнему могут видеть старые данные из реплики.

Подробности см. в #62078.

#DB::usingWritePdo()

Jack Bayliss также добавил  usingWritePdo()  в соединение с базой данных. Существующий метод  onWriteConnection()  применяется только к тому запросу, который он запускает, поэтому связь (relationship), загруженная из результата, все равно будет считываться из реплики:

// The posts query still runs on a replica
$posts = User::onWriteConnection()
    ->where('email', $email)
    ->first()
    ?->posts()
    ->whereNull('published_at')
    ->get();
 

Новый метод направляет все операции чтения внутри callback-функции на соединение для записи, а затем восстанавливает предыдущие настройки, даже если callback вызывает исключение:

$posts = DB::usingWritePdo(fn () => User::query()
    ->where('email', $email)
    ->first()
    ?->posts()
    ->whereNull('published_at')
    ->get()
);
 

Подробности см. в #62099.

#CountCrashesAsExceptions для слушателей, mailable-классов и уведомлений

Dennis Petersmann сделал так, чтобы атрибут  #[CountCrashesAsExceptions] , появившийся в Laravel 13.34, работал с поставленными в очередь слушателями (queued listeners), mailable-классами и уведомлениями. До этого релиза он работал только с заданиями (jobs).

Для остальных трех типов, если воркер падал (например, из-за нехватки памяти), это не учитывалось в  maxExceptions , поэтому слушатель или письмо перезапускались без ограничений, когда  $tries  был равен  0 :

#[MaxExceptions(3), CountCrashesAsExceptions]
class ExportOrders implements ShouldQueue
{
    public $tries = 0;
 
    public function handle(OrdersImported $event)
    {
        // ...
    }
}
 

Подробности см. в #62048.

#Атрибут соединения дочерней модели переопределяет унаследованное соединение

Volodya Kurshudyan исправил ошибку, из-за которой модели игнорировали атрибут  #[Connection] , если родительский класс устанавливал свойство  $connection . Запросы для дочерней модели уходили на соединение родителя без каких-либо ошибок:

abstract class BaseModel extends Model
{
    protected $connection = 'mysql';
}
 
#[Connection('reporting')]
class SalesReport extends BaseModel {}
 
(new SalesReport)->getConnectionName(); // Now returns "reporting"
 

Подробности см. в #62065.

#Исправления для уникальных заданий (Unique Jobs)

Sumaia Aktar исправила проблему, при которой уникальные задания, запланированные с помощью  Schedule::job() , сохраняли свою блокировку при неудачной попытке отправки в очередь. При значении  uniqueFor  по умолчанию, равном  0 , блокировка никогда не истекала, поэтому планировщик пропускал задание при каждом последующем запуске, даже после того, как очередь снова начинала работать. Теперь планировщик снимает блокировку и повторно выбрасывает исключение. В Laravel 13.37 аналогичное изменение было сделано для  dispatch()  и поставленных в очередь слушателей. См. #62083.

Jack Bayliss также сделал так, чтобы широковещательные события (broadcast events), реализующие  ShouldBeUnique , отправляли событие  UniqueJobSkipped  при пропуске, так же как это делают поставленные в очередь задания. См. #62077.

#Снижение потребления памяти в lazy() и lazyById()

Volodya Kurshudyan изменил  lazy()  и  lazyById()  так, чтобы они освобождали предыдущий чанк (chunk) перед выборкой следующего. До этого изменения в памяти одновременно удерживались два чанка, поэтому пиковое потребление памяти было примерно в два раза выше необходимого. Методы  chunk()  и  chunkById()  уже работали таким образом.

Подробности см. в #62086.

#Примечания к выпуску

Полный список новых функций и обновлений вы можете увидеть ниже, а также сравнение изменений (diff) между 13.37.0 и 13.38.0 на GitHub. Следующие примечания к выпуску взяты непосредственно из changelog:

#v13.38.0

  • [13.x] Разрешить значения float в правиле Unique  where  от @sertxudev в #62059
  • [13.x] Удалить неиспользуемый прокси-объект  until  higher order collection от @xurshudyan в #62057
  • [13.x] Исправить псевдоним ofMany() внутри замыканий от @xurshudyan в #62051
  • [13.x] Исправить запросы на существование и агрегатные запросы для самоссылающихся отношений one-of-many от @xurshudyan в #62043
  • [13.x] Поддержка CountCrashesAsExceptions для поставленных в очередь слушателей, mailable-классов и уведомлений от @Taldres в #62048
  • [13.x] Сделать маршрут проверки работоспособности совместимым с кэшированием маршрутов на разных машинах от @percymamedy в #62049
  • [13.x] Восстановить неймспейс для Cloud logging formatter от @xurshudyan в #62068
  • [13.x] Запустить интеграционные тесты для Memcached cache lock и tagged cache от @xurshudyan в #62069
  • [13.x] Исправить  mode() , возвращающий обрезанные значения для float от @xurshudyan в #62063
  • [13.x] Сбрасывать флаг  NotificationFailed  перед каждой отправкой канала от @xurshudyan в #62074
  • [13.x] Учитывать дочерний атрибут  #[Connection]  вместо унаследованного свойства  $connection  от @xurshudyan в #62065
  • [13.x] Исправить  whereBetween  и  whereNotBetween  с границами коллекции от @xurshudyan в #62062
  • [13.x] Исправить изменение таблиц SQLite с внешним ключом на имя таблицы, требующее кавычек от @sumaiazaman в #62084
  • [13.x] Исправить clone() для отношений от @xurshudyan в #62081
  • [13.x] Освобождать уникальную блокировку при сбое отправки запланированного задания от @sumaiazaman в #62083
  • [13.x] Исправить некорректные docblocks от @xurshudyan в #62097
  • [13.x] Исправить ошибку PHPStan при передаче замыкания в PendingBatch::allowFailures() от @xurshudyan в #62096
  • [13.x] Удалить неиспользуемые импорты от @xurshudyan в #62087
  • [13.x] Освобождать предыдущий чанк в  lazy()  и  lazyById()  от @xurshudyan в #62086
  • [13.x] Добавить middleware PreventStaleDatabaseReads от @jackbayliss в #62078
  • [13.x] Отправлять UniqueJobSkipped при пропуске уникального широковещательного события от @jackbayliss в #62077
  • [13.x] Удалить неиспользуемые файлы тестов от @xurshudyan в #62101
  • [13.x] Исправить TypeError в правилах сравнения дат для значений, не являющихся датами от @GrahamCampbell в #62103
  • [13.x] Исправить некорректные docblocks от @xurshudyan в #62100
  • [13.x] Добавить usingWritePdo в Connection от @jackbayliss в #62099