Xdebug Revealed How Laravel's `unique()` Rule Ignores Eloquent Traits

When you first meet Laravel’s validation rules, it is easy to assume they respect everything about your models. Traits, scopes, global filters, all of it.

I believed that too, until a Filament form with Laravel’s unique() rule quietly ignored my Eloquent tenancy trait. The setup looked fine: multi-tenant app, Tenancy for Laravel scoping models, and a form where names must be unique per tenant.

Then I turned on Xdebug in DevWorkspace Pro, stepped through the call stack, and watched unique() pretend my traits did not exist.


The setup

This was the Filament field:

TextInput::make('name')
    ->required()
    ->lazy()
    ->unique(Form::class, 'name', ignoreRecord: true)
Databases in VS Code? Get DevDb
  • Form::class points at an Eloquent model that uses BelongsToTenant.
  • ignoreRecord: true should let you edit an existing record without tripping validation.

I expected tenant-aware validation. Instead, Laravel complained even when I edited an unchanged record in the same tenant.

So I opened VS Code, flipped the Xdebug toggle in DevWorkspace Pro, and traced what was actually happening.

image screenshot of devworkspace pro xdebug toggle here

Following the request with Xdebug

With Xdebug on, the path from the form submit looks like this:

  1. Filament calls $livewire->validate(...).
  2. Livewire builds a validator with Validator::make(...).
  3. Laravel runs $validator->validate().
  4. Deep inside, ValidatesAttributes::validateUnique(...) is called.

In VS Code, DevWorkspace Pro makes this easy to see: you just step through and watch the stack evolve.

image screenshot sidebar vs code debug call stack

Inside Filament’s CanBeValidated.php, the unique() helper resolves to this rule:

$rule = Rule::unique($table, $column);

$table here is just the table name, not an Eloquent query builder. No Form::query(), no traits, no scopes. That rule gets passed to Laravel’s validator, which eventually reaches this core method:

public function validateUnique($attribute, $value, $parameters)
{
    [$connection, $table, $idColumn] = $this->parseTable($parameters[0]);
    $column = $this->getQueryColumn($parameters, $attribute);

    $verifier = $this->getPresenceVerifier($connection);

    return $verifier->getCount(
        $table, $column, $value, $id, $idColumn, $extra
    ) == 0;
}

This is not Eloquent. It is a raw presence check against the database.


Why your traits are ignored

Compare two ways of checking uniqueness:

$isTaken = Form::where('tenant_id', $tenantId)
    ->where('name', $name)
    ->exists();
$request->validate([
    'name' => ['required', Rule::unique('forms', 'name')],
]);

The first is an Eloquent query. Traits like BelongsToTenant can shape it.

The second bypasses Eloquent. Rule::unique() just asks the presence verifier if a row exists in a table. When Filament turns Form::class into a table name, your traits fall out of the picture.

That is why unique() feels like it ignores tenancy, even though your model is correctly configured.


Why Xdebug and DevWorkspace Pro helped

I could have pieced this together by reading source files, but stepping through with Xdebug was faster and clearer:

  • Start at the Filament form field.
  • Step into Livewire and Laravel’s validator.
  • Watch where the model class becomes just a table name.

DevWorkspace Pro removes the usual friction around Xdebug setup, so turning this on felt like flipping a switch, not starting a side project.

image screenshot VS Coe debug floating toolbar

Takeaway

Laravel’s unique() rule ignores your Eloquent traits. That is not a bug in Tenancy for Laravel or Filament. It is just the reality of how validation works under the hood.

If you need tenant-aware uniqueness, you have to bring that context into validation yourself, for example by adding constraints to Rule::unique() or writing a custom rule.

And if something ever feels like it should respect your traits but does not, it is a great excuse to open the project in VS Code, turn on Xdebug in DevWorkspace Pro, and walk the call stack until the behaviour makes sense.

Wanna chat about what you just read, or anything at all? Click here to tweet at me on 𝕏