Overview
The Uniqueness validation rules provide specialized validation for many-to-many relationships, bulk operations, and composite uniqueness constraints. These rules handle complex scenarios that Laravel’s built-in unique rule cannot address.
UniqueInPivot
Validates that a column value is unique within the scope of a many-to-many relationship by checking through a pivot table. Designed for single operations (create/update).
Constructor Parameters
Main table containing the column to validate (e.g., 'addresses')
Pivot table that links main table to owner (e.g., 'user_addresses')
Foreign key in pivot table pointing to main table (e.g., 'address_id')
Foreign key in pivot table pointing to owner (e.g., 'user_id')
Owner ID value (e.g., auth()->id())
Column in main table to check for uniqueness (e.g., 'phone')
Primary key of main table
Primary key value to ignore (for updates: $this->route('id'))
Create Example
use Ronu\RestGenericClass\Core\Rules\UniqueInPivot;
// Scenario: User 42 is creating a new address
// Rule: Phone must be unique among user 42's addresses
public function rules(): array
{
return [
'phone' => [
'nullable',
'string',
'max:64',
new UniqueInPivot(
connection: config('database.default'),
mainTable: 'addresses',
pivotTable: 'user_addresses',
pivotForeignKey: 'address_id',
pivotOwnerKey: 'user_id',
ownerValue: auth()->id(), // 42
column: 'phone',
// ignoreValue omitted - this is a create
),
],
];
}
SQL Executed:
SELECT EXISTS(
SELECT 1 FROM addresses AS _main
INNER JOIN user_addresses AS _pivot
ON _pivot.address_id = _main.id
WHERE _main.phone = '+1-555-0001'
AND _pivot.user_id = 42
)
Update Example
// Scenario: User 42 is updating address ID 7
// Rule: Phone must be unique among user 42's addresses, except for address 7 itself
public function rules(): array
{
return [
'phone' => [
'nullable',
'string',
'max:64',
new UniqueInPivot(
connection: config('database.default'),
mainTable: 'addresses',
pivotTable: 'user_addresses',
pivotForeignKey: 'address_id',
pivotOwnerKey: 'user_id',
ownerValue: auth()->id(), // 42
column: 'phone',
ignoreValue: $this->route('id'), // 7
),
],
];
}
SQL Executed:
SELECT EXISTS(
SELECT 1 FROM addresses AS _main
INNER JOIN user_addresses AS _pivot
ON _pivot.address_id = _main.id
WHERE _main.phone = '+1-555-0001'
AND _pivot.user_id = 42
AND _main.id != 7 -- Ignore own record
)
Schema Support
// PostgreSQL with schema
new UniqueInPivot(
connection: 'pgsql',
mainTable: 'security.addresses', // Schema prefix
pivotTable: 'security.user_addresses',
pivotForeignKey: 'address_id',
pivotOwnerKey: 'user_id',
ownerValue: auth()->id(),
column: 'phone',
)
UniqueInPivotArray
Validates column uniqueness within a many-to-many relationship for bulk (array) operations. Checks both intra-array duplicates and database conflicts.
Constructor Parameters
Main table containing the column to validate
Pivot table that links main table to owner
Foreign key in pivot table pointing to main table
Foreign key in pivot table pointing to owner
Column to check for uniqueness
Root array key in request data (e.g., 'addresses')
Primary key of main table
ignoreField
string|null
default:"null"
Field containing primary key to ignore (for updates: 'id')
Bulk Create Example
use Ronu\RestGenericClass\Core\Rules\UniqueInPivotArray;
// Scenario: User 42 is creating multiple addresses at once
// Rule: Each phone must be unique among user 42's existing addresses
// AND unique within the request itself
public function rules(): array
{
return [
'addresses' => ['required', 'array'],
'addresses.*.phone' => [
'nullable',
'string',
'max:64',
new UniqueInPivotArray(
connection: config('database.default'),
mainTable: 'addresses',
pivotTable: 'user_addresses',
pivotForeignKey: 'address_id',
pivotOwnerKey: 'user_id',
ownerValue: auth()->id(),
column: 'phone',
arrayKey: 'addresses',
// ignoreField omitted - this is a create
),
],
];
}
Request:
{
"addresses": [
{ "phone": "+1-555-8888", "city": "Miami" },
{ "phone": "+1-555-8888", "city": "NYC" }
]
}
Error:
{
"errors": {
"addresses.0.phone": [
"The phone '+1-555-8888' is duplicated in the request at addresses[0]."
],
"addresses.1.phone": [
"The phone '+1-555-8888' is duplicated in the request at addresses[1]."
]
}
}
Bulk Update Example
// Scenario: User 42 is updating multiple addresses
// Each address can keep its own phone, but can't take another address's phone
public function rules(): array
{
return [
'addresses' => ['required', 'array'],
'addresses.*.id' => ['required', 'integer'],
'addresses.*.phone' => [
'nullable',
'string',
'max:64',
new UniqueInPivotArray(
connection: config('database.default'),
mainTable: 'addresses',
pivotTable: 'user_addresses',
pivotForeignKey: 'address_id',
pivotOwnerKey: 'user_id',
ownerValue: auth()->id(),
column: 'phone',
arrayKey: 'addresses',
ignoreField: 'id', // Ignore each item's own ID
),
],
];
}
Valid Request:
{
"addresses": [
{ "id": 7, "phone": "+1-555-0001", "city": "Miami" },
{ "id": 9, "phone": "+1-555-0099", "city": "Chicago" }
]
}
Invalid Request:
{
"addresses": [
{ "id": 7, "phone": "+1-555-0001", "city": "Miami" },
{ "id": 9, "phone": "+1-555-0001", "city": "Chicago" }
]
}
Error:
{
"errors": {
"addresses.0.phone": [
"The phone '+1-555-0001' is duplicated in the request at addresses[0]."
]
}
}
UniqueCompositeInArray
Validates uniqueness of a column in a database table for bulk operations, supporting composite conditions. Simpler than UniqueInPivotArray when pivot tables aren’t involved.
Constructor Parameters
Column to check for uniqueness
Root array key in request data
Additional WHERE conditions (composite key)
ignoreField
string|null
default:"null"
Field containing primary key to ignore (for updates)
Bulk Create Example
use Ronu\RestGenericClass\Core\Rules\UniqueCompositeInArray;
// Scenario: Creating multiple states within a country
// Rule: State name must be unique within that country
public function rules(): array
{
return [
'states' => ['required', 'array'],
'states.*.name' => [
'required',
'string',
'max:255',
new UniqueCompositeInArray(
connection: config('database.default'),
table: 'states',
column: 'name',
arrayKey: 'states',
conditions: [
'country_id' => $this->country_id,
],
),
],
];
}
Request:
{
"country_id": 1,
"states": [
{ "name": "California", "code": "CA" },
{ "name": "Texas", "code": "TX" }
]
}
Bulk Update Example
// Allow updating state names while checking uniqueness
public function rules(): array
{
return [
'states' => ['required', 'array'],
'states.*.id' => ['required', 'integer'],
'states.*.name' => [
'required',
'string',
'max:255',
new UniqueCompositeInArray(
connection: config('database.default'),
table: 'states',
column: 'name',
arrayKey: 'states',
conditions: [
'country_id' => $this->country_id,
],
ignoreField: 'id', // Ignore each state's own ID
),
],
];
}
Multi-Tenant Example
// Ensure product SKUs are unique per organization
public function rules(): array
{
return [
'products' => ['required', 'array'],
'products.*.sku' => [
'required',
'string',
new UniqueCompositeInArray(
connection: config('database.default'),
table: 'products',
column: 'sku',
arrayKey: 'products',
conditions: [
'organization_id' => auth()->user()->organization_id,
'deleted_at' => null,
],
),
],
];
}
ArrayCount
Validates that an array contains between min and max elements, with support for custom messages and token replacement.
Constructor Parameters
Minimum number of items (inclusive)
Maximum number of items (inclusive)
message
string|null
default:"null"
Single custom message for all failure scenarios
Per-scenario messages: onMin, onMax, onBetween, onExact, onNotArray
Basic Usage
use Ronu\RestGenericClass\Core\Rules\ArrayCount;
'tags' => [
'required',
'array',
new ArrayCount(min: 1, max: 10),
],
Custom Global Message
'addresses' => [
'required',
'array',
new ArrayCount(
max: 1,
message: 'Only one address is allowed.'
),
],
Per-Scenario Messages
'attendees' => [
'required',
'array',
new ArrayCount(
min: 2,
max: 50,
messages: [
'onMin' => 'Please add at least :min attendees.',
'onMax' => 'You can add at most :max attendees.',
'onBetween' => 'Number of attendees must be between :min and :max.',
]
),
],
Token Replacement
Available tokens:
:attribute - Field name (e.g., “addresses”)
:min - Minimum value
:max - Maximum value
:count - Actual count provided
'items' => [
'required',
'array',
new ArrayCount(
min: 1,
max: 100,
messages: [
'onMax' => 'You provided :count items, but the maximum is :max.',
]
),
],
Exact Count
'options' => [
'required',
'array',
new ArrayCount(
min: 3,
max: 3, // Exactly 3 items
messages: [
'onExact' => 'Exactly :min options are required.',
]
),
],
Only Minimum
'categories' => [
'required',
'array',
new ArrayCount(min: 1),
],
Only Maximum
'attachments' => [
'required',
'array',
new ArrayCount(max: 5),
],
Complete Example
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Ronu\RestGenericClass\Core\Rules\UniqueInPivotArray;
use Ronu\RestGenericClass\Core\Rules\UniqueCompositeInArray;
use Ronu\RestGenericClass\Core\Rules\ArrayCount;
use Ronu\RestGenericClass\Core\Rules\IdsExistNotDelete;
class BulkUpdateUserAddressesRequest extends FormRequest
{
protected string $connection;
public function __construct()
{
parent::__construct();
$this->connection = config('database.default');
}
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
// Array size validation
'addresses' => [
'required',
'array',
new ArrayCount(
min: 1,
max: 10,
messages: [
'onMin' => 'At least one address is required.',
'onMax' => 'You can update a maximum of 10 addresses at once.',
]
),
],
// Address IDs must exist and not be deleted
'addresses.*.id' => [
'required',
'integer',
new IdsExistNotDelete(
connection: $this->connection,
table: 'addresses',
column: 'id'
),
],
// Phone must be unique within user's addresses (via pivot)
'addresses.*.phone' => [
'nullable',
'string',
'max:64',
new UniqueInPivotArray(
connection: $this->connection,
mainTable: 'addresses',
pivotTable: 'user_addresses',
pivotForeignKey: 'address_id',
pivotOwnerKey: 'user_id',
ownerValue: auth()->id(),
column: 'phone',
arrayKey: 'addresses',
ignoreField: 'id'
),
],
// Label must be unique for this user (direct table check)
'addresses.*.label' => [
'required',
'string',
'max:50',
new UniqueCompositeInArray(
connection: $this->connection,
table: 'addresses',
column: 'label',
arrayKey: 'addresses',
conditions: [
'user_id' => auth()->id(),
],
ignoreField: 'id'
),
],
];
}
}
See Also