How to throw ValidationException in Laravel without Request validation helpers or manually creating a Validator?

How to use ValidationException::withMessages() method?

Instead of manually creating a Validator, then adding some rules into it, and executing validate() method on the Validator, you can directly throw ValidationException with error messages in it.

Compare this:

use Illuminate\Support\Facades\Validator;

Validator::make([], [])->after(function ($validator) {
    if ($this->somethingIsInvalid()) {
        $validator->errors()->add('some_error', 'Something is invalid.');

with this:

use Illuminate\Validation\ValidationException;

if ($this->somethingIsInvalid()) {
    throw ValidationException::withMessages([
        'some_error' => 'Something is invalid.'),

The end result is the same, and if you use ValidationException::withMessages(), Laravel internally still calls Validator::make([], []) (source), but in situations where you need to use it, your code becomes slightly clearer

When you may need to manually throw ValidationException?

I recently wanted to throw Illuminate\Validation\ValidationException when Stripe transfer to a connected account failed due to some error on Stripe's end. For example, if the connected account was not fully onboarded, Stripe wouldn't let us execute the transfer. We find out about the failure when Stripe PHP throws Stripe\Exception\ApiErrorException.

Executing Stripe transfers is something that one of the admin users does in our app. I simply wanted to use the existing front-end components displaying validation messages to communicate to the admin that something went wrong and pass the message we received from Stripe. In other words, I wanted these errors to be shown within blade templates like:

    @foreach ($errors->all() as $error)
        <li>{!! $error !!}</li>

I could have adjusted the front-end templates, and flash custom messages into session, but somehow Stripe errors fall into my mental model of validation errors, so it feels better to wrap them into a ValidationException. I think of them as Stripe telling me "you didn't provide us a valid connected account id that is eligible for transfers".

So here's how I solved it:

try {
    $stripeTransfer = $stripeGateway->transfer($amount, $currency, $connectedAccountId, $internalTransferId, $description);
} catch (ApiErrorException $e) {
    // manually add errors to session:
    throw ValidationException::withMessages([
        'gateway' => $e->getMessage(),

// ...
// do something else with the $stripeTransfer, if we successfully executed it

This may not be a pattern you use very often in your app, but it's good to know that something like this exists :)