moonlydays / laravel-mno
Utilities for configuring interactions with operator phone numbers and subscribers.
Requires
- php: ^8.2
- giggsey/libphonenumber-for-php: ^9.0.27
- giggsey/locale: ^2.9
- illuminate/contracts: ^11.0||^12.0||^13.0
- spatie/laravel-package-tools: ^1.93
Requires (Dev)
- larastan/larastan: ^3.9.4
- laravel/pint: ^1.29
- nunomaduro/collision: ^8.9.3
- orchestra/testbench: ^10.11.0||^9.0.0
- pestphp/pest: ^3.8.6
- pestphp/pest-plugin-laravel: ^3.2
- rector/rector: ^2.4.1
README
Laravel package for validating, normalizing, and working with MSISDN phone numbers tied to a single Mobile Network
Operator. A wrapper around giggsey/libphonenumber-for-php with
integration into Laravel's validation system, Eloquent casts, Faker, schema builder, and facades.
Released under the MIT License.
Requirements
- PHP 8.2+
- Laravel 11, 12, or 13
Installation
composer require moonlydays/laravel-mno
Publish the configuration file:
php artisan vendor:publish --tag="mno-config"
Configuration
Set up the environment variables:
MNO_NAME=MTS MNO_COUNTRY=RU MNO_NETWORK_CODES=910,911,912 MNO_MIN_LENGTH=10 MNO_MAX_LENGTH=10
| Variable | Description |
|---|---|
MNO_NAME |
Name of the mobile network operator |
MNO_COUNTRY |
ISO 3166-1 alpha-2 country code (e.g., RU, TZ) |
MNO_NETWORK_CODES |
Comma-separated National Destination Code (NDC) prefixes for the operator |
MNO_MIN_LENGTH |
Minimum national number length (optional — inferred from libphonenumber metadata) |
MNO_MAX_LENGTH |
Maximum national number length (optional — inferred from libphonenumber metadata) |
When MNO_MIN_LENGTH / MNO_MAX_LENGTH are unset, the package infers the length from libphonenumber
metadata for the configured country, walking the number_types priority list in config/mno.php
(defaults to Mobile, then General).
Usage
Creating a Msisdn
use MoonlyDays\MNO\Values\Msisdn; // Parse, throwing InvalidMsisdnException on failure $phone = Msisdn::from('+79101234567'); $phone = Msisdn::from('9101234567', 'RU'); $phone = Msisdn::from(79101234567, 'RU'); // integers are accepted // Safe parse, returning null on failure $phone = Msisdn::tryFrom('invalid'); // null // Global helper $phone = msisdn('+79101234567');
Msisdn is a lightweight value object wrapping libphonenumber's native PhoneNumber. It implements
Stringable (casting to string produces the E.164 form), JsonSerializable (serializes as E.164), and
Castable (can be used directly as an Eloquent cast). It also uses Laravel's Macroable and Tappable traits.
Formatting
$phone = Msisdn::from('+79101234567'); $phone->e164(); // "+79101234567" $phone->national(); // "8 (910) 123-45-67" $phone->international(); // "+7 910 123-45-67" $phone->toInteger(); // 79101234567 (E.164 digits without the leading plus) (string) $phone; // "+79101234567"
Retrieving number components
$phone = Msisdn::from('+79101234567'); $phone->countryCode(); // 7 $phone->countryIso(); // "RU" $phone->nationalNumber(); // "9101234567" $phone->networkCode(); // "910" $phone->subscriberNumber(); // "1234567" $phone->toPhoneNumber(); // underlying libphonenumber\PhoneNumber
Two Msisdn instances can be compared via $a->equals($b) (equality is based on the E.164 form).
Timezones
$phone = Msisdn::from('+79101234567'); $phone->timezone(); // "Europe/Moscow" — primary IANA identifier, or null if unknown $phone->timezones(); // ["Europe/Moscow", ...] — all IANA identifiers for the number
Validation
use Illuminate\Validation\Rule; // Use the Rule::msisdn() macro — picks up defaults from config $request->validate([ 'phone' => ['required', Rule::msisdn()], ]);
use MoonlyDays\MNO\Rules\MsisdnRule; // Customize the rule fluently $request->validate([ 'phone' => [ 'required', (new MsisdnRule()) ->country('RU', 'BY', 'KZ') ->networkCodes('910', '911') ->minLength(10) ->maxLength(10), ], ]);
Validation failures translate the following keys, which you can publish or override in your own language files:
validation.msisdn.invalidvalidation.msisdn.countryvalidation.msisdn.min_length(receives:min)validation.msisdn.max_length(receives:max)validation.msisdn.network_code
Overriding the default rule
MsisdnRule::defaults() lets you swap in a custom resolver used by Rule::msisdn():
use MoonlyDays\MNO\Rules\MsisdnRule; MsisdnRule::defaults(fn () => (new MsisdnRule()) ->country('RU') ->minLength(10) ->maxLength(10));
Request macro
The service provider registers a msisdn macro on Illuminate\Http\Request:
$phone = $request->msisdn('phone'); // Msisdn or null $phone = $request->msisdn('phone', $default); // with fallback (value or closure)
Eloquent cast
Since Msisdn implements Castable, you can use it directly as an Eloquent cast. MsisdnCast is
also available if you prefer to be explicit:
use Illuminate\Database\Eloquent\Model; use MoonlyDays\MNO\Values\Msisdn; class User extends Model { protected $casts = [ 'phone' => Msisdn::class, // or MsisdnCast::class ]; } $user->phone = '+79101234567'; $user->save(); // Stored as unsigned bigInteger: 79101234567 $user->phone instanceof Msisdn; // true $user->phone->national(); // "8 (910) 123-45-67"
The cast accepts a string, integer, or Msisdn instance when setting, and persists the E.164 digits
as an unsigned integer (the leading + is stripped). When reading back, the configured MNO_COUNTRY is
used as the default region for parsing, so make sure it is set.
Back the cast with an unsigned bigInteger column (e.g., $table->unsignedBigInteger('phone')). A
VARCHAR column will break writes.
Faker provider
When the Faker generator resolves from the container, the package registers a provider for generating valid numbers within the configured MNO (country, network codes, and length constraints):
$faker = fake(); $faker->msisdn(); // Msisdn value object $faker->msisdn()->e164(); // "+79101234567" $faker->msisdn()->national(); // "8 (910) 123-45-67"
JSON resource
MsisdnFormatResource exposes the operator's format metadata as a JSON resource, for API responses
that need to tell clients about expected number shape:
use MoonlyDays\MNO\Resources\MsisdnFormatResource; return [ 'format' => MsisdnFormatResource::make(), ]; // { // "countryCode": 7, // "country": "RU", // "minLength": 10, // "maxLength": 10, // "networkCodes": ["910", "911", "912"] // }
MNO facade
use MoonlyDays\MNO\Facades\MNO; MNO::countryIsoCode(); // "RU" MNO::country(); // Country instance for "RU" MNO::countryCode(); // 7 MNO::carrierName(); // "MTS" MNO::carrier(); // Carrier instance for the configured MNO MNO::networkCodes(); // ["910", "911", "912"] MNO::minLength(); // 10 MNO::maxLength(); // 10 MNO::exampleNumber(); // Msisdn|null MNO::numberTypes(); // array<NumberType>
The facade resolves the MnoService singleton, which is also bound to the container alias mno and can be
injected directly.
Country and Carrier value objects
use MoonlyDays\MNO\Values\Country; use MoonlyDays\MNO\Values\Carrier; // Country — wraps an ISO 3166-1 alpha-2 code $country = Country::from('RU'); // throws InvalidCountryException on unknown code $country = Country::tryFrom('RU'); // returns null on failure $country->isoCode(); // "RU" $country->countryCode(); // 7 $country->name(); // "Russia" $country->exampleNumber(); // Msisdn|null $country->isMobileNumberPortable(); // bool $country->carriers(); // array<string, Carrier> — all carriers with allocations // Carrier — a carrier within a country $carrier = Carrier::from('RU', 'MTS'); // throws InvalidCarrierException on miss $carrier = Carrier::tryFrom('RU', 'MTS'); // returns null on failure $carrier->name(); // "MTS" $carrier->country(); // Country instance $carrier->networkCodes(); // ["910", "911", "912"] — NDC prefixes $carrier->prefixes(); // ["7910", "7911", "7912"] — with country code $carrier->matches($phone); // true if the phone number belongs to this carrier $carrier->owns('910'); // true if the carrier owns this NDC
Artisan command
Inspect the configured MNO, a country, or a specific carrier:
php artisan mno:show # show configured operator details php artisan mno:show RU # show country info with carrier list php artisan mno:show RU MTS # show carrier details with network codes
Extending via macros
Msisdn uses the Macroable trait, so you can add project-specific helpers:
use MoonlyDays\MNO\Values\Msisdn; Msisdn::macro('isRussian', function (): bool { /** @var Msisdn $this */ return $this->countryIso() === 'RU'; }); Msisdn::from('+79101234567')->isRussian(); // true