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.
Installation:
composer require craftcms/laravel-ruleset-validation
No service provider registration needed (auto-discovery).
First Use Case: Validate a DTO
ValidatesWithRuleset:
use CraftCms\RulesetValidation\Concerns\HasRuleset;
use CraftCms\RulesetValidation\Contracts\ValidatesWithRuleset;
use CraftCms\RulesetValidation\Attributes\Ruleset;
#[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,
];
}
}
use CraftCms\RulesetValidation\Ruleset;
class CreateUserRuleset extends Ruleset {
public function rules(): array {
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email'],
];
}
}
$data = new CreateUserData('John', 'john@example.com');
$validated = $data->ruleset->validate();
First Use Case: Validate a Request
use App\Rulesets\StoreUserRuleset;
class UserController {
public function store(StoreUserRuleset $ruleset) {
$validated = $ruleset->validate();
// ...
}
}
Ruleset class (core logic).HasRuleset trait (for object-backed validation).#[Ruleset] attribute (declarative ruleset binding).ValidatesWithRuleset + #[Ruleset] for reusable validation logic tied to business objects.// Define ruleset
class UpdateProfileRuleset extends Ruleset {
public function rules(): array {
return [
'name' => ['sometimes', 'string', 'max:255'],
'email' => ['sometimes', 'email'],
];
}
}
// Attach to DTO
#[Ruleset(UpdateProfileRuleset::class)]
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,
];
}
}
// Validate
$data = new UpdateProfileData('Jane', 'jane@example.com');
$validated = $data->ruleset->validate();
User, Order, Post).FormRequest)FormRequest boilerplate.use App\Rulesets\CreatePostRuleset;
class PostController {
public function store(CreatePostRuleset $ruleset) {
$validated = $ruleset->validate();
// ...
}
}
FormRequest class.class PostRuleset extends Ruleset {
public const SCENARIO_PUBLIC = 'public';
public const SCENARIO_ADMIN = 'admin';
public function rules(): array {
return [
'title' => [
Rule::requiredIf($this->inScenarios(self::SCENARIO_ADMIN)),
'string',
'max:255',
],
];
}
}
// Usage
$ruleset = new PostRuleset(subject: $request);
$validated = $ruleset->useScenario(PostRuleset::SCENARIO_ADMIN)->validate();
class UserRuleset extends Ruleset {
public function rules(): array { /* ... */ }
}
class PaymentRuleset extends Ruleset {
public function rules(): array { /* ... */ }
}
class CheckoutRuleset extends Ruleset {
public function rules(): array {
return [
'user' => [new UserRuleset],
'payment' => [new PaymentRuleset],
];
}
}
Leverage Laravel’s Validation Pipeline:
FormRequest features: authorize(), messages(), attributes(), prepareForValidation(), etc.class StorePostRuleset extends Ruleset {
public function authorize(): bool {
return auth()->check();
}
public function messages(): array {
return [
'title.required' => 'A title is required!',
];
}
}
Use with API Resources:
$data = new CreatePostData($request->all());
$validated = $data->ruleset->validate();
$post = new PostResource($validated);
Combine with Form Requests:
FormRequest for request-specific behavior:
class StorePostRequest extends FormRequest {
public function rules(): array {
return (new StorePostRuleset($this))->rules();
}
}
Testing:
$ruleset = $this->createMock(Ruleset::class);
$ruleset->method('validate')->willReturn(['validated' => 'data']);
$this->assertEquals(['validated' => 'data'], $data->ruleset->validate());
$ruleset->useScenario(PostRuleset::SCENARIO_ADMIN);
$this->assertTrue($ruleset->inScenarios(PostRuleset::SCENARIO_ADMIN));
Performance:
Ruleset Resolution Conflicts:
#[Ruleset] and a ruleset() method, the attribute takes precedence. Override in ruleset() if dynamic resolution is needed:
public function ruleset(): string {
return $this->isAdmin ? AdminRuleset::class : UserRuleset::class;
}
Scenario State Leaks:
useScenario() is sticky. Reset with withScenario() for one-off operations:
$validated = $ruleset->withScenario(
PostRuleset::SCENARIO_DRAFT,
fn () => $ruleset->validate()
);
Request vs. Object Validation Quirks:
$this->input is available in rules(), but for objects, use validationData().public function rules(): array {
return [
'email' => ['required', 'email'],
'user_id' => Rule::exists('users')->where('active', true),
];
}
Serialization Issues:
Attribute Caching:
#[Ruleset] attribute is resolved once during class initialization. For dynamic rulesets, useHow can I help you explore Laravel packages today?