national = $national; } /** * Parse any shape of Iranian mobile number. * * @param string $raw Number as the customer typed it. * * @return self|null Null when the input is not a valid Iranian mobile. */ public static function parse( string $raw ): ?self { $digits = self::toDigits( $raw ); if ( '' === $digits ) { return null; } // Strip whichever international prefix was used, then any national // trunk zero, so all six common shapes converge on the same nine digits. if ( str_starts_with( $digits, '00' . self::COUNTRY_CODE ) ) { $digits = substr( $digits, 4 ); } elseif ( str_starts_with( $digits, self::COUNTRY_CODE ) && strlen( $digits ) > 10 ) { $digits = substr( $digits, 2 ); } if ( str_starts_with( $digits, '0' ) ) { $digits = substr( $digits, 1 ); } if ( ! preg_match( self::NATIONAL_PATTERN, $digits ) ) { return null; } return new self( $digits ); } /** * Is this a valid Iranian mobile number? * * @param string $raw Number as the customer typed it. */ public static function isValid( string $raw ): bool { return null !== self::parse( $raw ); } /** * Canonical storage form: `989123456789`. * * This is what goes in the database and what lookups compare against. No * leading `+`, because providers differ on whether they accept one and a bare * digit string survives every round trip. */ public function canonical(): string { return self::COUNTRY_CODE . $this->national; } /** * E.164 form: `+989123456789`. */ public function e164(): string { return '+' . $this->canonical(); } /** * National form: `09123456789`. * * What Iranians actually recognise as their own number, so this is the form * shown back to a customer. */ public function national(): string { return '0' . $this->national; } /** * National form without the trunk zero: `9123456789`. * * Some provider APIs want exactly this. */ public function withoutTrunk(): string { return $this->national; } /** * Partly hidden form for display in logs and confirmation screens. * * `0912***6789` — enough for the owner to recognise, not enough to be useful * to anyone reading a support ticket over their shoulder. */ public function masked(): string { return substr( $this->national(), 0, 4 ) . '***' . substr( $this->national(), -4 ); } /** * Reduce any input to ASCII digits. * * Persian and Arabic-Indic digits are folded first — a customer typing on a * Persian keyboard produces `۰۹۱۲…`, which is the same number and must not be * rejected as malformed. * * @param string $raw Raw input. */ private static function toDigits( string $raw ): string { $normalised = Normaliser::digits( trim( $raw ) ); return (string) preg_replace( '/\D+/', '', $normalised ); } }