mpetrini/phone-number-bundle
A Symfony bundle for parsing, validating, and formatting international phone numbers using Google's libphonenumber library. Provides Twig filters, form types, and validator constraints, plus a service for phone number handling in your Symfony applications.
Install.
composer require mpetrini/phone-number-bundle
This pulls in giggsey/libphonenumber-for-php (the PHP port of Google's libphonenumber) as a transitive dependency — that package, not this bundle, does the actual parsing/formatting.
Register the bundle.
config/bundles.php:
return [
// ...
Mpetrini\PhoneNumberBundle\PhoneNumberBundle::class => ['all' => true],
];
$bundles[] = new Mpetrini\PhoneNumberBundle\PhoneNumberBundle(); in AppKernel.php.
Confirm the exact namespace in the README — this is a fork of misd/phone-number-bundle, and forks sometimes keep the original Misd\ namespace.First use case — normalize an inbound phone number. The bundle's real value is wiring libphonenumber\PhoneNumberUtil into the container, so you inject it instead of calling PhoneNumberUtil::getInstance() yourself:
use libphonenumber\PhoneNumberFormat;
use libphonenumber\PhoneNumberUtil;
final class PhoneNormalizer
{
public function __construct(private PhoneNumberUtil $util) {}
public function e164(string $raw, string $region = 'US'): string
{
$number = $this->util->parse($raw, $region);
return $this->util->format($number, PhoneNumberFormat::E164);
}
}
If autowiring works, PhoneNumberUtil is registered directly; otherwise use the classic alias libphonenumber.phone_number_util.
Where to look first.
README.md and Resources/config/*.xml for the service IDs it actually registers.bin/console debug:container libphonenumber to see what aliases exist in your install.giggsey/libphonenumber-for-php docs and its online demo for parsing/formatting behaviour — the bundle itself is a thin wrapper, so almost all real knowledge lives upstream.PhoneNumber objects. Parse on input, format to PhoneNumberFormat::E164, persist the string. It is stable, has no spaces, and sorts/indexes cleanly. Re-parse on display.PhoneNumber constraint so validation is declarative:
use Mpetrini\PhoneNumberBundle\Validator\Constraints\PhoneNumber;
class Contact
{
#[PhoneNumber(defaultRegion: 'US', type: 'mobile')]
private string $phone;
}
It accepts both a string (parsed using defaultRegion) and a libphonenumber\PhoneNumber object, and options cover type (fixed_line, mobile, etc.), defaultRegion, and message.PhoneNumberType and let the bundled data transformer handle parsing:
$builder->add('phone', PhoneNumberType::class, [
'default_region' => 'US',
'widget' => PhoneNumberType::WIDGET_SINGLE_TEXT, // or WIDGET_COUNTRY_CHOICE
]);
WIDGET_COUNTRY_CHOICE gives users a country dropdown and is a common first integration point for signup/checkout forms.{{ contact.phone|phone_number_format('INTERNATIONAL') }}
{{ contact.phone|phone_number_format('NATIONAL') }}
The bundle also exposes geocoder/carrier filters — use them sparingly (see Gotchas).PhoneNormalizer / PhoneFormatter service with methods like normalize(), display(), isMobile(). Keeps controllers thin, gives you one place to catch NumberParseException, and makes it trivial to swap the library later.(202) 555-0143, 202.555.0143, and +12025550143 must be stored identically.isValidNumber() is stricter than isPossibleNumber(). For lenient UX, validate isPossibleNumber() in the form (fast, catches obvious typos) and run isValidNumber() in a service/command for the final check.misd/phone-number-bundle repackaged. For new projects prefer misd/phone-number-bundle (more eyes, maintained-ish) or depend on giggsey/libphonenumber-for-php directly and register a single PhoneNumberUtil service yourself — that removes an abandoned layer for ~10 lines of config.parse() throws. Always expect libphonenumber\NumberParseException (INVALID_COUNTRY_CODE, NOT_A_NUMBER, TOO_SHORT_AFTER_IDD, …). Wrap calls in try/catch or route through your normalizer service.parse('202-555-0143', null) throws — the parser can't infer the country without a +. Only pass null when the input is guaranteed to start with a dial code.US, GB, DE — never dial codes like 1 or 44. A very common bug.format() will happily format garbage that parsed. Call isValidNumber() (or isPossibleNumber() for lenient checks) separately.{% if contact.phone is not null %} or register a null-safe wrapper; otherwise a null column can 500 a list page.PhoneNumberUtil::getInstance() is a singleton — inject the service, don't call getInstance() yourself, or you defeat the point of the bundle.giggsey/libphonenumber-for-php's build options for lighter metadata; also avoid eagerly constructing the geocoder/carrier mappers when you only need formatting.PhoneNumberType maps to a libphonenumber\PhoneNumber object by default. If your entity stores strings, either add the bundle's data transformer or use the validator constraint on a string property instead of the form type — mixing the two is a frequent source of "Expected PhoneNumber, got string" errors.composer update fails on constraints, either widen the constraint with a fork in your own repo or fall back to the direct-library approach. Test bundle registration on your Symfony version before committing.bin/console debug:container --show-private libphonenumber to enumerate them before guessing service IDs in YAML.libphonenumber\PhoneNumberUtil or override the form type/validator templates in templates/bundles/PhoneNumberBundle/ (if the fork published templates). The safest extension point is your own normalizer service wrapping the util.How can I help you explore Laravel packages today?