kumaravelk011/model-validation
A Laravel package for validating Eloquent models with rules defined alongside the model. Keep validation close to your data layer, reuse Laravel’s validator, and catch invalid attributes before saving or updating records.
This release is a hard modernization: PHP 8.2+, Laravel 12 or 13 only (Laravel 11 and earlier support removed). If your app is still on Laravel 11, do not upgrade the package until the framework is bumped — Composer will resolve against the individual illuminate/* components now, so you may hit conflicts with a pinned laravel/framework constraint before you even run tests.
Minimal setup:
composer require theriddleofenigma/laravel-model-validation (v2.x).ValidatorTrait to any Eloquent model you want validated on persist.validationRules on the model. Optionally validationMessages and validationAttributes.saving event, so create(), update(), save(), and firstOrCreate() paths all validate before writing.First use case to reach for: invariant enforcement on models that are written from multiple entry points (controllers, jobs, console commands, imports). Instead of duplicating a FormRequest, put the rules on the model and let every write path go through the same gate.
Where to look first in the source:
ValidatorTrait — the event wiring, the hooks, and the skip-validation state machine.ModelValidator — makeValidator() and the validate() return change.modelValidator() — the new per-model accessor; this is your debugging entry point.Rules as the single source of truth. Define validationRules on the model and reuse them at the HTTP layer via the new accessor instead of redeclaring rules in a FormRequest:
$validator = $this->model->modelValidator(); // or ModelValidator::makeValidator($model)
This is the headline ergonomic win of v2 — one rule set, used both on save and in request validation, so they cannot drift.
Capture validated data. validate() now returns the validated array rather than a boolean/void. Use it where you want the sanitized subset (e.g. building a payload for an API response or a downstream job):
$data = $order->validate(); // only keys covered by validationRules
Skipping validation — three distinct scopes, pick deliberately:
| Need | Use | Notes |
|---|---|---|
| One model instance, many saves | $model->skipValidation() |
Fluent; skipValidation(false) re-enables. |
| A single save | $model->saveWithoutValidation() |
Restores the prior state after the call. |
| A block (seeder, import, migration, job) | Model::withoutValidation(fn () => ...) |
Restored via finally, so safe against exceptions. |
Typical real-world uses: seeding legacy data, importing from a third-party system that violates current rules, an admin "force save" action, and one-off php artisan tinker repairs.
Per-event control. validateOnSaving() / validateOnCreating() / validateOnUpdating() let you narrow when validation fires. The new validateOnUpdating() helper is the natural fit for models where creation rules are lax (drafts) but updates are strict (published records).
Data shaping before validation. Implement validationData() to control exactly which attributes are handed to the validator — useful when rules should apply only to dirty attributes, or when you need to inject a computed value. Chain beforeValidation / afterValidation for normalization and side effects.
Update-aware unique rules. For unique rules you own the "ignore self" problem. The clean pattern is a closure rule or a validationData() that includes the model's key, since the package deliberately does not rewrite your rules for you.
Testing. The package's own suite is PHPUnit + Orchestra Testbench (20 tests) across PHP 8.2–8.4 × Laravel 12–13. Mirror that in your app: use RefreshDatabase, and assert on Illuminate\Validation\ValidationException for the failure paths. Test the skip paths too — a silently-skipped validation is the most expensive bug in this category.
validate() return type changed. If you wrote if ($model->validate()) { ... } in v1, it now returns an array (always truthy for non-empty data). Audit every direct call — this will fail silently rather than loudly if you don't have strict static analysis.
saveQuietly() and withoutEvents() bypass validation entirely. Validation rides on the saving event, so these escape hatches skip it without any of the intent that skipValidation() conveys. That's a footgun in code review: prefer withoutValidation() over saveQuietly() when the goal is specifically "skip validation," because it documents intent and restores state.
Skip state is instance-level and sticky. skipValidation() mutates the instance. In long-running workers (queues, Octane), a model instance that survives a request can carry the flag into the next job. withoutValidation() and saveWithoutValidation() restore state, but hand-rolled skipValidation() / skipValidation(false) pairs need a try/finally of your own.
Casts vs raw attributes. Rules run against the model's attributes, not your cast values. datetime, array/json, and enum casts can therefore fail rules that look correct on paper. Normalize in validationData() rather than loosening the rules.
Mass assignment ordering. Validation fires on saving, after fill(). Attributes excluded by $fillable are not present for validation — if a rule is silently "passing" on a required field, check $fillable and $guarded before you check the rule.
Composer footprint changed. The package now requires individual illuminate/* components rather than laravel/framework. In a normal Laravel app this is invisible; in a package, a lumen-style microservice, or a monorepo with pinned framework versions, it can produce surprising resolutions. Pin the package version if your dependency graph is sensitive.
Debugging a failed save. ValidationException is thrown from the saving event, so stack traces point at Eloquent internals, not your call site. In Laravel 12/13 it still converts to a 422 for HTTP requests. When the message is unclear, reach for $model->modelValidator()->errors() in a test or Tinker to inspect the validator state directly.
Extension points are the trait's public surface, not subclassing. validationData, beforeValidation, afterValidation, validationRules, validationMessages, validationAttributes, and modelValidator() are the supported seams. Overriding the trait's internals is not — strict types and typed signatures mean a signature mismatch is now a fatal error, not a warning.
Strict types and Pint. v2 is declare(strict_types=1) throughout and ships Pint checks in CI. If you extend ModelValidator or a hook with loose signatures (return; where array is expected, implicit int/string coercion), you will get TypeErrors that v1 tolerated. Match the typed signatures from the source exactly.
How can I help you explore Laravel packages today?