Skip to content

Commit

Permalink
Add phonenumber formatting (#18)
Browse files Browse the repository at this point in the history
* Add phonenumber formatting, as wrapper around the giggsey/libphonenumber-for-php-lite package
  • Loading branch information
bram123 authored May 10, 2023
1 parent 5b61d57 commit 6f149be
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 6 deletions.
31 changes: 25 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,34 @@ $formatter->format(DayOfTheWeekFormatter::MONDAY, 'en_US');
// output: Monday
```

### PhoneNumberFormatService
Format phoneNumbers
```php
use DR\Internationalization\PhoneNumber\PhoneNumberFormatOptions;
use DR\Internationalization\PhoneNumberFormatService;

// set default configuration
$phoneNumberOptions = (new PhoneNumberFormatOptions())
->setDefaultCountryCode('NL')
->setFormat(PhoneNumberFormatOptions::FORMAT_INTERNATIONAL_DIAL);
$service = new PhoneNumberFormatService($phoneNumberOptions);

$service->format("+31612345678");
// output: 0031612345678

$service->format("0612345678");
// output: 0031612345678
```

## Project structure

| Directory | Description |
|-----------|-------------------------------------------------------------------------------------------------------|
| Currency | Format `int`, `float` or `Money` value to locale specific format. Use `NumberFormatService::currency` |
| Date | Format ISO-8601 day of the week to user friendly names |
| Money | Create `Money` object from `float` |
| Number | Format `int` or `float` value to locale specific format. Use `NumberFormatService::number` |
| Directory | Description |
|-------------|-------------------------------------------------------------------------------------------------------|
| Currency | Format `int`, `float` or `Money` value to locale specific format. Use `NumberFormatService::currency` |
| Date | Format ISO-8601 day of the week to user friendly names |
| Money | Create `Money` object from `float` |
| Number | Format `int` or `float` value to locale specific format. Use `NumberFormatService::number` |
| PhoneNumber | Format phoneNumber value to specified format. Use `PhoneNumberFormatService::format` |

## Development

Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"require": {
"php": ">=8.0",
"ext-intl": "*",
"giggsey/libphonenumber-for-php-lite": "^8.13.11",
"moneyphp/money": "^3.3 || ^4.0"
},
"autoload": {
Expand Down
68 changes: 68 additions & 0 deletions src/PhoneNumber/PhoneNumberFormatOptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);


namespace DR\Internationalization\PhoneNumber;

use libphonenumber\PhoneNumberFormat;

/**
* @phpstan-type Format self::FORMAT_*
*/
class PhoneNumberFormatOptions
{
/**
* Formats the NL phoneNumber "101234567" as "+31101234567"
*/
public const FORMAT_E164 = PhoneNumberFormat::E164;

/**
* Formats the NL phoneNumber "101234567" as "+31 10 123 4567"
*/
public const FORMAT_INTERNATIONAL = PhoneNumberFormat::INTERNATIONAL;

/**
* Formats the NL phoneNumber "101234567" as "010 123 4567"
*/
public const FORMAT_NATIONAL = PhoneNumberFormat::NATIONAL;

/**
* Formats the NL phoneNumber "101234567" as "tel:+31-10-123-4567"
*/
public const FORMAT_RFC3966 = PhoneNumberFormat::RFC3966;

/**
* Formats the NL phoneNumber "101234567" as "0031101234567"
*/
public const FORMAT_INTERNATIONAL_DIAL = 4;

private ?string $defaultCountryCode = null;
private ?int $format = null;

public function getDefaultCountryCode(): ?string
{
return $this->defaultCountryCode;
}

public function setDefaultCountryCode(?string $defaultCountryCode): self
{
$this->defaultCountryCode = $defaultCountryCode;

return $this;
}

public function getFormat(): ?int
{
return $this->format;
}

/**
* @phpstan-param Format $format
*/
public function setFormat(?int $format): self
{
$this->format = $format;

return $this;
}
}
49 changes: 49 additions & 0 deletions src/PhoneNumberFormatService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);

namespace DR\Internationalization;

use DR\Internationalization\PhoneNumber\PhoneNumberFormatOptions;
use InvalidArgumentException;
use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumberUtil;

class PhoneNumberFormatService
{
private PhoneNumberFormatOptions $defaultOptions;
private ?PhoneNumberUtil $phoneNumberUtil = null;

public function __construct(PhoneNumberFormatOptions $phoneNumberOptions)
{
$this->defaultOptions = $phoneNumberOptions;
}

public function format(string $phoneNumber, PhoneNumberFormatOptions $options = null): string
{
$countryCode = $options?->getDefaultCountryCode() ?? $this->defaultOptions->getDefaultCountryCode();
$format = $options?->getFormat() ?? $this->defaultOptions->getFormat();
if ($format === null) {
throw new InvalidArgumentException('PhoneNumberOptions: unable to format phoneNumber without a given format');
}

$this->phoneNumberUtil ??= PhoneNumberUtil::getInstance();

try {
$parsedNumber = $this->phoneNumberUtil->parse($phoneNumber, $countryCode);
} catch (NumberParseException $e) {
throw new InvalidArgumentException("Unable to parse phoneNumber: " . $phoneNumber, 0, $e);
}

if ($format === PhoneNumberFormatOptions::FORMAT_INTERNATIONAL_DIAL) {
$metaData = $this->phoneNumberUtil->getMetadataForRegion((string)$countryCode);
$prefix = $metaData?->getInternationalPrefix() ?? $metaData?->getPreferredInternationalPrefix();
if (is_numeric($prefix)) {
return $prefix . ltrim($this->phoneNumberUtil->format($parsedNumber, PhoneNumberFormatOptions::FORMAT_E164), '+');
}

return $this->phoneNumberUtil->format($parsedNumber, PhoneNumberFormatOptions::FORMAT_E164);
}

return $this->phoneNumberUtil->format($parsedNumber, $format);
}
}
32 changes: 32 additions & 0 deletions tests/Unit/PhoneNumber/PhoneNumberFormatOptionsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);

namespace DR\Internationalization\Tests\Unit\PhoneNumber;

use DigitalRevolution\AccessorPairConstraint\AccessorPairAsserter;
use DigitalRevolution\AccessorPairConstraint\Constraint\ConstraintConfig;
use DR\Internationalization\PhoneNumber\PhoneNumberFormatOptions;
use PHPUnit\Framework\TestCase;

/**
* @coversDefaultClass \DR\Internationalization\PhoneNumber\PhoneNumberFormatOptions
*/
class PhoneNumberFormatOptionsTest extends TestCase
{
use AccessorPairAsserter;

/**
* @covers ::getDefaultCountryCode
* @covers ::setDefaultCountryCode
* @covers ::getFormat
* @covers ::setFormat
*/
public function testAccessors(): void
{
$config = new ConstraintConfig();
$config->setAssertPropertyDefaults(true);
$config->setAssertConstructor(true);
$config->setAssertAccessorPair(true);
static::assertAccessorPairs(PhoneNumberFormatOptions::class, $config);
}
}
126 changes: 126 additions & 0 deletions tests/Unit/PhoneNumberFormatServiceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);

namespace DR\Internationalization\Tests\Unit;

use DR\Internationalization\PhoneNumber\PhoneNumberFormatOptions;
use DR\Internationalization\PhoneNumberFormatService;
use Generator;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;

/**
* @coversDefaultClass \DR\Internationalization\PhoneNumberFormatService
* @covers ::__construct
*/
class PhoneNumberFormatServiceTest extends TestCase
{
/**
* @covers ::format
*/
public function testFormatMissingOption(): void
{
$formatter = new PhoneNumberFormatService((new PhoneNumberFormatOptions())->setDefaultCountryCode("NL"));

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('PhoneNumberOptions: unable to format phoneNumber without a given format');
$formatter->format('0612345678');
}

/**
* @covers ::format
*/
public function testFormatInvalidInput(): void
{
$options = (new PhoneNumberFormatOptions())->setDefaultCountryCode("__")->setFormat(PhoneNumberFormatOptions::FORMAT_NATIONAL);
$formatter = new PhoneNumberFormatService($options);

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage("Unable to parse phoneNumber: xxx");
$formatter->format('xxx');
}

/**
* @dataProvider optionFormatProvider
* @covers ::format
*/
public function testFormat(int $format, string $phoneNumber, string $expectedValue): void
{
$formatter = new PhoneNumberFormatService((new PhoneNumberFormatOptions())->setDefaultCountryCode("NL")->setFormat($format));
static::assertSame($expectedValue, $formatter->format($phoneNumber));
}

/**
* @dataProvider internationalDialProvider
* @covers ::format
*/
public function testFormatInternationDial(string $countryCode, string $phoneNumber, string $expectedValue): void
{
$options = (new PhoneNumberFormatOptions())
->setDefaultCountryCode($countryCode)
->setFormat(PhoneNumberFormatOptions::FORMAT_INTERNATIONAL_DIAL);

$formatter = new PhoneNumberFormatService($options);
static::assertSame($expectedValue, $formatter->format($phoneNumber));
}

/**
* @covers ::format
*/
public function testFormatDefaultFormat(): void
{
$defaultOptions = (new PhoneNumberFormatOptions())->setDefaultCountryCode('NL')->setFormat(PhoneNumberFormatOptions::FORMAT_NATIONAL);
$formatter = new PhoneNumberFormatService($defaultOptions);

static::assertSame('010 123 4567', $formatter->format("101234567"));
static::assertSame('06 12345678', $formatter->format("0612345678"));
}

/**
* @covers ::format
*/
public function testFormatOverwrittenCountryCode(): void
{
$defaultOptions = (new PhoneNumberFormatOptions())->setDefaultCountryCode('NL')->setFormat(PhoneNumberFormatOptions::FORMAT_NATIONAL);
$formatOptions = (new PhoneNumberFormatOptions())->setDefaultCountryCode('GB');
$formatter = new PhoneNumberFormatService($defaultOptions);

static::assertSame('0121 234 5678', $formatter->format("1212345678", $formatOptions));
static::assertSame('07400 123456', $formatter->format("7400123456", $formatOptions));
}

public function optionFormatProvider(): Generator
{
yield [PhoneNumberFormatOptions::FORMAT_E164, "101234567", "+31101234567"];
yield [PhoneNumberFormatOptions::FORMAT_E164, "0612345678", "+31612345678"];

yield [PhoneNumberFormatOptions::FORMAT_INTERNATIONAL, "101234567", "+31 10 123 4567"];
yield [PhoneNumberFormatOptions::FORMAT_INTERNATIONAL, "0612345678", "+31 6 12345678"];

yield [PhoneNumberFormatOptions::FORMAT_NATIONAL, "101234567", "010 123 4567"];
yield [PhoneNumberFormatOptions::FORMAT_NATIONAL, "0612345678", "06 12345678"];

yield [PhoneNumberFormatOptions::FORMAT_RFC3966, "101234567", "tel:+31-10-123-4567"];
yield [PhoneNumberFormatOptions::FORMAT_RFC3966, "0612345678", "tel:+31-6-12345678"];
}

public function internationalDialProvider(): Generator
{
yield ['NL', '612345678', '0031612345678'];
yield ['NL', '0612345678', '0031612345678'];
yield ['NL', '+31612345678', '0031612345678'];
yield ['NL', '0031612345678', '0031612345678'];

yield ['BE', '412345678', '0032412345678'];
yield ['BE', '+32412345678', '0032412345678'];
yield ['BE', '0032412345678', '0032412345678'];

yield ['US', '+31612345678', '01131612345678'];
yield ['US', '+32412345678', '01132412345678'];
yield ['US', '5062345678', '01115062345678'];
yield ['US', '+15062345678', '01115062345678'];

// BR internationalPrefix is'00(?:1[245]|2[1-35]|31|4[13]|[56]5|99)', and has no preferredInternationalPrefix
yield ['BR', '+1 201-555-0123', '+12015550123'];
}
}

0 comments on commit 6f149be

Please sign in to comment.