Skip to content

Commit 3216da7

Browse files
authored
Merge pull request #16 from chadicus/master
Add PhoneFilter
2 parents 96acaf9 + d87faea commit 3216da7

File tree

2 files changed

+305
-0
lines changed

2 files changed

+305
-0
lines changed

src/Filter/PhoneFilter.php

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<?php
2+
3+
4+
namespace TraderInteractive\Filter;
5+
6+
use Throwable;
7+
use TraderInteractive\Exceptions\FilterException;
8+
9+
final class PhoneFilter
10+
{
11+
12+
/**
13+
* The pattern for the separations between numbers.
14+
*
15+
* @var string
16+
*/
17+
const SEPARATOR_PATTERN = ' *[-.]? *';
18+
19+
/**
20+
* The pattern for the area code.
21+
*
22+
* @var string
23+
*/
24+
const AREA_CODE_PATTERN = '(?:\(([2–9]\d\d)\)|([2-9]\d\d))?';
25+
26+
/**
27+
* The pattern for the exchange code. Also known as the central office code.
28+
*
29+
* @var string
30+
*/
31+
const EXCHANGE_CODE_PATTERN = '([2-9]\d\d)';
32+
33+
/**
34+
* The pattern for the station code. Also known as the line number or subscriber number.
35+
*
36+
* @var string
37+
*/
38+
const STATION_CODE_PATTERN = '(\d{4})';
39+
40+
/**
41+
* The pattern for phone numbers according to the North American Numbering Plan specification.
42+
*
43+
* @var string
44+
*/
45+
const PHONE_PATTERN = (''
46+
. '/^ *'
47+
. self::AREA_CODE_PATTERN
48+
. self::SEPARATOR_PATTERN
49+
. self::EXCHANGE_CODE_PATTERN
50+
. self::SEPARATOR_PATTERN
51+
. self::STATION_CODE_PATTERN
52+
. ' *$/'
53+
);
54+
55+
/**
56+
* @var string
57+
*/
58+
const ERROR_INVALID_PHONE_NUMBER = "Value '%s' is not a valid phone number.";
59+
60+
/**
61+
* @var string
62+
*/
63+
const ERROR_VALUE_CANNOT_BE_NULL = 'Value cannot be null';
64+
65+
/**
66+
* @var string
67+
*/
68+
const DEFAULT_FILTERED_PHONE_FORMAT = '{area}{exchange}{station}';
69+
70+
/**
71+
* @var bool
72+
*/
73+
private $allowNull;
74+
75+
/**
76+
* @var string
77+
*/
78+
private $filteredPhoneFormat;
79+
80+
/**
81+
* @param bool $allowNull Flag to allow value to be null
82+
* @param string $filteredPhoneFormat The format for which the filtered phone value will be returned.
83+
*/
84+
public function __construct(
85+
bool $allowNull = false,
86+
string $filteredPhoneFormat = self::DEFAULT_FILTERED_PHONE_FORMAT
87+
) {
88+
$this->allowNull = $allowNull;
89+
$this->filteredPhoneFormat = $filteredPhoneFormat;
90+
}
91+
92+
/**
93+
* @param mixed $value The value to filter.
94+
*
95+
* @return string|null
96+
*
97+
* @throws FilterException Thrown when the value does not pass filtering.
98+
*/
99+
public function __invoke($value)
100+
{
101+
return self::filter($value, $this->allowNull, $this->filteredPhoneFormat);
102+
}
103+
104+
/**
105+
* @param mixed $value The value to filter.
106+
* @param bool $allowNull Flag to allow value to be null
107+
* @param string $filteredPhoneFormat The format for which the filtered phone value will be returned.
108+
*
109+
* @return string|null
110+
*
111+
* @throws FilterException Thrown when the value does not pass filtering.
112+
*/
113+
public static function filter(
114+
$value,
115+
bool $allowNull = false,
116+
string $filteredPhoneFormat = self::DEFAULT_FILTERED_PHONE_FORMAT
117+
) {
118+
if ($value === null) {
119+
return self::returnNullValue($allowNull);
120+
}
121+
122+
$value = self::getValueAsString($value);
123+
$matches = [];
124+
if (!preg_match(self::PHONE_PATTERN, $value, $matches)) {
125+
$message = sprintf(self::ERROR_INVALID_PHONE_NUMBER, $value);
126+
throw new FilterException($message);
127+
}
128+
129+
list($phone, $areaWithParenthesis, $area, $exchange, $station) = $matches;
130+
if ($areaWithParenthesis !== '') {
131+
$area = $areaWithParenthesis;
132+
}
133+
134+
$search = ['{area}', '{exchange}', '{station}'];
135+
$replace = [$area, $exchange, $station];
136+
return str_replace($search, $replace, $filteredPhoneFormat);
137+
}
138+
139+
private static function returnNullValue(bool $allowNull)
140+
{
141+
if ($allowNull === false) {
142+
throw new FilterException(self::ERROR_VALUE_CANNOT_BE_NULL);
143+
}
144+
145+
return null;
146+
}
147+
148+
private static function getValueAsString($value) : string
149+
{
150+
if (is_scalar($value)) {
151+
return (string)$value;
152+
}
153+
154+
$message = sprintf(self::ERROR_INVALID_PHONE_NUMBER, var_export($value, true));
155+
throw new FilterException($message);
156+
}
157+
}

