Laravel developers want to give users the best possible user feedback but maintaining data flow can be trying. As the app grows in size, excessive controller classes turn a majestic monolithic into piles of spaghetti code regrets. The framework does offers dozens of useful validation rules and form classes for simple use cases to cover most needs. Unfortunately the documentation on customizing new rules for business edge cases (and their error messages) is fairly sparse.

The problem with documenting such framework abilities is domain knowledge being strongly contextualized. A walk-through of an extended case study is required to showcase all the nuances of building an app, be it testing, database performance, or security concerns. Laravel’s biggest attraction is its documentation’s accessibility but sometimes devs just have to get their hands dirty.

For this post, I decided to showcase two custom validation rules (with tests) that cover business logic that’s not too obscure. Some framework trickery has to be wrenched into validation for the best possible error message so I’m going to introduce you to the DataAwareValidator trait.

Class-based Custom Validation Rule

Here is the basic configuration for a custom rule that doesn’t allow people to register accounts for certain domain names. When email addresses are visible in the app, this can prevent attackers using the app’s domain name in an email address to spoof employees.

app/Providers/ValidatorServiceProvider.php

resources/lang/en/validation.php

config/auth.php

app/Validators/EmailValidator.php

This bare-bones rule will get the requirement covered. However, will all your users understand technical jargon like “domain”? What can they do to correct this error?

Injecting input to the error message

Laravel allows validator replacer functions to be applied on error messages to substitute parameterized strings with better contextualized feedback. The official documentation doesn’t give a concrete example so novice developers may be left in the dark what to do next.

The replacer configuration is simple enough.

app/Providers/ValidatorServiceProvider

resources/lang/en/validation.php

How do we go about capturing the email address input and inserting its domain substring into this message? Closure and class-based replacer functions don’t get the Illuminate\Validation\Validator instance passed as a parameter. The validateEmailDomainAllowed() above does as its fourth argument. Without that object, there’s no data payload to pluck from.

Much to the dismay of some evangelical SRP jackholes, you could instead just grab that email address from the app container’s Illuminate\Http\Request:

Now this validation rule’s use is always tied to the HTTP request lifecycle. This becomes even more apparent when writing a unit test:

tests/rules/email.php

Using a DataAwareValidator trait

I wanted to keep the validation call self-contained by flowing data through the validator to that replacer function. Alas, I must be one of those aforementioned SRP jackholes. Since validation and replacement are called sequentially, with a single-use data() call in both methods the state of that input can be saved.

Now check out that former ugly test:

Laravel validation replacer for parameters from the database

A better fix for the above use case to create a framework PR to also pass the Illuminate\Validation\Validator object into the replacer function. However there are other cases where that payload isn’t enough. What if you wanted to show something from the database or a configuration file in the validator’s error message?

$this->data() calls would still be needed to flow that data through to the replacer function.

Disallow conflicting events on a calendar

Implementing calendar availability using a Laravel validation rule is simple and testable. The custom rule is applied to the calendar itself and then date range parameters are checked. Ideally the unavailable dates are disabled in the UI but you should never rely on JavaScript for validation. For the PHP backend, a FormRequest@rules() method may look like this:

app/Http/Requests/StoreEventRequest.php

The below code will make some assumptions about predicates, relationships, and scope methods on a Calendar model with related Event models. Hopefully they’ll be self-evident for the sake of this example.

The new calendar_availability rule will be defined to require three parameters:

  • calendar_id
  • starts_at
  • ends_at
app/Validators/CalendarValidator.php

A more granular feedback message is desired here to indicate what is already scheduled during those dates. Instead the conflicting Event details can be shown in the error message:

Instead of nesting str_replace() for three parameters in the validation rule’s localized translation string, use PHP standard library’s strtr() for a cleaner solution.

Now the resulting validation rule test becomes easy to setup.

tests/rules/calendar.php

DataAwareValidator trait implementation

app/Validators/Traits/DataAwareValidator.php

Inevitable Edge Cases

There are some important considerations to make when custom validation rules rely on multiple request parameters and database queries.

  • When dependent parameters are free-form text inputs, you can’t assume they’re fully validated as rules are run independent of each other. This is especially true for public <form>s submitting by bots and crawlers (CAPTCHAs be damned.)
    • For example, Carbon::parse() is susceptible to unexpected InvalidArgumentExceptions from improper date formats.
  • Custom validation rules can cause duplicate queries, like that Calendar::find() Eloquent call. I have a workable solution involving StoreEventRequest@all() above pulling implicit model binding $this->route('calendar') into the Validator@getData() payload. When validating rules using a FormRequest object, you can extend Laravel’s class to also add those route parameters.

    Now $validator->getData()['calendar'] is the model object.