felixkiss/uniquewith-validator
Laravel validation rule for uniqueness across multiple columns. The unique_with rule extends Laravel's built-in unique validator, letting you require combinations such as company_id and email to be unique together, with support for ignoring IDs, extra where clauses, and custom column names.
Install and register. The package is a single validation extension, so getting started is mostly about wiring it into Laravel's validator once:
composer require felixkiss/uniquewith-validator
On Laravel 5.5+ the Felixkiss\UniqueWithValidator\UniqueWithValidatorServiceProvider is auto-discovered. On older versions add it to config/app.php under providers. There is no config file to publish and no migrations — the package only adds a unique_with rule, registered through Validator::extend.
Where to look first. The README is the entire API surface (roughly one page), and the implementation lives in a single class that extends Illuminate\Validation\Validator and defines validateUniqueWith(). If you need to know exactly what query it builds, read that method — it is short. There is nothing else in the package worth exploring.
First use case: uniqueness scoped to an owner/tenant. The canonical scenario is "email must be unique within an account, not globally":
$rules = [
'account_id' => 'required|exists:accounts,id',
'email' => 'required|email|unique_with:users,account_id',
];
$validator = Validator::make($request->all(), $rules);
The rule reads as: the value of the current attribute (email) must not already exist in users where account_id equals the request input account_id. The scoping columns are resolved from the same input payload automatically — that is the whole value proposition versus unique:users plus hand-written where clauses.
Sanity-check it once with a feature test that inserts a duplicate pair and asserts a validation error, because the failure modes (missing input keys, null scoping columns) are silent rather than loud.
FormRequests are the natural home. Because it is a plain string rule, it drops into a rules() method and composes with bail, required, email, etc.:
class StoreUserRequest extends FormRequest
{
public function rules(): array
{
return [
'account_id' => ['required', 'integer', 'exists:accounts,id'],
'email' => ['bail', 'required', 'email', 'unique_with:users,account_id'],
];
}
}
Multiple scoping columns are just additional comma-separated parameters — they are ANDed together:
// name unique per tenant, per parent category
'name' => 'required|unique_with:categories,name,tenant_id,parent_id'
Pin a value instead of reading input. Use the column=value form when the scope is not user-supplied (console commands, seeders, admin actions, queued imports):
'email' => 'required|email|unique_with:users,account_id=' . $account->id
Exclude the current row on update. This is the update-day-to-day need; check the installed README for the exact parameter keyword your version exposes (it has changed across releases), but the shape is:
'email' => ['required', 'email', 'unique_with:users,account_id,ignore=' . $user->id]
Outside HTTP. Since the rule is registered on the global validator, it works identically in Validator::make() inside jobs, importers, and artisan commands — useful when bulk imports need per-tenant dedupe checks before insert.
Know the native alternative. Since Laravel 5.3+, Rule::unique() accepts a where() closure, which covers most of the same ground without a dependency:
use Illuminate\Validation\Rule;
'email' => [
'required', 'email',
Rule::unique('users', 'email')
->where(fn ($q) => $q->where('account_id', $this->input('account_id'))),
],
Reach for unique_with when you want terse string rules (config-driven rules, arrays of rules, legacy codebases); reach for Rule::unique()->where() when you need ignore(), soft-delete handling, or Laravel 9+/10 support, where the package's release matrix may lag.
Testing pattern. Assert both directions — a duplicate pair fails, and the same value under a different scope passes. The second assertion is the one that catches a mis-ordered parameter list.
unique_with:users,account_id=Some, Inc breaks at the comma. Never interpolate free-text values into the rule string; use the input-lookup form or build a Rule object instead.IS NULL. If account_id is absent from the payload, the query builder renders where account_id = null as where account_id is null, and you get uniqueness scoped to rows with a null account. Always mark scoping columns required so this can't happen, and order the rules so the scope fails first.unique_with:users,email,account_id is not the same rule as unique_with:users,account_id on the email field — the former looks for an email input key. Keep a test that proves the scoping....,deleted_at=NULL or equivalent) — verify against your installed version, or switch to Rule::unique()->whereNull('deleted_at').utf8mb4_unicode_ci treats [email protected] and [email protected] as duplicates; PostgreSQL will not. Behaviour differs between local SQLite and production MySQL — test on the real engine.account_id, email) in a migration and translate the QueryException (SQLSTATE 23000) into the same friendly validation message.SELECT ... LIMIT 1; the composite index that enforces uniqueness is also what keeps this cheap.DB::listen() or reproduce it with DB::table($table)->where(...)->toSql(). Swapping in a temporary Rule::unique()->where() version is the fastest way to isolate whether the problem is your parameters or the package.illuminate/support constraint with composer show felixkiss/uniquewith-validator; older tags target older frameworks. On modern Laravel the native rule is usually the lower-risk choice, and unique_with is best treated as a legacy convenience.whereIn, joins), don't fork the rule; register your own Validator::extend or return a closure-based Rule from rules().How can I help you explore Laravel packages today?