tests/Filter/PhoneFilterTest.php

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<?php
2+
3+
namespace TraderInteractive\Filter;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use TraderInteractive\Exceptions\FilterException;
7+
8+
/**
9+
* @coversDefaultClass \TraderInteractive\Filter\PhoneFilter
10+
* @covers ::__construct
11+
* @covers ::<private>
12+
*/
13+
final class PhoneFilterTest extends TestCase
14+
{
15+
/**
16+
* @param mixed $value The value to be filtered.
17+
* @param bool $allowNull The allowNull value for the filter.
18+
* @param string $format The format of the filtered phone.
19+
* @param string|null $expectedValue The expected filtered value.
20+
*
21+
* @test
22+
* @covers ::filter
23+
* @dataProvider provideFilterData
24+
*/
25+
public function filter($value, bool $allowNull, string $format, $expectedValue)
26+
{
27+
$actualValue = PhoneFilter::filter($value, $allowNull, $format);
28+
$this->assertSame($expectedValue, $actualValue);
29+
}
30+
31+
/**
32+
* @param mixed $value The value to be filtered.
33+
* @param bool $allowNull The allowNull value for the filter.
34+
* @param string $format The format of the filtered phone.
35+
* @param string|null $expectedValue The expected filtered value.
36+
*
37+
* @test
38+
* @covers ::__construct
39+
* @covers ::__invoke
40+
* @dataProvider provideFilterData
41+
*/
42+
public function invoke($value, bool $allowNull, string $format, $expectedValue)
43+
{
44+
$filter = new PhoneFilter($allowNull, $format);
45+
$actualValue = $filter($value);
46+
$this->assertSame($expectedValue, $actualValue);
47+
}
48+
49+
/**
50+
* @return array
51+
*/
52+
public function provideFilterData() : array
53+
{
54+
return [
55+
['2345678901', false, PhoneFilter::DEFAULT_FILTERED_PHONE_FORMAT, '2345678901'],
56+
['234 5678901', false, '({area}) {exchange}-{station}', '(234) 567-8901'],
57+
['234 567-8901', false, PhoneFilter::DEFAULT_FILTERED_PHONE_FORMAT, '2345678901'],
58+
['234 567.8901', false, '({area}) {exchange}-{station}', '(234) 567-8901'],
59+
['234 567 8901', false, PhoneFilter::DEFAULT_FILTERED_PHONE_FORMAT, '2345678901'],
60+
['234.5678901', false, '({area}) {exchange}-{station}', '(234) 567-8901'],
61+
['234.567-8901', false, PhoneFilter::DEFAULT_FILTERED_PHONE_FORMAT, '2345678901'],
62+
['234.567.8901', false, '({area}) {exchange}-{station}', '(234) 567-8901'],
63+
['234.567 8901', false, PhoneFilter::DEFAULT_FILTERED_PHONE_FORMAT, '2345678901'],
64+
['234-5678901', false, '({area}) {exchange}-{station}', '(234) 567-8901'],
65+
['234-567-8901', false, PhoneFilter::DEFAULT_FILTERED_PHONE_FORMAT, '2345678901'],
66+
['234-567.8901', false, '{area}-{exchange}-{station}', '234-567-8901'],
67+
['234-567 8901', false, PhoneFilter::DEFAULT_FILTERED_PHONE_FORMAT, '2345678901'],
68+
['234 - 567 - 8901', false, '({area}) {exchange}-{station}', '(234) 567-8901'],
69+
['234 . 567 . 8901', false, PhoneFilter::DEFAULT_FILTERED_PHONE_FORMAT, '2345678901'],
70+
['234 567 8901', false, '{area}.{exchange}.{station}', '234.567.8901'],
71+
['(234)-567-8901', false, PhoneFilter::DEFAULT_FILTERED_PHONE_FORMAT, '2345678901'],
72+
['(234).567.8901', false, '({area}) {exchange}-{station}', '(234) 567-8901'],
73+
['(234) 567 8901', false, PhoneFilter::DEFAULT_FILTERED_PHONE_FORMAT, '2345678901'],
74+
[' 234-567-8901 ', false, '{exchange}-{station}', '567-8901'],
75+
[null, true, PhoneFilter::DEFAULT_FILTERED_PHONE_FORMAT, null],
76+
];
77+
}
78+
79+
/**
80+
* @test
81+
* @covers ::filter
82+
*/
83+
public function filterWithAllowNull()
84+
{
85+
$value = null;
86+
$result = PhoneFilter::filter(null, true);
87+
88+
$this->assertSame($value, $result);
89+
}
90+
91+
/**
92+
* @param mixed $value The value to filter.
93+
*
94+
* @test
95+
* @covers ::__invoke
96+
* @dataProvider provideFilterThrowsException
97+
*/
98+
public function filterThrowsException($value)
99+
{
100+
$this->expectException(FilterException::class);
101+
$this->expectExceptionMessage(sprintf(PhoneFilter::ERROR_INVALID_PHONE_NUMBER, $value));
102+
103+
PhoneFilter::filter($value);
104+
}
105+
106+
/**
107+
* @return array
108+
*/
109+
public function provideFilterThrowsException() : array
110+
{
111+
return [
112+
'empty string' => [''],
113+
'not all digits' => ['234-567a'],
114+
'not enough digits' => ['234567'],
115+
'too many digits' => ['23456789012'],
116+
'invalid exchange code' => ['123-4567'],
117+
'invalid area code' => ['123-234-5678'],
118+
'invalid separator' => ['234:567:8901'],
119+
'no opening parenthesis' => ['234) 567 8901'],
120+
'no closing parenthesis' => ['(234 567 8901'],
121+
];
122+
}
123+
124+
/**
125+
* @test
126+
* @covers ::filter
127+
*/
128+
public function filterThrowsExceptionOnNonStringValues()
129+
{
130+
$value = ['foo' => 'bar'];
131+
$this->expectException(FilterException::class);
132+
$this->expectExceptionMessage(sprintf(PhoneFilter::ERROR_INVALID_PHONE_NUMBER, var_export($value, true)));
133+
134+
PhoneFilter::filter($value);
135+
}
136+
137+
/**
138+
* @test
139+
* @covers ::filter
140+
*/
141+
public function filterThrowsExceptionOnNull()
142+
{
143+
$this->expectException(FilterException::class);
144+
$this->expectExceptionMessage(PhoneFilter::ERROR_VALUE_CANNOT_BE_NULL);
145+
146+
PhoneFilter::filter(null);
147+
}
148+
}

0 commit comments

Comments
 (0)