lendable/form-error-log-bundle
A Symfony bundle that captures and logs form validation errors, so you can trace failed form submissions, debug validation issues, and monitor error patterns across your application.
This is a Symfony bundle, not a Laravel package — there is no service provider, no config publish step, and nothing under config/. Read it as a reference implementation of an idea, and plan to either (a) run it inside the Symfony app it was written for, or (b) port its ~40 lines into a Laravel listener. The whole package is essentially one event subscriber that hooks FormEvents::POST_SUBMIT on the root form, walks $form->getErrors(true), and writes each error to a PSR-3 logger.
The current release is v2.0.0, whose release notes mention only one thing: "PSR-4 replaces PSR-0 as the autoloading strategy." That is a packaging change, not a behaviour change — same classes, same namespace (Lendable\OhFormErrorLogBundle\…), same subscriber, same config key. Nothing in the day-to-day usage below changes between 1.x and 2.0. The major-version bump for an autoloading-only change is itself the signal: treat this as a low-traffic, low-maintenance package.
Minimal steps in a Symfony app:
composer require lendable/form-error-log-bundle:^2.0
// app/AppKernel.php (Symfony 2/3 era — this bundle predates bundles.php)
$bundles[] = new Lendable\OhFormErrorLogBundle\LendableOhFormErrorLogBundle();
Optional config, typically just the log level:
# app/config/config.yml
lendable_form_error_log:
level: notice
Where to look first: the bundle's EventSubscriber / form-event listener class. It is short and is the entire feature. Note exactly what it puts in the log message versus the log context — that determines whether your downstream queries are usable. If you are pinning ^1.0 anywhere (a lockfile, a CI cache, a forked copy), read the v2 composer.json first: the autoload block and the on-disk src/ layout differ, but the class names do not.
First use case: usability triage. Route its output to a separate channel from application errors, let it run for a week, then answer "which three fields do users fail on most, and what does the message say?" That is the whole value proposition: validation errors are user friction, not bugs, and they are invisible if you only log exceptions.
If you are evaluating this for Laravel, the equivalent one-liner is a ValidationException handler or a failedValidation() override in a base FormRequest, logging $exception->errors() with the route name and field names as context. Same shape, same caveats.
Dedicated channel, not the default logger. The single most important pattern: configure a Monolog channel (e.g. form_errors) with its own handler and its own retention. Mixing these into laravel.log / prod.log makes both the noise and the signal unusable.
monolog:
channels: ['form_errors']
handlers:
form_errors:
type: stream
path: "%kernel.logs_dir%/form_errors.log"
level: notice
channels: ['form_errors']
Enrich with context you cannot get from the message. The bundle logs a message string; the useful fields are form name, field name, error message, route, and (carefully) a user or session hash. In a port, log an array:
// Laravel: app/Http/Requests/BaseFormRequest.php
protected function failedValidation(Validator $validator): void
{
Log::channel('form_errors')->notice('form.validation_failed', [
'form' => class_basename(static::class),
'route' => request()->route()?->getName(),
'fields' => array_keys($validator->errors()->toArray()),
'count' => $validator->errors()->count(),
]);
parent::failedValidation($validator);
}
Log field names and error keys, not raw messages, if user input can end up inside them.
Turn logs into a counter, not a log. The real payoff is a dashboard. Either add a Monolog handler that increments a metric per error key, or parse the JSON log lines. "Errors per field per week" is a product signal; a text file nobody reads is not.
Scope by form. Not every form is interesting. Search boxes, filters, and multi-step wizards generate noise. In Symfony, decorate the subscriber and early-return for a denylist of form names; in Laravel, guard on static::class or the route name.
Test the instrumentation itself. A functional test that posts invalid data and asserts a log record was written protects you from silently losing the signal after a refactor:
Log::spy();
$this->post('/register', ['email' => 'not-an-email']);
Log::shouldHaveReceived('notice')->withArgs(fn ($m, $c) => $c['fields'] === ['email']);
For Symfony, extension points are the logger and the subscriber. Point the bundle's logger argument at your channel, or decorate the subscriber class to re-shape the message. There is no event dispatched you can listen to — the subscriber is the hook.
src/ tree is no longer nested per-namespace-segment. Consequences: any vendored copy, classmap, exclude-from-classmap, Composer autoload patch, or path-based exclusion referencing src/Lendable/OhFormErrorLogBundle/… breaks. Class names and namespaces are unchanged, so ordinary composer require/update consumers are unaffected. If you maintain a fork, rebase onto the v2 layout rather than merging it into a PSR-0 tree.composer.json and check the symfony/* versions it allows. If they are still 2.7/^3.0-era, it will not install against Symfony 5+/6+/7+ or a modern Laravel (which ships Symfony 6/7 components) without --ignore-platform-reqs and code edits. Treat it as a spec, not a dependency.$form->submit().getErrors(true) returns non-field errors with an empty origin. Global errors (CSRF, "form is invalid", cross-field rules) come back with $error->getOrigin()->getName() === ''. Handle that case explicitly or your top-error report will be dominated by a blank field name.error will page someone every time a user types a bad email. Use notice/info and alert only on rate changes, not presence."{{ value }} is not valid"), you are logging user input. Sample (1-in-N), hash identifiers, and set a short retention.composer update lendable/form-error-log-bundle, run composer dump-autoload -o, then confirm your bundle registration namespace still resolves (Lendable\OhFormErrorLogBundle\LendableOhFormErrorLogBundle — unchanged) and that no deployed opcache/classmap artifact points at the old nested src/Lendable/... path.handleRequest()/submit(), confirm the error actually lands on a child form (getErrors(true) is recursive; getErrors() is not), and confirm the log level in config is at or below the level your handler accepts.How can I help you explore Laravel packages today?