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

Nova Phone Field Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. The 2.0.0 changelog's breaking-change section (storage format).
  2. Your existing phone/mobile columns — values written under 1.x are formatted, not E.164.
  3. Any code that reads the phone attribute (SMS senders, search, exports, API resources) — it now receives a different string.

Implementation Patterns

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

  1. Branch, composer require yieldstudio/nova-phone-field:^2.0.
  2. Dump the phone column to a scratch table (or CSV) before touching anything.
  3. Run the backfill in a transaction-per-chunk job, not a single UPDATE.
  4. Spot-check 20 records across each country you serve.
  5. Deploy Nova assets as usual; verify index/detail formatting renders.

Gotchas and Tips

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.

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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource