it/input-mask-bundle
Symfony bundle that adds client-side input masking to form fields, with Twig helpers and asset integration for common masks such as phone numbers, dates and currency. It simplifies wiring mask scripts into templates, though it is a small, lightly maintained package with limited adoption.
Install and register
composer require it/input-mask-bundle
The bundle predates Symfony Flex, so there is no recipe. Register it manually:
new IT\InputMaskBundle\InputMaskBundle() to AppKernel::registerBundles().config/bundles.php (check the namespace in the package's composer.json autoload block — it is the fastest way to confirm the class name).Load the JavaScript side yourself. The bundle only supplies PHP form classes and (usually) a Twig form theme; it does not ship the jQuery plugin. You need, in this order:
jquery.inputmask (RobinHerbots) — via npm/yarn, a CDN, or an asset in web/.inputmask(){# templates/base.html.twig #}
<script src="{{ asset('js/jquery.min.js') }}"></script>
<script src="{{ asset('js/jquery.inputmask.bundle.min.js') }}"></script>
<script>
$(function () { $('[data-inputmask]').inputmask(); });
</script>
Where to look first
Form/Type/ — the masked field types the bundle exposes and which options each accepts.Resources/views/Form/ — the form theme that turns those options into data-inputmask* attributes on the rendered <input>.DependencyInjection/ — whether the theme is registered globally or must be opted into per template.First use case: a phone number in a contact form
use IT\InputMaskBundle\Form\Type\MaskedTextType; // verify exact name in Form/Type
$builder
->add('phone', MaskedTextType::class, [
'mask' => '(999) 999-9999',
'label' => 'Phone',
]);
Then make sure the bundle's form theme is active — either globally via twig.form_themes in config.yml, or per template:
{% form_theme form 'InputMaskBundle:Form:input_mask_layout.html.twig' %}
Render the form normally. View source and confirm the input carries a data-inputmask="'mask': '(999) 999-9999'" (or equivalent) attribute. That attribute is the contract between the bundle and the plugin — if it's missing, the theme isn't wired up and no amount of JS will help.
Treat the bundle as a "mask option on a text field" abstraction. The daily pattern is not "use the masked type everywhere"; it's "use it where the input format is genuinely fixed" — phone, postal code, tax ID, IBAN, date fragments, license plates. Free-text name/address fields should stay as normal TextType.
Keep mask definitions in one place. Hard-coding '(999) 999-9999' across ten forms guarantees drift. Put them in a config parameter or a small constants class and reference it:
// config/services.yaml
parameters:
masks.phone: '(999) 999-9999'
masks.postal: '99999-999'
$builder->add('phone', MaskedTextType::class, [
'mask' => '%masks.phone%', // if the bundle resolves parameters; otherwise inject via a form-type factory
]);
Pair every mask with server-side validation. This is the single most important pattern. The mask is a UX hint, not a rule. Add a constraint and normalise the value:
->add('phone', MaskedTextType::class, [
'mask' => $this->maskPhone,
'constraints' => [
new Assert\Regex([
'pattern' => '/^\(\d{3}\) \d{3}-\d{4}$/',
'message' => 'Enter a complete phone number.',
]),
],
])
Decide early how the masked characters are handled on submit. Inputmask can leave literal characters in the posted value ((415) 555-1234) or strip them (4155551234) depending on removeMaskOnSubmit/clearIncomplete and the plugin version. Choose one and enforce it with a data transformer so the entity always receives canonical data:
final class DigitsOnlyTransformer implements DataTransformerInterface
{
public function transform($value): ?string { return $value; }
public function reverseTransform($value): ?string
{
return $value === null ? null : preg_replace('/\D+/', '', $value);
}
}
Attach it with ->addModelTransformer() on the field, or via a PRE_SUBMIT listener if you prefer to keep the type dumb.
Prefer a form type extension if you want masks on TextType. Many teams find it cleaner to add a mask option to the standard TextType globally than to swap field classes. If the bundle only ships standalone types, a FormTypeExtensionInterface implementation giving TextType a mask option and adding a data-inputmask attribute via buildView() is a ~40-line replacement for the whole bundle, and it is upgrade-safe.
Re-initialise after DOM changes. For collection types (add/remove rows) or any AJAX-loaded form, the newly inserted inputs never saw your $(function(){...}) call:
$(document).on('collection-item-added', function (e, $row) {
$row.find('[data-inputmask]').inputmask();
});
Or bind with event delegation and an inputmask call on focusin, which is more forgiving of partial updates.
Testing pattern. Use Symfony's form type test case or just render-and-assert in a functional test:
$crawler = $client->request('GET', '/contact');
$this->assertGreaterThan(
0,
$crawler->filter('input[name="contact[phone]"][data-inputmask]')->count()
);
Then post a value that violates the mask and assert the form still fails validation — that test documents the client-side-only guarantee.
Integration tip: don't fight NumberType. For currency/thousands masks, either let NumberType + a JS decimal formatter own the display, or use a masked text input with a transformer. Trying to layer an Inputmask numeric alias on top of Symfony's number rendering produces double formatting and locale surprises.
The bundle is unmaintained (last release June 2017). Expect Symfony 4+ friction: legacy bundle registration, Resources/views template references (InputMaskBundle:Form:...) that are deprecated in favour of @InputMask/Form/..., and possible Symfony\Component\Form\AbstractType signature drift (buildView, configureOptions). Budget time to fork it into src/ or replace it with a local form type extension. At ~8 stars and zero dependents there is no upstream support path.
Version mismatch between the bundle and the plugin is the #1 failure mode. The bundle was written against an older Inputmask (v3-era) where options were passed as data-inputmask="'mask': '...'". Inputmask v4/v5 changed the attribute and option surface (data-inputmask, data-inputmask-mask, data-inputmask-regex, alias names). Pin your jquery.inputmask version, and if the generated attribute doesn't match the plugin's expectations, patch the form theme — that's the file to change, not the form classes.
$(...).inputmask is not a function means the plugin script didn't load, loaded before jQuery, or the page is using a second jQuery instance. Check the network tab and window.jQuery.fn.inputmask in the console. Also check for Turbo/Turbolinks/pjax — if the page swaps without a full reload, your $(function(){}) runs once and never again.
Masks don't validate anything. An attacker posts contact[phone]=lol. Every masked field needs a constraint or a transformer that rejects/completes the value server-side. Do not rely on required in the mask config.
Empty and incomplete inputs are ambiguous. clearIncomplete: true silently blanks a partially typed value on blur; false submits the partial string. Combine with optional mask segments ([9]) and you get several distinct "empty" representations. Normalise in the transformer ('' → null) so your entity doesn't store ( ) ___ -____.
Masks only make sense on text-like inputs. EntityType, ChoiceType, CheckboxType with a mask option will either be ignored or render an attribute nothing consumes. Don't try to mask a <select>.
Don't mask dates into a DateType. Three separate widgets and a mask on a single text field is a fight you'll lose. Use a single TextType with a date mask plus a DateTime transformer, or use the browser's native date input and skip the mask.
Debugging checklist, in order:
data-inputmask on the input? (No → form theme not registered.)$.fn.inputmask defined? (No → asset/load-order problem.)Extension points worth using: the Twig form theme block (single place to change attribute generation), a FormTypeExtensionInterface (add mask to existing types without swapping classes), and a DataTransformer (canonicalise submitted values). Between those three you can replicate all of the bundle's value on a supported code path — which, given the maintenance status, is likely the right long-term move.
How can I help you explore Laravel packages today?