j-ben87/parsley-bundle
Symfony bundle that integrates Parsley.js client-side form validation with Symfony forms. It maps form constraints to Parsley attributes, cuts manual markup, and helps add consistent browser-side validation with minimal configuration. Useful when you want browser-side checks that complement server-s...
First, confirm the stack. This is a Symfony bundle (bjd-php/parsley-bundle), not a Laravel package. It reads symfony/validator constraint metadata off form types. If you are on Laravel, there is no constraint graph to introspect — Illuminate\Validation rules are not the input this package consumes. For Laravel, use the rest of this document as a porting guide for a ~300-line rule-to-attribute helper; do not attempt to install the bundle.
Symfony path — minimal setup:
composer require j-ben87/parsley-bundle
// config/bundles.php
JBen87\ParsleyBundle\ParsleyBundle::class => ['all' => true],
Ensure the Symfony Form component and Validator are already in use (they almost certainly are). Confirm the Parsley runtime is loaded on the pages that need validation — the bundle only emits data-parsley-* attributes; it does not ship Parsley or jQuery.
Where to look first in the source:
First use case. Pick a simple form with a NotBlank + Email combo, or NotBlank + Length:
$builder
->add('email', EmailType::class, [
'constraints' => [
new NotBlank(['message' => 'Email is required']),
new Email(['message' => 'Enter a valid email']),
],
]);
Render it normally with form_row() / form_widget(), initialize Parsley once on the form, and inspect the rendered HTML. You should see data-parsley-required, data-parsley-type="email", and matching data-parsley-required-message / data-parsley-type-message attributes. That rendered HTML is your contract — check it before wiring up anything else.
Constraint-driven, not attribute-driven. Declare rules on the form type's constraints option — the bundle does the translation. Do not hand-write data-parsley-* attributes alongside the constraints; that reintroduces the duplication the package exists to remove, and it will drift.
Per-form rollout with a feature flag. Gate the Parsley initialization (or the extension, if you can toggle it) behind a config flag scoped to one form family first. Because the server-side constraints are untouched, rollback is just "stop initializing Parsley" — nothing authoritative lives in the attributes.
Map your coverage table before you ship. For each form you touch, enumerate the constraints in use and classify each one:
NotBlank, Length, Email, Regex, Range, Choice (per description).Length min/max mapped to Parsley min/max may not match your message semantics; Regex flags/anchoring may differ between PHP PCRE and JS regex.Callback, When, Valid traversal, group-conditional constraints, uniqueness. These must not appear to be validated client-side.Keep this table in the repo. It is the artifact that prevents silent divergence later.
Custom messages. Pass a message on each constraint; it flows through as data-parsley-<rule>-message. If the app uses the Symfony translator, verify whether the bundle runs the message through it (unverified) — if not, either translate at declaration time or in a post-render step.
Multi-step wizard pattern. Initialize Parsley per step's <form> element, not on the whole document, and re-bind after each step renders (AJAX-loaded steps need re-init). Use Parsley's data-parsley-group namespacing if the server constraints are logically grouped, so a step validates only its slice.
Test the mapping, not just the form. Add a table-driven test asserting the emitted attribute set for each constraint you rely on. This is cheap and catches upstream or Symfony-version regressions — the failure mode you care about is an attribute quietly disappearing. A rendered-HTML assertion per constraint type is the right shape.
Instrumentation pairing. Since the package emits no telemetry, wire your own: count client-blocked submits and server-side validation failures per session. That's the data that tells you whether the UX win materializes — without it, "we added client validation" is anecdotal.
Laravel port shape (if that's your path). Implement ParsleyAttributes::fromRules(array $rules): array reading Illuminate\Validation rule strings (required, max:255, email, regex:/.../) and emitting the same data-parsley-* shape. Expose it as a Blade component or a @parsley-style directive that merges into the field's attribute bag. Reuse the bundle's mapping semantics and escaping approach as reference, rewrite the plumbing.
Silent no-ops are the primary failure mode. A constraint without a Parsley equivalent emits nothing — the browser sees no rule and does not block. That is fail-safe (server still rejects) but invisible. Make unsupported constraints loud in dev (log a warning when a constraint doesn't map) and degrade quietly in prod. This is the single highest-value hardening you can add to a fork or port.
Never relax the server-side constraints. The attributes are a UX affordance. Keep every validator constraint authoritative, and correct any stakeholder who reads "client-side validation" as a control.
Attribute injection risk. Messages and option values land inside HTML attributes. Assume the escaping path is unproven absent a review — test with messages containing ", <, >, and &, and prefer single-quoting and htmlspecialchars(..., ENT_QUOTES) in any custom rendering you add. If you fork, add a test for it.
Groups and nested constraints are not traversed. Valid traversal into embedded objects, @Group-conditional constraints, and When/callback constraints are where client/server parity is most likely to break in both directions. Treat these fields as server-validated only and say so in the coverage table.
Regex is not portable by default. PHP PCRE and JS RegExp differ on lookbehind support, \A/\z anchors, and u-mode unicode. Any Regex constraint you expose to Parsley must be verified against JS semantics, or kept server-only.
jQuery dependency. Parsley is a jQuery plugin. If the app doesn't already ship jQuery, this is a real transitive cost — flag it explicitly rather than letting it leak in through Parsley.
Re-initialize after DOM mutations. Dynamically added fields (collections, AJAX-loaded fragments) need $(formEl).parsley() called again, or ParsleyForm re-binding. A common "it stopped validating" bug.
No-JS and accessibility. With JS disabled, only server validation runs — confirm the form still submits and errors render server-side. For a11y, wire Parsley's error events into aria-invalid and an aria-live region announcing errors; the default Parsley error DOM is not screen-reader-friendly out of the box.
CSP. Parsley's default error insertion may require unsafe-inline for styles or an allowance for its DOM manipulation — check against your CSP before rollout rather than during it.
Pin and own it. Last release was 2023-12-15 with a single maintainer and zero dependents. Pin the version, vendor or fork via Composer (repositories with a VCS path), and assign a named owner for the fork. Plan on Symfony major-version upgrades breaking this — assume you fix it yourself.
Version checks before committing. Confirm the Symfony major version the bundle supports (likely 5/6, given the release date), the PHP floor (attributes imply 8.0+), and the Parsley major version your front end loads. The composer.json was not inspected — read it before scheduling work.
Debugging shortcut. When a field "isn't validating," inspect the rendered HTML first: is the data-parsley-* attribute actually present? If not, the constraint didn't map (or groups filtered it out). Then check $(form).parsley().isValid() in the console. That ordering resolves most reports in under a minute.
How can I help you explore Laravel packages today?