Skip to main content

CoreFormRequest

This document covers the CoreFormRequest base class, which provides automatic sometimes validation rule handling for partial updates (PATCH/PUT requests).

Overview

When building update endpoints in Laravel, you often need to support partial updates where only a subset of fields are sent. This requires adding sometimes to validation rules for non-nullable fields. CoreFormRequest automates this pattern, eliminating boilerplate code and making FormRequest classes more maintainable.

The Problem

Without CoreFormRequest, update request classes require manual sometimes declarations:
class UpdateUserRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name' => ['sometimes', 'required', 'string', 'max:255'],
            'email' => ['nullable', 'email'], // nullable fields don't need 'sometimes'
            'age' => ['sometimes', 'required', 'integer', 'min:18'],
        ];
    }
}
This becomes repetitive across many update request classes and is easy to forget.

The Solution

With CoreFormRequest, you define clean base rules and the class automatically handles sometimes:
use Inly\Core\Http\Requests\CoreFormRequest;

class UpdateUserRequest extends CoreFormRequest
{
    protected function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['nullable', 'email'],
            'age' => ['required', 'integer', 'min:18'],
        ];
    }
}
For PATCH/PUT requests, CoreFormRequest automatically:
  • Prepends sometimes to name and age (non-nullable fields)
  • Leaves email unchanged (nullable fields don’t need sometimes)
  • For POST requests, returns the rules as-is (full validation)

Basic Usage

Simple Update Request

<?php

namespace App\Http\Requests;

use Inly\Core\Http\Requests\CoreFormRequest;

class UpdateCampaignRequest extends CoreFormRequest
{
    protected function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'company_id' => ['required', 'integer', 'exists:companies,id'],
            'description' => ['nullable', 'string'],
            'active' => ['required', 'boolean'],
        ];
    }
}
When this request receives a PATCH request with only name:
{
    "name": "Updated Campaign"
}
The validation rules become:
[
    'name' => ['sometimes', 'required', 'string', 'max:255'],
    'company_id' => ['sometimes', 'required', 'integer', 'exists:companies,id'],
    'description' => ['nullable', 'string'], // unchanged - already nullable
    'active' => ['sometimes', 'required', 'boolean'],
]

With Route Model Binding

You can use Laravel’s dependency injection features with route parameters. The rules() method supports variadic parameters, allowing you to inject route-bound models:
use App\Models\Company;
use Illuminate\Container\Attributes\RouteParameter;
use Inly\Core\Http\Requests\CoreFormRequest;

class UpdateCompanyRequest extends CoreFormRequest
{
    protected function rules(
        #[RouteParameter('company', Company::class)] Company $company
    ): array {
        return [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['nullable', 'email', 'unique:companies,email,' . $company->id],
            'active' => ['required', 'boolean'],
        ];
    }
}
The method signature is flexible - you can inject multiple route parameters or use other dependency injection:
protected function rules(
    #[RouteParameter('user')] User $user,
    #[RouteParameter('company')] Company $company
): array {
    // Rules using both $user and $company
}

With Custom Authorization

Authorization works the same as standard FormRequest:
use Inly\Core\Http\Requests\CoreFormRequest;

class UpdateCampaignRequest extends CoreFormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('update', $this->route('campaign'));
    }

    protected function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'status' => ['required', 'string', 'in:draft,published'],
        ];
    }
}

Advanced Features

Nested Array Rules

CoreFormRequest correctly handles nested array validation rules:
protected function rules(): array
{
    return [
        'tags' => ['nullable', 'array'],
        'tags.*' => ['required', 'string'], // Gets 'sometimes' prepended
        'settings' => ['required', 'array'], // Gets 'sometimes' prepended
        'settings.*.key' => ['required', 'string'],
        'settings.*.value' => ['nullable', 'string'],
    ];
}

Preventing Automatic sometimes

If you need to override the automatic behavior for specific fields, explicitly add sometimes:
protected function rules(): array
{
    return [
        'name' => ['required', 'string'],
        'email' => ['sometimes', 'required', 'email'], // Won't duplicate 'sometimes'
    ];
}

Mixed Validation Rules

