bissolli/nova-phone-field
A Laravel Nova phone field that renders an international telephone input with country flag selector, automatic formatting, and validation. Stores numbers in E.164 and drops into Nova resources with minimal setup.
Adoption is a single Composer line — no service provider publishing, no migration, no config file to seed:
composer require bissolli/nova-phone-field
Then drop it into any Nova resource where a Text field would otherwise live:
use Bissolli\NovaPhoneField\PhoneNumber;
public function fields(Request $request): array
{
return [
ID::make()->sortable(),
PhoneNumber::make('Phone')
->rules(['nullable', 'string', 'max:32'])
->placeholder('+55 (11) 99999-9999'),
];
}
Before you type the first mask, verify the constraint. The most recent tag, v0.0.4 (2019-09-08), is a one-line Laravel 6.0 compatibility bump. If composer require fails to resolve against your framework, treat the resolution error as the answer — do not force it through with --ignore-platform-reqs and assume the compiled Vue asset will behave.
Where to read first, in order:
vendor/bissolli/nova-phone-field/src/PhoneNumber.php — you are looking at a thin subclass of Laravel\Nova\Fields\Text. Every public setter in that class is your configuration API. Read it once before guessing at method names.vendor/bissolli/nova-phone-field/resources/js/ — the Vue component and the hardcoded country code → mask map. This is where the dynamic masking lives and where your debugging will end up.package.json in the same directory — reveals which mask library is bundled (inputmask, jquery-mask-plugin, or similar). That library is what you will actually be debugging.composer.json — the laravel/framework and nova constraints, before you promise anything to a planning meeting.First use case: a phone string column on User, Lead, or Customer. Put the field on the resource, type +55, +1, or +44 as a prefix, and watch the input reformat as the prefix changes. That is the entire value proposition — one line for an international, masked admin input.
First day workflow: add the field to one resource, save a record with three different country prefixes, then immediately inspect the raw column value with php artisan tinker or a DB client. What you see there (spaces, parentheses, a leading +) is what you are actually storing. Until you know that answer for your stack, do not add the field to a second resource.
Treat the field as a UI hint, not a data layer. The mask is cosmetic and client-side. Whatever the user sees in the input — parentheses, dashes, spaces — is what arrives in the request. Splitting normalization on write from validation on read is the single most important pattern with this package.
Normalize to E.164 on save via fillUsing() or a model mutator:
PhoneNumber::make('Phone')
->rules(['nullable', 'string', 'max:32'])
->fillUsing(function ($request, $model, $attribute) {
$model->{$attribute} = $request->{$attribute}
? '+' . preg_replace('/\D/', '', $request->{$attribute})
: null;
});
You now have a canonical value for search, API output, deduplication, and sorting, while the Nova form still displays human-friendly formatting. Do the same normalization in any API/import path — do not rely on the field being present.
Validate separately from the mask. Pair with propaganistas/laravel-phone or giggsey/libphonenumber-for-php in ->rules() or a FormRequest. A value that looks correctly masked can still have the wrong digit count for its region; the field will silently accept it.
Layer ordinary Text methods. Because it extends Text, the usual suspects pass through unchanged: ->nullable(), ->placeholder(), ->help(), ->default(), ->onlyOnForms(), ->readonly(), ->rules(). Use ->default('+55 ') on create forms to make the prefix-first convention obvious and sidestep the awkward partial-prefix state.
Wrap it once if it appears in several resources:
protected function phoneField(string $attribute = 'phone'): PhoneNumber
{
return PhoneNumber::make($attribute)
->rules(['nullable', 'string', 'max:32'])
->placeholder('+55 (11) 99999-9999')
->resolveUsing(fn ($value) => $value); // swap point: normalizer later
}
That helper becomes the one place you patch when you fork, replace, or upgrade the field.
On index views, display the normalized value. Masked strings sort and filter badly against an indexed column. If you want ->sortable(), ensure the column holds clean E.164 — not the masked display string — so ordering is meaningful.
Assert on normalized values in tests. Nova's AssertableJson and Dusk flows work as usual, but assert against the stored canonical number, not the masked string. Anything else will break the moment the bundled mask patterns change — or the moment you fork them.
Plan the fork as a first-class option, not a last resort. Adding a country region, restyling the input, or supporting Nova 4/5 (Vue 3) realistically means copying the component into your app and owning it. Budget that from the start rather than discovering it mid-sprint.
The framework ceiling is the headline risk. v0.0.4 adds Laravel 6 support and nothing else — no Nova 4/5 (Vue 3) work, no new mask regions, no config file, no tests, no dependency refresh. The compiled JS is from the Nova 2/3 era. Assume Nova 2.x-era APIs; anything newer is unproven until you spike it in a browser on your actual stack.
Composer may refuse to install outright. A resolution error on composer require is your first signal to stop and reconsider — not your first signal to pass --ignore-platform-reqs. The runtime was never tested against your framework version, so forcing the install buys you a field that may mount but never mask.
Silent JS failure is the classic symptom. If the input renders as a plain text box with no formatting, the component mounted but the mask library did not bind. Watch the browser console and the Network tab for the field's JS bundle. Then, if you have touched any assets, run php artisan nova:publish, and rebuild with npm run dev inside Nova.
Partial prefix states are awkward. The mask re-evaluates as the prefix changes, so typing + or +5 alone produces a temporarily nonsense pattern. It is cosmetic but confusing for users who type the local number first — nudge them with ->placeholder() or ->default().
The country-code list is hardcoded in the package's JS. There is no config file to extend. Check resources/js/ for the map before you promise coverage of a region in a spec — otherwise "supports international numbers" quietly becomes "supports the countries someone thought of in 2019."
Never trust the field for integrity. Users paste arbitrary text, disable JavaScript, or submit via API. Normalization and validation belong server-side, always, in every write path.
Debugging checklist, in order: confirm package discovery ran (composer dump-autoload); verify your Nova version against the compiled assets; open the browser console for a mask-library error; check the Network tab that the field's JS actually loaded; then inspect resources/js/ to confirm the country code you are testing is even in the map.
Extension points are thin. No published config. Customization happens through public setters on PhoneNumber (inspect the class), by overriding component() to point at your own Vue component, or by replacing the field outright. Anything beyond light styling is a fork — keep the wrapper helper in place so that fork touches one method.
Know the escape hatch. With 31 stars, zero dependents, and a release history consisting of a single compatibility bump, treat this as a convenience, not a dependency. A ~60-line custom Nova field wrapping intl-tel-input, or a plain Text field plus a FormRequest rule backed by propaganistas/laravel-phone, is easier to maintain and will not pin you to the Laravel 6 era. If you adopt it, assign an owner and assume responsibility for patching compatibility yourself.
How can I help you explore Laravel packages today?