yieldstudio/nova-phone-field
A Laravel Nova field for phone numbers, built on the intl-tel-input library. It provides an interactive country selector, automatic formatting, and validation so you can store phone numbers in a consistent international format within your Nova resources.
nova-phone-field is a Laravel Nova form field that wraps an international phone input (country selector + intl-tel-input-style masking). In 2.0.0 the package also became a storage contract: it writes E.164 (+33612345678) instead of the formatted string it used to save.
Install and register the field in a resource:
composer require yieldstudio/nova-phone-field
use YieldStudio\NovaPhoneField\PhoneNumber;
public function fields(NovaRequest $request): array
{
return [
PhoneNumber::make('Phone'),
];
}
First use case: add it to UserResource for a phone column and immediately verify what actually lands in the database — User::first()->phone should now be +33612345678, not 06 12 34 56 78. That single check tells you whether you are on 1.x behaviour or 2.x behaviour and whether your existing rows need backfilling.
Where to look first, in order:
phone/mobile columns — values written under 1.x are formatted, not E.164.Basic field with display niceties
PhoneNumber::make('Phone')
->copyable() // new in 2.0.0 — copy button on index/detail
->help('Include your country code.'), // new in 2.0.0 — helper text under the input
copyable() and the helper text are pure UX additions; they don't change what is persisted, so they can be adopted on existing fields without a migration.
Treat E.164 as the canonical model value
The whole point of the breaking change is that the stored value is directly usable:
// app/Models/User.php
protected $casts = [
'phone' => 'string', // keep raw E.164, do not mutate
];
Twilio::messages()->create($user->phone, $from, $body); // no reformatting step
Resist the urge to add an accessor that re-formats the attribute — that reintroduces exactly the ambiguity 2.0.0 removed. Format at the presentation layer instead (index/detail already do this via libphonenumber-js, and Nova renders it for free).
Paired server-side validation
The masked input is not validation. Keep a rule on the same attribute so a bad number can't be persisted:
use Propaganistas\LaravelPhone\Rules\Phone;
PhoneNumber::make('Phone')
->rules(['required', (new Phone)->international()]),
This is the pattern that matters day-to-day: field for UX, rule for correctness, E.164 for storage.
Backfill pattern for the 1.x → 2.x upgrade
The changelog suggests:
UPDATE users SET phone = CONCAT('+', REGEXP_REPLACE(phone, '\\D', ''));
That is fast and fine for well-formed data already carrying a country code. For anything else (local-format numbers, 00 prefixes, mixed columns), prefer a parse-and-format pass:
use Propaganistas\LaravelPhone\PhoneNumber as Phone;
User::query()->whereNotNull('phone')->chunkById(500, function ($users) {
foreach ($users as $user) {
try {
$user->forceFill([
'phone' => Phone::make($user->phone, 'FR')->formatE164(),
])->saveQuietly();
} catch (\Throwable $e) {
report($e); // keep a list of unparseable rows to fix manually
}
}
});
Do the SQL version only when you have a database that supports it (MySQL 8+, Postgres with regexp_replace(x, '\D', '', 'g'); SQLite has no REGEXP_REPLACE at all) and you've confirmed every row already has a country code.
Rollout workflow
composer require yieldstudio/nova-phone-field:^2.0.phone column to a scratch table (or CSV) before touching anything.UPDATE.The CONCAT('+', ...) recipe can silently corrupt data. A local number stored as 06 12 34 56 78 becomes +0612345678 — syntactically +-prefixed, semantically garbage, and it will fail downstream SMS APIs with a confusing error. Same for 0033... → +0033.... Only run the raw SQL if you are certain every pre-2.0 value was stored with an international prefix. When in doubt, use the PHP backfill above and log failures.
Column width. E.164 is at most 15 digits plus the +. If you sized the column tightly around formatted values, widen it to string(16) (or more) before deploying, otherwise you'll get truncation errors on save in production rather than on the developer's machine.
Existing data is not migrated by the package. Upgrading the composer constraint changes write behaviour only; every historical row keeps its old format. Any read path — reports, exports, where('phone', ...) lookups, duplicate detection — will see two different formats until you backfill. Do the backfill and the deploy in the same window.
Index/detail formatting is JS-side. The pretty display relies on the bundled libphonenumber-js. If a row shows a raw +33... on index/detail while the form looks fine, suspect stale/uncached Nova assets or a custom tool overriding the field component, not the data. Clear compiled Nova assets and reproduce in a private window before debugging the PHP side.
Anything comparing phone strings breaks. Login-by-phone, where('phone', $request->phone), unique validation rules, and import dedupe all now compare against E.164. Add a normalisation step at the boundary (Phone::make($input, $country)->formatE164()) rather than sprinkling str_replace calls.
Validation must supply a country. Server-side parsers can't infer a country from 06 12 34 56 78. Whatever default country the field uses in the browser has to be mirrored in the validation rule and in the backfill script; three places, one source of truth — put it in a config constant.
copyable() is a UX trap on index. A copy button on a table of users makes it trivial to copy the wrong row's number. It's genuinely useful on a detail page or a single-record dashboard; be deliberate about where you enable it.
Don't reformat on read. If you need pretty output outside Nova (PDFs, emails, API responses), format explicitly at that call site. An accessor that returns +33 6 12 34 56 78 will quietly break any consumer expecting E.164 — and it's the exact failure mode 2.0.0 was released to fix.
Pint migration is internal. The PHPCS → Laravel Pint swap only affects the package's own CI; unless you were submitting PRs upstream, ignore it. Same for the inputmask 5 upgrade: it's a real fix for copy/paste and cursor-position bugs in the input, but it's not something you configure. If you had workarounds in place for the old mask behaviour, re-test them — they may now be unnecessary or actively harmful.
How can I help you explore Laravel packages today?