Query Binding Masking and whereBinary() in Laravel 13.27
Laravel #Laravel 13.27 #Eloquent #Query Builder #Security #Releases

Query Binding Masking and whereBinary() in Laravel 13.27

4 min read Mohamed Said Mohamed Said

What's New in Laravel 13.27

Laravel 13.27 was released on August 26, 2026, bringing several developer-quality-of-life improvements alongside meaningful security and correctness fixes. Here is a breakdown of the most important changes.


Masking Query Bindings in Exception Messages

By default, QueryException interpolates bound values directly into its message. That means a failed INSERT can expose email addresses, names, or other sensitive data in log files, APM spans, and the failed_jobs table.

A new per-connection config key stops the interpolation:

'mysql' => [
    'driver' => 'mysql',
    // ...
    'mask_bindings_in_exception_messages' => env('DB_MASK_BINDINGS', false),
],

With masking enabled, the exception message retains ? placeholders instead of real values. getBindings() is unaffected, so debugging tooling that reads bindings directly still works. Applications that never published config/database.php can enable the feature with a single environment variable: DB_MASK_BINDINGS=true.


whereBinary() for Case-Sensitive Comparisons

MySQL's default collations are case-insensitive, so where('name', 'John') also matches john and JOHN. Previously, a byte-exact comparison required raw SQL:

DB::table('queues')->whereRaw('name = BINARY ?', [$queueName])->first();

Laravel 13.27 adds a full family of query builder methods:

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 ?

The family includes orWhereBinary() and orWhereNotBinary(). MariaDB inherits the MySQL grammar and works automatically. Postgres, SQLite, and SQL Server throw a RuntimeException, consistent with how whereLike() handles engines that are already case-sensitive.


refreshForUpdate() for Pessimistic Locking

Models resolved before a transaction starts — through route model binding or a job payload — need to be re-queried under a lock before writing. The old pattern discarded the existing instance:

DB::transaction(function () use ($product) {
    $product = Product::query()->lockForUpdate()->findOrFail($product->getKey());
    $product->decrement('stock');
});

The new refreshForUpdate() method refreshes the instance in place:

DB::transaction(function () use ($product) {
    $product->refreshForUpdate();

    if ($product->stock === 0) {
        throw new RuntimeException('The product is out of stock.');
    }

    $product->decrement('stock');
});

The lock only holds for the life of the transaction, so the call must be made inside one.


Cloud Facade

A new Cloud facade consolidates Laravel Cloud environment checks into three methods:

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() throws a RuntimeException when managed queues are not configured, so pair it with usesManagedQueues(). The facade is not registered in the default aliases to avoid collisions with existing application classes.


Queue Size Totals

Three new methods sum queue sizes across every queue a connection knows about, without decoding job payloads:

Queue::totalPendingSize();
Queue::totalDelayedSize();
Queue::totalReservedSize();

Implemented for the Redis, database, failover, and fake drivers.


Other Notable Changes

  • Vector distance queries now work on MariaDB 11.7+ via vec_distance_cosine().
  • Validation hardening: in_array and doesnt_contain rules now use strict comparison, preventing scientific-notation string matches like "1e0" matching "1".
  • Request::merge(['*' => value]) no longer wipes the entire input array; * is now stored literally.
  • MaintenanceModeBypassCookie::isValid() now checks is_string() on the mac field, preventing a malformed cookie from causing a 500 error.
  • Postgres keepalive DSN options prevent idle firewall timeouts from silently killing long-lived worker connections.
  • SQS credential caching reduces AWS credential fetches to one per rotation instead of one per PHP-FPM worker.

Key Takeaways

  • Enable DB_MASK_BINDINGS=true to keep sensitive values out of exception messages and logs.
  • Replace whereRaw('name = BINARY ?', [...]) with whereBinary('name', ...) for cleaner, driver-aware code.
  • Use refreshForUpdate() inside transactions to simplify pessimistic locking on already-resolved models.
  • The Cloud facade provides a clean API for Laravel Cloud environment detection.
  • Strict comparison fixes in validation rules close subtle type-juggling edge cases.

Source: Laravel News — Laravel 13.27.0

Found this useful?

Frequently Asked Questions

3 questions
Q01 Does enabling `mask_bindings_in_exception_messages` affect what `getBindings()` returns?
No. Only the exception message changes — `?` placeholders replace the interpolated values. `getBindings()` still returns the actual bound values, so debugging tools that read bindings directly are unaffected.
Q02 Which database drivers support the new `whereBinary()` methods?
`whereBinary()` and its variants work on MySQL and MariaDB. Calling them on Postgres, SQLite, or SQL Server throws a `RuntimeException`, consistent with how `whereLike()` handles those engines.
Q03 When should I use `refreshForUpdate()` instead of `refresh()`?
Use `refreshForUpdate()` inside a database transaction when you need a pessimistic lock on a model that was resolved before the transaction started (e.g., via route model binding or a job payload). It reloads the model with `lockForUpdate()` applied, closing the data race between reading and writing.

Continue reading

More Articles

View all