Works with all Laravel validation rule types:
use Illuminate\Validation\Rule;
use App\Enums\AccountType;

protected function rules(): array
{
    return [
        'type' => [Rule::enum(AccountType::class)],
        'status' => ['required', 'string', Rule::in(['active', 'inactive'])],
        'notes' => ['nullable', 'string'],
    ];
}

Custom Request Methods Validation

Override isPartialUpdate() if you have custom logic for determining partial updates:
protected function isPartialUpdate(): bool
{
    // Custom logic - e.g., check a specific header
    return $this->header('X-Partial-Update') === 'true'
        || in_array($this->method(), ['PATCH', 'PUT']);
}

How It Works

Request Flow

  1. Request arrives → Laravel routes to controller
  2. FormRequest resolves → Laravel instantiates your CoreFormRequest subclass
  3. validationRules() called → CoreFormRequest intercepts before validation
  4. Method check → Determines if PATCH/PUT (partial update)
  5. Rule transformation:
    • For PATCH/PUT: Prepends sometimes to non-nullable fields
    • For POST: Returns rules unchanged
  6. Validation runs → Laravel validator processes the transformed rules

Under the Hood

CoreFormRequest overrides the protected validationRules() method from Laravel’s base FormRequest class. This method is called internally by Laravel during validation, giving us the perfect hook to modify rules before they’re processed.
// Laravel calls this internally
protected function validationRules(): array
{
    // Get rules from your rules() method
    $rules = parent::validationRules();

    // For PATCH/PUT, prepend 'sometimes' to non-nullable fields
    if ($this->isPartialUpdate()) {
        return $this->prependSometimesToNonNullableRules($rules);
    }

    return $rules;
}
This approach is clean because:
  • ✅ You use the standard rules() method
  • ✅ Laravel’s dependency injection still works
  • ✅ No abstract methods to remember
  • ✅ Automatic, transparent modification

Rule Analysis Logic

For each field, CoreFormRequest checks:
  1. Already has sometimes? → Skip (no duplication)
  2. Is nullable? → Skip (nullable fields don’t need sometimes)
  3. Otherwise → Prepend sometimes to the rules array

String vs Array Rules

The class handles both formats:
// Array format (recommended)
'email' => ['required', 'email']

// String format (also supported)
'email' => 'required|email'
String rules are converted to arrays before sometimes is prepended.

Best Practices

When to Use CoreFormRequest

Use for:
  • Update (PATCH/PUT) endpoints with partial update support
  • Resources where not all fields are required in updates
  • Reducing boilerplate in Update request classes
Don’t use for:
  • Store (POST) endpoints that always require full validation
  • Requests where you need complete control over sometimes logic
  • Simple requests with only nullable fields

Migration Strategy

When migrating existing Update requests:
  1. Change parent class from FormRequest to CoreFormRequest
  2. Rename rules() to rules()
  3. Remove manual sometimes from non-nullable fields
  4. Keep nullable fields unchanged
  5. Test with partial updates
Before:
class UpdateUserRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name' => ['sometimes', 'required', 'string'],
            'email' => ['nullable', 'email'],
        ];
    }
}
After:
class UpdateUserRequest extends CoreFormRequest
{
    protected function rules(): array
    {
        return [
            'name' => ['required', 'string'],
            'email' => ['nullable', 'email'],
        ];
    }
}

Testing

Test your CoreFormRequest subclasses to ensure proper validation:
it('validates partial updates', function () {
    $campaign = Campaign::factory()->create();
    $user = User::factory()->admin()->create();

    actingAs($user)
        ->patch(route('campaigns.update', $campaign), [
            'name' => 'Updated Name', // Only updating name
        ])
        ->assertSessionHasNoErrors();

    expect($campaign->fresh()->name)->toBe('Updated Name');
});

it('requires valid data when present', function () {
    $campaign = Campaign::factory()->create();
    $user = User::factory()->admin()->create();

    actingAs($user)
        ->patch(route('campaigns.update', $campaign), [
            'name' => '', // Invalid when present
        ])
        ->assertSessionHasErrors(['name']);
});

Common Patterns

Update with Conditional Rules

