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 Number Laravel Package

devloops/phone-number

A Laravel package for validating, formatting, and normalizing international phone numbers. Parse numbers by country, convert to E.164, and integrate cleanly with Laravel validation rules for reliable phone input handling.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a thin Nova field that wraps intl-tel-input so a phone column gets a country selector, live formatting, and a predictable submitted value. It is a small, low-adoption package (8 stars, 0 dependents), so treat the source in vendor/devloops/ as part of your documentation.

Install and wire it up

composer require devloops/phone-number

Check the Nova version constraint in vendor/devloops/phone-number/composer.json before upgrading Nova — a field released in 2025-05 usually supports Nova 4/5, but verify rather than assume. If the package ships front-end assets, publish them with the tag from its README (typically php artisan vendor:publish --tag=... or nova:publish) and commit the compiled output if your deploy pipeline runs composer install --no-dev.

Add the column

Schema::table('contacts', function (Blueprint $table) {
    $table->string('phone', 20)->nullable()->after('email');
});

Store E.164 (+447911123456). Twenty characters is enough for E.164 plus a little slack, and it keeps the column useless for arithmetic — which is correct, phone numbers are strings, not integers.

First use case

Add one field to a Contact resource and see what the widget submits:

use Devloops\PhoneNumber\PhoneNumber; // confirm namespace in the README

public function fields(NovaRequest $request)
{
    return [
        ID::make()->sortable(),
        Text::make('Name')->rules('required'),
        PhoneNumber::make('Phone')
            ->defaultCountry('GB')
            ->rules('required', 'phone:GB'),
    ];
}

Then create a contact, submit a number, and dd() the raw request payload once. You need to know now whether the field posts E.164, a national string, or a JSON object with country data — every downstream decision (validation, uniqueness, index formatting) depends on it.

If the field does not post E.164 out of the box, normalise immediately (see the next section) before you have data in the wild.

Implementation Patterns

Normalise on fill, never trust the widget

The client-side widget is a UX convenience, not a data contract. Force a canonical format in fillUsing so bulk imports, API writes, and future field swaps all converge:

PhoneNumber::make('Phone')
    ->rules('required', 'phone:AUTO')
    ->fillUsing(function ($request, $model, $attribute, $requestAttribute) {
        $value = $request->input($requestAttribute);
        $model->{$attribute} = filled($value)
            ? phone($value)->formatE164()
            : null;
    });

This pairs naturally with propaganistas/laravel-phone for both the phone validation rule and the phone() helper. If you would rather not add that dependency, at minimum strip whitespace and reject anything that does not start with + after normalisation.

Show the pretty version, store the ugly one

Index and detail views should not display +447911123456:

PhoneNumber::make('Phone')
    ->displayUsing(fn ($value) => blank($value)
        ? '—'
        : phone($value)->formatInternational())
    ->textAlign('left'),

Nova's displayUsing affects the index and detail views while the form still gets the interactive widget. Test both: some field packages apply displayUsing to the form input too, which then feeds a formatted string back into the widget on edit and produces a double-formatted value.

Constrain the country list per tenant or locale

If your app is single-market, lock the selector down to avoid a support burden of numbers entered under the wrong country code:

PhoneNumber::make('Phone')
    ->onlyCountries(['GB', 'IE'])
    ->defaultCountry('GB'),

The fluent setters map onto intl-tel-input options (defaultCountry, onlyCountries, preferredCountries, separateDialCode), so anything you know from that library transfers. Where the package has no setter, ->withMeta([...]) is the usual escape hatch — check the Vue component's props first to confirm the key name.

Factor a reusable field factory

Phone fields appear on users, contacts, leads, and organisations. Wrap the config once so validation and formatting cannot drift:

// app/Nova/Fields/PhoneField.php
class PhoneField extends \Devloops\PhoneNumber\PhoneNumber
{
    public function __construct($name, $attribute = null, $resolveCallback = null)
    {
        parent::__construct($name, $attribute ?? 'phone', $resolveCallback);

        $this->defaultCountry(config('app.phone_country', 'GB'))
            ->rules('nullable', 'phone:AUTO')
            ->fillUsing(fn ($request, $model, $attribute, $requestAttribute) =>
                $model->{$attribute} = filled($request->input($requestAttribute))
                    ? phone($request->input($requestAttribute))->formatE164()
                    : null
            );
    }
}

Subclassing rather than a trait is important here: when this package goes unmaintained (few stars, minimal release cadence), you replace one parent class instead of hunting through a dozen resources.

Validation order matters for uniqueness

unique:users,phone runs against whatever is in the database. If you normalise in fillUsing, normalisation happens after validation, so +44 7911 123456 and +447911123456 both pass uniqueness and you get duplicates. Fix it with prepareForValidation on the FormRequest, or by normalising in a Nova NovaRequest macro / ->rules() closure that compares against the E.164 form:

->rules('required', 'phone:AUTO', Rule::unique('users', 'phone')->ignore($request->resourceId))

...which only works if the value is already E.164 at validation time. Add a NovaRequest prepareForValidation or a model mutator (setPhoneAttribute) so normalisation happens exactly once, as early as possible, and fillUsing becomes a redundant safety net rather than the only guard.

Search and sort

Phone columns are searchable via Nova's default searchable() only if the stored format matches what a user types. Since E.164 has no spaces, searchable() will fail for 07911 123456. Either add a computed/searchable index on a normalised column or accept that phone search is exact-match on +44.... Document this for whoever writes the support runbook.

Gotchas and Tips

  • Publish tags are the number one failure mode. If the field renders as a plain text input, the Vue component or the intl-tel-input assets did not get compiled/published. Check public/vendor/ (or resources/js/ for Nova 4+ custom tooling), re-run the publish command, and look at the browser console for a 404.
  • CSP and CDN loading. Some versions of intl-tel-input pull the flag sprite or the utils.js bundle from a CDN. If you run a strict Content-Security-Policy, self-host the assets and point the field at them, or the dropdown will render without flags and getNumber() will throw.
  • Legacy data hydrates badly. Rows written before the field existed may be stored as 07911123456, (07911) 123456, or an empty string. The widget tries to infer a country from the value; national-format numbers with no + will be guessed as the default country, which is silently wrong for imported international data. Write a one-off normalisation command and dry-run it before shipping the field.
  • '' versus null. A cleared field often submits an empty string. phone('') returns null but blank('') is true — be consistent, and add ->nullable() to the column so unique indexes and whereNotNull behave.
  • The field does not validate server-side by itself. Client-side "valid" is advisory. Always pair with the phone rule; otherwise a crafted POST bypasses everything.
  • Dark mode and Tailwind. intl-tel-input's default stylesheet assumes a light background. In Nova's dark theme the dropdown can be white-on-white. Override with a small CSS file in resources/css and register it via Nova::style() in your NovaServiceProvider; there is no theming hook in the package.
  • Debugging tip: open the edit form, run document.querySelector('[data-phone-number]') (or inspect the input's class) in the console, and check window.intlTelInputGlobals — if the instance is missing, the field's mounted() never ran, which almost always means an asset or registration problem rather than a data problem.
  • Extension point: the Vue component accepts withMeta payloads. When you need behaviour the PHP class does not expose (custom placeholder, separate dial code display, stricter formatting), push meta from PHP and read it in a locally vendored copy of the component. Copy the package's resources/js into your app rather than patching vendor/.
  • Pin the version. With 0 dependents the package has no downstream pressure to keep its API stable. "devloops/phone-number": "1.0.*" in composer.json (rather than ^1.0) plus the subclass wrapper above gives you a cheap upgrade path — and a cheap exit if the project stalls.
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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect