Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Model Validation Laravel Package

theriddleofenigma/laravel-model-validation

Declare validation rules directly on your Eloquent models and have them run automatically on the events you choose, such as saving, creating, or updating. Invalid saves throw Laravel's ValidationException, so no form requests or repeated validator calls are needed.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install — composer require theriddleofenigma/laravel-model-validation. No service provider registration, migrations, or config publishing is required; Laravel package discovery handles it. Check composer.json first: the package targets PHP 8.2+ and Laravel 12/13 only, so it will not slot into a legacy L9/L10 app.

  2. Opt a model in by adding the trait and declaring rules where the data lives:

namespace App\Models;

use Enigma\ValidatorTrait;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use ValidatorTrait;

    protected $fillable = ['title', 'slug', 'body', 'published_at'];

    protected $rules = [
        'title' => ['required', 'string', 'max:255'],
        'slug'  => ['required', 'alpha_dash', 'max:255'],
        'body'  => ['required', 'string'],
    ];

    // the model event that triggers validation
    protected $validationEvent = 'saving';
}
  1. First use case — a normal controller store action:
public function store(StorePostRequest $request)
{
    return Post::create($request->validated()); // validated twice, by design
}

The payoff isn't the controller; it's that every other write path now gets the same rules for free — an artisan import command, a queued job, a seeder, a Nova/Livewire form, or a console one-liner.

  1. Where to look first — read the trait source itself (vendor/theriddleofenigma/laravel-model-validation/src/). It is small, and it is the authoritative list of which properties ($rules, $validationEvent, message/attribute overrides) and hooks actually exist, plus which event fires by default. The README's quick start shows the happy path; the trait shows the escape hatches you'll need in week two.

Implementation Patterns

Split create vs. update rule sets. The common case is unique rules that must ignore the row being updated:

protected $rules = [
    'email' => ['required', 'email'],
];

// in a resolver method / per-event rule set, for updates:
'email' => [Rule::unique('users')->ignore($this->getKey())],

Switch on $this->exists or on the configured event rather than maintaining two diverging arrays.

Custom messages and attribute names. Always set human-readable attributes for snake_case columns; otherwise API consumers get The published at field is required.

Let form requests own authorization and input shaping; let the model own invariants. Requests answer "may this user submit this?" and normalize/filter input. Model rules answer "is this row ever allowed to exist?" Don't duplicate the same message in both layers — pick the model as the source of truth and keep the request thin.

API responses come for free. The package throws Illuminate\Validation\ValidationException, so Laravel's handler already returns 422 with an errors bag for JSON requests and a redirect-with-errors for web. If you use a custom error envelope, map it once in bootstrap/app.php under withExceptions() rather than catching per-controller.

Action/service classes can trust their input. Once validation sits at persistence, a PublishPost action doesn't need defensive re-validation; it can assume the model will reject bad state.

Test the rules, not the call site:

$this->expectException(ValidationException::class);
Post::create(['title' => '']);

This makes rule changes fail loudly across the whole suite, which is the main reason to adopt the package at all.

Multi-tenant / auth-dependent rules should be resolved at runtime (closure rules, Rule::when, or a resolver method reading the current tenant) instead of a static $rules property that can't see request context.

Gotchas and Tips

  • Silent bypass paths. saveQuietly(), updateQuietly(), Model::withoutEvents(), Model::query()->update(), insert(), upsert(), and raw SQL all skip model events and therefore skip validation. The trait is a guardrail for Eloquent writes, not a database constraint — keep your unique indexes and NOT NULLs.
  • firstOrCreate / updateOrCreate are the classic trap. They fire events, so validation runs on the attempted create. If you pass only a subset of attributes, required rules on the rest blow up. Pass the full attribute set, or use firstOrNew() + explicit fill.
  • Partially-selected models. User::select('id', 'name')->first()->save() has no email attribute loaded, so required|email fails even though the row is valid. Either select the columns your rules touch, or use saveQuietly() for these maintenance writes.
  • Unique + soft deletes. Rule::unique() counts trashed rows. Add ->whereNull('deleted_at') if "unique among live records" is the intent.
  • Validation is not transactional. Unique checks run before the insert, so concurrent requests can still race. The DB unique index is the real guarantee.
  • $fillable vs. rules. Rules don't imply fillable. create() silently drops non-fillable keys, which then fails required on a field the user did submit. Keep mass-assignment config and rules in sync.
  • Queues. A ValidationException inside a job is treated as a normal failure: retries, then a failed job. If the data originates from a user, catch it and route to a dead-letter/notification path.
  • Performance. The full rule set runs on every save. unique, exists, and file rules cost queries. Use sometimes, nullable, and prefer creating-only for expensive checks that can't change on update.
  • Debugging. If a save fails mysteriously, reproduce with $model->saveQuietly() to confirm the validation layer is the culprit, then inspect $e->errors() / $e->validator->errors(). Temporarily removing the trait is also a legitimate bisect.
  • Extension point. Put shared rules (tenancy scoping, common audit columns) in a base model or an overridden rule-resolution hook in the trait, rather than copy-pasting arrays into twenty models.
  • Maturity risk. 33 stars, zero dependents, single maintainer, and a Laravel-12/13-only constraint. Pin the version in composer.json, read the trait before relying on undocumented properties, and accept that you may need to fork it. For a small app with one or two write paths, form requests are simpler; the package earns its keep in apps with many write paths (API + admin + jobs + imports) that must not drift apart.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator