Skip to main content

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

connection
string
required
Database connection name
mainTable
string
required
Main table containing the column to validate (e.g., 'addresses')
pivotTable
string
required
Pivot table that links main table to owner (e.g., 'user_addresses')
pivotForeignKey
string
required
Foreign key in pivot table pointing to main table (e.g., 'address_id')
pivotOwnerKey
string
required
Foreign key in pivot table pointing to owner (e.g., 'user_id')
ownerValue
mixed
required
Owner ID value (e.g., auth()->id())
column
string
required
Column in main table to check for uniqueness (e.g., 'phone')
mainTablePk
string
default:"id"
Primary key of main table
ignoreValue
mixed
default:"null"
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

connection
string
required
Database connection name
mainTable
string
required
Main table containing the column to validate
pivotTable
string
required
Pivot table that links main table to owner
pivotForeignKey
string
required
Foreign key in pivot table pointing to main table
pivotOwnerKey
string
required
Foreign key in pivot table pointing to owner
ownerValue
mixed
required
Owner ID value
column
string
required
Column to check for uniqueness
arrayKey
string
required
Root array key in request data (e.g., 'addresses')
mainTablePk
string
default:"id"
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

connection
string
required
Database connection name
table
string
required
Table name to check
column
string
required
Column to check for uniqueness
arrayKey
string
required
Root array key in request data
conditions
array
default:"[]"
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

min
int|null
default:"null"
Minimum number of items (inclusive)
max
int|null
default:"null"
Maximum number of items (inclusive)
message
string|null
default:"null"
Single custom message for all failure scenarios
messages
array
default:"[]"
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

Build docs developers (and LLMs) love