craftcms/laravel-ruleset-validation
Validate Laravel request data against Craft CMS field rulesets. Map Craft-style constraints (required, min/max, regex, etc.) into Laravel’s validator, keeping validation logic consistent between Craft and Laravel apps.
Install the package:
composer require craftcms/laravel-ruleset-validation
No service provider registration needed (auto-discovery).
First use case: Validate a DTO Create a data object with validation rules:
// app/Data/CreateUserData.php
use CraftCms\RulesetValidation\Attributes\Ruleset;
use CraftCms\RulesetValidation\Concerns\HasRuleset;
use CraftCms\RulesetValidation\Contracts\ValidatesWithRuleset;
#[Ruleset(CreateUserRuleset::class)]
class CreateUserData implements ValidatesWithRuleset
{
use HasRuleset;
public function __construct(
public string $name,
public string $email,
) {}
public function validationData(): array
{
return [
'name' => $this->name,
'email' => $this->email,
];
}
}
Define the ruleset:
// app/Rulesets/CreateUserRuleset.php
use CraftCms\RulesetValidation\Ruleset;
class CreateUserRuleset extends Ruleset
{
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email'],
];
}
}
Validate in a controller:
public function store(CreateUserData $data)
{
$validated = $data->ruleset->validate();
// $validated contains ['name' => '...', 'email' => '...']
}
ValidatesWithRuleset, HasRuleset, and #[Ruleset].FormRequest but without subclassing.Rule::when() and scenarios (useScenario()).getValidator() to inspect errors.// 1. Define data object
class UpdateProfileData implements ValidatesWithRuleset
{
use HasRuleset;
public function __construct(
public ?string $name,
public ?string $email,
) {}
public function validationData(): array
{
return [
'name' => $this->name,
'email' => $this->email,
];
}
}
// 2. Attach ruleset (via attribute or method)
#[Ruleset(UpdateProfileRuleset::class)]
class UpdateProfileData { ... }
// 3. Validate in controller/service
public function update(UpdateProfileData $data)
{
$validated = $data->ruleset->validate();
// Use $validated['name'] or $validated['email']
}
// 1. Define ruleset
class StorePostRuleset extends Ruleset
{
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'body' => ['nullable', 'string'],
];
}
}
// 2. Inject into controller (Laravel resolves it)
public function store(StorePostRuleset $ruleset)
{
$validated = $ruleset->validate();
// $validated is the cleaned input
}
class UserRuleset extends Ruleset
{
public const SCENARIO_ADMIN = 'admin';
public function rules(): array
{
return [
'email' => [
'required',
Rule::unique('users')->ignore($this->subject->id),
Rule::when(
$this->inScenarios(self::SCENARIO_ADMIN),
fn() => Rule::exists('admin_users')
),
],
];
}
}
// Usage
$userRuleset = new UserRuleset($user);
$validated = $userRuleset
->useScenario(UserRuleset::SCENARIO_ADMIN)
->validate();
// Validate only 'email' field
$emailValidated = $data->ruleset
->only(['email'])
->validate();
// Non-throwing validation
if ($data->ruleset->only(['email'])->fails()) {
$errors = $data->ruleset->getValidator()->errors();
}
authorize() in Ruleset:
public function authorize(): bool
{
return auth()->check();
}
messages():
public function messages(): array
{
return [
'email.required' => 'We need your email to proceed.',
];
}
#[StopOnFirstFailure] or #[RedirectTo] attributes (same as FormRequest).Ruleset Resolution Conflicts
#[Ruleset] attribute and a ruleset() method, the method takes precedence (fixed in v1.0.1).Scenario State Leaks
useScenario() persists across method calls. Use withScenario() for temporary changes:
$validated = $ruleset->withScenario(
'admin',
fn() => $ruleset->validate()
);
Request vs. Object Validation Quirks
subject is the Request object.subject is the validated object (e.g., CreateUserData).$this->subject in rules:
'email' => [
Rule::unique('users')->ignore($this->subject->id),
],
Validation Data Mismatch
validationData() returns keys that match your rules() array. Typos here cause silent failures.$ruleset->getValidator()->validate() to inspect raw data.Serialization Issues
$validator = $data->ruleset->getValidator();
dd($validator->errors()); // Debug errors
dd($validator->validated()); // Debug validated data
$validator = $data->ruleset->validator();
if ($validator->fails()) {
// Handle errors
}
$currentScenario = $data->ruleset->getScenario();
Custom Validation Logic
Override prepareForValidation() to modify data before validation:
public function prepareForValidation(): void
{
$this->data['full_name'] = strtoupper($this->data['name']);
}
Post-Validation Hooks
Use after() (like FormRequest):
protected function after(): void
{
$this->data['processed_at'] = now();
}
Conditional Rules
Leverage Rule::when() or scenarios for dynamic validation:
'password' => [
'required_if' => 'is_new_user',
Rule::when(
fn() => $this->inScenarios('admin'),
fn() => Rule::min(12)
),
],
Testing Mock rulesets in tests:
$ruleset = $this->createMock(Ruleset::class);
$ruleset->method('validate')->willReturn(['key' => 'value']);
$this->app->instance(Ruleset::class, $ruleset);
only() multiple times creates new validator instances.passes()/fails() instead of validate() to avoid throwing exceptions.#[Ruleset] attributes are parsed by Laravel (works out-of-the-box in Laravel 8+).App\Rulesets\) to avoid conflicts.How can I help you explore Laravel packages today?