Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Phone Input Bundle Laravel Package

nowo-tech/phone-input-bundle

nowo-tech/phone-input-bundle provides a reusable phone-number form input for Symfony applications, with international formatting, validation helpers, and country-aware UI components to simplify collecting normalized phone numbers in your forms.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, not a Laravel package. Before anything else, decide which side of that line you are on, because it changes the first hour completely:

Your app Realistic path
Symfony 6/7/8 Install, register bundle, use PhoneInputType directly.
Laravel 10/11/12 Do not expect config/app.php providers to work. Either use the underlying value object / normalizer standalone, or wrap symfony/form manually.

Where to look first (15-minute orientation)

  1. README.md — usually the only source of option names. Read it end to end; it is small.
  2. src/Form/Type/ — the phone type itself. This is the contract you actually consume.
  3. src/DependencyInjection/Configuration.php — the real config keys (debug:config on Symfony confirms them; do not trust the README blindly on defaults).
  4. src/Resources/config/services.php (or .yaml) — what service IDs and aliases exist, and whether the form type is autowired by FQCN.
  5. tests/ — the fixtures here are the fastest answer to "what shape does the submitted array have?" and "what does the value object look like?" One test using the type is worth ten README lines.

Minimal Symfony steps

composer require nowo-tech/phone-input-bundle
// config/bundles.php
return [
    Nowo\PhoneInputBundle\PhoneInputBundle::class => ['all' => true],
];
use Nowo\PhoneInputBundle\Form\Type\PhoneInputType;

$builder->add('phone', PhoneInputType::class, [
    'label' => 'Phone number',
]);

That default is the first use case you should ship: one field, E.164 output, store it in a varchar(16) column. Everything else (country selector, array output, value objects) is a variation on this and should only be adopted once the plain field works end to end — submit, validate, persist, re-render the form with the stored value.

Minimal Laravel steps (the honest version)

You have three options, in ascending order of effort:

# 1. Use only the non-Symfony parts (value object, normalizer) — cheapest
composer require nowo-tech/phone-input-bundle
// 2. Standalone symfony/form — works, but you own the wiring
$formFactory = Forms::createFormFactoryBuilder()
    ->addExtension(new HttpFoundationExtension())
    ->addType(new PhoneInputType(/* deps from the bundle's services */))
    ->getFormFactory();
// 3. Port the type into app/Forms/Types/PhoneInputType.php — most common in practice

If the goal is day-to-day Laravel usage rather than evaluation, option 3 (a 60-line local form type or a custom Blade component + a normalizer service) will outlive this dependency and costs less than fighting bundle wiring. Keep the bundle installed only if you genuinely consume its value object or parsing logic.


Implementation Patterns

Pattern 1 — E.164 everywhere, format only at the edge

This is the pattern that survives contact with production. Persist one canonical string; never store what the user typed.

// Entity: single canonical column
#[ORM\Column(length: 16, nullable: true)]
private ?string $phone = null; // always "+33612345678"

// Form: the bundle normalizes on submit
$builder->add('phone', PhoneInputType::class, [
    'required' => false,
    'format'   => 'e164', // verify exact option name in Configuration.php
]);

Then format for display in a presenter/Twig filter, not in the entity. If you ever need the raw input back ("what the user typed"), store it separately — do not overload the canonical column.

Pattern 2 — Country selector as a dependent field

The bundle's optional prefix selector is convenient but couples country selection to the phone widget. Two rules:

  • Always provide an explicit preferred_countries / default list from your own domain data (the countries you actually ship to), not the full ISO set.
  • If you already have a country field on the address, do not add a second selector. Feed the existing value in via form options or a PRE_SET_DATA listener so there is one source of truth.
$builder
    ->add('country', CountryType::class)
    ->add('phone', PhoneInputType::class, [
        'country_selector' => false,          // reuse the field above
        'default_country'  => 'FR',
    ]);

// Keep the phone field's country in sync with the address country
$builder->get('country')->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $e) {
    $country = $e->getForm()->getData();
    $e->getForm()->getParent()->get('phone')->addError(/* or re-add with option */);
});

Simpler and more robust: add a form option phone_country and construct the type in a FormTypeInterface::buildForm() that reads it.

Pattern 3 — Separated array output for UI round-tripping

If the UI submits three inputs (+33, 6, 12 34 56 78) and you want Symfony to re-render them after a validation failure, map the array through the form and normalize on POST_SUBMIT:

$builder->add('phone', PhoneInputType::class, [
    'format' => 'array', // getData() => ['country' => 'FR', 'number' => '612345678']
]);

