Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Finally, get to improving your user experience. At LaraconUS I gave a [talk on h
- Support Inertia validation errors
- Support Livewire validation errors
- Fallback for undetectable validation errors (based on 422 response status)
- Capture generic validation exceptions for custom response types

## Ignore specific error messages

Expand Down Expand Up @@ -91,3 +92,35 @@ public function boot(): void
});
}
```

## Capture validation errors for custom response formats

If you are returning custom response formats, you may see `__laravel_unknown` in the dashboard instead of the input names and error messages. This is because the package parses the response body to determine the validation errors. When the body is in an unrecognised format it is unable to parse the keys and messages from the response.

You should instead dispatch the `ValidationExceptionOccurred` event to pass the validation messages to the card's recorder. You may do this wherever you are converting your exceptions into responses. This usually happens in the `app/Exceptions/Handler`:

```php
<?php

namespace App\Exceptions\Handler;

use Illuminate\Support\Facades\Event;
use Illuminate\Validation\ValidationException;
use Laravel\Pulse\Facades\Pulse;
use Throwable;
use TiMacDonald\Pulse\ValidationExceptionOccurred

class Handler
{
// ...

public function render($request, Throwable $e)
{
if ($e instanceof ValidationException) {
Pulse::rescue(fn () => Event::dispatch(new ValidationExceptionOccurred($request, $e)));
}

// custom exception rendering logic...
}
}
```
27 changes: 22 additions & 5 deletions src/Recorders/ValidationErrors.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
use Livewire\Livewire;
use Livewire\LivewireManager;
use TiMacDonald\Pulse\LivewireValidationError;
use TiMacDonald\Pulse\ValidationExceptionOccurred;

/**
* @internal
Expand All @@ -41,6 +41,7 @@ class ValidationErrors
*/
public array $listen = [
RequestHandled::class,
ValidationExceptionOccurred::class,
];

/**
Expand All @@ -57,15 +58,25 @@ public function register(callable $record, Application $app): void
{
$this->afterResolving($app, 'livewire', function (LivewireManager $livewire) use ($record, $app) {
$livewire->listen('exception', function (Component $component, Throwable $exception) use ($record, $app) {
$exception instanceof ValidationException && $record(new LivewireValidationError($app['request'], $exception));
if (! $exception instanceof ValidationException) {
return;
}

with($app['request'], function (Request $request) use ($record, $exception) {
// Livewire can reuse the same request instance when polling or
// performing grouped requests.
$request->attributes->remove('pulse_validation_messages_recorded');

$record(new ValidationExceptionOccurred($request, $exception));
});
});
});
}

/**
* Record validation errors.
*/
public function record(LivewireValidationError|RequestHandled $event): void
public function record(ValidationExceptionOccurred|RequestHandled $event): void
{
if (
$event->request->route() === null ||
Expand All @@ -75,12 +86,18 @@ public function record(LivewireValidationError|RequestHandled $event): void
}

$this->pulse->lazy(function () use ($event) {
if ($event->request->attributes->has('pulse_validation_messages_recorded')) {
return;
}

[$path, $via] = $this->resolveRoutePath($event->request);

if ($this->shouldIgnore($path)) {
return;
}

$event->request->attributes->set('pulse_validation_messages_recorded', true);

$path = $this->group($path);

$this->parseValidationErrors($event)->each(fn ($values) => $this->pulse->record(
Expand All @@ -95,9 +112,9 @@ public function record(LivewireValidationError|RequestHandled $event): void
*
* @return \Illuminate\Support\Collection<int, array{ 0: string, 1: string }>
*/
protected function parseValidationErrors(LivewireValidationError|RequestHandled $event): Collection
protected function parseValidationErrors(ValidationExceptionOccurred|RequestHandled $event): Collection
{
if ($event instanceof LivewireValidationError) {
if ($event instanceof ValidationExceptionOccurred) {
return $this->parseValidationExceptionMessages($event->request, $event->exception);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;

class LivewireValidationError
class ValidationExceptionOccurred
{
public function __construct(
public Request $request,
Expand Down
40 changes: 40 additions & 0 deletions tests/RecorderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
use Illuminate\Http\JsonResponse as IlluminateJsonResponse;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Inertia\Middleware as InertiaMiddleware;
use Laravel\Pulse\Entry;
use Laravel\Pulse\Facades\Pulse;
Expand All @@ -15,6 +17,7 @@
use Symfony\Component\HttpFoundation\JsonResponse as SymfonyJsonResponse;
use Tests\TestClasses\DummyComponent;
use TiMacDonald\Pulse\Recorders\ValidationErrors;
use TiMacDonald\Pulse\ValidationExceptionOccurred;

use function Pest\Laravel\get;
use function Orchestra\Testbench\Pest\defineEnvironment;
Expand Down Expand Up @@ -584,3 +587,40 @@
expect($aggregates->pluck('aggregate')->all())->toBe(array_fill(0, 4, 'count'));
expect($aggregates->pluck('value')->every(fn ($value) => $value == 1.0))->toBe(true);
});

it('captures validation errors from custom event', function () {
Route::post('users', function () {
try {
Request::validate([
'name' => 'required',
'email' => 'required',
]);
} catch (ValidationException $e) {
Event::dispatch(new ValidationExceptionOccurred(request(), $e));

throw $e;
}
})->middleware('web');

$response = post('users');

$response->assertStatus(302);
$response->assertInvalid(['name', 'email']);
$entries = Pulse::ignore(fn () => DB::table('pulse_entries')->whereType('validation_error')->get());
expect($entries)->toHaveCount(2);
expect($entries[0]->key)->toBe('["POST","\/users","Closure","default","name","The name field is required."]');
expect($entries[1]->key)->toBe('["POST","\/users","Closure","default","email","The email field is required."]');
$aggregates = Pulse::ignore(fn () => DB::table('pulse_aggregates')->whereType('validation_error')->orderBy('period')->get());
expect($aggregates->pluck('key')->all())->toBe([
'["POST","\/users","Closure","default","name","The name field is required."]',
'["POST","\/users","Closure","default","email","The email field is required."]',
'["POST","\/users","Closure","default","name","The name field is required."]',
'["POST","\/users","Closure","default","email","The email field is required."]',
'["POST","\/users","Closure","default","name","The name field is required."]',
'["POST","\/users","Closure","default","email","The email field is required."]',
'["POST","\/users","Closure","default","name","The name field is required."]',
'["POST","\/users","Closure","default","email","The email field is required."]',
]);
expect($aggregates->pluck('aggregate')->all())->toBe(array_fill(0, 8, 'count'));
expect($aggregates->pluck('value')->every(fn ($value) => $value == 1.0))->toBe(true);
});