protected function rules(): array
{
    $rules = [
        'name' => ['required', 'string', 'max:255'],
        'status' => ['required', 'string'],
    ];

    // Add conditional rules based on other input
    if ($this->input('status') === 'published') {
        $rules['published_at'] = ['required', 'date'];
    }

    return $rules;
}

Update with Dynamic Unique Rules

use Illuminate\Validation\Rule;

protected function rules(): array
{
    $userId = $this->route('user')->id;

    return [
        'name' => ['required', 'string', 'max:255'],
        'email' => [
            'required',
            'email',
            Rule::unique('users', 'email')->ignore($userId),
        ],
    ];
}

Update with Custom Messages

protected function rules(): array
{
    return [
        'name' => ['required', 'string', 'max:255'],
        'email' => ['required', 'email'],
    ];
}

public function messages(): array
{
    return [
        'name.required' => __('Please provide a name'),
        'email.email' => __('Please provide a valid email address'),
    ];
}

API Reference

Abstract Methods

rules()

Define your base validation rules. This method replaces the standard rules() method.
abstract protected function rules(mixed ...$params): array;
Parameters:
  • $params - Variadic parameters for dependency injection (e.g., route-bound models)
Returns: Array of validation rules Example:
// Simple usage
protected function rules(): array
{
    return ['name' => ['required', 'string']];
}

// With route parameter injection
protected function rules(
    #[RouteParameter('user')] User $user
): array {
    return [
        'email' => ['required', 'email', 'unique:users,email,' . $user->id],
    ];
}

Protected Methods

isPartialUpdate()

Determines if the request is a partial update (PATCH/PUT).
protected function isPartialUpdate(): bool
Returns: true for PATCH/PUT requests, false otherwise Override: You can override this method for custom partial update detection logic.

prependSometimesToNonNullableRules()

Processes rules array and prepends sometimes to non-nullable fields.
protected function prependSometimesToNonNullableRules(array $rules): array
Parameters:
  • $rules - The base rules array
Returns: Transformed rules array with sometimes prepended

hasSometimes()

Checks if a field’s rules already contain sometimes.
protected function hasSometimes(array|string $rules): bool

isNullable()

Checks if a field’s rules contain nullable.
protected function isNullable(array|string $rules): bool

prependSometimes()

Prepends sometimes to a rules array.
protected function prependSometimes(array|string $rules): array

Examples

Complete CRUD Request Setup

// Store request - doesn't need CoreFormRequest
class StoreCampaignRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'company_id' => ['required', 'exists:companies,id'],
            'description' => ['nullable', 'string'],
        ];
    }
}

// Update request - uses CoreFormRequest for automatic 'sometimes'
class UpdateCampaignRequest extends CoreFormRequest
{
    protected function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'company_id' => ['required', 'exists:companies,id'],
            'description' => ['nullable', 'string'],
        ];
    }
}

Controller Usage

class CampaignController extends Controller
{
    public function update(UpdateCampaignRequest $request, Campaign $campaign)
    {
        // Validated data automatically has 'sometimes' applied for partial updates
        $campaign->update($request->validated());

        return redirect()
            ->route('campaigns.show', $campaign)
            ->withSuccess(__('Campaign updated successfully'));
    }
}

Route Definition

Route::patch('/campaigns/{campaign}', [CampaignController::class, 'update'])
    ->name('campaigns.update');

Troubleshooting

”Field is required” on partial updates

If you’re getting validation errors for fields not included in the request:
  1. Ensure your request is using PATCH or PUT method (not POST)
  2. Verify the field isn’t marked as nullable when it shouldn’t be
  3. Check that you’re extending CoreFormRequest and implementing rules() (not rules())

Rules not being transformed

If sometimes isn’t being added:
  1. Verify the request method is PATCH or PUT
  2. Check if the field is marked as nullable (nullable fields don’t get sometimes)
  3. Ensure you haven’t overridden rules() method directly

Duplicate sometimes rules

This shouldn’t happen as the class checks for existing sometimes, but if it does:
  1. Remove manual sometimes from your rules() - let CoreFormRequest handle it
  2. Check if you’ve overridden prependSometimesToNonNullableRules() incorrectly