Skip to main content

Introduction

Rest Generic Class provides a comprehensive set of custom validation rules designed for complex database validation scenarios commonly found in REST APIs. These rules extend Laravel’s built-in validation with specialized functionality for:
  • Database existence checks with complex conditions
  • Pivot table uniqueness validation
  • Bulk/array operation validation
  • Composite uniqueness constraints
  • Array size validation with custom messages

Available Rules

Existence Validation

Rules for validating that IDs exist in database tables with various conditions:

IdsExistInTable

Validate that IDs exist in a specific table

IdsExistNotDelete

Validate IDs exist and are not soft-deleted

IdsExistWithAnyStatus

Validate IDs exist with specific status values

IdsExistWithDateRange

Validate IDs exist within a date range

IdsWithCustomQuery

Validate IDs with a custom query callback

Uniqueness Validation

Rules for ensuring data uniqueness in various contexts:

UniqueInPivot

Validate uniqueness in many-to-many relationships via pivot tables

UniqueInPivotArray

Bulk validation of pivot table uniqueness

UniqueCompositeInArray

Validate composite uniqueness for bulk operations

ArrayCount

Validate array element count with custom messages

Common Patterns

Database Connection

All database validation rules accept a connection parameter:
use Ronu\RestGenericClass\Core\Rules\IdsExistInTable;

'user_ids' => [
    'required',
    'array',
    new IdsExistInTable(
        connection: 'mysql',  // or config('database.default')
        table: 'users',
        column: 'id'
    ),
],

Array vs Single Value

Most rules automatically handle both single values and arrays:
// Single ID
'category_id' => ['required', new IdsExistInTable('mysql', 'categories')],

// Array of IDs
'category_ids' => ['required', 'array', new IdsExistInTable('mysql', 'categories')],

Additional Conditions

Many rules support additional WHERE conditions:
use Ronu\RestGenericClass\Core\Rules\IdsExistInTable;

'product_ids' => [
    'required',
    'array',
    new IdsExistInTable(
        connection: 'mysql',
        table: 'products',
        column: 'id',
        additionalConditions: [
            'is_active' => true,
            'deleted_at' => null,
        ]
    ),
],

Usage in FormRequests

Basic Example

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Ronu\RestGenericClass\Core\Rules\IdsExistInTable;
use Ronu\RestGenericClass\Core\Rules\UniqueInPivot;

class AssignRolesRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'user_id' => [
                'required',
                'integer',
                new IdsExistInTable(
                    connection: config('database.default'),
                    table: 'users',
                    column: 'id'
                ),
            ],
            'role_ids' => [
                'required',
                'array',
                'min:1',
                new IdsExistInTable(
                    connection: config('database.default'),
                    table: 'roles',
                    column: 'id'
                ),
            ],
        ];
    }
}

Advanced Example with Multiple Rules

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Ronu\RestGenericClass\Core\Rules\IdsExistNotDelete;
use Ronu\RestGenericClass\Core\Rules\UniqueInPivotArray;
use Ronu\RestGenericClass\Core\Rules\ArrayCount;

class BulkUpdateAddressesRequest extends FormRequest
{
    protected string $connection;

    public function __construct()
    {
        parent::__construct();
        $this->connection = config('database.default');
    }

    public function rules(): array
    {
        return [
            'addresses' => [
                'required',
                'array',
                new ArrayCount(
                    min: 1,
                    max: 10,
                    messages: [
                        'onMin' => 'Please provide at least one address.',
                        'onMax' => 'You can update a maximum of 10 addresses at once.',
                    ]
                ),
            ],
            'addresses.*.id' => [
                'required',
                'integer',
                new IdsExistNotDelete(
                    connection: $this->connection,
                    table: 'addresses',
                    column: 'id'
                ),
            ],
            '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'
                ),
            ],
        ];
    }
}

Error Messages

All rules provide clear, actionable error messages:
{
  "message": "The given data was invalid.",
  "errors": {
    "user_ids": [
      "The following IDs do not exist: 42, 99"
    ],
    "addresses.0.phone": [
      "The phone '+1-555-0001' at addresses[0] has already been taken."
    ]
  }
}

Performance Considerations

All validation rules use efficient database queries with proper indexing support. Batch validations use single queries with whereIn clauses rather than multiple individual queries.

Efficient Bulk Validation

// BAD: Multiple queries
foreach ($ids as $id) {
    // Laravel's built-in exists rule queries once per ID
    'id' => 'exists:users,id'
}

// GOOD: Single query
'ids' => [
    'array',
    new IdsExistInTable('mysql', 'users', 'id')
]
// Executes: SELECT id FROM users WHERE id IN (1, 2, 3, ...)

Testing

Example test for custom validation rules:
namespace Tests\Feature\Validation;

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Models\User;
use App\Models\Role;

class RoleAssignmentValidationTest extends TestCase
{
    use RefreshDatabase;

    public function test_validates_existing_user_ids()
    {
        $response = $this->postJson('/api/users/123/roles', [
            'role_ids' => [999], // Non-existent role
        ]);

        $response->assertStatus(422)
            ->assertJsonValidationErrors(['role_ids']);
    }

    public function test_accepts_valid_role_ids()
    {
        $user = User::factory()->create();
        $role = Role::factory()->create();

        $response = $this->postJson("/api/users/{$user->id}/roles", [
            'role_ids' => [$role->id],
        ]);

        $response->assertStatus(200);
    }
}

Next Steps

ID Existence Rules

Learn about validating ID existence with various conditions

Uniqueness Rules

Explore pivot table and composite uniqueness validation

Build docs developers (and LLMs) love