Then, in your controller/service, run the array through the bundle's normalizer once and persist the E.164 result. Do not let the array shape leak into your entity or your API response — it is a transport shape, and the key names are a bundle implementation detail you do not want in your public contract.

Pattern 4 — Value object hydration (the boundary pattern)

If the bundle ships a value object, treat it as a boundary type:

$phone = $form->get('phone')->getData();   // PhoneNumber value object
$entity->setPhone($phone?->toE164());      // entity stays string-based

Keeping the entity string-based means your migrations, API resources, and existing where phone = ? queries never change. Value objects are excellent at the form/DTO boundary and painful inside Doctrine entities without a custom DBAL type.

Pattern 5 — Validation belongs to you, not the form type

A form type normalizes; it does not decide business rules. Add your own constraint so libphonenumber-style validity (right length for that country, not just parseable) is enforced:

$builder->add('phone', PhoneInputType::class, [
    'required' => true,
    'constraints' => [
        new NotBlank(),
        new Regex('/^\+[1-9]\d{6,14}$/', message: 'Enter a valid international number.'),
    ],
]);

If you accept numbers without a country context, you cannot length-validate reliably — pick a default country or make the selector mandatory. Half-configured country handling is the #1 source of "valid number rejected" bug reports.

Pattern 6 — One normalizer service, used by forms, API, and imports

The day-to-day win is having one place that turns arbitrary input into E.164, reused by:

  • the Symfony form type,
  • a Laravel FormRequest / API request (via the same underlying library),
  • CSV/import commands,
  • factories and seeders.
final readonly class PhoneNormalizer
{
    public function normalize(?string $raw, string $defaultCountry = 'FR'): ?string
    {
        // delegate to the bundle's parser/value object
    }
}

Bind it as a singleton in Laravel ($this->app->singleton(...)) or autowire in Symfony. Every other pattern above then becomes a two-line call, and swapping the bundle out later is a one-file change.


Gotchas and Tips

The bundle/Laravel impedance mismatch is the main one. Laravel has no bundle lifecycle: PhoneInputType is a Symfony form type, so it only works where you have a FormFactory. In Livewire/Filament/Blade you will not get it for free. Budget a wrapper (custom component, or a ported local type) or you will spend the day debugging "class not found" / "no extension able to load type".

Pin the version. Zero stars, no dependents, single maintainer, and a release date that looks ahead of the current date. Treat it as pre-1.0 regardless of what the tag says:

"nowo-tech/phone-input-bundle": "0.1.*"

Wrap it behind your own PhoneInputType subclass or form-extension so an abandoned package is a 30-minute swap, not a refactor.

Array output has ordering and key-name traps. Leading zeros (national trunk prefixes) are stripped during normalization, and + handling differs depending on whether the selector was used. Never implode() the array yourself to build a number; always go through the normalizer.

getData() before vs. after PRE_SUBMIT. In PRE_SUBMIT you get the raw submitted value (string or array, potentially malformed). In POST_SUBMIT you get the transformed value. Listeners written against the wrong event are the classic "my phone is always null" bug. Same for getData() vs getViewData() in Twig — use the view data.

null vs empty string. With required => false, set 'empty_data' => null explicitly. Otherwise an untouched optional phone field can land in the database as '', which then fails your UNIQUE index the moment you add one.

The country selector defaults matter. If it falls back to IP geolocation or a hardcoded country, users abroad get subtly wrong normalization (a 06… French-looking number normalized as Italian). Set default_country from your app's locale/address data, and make the selector visible whenever the user's locale and their number's country can disagree.

Frontend assets. If the bundle ships Twig + compiled JS for the selector, assets live under src/Resources/public/. Do not patch src/Resources/public/build — override the template block or add your own initialization script instead, or your change disappears on the next composer update.

Debugging checklist when a number "doesn't work":

  1. Dump the raw submitted value in PRE_SUBMIT — is it a string, an array, or three separate fields?
  2. Dump the transformed value in POST_SUBMIT.
  3. Check the constraint order: a Regex/Length constraint on the untransformed array will fail confusingly.
  4. bin/console debug:config <bundle_alias> (Symfony) to confirm which config keys actually exist — README drift is common in young bundles.
  5. $form->getErrors(true) — nested child errors are invisible by default.

Extension point of choice: a custom data transformer. If you need a format the bundle doesn't support (e.g. storing a separate +prefix and national column pair), write a transformer around PhoneInputType rather than forking the type. It keeps the package upgradeable and keeps your entity schema yours.

Watch for future-looking release metadata. The listed last release is dated 2026-10-22, which will not match a real tag date; verify with composer show -a before assuming the API you're reading matches what Composer installs. If the installed tag differs from the README, trust the tag.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator